From 6ba9539fcb1de38a97bc4a905282c3138ee278d2 Mon Sep 17 00:00:00 2001 From: white <122345776@umail.ucc.ie> Date: Wed, 26 Feb 2025 20:40:04 +0000 Subject: [PATCH 01/15] UPDATE: moved admin database usage to admin_utils --- web_server/blueprints/admin.py | 16 ++++---------- web_server/utils/admin_utils.py | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 12 deletions(-) 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/utils/admin_utils.py b/web_server/utils/admin_utils.py index e69de29..61c1d42 100644 --- a/web_server/utils/admin_utils.py +++ b/web_server/utils/admin_utils.py @@ -0,0 +1,39 @@ +from database.database import Database + +def check_if_admin(username): + # Create a connection to the database + db = Database() + db.create_connection() + + is_admin = db.fetchone(""" + SELECT is_admin + FROM users + WHERE username = ?; + """, (username,)) + + return is_admin + +def check_if_user_exists(banned_user): + # Create a connection to the database + db = Database() + db.create_connection() + + user_exists = db.fetchone(""" + SELECT user_id + FROM users + WHERE username = ?;""", + (banned_user,)) + + return user_exists + +def ban_user(banned_user): + """Ban a user.""" + # Create a connection to the database + db = Database() + db.create_connection() + + db.execute(""" + DELETE FROM users + WHERE username = ?;""", + (banned_user) + ) \ No newline at end of file From 3f95b35accc1320ad3365c090b1807e72d1d45ba Mon Sep 17 00:00:00 2001 From: JustIceO7 Date: Thu, 27 Feb 2025 00:37:19 +0000 Subject: [PATCH 02/15] CHANGE: Modified admin utils to use with clause --- .../BrightnessControl.tsx | 0 .../Screenshot.tsx | 0 web_server/utils/admin_utils.py | 63 +++++++++---------- 3 files changed, 31 insertions(+), 32 deletions(-) rename frontend/src/components/{functionality => Functionality}/BrightnessControl.tsx (100%) rename frontend/src/components/{functionality => Functionality}/Screenshot.tsx (100%) diff --git a/frontend/src/components/functionality/BrightnessControl.tsx b/frontend/src/components/Functionality/BrightnessControl.tsx similarity index 100% rename from frontend/src/components/functionality/BrightnessControl.tsx rename to frontend/src/components/Functionality/BrightnessControl.tsx diff --git a/frontend/src/components/functionality/Screenshot.tsx b/frontend/src/components/Functionality/Screenshot.tsx similarity index 100% rename from frontend/src/components/functionality/Screenshot.tsx rename to frontend/src/components/Functionality/Screenshot.tsx diff --git a/web_server/utils/admin_utils.py b/web_server/utils/admin_utils.py index 61c1d42..1e6999a 100644 --- a/web_server/utils/admin_utils.py +++ b/web_server/utils/admin_utils.py @@ -1,39 +1,38 @@ from database.database import Database def check_if_admin(username): - # Create a connection to the database - db = Database() - db.create_connection() - - is_admin = db.fetchone(""" - SELECT is_admin - FROM users - WHERE username = ?; - """, (username,)) - - return is_admin + """ + 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): - # Create a connection to the database - db = Database() - db.create_connection() - - user_exists = db.fetchone(""" - SELECT user_id - FROM users - WHERE username = ?;""", - (banned_user,)) - - return user_exists + """ + 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): - """Ban a user.""" - # Create a connection to the database - db = Database() - db.create_connection() - - db.execute(""" - DELETE FROM users - WHERE username = ?;""", - (banned_user) - ) \ No newline at end of file + """ + Bans a user + """ + with Database() as db: + db.execute(""" + DELETE FROM users + WHERE username = ?;""", + (banned_user) + ) \ No newline at end of file From 74baa49c04917ef8a0fcac988b373dce5474a228 Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Thu, 27 Feb 2025 01:20:40 +0000 Subject: [PATCH 03/15] UPDATE/REFACTOR: Replace `ContentContext` with `useContent` hook; REFACTOR: Add content type files; --- frontend/src/App.tsx | 3 - frontend/src/context/ContentContext.tsx | 145 ------------------------ frontend/src/hooks/useContent.ts | 140 +++++++++++++++++++++++ frontend/src/pages/CategoryPage.tsx | 6 +- frontend/src/pages/Following.tsx | 1 + frontend/src/pages/HomePage.tsx | 14 +-- frontend/src/types/CategoryType.ts | 8 ++ frontend/src/types/StreamType.ts | 9 ++ frontend/src/types/UserType.ts | 10 ++ 9 files changed, 178 insertions(+), 158 deletions(-) delete mode 100644 frontend/src/context/ContentContext.tsx create mode 100644 frontend/src/hooks/useContent.ts create mode 100644 frontend/src/types/CategoryType.ts create mode 100644 frontend/src/types/StreamType.ts create mode 100644 frontend/src/types/UserType.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b4631d7..0017039 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from "react"; import { AuthContext } from "./context/AuthContext"; -import { ContentProvider } from "./context/ContentContext"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import HomePage from "./pages/HomePage"; import StreamerRoute from "./components/Stream/StreamerRoute"; @@ -57,7 +56,6 @@ function App() { setUserId, }} > - @@ -105,7 +103,6 @@ function App() { - ); 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/useContent.ts b/frontend/src/hooks/useContent.ts new file mode 100644 index 0000000..4a5d0b4 --- /dev/null +++ b/frontend/src/hooks/useContent.ts @@ -0,0 +1,140 @@ +// 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"; + +// 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: + stream.thumbnail || + `/images/category_thumbnails/${stream.category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`, + })); +}; + +const processCategoryData = (data: any[]): CategoryType[] => { + return 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`, + })); +}; + +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/CategoryPage.tsx b/frontend/src/pages/CategoryPage.tsx index 6488bca..a87ad07 100644 --- a/frontend/src/pages/CategoryPage.tsx +++ b/frontend/src/pages/CategoryPage.tsx @@ -6,12 +6,12 @@ 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"; 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,7 +50,7 @@ const CategoryPage: React.FC = () => { setStreamOffset((prev) => prev + data.length); - const processedStreams: StreamData[] = data.map((stream: any) => ({ + const processedStreams: StreamType[] = data.map((stream: any) => ({ type: "stream", id: stream.user_id, title: stream.title, diff --git a/frontend/src/pages/Following.tsx b/frontend/src/pages/Following.tsx index bbf9fce..012b30e 100644 --- a/frontend/src/pages/Following.tsx +++ b/frontend/src/pages/Following.tsx @@ -4,6 +4,7 @@ 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 { diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 71b464f..90c331c 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,7 +1,7 @@ import React from "react"; import ListRow from "../components/Layout/ListRow"; import { useNavigate } from "react-router-dom"; -import { useStreams, useCategories } from "../context/ContentContext"; +import { useStreams, useCategories } from "../hooks/useContent"; import Button from "../components/Input/Button"; import DynamicPageContent from "../components/Layout/DynamicPageContent"; import LoadingScreen from "../components/Layout/LoadingScreen"; @@ -12,8 +12,8 @@ interface HomePageProps { } const HomePage: React.FC = ({ variant = "default" }) => { - const { streams } = useStreams(); - const { categories } = useCategories(); + const { streams, isLoading: isLoadingStreams } = useStreams(); + const { categories, isLoading: isLoadingCategories } = useCategories(); const navigate = useNavigate(); const handleStreamClick = (streamerName: string) => { @@ -24,9 +24,9 @@ const HomePage: React.FC = ({ variant = "default" }) => { navigate(`/category/${categoryName}`); }; - if (!categories || categories.length === 0) { - console.log("No categories found yet"); - return Loading Categories...; + if (isLoadingStreams || isLoadingCategories) { + console.log("No content found yet"); + return Loading Content...; } return ( @@ -79,7 +79,7 @@ const HomePage: React.FC = ({ variant = "default" }) => { Show More -