FIX: Navigation from ListItems;

REFACTOR: Format all files;
This commit is contained in:
Chris-1010
2025-02-23 22:57:00 +00:00
parent 5c81f58e66
commit a27ee52de1
34 changed files with 387 additions and 255 deletions

View File

@@ -1,5 +1,5 @@
export const paths = {
pfps:'',
category_thumbnails:'',
icons:''
};
pfps: "",
category_thumbnails: "",
icons: "",
};

View File

@@ -36,45 +36,64 @@ function App() {
return (
<Brightness>
<AuthContext.Provider
value={{ isLoggedIn, username, userId, setIsLoggedIn, setUsername, setUserId }}
>
<ContentProvider>
<SidebarProvider>
<QuickSettingsProvider>
<BrowserRouter>
<Routes>
<Route
path="/"
element={
isLoggedIn ? (
<HomePage variant="personalised" />
) : (
<HomePage />
)
}
/>
<Route path="/go-live" element={isLoggedIn ? <StreamDashboardPage /> : <Navigate to="/" replace />} />
<Route path="/:streamerName" element={<StreamerRoute />} />
<Route path="/user/:username" element={<UserPage />} />
<Route
path="/reset_password/:token"
element={<ResetPasswordPage />}
></Route>
<Route
path="/category/:categoryName"
element={<CategoryPage />}
></Route>
<Route path="/categories" element={<CategoriesPage />}></Route>
<Route path="/results" element={<ResultsPage />}></Route>
<Route path="/404" element={<NotFoundPage />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
</BrowserRouter>
</QuickSettingsProvider>
</SidebarProvider>
</ContentProvider>
</AuthContext.Provider>
<AuthContext.Provider
value={{
isLoggedIn,
username,
userId,
setIsLoggedIn,
setUsername,
setUserId,
}}
>
<ContentProvider>
<SidebarProvider>
<QuickSettingsProvider>
<BrowserRouter>
<Routes>
<Route
path="/"
element={
isLoggedIn ? (
<HomePage variant="personalised" />
) : (
<HomePage />
)
}
/>
<Route
path="/go-live"
element={
isLoggedIn ? (
<StreamDashboardPage />
) : (
<Navigate to="/" replace />
)
}
/>
<Route path="/:streamerName" element={<StreamerRoute />} />
<Route path="/user/:username" element={<UserPage />} />
<Route
path="/reset_password/:token"
element={<ResetPasswordPage />}
></Route>
<Route
path="/category/:categoryName"
element={<CategoryPage />}
></Route>
<Route
path="/categories"
element={<CategoriesPage />}
></Route>
<Route path="/results" element={<ResultsPage />}></Route>
<Route path="/404" element={<NotFoundPage />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
</BrowserRouter>
</QuickSettingsProvider>
</SidebarProvider>
</ContentProvider>
</AuthContext.Provider>
</Brightness>
);
}

View File

@@ -1,9 +1,6 @@
import React, { useState } from "react";
import { ToggleButton } from "../Input/Button";
import {
LogIn as LogInIcon,
User as UserIcon,
} from "lucide-react";
import { LogIn as LogInIcon, User as UserIcon } from "lucide-react";
import LoginForm from "./LoginForm";
import RegisterForm from "./RegisterForm";
import ForgotPasswordForm from "./ForgotPasswordForm";
@@ -93,7 +90,10 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
>
</button>
<div id="login-methods" className="w-full flex flex-row items-center justify-evenly">
<div
id="login-methods"
className="w-full flex flex-row items-center justify-evenly"
>
{authSwitch()}
</div>
</div>
@@ -104,4 +104,4 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
);
};
export default AuthModal;
export default AuthModal;

View File

