Merge branch 'main' of https://github.com/john-david3/cs3305-team11
This commit is contained in:
@@ -2,9 +2,7 @@
|
||||
<html lang="en" class="min-w-[850px]">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<!-- <link rel="icon" href="public/images/favicon_monke.ico" /> -->
|
||||
<link rel="icon" href="public/images/favicon_goose.ico" />
|
||||
<link rel="icon" href="/images/favicon_goose.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gander</title>
|
||||
</head>
|
||||
|
||||
@@ -39,7 +39,7 @@ const StreamListItem: React.FC<StreamListItemProps> = ({
|
||||
<div className="p-3">
|
||||
<h3 className="font-semibold text-lg text-center truncate max-w-full">{title}</h3>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,13 +149,6 @@ const VodListItem: React.FC<VodListItemProps> = ({
|
||||
</div>
|
||||
{variant === "vodDashboard" && (
|
||||
<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
|
||||
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`}
|
||||
|
||||
@@ -16,20 +16,16 @@ interface NavbarProps {
|
||||
}
|
||||
|
||||
const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
const { isLoggedIn, isLive } = useAuth();
|
||||
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||
const { showSideBar } = useSidebar();
|
||||
const { showQuickSettings, setShowQuickSettings } = useQuickSettings();
|
||||
const [justToggled, setJustToggled] = React.useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
console.log("Logging out...");
|
||||
fetch("/api/logout")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
window.location.reload();
|
||||
});
|
||||
.then(() => window.location.reload());
|
||||
};
|
||||
|
||||
const handleQuickSettings = () => {
|
||||
@@ -57,7 +53,10 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
className={`relative flex justify-evenly items-center ${variant === "home" ? "h-[45vh] flex-col" : "h-[15vh] col-span-2 flex-row"}`}
|
||||
>
|
||||
{isLoggedIn && window.innerWidth > 900 && <Sidebar />}
|
||||
<Logo extraClasses={variant != "home" && showSideBar && !window.location.pathname.includes("dashboard") ? "scale-0" : "duration-[3s]"} variant={variant} />
|
||||
<Logo
|
||||
extraClasses={variant != "home" && showSideBar && !window.location.pathname.includes("dashboard") ? "scale-0" : "duration-[3s]"}
|
||||
variant={variant}
|
||||
/>
|
||||
{/* Login / Logout Button */}
|
||||
<Button
|
||||
extraClasses={`absolute top-[2vh] ${
|
||||
@@ -97,7 +96,9 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
{/* Stream Button */}
|
||||
{isLoggedIn && !window.location.pathname.includes("dashboard") && (
|
||||
<Button
|
||||
extraClasses={`${variant === "home" ? "absolute top-[2vh] right-[10vw]" : ""} flex flex-row items-center`}
|
||||
extraClasses={`${variant === "home" ? "absolute top-[2vh] right-[10vw]" : ""} ${
|
||||
isLive ? "bg-red-600" : ""
|
||||
} flex flex-row items-center`}
|
||||
onClick={() => (window.location.href = "/dashboard")}
|
||||
>
|
||||
<LiveIcon className="h-15 w-15 mr-2" />
|
||||
|
||||
@@ -8,13 +8,10 @@ import VideoPlayer from "../../components/Stream/VideoPlayer";
|
||||
import { CategoryType } from "../../types/CategoryType";
|
||||
import { StreamListItem } from "../../components/Layout/ListItem";
|
||||
import { getCategoryThumbnail } from "../../utils/thumbnailUtils";
|
||||
import { StreamType } from "../../types/StreamType";
|
||||
|
||||
interface StreamData {
|
||||
title: string;
|
||||
category_name: string;
|
||||
viewer_count: number;
|
||||
start_time: string;
|
||||
stream_key: string;
|
||||
interface StreamData extends Omit<StreamType, "type" | "username" | "id"> {
|
||||
streamKey: string;
|
||||
}
|
||||
|
||||
interface StreamDashboardProps {
|
||||
@@ -26,10 +23,10 @@ interface StreamDashboardProps {
|
||||
const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isLive }) => {
|
||||
const [streamData, setStreamData] = useState<StreamData>({
|
||||
title: "",
|
||||
category_name: "",
|
||||
viewer_count: 0,
|
||||
start_time: "",
|
||||
stream_key: "",
|
||||
streamCategory: "",
|
||||
viewers: 0,
|
||||
startTime: "",
|
||||
streamKey: "",
|
||||
});
|
||||
const [timeStarted, setTimeStarted] = useState("");
|
||||
const [streamDetected, setStreamDetected] = useState(false);
|
||||
@@ -79,17 +76,17 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
try {
|
||||
if (isLive) {
|
||||
const streamResponse = await fetch(`/api/streams/${userId}/data`, { credentials: "include" });
|
||||
const streamData = await streamResponse.json();
|
||||
const data = await streamResponse.json();
|
||||
setStreamData({
|
||||
title: streamData.title,
|
||||
category_name: streamData.category_name,
|
||||
viewer_count: streamData.num_viewers,
|
||||
start_time: streamData.start_time,
|
||||
stream_key: streamData.stream_key,
|
||||
title: data.title,
|
||||
streamCategory: data.category_name,
|
||||
viewers: data.num_viewers,
|
||||
startTime: data.start_time,
|
||||
streamKey: data.stream_key,
|
||||
});
|
||||
|
||||
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`);
|
||||
@@ -101,7 +98,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
const keyData = await response.json();
|
||||
setStreamData((prev) => ({
|
||||
...prev,
|
||||
stream_key: keyData.stream_key,
|
||||
streamKey: keyData.stream_key,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -113,7 +110,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
const { name, value } = e.target;
|
||||
setStreamData((prev) => ({ ...prev, [name]: value }));
|
||||
|
||||
if (name === "category_name") {
|
||||
if (name === "streamCategory") {
|
||||
const filtered = categories.filter((cat: CategoryType) => cat.title.toLowerCase().includes(value.toLowerCase()));
|
||||
setFilteredCategories(filtered);
|
||||
if (debouncedCheck) {
|
||||
@@ -124,7 +121,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
|
||||
const handleCategorySelect = (categoryName: string) => {
|
||||
console.log("Selected category:", categoryName);
|
||||
setStreamData((prev) => ({ ...prev, category_name: categoryName }));
|
||||
setStreamData((prev) => ({ ...prev, streamCategory: categoryName }));
|
||||
setFilteredCategories([]);
|
||||
if (debouncedCheck) {
|
||||
debouncedCheck(categoryName);
|
||||
@@ -141,8 +138,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
});
|
||||
} else {
|
||||
setThumbnail(null);
|
||||
if (streamData.category_name && debouncedCheck) {
|
||||
debouncedCheck(streamData.category_name);
|
||||
if (streamData.streamCategory && debouncedCheck) {
|
||||
debouncedCheck(streamData.streamCategory);
|
||||
} else {
|
||||
setThumbnailPreview({ url: "", isCustom: false });
|
||||
}
|
||||
@@ -151,9 +148,9 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
|
||||
const clearThumbnail = () => {
|
||||
setThumbnail(null);
|
||||
if (streamData.category_name) {
|
||||
if (streamData.streamCategory) {
|
||||
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 });
|
||||
} else {
|
||||
setThumbnailPreview({ url: "", isCustom: false });
|
||||
@@ -163,8 +160,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
const isFormValid = () => {
|
||||
return (
|
||||
streamData.title.trim() !== "" &&
|
||||
streamData.category_name.trim() !== "" &&
|
||||
categories.some((cat: CategoryType) => cat.title.toLowerCase() === streamData.category_name.toLowerCase()) &&
|
||||
streamData.streamCategory.trim() !== "" &&
|
||||
categories.some((cat: CategoryType) => cat.title.toLowerCase() === streamData.streamCategory.toLowerCase()) &&
|
||||
streamDetected
|
||||
);
|
||||
};
|
||||
@@ -198,9 +195,9 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
console.log("Updating stream with data:", streamData);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("key", streamData.stream_key);
|
||||
formData.append("key", streamData.streamKey);
|
||||
formData.append("title", streamData.title);
|
||||
formData.append("category_name", streamData.category_name);
|
||||
formData.append("streamCategory", streamData.streamCategory);
|
||||
if (thumbnail) {
|
||||
formData.append("thumbnail", thumbnail);
|
||||
}
|
||||
@@ -231,7 +228,7 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ key: streamData.stream_key }),
|
||||
body: JSON.stringify({ key: streamData.streamKey }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -267,8 +264,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
<div className="relative">
|
||||
<label className="block text-white mb-2">Category</label>
|
||||
<Input
|
||||
name="category_name"
|
||||
value={streamData.category_name}
|
||||
name="streamCategory"
|
||||
value={streamData.streamCategory}
|
||||
onChange={handleInputChange}
|
||||
onFocus={() => setIsCategoryFocused(true)}
|
||||
onBlur={() => setTimeout(() => setIsCategoryFocused(false), 200)}
|
||||
@@ -321,16 +318,16 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
{isLive && (
|
||||
<div className="bg-gray-700 p-4 rounded-lg">
|
||||
<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">
|
||||
Started: {new Date(streamData.start_time!).toLocaleTimeString()}
|
||||
Started: {new Date(streamData.startTime!).toLocaleTimeString()}
|
||||
{` (${timeStarted})`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<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>
|
||||
<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">
|
||||
{showKey ? <HideIcon className="h-6 w-6" /> : <ShowIcon className="h-6 w-6" />}
|
||||
</button>
|
||||
@@ -372,8 +369,8 @@ const StreamDashboard: React.FC<StreamDashboardProps> = ({ username, userId, isL
|
||||
id={1}
|
||||
title={streamData.title || "Stream Title"}
|
||||
username={username || ""}
|
||||
streamCategory={streamData.category_name || "Category"}
|
||||
viewers={streamData.viewer_count}
|
||||
streamCategory={streamData.streamCategory || "Category"}
|
||||
viewers={streamData.viewers}
|
||||
thumbnail={thumbnailPreview.url || ""}
|
||||
onItemClick={() => {
|
||||
window.open(`/${username}`, "_blank");
|
||||
|
||||
@@ -105,7 +105,6 @@ export function useStreams(customUrl?: string): {
|
||||
const [streams, setStreams] = useState<StreamType[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
console.log(streams)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStreams = async () => {
|
||||
@@ -159,7 +158,6 @@ export function useStreams(customUrl?: string): {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Data: ", data)
|
||||
|
||||
// Make sure it is 100% ARRAY NOT OBJECT
|
||||
const formattedData = Array.isArray(data) ? data : [data];
|
||||
@@ -196,94 +194,92 @@ export function useCategories(customUrl?: string): {
|
||||
categories: CategoryType[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
} {
|
||||
} {
|
||||
const { isLoggedIn } = useAuth();
|
||||
const [categories, setCategories] = useState<CategoryType[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCategories = async () => {
|
||||
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) {
|
||||
const fetchCategories = async () => {
|
||||
setIsLoading(true);
|
||||
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);
|
||||
// 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 {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
};
|
||||
|
||||
fetchCategories();
|
||||
}, [isLoggedIn, customUrl]);
|
||||
|
||||
|
||||
return { categories, isLoading, error };
|
||||
}
|
||||
}
|
||||
|
||||
export function useVods(customUrl?: string): {
|
||||
vods: VodType[];
|
||||
|
||||
@@ -8,96 +8,89 @@ import { CategoryType } from "../types/CategoryType";
|
||||
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||
|
||||
const AllCategoriesPage: React.FC = () => {
|
||||
const [categories, setCategories] = useState<CategoryType[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const [categoryOffset, setCategoryOffset] = useState(0);
|
||||
const [noCategories, setNoCategories] = useState(12);
|
||||
const [hasMoreData, setHasMoreData] = useState(true);
|
||||
|
||||
const listRowRef = useRef<any>(null);
|
||||
const isLoading = useRef(false);
|
||||
|
||||
const fetchCategories = async () => {
|
||||
// If already loading, skip this fetch
|
||||
if (isLoading.current) return;
|
||||
|
||||
isLoading.current = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/categories/popular/${noCategories}/${categoryOffset}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch categories");
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
setHasMoreData(false);
|
||||
return [];
|
||||
}
|
||||
const [categories, setCategories] = useState<CategoryType[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const [categoryOffset, setCategoryOffset] = useState(0);
|
||||
const [noCategories, setNoCategories] = useState(12);
|
||||
const [hasMoreData, setHasMoreData] = useState(true);
|
||||
|
||||
setCategoryOffset(prev => prev + data.length);
|
||||
const listRowRef = useRef<any>(null);
|
||||
const isLoading = useRef(false);
|
||||
|
||||
const processedCategories = data.map((category: any) => ({
|
||||
type: "category" as const,
|
||||
id: category.category_id,
|
||||
title: category.category_name,
|
||||
viewers: category.num_viewers,
|
||||
thumbnail: getCategoryThumbnail(category.category_name)
|
||||
}));
|
||||
const fetchCategories = async () => {
|
||||
// If already loading, skip this fetch
|
||||
if (isLoading.current) return;
|
||||
|
||||
setCategories(prev => [...prev, ...processedCategories]);
|
||||
return processedCategories;
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return [];
|
||||
} finally {
|
||||
isLoading.current = false;
|
||||
}
|
||||
};
|
||||
isLoading.current = true;
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
}, []);
|
||||
try {
|
||||
const response = await fetch(`/api/categories/popular/${noCategories}/${categoryOffset}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch categories");
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
const loadOnScroll = async () => {
|
||||
if (hasMoreData && listRowRef.current) {
|
||||
const newCategories = await fetchCategories();
|
||||
if (newCategories?.length > 0) {
|
||||
listRowRef.current.addMoreItems(newCategories);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (data.length === 0) {
|
||||
setHasMoreData(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
fetchContentOnScroll(loadOnScroll, hasMoreData);
|
||||
setCategoryOffset((prev) => prev + data.length);
|
||||
|
||||
if (hasMoreData && !categories.length) return <LoadingScreen />;
|
||||
const processedCategories = data.map((category: any) => ({
|
||||
type: "category" as const,
|
||||
id: category.category_id,
|
||||
title: category.category_name,
|
||||
viewers: category.num_viewers,
|
||||
thumbnail: getCategoryThumbnail(category.category_name),
|
||||
}));
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
console.log(categoryName);
|
||||
navigate(`/category/${categoryName}`);
|
||||
};
|
||||
setCategories((prev) => [...prev, ...processedCategories]);
|
||||
return processedCategories;
|
||||
} catch (error) {
|
||||
console.error("Error fetching categories:", error);
|
||||
return [];
|
||||
} finally {
|
||||
isLoading.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DynamicPageContent
|
||||
className="min-h-screen"
|
||||
>
|
||||
<ListRow
|
||||
ref={listRowRef}
|
||||
type="category"
|
||||
title="All Categories"
|
||||
items={categories}
|
||||
onItemClick={handleCategoryClick}
|
||||
extraClasses="bg-[var(--recommend)] text-center"
|
||||
itemExtraClasses="w-[20vw]"
|
||||
wrap={true}
|
||||
/>
|
||||
{!hasMoreData && !categories.length && (
|
||||
<div className="text-center text-gray-500 p-4">
|
||||
No more categories to load
|
||||
</div>
|
||||
)}
|
||||
</DynamicPageContent>
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchCategories();
|
||||
}, []);
|
||||
|
||||
const loadOnScroll = async () => {
|
||||
if (hasMoreData && listRowRef.current) {
|
||||
const newCategories = await fetchCategories();
|
||||
if (newCategories?.length > 0) {
|
||||
listRowRef.current.addMoreItems(newCategories);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fetchContentOnScroll(loadOnScroll, hasMoreData);
|
||||
|
||||
if (hasMoreData && !categories.length) return <LoadingScreen />;
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
navigate(`/category/${categoryName}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<DynamicPageContent className="min-h-screen">
|
||||
<ListRow
|
||||
ref={listRowRef}
|
||||
type="category"
|
||||
title="All Categories"
|
||||
items={categories}
|
||||
onItemClick={handleCategoryClick}
|
||||
extraClasses="bg-[var(--recommend)] text-center"
|
||||
itemExtraClasses="w-[20vw]"
|
||||
wrap={true}
|
||||
/>
|
||||
{!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;
|
||||
|
||||
@@ -11,124 +11,113 @@ import { StreamType } from "../types/StreamType";
|
||||
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||
|
||||
const CategoryPage: React.FC = () => {
|
||||
const { categoryName } = useParams<{ categoryName: string }>();
|
||||
const [streams, setStreams] = useState<StreamType[]>([]);
|
||||
const listRowRef = useRef<any>(null);
|
||||
const isLoading = useRef(false);
|
||||
const [streamOffset, setStreamOffset] = useState(0);
|
||||
const [noStreams, setNoStreams] = useState(12);
|
||||
const [hasMoreData, setHasMoreData] = useState(true);
|
||||
const { isLoggedIn } = useAuth();
|
||||
const {
|
||||
isCategoryFollowing,
|
||||
checkCategoryFollowStatus,
|
||||
followCategory,
|
||||
unfollowCategory,
|
||||
} = useCategoryFollow();
|
||||
const { categoryName } = useParams<{ categoryName: string }>();
|
||||
const [streams, setStreams] = useState<StreamType[]>([]);
|
||||
const listRowRef = useRef<any>(null);
|
||||
const isLoading = useRef(false);
|
||||
const [streamOffset, setStreamOffset] = useState(0);
|
||||
const [noStreams, setNoStreams] = useState(12);
|
||||
const [hasMoreData, setHasMoreData] = useState(true);
|
||||
const { isLoggedIn } = useAuth();
|
||||
const { isCategoryFollowing, checkCategoryFollowStatus, followCategory, unfollowCategory } = useCategoryFollow();
|
||||
|
||||
useEffect(() => {
|
||||
if (categoryName) checkCategoryFollowStatus(categoryName);
|
||||
}, [categoryName]);
|
||||
useEffect(() => {
|
||||
if (categoryName) checkCategoryFollowStatus(categoryName);
|
||||
}, [categoryName]);
|
||||
|
||||
const fetchCategoryStreams = async () => {
|
||||
// If already loading, skip this fetch
|
||||
if (isLoading.current) return;
|
||||
const fetchCategoryStreams = async () => {
|
||||
// If already loading, skip this fetch
|
||||
if (isLoading.current) return;
|
||||
|
||||
isLoading.current = true;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}`
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch category streams");
|
||||
}
|
||||
const data = await response.json();
|
||||
isLoading.current = true;
|
||||
try {
|
||||
const response = await fetch(`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch category streams");
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
setHasMoreData(false);
|
||||
return [];
|
||||
}
|
||||
if (data.length === 0) {
|
||||
setHasMoreData(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
setStreamOffset((prev) => prev + data.length);
|
||||
setStreamOffset((prev) => prev + data.length);
|
||||
|
||||
const processedStreams = data.map((stream: any) => ({
|
||||
type: "stream",
|
||||
id: stream.user_id,
|
||||
title: stream.title,
|
||||
username: stream.username,
|
||||
streamCategory: categoryName,
|
||||
viewers: stream.num_viewers,
|
||||
thumbnail: getCategoryThumbnail(categoryName, stream.thumbnail),
|
||||
}));
|
||||
const processedStreams = data.map((stream: any) => ({
|
||||
type: "stream",
|
||||
id: stream.user_id,
|
||||
title: stream.title,
|
||||
username: stream.username,
|
||||
streamCategory: categoryName,
|
||||
viewers: stream.num_viewers,
|
||||
thumbnail: getCategoryThumbnail(categoryName, stream.thumbnail),
|
||||
}));
|
||||
|
||||
setStreams((prev) => [...prev, ...processedStreams]);
|
||||
return processedStreams;
|
||||
} catch (error) {
|
||||
console.error("Error fetching category streams:", error);
|
||||
} finally {
|
||||
isLoading.current = false;
|
||||
}
|
||||
};
|
||||
setStreams((prev) => [...prev, ...processedStreams]);
|
||||
return processedStreams;
|
||||
} catch (error) {
|
||||
console.error("Error fetching category streams:", error);
|
||||
} finally {
|
||||
isLoading.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategoryStreams();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fetchCategoryStreams();
|
||||
}, []);
|
||||
|
||||
const loadOnScroll = async () => {
|
||||
if (hasMoreData && listRowRef.current) {
|
||||
const newStreams = await fetchCategoryStreams();
|
||||
if (newStreams?.length > 0) {
|
||||
listRowRef.current.addMoreItems(newStreams);
|
||||
} else console.log("No more data to fetch");
|
||||
}
|
||||
};
|
||||
const loadOnScroll = async () => {
|
||||
if (hasMoreData && listRowRef.current) {
|
||||
const newStreams = await fetchCategoryStreams();
|
||||
if (newStreams?.length > 0) {
|
||||
listRowRef.current.addMoreItems(newStreams);
|
||||
} else console.log("No more data to fetch");
|
||||
}
|
||||
};
|
||||
|
||||
fetchContentOnScroll(loadOnScroll, hasMoreData);
|
||||
fetchContentOnScroll(loadOnScroll, hasMoreData);
|
||||
|
||||
const handleStreamClick = (streamerName: string) => {
|
||||
window.location.href = `/${streamerName}`;
|
||||
};
|
||||
const handleStreamClick = (streamerName: string) => {
|
||||
window.location.href = `/${streamerName}`;
|
||||
};
|
||||
|
||||
if (hasMoreData && !streams.length) return <LoadingScreen />;
|
||||
if (hasMoreData && !streams.length) return <LoadingScreen />;
|
||||
|
||||
return (
|
||||
<DynamicPageContent className="min-h-screen">
|
||||
<div className="pt-8">
|
||||
<ListRow
|
||||
ref={listRowRef}
|
||||
type="stream"
|
||||
title={`${categoryName}`}
|
||||
description={`Live streams in the ${categoryName} category`}
|
||||
items={streams}
|
||||
wrap={true}
|
||||
onItemClick={handleStreamClick}
|
||||
extraClasses="bg-[var(--recommend)]"
|
||||
itemExtraClasses="w-[20vw]"
|
||||
>
|
||||
{isLoggedIn && (
|
||||
<Button
|
||||
extraClasses="absolute right-10"
|
||||
onClick={() => {
|
||||
if (categoryName) {
|
||||
isCategoryFollowing
|
||||
? unfollowCategory(categoryName)
|
||||
: followCategory(categoryName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCategoryFollowing ? "Unfollow" : "Follow"}
|
||||
</Button>
|
||||
)}
|
||||
</ListRow>
|
||||
</div>
|
||||
return (
|
||||
<DynamicPageContent className="min-h-screen">
|
||||
<div className="pt-8">
|
||||
<ListRow
|
||||
ref={listRowRef}
|
||||
type="stream"
|
||||
title={`${categoryName}`}
|
||||
description={`Live streams in the ${categoryName} category`}
|
||||
items={streams}
|
||||
wrap={true}
|
||||
onItemClick={handleStreamClick}
|
||||
extraClasses="bg-[var(--recommend)]"
|
||||
itemExtraClasses="w-[20vw]"
|
||||
>
|
||||
{isLoggedIn && (
|
||||
<Button
|
||||
extraClasses="absolute right-10"
|
||||
onClick={() => {
|
||||
if (categoryName) {
|
||||
isCategoryFollowing ? unfollowCategory(categoryName) : followCategory(categoryName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCategoryFollowing ? "Unfollow" : "Follow"}
|
||||
</Button>
|
||||
)}
|
||||
</ListRow>
|
||||
</div>
|
||||
|
||||
{streams.length === 0 && !isLoading && (
|
||||
<div className="text-white text-center text-2xl mt-8">
|
||||
No live streams found in this category
|
||||
</div>
|
||||
)}
|
||||
</DynamicPageContent>
|
||||
);
|
||||
{streams.length === 0 && !isLoading && (
|
||||
<div className="text-white text-center text-2xl mt-8">No live streams found in this category</div>
|
||||
)}
|
||||
</DynamicPageContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoryPage;
|
||||
|
||||
Reference in New Issue
Block a user