UPDATE/REFACTOR: Replace ContentContext with useContent hook;
REFACTOR: Add content type files;
This commit is contained in:
@@ -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,
|
||||
}}
|
||||
>
|
||||
<ContentProvider>
|
||||
<SidebarProvider>
|
||||
<QuickSettingsProvider>
|
||||
<BrowserRouter>
|
||||
@@ -105,7 +103,6 @@ function App() {
|
||||
</BrowserRouter>
|
||||
</QuickSettingsProvider>
|
||||
</SidebarProvider>
|
||||
</ContentProvider>
|
||||
</AuthContext.Provider>
|
||||
</Brightness>
|
||||
);
|
||||
|
||||
@@ -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<ContentContextType | undefined>(undefined);
|
||||
|
||||
export function ContentProvider({ children }: { children: React.ReactNode }) {
|
||||
const [streams, setStreams] = useState<StreamItem[]>([]);
|
||||
const [categories, setCategories] = useState<CategoryItem[]>([]);
|
||||
const [users, setUsers] = useState<UserItem[]>([]);
|
||||
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 (
|
||||
<ContentContext.Provider
|
||||
value={{
|
||||
streams,
|
||||
categories,
|
||||
users,
|
||||
setStreams,
|
||||
setCategories,
|
||||
setUsers,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ContentContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
140
frontend/src/hooks/useContent.ts
Normal file
140
frontend/src/hooks/useContent.ts
Normal file
@@ -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<T>(
|
||||
url: string,
|
||||
processor: (data: any[]) => T[],
|
||||
dependencies: any[] = []
|
||||
): { data: T[]; isLoading: boolean; error: string | null } {
|
||||
const [data, setData] = useState<T[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(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<StreamType>(
|
||||
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<CategoryType>(
|
||||
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<UserType>(
|
||||
url,
|
||||
processUserData,
|
||||
[customUrl]
|
||||
);
|
||||
|
||||
return { users: data, isLoading, error };
|
||||
}
|
||||
@@ -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<StreamData[]>([]);
|
||||
const [streams, setStreams] = useState<StreamType[]>([]);
|
||||
const listRowRef = useRef<any>(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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<HomePageProps> = ({ 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<HomePageProps> = ({ variant = "default" }) => {
|
||||
navigate(`/category/${categoryName}`);
|
||||
};
|
||||
|
||||
if (!categories || categories.length === 0) {
|
||||
console.log("No categories found yet");
|
||||
return <LoadingScreen>Loading Categories...</LoadingScreen>;
|
||||
if (isLoadingStreams || isLoadingCategories) {
|
||||
console.log("No content found yet");
|
||||
return <LoadingScreen>Loading Content...</LoadingScreen>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
8
frontend/src/types/CategoryType.ts
Normal file
8
frontend/src/types/CategoryType.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// types/CategoryType.ts
|
||||
export interface CategoryType {
|
||||
type: "category";
|
||||
id: number;
|
||||
title: string;
|
||||
viewers: number;
|
||||
thumbnail?: string;
|
||||
}
|
||||
9
frontend/src/types/StreamType.ts
Normal file
9
frontend/src/types/StreamType.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface StreamType {
|
||||
type: "stream";
|
||||
id: number;
|
||||
title: string;
|
||||
username: string;
|
||||
streamCategory: string;
|
||||
viewers: number;
|
||||
thumbnail?: string;
|
||||
}
|
||||
10
frontend/src/types/UserType.ts
Normal file
10
frontend/src/types/UserType.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// types/UserType.ts
|
||||
export interface UserType {
|
||||
type: "user";
|
||||
id: number;
|
||||
title: string;
|
||||
username: string;
|
||||
isLive: boolean;
|
||||
viewers: number;
|
||||
thumbnail?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user