From eaf6e3d693ead9c681157ee73aaa429ad5a367db Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Thu, 27 Feb 2025 01:21:18 +0000 Subject: [PATCH] REFACTOR: Separate `ListItem` into separate content ListItem components --- frontend/src/components/Layout/ListItem.tsx | 139 ++++++--- frontend/src/components/Layout/ListRow.tsx | 299 +++++++++++--------- frontend/src/pages/StreamDashboardPage.tsx | 58 ++-- frontend/src/pages/UserPage.tsx | 6 +- 4 files changed, 297 insertions(+), 205 deletions(-) diff --git a/frontend/src/components/Layout/ListItem.tsx b/frontend/src/components/Layout/ListItem.tsx index 4fe6af0..a5a7cf8 100644 --- a/frontend/src/components/Layout/ListItem.tsx +++ b/frontend/src/components/Layout/ListItem.tsx @@ -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 = ({ - type, +// Stream item component +interface StreamListItemProps extends BaseListItemProps, Omit {} + +const StreamListItem: React.FC = ({ title, username, streamCategory, @@ -22,31 +21,6 @@ const ListItem: React.FC = ({ onItemClick, extraClasses = "", }) => { - if (type === "user") { - return ( -
-
- {`user - - - {title.includes("🔴") && ( -

- Currently Live! -

- )} -
-
- ); - } return (
= ({

{title}

- {type === "stream" &&

{username}

} - {type === "stream" && ( -

{streamCategory}

- )} +

{username}

+

{streamCategory}

{viewers} viewers

@@ -79,4 +51,91 @@ const ListItem: React.FC = ({ ); }; -export default ListItem; +// Category item component +interface CategoryListItemProps extends BaseListItemProps, Omit {} + +const CategoryListItem: React.FC = ({ + title, + viewers, + thumbnail, + onItemClick, + extraClasses = "", +}) => { + return ( +
+
+
+ {thumbnail ? ( + {title} + ) : ( +
+ )} +
+
+

+ {title} +

+

{viewers} viewers

+
+
+
+ ); +}; + +// User item component +interface UserListItemProps extends BaseListItemProps, Omit {} + +const UserListItem: React.FC = ({ + title, + username, + isLive, + onItemClick, + extraClasses = "", +}) => { + return ( +
+
+ {`user + + + {isLive && ( +

+ Currently Live! +

+ )} +
+
+ ); +}; + +// 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 }; \ No newline at end of file diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index 5f98440..0ae9e81 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -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(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((props, ref) => { + const { + variant = "default", + type, + title = "", + description = "", + items, + onItemClick, + titleClickable = false, + wrap = false, + extraClasses = "", + itemExtraClasses = "", + amountForScroll = 4, + children, + } = props; + + const [currentItems, setCurrentItems] = useState(items); + const slider = useRef(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 ( +
+ {/* List Details */}
- {/* List Details */} -
-

handleTitleClick(type) : undefined} - > - {title} -

-

{description}

-
+ {title} + +

{description}

+
- {/* List Items */} -
- {!wrap && currentItems.length > amountForScroll && ( + {/* List Items */} +
+ {!wrap && currentItems.length > amountForScroll && ( + <> + + + + )} + +
+ {currentItems.length === 0 ? ( +

Nothing Found

+ ) : ( <> - - + {currentItems.map((item) => { + if (type === "stream" && isStreamType(item)) { + return ( + onItemClick(item.username)} + extraClasses={itemExtraClasses} + /> + ); + } else if (type === "category" && isCategoryType(item)) { + return ( + onItemClick(item.title)} + extraClasses={itemExtraClasses} + /> + ); + } else if (type === "user" && isUserType(item)) { + return ( + onItemClick(item.username)} + extraClasses={itemExtraClasses} + /> + ); + } + return null; + })} )} - -
- {currentItems.length === 0 ? ( -

Nothing Found

- ) : ( - <> - {currentItems.map((item) => ( - - (item.type === "stream" || item.type === "user") && - item.username - ? onItemClick?.(item.username) - : onItemClick?.(item.title) - } - extraClasses={`${itemExtraClasses}`} - /> - ))} - - )} -
- {children}
- ); - } -); + {children} +
+ ); +}); -export default ListRow; +export default ListRow; \ No newline at end of file diff --git a/frontend/src/pages/StreamDashboardPage.tsx b/frontend/src/pages/StreamDashboardPage.tsx index c8e7284..e2929b8 100644 --- a/frontend/src/pages/StreamDashboardPage.tsx +++ b/frontend/src/pages/StreamDashboardPage.tsx @@ -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([]); const [isCategoryFocused, setIsCategoryFocused] = useState(false); - const [filteredCategories, setFilteredCategories] = useState([]); + const [filteredCategories, setFilteredCategories] = useState([]); const [thumbnail, setThumbnail] = useState(null); const [thumbnailPreview, setThumbnailPreview] = useState<{ url: string; @@ -44,10 +40,23 @@ const StreamDashboardPage: React.FC = () => { const [debouncedCheck, setDebouncedCheck] = useState(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) => { 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 = () => {
{filteredCategories.map((category) => (
- handleCategorySelect(category.category_name) + handleCategorySelect(category.title) } > - {category.category_name} + {category.title}
))}
@@ -457,8 +458,7 @@ const StreamDashboardPage: React.FC = () => {

List Item

- {

Currently Live!

-