@@ -4,6 +4,7 @@ import Button from "../Input/Button";
interface ForgotPasswordProps {
email?: string;
general?: string;
}
interface SubmitProps {
@@ -51,7 +52,9 @@ const ForgotPasswordForm: React.FC<SubmitProps> = ({ onSubmit }) => {
if (!response.ok) {
const data = await response.json();
throw new Error(data.message || "An error has occurred while resetting");
throw new Error(
data.message || "An error has occurred while resetting"
);
} else {
confirmPasswordReset();
}
@@ -68,8 +71,10 @@ const ForgotPasswordForm: React.FC<SubmitProps> = ({ onSubmit }) => {
return (
<div className="mb-2">
<div className="flex flex-col items-center p-[2.5rem]">
<h1 className="text-white text-[1.5em] font-[800] md:text-[1.75em] lg:text-[2em]">Forgot Password</h1>
<div className="mt-10 bg-white/10 backdrop-blur-md p-6 rounded-xl shadow-lg w-full max-w-[10em] min-w-[14em] border border-white/10 sm:max-w-[16em] md:max-w-[18em] lg:max-w-[20em]">
<h1 className="text-white text-[1.5em] font-[800] md:text-[1.75em] lg:text-[2em]">
Forgot Password
</h1>
<div className="mt-10 bg-white/10 backdrop-blur-md p-6 rounded-xl shadow-lg w-full max-w-[10em] min-w-[14em] border border-white/10 sm:max-w-[16em] md:max-w-[18em] lg:max-w-[20em]">
<form
onSubmit={handleSubmit}
id="forgot-password-form"
@@ -93,7 +98,9 @@ const ForgotPasswordForm: React.FC<SubmitProps> = ({ onSubmit }) => {
placeholder="Enter your email"
value={email}
onChange={handleEmailChange}
extraClasses={`w-full mb-[1.5em] p-[0.5rem] ${errors.email ? "border-red-500" : ""}`}
extraClasses={`w-full mb-[1.5em] p-[0.5rem] ${
errors.email ? "border-red-500" : ""
}`}
/>
</div>
<Button type="submit">Send Link</Button>

View File

@@ -102,9 +102,10 @@ const LoginForm: React.FC<SubmitProps> = ({ onSubmit, onForgotPassword }) => {
return (
<>
<div className="flex flex-col items-center p-10">
<h1 className="flex flex-col text-white text-[1.5em] font-[800] md:text-[1.75em] lg:text-[2em]">Login</h1>
<h1 className="flex flex-col text-white text-[1.5em] font-[800] md:text-[1.75em] lg:text-[2em]">
Login
</h1>
<div className="mt-10 bg-white/10 backdrop-blur-md p-6 rounded-xl shadow-lg w-full max-w-[10em] min-w-[14em] border border-white/10 sm:max-w-[16em] md:max-w-[18em] lg:max-w-[20em]">
<form
onSubmit={handleSubmit}
id="login-form"

View File

@@ -20,7 +20,9 @@ export default function GoogleLogin() {
alt="Google logo"
className="w-[2em] h-[2em] mr-2"
/>
<span className="flex-grow text-[0.6em] lx:text-[0.75em] 2lg:text-[1em]">Sign in with Google</span>
<span className="flex-grow text-[0.6em] lx:text-[0.75em] 2lg:text-[1em]">
Sign in with Google
</span>
</button>
</div>
</div>

View File

@@ -21,7 +21,7 @@ export const Return: React.FC = () => {
const sessionId = urlParams.get("session_id");
if (sessionId) {
console.log("1")
console.log("1");
fetch(`/api/session-status?session_id=${sessionId}`)
.then((res) => res.json())
.then((data) => {

View File

@@ -23,8 +23,7 @@ const Button: React.FC<ButtonProps> = ({
);
};
interface EditButtonProps extends ButtonProps {
}
interface EditButtonProps extends ButtonProps {}
export const EditButton: React.FC<EditButtonProps> = ({
children = "",
@@ -39,7 +38,7 @@ export const EditButton: React.FC<EditButtonProps> = ({
{children}
</button>
);
};
};
interface ToggleButtonProps extends ButtonProps {
toggled?: boolean;

View File

@@ -11,28 +11,26 @@ const Input: React.FC<InputProps> = ({
placeholder = "",
value = "",
extraClasses = "",
onChange = () => { },
onKeyDown = () => { },
onChange = () => {},
onKeyDown = () => {},
children,
...props // all other HTML input props
}) => {
return (
<>
<div className="flex flex-col items-center">
<input
name={name}
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
{...props}
className={`${extraClasses} relative p-2 rounded-[1rem] w-[20vw] focus:w-[22vw] bg-black/40 border border-gray-300 focus:border-purple-500 focus:outline-purple-500 text-center text-white text-xl transition-all`}
/>
</div>
<div className="flex flex-col items-center">
<input
name={name}
type={type}
placeholder={placeholder}
value={value}
onChange={onChange}
onKeyDown={onKeyDown}
{...props}
className={`${extraClasses} relative p-2 rounded-[1rem] w-[20vw] focus:w-[22vw] bg-black/40 border border-gray-300 focus:border-purple-500 focus:outline-purple-500 text-center text-white text-xl transition-all`}
/>
</div>
</>
);
};

View File

@@ -9,22 +9,27 @@ interface DynamicPageContentProps {
style?: React.CSSProperties;
}
const DynamicPageContent: React.FC<DynamicPageContentProps> = ({
children,
const DynamicPageContent: React.FC<DynamicPageContentProps> = ({
children,
navbarVariant = "default",
className = "",
style
style,
}) => {
const { showSideBar } = useSidebar();
return (
<div className={className} style={style}>
<Navbar variant={navbarVariant} />
<div id="content" className={`${showSideBar ? "w-[85vw] translate-x-[15vw]" : "w-[100vw]"} transition-all duration-[500ms] ease-in-out`}>
<div
id="content"
className={`${
showSideBar ? "w-[85vw] translate-x-[15vw]" : "w-[100vw]"
} transition-all duration-[500ms] ease-in-out`}
>
{children}
</div>
</div>
);
};
export default DynamicPageContent;
export default DynamicPageContent;

View File

@@ -65,7 +65,9 @@ const ListItem: React.FC<ListItemProps> = ({
)}
</div>
<div className="p-3">
<h3 className="font-semibold text-lg text-center truncate max-w-full">{title}</h3>
<h3 className="font-semibold text-lg text-center truncate max-w-full">
{title}
</h3>
{type === "stream" && <p className="font-bold">{username}</p>}
{type === "stream" && (
<p className="text-sm text-gray-300">{streamCategory}</p>

View File

@@ -1,11 +1,16 @@
import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
import {
ArrowLeft as ArrowLeftIcon,
ArrowRight as ArrowRightIcon,
} from "lucide-react";
import React, {
forwardRef,
useImperativeHandle,
useRef,
useState,
} from "react";
import { useNavigate } from "react-router-dom";
import "../../assets/styles/listRow.css";
import ListItem, { ListItemProps } from "./ListItem";
import { useNavigate } from "react-router-dom";
interface ListRowProps {
variant?: "default" | "search";
@@ -14,7 +19,7 @@ interface ListRowProps {
description?: string;
items: ListItemProps[];
wrap?: boolean;
onClick: (itemName: string) => void;
onItemClick: (itemName: string) => void;
titleClickable?: boolean;
extraClasses?: string;
itemExtraClasses?: string;
@@ -22,8 +27,27 @@ interface ListRowProps {
children?: React.ReactNode;
}
const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }, ListRowProps>(
({ variant, type, title = "", description = "", items, wrap, onClick, titleClickable, extraClasses = "", itemExtraClasses = "", amountForScroll, children }, ref) => {
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<HTMLDivElement>(null);
const scrollAmount = window.innerWidth * 0.3;
@@ -79,7 +103,9 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
>
<h2
className={`${
titleClickable ? "cursor-pointer hover:underline" : "cursor-default"
titleClickable
? "cursor-pointer hover:underline"
: "cursor-default"
} text-2xl font-bold`}
onClick={titleClickable ? () => handleTitleClick(type) : undefined}
>
@@ -90,7 +116,7 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
{/* List Items */}
<div className="relative overflow-hidden flex flex-grow items-center z-0">
{!wrap && currentItems.length > (amountForScroll || 0) && (
{!wrap && currentItems.length > amountForScroll && (
<>
<ArrowLeftIcon
onClick={slideLeft}
@@ -126,10 +152,10 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
onItemClick={() =>
(item.type === "stream" || item.type === "user") &&
item.username
? onClick?.(item.username)
: onClick?.(item.title)
? onItemClick?.(item.username)
: onItemClick?.(item.title)
}
extraClasses={`${itemExtraClasses} min-w-[20vw] max-w-[20vw]`}
extraClasses={`${itemExtraClasses} w-[20vw]`}
/>
))}
</div>
@@ -140,4 +166,4 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
}
);
export default ListRow;
export default ListRow;

View File

@@ -145,12 +145,12 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
<SearchBar />
{/* Stream Button */}
{isLoggedIn && !window.location.pathname.includes('go-live') && (
{isLoggedIn && !window.location.pathname.includes("go-live") && (
<Button
extraClasses={`${
variant === "home" ? "absolute top-[2vh] right-[10vw]" : ""
} flex flex-row items-center`}
onClick={() => window.location.href = "/go-live"}
onClick={() => (window.location.href = "/go-live")}
>
<LiveIcon className="h-15 w-15 mr-2" />
Go Live

View File

@@ -85,22 +85,30 @@ const Sidebar: React.FC<SideBarProps> = ({ extraClasses }) => {
className="font-black text-[1.4rem] hover:underline"
onClick={() => navigate(`/user/${username}`)}
>
<div className="text-[var(--sideBar-profile-text)]">
{username}
</div>
<div className="text-[var(--sideBar-profile-text)]">{username}</div>
</button>
</div>
</div>
<div id="following" className="flex flex-col flex-grow justify-evenly gap-4 p-[1rem]">
<div className="bg-[var(--follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300"
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
<div
id="following"
className="flex flex-col flex-grow justify-evenly gap-4 p-[1rem]"
>
<div
className="bg-[var(--follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300"
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "var(--follow-shadow)")
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
<h1 className="text-[var(--follow-text)] font-bold text-2xl p-[0.75rem] cursor-default">Following</h1>
<h1 className="text-[var(--follow-text)] font-bold text-2xl p-[0.75rem] cursor-default">
Following
</h1>
</div>
<div id="streamers-followed" className="flex-grow">
<h2 className="border-b-4 border-t-4 text-2xl cursor-default">Streamers</h2>
<h2 className="border-b-4 border-t-4 text-2xl cursor-default">
Streamers
</h2>
<ul className="mt-2 space-y-2">
{followedStreamers.map((streamer) => (
<li
@@ -115,7 +123,9 @@ const Sidebar: React.FC<SideBarProps> = ({ extraClasses }) => {
</div>
<div id="categories-followed" className="flex-grow">
<h2 className="border-b-4 border-t-4 text-[1.5rem] cursor-default">Categories</h2>
<h2 className="border-b-4 border-t-4 text-[1.5rem] cursor-default">
Categories
</h2>
<ul className="mt-2 space-y-2">
{followedCategories.map((category) => (
<li

View File

@@ -2,7 +2,7 @@ import React from "react";
import ThemeSetting from "./ThemeSetting";
import { useTheme } from "../../context/ThemeContext";
import { useQuickSettings } from "../../context/QuickSettingsContext";
import Screenshot from "../Functionality/Screenshot"
import Screenshot from "../Functionality/Screenshot";
import BrightnessControl from "../Functionality/BrightnessControl";
const QuickSettings: React.FC = () => {
@@ -11,19 +11,24 @@ const QuickSettings: React.FC = () => {
return (
<div
className={`fixed top-0 right-0 w-[20vw] h-screen p-4 flex flex-col items-center overflow-y-hidden overflow-x-hidden ${showQuickSettings ? "opacity-100 z-[90]" : "opacity-0 z-[-1]"
} transition-all duration-300 ease-in-out pt-0 bg-[var(--quickBar-bg)] text-[var(--quickBar-text)]`}
className={`fixed top-0 right-0 w-[20vw] h-screen p-4 flex flex-col items-center overflow-y-hidden overflow-x-hidden ${
showQuickSettings ? "opacity-100 z-[90]" : "opacity-0 z-[-1]"
} transition-all duration-300 ease-in-out pt-0 bg-[var(--quickBar-bg)] text-[var(--quickBar-text)]`}
>
<div className="w-[20vw] p-[1em] flex flex-col items-center bg-[var(--quickBar-title-bg)] text-[var(--quickBar-title)]
border-b-[0.25em] border-[var(--quickBar-border)] ">
<div
className="w-[20vw] p-[1em] flex flex-col items-center bg-[var(--quickBar-title-bg)] text-[var(--quickBar-title)]
border-b-[0.25em] border-[var(--quickBar-border)] "
>
<h1 className="text-[2rem] font-black">Quick Settings</h1>
</div>
<div id="quick-settings-menu" className="flex flex-col flex-grow my-8 gap-4">
<div
id="quick-settings-menu"
className="flex flex-col flex-grow my-8 gap-4"
>
<ThemeSetting />
</div>
<Screenshot />
<BrightnessControl />
<Screenshot />
<BrightnessControl />
</div>
);
};

View File

@@ -140,12 +140,13 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
>
{/* User avatar with image */}
<div
className={`w-2em h-2em rounded-full overflow-hidden flex-shrink-0 ${msg.chatter_username === username ? "" : "cursor-pointer"
}`}
className={`w-2em h-2em rounded-full overflow-hidden flex-shrink-0 ${
msg.chatter_username === username ? "" : "cursor-pointer"
}`}
onClick={() =>
msg.chatter_username === username
? null
: window.location.href = `/user/${msg.chatter_username}`
: (window.location.href = `/user/${msg.chatter_username}`)
}
>
<img
@@ -160,28 +161,33 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
<div className="flex items-center space-x-0.5em">
{/* Username */}
<span
className={`font-bold text-[1em] ${msg.chatter_username === username
className={`font-bold text-[1em] ${
msg.chatter_username === username
? "text-purple-600"
: "text-green-400 cursor-pointer"
}`}
}`}
onClick={() =>
msg.chatter_username === username
? null
: window.location.href = `/user/${msg.chatter_username}`
: (window.location.href = `/user/${msg.chatter_username}`)
}
>
{msg.chatter_username}
</span>
</div>
{/* Message content */}
<div className="message w-full text-[0.9em] mt-0.5em flex flex-col overflow-hidden" >
<div className="message w-full text-[0.9em] mt-0.5em flex flex-col overflow-hidden">
{msg.message}
</div>
</div>
{/* Time sent */}
<div className="text-gray-500 text-[0.8em] absolute top-0 right-0 p-2">
{new Date(msg.time_sent).toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit' })}
{new Date(msg.time_sent).toLocaleTimeString("en-GB", {
hour12: false,
hour: "2-digit",
minute: "2-digit",
})}
</div>
</div>
))}

View File

@@ -44,12 +44,12 @@ const StreamerRoute: React.FC = () => {
if (isLive) {
return <VideoPage streamerId={streamId} />;
}
if (streamerName) {
navigate(`/user/${streamerName}`);
return null;
}
return <div>Streamer not found</div>;
};

View File

@@ -1,5 +1,3 @@
import React from "react";
interface ThumbnailProps {
path: string;
alt?: string;

View File

@@ -32,7 +32,6 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const setupPlayer = async () => {
const streamKey = await fetchStreamKey();
const streamUrl = `/stream/${streamKey}/index.m3u8`;
console.log("Player created with src:", streamUrl);
if (!playerRef.current) {
const videoElement = document.createElement("video");

View File

@@ -4,15 +4,19 @@ import { useBrightness } from "../../context/BrightnessContext";
const BrightnessControl: React.FC = () => {
const { brightness, setBrightness } = useBrightness();
const handleBrightnessChange = (event: React.ChangeEvent<HTMLInputElement>) => {
{/* Set brightness based on the value. Calls BrightnessContext too */}
const handleBrightnessChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
{
/* Set brightness based on the value. Calls BrightnessContext too */
}
setBrightness(Number(event.target.value));
};
return (
<div className="flex flex-col items-center p-4">
<h2 className="text-lg font-semibold mb-2">Brightness Control</h2>
{/* Changes based on the range of input */}
{/* Changes based on the range of input */}
<input
type="range"
min="0"

View File

@@ -5,9 +5,13 @@ interface BrightnessContextType {
setBrightness: (value: number) => void;
}
const BrightnessContext = createContext<BrightnessContextType | undefined>(undefined);
const BrightnessContext = createContext<BrightnessContextType | undefined>(
undefined
);
export const Brightness: React.FC<{ children: React.ReactNode }> = ({ children }) => {
export const Brightness: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [brightness, setBrightness] = useState<number>(100);
useEffect(() => {

View File

@@ -45,40 +45,45 @@ export function ContentProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
// Fetch streams
const streamsUrl = isLoggedIn
? "/api/streams/recommended"
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 => ({
const processedStreams: StreamItem[] = data.map((stream) => ({
type: "stream",
id: stream.user_id,
title: stream.title,
streamer: stream.username,
streamCategory: stream.category_name,
viewers: stream.num_viewers,
thumbnail: stream.thumbnail ||
`/images/category_thumbnails/${stream.category_name.toLowerCase().replace(/ /g, "_")}.webp`
thumbnail:
stream.thumbnail ||
`/images/category_thumbnails/${stream.category_name
.toLowerCase()
.replace(/ /g, "_")}.webp`,
}));
setStreams(processedStreams);
});
// Fetch categories
const categoriesUrl = isLoggedIn
? "/api/categories/recommended"
const categoriesUrl = isLoggedIn
? "/api/categories/recommended"
: "/api/categories/popular/4";
fetch(categoriesUrl)
.then((response) => response.json())
.then((data: any[]) => {
const processedCategories: CategoryItem[] = data.map(category => ({
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`,
thumbnail: `/images/category_thumbnails/${category.category_name
.toLowerCase()
.replace(/ /g, "_")}.webp`,
}));
setCategories(processedCategories);
});
@@ -114,7 +119,10 @@ export function useCategories() {
if (!context) {
throw new Error("useCategories must be used within a ContentProvider");
}
return { categories: context.categories, setCategories: context.setCategories };
return {
categories: context.categories,
setCategories: context.setCategories,
};
}
export function useUsers() {

View File

@@ -23,4 +23,4 @@ export function useSidebar() {
throw new Error("useSidebar must be used within a SidebarProvider");
}
return context;
}
}

View File

@@ -1,9 +1,15 @@
import { createContext, useContext, useState, useEffect, ReactNode } from "react";
import {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from "react";
// Defines the Theme (Colour Theme) that would be shared/used
interface ThemeContextType {
theme: string;
setTheme: (theme: string) => void;
theme: string;
setTheme: (theme: string) => void;
}
// Store theme and provide access to setTheme function
@@ -11,44 +17,45 @@ interface ThemeContextType {
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
// Set default theme to dark
// Set default theme to dark
const [theme, setTheme] = useState<string>(() => {
// If exist on user cache, use that instead
return localStorage.getItem("user-theme") || "dark";
});
const [theme, setTheme] = useState<string>(() => {
// If exist on user cache, use that instead
return localStorage.getItem("user-theme") || "dark";
});
useEffect(() => {
// Store current theme set by user
localStorage.setItem("user-theme", theme);
useEffect(() => {
// Store current theme set by user
localStorage.setItem("user-theme", theme);
// Update the theme
document.body.setAttribute("data-theme", theme);
}, [theme]);
// Update the theme
document.body.setAttribute("data-theme", theme);
}, [theme]);
return (
// Sets the selected theme to child component
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
return (
// Sets the selected theme to child component
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Custom Hook which allows any component to access theme & setTheme with "useTheme()"
export const useTheme = () => {
const context = useContext(ThemeContext); //Retrieves current value of context
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider"); //If called outside of ThemeContext.tsx, errorHandle
}
return context;
const context = useContext(ThemeContext); //Retrieves current value of context
if (!context) {
throw new Error("useTheme must be used within a ThemeProvider"); //If called outside of ThemeContext.tsx, errorHandle
}
return context;
};
{/**
{
/**
createContext: Allow components to share data without directly passing props through multiple levels
useContext: Allows a component to access the current value of a context ("Hook")
useState: Manages state of a component ("Hook")
ReactNode: Allows to take in HTML / React / Arrays of Component
*/}
*/
}

View File

@@ -15,7 +15,7 @@ export function useCategoryFollow() {
}
};
const followCategory = async (categoryName: number) => {
const followCategory = async (categoryName: string) => {
if (!isLoggedIn) {
return;
}
@@ -34,7 +34,7 @@ export function useCategoryFollow() {
}
};
const unfollowCategory = async (categoryName: number) => {
const unfollowCategory = async (categoryName: string) => {
if (!isLoggedIn) {
return;
}

View File

@@ -5,6 +5,6 @@ import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<ThemeProvider>
<App />
<App />
</ThemeProvider>
);

View File

@@ -17,29 +17,31 @@ const AllCategoriesPage: React.FC = () => {
const [categoryOffset, setCategoryOffset] = useState(0);
const [noCategories, setNoCategories] = useState(12);
const [hasMoreData, setHasMoreData] = useState(true);
const listRowRef = useRef<any>(null);
const isLoading = useRef(false);
const fetchCategories = async () => {
// If already loading, skip this fetch
if (isLoading.current) return;
isLoading.current = true;
try {
const response = await fetch(`/api/categories/popular/${noCategories}/${categoryOffset}`);
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,
@@ -51,7 +53,7 @@ const AllCategoriesPage: React.FC = () => {
.replace(/ /g, "_")}.webp`,
}));
setCategories(prev => [...prev, ...processedCategories]);
setCategories((prev) => [...prev, ...processedCategories]);
return processedCategories;
} catch (error) {
console.error("Error fetching categories:", error);
@@ -99,7 +101,7 @@ const AllCategoriesPage: React.FC = () => {
type="category"
title="All Categories"
items={categories}
onClick={handleCategoryClick}
onItemClick={handleCategoryClick}
extraClasses="bg-[var(--recommend)] text-center"
wrap={true}
/>
@@ -107,4 +109,4 @@ const AllCategoriesPage: React.FC = () => {
);
};
export default AllCategoriesPage;
export default AllCategoriesPage;

View File

@@ -6,16 +6,7 @@ import { fetchContentOnScroll } from "../hooks/fetchContentOnScroll";
import Button from "../components/Input/Button";
import { useAuth } from "../context/AuthContext";
import { useCategoryFollow } from "../hooks/useCategoryFollow";
interface StreamData {
type: "stream";
id: number;
title: string;
streamer: string;
streamCategory: string;
viewers: number;
thumbnail?: string;
}
import { ListItemProps as StreamData } from "../components/Layout/ListItem";
const CategoryPage: React.FC = () => {
const { categoryName } = useParams<{ categoryName: string }>();
@@ -26,10 +17,15 @@ const CategoryPage: React.FC = () => {
const [noStreams, setNoStreams] = useState(12);
const [hasMoreData, setHasMoreData] = useState(true);
const { isLoggedIn } = useAuth();
const { isCategoryFollowing, checkCategoryFollowStatus, followCategory, unfollowCategory } = useCategoryFollow()
const {
isCategoryFollowing,
checkCategoryFollowStatus,
followCategory,
unfollowCategory,
} = useCategoryFollow();
useEffect(() => {
checkCategoryFollowStatus(categoryName);
if (categoryName) checkCategoryFollowStatus(categoryName);
}, [categoryName]);
const fetchCategoryStreams = async () => {
@@ -38,7 +34,9 @@ const CategoryPage: React.FC = () => {
isLoading.current = true;
try {
const response = await fetch(`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}`);
const response = await fetch(
`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}`
);
if (!response.ok) {
throw new Error("Failed to fetch category streams");
}
@@ -49,13 +47,13 @@ const CategoryPage: React.FC = () => {
return [];
}
setStreamOffset(prev => prev + data.length);
setStreamOffset((prev) => prev + data.length);
const processedStreams = data.map((stream: any) => ({
type: "stream",
id: stream.user_id,
title: stream.title,
streamer: stream.username,
username: stream.username,
streamCategory: categoryName,
viewers: stream.num_viewers,
thumbnail:
@@ -66,8 +64,8 @@ const CategoryPage: React.FC = () => {
.replace(/ /g, "_")}.webp`),
}));
setStreams(prev => [...prev, ...processedStreams]);
return processedStreams
setStreams((prev) => [...prev, ...processedStreams]);
return processedStreams;
} catch (error) {
console.error("Error fetching category streams:", error);
} finally {
@@ -90,7 +88,6 @@ const CategoryPage: React.FC = () => {
fetchContentOnScroll(logOnScroll, hasMoreData);
const handleStreamClick = (streamerName: string) => {
window.location.href = `/${streamerName}`;
};
@@ -115,14 +112,18 @@ const CategoryPage: React.FC = () => {
description={`Live streams in the ${categoryName} category`}
items={streams}
wrap={true}
onClick={handleStreamClick}
onItemClick={handleStreamClick}
extraClasses="bg-[var(--recommend)]"
>
{isLoggedIn && (
<Button
extraClasses="absolute right-10"
onClick={() => {
isCategoryFollowing ? unfollowCategory(categoryName) : followCategory(categoryName)
if (categoryName) {
isCategoryFollowing
? unfollowCategory(categoryName)
: followCategory(categoryName);
}
}}
>
{isCategoryFollowing ? "Unfollow" : "Follow"}

View File

@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from "react";
import React from "react";
import ListRow from "../components/Layout/ListRow";
import { useNavigate } from "react-router-dom";
import { useStreams, useCategories } from "../context/ContentContext";
@@ -45,7 +45,7 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
}
items={streams}
wrap={false}
onClick={handleStreamClick}
onItemClick={handleStreamClick}
extraClasses="bg-[var(--liveNow)]"
/>
@@ -64,7 +64,7 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
}
items={categories}
wrap={false}
onClick={handleCategoryClick}
onItemClick={handleCategoryClick}
titleClickable={true}
extraClasses="bg-[var(--recommend)]"
>
@@ -79,4 +79,4 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
);
};
export default HomePage;
export default HomePage;

View File

@@ -4,7 +4,9 @@ import Button from "../components/Input/Button";
import ChromeDinoGame from "react-chrome-dino";
const NotFoundPage: React.FC = () => {
const [stars, setStars] = useState<{ x: number; y: number, xChange: number, yChange: number }[]>([]);
const [stars, setStars] = useState<
{ x: number; y: number; xChange: number; yChange: number }[]
>([]);
const starSize = 30;
const [score, setScore] = useState(0);
@@ -16,9 +18,15 @@ const NotFoundPage: React.FC = () => {
const loop = setInterval(() => {
if (Math.random() < 0.1) {
const newStar = {
x: score > 20000 ? (window.innerWidth + starSize) : Math.random() * (window.innerWidth - starSize),
y: score > 20000 ? Math.random() * (window.innerHeight - starSize) : -starSize,
xChange: score * .001,
x:
score > 20000
? window.innerWidth + starSize
: Math.random() * (window.innerWidth - starSize),
y:
score > 20000
? Math.random() * (window.innerHeight - starSize)
: -starSize,
xChange: score * 0.001,
yChange: 5,
};
setStars((prev) => [...prev, newStar]);
@@ -39,7 +47,7 @@ const NotFoundPage: React.FC = () => {
return newStars.map((star) => ({
x: star.x - star.xChange,
y: star.y + star.yChange,
xChange: score * .001,
xChange: score * 0.001,
yChange: star.yChange,
}));
});
@@ -68,7 +76,15 @@ const NotFoundPage: React.FC = () => {
}, []);
return (
<div className={`h-screen w-screen ${score > 25000 ? "bg-black" : score > 10000 ? "bg-[#0f0024]" : "bg-slate-900"} text-white overflow-hidden relative transition-colors duration-[5s]`}>
<div
className={`h-screen w-screen ${
score > 25000
? "bg-black"
: score > 10000
? "bg-[#0f0024]"
: "bg-slate-900"
} text-white overflow-hidden relative transition-colors duration-[5s]`}
>
<div>
{stars.map((star, index) => (
<div
@@ -81,7 +97,11 @@ const NotFoundPage: React.FC = () => {
))}
</div>
<div className="absolute flex justify-center items-center h-full z-0 inset-0 bg-[radial-gradient(rgba(255,255,255,0.5)_1px,transparent_1px)] bg-[length:50px_50px]">
<div className={`${score > 30000 && "drop-shadow-[0_0_5px_rgb(220,20,60)]" } w-full text-center animate-floating transition-all duration-[5s]`}>
<div
className={`${
score > 30000 && "drop-shadow-[0_0_5px_rgb(220,20,60)]"
} w-full text-center animate-floating transition-all duration-[5s]`}
>
<h1 className="text-6xl font-bold mb-4">404</h1>
<p className="text-2xl mb-8">Page Not Found</p>
<ChromeDinoGame />

View File

@@ -3,28 +3,29 @@ import PasswordResetForm from "../components/Auth/PasswordResetForm";
import { useParams } from "react-router-dom";
const ResetPasswordPage: React.FC = () => {
const { token } = useParams<{ token: string }>();
const { token } = useParams<{ token: string }>();
const handlePasswordReset = (success: boolean) => {
if (success) {
alert("Password reset successful!");
window.location.href = "/";
}
else {
alert("Password reset failed.");
}
};
if (!token) {
return <p className="text-red-500 text-center mt-4">Invalid or missing token.</p>;
const handlePasswordReset = (success: boolean) => {
if (success) {
alert("Password reset successful!");
window.location.href = "/";
} else {
alert("Password reset failed.");
}
};
if (!token) {
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-2xl font-bold mb-4">Forgot Password</h1>
<PasswordResetForm onSubmit={handlePasswordReset} token={token} />
</div>
<p className="text-red-500 text-center mt-4">Invalid or missing token.</p>
);
}
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="text-2xl font-bold mb-4">Forgot Password</h1>
<PasswordResetForm onSubmit={handlePasswordReset} token={token} />
</div>
);
};
export default ResetPasswordPage;

View File

@@ -70,7 +70,7 @@ const ResultsPage: React.FC = ({}) => {
thumbnail: stream.thumbnail_url,
}))}
title="Streams"
onClick={(streamer_name: string) =>
onItemClick={(streamer_name: string) =>
(window.location.href = `/${streamer_name}`)
}
itemExtraClasses="min-w-[calc(12vw+12vh/2)]"
@@ -92,7 +92,7 @@ const ResultsPage: React.FC = ({}) => {
.replace(/ /g, "_")}.webp`,
}))}
title="Categories"
onClick={(category_name: string) =>
onItemClick={(category_name: string) =>
navigate(`/category/${category_name}`)
}
titleClickable={true}
@@ -114,7 +114,7 @@ const ResultsPage: React.FC = ({}) => {
thumbnail: user.profile_picture,
}))}
title="Users"
onClick={(username: string) =>
onItemClick={(username: string) =>
(window.location.href = `/user/${username}`)
}
amountForScroll={3}

View File

@@ -235,7 +235,7 @@ const StreamDashboardPage: React.FC = () => {
if (thumbnail) {
formData.append("thumbnail", thumbnail);
}
try {
const response = await fetch("/api/update_stream", {
method: "POST",
@@ -461,7 +461,7 @@ const StreamDashboardPage: React.FC = () => {
type="stream"
id={1}
title={streamData.title || "Stream Title"}
streamer={username || ""}
username={username || ""}
streamCategory={streamData.category_name || "Category"}
viewers={streamData.viewer_count}
thumbnail={thumbnailPreview.url || ""}

View File

@@ -116,7 +116,6 @@ const UserPage: React.FC = () => {
} text-white flex flex-col`}
>
<div className="flex justify-evenly justify-self-center items-center h-full px-4 py-8 max-w-[80vw] w-full">
<div className="grid grid-cols-4 grid-rows-[0.1fr_4fr] w-full gap-8">
{/* Profile Section - TOP */}
@@ -129,9 +128,9 @@ const UserPage: React.FC = () => {
{/* Border Overlay (Always on Top) */}
<div className="absolute left-[0px] inset-0 border-[5px] border-[var(--user-borderBg)] rounded-[20px] z-20"></div>
{/* Background Box */}
<div className="absolute flex top-0 left-[0.55px] w-[99.9%] h-[5vh] min-h-[1em] max-h-[10em] rounded-t-[25.5px]
<div
className="absolute flex top-0 left-[0.55px] w-[99.9%] h-[5vh] min-h-[1em] max-h-[10em] rounded-t-[25.5px]
bg-[var(--user-box)] z-10 flex-shrink justify-center"
style={{ boxShadow: "var(--user-box-shadow)" }}
>
@@ -197,7 +196,9 @@ const UserPage: React.FC = () => {
className="col-span-1 bg-[var(--user-sideBox)] rounded-lg p-6 grid grid-rows-[auto_1fr] text-center items-center justify-center"
>
{/* User Type (e.g., "USER") */}
<small className="text-green-400">{userPageVariant.toUpperCase()}</small>
<small className="text-green-400">
{userPageVariant.toUpperCase()}
</small>
<div className="mt-6 text-center">
<h2 className="text-xl font-semibold mb-3">
@@ -258,7 +259,6 @@ const UserPage: React.FC = () => {
</div>
</>
)}
</div>
<div
@@ -269,31 +269,39 @@ const UserPage: React.FC = () => {
<div
className="bg-[var(--user-follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300
flex items-center justify-center w-full p-4 content-start"
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "var(--follow-shadow)")
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
<li className="text-[var(--follow-text)] whitespace-pre-wrap">Following</li>
<li className="text-[var(--follow-text)] whitespace-pre-wrap">
Following
</li>
</div>
<div
className="bg-[var(--user-follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300
flex items-center justify-center w-full p-4 content-start"
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "var(--follow-shadow)")
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
<li className="text-[var(--follow-text)] whitespace-pre-wrap">Streamers</li>
<li className="text-[var(--follow-text)] whitespace-pre-wrap">
Streamers
</li>
</div>
<div
className="bg-[var(--user-follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300
flex items-center justify-center w-full p-4 content-start"
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "var(--follow-shadow)")
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
<li className="text-[var(--follow-text)] whitespace-pre-wrap">Category</li>
<li className="text-[var(--follow-text)] whitespace-pre-wrap">
Category
</li>
</div>
</div>
</div>
</div>