REFACTOR: Separate ListItem into separate content ListItem components

This commit is contained in:
Chris-1010
2025-02-27 01:21:18 +00:00
parent 74baa49c04
commit eaf6e3d693
4 changed files with 297 additions and 205 deletions

View File

@@ -1,19 +1,18 @@
import React from "react";
import { StreamType } from "../../types/StreamType";
import { CategoryType } from "../../types/CategoryType";
import { UserType } from "../../types/UserType";
export interface ListItemProps {
type: "stream" | "category" | "user";
id: number;
title: string;
username?: string;
streamCategory?: string;
viewers: number;
thumbnail?: string;
// Base props that all item types share
interface BaseListItemProps {
onItemClick?: () => void;
extraClasses?: string;
}
const ListItem: React.FC<ListItemProps> = ({
type,
// Stream item component
interface StreamListItemProps extends BaseListItemProps, Omit<StreamType, 'type'> {}
const StreamListItem: React.FC<StreamListItemProps> = ({
title,
username,
streamCategory,
@@ -22,31 +21,6 @@ const ListItem: React.FC<ListItemProps> = ({
onItemClick,
extraClasses = "",
}) => {
if (type === "user") {
return (
<div className="p-4 pb-10">
<div
className={`group relative w-fit flex flex-col bg-purple-900 rounded-tl-xl rounded-xl min-h-[calc((20vw+20vh)/3)] max-w-[calc((27vw+27vh)/2)] justify-end items-center cursor-pointer mx-auto hover:bg-purple-600 hover:scale-105 z-50 transition-all`}
onClick={onItemClick}
>
<img
src="/images/monkey.png"
alt={`user ${username}`}
className="rounded-xl border-[0.15em] border-[var(--bg-color)] cursor-pointer"
/>
<button className="text-[calc((2vw+2vh)/2)] font-bold hover:underline w-full py-2">
{title}
</button>
{title.includes("🔴") && (
<p className="absolute font-black bottom-5 opacity-0 group-hover:translate-y-full group-hover:opacity-100 group-hover:-bottom-1 transition-all">
Currently Live!
</p>
)}
</div>
</div>
);
}
return (
<div className="p-4">
<div
@@ -68,10 +42,8 @@ const ListItem: React.FC<ListItemProps> = ({
<h3 className="font-semibold text-lg text-center truncate max-w-full">
{title}
</h3>
{type === "stream" && <p className="font-bold">{username}</p>}
{type === "stream" && (
<p className="text-sm text-gray-300">{streamCategory}</p>
)}
<p className="font-bold">{username}</p>
<p className="text-sm text-gray-300">{streamCategory}</p>
<p className="text-sm text-gray-300">{viewers} viewers</p>
</div>
</div>
@@ -79,4 +51,91 @@ const ListItem: React.FC<ListItemProps> = ({
);
};
export default ListItem;
// Category item component
interface CategoryListItemProps extends BaseListItemProps, Omit<CategoryType, 'type'> {}
const CategoryListItem: React.FC<CategoryListItemProps> = ({
title,
viewers,
thumbnail,
onItemClick,
extraClasses = "",
}) => {
return (
<div className="p-4">
<div
className={`${extraClasses} overflow-hidden flex-shrink-0 flex flex-col bg-purple-900 rounded-lg cursor-pointer mx-auto hover:bg-purple-600 hover:scale-105 transition-all`}
onClick={onItemClick}
>
<div className="relative w-full aspect-video overflow-hidden rounded-t-lg">
{thumbnail ? (
<img
src={thumbnail}
alt={title}
className="absolute top-0 left-0 w-full h-full object-cover"
/>
) : (
<div className="absolute top-0 left-0 w-full h-full bg-gray-600" />
)}
</div>
<div className="p-3">
<h3 className="font-semibold text-lg text-center truncate max-w-full">
{title}
</h3>
<p className="text-sm text-gray-300">{viewers} viewers</p>
</div>
</div>
</div>
);
};
// User item component
interface UserListItemProps extends BaseListItemProps, Omit<UserType, 'type'> {}
const UserListItem: React.FC<UserListItemProps> = ({
title,
username,
isLive,
onItemClick,
extraClasses = "",
}) => {
return (
<div className="p-4 pb-10">
<div
className={`group relative w-fit flex flex-col bg-purple-900 rounded-tl-xl rounded-xl min-h-[calc((20vw+20vh)/3)] max-w-[calc((27vw+27vh)/2)] justify-end items-center cursor-pointer mx-auto hover:bg-purple-600 hover:scale-105 z-50 transition-all ${extraClasses}`}
onClick={onItemClick}
>
<img
src="/images/monkey.png"
alt={`user ${username}`}
className="rounded-xl border-[0.15em] border-[var(--bg-color)] cursor-pointer"
/>
<button className="text-[calc((2vw+2vh)/2)] font-bold hover:underline w-full py-2">
{title}
</button>
{isLive && (
<p className="absolute font-black bottom-5 opacity-0 group-hover:translate-y-full group-hover:opacity-100 group-hover:-bottom-1 transition-all">
Currently Live!
</p>
)}
</div>
</div>
);
};
// Legacy wrapper component for backward compatibility
export interface ListItemProps {
type: "stream" | "category" | "user";
id: number;
title: string;
username?: string;
streamCategory?: string;
viewers: number;
thumbnail?: string;
onItemClick?: () => void;
extraClasses?: string;
isLive?: boolean;
}
export { StreamListItem, CategoryListItem, UserListItem };

View File

@@ -10,14 +10,19 @@ import React, {
} from "react";
import { useNavigate } from "react-router-dom";
import "../../assets/styles/listRow.css";
import ListItem, { ListItemProps } from "./ListItem";
import { StreamListItem, CategoryListItem, UserListItem } from "./ListItem";
import { StreamType } from "../../types/StreamType";
import { CategoryType } from "../../types/CategoryType";
import { UserType } from "../../types/UserType";
type ItemType = StreamType | CategoryType | UserType;
interface ListRowProps {
variant?: "default" | "search";
type: "stream" | "category" | "user";
title?: string;
description?: string;
items: ListItemProps[];
items: ItemType[];
wrap?: boolean;
onItemClick: (itemName: string) => void;
titleClickable?: boolean;
@@ -27,151 +32,179 @@ interface ListRowProps {
children?: React.ReactNode;
}
const ListRow = forwardRef<
{ addMoreItems: (newItems: ListItemProps[]) => void },
ListRowProps
>(
(
{
variant = "default",
type,
title = "",
description = "",
items,
onItemClick,
titleClickable = false,
wrap = false,
extraClasses = "",
itemExtraClasses = "",
amountForScroll = 4,
children,
},
ref
) => {
const [currentItems, setCurrentItems] = useState(items);
const slider = useRef<HTMLDivElement>(null);
const scrollAmount = window.innerWidth * 0.3;
const navigate = useNavigate();
interface ListRowRef {
addMoreItems: (newItems: ItemType[]) => void;
}
const addMoreItems = (newItems: ListItemProps[]) => {
setCurrentItems((prevItems) => [...prevItems, ...newItems]);
};
const ListRow = forwardRef<ListRowRef, ListRowProps>((props, ref) => {
const {
variant = "default",
type,
title = "",
description = "",
items,
onItemClick,
titleClickable = false,
wrap = false,
extraClasses = "",
itemExtraClasses = "",
amountForScroll = 4,
children,
} = props;
const [currentItems, setCurrentItems] = useState<ItemType[]>(items);
const slider = useRef<HTMLDivElement>(null);
const scrollAmount = window.innerWidth * 0.3;
const navigate = useNavigate();
useImperativeHandle(ref, () => ({
addMoreItems,
}));
const addMoreItems = (newItems: ItemType[]) => {
setCurrentItems((prevItems) => [...prevItems, ...newItems]);
};
const slideRight = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: +scrollAmount, behavior: "smooth" });
}
};
useImperativeHandle(ref, () => ({
addMoreItems,
}));
const slideLeft = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
}
};
const slideRight = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: +scrollAmount, behavior: "smooth" });
}
};
const handleTitleClick = (type: string) => {
switch (type) {
case "stream":
break;
case "category":
navigate("/categories");
break;
case "user":
break;
default:
break;
}
};
const slideLeft = () => {
if (!wrap && slider.current) {
slider.current.scrollBy({ left: -scrollAmount, behavior: "smooth" });
}
};
return (
const handleTitleClick = () => {
switch (type) {
case "stream":
break;
case "category":
navigate("/categories");
break;
case "user":
break;
default:
break;
}
};
const isStreamType = (item: ItemType): item is StreamType =>
item.type === "stream";
const isCategoryType = (item: ItemType): item is CategoryType =>
item.type === "category";
const isUserType = (item: ItemType): item is UserType =>
item.type === "user";
return (
<div
className={`${extraClasses} flex relative rounded-[1.5rem] text-white transition-all ${
variant === "search"
? "items-center"
: "flex-col space-y-4 py-6 px-5 mx-2 mt-5"
}`}
>
{/* List Details */}
<div
className={`${extraClasses} flex relative rounded-[1.5rem] text-white transition-all ${
variant === "search"
? "items-center"
: "flex-col space-y-4 py-6 px-5 mx-2 mt-5"
className={`text-center ${
variant === "search" ? "min-w-fit px-auto w-[15vw]" : ""
}`}
>
{/* List Details */}
<div
className={`text-center ${
variant === "search" ? "min-w-fit px-auto w-[15vw]" : ""
}`}
<h2
className={`${
titleClickable
? "cursor-pointer hover:underline"
: "cursor-default"
} text-2xl font-bold`}
onClick={titleClickable ? handleTitleClick : undefined}
>
<h2
className={`${
titleClickable
? "cursor-pointer hover:underline"
: "cursor-default"
} text-2xl font-bold`}
onClick={titleClickable ? () => handleTitleClick(type) : undefined}
>
{title}
</h2>
<p>{description}</p>
</div>
{title}
</h2>
<p>{description}</p>
</div>
{/* List Items */}
<div className="relative overflow-hidden flex flex-grow items-center z-0">
{!wrap && currentItems.length > amountForScroll && (
{/* List Items */}
<div className="relative overflow-hidden flex flex-grow items-center z-0">
{!wrap && currentItems.length > amountForScroll && (
<>
<ArrowLeftIcon
onClick={slideLeft}
size={30}
className="absolute left-0 cursor-pointer z-[999]"
/>
<ArrowRightIcon
onClick={slideRight}
size={30}
className="absolute right-0 cursor-pointer z-[999]"
/>
</>
)}
<div
ref={slider}
className={`flex ${
wrap ? "flex-wrap justify-between" : "overflow-x-scroll whitespace-nowrap"
} max-w-[95%] items-center w-full mx-auto scroll-smooth scrollbar-hide`}
>
{currentItems.length === 0 ? (
<h1 className="mx-auto">Nothing Found</h1>
) : (
<>
<ArrowLeftIcon
onClick={slideLeft}
size={30}
className="absolute left-0 cursor-pointer z-[999]"
/>
<ArrowRightIcon
onClick={slideRight}
size={30}
className="absolute right-0 cursor-pointer z-[999]"
/>
{currentItems.map((item) => {
if (type === "stream" && isStreamType(item)) {
return (
<StreamListItem
key={`stream-${item.id}`}
id={item.id}
title={item.title}
username={item.username}
streamCategory={item.streamCategory}
viewers={item.viewers}
thumbnail={item.thumbnail}
onItemClick={() => onItemClick(item.username)}
extraClasses={itemExtraClasses}
/>
);
} else if (type === "category" && isCategoryType(item)) {
return (
<CategoryListItem
key={`category-${item.id}`}
id={item.id}
title={item.title}
viewers={item.viewers}
thumbnail={item.thumbnail}
onItemClick={() => onItemClick(item.title)}
extraClasses={itemExtraClasses}
/>
);
} else if (type === "user" && isUserType(item)) {
return (
<UserListItem
key={`user-${item.id}`}
id={item.id}
title={item.title}
username={item.username}
isLive={item.isLive}
viewers={item.viewers}
thumbnail={item.thumbnail}
onItemClick={() => onItemClick(item.username)}
extraClasses={itemExtraClasses}
/>
);
}
return null;
})}
</>
)}
<div
ref={slider}
className={`flex ${
wrap ? "flex-wrap justify-between" : "overflow-x-scroll whitespace-nowrap"
} max-w-[95%] items-center w-full mx-auto scroll-smooth scrollbar-hide`}
>
{currentItems.length === 0 ? (
<h1 className="mx-auto">Nothing Found</h1>
) : (
<>
{currentItems.map((item) => (
<ListItem
key={`${item.type}-${item.id}`}
id={item.id}
type={type}
title={item.title}
username={
item.type === "category" ? undefined : item.username
}
streamCategory={
item.type === "stream" ? item.streamCategory : undefined
}
viewers={item.viewers}
thumbnail={item.thumbnail}
onItemClick={() =>
(item.type === "stream" || item.type === "user") &&
item.username
? onItemClick?.(item.username)
: onItemClick?.(item.title)
}
extraClasses={`${itemExtraClasses}`}
/>
))}
</>
)}
</div>
</div>
{children}
</div>
);
}
);
{children}
</div>
);
});
export default ListRow;
export default ListRow;

View File

@@ -2,11 +2,13 @@ import React, { useState, useEffect } from "react";
import DynamicPageContent from "../components/Layout/DynamicPageContent";
import Button from "../components/Input/Button";
import Input from "../components/Input/Input";
import ListItem from "../components/Layout/ListItem";
import { useCategories } from "../hooks/useContent";
import { X as XIcon, Eye as ShowIcon, EyeOff as HideIcon } from "lucide-react";
import { useAuth } from "../context/AuthContext";
import { debounce } from "lodash";
import VideoPlayer from "../components/Stream/VideoPlayer";
import { CategoryType } from "../types/CategoryType";
import { StreamListItem } from "../components/Layout/ListItem";
interface StreamData {
title: string;
@@ -16,11 +18,6 @@ interface StreamData {
stream_key: string;
}
interface Category {
category_id: number;
category_name: string;
}
const StreamDashboardPage: React.FC = () => {
const { username } = useAuth();
const [isStreaming, setIsStreaming] = useState(false);
@@ -33,9 +30,8 @@ const StreamDashboardPage: React.FC = () => {
});
const [streamDetected, setStreamDetected] = useState(false);
const [timeStarted, setTimeStarted] = useState("");
const [categories, setCategories] = useState<Category[]>([]);
const [isCategoryFocused, setIsCategoryFocused] = useState(false);
const [filteredCategories, setFilteredCategories] = useState<Category[]>([]);
const [filteredCategories, setFilteredCategories] = useState<CategoryType[]>([]);
const [thumbnail, setThumbnail] = useState<File | null>(null);
const [thumbnailPreview, setThumbnailPreview] = useState<{
url: string;
@@ -44,10 +40,23 @@ const StreamDashboardPage: React.FC = () => {
const [debouncedCheck, setDebouncedCheck] = useState<Function | null>(null);
const [showKey, setShowKey] = useState(false);
const {
categories,
isLoading: categoriesLoading,
error: categoriesError
} = useCategories("/api/categories/popular/100");
useEffect(() => {
// Set filtered categories when categories load
if (categories.length > 0) {
setFilteredCategories(categories);
}
}, [categories]);
useEffect(() => {
const categoryCheck = debounce((categoryName: string) => {
const isValidCategory = categories.some(
(cat) => cat.category_name.toLowerCase() === categoryName.toLowerCase()
(cat) => cat.title.toLowerCase() === categoryName.toLowerCase()
);
if (isValidCategory && !thumbnailPreview.isCustom) {
@@ -66,11 +75,14 @@ const StreamDashboardPage: React.FC = () => {
}, [categories, thumbnailPreview.isCustom]);
useEffect(() => {
checkStreamStatus();
fetchCategories();
if (username) {
checkStreamStatus();
}
}, [username]);
const checkStreamStatus = async () => {
if (!username) return;
try {
const response = await fetch(`/api/user/${username}/status`);
const data = await response.json();
@@ -119,24 +131,13 @@ const StreamDashboardPage: React.FC = () => {
}
};
const fetchCategories = async () => {
try {
const response = await fetch("/api/categories/popular/100");
const data = await response.json();
setCategories(data);
setFilteredCategories(data);
} catch (error) {
console.error("Error fetching categories:", error);
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setStreamData((prev) => ({ ...prev, [name]: value }));
if (name === "category_name") {
const filtered = categories.filter((cat) =>
cat.category_name.toLowerCase().includes(value.toLowerCase())
cat.title.toLowerCase().includes(value.toLowerCase())
);
setFilteredCategories(filtered);
if (debouncedCheck) {
@@ -193,7 +194,7 @@ const StreamDashboardPage: React.FC = () => {
streamData.category_name.trim() !== "" &&
categories.some(
(cat) =>
cat.category_name.toLowerCase() ===
cat.title.toLowerCase() ===
streamData.category_name.toLowerCase()
) &&
streamDetected
@@ -325,13 +326,13 @@ const StreamDashboardPage: React.FC = () => {
<div className="absolute z-10 w-full bg-gray-700 mt-1 rounded-md shadow-lg max-h-48 overflow-y-auto">
{filteredCategories.map((category) => (
<div
key={category.category_id}
key={category.title}
className="px-4 py-2 hover:bg-gray-600 cursor-pointer text-white"
onClick={() =>
handleCategorySelect(category.category_name)
handleCategorySelect(category.title)
}
>
{category.category_name}
{category.title}
</div>
))}
</div>
@@ -457,8 +458,7 @@ const StreamDashboardPage: React.FC = () => {
</div>
<div className="flex flex-col">
<p className="text-white text-center">List Item</p>
<ListItem
type="stream"
<StreamListItem
id={1}
title={streamData.title || "Stream Title"}
username={username || ""}

View File

@@ -3,12 +3,12 @@ import AuthModal from "../components/Auth/AuthModal";
import { useAuthModal } from "../hooks/useAuthModal";
import { useAuth } from "../context/AuthContext";
import { useParams } from "react-router-dom";
import ListItem from "../components/Layout/ListItem";
import { useFollow } from "../hooks/useFollow";
import { useNavigate } from "react-router-dom";
import Button, { EditButton } from "../components/Input/Button";
import DynamicPageContent from "../components/Layout/DynamicPageContent";
import LoadingScreen from "../components/Layout/LoadingScreen";
import { StreamListItem } from "../components/Layout/ListItem";
interface UserProfileData {
id: number;
@@ -218,10 +218,10 @@ const UserPage: React.FC = () => {
<h2 className="text-2xl bg-[#ff0000] border py-4 px-12 font-black mb-4 rounded-[4rem]">
Currently Live!
</h2>
<ListItem
<StreamListItem
id={profileData.id}
type="stream"
title={profileData.currentStreamTitle || ""}
streamCategory=""
username=""
viewers={profileData.currentStreamViewers || 0}
thumbnail={profileData.currentStreamThumbnail}