From 74baa49c04917ef8a0fcac988b373dce5474a228 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Thu, 27 Feb 2025 01:20:40 +0000 Subject: [PATCH] UPDATE/REFACTOR: Replace `ContentContext` with `useContent` hook; REFACTOR: Add content type files; --- frontend/src/App.tsx | 3 - frontend/src/context/ContentContext.tsx | 145 ------------------------ frontend/src/hooks/useContent.ts | 140 +++++++++++++++++++++++ frontend/src/pages/CategoryPage.tsx | 6 +- frontend/src/pages/Following.tsx | 1 + frontend/src/pages/HomePage.tsx | 14 +-- frontend/src/types/CategoryType.ts | 8 ++ frontend/src/types/StreamType.ts | 9 ++ frontend/src/types/UserType.ts | 10 ++ 9 files changed, 178 insertions(+), 158 deletions(-) delete mode 100644 frontend/src/context/ContentContext.tsx create mode 100644 frontend/src/hooks/useContent.ts create mode 100644 frontend/src/types/CategoryType.ts create mode 100644 frontend/src/types/StreamType.ts create mode 100644 frontend/src/types/UserType.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b4631d7..0017039 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; import { AuthContext } from "./context/AuthContext"; -import { ContentProvider } from "./context/ContentContext"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import HomePage from "./pages/HomePage"; import StreamerRoute from "./components/Stream/StreamerRoute"; @@ -57,7 +56,6 @@ function App() { setUserId, }} > - @@ -105,7 +103,6 @@ function App() { - ); diff --git a/frontend/src/context/ContentContext.tsx b/frontend/src/context/ContentContext.tsx deleted file mode 100644 index 442245f..0000000 --- a/frontend/src/context/ContentContext.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { createContext, useContext, useState, useEffect } from "react"; -import { useAuth } from "./AuthContext"; - -// Base interfaces -interface Item { - id: number; - title: string; - viewers: number; - thumbnail?: string; -} - -interface StreamItem extends Item { - type: "stream"; - username: string; - streamCategory: string; -} - -interface CategoryItem extends Item { - type: "category"; -} - -interface UserItem extends Item { - type: "user"; - username: string; - isLive: boolean; -} - -// Context type -interface ContentContextType { - streams: StreamItem[]; - categories: CategoryItem[]; - users: UserItem[]; - setStreams: (streams: StreamItem[]) => void; - setCategories: (categories: CategoryItem[]) => void; - setUsers: (users: UserItem[]) => void; -} - -const ContentContext = createContext(undefined); - -export function ContentProvider({ children }: { children: React.ReactNode }) { - const [streams, setStreams] = useState([]); - const [categories, setCategories] = useState([]); - const [users, setUsers] = useState([]); - const { isLoggedIn } = useAuth(); - - useEffect(() => { - // Fetch streams - const streamsUrl = isLoggedIn - ? "/api/streams/recommended" - : "/api/streams/popular/4"; - - fetch(streamsUrl) - .then((response) => response.json()) - .then((data: any[]) => { - const processedStreams: StreamItem[] = data.map((stream) => ({ - type: "stream", - id: stream.user_id, - title: stream.title, - username: stream.username, - streamCategory: stream.category_name, - viewers: stream.num_viewers, - thumbnail: - stream.thumbnail || - `/images/category_thumbnails/${stream.category_name - .toLowerCase() - .replace(/ /g, "_")}.webp`, - })); - setStreams(processedStreams); - }); - - // Fetch categories - const categoriesUrl = isLoggedIn - ? "/api/categories/recommended" - : "/api/categories/popular/4"; - console.log("Fetching categories from", categoriesUrl); - - fetch(categoriesUrl) - .then((response) => response.json()) - .then((data: any[]) => { - const processedCategories: CategoryItem[] = data.map((category) => ({ - type: "category", - id: category.category_id, - title: category.category_name, - viewers: category.num_viewers, - thumbnail: `/images/category_thumbnails/${category.category_name - .toLowerCase() - .replace(/ /g, "_")}.webp`, - })); - setCategories(processedCategories); - console.log("Categories fetched", processedCategories); - }); - }, [isLoggedIn]); - - return ( - - {children} - - ); -} - -// Custom hooks for specific content types -export function useStreams() { - const context = useContext(ContentContext); - if (!context) { - throw new Error("useStreams must be used within a ContentProvider"); - } - return { streams: context.streams, setStreams: context.setStreams }; -} - -export function useCategories() { - const context = useContext(ContentContext); - if (!context) { - throw new Error("useCategories must be used within a ContentProvider"); - } - return { - categories: context.categories, - setCategories: context.setCategories, - }; -} - -export function useUsers() { - const context = useContext(ContentContext); - if (!context) { - throw new Error("useUsers must be used within a ContentProvider"); - } - return { users: context.users, setUsers: context.setUsers }; -} - -// General hook for all content -export function useContent() { - const context = useContext(ContentContext); - if (!context) { - throw new Error("useContent must be used within a ContentProvider"); - } - return context; -} diff --git a/frontend/src/hooks/useContent.ts b/frontend/src/hooks/useContent.ts new file mode 100644 index 0000000..4a5d0b4 --- /dev/null +++ b/frontend/src/hooks/useContent.ts @@ -0,0 +1,140 @@ +// hooks/useContent.ts +import { useState, useEffect } from "react"; +import { useAuth } from "../context/AuthContext"; +import { StreamType } from "../types/StreamType"; +import { CategoryType } from "../types/CategoryType"; +import { UserType } from "../types/UserType"; + +// Helper function to process API data into our consistent types +const processStreamData = (data: any[]): StreamType[] => { + return data.map((stream) => ({ + type: "stream", + id: stream.user_id, + title: stream.title, + username: stream.username, + streamCategory: stream.category_name, + viewers: stream.num_viewers, + thumbnail: + stream.thumbnail || + `/images/category_thumbnails/${stream.category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`, + })); +}; + +const processCategoryData = (data: any[]): CategoryType[] => { + return data.map((category) => ({ + type: "category", + id: category.category_id, + title: category.category_name, + viewers: category.num_viewers, + thumbnail: `/images/category_thumbnails/${category.category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`, + })); +}; + +const processUserData = (data: any[]): UserType[] => { + return data.map((user) => ({ + type: "user", + id: user.user_id, + title: user.username, + username: user.username, + isLive: user.is_live, + viewers: 0, // This may need to be updated based on your API + thumbnail: user.thumbnail || "/images/pfps/default.webp", + })); +}; + +// Generic fetch hook that can be used for any content type +export function useFetchContent( + url: string, + processor: (data: any[]) => T[], + dependencies: any[] = [] +): { data: T[]; isLoading: boolean; error: string | null } { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + setIsLoading(true); + try { + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Error fetching data: ${response.status}`); + } + + const rawData = await response.json(); + const processedData = processor(rawData); + setData(processedData); + setError(null); + } catch (err) { + console.error("Error fetching content:", err); + setError(err instanceof Error ? err.message : "Unknown error"); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, dependencies); + + return { data, isLoading, error }; +} + +// Specific hooks for each content type +export function useStreams(customUrl?: string): { + streams: StreamType[]; + isLoading: boolean; + error: string | null +} { + const { isLoggedIn } = useAuth(); + const url = customUrl || (isLoggedIn + ? "/api/streams/recommended" + : "/api/streams/popular/4"); + + const { data, isLoading, error } = useFetchContent( + url, + processStreamData, + [isLoggedIn, customUrl] + ); + + return { streams: data, isLoading, error }; +} + +export function useCategories(customUrl?: string): { + categories: CategoryType[]; + isLoading: boolean; + error: string | null +} { + const { isLoggedIn } = useAuth(); + const url = customUrl || (isLoggedIn + ? "/api/categories/recommended" + : "/api/categories/popular/4"); + + const { data, isLoading, error } = useFetchContent( + url, + processCategoryData, + [isLoggedIn, customUrl] + ); + + return { categories: data, isLoading, error }; +} + +export function useUsers(customUrl?: string): { + users: UserType[]; + isLoading: boolean; + error: string | null +} { + const url = customUrl || "/api/users/popular"; + + const { data, isLoading, error } = useFetchContent( + url, + processUserData, + [customUrl] + ); + + return { users: data, isLoading, error }; +} \ No newline at end of file diff --git a/frontend/src/pages/CategoryPage.tsx b/frontend/src/pages/CategoryPage.tsx index 6488bca..a87ad07 100644 --- a/frontend/src/pages/CategoryPage.tsx +++ b/frontend/src/pages/CategoryPage.tsx @@ -6,12 +6,12 @@ import { fetchContentOnScroll } from "../hooks/fetchContentOnScroll"; import Button from "../components/Input/Button"; import { useAuth } from "../context/AuthContext"; import { useCategoryFollow } from "../hooks/useCategoryFollow"; -import { ListItemProps as StreamData } from "../components/Layout/ListItem"; import LoadingScreen from "../components/Layout/LoadingScreen"; +import { StreamType } from "../types/StreamType"; const CategoryPage: React.FC = () => { const { categoryName } = useParams<{ categoryName: string }>(); - const [streams, setStreams] = useState([]); + const [streams, setStreams] = useState([]); const listRowRef = useRef(null); const isLoading = useRef(false); const [streamOffset, setStreamOffset] = useState(0); @@ -50,7 +50,7 @@ const CategoryPage: React.FC = () => { setStreamOffset((prev) => prev + data.length); - const processedStreams: StreamData[] = data.map((stream: any) => ({ + const processedStreams: StreamType[] = data.map((stream: any) => ({ type: "stream", id: stream.user_id, title: stream.title, diff --git a/frontend/src/pages/Following.tsx b/frontend/src/pages/Following.tsx index bbf9fce..012b30e 100644 --- a/frontend/src/pages/Following.tsx +++ b/frontend/src/pages/Following.tsx @@ -4,6 +4,7 @@ import { useSidebar } from "../context/SidebarContext"; import { ToggleButton } from "../components/Input/Button"; import { Sidebar as SidebarIcon } from "lucide-react"; import { useNavigate } from "react-router-dom"; // Import useNavigate +import { CategoryType } from "../types/CategoryType"; // Define TypeScript interfaces interface Streamer { diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 71b464f..90c331c 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,7 +1,7 @@ import React from "react"; import ListRow from "../components/Layout/ListRow"; import { useNavigate } from "react-router-dom"; -import { useStreams, useCategories } from "../context/ContentContext"; +import { useStreams, useCategories } from "../hooks/useContent"; import Button from "../components/Input/Button"; import DynamicPageContent from "../components/Layout/DynamicPageContent"; import LoadingScreen from "../components/Layout/LoadingScreen"; @@ -12,8 +12,8 @@ interface HomePageProps { } const HomePage: React.FC = ({ variant = "default" }) => { - const { streams } = useStreams(); - const { categories } = useCategories(); + const { streams, isLoading: isLoadingStreams } = useStreams(); + const { categories, isLoading: isLoadingCategories } = useCategories(); const navigate = useNavigate(); const handleStreamClick = (streamerName: string) => { @@ -24,9 +24,9 @@ const HomePage: React.FC = ({ variant = "default" }) => { navigate(`/category/${categoryName}`); }; - if (!categories || categories.length === 0) { - console.log("No categories found yet"); - return Loading Categories...; + if (isLoadingStreams || isLoadingCategories) { + console.log("No content found yet"); + return Loading Content...; } return ( @@ -79,7 +79,7 @@ const HomePage: React.FC = ({ variant = "default" }) => { Show More -