REFACTOR: Clean code, don't show category name on categoryPage, fix path, remove console logs
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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`}
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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);
|
|
||||||
|
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]);
|
}, [isLoggedIn, customUrl]);
|
||||||
|
|
||||||
return { categories, isLoading, error };
|
return { categories, isLoading, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVods(customUrl?: string): {
|
export function useVods(customUrl?: string): {
|
||||||
vods: VodType[];
|
vods: VodType[];
|
||||||
|
|||||||
@@ -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 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 [];
|
|
||||||
}
|
|
||||||
|
|
||||||
setCategoryOffset(prev => prev + data.length);
|
const listRowRef = useRef<any>(null);
|
||||||
|
const isLoading = useRef(false);
|
||||||
|
|
||||||
const processedCategories = data.map((category: any) => ({
|
const fetchCategories = async () => {
|
||||||
type: "category" as const,
|
// If already loading, skip this fetch
|
||||||
id: category.category_id,
|
if (isLoading.current) return;
|
||||||
title: category.category_name,
|
|
||||||
viewers: category.num_viewers,
|
|
||||||
thumbnail: getCategoryThumbnail(category.category_name)
|
|
||||||
}));
|
|
||||||
|
|
||||||
setCategories(prev => [...prev, ...processedCategories]);
|
isLoading.current = true;
|
||||||
return processedCategories;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching categories:", error);
|
|
||||||
return [];
|
|
||||||
} finally {
|
|
||||||
isLoading.current = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
try {
|
||||||
fetchCategories();
|
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 (data.length === 0) {
|
||||||
if (hasMoreData && listRowRef.current) {
|
setHasMoreData(false);
|
||||||
const newCategories = await fetchCategories();
|
return [];
|
||||||
if (newCategories?.length > 0) {
|
}
|
||||||
listRowRef.current.addMoreItems(newCategories);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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) => {
|
setCategories((prev) => [...prev, ...processedCategories]);
|
||||||
console.log(categoryName);
|
return processedCategories;
|
||||||
navigate(`/category/${categoryName}`);
|
} catch (error) {
|
||||||
};
|
console.error("Error fetching categories:", error);
|
||||||
|
return [];
|
||||||
|
} finally {
|
||||||
|
isLoading.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<DynamicPageContent
|
fetchCategories();
|
||||||
className="min-h-screen"
|
}, []);
|
||||||
>
|
|
||||||
<ListRow
|
const loadOnScroll = async () => {
|
||||||
ref={listRowRef}
|
if (hasMoreData && listRowRef.current) {
|
||||||
type="category"
|
const newCategories = await fetchCategories();
|
||||||
title="All Categories"
|
if (newCategories?.length > 0) {
|
||||||
items={categories}
|
listRowRef.current.addMoreItems(newCategories);
|
||||||
onItemClick={handleCategoryClick}
|
}
|
||||||
extraClasses="bg-[var(--recommend)] text-center"
|
}
|
||||||
itemExtraClasses="w-[20vw]"
|
};
|
||||||
wrap={true}
|
|
||||||
/>
|
fetchContentOnScroll(loadOnScroll, hasMoreData);
|
||||||
{!hasMoreData && !categories.length && (
|
|
||||||
<div className="text-center text-gray-500 p-4">
|
if (hasMoreData && !categories.length) return <LoadingScreen />;
|
||||||
No more categories to load
|
|
||||||
</div>
|
const handleCategoryClick = (categoryName: string) => {
|
||||||
)}
|
navigate(`/category/${categoryName}`);
|
||||||
</DynamicPageContent>
|
};
|
||||||
);
|
|
||||||
|
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";
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user