UPDATE: Extract category thumbnail handling to separate function
This commit is contained in:
@@ -4,6 +4,7 @@ import { useAuth } from "../context/AuthContext";
|
|||||||
import { StreamType } from "../types/StreamType";
|
import { StreamType } from "../types/StreamType";
|
||||||
import { CategoryType } from "../types/CategoryType";
|
import { CategoryType } from "../types/CategoryType";
|
||||||
import { UserType } from "../types/UserType";
|
import { UserType } from "../types/UserType";
|
||||||
|
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||||
|
|
||||||
// Helper function to process API data into our consistent types
|
// Helper function to process API data into our consistent types
|
||||||
const processStreamData = (data: any[]): StreamType[] => {
|
const processStreamData = (data: any[]): StreamType[] => {
|
||||||
@@ -14,12 +15,8 @@ const processStreamData = (data: any[]): StreamType[] => {
|
|||||||
username: stream.username,
|
username: stream.username,
|
||||||
streamCategory: stream.category_name,
|
streamCategory: stream.category_name,
|
||||||
viewers: stream.num_viewers,
|
viewers: stream.num_viewers,
|
||||||
thumbnail:
|
thumbnail: getCategoryThumbnail(stream.category_name, stream.thumbnail),
|
||||||
stream.thumbnail ||
|
}))
|
||||||
`/images/category_thumbnails/${stream.category_name
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "_")}.webp`,
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const processCategoryData = (data: any[]): CategoryType[] => {
|
const processCategoryData = (data: any[]): CategoryType[] => {
|
||||||
@@ -28,9 +25,7 @@ const processCategoryData = (data: any[]): CategoryType[] => {
|
|||||||
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: `/images/category_thumbnails/${category.category_name
|
thumbnail: getCategoryThumbnail(category.category_name)
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "_")}.webp`,
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import DynamicPageContent from "../components/Layout/DynamicPageContent";
|
|||||||
import { fetchContentOnScroll } from "../hooks/fetchContentOnScroll";
|
import { fetchContentOnScroll } from "../hooks/fetchContentOnScroll";
|
||||||
import LoadingScreen from "../components/Layout/LoadingScreen";
|
import LoadingScreen from "../components/Layout/LoadingScreen";
|
||||||
import { CategoryType } from "../types/CategoryType";
|
import { CategoryType } from "../types/CategoryType";
|
||||||
|
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||||
|
|
||||||
const AllCategoriesPage: React.FC = () => {
|
const AllCategoriesPage: React.FC = () => {
|
||||||
const [categories, setCategories] = useState<CategoryType[]>([]);
|
const [categories, setCategories] = useState<CategoryType[]>([]);
|
||||||
@@ -41,9 +42,7 @@ const AllCategoriesPage: React.FC = () => {
|
|||||||
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: `/images/category_thumbnails/${category.category_name
|
thumbnail: getCategoryThumbnail(category.category_name)
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "_")}.webp`,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setCategories(prev => [...prev, ...processedCategories]);
|
setCategories(prev => [...prev, ...processedCategories]);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useAuth } from "../context/AuthContext";
|
|||||||
import { useCategoryFollow } from "../hooks/useCategoryFollow";
|
import { useCategoryFollow } from "../hooks/useCategoryFollow";
|
||||||
import LoadingScreen from "../components/Layout/LoadingScreen";
|
import LoadingScreen from "../components/Layout/LoadingScreen";
|
||||||
import { StreamType } from "../types/StreamType";
|
import { StreamType } from "../types/StreamType";
|
||||||
|
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||||
|
|
||||||
const CategoryPage: React.FC = () => {
|
const CategoryPage: React.FC = () => {
|
||||||
const { categoryName } = useParams<{ categoryName: string }>();
|
const { categoryName } = useParams<{ categoryName: string }>();
|
||||||
@@ -57,12 +58,7 @@ const CategoryPage: React.FC = () => {
|
|||||||
username: stream.username,
|
username: stream.username,
|
||||||
streamCategory: categoryName,
|
streamCategory: categoryName,
|
||||||
viewers: stream.num_viewers,
|
viewers: stream.num_viewers,
|
||||||
thumbnail:
|
thumbnail: getCategoryThumbnail(categoryName, stream.thumbnail),
|
||||||
stream.thumbnail ||
|
|
||||||
(categoryName &&
|
|
||||||
`/images/category_thumbnails/${categoryName
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "_")}.webp`),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setStreams((prev) => [...prev, ...processedStreams]);
|
setStreams((prev) => [...prev, ...processedStreams]);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Button from "../components/Input/Button";
|
|||||||
import { useCategoryFollow } from "../hooks/useCategoryFollow";
|
import { useCategoryFollow } from "../hooks/useCategoryFollow";
|
||||||
import { ListItemProps as StreamData } from "../components/Layout/ListItem";
|
import { ListItemProps as StreamData } from "../components/Layout/ListItem";
|
||||||
import LoadingScreen from "../components/Layout/LoadingScreen";
|
import LoadingScreen from "../components/Layout/LoadingScreen";
|
||||||
|
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
isFollowing: any;
|
isFollowing: any;
|
||||||
@@ -130,7 +131,7 @@ const FollowedCategories: React.FC<FollowedCategoryProps> = ({ extraClasses = ""
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={`/images/category_thumbnails/${category.category_name.toLowerCase().replace(/ /g, "_")}.webp`}
|
src={getCategoryThumbnail(category.category_name)}
|
||||||
alt={category.category_name}
|
alt={category.category_name}
|
||||||
className="w-full h-28 object-cover"
|
className="w-full h-28 object-cover"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import Button from "../components/Input/Button";
|
|
||||||
import SearchBar from "../components/Input/SearchBar";
|
import SearchBar from "../components/Input/SearchBar";
|
||||||
import ListRow from "../components/Layout/ListRow";
|
import ListRow from "../components/Layout/ListRow";
|
||||||
import DynamicPageContent from "../components/Layout/DynamicPageContent";
|
import DynamicPageContent from "../components/Layout/DynamicPageContent";
|
||||||
|
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||||
|
|
||||||
const ResultsPage: React.FC = ({ }) => {
|
const ResultsPage: React.FC = ({ }) => {
|
||||||
const [overflow, setOverflow] = useState(false);
|
const [overflow, setOverflow] = useState(false);
|
||||||
@@ -65,9 +65,7 @@ const ResultsPage: React.FC = ({ }) => {
|
|||||||
type: "category",
|
type: "category",
|
||||||
title: category.category_name,
|
title: category.category_name,
|
||||||
viewers: 0,
|
viewers: 0,
|
||||||
thumbnail: `/images/category_thumbnails/${category.category_name
|
thumbnail: getCategoryThumbnail(category.category_name),
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "_")}.webp`,
|
|
||||||
}))}
|
}))}
|
||||||
title="Categories"
|
title="Categories"
|
||||||
onItemClick={(category_name: string) =>
|
onItemClick={(category_name: string) =>
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ import DynamicPageContent from "../components/Layout/DynamicPageContent";
|
|||||||
import Button from "../components/Input/Button";
|
import Button from "../components/Input/Button";
|
||||||
import Input from "../components/Input/Input";
|
import Input from "../components/Input/Input";
|
||||||
import { useCategories } from "../hooks/useContent";
|
import { useCategories } from "../hooks/useContent";
|
||||||
import { X as CloseIcon, Eye as ShowIcon, EyeOff as HideIcon } from "lucide-react";
|
import {
|
||||||
|
X as CloseIcon,
|
||||||
|
Eye as ShowIcon,
|
||||||
|
EyeOff as HideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import VideoPlayer from "../components/Stream/VideoPlayer";
|
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";
|
||||||
|
|
||||||
interface StreamData {
|
interface StreamData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -31,7 +36,9 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
const [streamDetected, setStreamDetected] = useState(false);
|
const [streamDetected, setStreamDetected] = useState(false);
|
||||||
const [timeStarted, setTimeStarted] = useState("");
|
const [timeStarted, setTimeStarted] = useState("");
|
||||||
const [isCategoryFocused, setIsCategoryFocused] = useState(false);
|
const [isCategoryFocused, setIsCategoryFocused] = useState(false);
|
||||||
const [filteredCategories, setFilteredCategories] = useState<CategoryType[]>([]);
|
const [filteredCategories, setFilteredCategories] = useState<CategoryType[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
const [thumbnail, setThumbnail] = useState<File | null>(null);
|
const [thumbnail, setThumbnail] = useState<File | null>(null);
|
||||||
const [thumbnailPreview, setThumbnailPreview] = useState<{
|
const [thumbnailPreview, setThumbnailPreview] = useState<{
|
||||||
url: string;
|
url: string;
|
||||||
@@ -40,10 +47,10 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
const [debouncedCheck, setDebouncedCheck] = useState<Function | null>(null);
|
const [debouncedCheck, setDebouncedCheck] = useState<Function | null>(null);
|
||||||
const [showKey, setShowKey] = useState(false);
|
const [showKey, setShowKey] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
categories,
|
categories,
|
||||||
isLoading: categoriesLoading,
|
isLoading: categoriesLoading,
|
||||||
error: categoriesError
|
error: categoriesError,
|
||||||
} = useCategories("/api/categories/popular/100");
|
} = useCategories("/api/categories/popular/100");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,9 +67,7 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (isValidCategory && !thumbnailPreview.isCustom) {
|
if (isValidCategory && !thumbnailPreview.isCustom) {
|
||||||
const defaultThumbnail = `/images/thumbnails/categories/${categoryName
|
const defaultThumbnail = getCategoryThumbnail(categoryName);
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "_")}.webp`;
|
|
||||||
setThumbnailPreview({ url: defaultThumbnail, isCustom: false });
|
setThumbnailPreview({ url: defaultThumbnail, isCustom: false });
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
@@ -82,7 +87,7 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
|
|
||||||
const checkStreamStatus = async () => {
|
const checkStreamStatus = async () => {
|
||||||
if (!username) return;
|
if (!username) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/user/${username}/status`);
|
const response = await fetch(`/api/user/${username}/status`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -179,9 +184,7 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
console.log(
|
console.log(
|
||||||
"Clearing thumbnail as category is set and default category thumbnail will be used"
|
"Clearing thumbnail as category is set and default category thumbnail will be used"
|
||||||
);
|
);
|
||||||
const defaultThumbnail = `/images/thumbnails/categories/${streamData.category_name
|
const defaultThumbnail = getCategoryThumbnail(streamData.category_name);
|
||||||
.toLowerCase()
|
|
||||||
.replace(/ /g, "_")}.webp`;
|
|
||||||
setThumbnailPreview({ url: defaultThumbnail, isCustom: false });
|
setThumbnailPreview({ url: defaultThumbnail, isCustom: false });
|
||||||
} else {
|
} else {
|
||||||
setThumbnailPreview({ url: "", isCustom: false });
|
setThumbnailPreview({ url: "", isCustom: false });
|
||||||
@@ -194,8 +197,7 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
streamData.category_name.trim() !== "" &&
|
streamData.category_name.trim() !== "" &&
|
||||||
categories.some(
|
categories.some(
|
||||||
(cat) =>
|
(cat) =>
|
||||||
cat.title.toLowerCase() ===
|
cat.title.toLowerCase() === streamData.category_name.toLowerCase()
|
||||||
streamData.category_name.toLowerCase()
|
|
||||||
) &&
|
) &&
|
||||||
streamDetected
|
streamDetected
|
||||||
);
|
);
|
||||||
@@ -328,9 +330,7 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
key={category.title}
|
key={category.title}
|
||||||
className="px-4 py-2 hover:bg-gray-600 cursor-pointer text-white"
|
className="px-4 py-2 hover:bg-gray-600 cursor-pointer text-white"
|
||||||
onClick={() =>
|
onClick={() => handleCategorySelect(category.title)}
|
||||||
handleCategorySelect(category.title)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{category.title}
|
{category.title}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import DynamicPageContent from "../components/Layout/DynamicPageContent";
|
|||||||
import LoadingScreen from "../components/Layout/LoadingScreen";
|
import LoadingScreen from "../components/Layout/LoadingScreen";
|
||||||
import { StreamListItem } from "../components/Layout/ListItem";
|
import { StreamListItem } from "../components/Layout/ListItem";
|
||||||
import { CameraIcon } from "lucide-react";
|
import { CameraIcon } from "lucide-react";
|
||||||
|
import { getCategoryThumbnail } from "../utils/thumbnailUtils";
|
||||||
|
|
||||||
interface UserProfileData {
|
interface UserProfileData {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -78,11 +79,10 @@ const UserPage: React.FC = () => {
|
|||||||
currentStreamCategory: streamData.category_id,
|
currentStreamCategory: streamData.category_id,
|
||||||
currentStreamViewers: streamData.num_viewers,
|
currentStreamViewers: streamData.num_viewers,
|
||||||
currentStreamStartTime: streamData.start_time,
|
currentStreamStartTime: streamData.start_time,
|
||||||
currentStreamThumbnail:
|
currentStreamThumbnail: getCategoryThumbnail(
|
||||||
streamData.thumbnail ||
|
streamData.category_name,
|
||||||
`/images/category_thumbnails/${streamData.category_name
|
streamData.thumbnail
|
||||||
.toLowerCase()
|
),
|
||||||
.replace(/ /g, "_")}.webp`,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let variant: "user" | "streamer" | "personal" | "admin";
|
let variant: "user" | "streamer" | "personal" | "admin";
|
||||||
|
|||||||
24
frontend/src/utils/thumbnailUtils.ts
Normal file
24
frontend/src/utils/thumbnailUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Generates a thumbnail path for a given category name
|
||||||
|
*
|
||||||
|
* @param categoryName - The name of the category
|
||||||
|
* @param customThumbnail - Optional custom thumbnail path that takes precedence if provided
|
||||||
|
* @returns The path to the category thumbnail image
|
||||||
|
*/
|
||||||
|
export function getCategoryThumbnail(categoryName?: string, customThumbnail?: string): string {
|
||||||
|
if (customThumbnail) {
|
||||||
|
return customThumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!categoryName) {
|
||||||
|
return '/images/category_thumbnails/default.webp';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to lowercase, replace spaces with underscores, and remove all other special characters
|
||||||
|
const formattedName = categoryName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, '_') // Replace spaces with underscores
|
||||||
|
.replace(/[^a-z0-9_]/g, ''); // Remove all other non-alphanumeric characters except underscores
|
||||||
|
|
||||||
|
return `/images/category_thumbnails/${formattedName}.webp`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user