REFACTOR: Clean code, don't show category name on categoryPage, fix path, remove console logs

This commit is contained in:
Chris-1010
2025-03-05 13:52:24 +00:00
parent 37156ead22
commit c71ffaf8d7
7 changed files with 281 additions and 319 deletions

View File

@@ -2,9 +2,7 @@
<html lang="en" class="min-w-[850px]"> <html lang="en" class="min-w-[850px]">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> --> <link rel="icon" href="/images/favicon_goose.ico" />
<!-- <link rel="icon" href="public/images/favicon_monke.ico" /> -->
<link rel="icon" href="public/images/favicon_goose.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gander</title> <title>Gander</title>
</head> </head>

View File

@@ -39,7 +39,7 @@ const StreamListItem: React.FC<StreamListItemProps> = ({
<div className="p-3"> <div className="p-3">
<h3 className="font-semibold text-lg text-center truncate max-w-full">{title}</h3> <h3 className="font-semibold text-lg text-center truncate max-w-full">{title}</h3>
<p className="font-bold">{username}</p> <p className="font-bold">{username}</p>
<p className="text-sm text-gray-300">{streamCategory}</p> <p className="text-sm text-gray-300">{!window.location.href.includes('/category/') ? streamCategory : ""}</p>
<p className="text-sm text-gray-300">{viewers} viewers</p> <p className="text-sm text-gray-300">{viewers} viewers</p>
</div> </div>
</div> </div>
@@ -149,13 +149,6 @@ const VodListItem: React.FC<VodListItemProps> = ({
</div> </div>
{variant === "vodDashboard" && ( {variant === "vodDashboard" && (
<div className="flex justify-evenly items-stretch rounded-b-lg"> <div className="flex justify-evenly items-stretch rounded-b-lg">
{/* <button
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
onClick={() => console.log("Publish")}
>
<UploadIcon />
Publish
</button> */}
<a <a
className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white" className="flex justify-around w-full h-full bg-black/50 hover:bg-black/80 p-2 mx-1 font-semibold rounded-full border border-transparent hover:border-white"
href={`/vods/${username}/${vod_id}.mp4`} href={`/vods/${username}/${vod_id}.mp4`}

View File

@@ -23,13 +23,9 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
const [justToggled, setJustToggled] = React.useState(false); const [justToggled, setJustToggled] = React.useState(false);
const handleLogout = () => { const handleLogout = () => {
console.log("Logging out...");
fetch("/api/logout") fetch("/api/logout")
.then((response) => response.json()) .then((response) => response.json())
.then((data) => { .then(() => window.location.reload());
console.log(data);
window.location.reload();
});
}; };
const handleQuickSettings = () => { const handleQuickSettings = () => {

View File

@@ -8,13 +8,10 @@ import VideoPlayer from "../../components/Stream/VideoPlayer";
import { CategoryType } from "../../types/CategoryType"; import { CategoryType } from "../../types/CategoryType";
import { StreamListItem } from "../../components/Layout/ListItem"; import { StreamListItem } from "../../components/Layout/ListItem";
import { getCategoryThumbnail } from "../../utils/thumbnailUtils"; import { getCategoryThumbnail } from "../../utils/thumbnailUtils";
import { StreamType } from "../../types/StreamType";
interface StreamData { interface StreamData extends Omit<StreamType, "type" | "username" | "id"> {
title: string; streamKey: string;
category_name: string;
viewer_count: number;
start_time: string;
stream_key: string;
} }
interface StreamDashboardProps { interface StreamDashboardProps {
@@ -26,10 +23,10 @@ interface StreamDashboardProps {
const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isLive }) => { const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isLive }) => {
const [streamData, setStreamData] = useState<StreamData>({ const [streamData, setStreamData] = useState<StreamData>({
title: "", title: "",
category_name: "", streamCategory: "",
viewer_count: 0, viewers: 0,
start_time: "", startTime: "",
stream_key: "", streamKey: "",
}); });
const [timeStarted, setTimeStarted] = useState(""); const [timeStarted, setTimeStarted] = useState("");
const [streamDetected, setStreamDetected] = useState(false); const [streamDetected, setStreamDetected] = useState(false);
@@ -79,17 +76,17 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
try { try {
if (isLive) { if (isLive) {
const streamResponse = await fetch(`/api/streams/${userId}/data`, { credentials: "include" }); const streamResponse = await fetch(`/api/streams/${userId}/data`, { credentials: "include" });
const streamData = await streamResponse.json(); const data = await streamResponse.json();
setStreamData({ setStreamData({
title: streamData.title, title: data.title,
category_name: streamData.category_name, streamCategory: data.category_name,
viewer_count: streamData.num_viewers, viewers: data.num_viewers,
start_time: streamData.start_time, startTime: data.start_time,
stream_key: streamData.stream_key, streamKey: data.stream_key,
}); });
const time = Math.floor( const time = Math.floor(
(Date.now() - new Date(streamData.start_time).getTime()) / 60000 // Convert to minutes (Date.now() - new Date(data.startTime).getTime()) / 60000 // Convert to minutes
); );
if (time < 60) setTimeStarted(`${time}m ago`); if (time < 60) setTimeStarted(`${time}m ago`);
@@ -101,7 +98,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
const keyData = await response.json(); const keyData = await response.json();
setStreamData((prev) => ({ setStreamData((prev) => ({
...prev, ...prev,
stream_key: keyData.stream_key, streamKey: keyData.stream_key,
})); }));
} }
} catch (error) { } catch (error) {
@@ -113,7 +110,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
const { name, value } = e.target; const { name, value } = e.target;
setStreamData((prev) => ({ ...prev, [name]: value })); setStreamData((prev) => ({ ...prev, [name]: value }));
if (name === "category_name") { if (name === "streamCategory") {
const filtered = categories.filter((cat: CategoryType) => cat.title.toLowerCase().includes(value.toLowerCase())); const filtered = categories.filter((cat: CategoryType) => cat.title.toLowerCase().includes(value.toLowerCase()));
setFilteredCategories(filtered); setFilteredCategories(filtered);
if (debouncedCheck) { if (debouncedCheck) {
@@ -124,7 +121,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
const handleCategorySelect = (categoryName: string) => { const handleCategorySelect = (categoryName: string) => {
console.log("Selected category:", categoryName); console.log("Selected category:", categoryName);
setStreamData((prev) => ({ ...prev, category_name: categoryName })); setStreamData((prev) => ({ ...prev, streamCategory: categoryName }));
setFilteredCategories([]); setFilteredCategories([]);
if (debouncedCheck) { if (debouncedCheck) {
debouncedCheck(categoryName); debouncedCheck(categoryName);
@@ -141,8 +138,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
}); });
} else { } else {
setThumbnail(null); setThumbnail(null);
if (streamData.category_name && debouncedCheck) { if (streamData.streamCategory && debouncedCheck) {
debouncedCheck(streamData.category_name); debouncedCheck(streamData.streamCategory);
} else { } else {
setThumbnailPreview({ url: "", isCustom: false }); setThumbnailPreview({ url: "", isCustom: false });
} }
@@ -151,9 +148,9 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
const clearThumbnail = () => { const clearThumbnail = () => {
setThumbnail(null); setThumbnail(null);
if (streamData.category_name) { if (streamData.streamCategory) {
console.log("Clearing thumbnail as category is set and default category thumbnail will be used"); console.log("Clearing thumbnail as category is set and default category thumbnail will be used");
const defaultThumbnail = getCategoryThumbnail(streamData.category_name); const defaultThumbnail = getCategoryThumbnail(streamData.streamCategory);
setThumbnailPreview({ url: defaultThumbnail, isCustom: false }); setThumbnailPreview({ url: defaultThumbnail, isCustom: false });
} else { } else {
setThumbnailPreview({ url: "", isCustom: false }); setThumbnailPreview({ url: "", isCustom: false });
@@ -163,8 +160,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
const isFormValid = () => { const isFormValid = () => {
return ( return (
streamData.title.trim() !== "" && streamData.title.trim() !== "" &&
streamData.category_name.trim() !== "" && streamData.streamCategory.trim() !== "" &&
categories.some((cat: CategoryType) => cat.title.toLowerCase() === streamData.category_name.toLowerCase()) && categories.some((cat: CategoryType) => cat.title.toLowerCase() === streamData.streamCategory.toLowerCase()) &&
streamDetected streamDetected
); );
}; };
@@ -198,9 +195,9 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
console.log("Updating stream with data:", streamData); console.log("Updating stream with data:", streamData);
const formData = new FormData(); const formData = new FormData();
formData.append("key", streamData.stream_key); formData.append("key", streamData.streamKey);
formData.append("title", streamData.title); formData.append("title", streamData.title);
formData.append("category_name", streamData.category_name); formData.append("streamCategory", streamData.streamCategory);
if (thumbnail) { if (thumbnail) {
formData.append("thumbnail", thumbnail); formData.append("thumbnail", thumbnail);
} }
@@ -231,7 +228,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ key: streamData.stream_key }), body: JSON.stringify({ key: streamData.streamKey }),
}); });
if (response.ok) { if (response.ok) {
@@ -267,8 +264,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
<div className="relative"> <div className="relative">
<label className="block text-white mb-2">Category</label> <label className="block text-white mb-2">Category</label>
<Input <Input
name="category_name" name="streamCategory"
value={streamData.category_name} value={streamData.streamCategory}
onChange={handleInputChange} onChange={handleInputChange}
onFocus={() => setIsCategoryFocused(true)} onFocus={() => setIsCategoryFocused(true)}
onBlur={() => setTimeout(() => setIsCategoryFocused(false), 200)} onBlur={() => setTimeout(() => setIsCategoryFocused(false), 200)}
@@ -321,16 +318,16 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
{isLive && ( {isLive && (
<div className="bg-gray-700 p-4 rounded-lg"> <div className="bg-gray-700 p-4 rounded-lg">
<h3 className="text-white font-semibold mb-2">Stream Info</h3> <h3 className="text-white font-semibold mb-2">Stream Info</h3>
<p className="text-gray-300">Viewers: {streamData.viewer_count}</p> <p className="text-gray-300">Viewers: {streamData.viewers}</p>
<p className="text-gray-300"> <p className="text-gray-300">
Started: {new Date(streamData.start_time!).toLocaleTimeString()} Started: {new Date(streamData.startTime!).toLocaleTimeString()}
{` (${timeStarted})`} {` (${timeStarted})`}
</p> </p>
</div> </div>
)} )}
<div className="flex items-center mx-auto p-10 bg-gray-900 w-fit rounded-xl py-4"> <div className="flex items-center mx-auto p-10 bg-gray-900 w-fit rounded-xl py-4">
<label className="block text-white mr-8">Stream Key</label> <label className="block text-white mr-8">Stream Key</label>
<Input type={showKey ? "text" : "password"} value={streamData.stream_key} readOnly extraClasses="w-fit pr-[30px]" disabled /> <Input type={showKey ? "text" : "password"} value={streamData.streamKey} readOnly extraClasses="w-fit pr-[30px]" disabled />
<button type="button" onClick={() => setShowKey(!showKey)} className="-translate-x-[30px] top-1/2 h-6 w-6 text-white"> <button type="button" onClick={() => setShowKey(!showKey)} className="-translate-x-[30px] top-1/2 h-6 w-6 text-white">
{showKey ? <HideIcon className="h-6 w-6" /> : <ShowIcon className="h-6 w-6" />} {showKey ? <HideIcon className="h-6 w-6" /> : <ShowIcon className="h-6 w-6" />}
</button> </button>
@@ -372,8 +369,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
id={1} id={1}
title={streamData.title || "Stream Title"} title={streamData.title || "Stream Title"}
username={username || ""} username={username || ""}
streamCategory={streamData.category_name || "Category"} streamCategory={streamData.streamCategory || "Category"}
viewers={streamData.viewer_count} viewers={streamData.viewers}
thumbnail={thumbnailPreview.url || ""} thumbnail={thumbnailPreview.url || ""}
onItemClick={() => { onItemClick={() => {
window.open(`/${username}`, "_blank"); window.open(`/${username}`, "_blank");

View File

@@ -105,7 +105,6 @@ export function useStreams(customUrl?: string): {
const [streams, setStreams] = useState<StreamType[]>([]); const [streams, setStreams] = useState<StreamType[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
console.log(streams)
useEffect(() => { useEffect(() => {
const fetchStreams = async () => { const fetchStreams = async () => {
@@ -159,7 +158,6 @@ export function useStreams(customUrl?: string): {
} }
const data = await response.json(); const data = await response.json();
console.log("Data: ", data)
// Make sure it is 100% ARRAY NOT OBJECT // Make sure it is 100% ARRAY NOT OBJECT
const formattedData = Array.isArray(data) ? data : [data]; const formattedData = Array.isArray(data) ? data : [data];
@@ -196,94 +194,92 @@ export function useCategories(customUrl?: string): {
categories: CategoryType[]; categories: CategoryType[];
isLoading: boolean; isLoading: boolean;
error: string | null; error: string | null;
} { } {
const { isLoggedIn } = useAuth(); const { isLoggedIn } = useAuth();
const [categories, setCategories] = useState<CategoryType[]>([]); const [categories, setCategories] = useState<CategoryType[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchCategories = async () => { const fetchCategories = async () => {
setIsLoading(true); setIsLoading(true);
try {
// Always fetch the recommended categories if logged in
if (isLoggedIn && !customUrl) {
const recommendedResponse = await fetch("/api/categories/recommended");
if (!recommendedResponse.ok) {
throw new Error(`Error fetching recommended categories: ${recommendedResponse.status}`);
}
const recommendedData = await recommendedResponse.json();
const processedRecommended = processCategoryData(recommendedData);
// If we have at least 4 recommended categories, use just those
if (processedRecommended.length >= 4) {
setCategories(processedRecommended);
}
// If we have fewer than 4, fetch popular categories to fill the gap
else {
const popularResponse = await fetch(`/api/categories/popular/8`);
if (!popularResponse.ok) {
throw new Error(`Error fetching popular categories: ${popularResponse.status}`);
}
const popularData = await popularResponse.json();
const processedPopular = processCategoryData(popularData);
// Get IDs of recommended categories to avoid duplicates
const recommendedIds = processedRecommended.map(cat => cat.id);
// Filter popular categories to only include ones not in recommended
const uniquePopularCategories = processedPopular.filter(
popularCat => !recommendedIds.includes(popularCat.id)
);
// Combine with recommended categories first to maintain priority
const combinedCategories = [...processedRecommended, ...uniquePopularCategories];
setCategories(combinedCategories);
}
}
// For custom URL or not logged in, use the original approach
else {
const url = customUrl || "/api/categories/popular/4";
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error fetching categories: ${response.status}`);
}
const data = await response.json();
setCategories(processCategoryData(data));
}
setError(null);
} catch (err) {
console.error("Error in useCategories:", err);
setError(err instanceof Error ? err.message : "Unknown error");
// Fallback to popular categories on error
if (!customUrl) {
try { try {
const fallbackResponse = await fetch("/api/categories/popular/4"); // Always fetch the recommended categories if logged in
if (fallbackResponse.ok) { if (isLoggedIn && !customUrl) {
const fallbackData = await fallbackResponse.json(); const recommendedResponse = await fetch("/api/categories/recommended");
setCategories(processCategoryData(fallbackData)); if (!recommendedResponse.ok) {
} throw new Error(`Error fetching recommended categories: ${recommendedResponse.status}`);
} catch (fallbackErr) { }
console.error("Error fetching fallback categories:", fallbackErr);
}
}
} finally {
setIsLoading(false);
}
};
fetchCategories(); const recommendedData = await recommendedResponse.json();
const processedRecommended = processCategoryData(recommendedData);
// If we have at least 4 recommended categories, use just those
if (processedRecommended.length >= 4) {
setCategories(processedRecommended);
}
// If we have fewer than 4, fetch popular categories to fill the gap
else {
const popularResponse = await fetch(`/api/categories/popular/8`);
if (!popularResponse.ok) {
throw new Error(`Error fetching popular categories: ${popularResponse.status}`);
}
const popularData = await popularResponse.json();
const processedPopular = processCategoryData(popularData);
// Get IDs of recommended categories to avoid duplicates
const recommendedIds = processedRecommended.map((cat) => cat.id);
// Filter popular categories to only include ones not in recommended
const uniquePopularCategories = processedPopular.filter((popularCat) => !recommendedIds.includes(popularCat.id));
// Combine with recommended categories first to maintain priority
const combinedCategories = [...processedRecommended, ...uniquePopularCategories];
setCategories(combinedCategories);
}
}
// For custom URL or not logged in, use the original approach
else {
const url = customUrl || "/api/categories/popular/4";
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error fetching categories: ${response.status}`);
}
const data = await response.json();
setCategories(processCategoryData(data));
}
setError(null);
} catch (err) {
console.error("Error in useCategories:", err);
setError(err instanceof Error ? err.message : "Unknown error");
// Fallback to popular categories on error
if (!customUrl) {
try {
const fallbackResponse = await fetch("/api/categories/popular/4");
if (fallbackResponse.ok) {
const fallbackData = await fallbackResponse.json();
setCategories(processCategoryData(fallbackData));
}
} catch (fallbackErr) {
console.error("Error fetching fallback categories:", fallbackErr);
}
}
} finally {
setIsLoading(false);
}
};
fetchCategories();
}, [isLoggedIn, customUrl]); }, [isLoggedIn, customUrl]);
return { categories, isLoading, error }; return { categories, isLoading, error };
} }
export function useVods(customUrl?: string): { export function useVods(customUrl?: string): {
vods: VodType[]; vods: VodType[];

View File

@@ -8,96 +8,89 @@ import { CategoryType } from "../types/CategoryType";
import { getCategoryThumbnail } from "../utils/thumbnailUtils"; import { getCategoryThumbnail } from "../utils/thumbnailUtils";
const AllCategoriesPage: React.FC = () => { const AllCategoriesPage: React.FC = () => {
const [categories, setCategories] = useState<CategoryType[]>([]); const [categories, setCategories] = useState<CategoryType[]>([]);
const navigate = useNavigate(); const navigate = useNavigate();
const [categoryOffset, setCategoryOffset] = useState(0); const [categoryOffset, setCategoryOffset] = useState(0);
const [noCategories, setNoCategories] = useState(12); const [noCategories, setNoCategories] = useState(12);
const [hasMoreData, setHasMoreData] = useState(true); const [hasMoreData, setHasMoreData] = useState(true);
const listRowRef = useRef<any>(null); const listRowRef = useRef<any>(null);
const isLoading = useRef(false); const isLoading = useRef(false);
const fetchCategories = async () => { const fetchCategories = async () => {
// If already loading, skip this fetch // If already loading, skip this fetch
if (isLoading.current) return; if (isLoading.current) return;
isLoading.current = true; isLoading.current = true;
try { try {
const response = await fetch(`/api/categories/popular/${noCategories}/${categoryOffset}`); const response = await fetch(`/api/categories/popular/${noCategories}/${categoryOffset}`);
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to fetch categories"); throw new Error("Failed to fetch categories");
} }
const data = await response.json(); const data = await response.json();
if (data.length === 0) { if (data.length === 0) {
setHasMoreData(false); setHasMoreData(false);
return []; return [];
} }
setCategoryOffset(prev => prev + data.length); setCategoryOffset((prev) => prev + data.length);
const processedCategories = data.map((category: any) => ({ const processedCategories = data.map((category: any) => ({
type: "category" as const, type: "category" as const,
id: category.category_id, id: category.category_id,
title: category.category_name, title: category.category_name,
viewers: category.num_viewers, viewers: category.num_viewers,
thumbnail: getCategoryThumbnail(category.category_name) thumbnail: getCategoryThumbnail(category.category_name),
})); }));
setCategories(prev => [...prev, ...processedCategories]); setCategories((prev) => [...prev, ...processedCategories]);
return processedCategories; return processedCategories;
} catch (error) { } catch (error) {
console.error("Error fetching categories:", error); console.error("Error fetching categories:", error);
return []; return [];
} finally { } finally {
isLoading.current = false; isLoading.current = false;
} }
}; };
useEffect(() => { useEffect(() => {
fetchCategories(); fetchCategories();
}, []); }, []);
const loadOnScroll = async () => { const loadOnScroll = async () => {
if (hasMoreData && listRowRef.current) { if (hasMoreData && listRowRef.current) {
const newCategories = await fetchCategories(); const newCategories = await fetchCategories();
if (newCategories?.length > 0) { if (newCategories?.length > 0) {
listRowRef.current.addMoreItems(newCategories); listRowRef.current.addMoreItems(newCategories);
} }
} }
}; };
fetchContentOnScroll(loadOnScroll, hasMoreData); fetchContentOnScroll(loadOnScroll, hasMoreData);
if (hasMoreData && !categories.length) return <LoadingScreen />; if (hasMoreData && !categories.length) return <LoadingScreen />;
const handleCategoryClick = (categoryName: string) => { const handleCategoryClick = (categoryName: string) => {
console.log(categoryName); navigate(`/category/${categoryName}`);
navigate(`/category/${categoryName}`); };
};
return ( return (
<DynamicPageContent <DynamicPageContent className="min-h-screen">
className="min-h-screen" <ListRow
> ref={listRowRef}
<ListRow type="category"
ref={listRowRef} title="All Categories"
type="category" items={categories}
title="All Categories" onItemClick={handleCategoryClick}
items={categories} extraClasses="bg-[var(--recommend)] text-center"
onItemClick={handleCategoryClick} itemExtraClasses="w-[20vw]"
extraClasses="bg-[var(--recommend)] text-center" wrap={true}
itemExtraClasses="w-[20vw]" />
wrap={true} {!hasMoreData && !categories.length && <div className="text-center text-gray-500 p-4">No more categories to load</div>}
/> </DynamicPageContent>
{!hasMoreData && !categories.length && ( );
<div className="text-center text-gray-500 p-4">
No more categories to load
</div>
)}
</DynamicPageContent>
);
}; };
export default AllCategoriesPage; export default AllCategoriesPage;

View File

@@ -11,124 +11,113 @@ import { StreamType } from "../types/StreamType";
import { getCategoryThumbnail } from "../utils/thumbnailUtils"; import { getCategoryThumbnail } from "../utils/thumbnailUtils";
const CategoryPage: React.FC = () => { const CategoryPage: React.FC = () => {
const { categoryName } = useParams<{ categoryName: string }>(); const { categoryName } = useParams<{ categoryName: string }>();
const [streams, setStreams] = useState<StreamType[]>([]); const [streams, setStreams] = useState<StreamType[]>([]);
const listRowRef = useRef<any>(null); const listRowRef = useRef<any>(null);
const isLoading = useRef(false); const isLoading = useRef(false);
const [streamOffset, setStreamOffset] = useState(0); const [streamOffset, setStreamOffset] = useState(0);
const [noStreams, setNoStreams] = useState(12); const [noStreams, setNoStreams] = useState(12);
const [hasMoreData, setHasMoreData] = useState(true); const [hasMoreData, setHasMoreData] = useState(true);
const { isLoggedIn } = useAuth(); const { isLoggedIn } = useAuth();
const { const { isCategoryFollowing, checkCategoryFollowStatus, followCategory, unfollowCategory } = useCategoryFollow();
isCategoryFollowing,
checkCategoryFollowStatus,
followCategory,
unfollowCategory,
} = useCategoryFollow();
useEffect(() => { useEffect(() => {
if (categoryName) checkCategoryFollowStatus(categoryName); if (categoryName) checkCategoryFollowStatus(categoryName);
}, [categoryName]); }, [categoryName]);
const fetchCategoryStreams = async () => { const fetchCategoryStreams = async () => {
// If already loading, skip this fetch // If already loading, skip this fetch
if (isLoading.current) return; if (isLoading.current) return;
isLoading.current = true; isLoading.current = true;
try { try {
const response = await fetch( const response = await fetch(`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}`);
`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}` if (!response.ok) {
); throw new Error("Failed to fetch category streams");
if (!response.ok) { }
throw new Error("Failed to fetch category streams"); const data = await response.json();
}
const data = await response.json();
if (data.length === 0) { if (data.length === 0) {
setHasMoreData(false); setHasMoreData(false);
return []; return [];
} }
setStreamOffset((prev) => prev + data.length); setStreamOffset((prev) => prev + data.length);
const processedStreams = data.map((stream: any) => ({ const processedStreams = data.map((stream: any) => ({
type: "stream", type: "stream",
id: stream.user_id, id: stream.user_id,
title: stream.title, title: stream.title,
username: stream.username, username: stream.username,
streamCategory: categoryName, streamCategory: categoryName,
viewers: stream.num_viewers, viewers: stream.num_viewers,
thumbnail: getCategoryThumbnail(categoryName, stream.thumbnail), thumbnail: getCategoryThumbnail(categoryName, stream.thumbnail),
})); }));
setStreams((prev) => [...prev, ...processedStreams]); setStreams((prev) => [...prev, ...processedStreams]);
return processedStreams; return processedStreams;
} catch (error) { } catch (error) {
console.error("Error fetching category streams:", error); console.error("Error fetching category streams:", error);
} finally { } finally {
isLoading.current = false; isLoading.current = false;
} }
}; };
useEffect(() => { useEffect(() => {
fetchCategoryStreams(); fetchCategoryStreams();
}, []); }, []);
const loadOnScroll = async () => { const loadOnScroll = async () => {
if (hasMoreData && listRowRef.current) { if (hasMoreData && listRowRef.current) {
const newStreams = await fetchCategoryStreams(); const newStreams = await fetchCategoryStreams();
if (newStreams?.length > 0) { if (newStreams?.length > 0) {
listRowRef.current.addMoreItems(newStreams); listRowRef.current.addMoreItems(newStreams);
} else console.log("No more data to fetch"); } else console.log("No more data to fetch");
} }
}; };
fetchContentOnScroll(loadOnScroll, hasMoreData); fetchContentOnScroll(loadOnScroll, hasMoreData);
const handleStreamClick = (streamerName: string) => { const handleStreamClick = (streamerName: string) => {
window.location.href = `/${streamerName}`; window.location.href = `/${streamerName}`;
}; };
if (hasMoreData && !streams.length) return <LoadingScreen />; if (hasMoreData && !streams.length) return <LoadingScreen />;
return ( return (
<DynamicPageContent className="min-h-screen"> <DynamicPageContent className="min-h-screen">
<div className="pt-8"> <div className="pt-8">
<ListRow <ListRow
ref={listRowRef} ref={listRowRef}
type="stream" type="stream"
title={`${categoryName}`} title={`${categoryName}`}
description={`Live streams in the ${categoryName} category`} description={`Live streams in the ${categoryName} category`}
items={streams} items={streams}
wrap={true} wrap={true}
onItemClick={handleStreamClick} onItemClick={handleStreamClick}
extraClasses="bg-[var(--recommend)]" extraClasses="bg-[var(--recommend)]"
itemExtraClasses="w-[20vw]" itemExtraClasses="w-[20vw]"
> >
{isLoggedIn && ( {isLoggedIn && (
<Button <Button
extraClasses="absolute right-10" extraClasses="absolute right-10"
onClick={() => { onClick={() => {
if (categoryName) { if (categoryName) {
isCategoryFollowing isCategoryFollowing ? unfollowCategory(categoryName) : followCategory(categoryName);
? unfollowCategory(categoryName) }
: followCategory(categoryName); }}
} >
}} {isCategoryFollowing ? "Unfollow" : "Follow"}
> </Button>
{isCategoryFollowing ? "Unfollow" : "Follow"} )}
</Button> </ListRow>
)} </div>
</ListRow>
</div>
{streams.length === 0 && !isLoading && ( {streams.length === 0 && !isLoading && (
<div className="text-white text-center text-2xl mt-8"> <div className="text-white text-center text-2xl mt-8">No live streams found in this category</div>
No live streams found in this category )}
</div> </DynamicPageContent>
)} );
</DynamicPageContent>
);
}; };
export default CategoryPage; export default CategoryPage;