+ {/* List Details */}
- {/* List Details */}
-
-
handleTitleClick(type) : undefined}
- >
- {title}
-
-
{description}
-
+ {title}
+
+
{description}
+
- {/* List Items */}
-
- {!wrap && currentItems.length > amountForScroll && (
+ {/* List Items */}
+
+ {!wrap && currentItems.length > amountForScroll && (
+ <>
+
+
+ >
+ )}
+
+
+ {currentItems.length === 0 ? (
+
Nothing Found
+ ) : (
<>
-
-
+ {currentItems.map((item) => {
+ if (type === "stream" && isStreamType(item)) {
+ return (
+
onItemClick(item.username)}
+ extraClasses={itemExtraClasses}
+ />
+ );
+ } else if (type === "category" && isCategoryType(item)) {
+ return (
+ onItemClick(item.title)}
+ extraClasses={itemExtraClasses}
+ />
+ );
+ } else if (type === "user" && isUserType(item)) {
+ return (
+ onItemClick(item.username)}
+ extraClasses={itemExtraClasses}
+ />
+ );
+ }
+ return null;
+ })}
>
)}
-
-
- {currentItems.length === 0 ? (
-
Nothing Found
- ) : (
- <>
- {currentItems.map((item) => (
-
- (item.type === "stream" || item.type === "user") &&
- item.username
- ? onItemClick?.(item.username)
- : onItemClick?.(item.title)
- }
- extraClasses={`${itemExtraClasses}`}
- />
- ))}
- >
- )}
-
- {children}
- );
- }
-);
+ {children}
+
+ );
+});
-export default ListRow;
+export default ListRow;
\ No newline at end of file
diff --git a/frontend/src/components/Navigation/Navbar.tsx b/frontend/src/components/Navigation/Navbar.tsx
index 85ba348..3e91eb6 100644
--- a/frontend/src/components/Navigation/Navbar.tsx
+++ b/frontend/src/components/Navigation/Navbar.tsx
@@ -2,9 +2,9 @@ import React, { useEffect } from "react";
import Logo from "../Layout/Logo";
import Button, { ToggleButton } from "../Input/Button";
import {
- LogIn as LogInIcon,
- LogOut as LogOutIcon,
- Settings as SettingsIcon,
+ LogInIcon,
+ LogOutIcon,
+ SettingsIcon,
Radio as LiveIcon,
} from "lucide-react";
import SearchBar from "../Input/SearchBar";
diff --git a/frontend/src/components/Navigation/Sidebar.tsx b/frontend/src/components/Navigation/Sidebar.tsx
index 67bc78d..da7e580 100644
--- a/frontend/src/components/Navigation/Sidebar.tsx
+++ b/frontend/src/components/Navigation/Sidebar.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
-import { Sidebar as SidebarIcon } from "lucide-react";
+import { SidebarIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../../context/AuthContext";
import { useSidebar } from "../../context/SidebarContext";
diff --git a/frontend/src/components/Settings/ThemeSetting.tsx b/frontend/src/components/Settings/ThemeSetting.tsx
index ef9fb16..69440c2 100644
--- a/frontend/src/components/Settings/ThemeSetting.tsx
+++ b/frontend/src/components/Settings/ThemeSetting.tsx
@@ -1,38 +1,38 @@
import React from "react";
-import { Sun, Moon, Droplet, Leaf, Flame } from "lucide-react";
+import { SunIcon, MoonIcon, DropletIcon, LeafIcon, FlameIcon } from "lucide-react";
import { useTheme } from "../../context/ThemeContext";
const themeConfig = {
light: {
- icon: Sun,
+ icon: SunIcon,
color: "text-yellow-400",
background: "bg-white",
hoverBg: "hover:bg-gray-100",
label: "Light Theme",
},
dark: {
- icon: Moon,
+ icon: MoonIcon,
color: "text-white",
background: "bg-gray-800",
hoverBg: "hover:bg-gray-700",
label: "Dark Theme",
},
blue: {
- icon: Droplet,
+ icon: DropletIcon,
color: "text-blue-500",
background: "bg-blue-50",
hoverBg: "hover:bg-blue-100",
label: "Blue Theme",
},
green: {
- icon: Leaf,
+ icon: LeafIcon,
color: "text-green-500",
background: "bg-green-50",
hoverBg: "hover:bg-green-100",
label: "Green Theme",
},
orange: {
- icon: Flame,
+ icon: FlameIcon,
color: "text-orange-500",
background: "bg-orange-50",
hoverBg: "hover:bg-orange-100",
diff --git a/frontend/src/components/Stream/ChatPanel.tsx b/frontend/src/components/Stream/ChatPanel.tsx
index 526e5e4..13d8149 100644
--- a/frontend/src/components/Stream/ChatPanel.tsx
+++ b/frontend/src/components/Stream/ChatPanel.tsx
@@ -6,7 +6,7 @@ import { useAuthModal } from "../../hooks/useAuthModal";
import { useAuth } from "../../context/AuthContext";
import { useSocket } from "../../context/SocketContext";
import { useChat } from "../../context/ChatContext";
-import { ArrowLeftFromLineIcon, ArrowRightFromLine } from "lucide-react";
+import { ArrowLeftFromLineIcon, ArrowRightFromLineIcon } from "lucide-react";
interface ChatMessage {
chatter_username: string;
@@ -149,7 +149,7 @@ const ChatPanel: React.FC
= ({
onClick={toggleChat}
className={`group cursor-pointer p-2 hover:bg-gray-800 rounded-md absolute top-[1vh] left-[1vw] ${showChat ? "" : "delay-[0.75s] -translate-x-[3.3vw]"} text-[1rem] text-purple-500 flex items-center flex-nowrap z-[50] duration-[0.3s] transition-all`}
>
- {showChat ? : }
+ {showChat ? : }
Press C
diff --git a/frontend/src/context/ContentContext.tsx b/frontend/src/context/ContentContext.tsx
deleted file mode 100644
index 442245f..0000000
--- a/frontend/src/context/ContentContext.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-import { createContext, useContext, useState, useEffect } from "react";
-import { useAuth } from "./AuthContext";
-
-// Base interfaces
-interface Item {
- id: number;
- title: string;
- viewers: number;
- thumbnail?: string;
-}
-
-interface StreamItem extends Item {
- type: "stream";
- username: string;
- streamCategory: string;
-}
-
-interface CategoryItem extends Item {
- type: "category";
-}
-
-interface UserItem extends Item {
- type: "user";
- username: string;
- isLive: boolean;
-}
-
-// Context type
-interface ContentContextType {
- streams: StreamItem[];
- categories: CategoryItem[];
- users: UserItem[];
- setStreams: (streams: StreamItem[]) => void;
- setCategories: (categories: CategoryItem[]) => void;
- setUsers: (users: UserItem[]) => void;
-}
-
-const ContentContext = createContext(undefined);
-
-export function ContentProvider({ children }: { children: React.ReactNode }) {
- const [streams, setStreams] = useState([]);
- const [categories, setCategories] = useState([]);
- const [users, setUsers] = useState([]);
- const { isLoggedIn } = useAuth();
-
- useEffect(() => {
- // Fetch streams
- const streamsUrl = isLoggedIn
- ? "/api/streams/recommended"
- : "/api/streams/popular/4";
-
- fetch(streamsUrl)
- .then((response) => response.json())
- .then((data: any[]) => {
- const processedStreams: StreamItem[] = data.map((stream) => ({
- type: "stream",
- id: stream.user_id,
- title: stream.title,
- username: stream.username,
- streamCategory: stream.category_name,
- viewers: stream.num_viewers,
- thumbnail:
- stream.thumbnail ||
- `/images/category_thumbnails/${stream.category_name
- .toLowerCase()
- .replace(/ /g, "_")}.webp`,
- }));
- setStreams(processedStreams);
- });
-
- // Fetch categories
- const categoriesUrl = isLoggedIn
- ? "/api/categories/recommended"
- : "/api/categories/popular/4";
- console.log("Fetching categories from", categoriesUrl);
-
- fetch(categoriesUrl)
- .then((response) => response.json())
- .then((data: any[]) => {
- const processedCategories: CategoryItem[] = data.map((category) => ({
- type: "category",
- id: category.category_id,
- title: category.category_name,
- viewers: category.num_viewers,
- thumbnail: `/images/category_thumbnails/${category.category_name
- .toLowerCase()
- .replace(/ /g, "_")}.webp`,
- }));
- setCategories(processedCategories);
- console.log("Categories fetched", processedCategories);
- });
- }, [isLoggedIn]);
-
- return (
-
- {children}
-
- );
-}
-
-// Custom hooks for specific content types
-export function useStreams() {
- const context = useContext(ContentContext);
- if (!context) {
- throw new Error("useStreams must be used within a ContentProvider");
- }
- return { streams: context.streams, setStreams: context.setStreams };
-}
-
-export function useCategories() {
- const context = useContext(ContentContext);
- if (!context) {
- throw new Error("useCategories must be used within a ContentProvider");
- }
- return {
- categories: context.categories,
- setCategories: context.setCategories,
- };
-}
-
-export function useUsers() {
- const context = useContext(ContentContext);
- if (!context) {
- throw new Error("useUsers must be used within a ContentProvider");
- }
- return { users: context.users, setUsers: context.setUsers };
-}
-
-// General hook for all content
-export function useContent() {
- const context = useContext(ContentContext);
- if (!context) {
- throw new Error("useContent must be used within a ContentProvider");
- }
- return context;
-}
diff --git a/frontend/src/hooks/fetchContentOnScroll.ts b/frontend/src/hooks/fetchContentOnScroll.ts
index 520dac3..d11a5c5 100644
--- a/frontend/src/hooks/fetchContentOnScroll.ts
+++ b/frontend/src/hooks/fetchContentOnScroll.ts
@@ -2,20 +2,29 @@ import { useEffect } from "react";
export function fetchContentOnScroll(callback: () => void, hasMoreData: boolean) {
useEffect(() => {
+ const root = document.querySelector("#root") as HTMLElement;
+
const handleScroll = () => {
if (!hasMoreData) return; // Don't trigger scroll if no more data
- const scrollPosition = window.innerHeight + document.documentElement.scrollTop;
- const scrollHeight = document.documentElement.scrollHeight;
+
+ // Use properties of the element itself, not document
+ const scrollPosition = root.scrollTop + root.clientHeight;
+ const scrollHeight = root.scrollHeight;
if (scrollPosition >= scrollHeight * 0.9) {
callback(); // Trigger data fetching when 90% scroll is reached
+ setTimeout(() => {
+ // Delay to prevent multiple fetches
+ root.scrollTop = root.scrollTop - 1;
+ }, 100);
}
};
- window.addEventListener("scroll", handleScroll);
+ // Add scroll event listener to the root element
+ root.addEventListener("scroll", handleScroll);
return () => {
- window.removeEventListener("scroll", handleScroll); // Cleanup on unmount
+ root.removeEventListener("scroll", handleScroll); // Cleanup on unmount
};
}, [callback, hasMoreData]);
}
\ No newline at end of file
diff --git a/frontend/src/hooks/useContent.ts b/frontend/src/hooks/useContent.ts
new file mode 100644
index 0000000..5e99aaf
--- /dev/null
+++ b/frontend/src/hooks/useContent.ts
@@ -0,0 +1,135 @@
+// hooks/useContent.ts
+import { useState, useEffect } from "react";
+import { useAuth } from "../context/AuthContext";
+import { StreamType } from "../types/StreamType";
+import { CategoryType } from "../types/CategoryType";
+import { UserType } from "../types/UserType";
+import { getCategoryThumbnail } from "../utils/thumbnailUtils";
+
+// Helper function to process API data into our consistent types
+const processStreamData = (data: any[]): StreamType[] => {
+ return data.map((stream) => ({
+ type: "stream",
+ id: stream.user_id,
+ title: stream.title,
+ username: stream.username,
+ streamCategory: stream.category_name,
+ viewers: stream.num_viewers,
+ thumbnail: getCategoryThumbnail(stream.category_name, stream.thumbnail),
+ }))
+};
+
+const processCategoryData = (data: any[]): CategoryType[] => {
+ return data.map((category) => ({
+ type: "category",
+ id: category.category_id,
+ title: category.category_name,
+ viewers: category.num_viewers,
+ thumbnail: getCategoryThumbnail(category.category_name)
+ }));
+};
+
+const processUserData = (data: any[]): UserType[] => {
+ return data.map((user) => ({
+ type: "user",
+ id: user.user_id,
+ title: user.username,
+ username: user.username,
+ isLive: user.is_live,
+ viewers: 0, // This may need to be updated based on your API
+ thumbnail: user.thumbnail || "/images/pfps/default.webp",
+ }));
+};
+
+// Generic fetch hook that can be used for any content type
+export function useFetchContent(
+ url: string,
+ processor: (data: any[]) => T[],
+ dependencies: any[] = []
+): { data: T[]; isLoading: boolean; error: string | null } {
+ const [data, setData] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`Error fetching data: ${response.status}`);
+ }
+
+ const rawData = await response.json();
+ const processedData = processor(rawData);
+ setData(processedData);
+ setError(null);
+ } catch (err) {
+ console.error("Error fetching content:", err);
+ setError(err instanceof Error ? err.message : "Unknown error");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, dependencies);
+
+ return { data, isLoading, error };
+}
+
+// Specific hooks for each content type
+export function useStreams(customUrl?: string): {
+ streams: StreamType[];
+ isLoading: boolean;
+ error: string | null
+} {
+ const { isLoggedIn } = useAuth();
+ const url = customUrl || (isLoggedIn
+ ? "/api/streams/recommended"
+ : "/api/streams/popular/4");
+
+ const { data, isLoading, error } = useFetchContent(
+ url,
+ processStreamData,
+ [isLoggedIn, customUrl]
+ );
+
+ return { streams: data, isLoading, error };
+}
+
+export function useCategories(customUrl?: string): {
+ categories: CategoryType[];
+ isLoading: boolean;
+ error: string | null
+} {
+ const { isLoggedIn } = useAuth();
+ const url = customUrl || (isLoggedIn
+ ? "/api/categories/recommended"
+ : "/api/categories/popular/4");
+
+ const { data, isLoading, error } = useFetchContent(
+ url,
+ processCategoryData,
+ [isLoggedIn, customUrl]
+ );
+
+ return { categories: data, isLoading, error };
+}
+
+export function useUsers(customUrl?: string): {
+ users: UserType[];
+ isLoading: boolean;
+ error: string | null
+} {
+ const url = customUrl || "/api/users/popular";
+
+ const { data, isLoading, error } = useFetchContent(
+ url,
+ processUserData,
+ [customUrl]
+ );
+
+ return { users: data, isLoading, error };
+}
\ No newline at end of file
diff --git a/frontend/src/pages/AllCategoriesPage.tsx b/frontend/src/pages/AllCategoriesPage.tsx
index d75aaa2..26fffcb 100644
--- a/frontend/src/pages/AllCategoriesPage.tsx
+++ b/frontend/src/pages/AllCategoriesPage.tsx
@@ -4,59 +4,54 @@ import ListRow from "../components/Layout/ListRow";
import DynamicPageContent from "../components/Layout/DynamicPageContent";
import { fetchContentOnScroll } from "../hooks/fetchContentOnScroll";
import LoadingScreen from "../components/Layout/LoadingScreen";
+import { CategoryType } from "../types/CategoryType";
+import { getCategoryThumbnail } from "../utils/thumbnailUtils";
-interface CategoryData {
- type: "category";
- id: number;
- title: string;
- viewers: number;
- thumbnail: string;
-}
const AllCategoriesPage: React.FC = () => {
- const [categories, setCategories] = useState([]);
+ const [categories, setCategories] = useState([]);
const navigate = useNavigate();
const [categoryOffset, setCategoryOffset] = useState(0);
const [noCategories, setNoCategories] = useState(12);
const [hasMoreData, setHasMoreData] = useState(true);
- const [isLoading, setIsLoading] = useState(true);
+
const listRowRef = useRef(null);
-
+ const isLoading = useRef(false);
+
const fetchCategories = async () => {
- if (isLoading) return;
-
+ // If already loading, skip this fetch
+ if (isLoading.current) return;
+
+ isLoading.current = true;
+
try {
- const response = await fetch(
- `/api/categories/popular/${noCategories}/${categoryOffset}`
- );
+ 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);
+ setCategoryOffset(prev => prev + data.length);
const processedCategories = data.map((category: any) => ({
type: "category" as const,
id: category.category_id,
title: category.category_name,
viewers: category.num_viewers,
- thumbnail: `/images/category_thumbnails/${category.category_name
- .toLowerCase()
- .replace(/ /g, "_")}.webp`,
+ thumbnail: getCategoryThumbnail(category.category_name)
}));
- setCategories((prev) => [...prev, ...processedCategories]);
+ setCategories(prev => [...prev, ...processedCategories]);
return processedCategories;
} catch (error) {
console.error("Error fetching categories:", error);
return [];
} finally {
- setIsLoading(false);
+ isLoading.current = false;
}
};
@@ -83,19 +78,27 @@ const AllCategoriesPage: React.FC = () => {
};
return (
-
+
+ {!hasMoreData && !categories.length && (
+
+ No more categories to load
+
+ )}
);
};
-export default AllCategoriesPage;
+export default AllCategoriesPage;
\ No newline at end of file
diff --git a/frontend/src/pages/CategoryPage.tsx b/frontend/src/pages/CategoryPage.tsx
index 6488bca..8562c72 100644
--- a/frontend/src/pages/CategoryPage.tsx
+++ b/frontend/src/pages/CategoryPage.tsx
@@ -6,12 +6,13 @@ import { fetchContentOnScroll } from "../hooks/fetchContentOnScroll";
import Button from "../components/Input/Button";
import { useAuth } from "../context/AuthContext";
import { useCategoryFollow } from "../hooks/useCategoryFollow";
-import { ListItemProps as StreamData } from "../components/Layout/ListItem";
import LoadingScreen from "../components/Layout/LoadingScreen";
+import { StreamType } from "../types/StreamType";
+import { getCategoryThumbnail } from "../utils/thumbnailUtils";
const CategoryPage: React.FC = () => {
const { categoryName } = useParams<{ categoryName: string }>();
- const [streams, setStreams] = useState([]);
+ const [streams, setStreams] = useState([]);
const listRowRef = useRef(null);
const isLoading = useRef(false);
const [streamOffset, setStreamOffset] = useState(0);
@@ -50,19 +51,14 @@ const CategoryPage: React.FC = () => {
setStreamOffset((prev) => prev + data.length);
- const processedStreams: StreamData[] = data.map((stream: any) => ({
+ 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:
- stream.thumbnail ||
- (categoryName &&
- `/images/category_thumbnails/${categoryName
- .toLowerCase()
- .replace(/ /g, "_")}.webp`),
+ thumbnail: getCategoryThumbnail(categoryName, stream.thumbnail),
}));
setStreams((prev) => [...prev, ...processedStreams]);
@@ -78,16 +74,16 @@ const CategoryPage: React.FC = () => {
fetchCategoryStreams();
}, []);
- const logOnScroll = async () => {
+ const loadOnScroll = async () => {
if (hasMoreData && listRowRef.current) {
- const newCategories = await fetchCategoryStreams();
- if (newCategories && newCategories.length > 0) {
- listRowRef.current.addMoreItems(newCategories);
+ const newStreams = await fetchCategoryStreams();
+ if (newStreams?.length > 0) {
+ listRowRef.current.addMoreItems(newStreams);
} else console.log("No more data to fetch");
}
};
- fetchContentOnScroll(logOnScroll, hasMoreData);
+ fetchContentOnScroll(loadOnScroll, hasMoreData);
const handleStreamClick = (streamerName: string) => {
window.location.href = `/${streamerName}`;
@@ -99,6 +95,7 @@ const CategoryPage: React.FC = () => {
{
wrap={true}
onItemClick={handleStreamClick}
extraClasses="bg-[var(--recommend)]"
+ itemExtraClasses="w-[20vw]"
>
{isLoggedIn && (
diff --git a/frontend/src/pages/Following.tsx b/frontend/src/pages/Following.tsx
index bbf9fce..0719523 100644
--- a/frontend/src/pages/Following.tsx
+++ b/frontend/src/pages/Following.tsx
@@ -4,125 +4,140 @@ import { useSidebar } from "../context/SidebarContext";
import { ToggleButton } from "../components/Input/Button";
import { Sidebar as SidebarIcon } from "lucide-react";
import { useNavigate } from "react-router-dom"; // Import useNavigate
+import { CategoryType } from "../types/CategoryType";
// Define TypeScript interfaces
interface Streamer {
- user_id: number;
- username: string;
+ user_id: number;
+ username: string;
}
interface FollowingProps {
- extraClasses?: string;
+ extraClasses?: string;
}
const Following: React.FC = ({ extraClasses = "" }) => {
- const { showSideBar, setShowSideBar } = useSidebar();
- const navigate = useNavigate();
- const { username, isLoggedIn } = useAuth();
- const [followedStreamers, setFollowedStreamers] = useState([]);
+ const { showSideBar, setShowSideBar } = useSidebar();
+ const navigate = useNavigate();
+ const { username, isLoggedIn } = useAuth();
+ const [followedStreamers, setFollowedStreamers] = useState([]);
- // Fetch followed streamers
- useEffect(() => {
- const fetchFollowedStreamers = async () => {
- try {
- const response = await fetch("/api/user/following");
- if (!response.ok) throw new Error("Failed to fetch followed streamers");
- const data = await response.json();
- setFollowedStreamers(data.streamers || []);
- } catch (error) {
- console.error("Error fetching followed streamers:", error);
- }
- };
-
- if (isLoggedIn) {
- fetchFollowedStreamers();
- }
- }, [isLoggedIn]);
-
- // Handle sidebar toggle
- const handleSideBar = () => {
- setShowSideBar(!showSideBar);
+ // Fetch followed streamers
+ useEffect(() => {
+ const fetchFollowedStreamers = async () => {
+ try {
+ const response = await fetch("/api/user/following");
+ if (!response.ok) throw new Error("Failed to fetch followed streamers");
+ const data = await response.json();
+ setFollowedStreamers(data.streamers || []);
+ } catch (error) {
+ console.error("Error fetching followed streamers:", error);
+ }
};
- return (
- <>
- {/* Sidebar Toggle Button */}
- {
+ setShowSideBar(!showSideBar);
+ };
+
+ return (
+ <>
+ {/* Sidebar Toggle Button */}
+
+
+
+ {showSideBar && (
+
+ Press S
+
+ )}
+
+
+ {/* Sidebar Container */}
+
@@ -457,8 +458,7 @@ const StreamDashboardPage: React.FC = () => {
List Item
-
{
const { showAuthModal, setShowAuthModal } = useAuthModal();
const { username: loggedInUsername } = useAuth();
const { username } = useParams();
+ const [isUser, setIsUser] = useState(true);
const navigate = useNavigate();
const bgColors = {
personal: "",
- streamer: "bg-gradient-radial from-[#ff00f1] via-[#0400ff] to-[#ff0000]", // offline streamer
- user: "bg-gradient-radial from-[#ff00f1] via-[#0400ff] to-[#ff00f1]",
+ streamer:
+ "bg-gradient-radial from-[rgba(255, 0, 241, 0.5)] via-[rgba(4, 0, 255, 0.5)] to-[rgba(255, 0, 0, 0.5)]", // offline streamer
+ user: "bg-gradient-radial from-[rgba(255, 0, 241, 0.5)] via-[rgba(4, 0, 255, 0.5)] to-[rgba(255, 0, 241, 0.5)]",
admin:
- "bg-gradient-to-r from-[rgb(255,0,0)] via-transparent to-[rgb(0,0,255)]",
+ "bg-gradient-to-r from-[rgba(255,100,100,0.5)] via-transparent to-[rgba(100,100,255,0.5)]",
};
useEffect(() => {
@@ -75,11 +79,10 @@ const UserPage: React.FC = () => {
currentStreamCategory: streamData.category_id,
currentStreamViewers: streamData.num_viewers,
currentStreamStartTime: streamData.start_time,
- currentStreamThumbnail:
- streamData.thumbnail ||
- `/images/category_thumbnails/${streamData.category_name
- .toLowerCase()
- .replace(/ /g, "_")}.webp`,
+ currentStreamThumbnail: getCategoryThumbnail(
+ streamData.category_name,
+ streamData.thumbnail
+ ),
};
});
let variant: "user" | "streamer" | "personal" | "admin";
@@ -105,12 +108,12 @@ const UserPage: React.FC = () => {
return (
-
{/* Profile Section - TOP */}
@@ -138,12 +141,28 @@ const UserPage: React.FC = () => {
rounded-full overflow-hidden flex-shrink-0 border-4 border-[var(--user-pfp-border)] inset-0 z-20"
style={{ boxShadow: "var(--user-pfp-border-shadow)" }}
>
-

+
{/* Username - Now Directly Below PFP */}
@@ -218,10 +237,10 @@ const UserPage: React.FC = () => {
Currently Live!
-
{
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
-
diff --git a/frontend/src/types/CategoryType.ts b/frontend/src/types/CategoryType.ts
new file mode 100644
index 0000000..8b3a47b
--- /dev/null
+++ b/frontend/src/types/CategoryType.ts
@@ -0,0 +1,8 @@
+// types/CategoryType.ts
+export interface CategoryType {
+ type: "category";
+ id: number;
+ title: string;
+ viewers: number;
+ thumbnail?: string;
+ }
\ No newline at end of file
diff --git a/frontend/src/types/StreamType.ts b/frontend/src/types/StreamType.ts
new file mode 100644
index 0000000..c6d4d58
--- /dev/null
+++ b/frontend/src/types/StreamType.ts
@@ -0,0 +1,9 @@
+export interface StreamType {
+ type: "stream";
+ id: number;
+ title: string;
+ username: string;
+ streamCategory: string;
+ viewers: number;
+ thumbnail?: string;
+ }
\ No newline at end of file
diff --git a/frontend/src/types/UserType.ts b/frontend/src/types/UserType.ts
new file mode 100644
index 0000000..7dee37e
--- /dev/null
+++ b/frontend/src/types/UserType.ts
@@ -0,0 +1,10 @@
+// types/UserType.ts
+export interface UserType {
+ type: "user";
+ id: number;
+ title: string;
+ username: string;
+ isLive: boolean;
+ viewers: number;
+ thumbnail?: string;
+ }
\ No newline at end of file
diff --git a/frontend/src/utils/thumbnailUtils.ts b/frontend/src/utils/thumbnailUtils.ts
new file mode 100644
index 0000000..c7e7cc3
--- /dev/null
+++ b/frontend/src/utils/thumbnailUtils.ts
@@ -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`;
+ }
\ No newline at end of file
diff --git a/web_server/blueprints/admin.py b/web_server/blueprints/admin.py
index 9efe9c5..4830b37 100644
--- a/web_server/blueprints/admin.py
+++ b/web_server/blueprints/admin.py
@@ -1,6 +1,6 @@
from flask import Blueprint, session
-from database.database import Database
from utils.utils import sanitize
+from utils.admin_utils import *
admin_bp = Blueprint("admin", __name__)
@@ -9,21 +9,13 @@ def admin_delete_user(banned_user):
# Sanitise the user input
banned_user = sanitize(banned_user)
- # Create a connection to the database
- db = Database()
- db.create_connection()
-
# Check if the user is an admin
username = session.get("username")
- is_admin = db.fetchone("""
- SELECT is_admin
- FROM users
- WHERE username = ?;
- """, (username,))
+ is_admin = check_if_admin(username)
# Check if the user exists
- user_exists = db.fetchone("""SELECT user_id from users WHERE username = ?;""", (banned_user))
+ user_exists = check_if_user_exists(banned_user)
# If the user is an admin, try to delete the account
if is_admin and user_exists:
- db.execute("""DELETE FROM users WHERE username = ?;""", (banned_user))
\ No newline at end of file
+ ban_user(banned_user)
\ No newline at end of file
diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py
index e9a7b56..854ce78 100644
--- a/web_server/blueprints/streams.py
+++ b/web_server/blueprints/streams.py
@@ -215,8 +215,7 @@ def publish_stream():
with Database() as db:
- user_info = db.fetchone("""SELECT user_id, username, current_stream_title,
- current_selected_category_id, is_live
+ user_info = db.fetchone("""SELECT user_id, username, is_live
FROM users
WHERE stream_key = ?""", (data['stream_key'],))
diff --git a/web_server/database/app.db b/web_server/database/app.db
index d11e5fd..58d0e65 100644
Binary files a/web_server/database/app.db and b/web_server/database/app.db differ
diff --git a/web_server/database/testing_data.sql b/web_server/database/testing_data.sql
index 710d14c..e00d4cc 100644
--- a/web_server/database/testing_data.sql
+++ b/web_server/database/testing_data.sql
@@ -1,10 +1,10 @@
-- Sample Data for users
-INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio, is_live, is_admin, current_stream_title, current_selected_category_id) VALUES
-('GamerDude', 'password123', 'gamerdude@example.com', 500, '1234', 0, 'Streaming my gaming adventures!', 1, 0, 'Game On!', 1),
-('MusicLover', 'music4life', 'musiclover@example.com', 1200, '2345', 0, 'I share my favorite tunes.', 1, 0, 'Live Music Jam', 2),
-('ArtFan', 'artistic123', 'artfan@example.com', 300, '3456', 0, 'Exploring the world of art.', 1, 0, 'Sketching Live', 3),
-('EduGuru', 'learn123', 'eduguru@example.com', 800, '4567', 0, 'Teaching everything I know.', 1, 0, 'Math Made Easy', 4),
-('SportsStar', 'sports123', 'sportsstar@example.com', 2000, '5678', 0, 'Join me for live sports updates!', 1, 0, 'Sports Highlights', 5);
+INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio, is_live, is_admin) VALUES
+('GamerDude', 'password123', 'gamerdude@example.com', 500, '1234', 0, 'Streaming my gaming adventures!', 1, 0),
+('MusicLover', 'music4life', 'musiclover@example.com', 1200, '2345', 0, 'I share my favorite tunes.', 1, 0),
+('ArtFan', 'artistic123', 'artfan@example.com', 300, '3456', 0, 'Exploring the world of art.', 1, 0),
+('EduGuru', 'learn123', 'eduguru@example.com', 800, '4567', 0, 'Teaching everything I know.', 1, 0),
+('SportsStar', 'sports123', 'sportsstar@example.com', 2000, '5678', 0, 'Join me for live sports updates!', 1, 0);
INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio, is_live, is_admin) VALUES
('GamerDude2', 'password123', 'gamerdude3@gmail.com', 3200, '7890', 0, 'Streaming my gaming adventures!', 0, 0),
@@ -45,35 +45,35 @@ INSERT INTO followed_categories (user_id, category_id) VALUES
(5, 5);
-- Sample Data for categories
-INSERT INTO categories (category_name) VALUES
-('Gaming'),
-('Music'),
-('Art'),
-('Education'),
-('Sports'),
-('League of Legends'),
-('Fortnite'),
-('Minecraft'),
-('Call of Duty'),
-('Counter-Strike 2'),
-('Valorant'),
-('Dota 2'),
-('Apex Legends'),
-('Grand Theft Auto V'),
-('The Legend of Zelda Breath of the Wild'),
-('Elden Ring'),
-('Red Dead Redemption 2'),
-('Cyberpunk 2077'),
-('Super Smash Bros Ultimate'),
-('Overwatch 2'),
-('Genshin Impact'),
-('World of Warcraft'),
-('Rocket League'),
-('FIFA 24'),
-('The Sims 4'),
-('Among Us'),
-('Dead by Daylight'),
-('Hogwarts Legacy');
+INSERT INTO categories (category_name) VALUES
+('Gaming'),
+('Music'),
+('Art'),
+('Education'),
+('Sports'),
+('League of Legends'),
+('Fortnite'),
+('Minecraft'),
+('Call of Duty'),
+('Counter-Strike 2'),
+('Valorant'),
+('Dota 2'),
+('Apex Legends'),
+('Grand Theft Auto V'),
+('The Legend of Zelda: Tears of the Kingdom'),
+('Elden Ring'),
+('Red Dead Redemption 2'),
+('Cyberpunk 2077'),
+('Super Smash Bros. Ultimate'),
+('Overwatch 2'),
+('Genshin Impact'),
+('World of Warcraft'),
+('Rocket League'),
+('EA Sports FC 25'),
+('The Sims 4'),
+('Among Us'),
+('Dead by Daylight'),
+('Hogwarts Legacy');
-- Sample Data for streams
diff --git a/web_server/database/users.sql b/web_server/database/users.sql
index 5fae6c9..47e7a0d 100644
--- a/web_server/database/users.sql
+++ b/web_server/database/users.sql
@@ -10,10 +10,7 @@ CREATE TABLE users
is_partnered BOOLEAN NOT NULL DEFAULT 0,
is_live BOOLEAN NOT NULL DEFAULT 0,
bio VARCHAR(1024) DEFAULT 'This user does not have a Bio.',
- is_admin BOOLEAN NOT NULL DEFAULT 0,
-
- current_stream_title VARCHAR(100) DEFAULT 'Stream',
- current_selected_category_id INTEGER DEFAULT 1
+ is_admin BOOLEAN NOT NULL DEFAULT 0
);
SELECT * FROM users;
diff --git a/web_server/utils/admin_utils.py b/web_server/utils/admin_utils.py
index e69de29..1e6999a 100644
--- a/web_server/utils/admin_utils.py
+++ b/web_server/utils/admin_utils.py
@@ -0,0 +1,38 @@
+from database.database import Database
+
+def check_if_admin(username):
+ """
+ Returns whether user is admin
+ """
+ with Database() as db:
+ is_admin = db.fetchone("""
+ SELECT is_admin
+ FROM users
+ WHERE username = ?;
+ """, (username,))
+
+ return bool(is_admin)
+
+def check_if_user_exists(banned_user):
+ """
+ Returns whether user exists
+ """
+ with Database() as db:
+ user_exists = db.fetchone("""
+ SELECT user_id
+ FROM users
+ WHERE username = ?;""",
+ (banned_user,))
+
+ return bool(user_exists)
+
+def ban_user(banned_user):
+ """
+ Bans a user
+ """
+ with Database() as db:
+ db.execute("""
+ DELETE FROM users
+ WHERE username = ?;""",
+ (banned_user)
+ )
\ No newline at end of file