diff --git a/frontend/public/images/category_thumbnails/abstract.webp b/frontend/public/images/category_thumbnails/abstract.webp new file mode 100644 index 0000000..ee0b5f9 Binary files /dev/null and b/frontend/public/images/category_thumbnails/abstract.webp differ diff --git a/frontend/public/images/category_thumbnails/among_us.webp b/frontend/public/images/category_thumbnails/among_us.webp new file mode 100644 index 0000000..ab59121 Binary files /dev/null and b/frontend/public/images/category_thumbnails/among_us.webp differ diff --git a/frontend/public/images/category_thumbnails/art.webp b/frontend/public/images/category_thumbnails/art.webp index ee0b5f9..9bf9991 100644 Binary files a/frontend/public/images/category_thumbnails/art.webp and b/frontend/public/images/category_thumbnails/art.webp differ diff --git a/frontend/public/images/category_thumbnails/counter-strike_2.webp b/frontend/public/images/category_thumbnails/counterstrike_2.webp similarity index 100% rename from frontend/public/images/category_thumbnails/counter-strike_2.webp rename to frontend/public/images/category_thumbnails/counterstrike_2.webp diff --git a/frontend/public/images/category_thumbnails/cyberpunk_2077.webp b/frontend/public/images/category_thumbnails/cyberpunk_2077.webp new file mode 100644 index 0000000..b335180 Binary files /dev/null and b/frontend/public/images/category_thumbnails/cyberpunk_2077.webp differ diff --git a/frontend/public/images/category_thumbnails/dead_by_daylight.webp b/frontend/public/images/category_thumbnails/dead_by_daylight.webp new file mode 100644 index 0000000..3d41b82 Binary files /dev/null and b/frontend/public/images/category_thumbnails/dead_by_daylight.webp differ diff --git a/frontend/public/images/category_thumbnails/default.webp b/frontend/public/images/category_thumbnails/default.webp new file mode 100644 index 0000000..9b7495f Binary files /dev/null and b/frontend/public/images/category_thumbnails/default.webp differ diff --git a/frontend/public/images/category_thumbnails/ea_sports_fc_25.webp b/frontend/public/images/category_thumbnails/ea_sports_fc_25.webp new file mode 100644 index 0000000..99589a9 Binary files /dev/null and b/frontend/public/images/category_thumbnails/ea_sports_fc_25.webp differ diff --git a/frontend/public/images/category_thumbnails/elden_ring.webp b/frontend/public/images/category_thumbnails/elden_ring.webp index caa0d3b..d144147 100644 Binary files a/frontend/public/images/category_thumbnails/elden_ring.webp and b/frontend/public/images/category_thumbnails/elden_ring.webp differ diff --git a/frontend/public/images/category_thumbnails/genshin_impact.webp b/frontend/public/images/category_thumbnails/genshin_impact.webp new file mode 100644 index 0000000..b108693 Binary files /dev/null and b/frontend/public/images/category_thumbnails/genshin_impact.webp differ diff --git a/frontend/public/images/category_thumbnails/hogwarts_legacy.webp b/frontend/public/images/category_thumbnails/hogwarts_legacy.webp new file mode 100644 index 0000000..f809220 Binary files /dev/null and b/frontend/public/images/category_thumbnails/hogwarts_legacy.webp differ diff --git a/frontend/public/images/category_thumbnails/league_of_legends.webp b/frontend/public/images/category_thumbnails/league_of_legends.webp index 9db56a6..821adf7 100644 Binary files a/frontend/public/images/category_thumbnails/league_of_legends.webp and b/frontend/public/images/category_thumbnails/league_of_legends.webp differ diff --git a/frontend/public/images/category_thumbnails/overwatch_2.webp b/frontend/public/images/category_thumbnails/overwatch_2.webp new file mode 100644 index 0000000..f1f206f Binary files /dev/null and b/frontend/public/images/category_thumbnails/overwatch_2.webp differ diff --git a/frontend/public/images/category_thumbnails/red_dead_redemption_2.webp b/frontend/public/images/category_thumbnails/red_dead_redemption_2.webp new file mode 100644 index 0000000..22c0db7 Binary files /dev/null and b/frontend/public/images/category_thumbnails/red_dead_redemption_2.webp differ diff --git a/frontend/public/images/category_thumbnails/rocket_league.webp b/frontend/public/images/category_thumbnails/rocket_league.webp new file mode 100644 index 0000000..e4ace1c Binary files /dev/null and b/frontend/public/images/category_thumbnails/rocket_league.webp differ diff --git a/frontend/public/images/category_thumbnails/super_smash_bros_ultimate.webp b/frontend/public/images/category_thumbnails/super_smash_bros_ultimate.webp new file mode 100644 index 0000000..f5e6f00 Binary files /dev/null and b/frontend/public/images/category_thumbnails/super_smash_bros_ultimate.webp differ diff --git a/frontend/public/images/category_thumbnails/the_legend_of_zelda_breath_of_the_wild.webp b/frontend/public/images/category_thumbnails/the_legend_of_zelda_breath_of_the_wild.webp deleted file mode 100644 index d009a1d..0000000 Binary files a/frontend/public/images/category_thumbnails/the_legend_of_zelda_breath_of_the_wild.webp and /dev/null differ diff --git a/frontend/public/images/category_thumbnails/the_legend_of_zelda_tears_of_the_kingdom.webp b/frontend/public/images/category_thumbnails/the_legend_of_zelda_tears_of_the_kingdom.webp new file mode 100644 index 0000000..457e644 Binary files /dev/null and b/frontend/public/images/category_thumbnails/the_legend_of_zelda_tears_of_the_kingdom.webp differ diff --git a/frontend/public/images/category_thumbnails/the_sims_4.webp b/frontend/public/images/category_thumbnails/the_sims_4.webp new file mode 100644 index 0000000..83ec4ab Binary files /dev/null and b/frontend/public/images/category_thumbnails/the_sims_4.webp differ diff --git a/frontend/public/images/category_thumbnails/valorant.webp b/frontend/public/images/category_thumbnails/valorant.webp index 793233d..2d30dfc 100644 Binary files a/frontend/public/images/category_thumbnails/valorant.webp and b/frontend/public/images/category_thumbnails/valorant.webp differ diff --git a/frontend/public/images/category_thumbnails/world_of_warcraft.webp b/frontend/public/images/category_thumbnails/world_of_warcraft.webp new file mode 100644 index 0000000..6b979e0 Binary files /dev/null and b/frontend/public/images/category_thumbnails/world_of_warcraft.webp differ 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/assets/styles/index.css b/frontend/src/assets/styles/index.css index f223c87..1832a2a 100644 --- a/frontend/src/assets/styles/index.css +++ b/frontend/src/assets/styles/index.css @@ -33,11 +33,11 @@ body[data-theme="light"] { --sideBar-bg: rgb(255, 255, 255); --sideBar-text: black; - --sideBar-profile-bg: rgb(132, 0, 255); + --sideBar-profile-bg: rgb(224, 205, 241); --sideBar-profile-text: #ffffff; --profile-border: #ffffff; - --follow-bg: #ff0000; + --follow-bg: #aa00ff; --follow-text: white; --follow-shadow: 0px 0px 15px rgba(94, 94, 94, 0.754); @@ -45,7 +45,7 @@ body[data-theme="light"] { --recommend: rgba(5, 46, 22, 0.6); --quickBar-title: #ffffff; - --quickBar-title-bg: rgb(132, 0, 255); + --quickBar-title-bg: rgb(183, 149, 214); --quickBar-bg: #ffffff; --quickBar-text: #000000; --quickBar-border: #ffffff; diff --git a/frontend/src/components/Auth/AuthModal.tsx b/frontend/src/components/Auth/AuthModal.tsx index 8b10573..7188c94 100644 --- a/frontend/src/components/Auth/AuthModal.tsx +++ b/frontend/src/components/Auth/AuthModal.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { ToggleButton } from "../Input/Button"; -import { LogIn as LogInIcon, User as UserIcon } from "lucide-react"; +import { LogInIcon, UserIcon } from "lucide-react"; import LoginForm from "./LoginForm"; import RegisterForm from "./RegisterForm"; import ForgotPasswordForm from "./ForgotPasswordForm"; diff --git a/frontend/src/components/Auth/LoginForm.tsx b/frontend/src/components/Auth/LoginForm.tsx index 0a9d88b..23757aa 100644 --- a/frontend/src/components/Auth/LoginForm.tsx +++ b/frontend/src/components/Auth/LoginForm.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import Input from "../Input/Input"; -import Button, { ToggleButton } from "../Input/Button"; +import Button from "../Input/Button"; import { useAuth } from "../../context/AuthContext"; import GoogleLogin from "./OAuth"; import { CircleHelp as ForgotIcon } from "lucide-react"; 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/frontend/src/components/Input/Button.tsx b/frontend/src/components/Input/Button.tsx index a4bee8b..6e64a2c 100644 --- a/frontend/src/components/Input/Button.tsx +++ b/frontend/src/components/Input/Button.tsx @@ -23,23 +23,6 @@ const Button: React.FC = ({ ); }; -interface EditButtonProps extends ButtonProps {} - -export const EditButton: React.FC = ({ - children = "", - extraClasses = "", - onClick, -}) => { - return ( - - ); -}; - interface ToggleButtonProps extends ButtonProps { toggled?: boolean; } diff --git a/frontend/src/components/Input/SearchBar.tsx b/frontend/src/components/Input/SearchBar.tsx index be397c6..708888d 100644 --- a/frontend/src/components/Input/SearchBar.tsx +++ b/frontend/src/components/Input/SearchBar.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import Input from "./Input"; -import { Search as SearchIcon } from "lucide-react"; +import { SearchIcon } from "lucide-react"; import { useNavigate } from "react-router-dom"; interface SearchBarProps { diff --git a/frontend/src/components/Layout/DynamicPageContent.tsx b/frontend/src/components/Layout/DynamicPageContent.tsx index a757462..87f6f45 100644 --- a/frontend/src/components/Layout/DynamicPageContent.tsx +++ b/frontend/src/components/Layout/DynamicPageContent.tsx @@ -1,6 +1,7 @@ import React from "react"; import Navbar from "../Navigation/Navbar"; import { useSidebar } from "../../context/SidebarContext"; +import Footer from "./Footer"; interface DynamicPageContentProps extends React.HTMLProps { children: React.ReactNode; @@ -20,13 +21,16 @@ const DynamicPageContent: React.FC = ({ const { showSideBar } = useSidebar(); return ( -
+
{children}
diff --git a/frontend/src/components/Layout/Footer.tsx b/frontend/src/components/Layout/Footer.tsx index 5669add..9eff8cb 100644 --- a/frontend/src/components/Layout/Footer.tsx +++ b/frontend/src/components/Layout/Footer.tsx @@ -1,11 +1,17 @@ +import { MailIcon } from "lucide-react"; import { useState } from "react"; -import { Mail, Facebook, Twitter, Instagram, Linkedin } from "lucide-react"; const Footer = () => { const [email, setEmail] = useState(""); - const handleKeyDown = async (event) => { + const handleKeyDown = async (event: React.KeyboardEvent) => { if (event.key === "Enter") { + send_newsletter(); + } + } + + const send_newsletter = async () => { + if (email) { if (email.trim() === "") return; try { const response = await fetch(`/api/send_newsletter/${email}`, @@ -26,41 +32,26 @@ const Footer = () => { } catch (error) { console.error("Error subscribing:", error); } - } }; return ( -
+
{/* About Section */}

Gander

-

Your very favourite streaming service

+

+ Your very favourite streaming service +

{/* Office Section */}
-

Some Street

-

On Some Road

-

Near Some Country

-

That is definitely on Earth

-

- info@gander.com -

-

+69-280690345

-
- - {/* Links Section */} - {/* Newsletter Section */} @@ -75,25 +66,17 @@ const Footer = () => { onChange={(e) => setEmail(e.target.value)} onKeyDown={handleKeyDown} /> - handleKeyDown({ key: "Enter" })} /> + +
- {/* Social Icons */} -
- - - - -
+ {/* Footer Bottom */} +
+ Group 11
- - {/* Footer Bottom */} -
- Group 11 -
); }; -export default Footer; +export default Footer; \ No newline at end of file diff --git a/frontend/src/components/Layout/ListItem.tsx b/frontend/src/components/Layout/ListItem.tsx index 4fe6af0..a5a7cf8 100644 --- a/frontend/src/components/Layout/ListItem.tsx +++ b/frontend/src/components/Layout/ListItem.tsx @@ -1,19 +1,18 @@ import React from "react"; +import { StreamType } from "../../types/StreamType"; +import { CategoryType } from "../../types/CategoryType"; +import { UserType } from "../../types/UserType"; -export interface ListItemProps { - type: "stream" | "category" | "user"; - id: number; - title: string; - username?: string; - streamCategory?: string; - viewers: number; - thumbnail?: string; +// Base props that all item types share +interface BaseListItemProps { onItemClick?: () => void; extraClasses?: string; } -const ListItem: React.FC = ({ - type, +// Stream item component +interface StreamListItemProps extends BaseListItemProps, Omit {} + +const StreamListItem: React.FC = ({ title, username, streamCategory, @@ -22,31 +21,6 @@ const ListItem: React.FC = ({ onItemClick, extraClasses = "", }) => { - if (type === "user") { - return ( -
-
- {`user - - - {title.includes("🔴") && ( -

- Currently Live! -

- )} -
-
- ); - } return (
= ({

{title}

- {type === "stream" &&

{username}

} - {type === "stream" && ( -

{streamCategory}

- )} +

{username}

+

{streamCategory}

{viewers} viewers

@@ -79,4 +51,91 @@ const ListItem: React.FC = ({ ); }; -export default ListItem; +// Category item component +interface CategoryListItemProps extends BaseListItemProps, Omit {} + +const CategoryListItem: React.FC = ({ + title, + viewers, + thumbnail, + onItemClick, + extraClasses = "", +}) => { + return ( +
+
+
+ {thumbnail ? ( + {title} + ) : ( +
+ )} +
+
+

+ {title} +

+

{viewers} viewers

+
+
+
+ ); +}; + +// User item component +interface UserListItemProps extends BaseListItemProps, Omit {} + +const UserListItem: React.FC = ({ + title, + username, + isLive, + onItemClick, + extraClasses = "", +}) => { + return ( +
+
+ {`user + + + {isLive && ( +

+ Currently Live! +

+ )} +
+
+ ); +}; + +// Legacy wrapper component for backward compatibility +export interface ListItemProps { + type: "stream" | "category" | "user"; + id: number; + title: string; + username?: string; + streamCategory?: string; + viewers: number; + thumbnail?: string; + onItemClick?: () => void; + extraClasses?: string; + isLive?: boolean; +} + +export { StreamListItem, CategoryListItem, UserListItem }; \ No newline at end of file diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index 5f98440..f61293a 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -1,6 +1,6 @@ import { - ArrowLeft as ArrowLeftIcon, - ArrowRight as ArrowRightIcon, + ArrowLeftIcon, + ArrowRightIcon, } from "lucide-react"; import React, { forwardRef, @@ -10,14 +10,19 @@ import React, { } from "react"; import { useNavigate } from "react-router-dom"; import "../../assets/styles/listRow.css"; -import ListItem, { ListItemProps } from "./ListItem"; +import { StreamListItem, CategoryListItem, UserListItem } from "./ListItem"; +import { StreamType } from "../../types/StreamType"; +import { CategoryType } from "../../types/CategoryType"; +import { UserType } from "../../types/UserType"; + +type ItemType = StreamType | CategoryType | UserType; interface ListRowProps { variant?: "default" | "search"; type: "stream" | "category" | "user"; title?: string; description?: string; - items: ListItemProps[]; + items: ItemType[]; wrap?: boolean; onItemClick: (itemName: string) => void; titleClickable?: boolean; @@ -27,151 +32,179 @@ interface ListRowProps { children?: React.ReactNode; } -const ListRow = forwardRef< - { addMoreItems: (newItems: ListItemProps[]) => void }, - ListRowProps ->( - ( - { - variant = "default", - type, - title = "", - description = "", - items, - onItemClick, - titleClickable = false, - wrap = false, - extraClasses = "", - itemExtraClasses = "", - amountForScroll = 4, - children, - }, - ref - ) => { - const [currentItems, setCurrentItems] = useState(items); - const slider = useRef(null); - const scrollAmount = window.innerWidth * 0.3; - const navigate = useNavigate(); +interface ListRowRef { + addMoreItems: (newItems: ItemType[]) => void; +} - const addMoreItems = (newItems: ListItemProps[]) => { - setCurrentItems((prevItems) => [...prevItems, ...newItems]); - }; +const ListRow = forwardRef((props, ref) => { + const { + variant = "default", + type, + title = "", + description = "", + items, + onItemClick, + titleClickable = false, + wrap = false, + extraClasses = "", + itemExtraClasses = "", + amountForScroll = 4, + children, + } = props; + + const [currentItems, setCurrentItems] = useState(items); + const slider = useRef(null); + const scrollAmount = window.innerWidth * 0.3; + const navigate = useNavigate(); - useImperativeHandle(ref, () => ({ - addMoreItems, - })); + const addMoreItems = (newItems: ItemType[]) => { + setCurrentItems((prevItems) => [...prevItems, ...newItems]); + }; - const slideRight = () => { - if (!wrap && slider.current) { - slider.current.scrollBy({ left: +scrollAmount, behavior: "smooth" }); - } - }; + useImperativeHandle(ref, () => ({ + addMoreItems, + })); - const slideLeft = () => { - if (!wrap && slider.current) { - slider.current.scrollBy({ left: -scrollAmount, behavior: "smooth" }); - } - }; + const slideRight = () => { + if (!wrap && slider.current) { + slider.current.scrollBy({ left: +scrollAmount, behavior: "smooth" }); + } + }; - const handleTitleClick = (type: string) => { - switch (type) { - case "stream": - break; - case "category": - navigate("/categories"); - break; - case "user": - break; - default: - break; - } - }; + const slideLeft = () => { + if (!wrap && slider.current) { + slider.current.scrollBy({ left: -scrollAmount, behavior: "smooth" }); + } + }; - return ( + const handleTitleClick = () => { + switch (type) { + case "stream": + break; + case "category": + navigate("/categories"); + break; + case "user": + break; + default: + break; + } + }; + + const isStreamType = (item: ItemType): item is StreamType => + item.type === "stream"; + + const isCategoryType = (item: ItemType): item is CategoryType => + item.type === "category"; + + const isUserType = (item: ItemType): item is UserType => + item.type === "user"; + + return ( +
+ {/* 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 && ( {category.category_name} 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)" }} > - {`${profileData.username}'s +
{/* 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