FIX: Navigation from ListItems;
REFACTOR: Format all files;
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
export const paths = {
|
export const paths = {
|
||||||
pfps:'',
|
pfps: "",
|
||||||
category_thumbnails:'',
|
category_thumbnails: "",
|
||||||
icons:''
|
icons: "",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,45 +36,64 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Brightness>
|
<Brightness>
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{ isLoggedIn, username, userId, setIsLoggedIn, setUsername, setUserId }}
|
value={{
|
||||||
>
|
isLoggedIn,
|
||||||
<ContentProvider>
|
username,
|
||||||
<SidebarProvider>
|
userId,
|
||||||
<QuickSettingsProvider>
|
setIsLoggedIn,
|
||||||
<BrowserRouter>
|
setUsername,
|
||||||
<Routes>
|
setUserId,
|
||||||
<Route
|
}}
|
||||||
path="/"
|
>
|
||||||
element={
|
<ContentProvider>
|
||||||
isLoggedIn ? (
|
<SidebarProvider>
|
||||||
<HomePage variant="personalised" />
|
<QuickSettingsProvider>
|
||||||
) : (
|
<BrowserRouter>
|
||||||
<HomePage />
|
<Routes>
|
||||||
)
|
<Route
|
||||||
}
|
path="/"
|
||||||
/>
|
element={
|
||||||
<Route path="/go-live" element={isLoggedIn ? <StreamDashboardPage /> : <Navigate to="/" replace />} />
|
isLoggedIn ? (
|
||||||
<Route path="/:streamerName" element={<StreamerRoute />} />
|
<HomePage variant="personalised" />
|
||||||
<Route path="/user/:username" element={<UserPage />} />
|
) : (
|
||||||
<Route
|
<HomePage />
|
||||||
path="/reset_password/:token"
|
)
|
||||||
element={<ResetPasswordPage />}
|
}
|
||||||
></Route>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/category/:categoryName"
|
path="/go-live"
|
||||||
element={<CategoryPage />}
|
element={
|
||||||
></Route>
|
isLoggedIn ? (
|
||||||
<Route path="/categories" element={<CategoriesPage />}></Route>
|
<StreamDashboardPage />
|
||||||
<Route path="/results" element={<ResultsPage />}></Route>
|
) : (
|
||||||
<Route path="/404" element={<NotFoundPage />} />
|
<Navigate to="/" replace />
|
||||||
<Route path="*" element={<Navigate to="/404" replace />} />
|
)
|
||||||
</Routes>
|
}
|
||||||
</BrowserRouter>
|
/>
|
||||||
</QuickSettingsProvider>
|
<Route path="/:streamerName" element={<StreamerRoute />} />
|
||||||
</SidebarProvider>
|
<Route path="/user/:username" element={<UserPage />} />
|
||||||
</ContentProvider>
|
<Route
|
||||||
</AuthContext.Provider>
|
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>
|
</Brightness>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ToggleButton } from "../Input/Button";
|
import { ToggleButton } from "../Input/Button";
|
||||||
import {
|
import { LogIn as LogInIcon, User as UserIcon } from "lucide-react";
|
||||||
LogIn as LogInIcon,
|
|
||||||
User as UserIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import LoginForm from "./LoginForm";
|
import LoginForm from "./LoginForm";
|
||||||
import RegisterForm from "./RegisterForm";
|
import RegisterForm from "./RegisterForm";
|
||||||
import ForgotPasswordForm from "./ForgotPasswordForm";
|
import ForgotPasswordForm from "./ForgotPasswordForm";
|
||||||
@@ -93,7 +90,10 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
|
|||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</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()}
|
{authSwitch()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,4 +104,4 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthModal;
|
export default AuthModal;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Button from "../Input/Button";
|
|||||||
|
|
||||||
interface ForgotPasswordProps {
|
interface ForgotPasswordProps {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
general?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubmitProps {
|
interface SubmitProps {
|
||||||
@@ -51,7 +52,9 @@ const ForgotPasswordForm: React.FC<SubmitProps> = ({ onSubmit }) => {
|
|||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json();
|
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 {
|
} else {
|
||||||
confirmPasswordReset();
|
confirmPasswordReset();
|
||||||
}
|
}
|
||||||
@@ -68,8 +71,10 @@ const ForgotPasswordForm: React.FC<SubmitProps> = ({ onSubmit }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex flex-col items-center p-[2.5rem]">
|
<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>
|
<h1 className="text-white text-[1.5em] font-[800] md:text-[1.75em] lg:text-[2em]">
|
||||||
<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]">
|
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
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
id="forgot-password-form"
|
id="forgot-password-form"
|
||||||
@@ -93,7 +98,9 @@ const ForgotPasswordForm: React.FC<SubmitProps> = ({ onSubmit }) => {
|
|||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={handleEmailChange}
|
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>
|
</div>
|
||||||
<Button type="submit">Send Link</Button>
|
<Button type="submit">Send Link</Button>
|
||||||
|
|||||||
@@ -102,9 +102,10 @@ const LoginForm: React.FC<SubmitProps> = ({ onSubmit, onForgotPassword }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center p-10">
|
<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]">
|
<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
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
id="login-form"
|
id="login-form"
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ export default function GoogleLogin() {
|
|||||||
alt="Google logo"
|
alt="Google logo"
|
||||||
className="w-[2em] h-[2em] mr-2"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const Return: React.FC = () => {
|
|||||||
const sessionId = urlParams.get("session_id");
|
const sessionId = urlParams.get("session_id");
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
console.log("1")
|
console.log("1");
|
||||||
fetch(`/api/session-status?session_id=${sessionId}`)
|
fetch(`/api/session-status?session_id=${sessionId}`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|||||||
@@ -23,8 +23,7 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EditButtonProps extends ButtonProps {
|
interface EditButtonProps extends ButtonProps {}
|
||||||
}
|
|
||||||
|
|
||||||
export const EditButton: React.FC<EditButtonProps> = ({
|
export const EditButton: React.FC<EditButtonProps> = ({
|
||||||
children = "",
|
children = "",
|
||||||
@@ -39,7 +38,7 @@ export const EditButton: React.FC<EditButtonProps> = ({
|
|||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ToggleButtonProps extends ButtonProps {
|
interface ToggleButtonProps extends ButtonProps {
|
||||||
toggled?: boolean;
|
toggled?: boolean;
|
||||||
|
|||||||
@@ -11,28 +11,26 @@ const Input: React.FC<InputProps> = ({
|
|||||||
placeholder = "",
|
placeholder = "",
|
||||||
value = "",
|
value = "",
|
||||||
extraClasses = "",
|
extraClasses = "",
|
||||||
onChange = () => { },
|
onChange = () => {},
|
||||||
onKeyDown = () => { },
|
onKeyDown = () => {},
|
||||||
children,
|
children,
|
||||||
...props // all other HTML input props
|
...props // all other HTML input props
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<input
|
<input
|
||||||
name={name}
|
name={name}
|
||||||
type={type}
|
type={type}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
{...props}
|
{...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`}
|
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>
|
|
||||||
</>
|
</>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,22 +9,27 @@ interface DynamicPageContentProps {
|
|||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DynamicPageContent: React.FC<DynamicPageContentProps> = ({
|
const DynamicPageContent: React.FC<DynamicPageContentProps> = ({
|
||||||
children,
|
children,
|
||||||
navbarVariant = "default",
|
navbarVariant = "default",
|
||||||
className = "",
|
className = "",
|
||||||
style
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
const { showSideBar } = useSidebar();
|
const { showSideBar } = useSidebar();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className} style={style}>
|
<div className={className} style={style}>
|
||||||
<Navbar variant={navbarVariant} />
|
<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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DynamicPageContent;
|
export default DynamicPageContent;
|
||||||
|
|||||||
@@ -65,7 +65,9 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3">
|
<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="font-bold">{username}</p>}
|
||||||
{type === "stream" && (
|
{type === "stream" && (
|
||||||
<p className="text-sm text-gray-300">{streamCategory}</p>
|
<p className="text-sm text-gray-300">{streamCategory}</p>
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import React, { forwardRef, useImperativeHandle, useRef, useState } from "react";
|
|
||||||
import {
|
import {
|
||||||
ArrowLeft as ArrowLeftIcon,
|
ArrowLeft as ArrowLeftIcon,
|
||||||
ArrowRight as ArrowRightIcon,
|
ArrowRight as ArrowRightIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import React, {
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import "../../assets/styles/listRow.css";
|
import "../../assets/styles/listRow.css";
|
||||||
import ListItem, { ListItemProps } from "./ListItem";
|
import ListItem, { ListItemProps } from "./ListItem";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
|
|
||||||
interface ListRowProps {
|
interface ListRowProps {
|
||||||
variant?: "default" | "search";
|
variant?: "default" | "search";
|
||||||
@@ -14,7 +19,7 @@ interface ListRowProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
items: ListItemProps[];
|
items: ListItemProps[];
|
||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
onClick: (itemName: string) => void;
|
onItemClick: (itemName: string) => void;
|
||||||
titleClickable?: boolean;
|
titleClickable?: boolean;
|
||||||
extraClasses?: string;
|
extraClasses?: string;
|
||||||
itemExtraClasses?: string;
|
itemExtraClasses?: string;
|
||||||
@@ -22,8 +27,27 @@ interface ListRowProps {
|
|||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }, ListRowProps>(
|
const ListRow = forwardRef<
|
||||||
({ variant, type, title = "", description = "", items, wrap, onClick, titleClickable, extraClasses = "", itemExtraClasses = "", amountForScroll, children }, ref) => {
|
{ 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 [currentItems, setCurrentItems] = useState(items);
|
||||||
const slider = useRef<HTMLDivElement>(null);
|
const slider = useRef<HTMLDivElement>(null);
|
||||||
const scrollAmount = window.innerWidth * 0.3;
|
const scrollAmount = window.innerWidth * 0.3;
|
||||||
@@ -79,7 +103,9 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
|
|||||||
>
|
>
|
||||||
<h2
|
<h2
|
||||||
className={`${
|
className={`${
|
||||||
titleClickable ? "cursor-pointer hover:underline" : "cursor-default"
|
titleClickable
|
||||||
|
? "cursor-pointer hover:underline"
|
||||||
|
: "cursor-default"
|
||||||
} text-2xl font-bold`}
|
} text-2xl font-bold`}
|
||||||
onClick={titleClickable ? () => handleTitleClick(type) : undefined}
|
onClick={titleClickable ? () => handleTitleClick(type) : undefined}
|
||||||
>
|
>
|
||||||
@@ -90,7 +116,7 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
|
|||||||
|
|
||||||
{/* List Items */}
|
{/* List Items */}
|
||||||
<div className="relative overflow-hidden flex flex-grow items-center z-0">
|
<div className="relative overflow-hidden flex flex-grow items-center z-0">
|
||||||
{!wrap && currentItems.length > (amountForScroll || 0) && (
|
{!wrap && currentItems.length > amountForScroll && (
|
||||||
<>
|
<>
|
||||||
<ArrowLeftIcon
|
<ArrowLeftIcon
|
||||||
onClick={slideLeft}
|
onClick={slideLeft}
|
||||||
@@ -126,10 +152,10 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
|
|||||||
onItemClick={() =>
|
onItemClick={() =>
|
||||||
(item.type === "stream" || item.type === "user") &&
|
(item.type === "stream" || item.type === "user") &&
|
||||||
item.username
|
item.username
|
||||||
? onClick?.(item.username)
|
? onItemClick?.(item.username)
|
||||||
: onClick?.(item.title)
|
: onItemClick?.(item.title)
|
||||||
}
|
}
|
||||||
extraClasses={`${itemExtraClasses} min-w-[20vw] max-w-[20vw]`}
|
extraClasses={`${itemExtraClasses} w-[20vw]`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -140,4 +166,4 @@ const ListRow = forwardRef<{ addMoreItems: (newItems: ListItemProps[]) => void }
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export default ListRow;
|
export default ListRow;
|
||||||
|
|||||||
@@ -145,12 +145,12 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
|||||||
<SearchBar />
|
<SearchBar />
|
||||||
|
|
||||||
{/* Stream Button */}
|
{/* Stream Button */}
|
||||||
{isLoggedIn && !window.location.pathname.includes('go-live') && (
|
{isLoggedIn && !window.location.pathname.includes("go-live") && (
|
||||||
<Button
|
<Button
|
||||||
extraClasses={`${
|
extraClasses={`${
|
||||||
variant === "home" ? "absolute top-[2vh] right-[10vw]" : ""
|
variant === "home" ? "absolute top-[2vh] right-[10vw]" : ""
|
||||||
} flex flex-row items-center`}
|
} 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" />
|
<LiveIcon className="h-15 w-15 mr-2" />
|
||||||
Go Live
|
Go Live
|
||||||
|
|||||||
@@ -85,22 +85,30 @@ const Sidebar: React.FC<SideBarProps> = ({ extraClasses }) => {
|
|||||||
className="font-black text-[1.4rem] hover:underline"
|
className="font-black text-[1.4rem] hover:underline"
|
||||||
onClick={() => navigate(`/user/${username}`)}
|
onClick={() => navigate(`/user/${username}`)}
|
||||||
>
|
>
|
||||||
<div className="text-[var(--sideBar-profile-text)]">
|
<div className="text-[var(--sideBar-profile-text)]">{username}</div>
|
||||||
{username}
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="following" className="flex flex-col flex-grow justify-evenly gap-4 p-[1rem]">
|
<div
|
||||||
<div className="bg-[var(--follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300"
|
id="following"
|
||||||
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
|
className="flex flex-col flex-grow justify-evenly gap-4 p-[1rem]"
|
||||||
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
|
>
|
||||||
|
<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>
|
||||||
<div id="streamers-followed" className="flex-grow">
|
<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">
|
<ul className="mt-2 space-y-2">
|
||||||
{followedStreamers.map((streamer) => (
|
{followedStreamers.map((streamer) => (
|
||||||
<li
|
<li
|
||||||
@@ -115,7 +123,9 @@ const Sidebar: React.FC<SideBarProps> = ({ extraClasses }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="categories-followed" className="flex-grow">
|
<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">
|
<ul className="mt-2 space-y-2">
|
||||||
{followedCategories.map((category) => (
|
{followedCategories.map((category) => (
|
||||||
<li
|
<li
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from "react";
|
|||||||
import ThemeSetting from "./ThemeSetting";
|
import ThemeSetting from "./ThemeSetting";
|
||||||
import { useTheme } from "../../context/ThemeContext";
|
import { useTheme } from "../../context/ThemeContext";
|
||||||
import { useQuickSettings } from "../../context/QuickSettingsContext";
|
import { useQuickSettings } from "../../context/QuickSettingsContext";
|
||||||
import Screenshot from "../Functionality/Screenshot"
|
import Screenshot from "../Functionality/Screenshot";
|
||||||
import BrightnessControl from "../Functionality/BrightnessControl";
|
import BrightnessControl from "../Functionality/BrightnessControl";
|
||||||
|
|
||||||
const QuickSettings: React.FC = () => {
|
const QuickSettings: React.FC = () => {
|
||||||
@@ -11,19 +11,24 @@ const QuickSettings: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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]"
|
className={`fixed top-0 right-0 w-[20vw] h-screen p-4 flex flex-col items-center overflow-y-hidden overflow-x-hidden ${
|
||||||
} transition-all duration-300 ease-in-out pt-0 bg-[var(--quickBar-bg)] text-[var(--quickBar-text)]`}
|
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)]
|
<div
|
||||||
border-b-[0.25em] border-[var(--quickBar-border)] ">
|
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>
|
<h1 className="text-[2rem] font-black">Quick Settings</h1>
|
||||||
</div>
|
</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 />
|
<ThemeSetting />
|
||||||
</div>
|
</div>
|
||||||
<Screenshot />
|
<Screenshot />
|
||||||
<BrightnessControl />
|
<BrightnessControl />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -140,12 +140,13 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
|||||||
>
|
>
|
||||||
{/* User avatar with image */}
|
{/* User avatar with image */}
|
||||||
<div
|
<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={() =>
|
onClick={() =>
|
||||||
msg.chatter_username === username
|
msg.chatter_username === username
|
||||||
? null
|
? null
|
||||||
: window.location.href = `/user/${msg.chatter_username}`
|
: (window.location.href = `/user/${msg.chatter_username}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -160,28 +161,33 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
|
|||||||
<div className="flex items-center space-x-0.5em">
|
<div className="flex items-center space-x-0.5em">
|
||||||
{/* Username */}
|
{/* Username */}
|
||||||
<span
|
<span
|
||||||
className={`font-bold text-[1em] ${msg.chatter_username === username
|
className={`font-bold text-[1em] ${
|
||||||
|
msg.chatter_username === username
|
||||||
? "text-purple-600"
|
? "text-purple-600"
|
||||||
: "text-green-400 cursor-pointer"
|
: "text-green-400 cursor-pointer"
|
||||||
}`}
|
}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
msg.chatter_username === username
|
msg.chatter_username === username
|
||||||
? null
|
? null
|
||||||
: window.location.href = `/user/${msg.chatter_username}`
|
: (window.location.href = `/user/${msg.chatter_username}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{msg.chatter_username}
|
{msg.chatter_username}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Message content */}
|
{/* 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}
|
{msg.message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Time sent */}
|
{/* Time sent */}
|
||||||
<div className="text-gray-500 text-[0.8em] absolute top-0 right-0 p-2">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ const StreamerRoute: React.FC = () => {
|
|||||||
if (isLive) {
|
if (isLive) {
|
||||||
return <VideoPage streamerId={streamId} />;
|
return <VideoPage streamerId={streamId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamerName) {
|
if (streamerName) {
|
||||||
navigate(`/user/${streamerName}`);
|
navigate(`/user/${streamerName}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>Streamer not found</div>;
|
return <div>Streamer not found</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
interface ThumbnailProps {
|
interface ThumbnailProps {
|
||||||
path: string;
|
path: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
|||||||
const setupPlayer = async () => {
|
const setupPlayer = async () => {
|
||||||
const streamKey = await fetchStreamKey();
|
const streamKey = await fetchStreamKey();
|
||||||
const streamUrl = `/stream/${streamKey}/index.m3u8`;
|
const streamUrl = `/stream/${streamKey}/index.m3u8`;
|
||||||
console.log("Player created with src:", streamUrl);
|
|
||||||
|
|
||||||
if (!playerRef.current) {
|
if (!playerRef.current) {
|
||||||
const videoElement = document.createElement("video");
|
const videoElement = document.createElement("video");
|
||||||
|
|||||||
@@ -4,15 +4,19 @@ import { useBrightness } from "../../context/BrightnessContext";
|
|||||||
const BrightnessControl: React.FC = () => {
|
const BrightnessControl: React.FC = () => {
|
||||||
const { brightness, setBrightness } = useBrightness();
|
const { brightness, setBrightness } = useBrightness();
|
||||||
|
|
||||||
const handleBrightnessChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleBrightnessChange = (
|
||||||
{/* Set brightness based on the value. Calls BrightnessContext too */}
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
{
|
||||||
|
/* Set brightness based on the value. Calls BrightnessContext too */
|
||||||
|
}
|
||||||
setBrightness(Number(event.target.value));
|
setBrightness(Number(event.target.value));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center p-4">
|
<div className="flex flex-col items-center p-4">
|
||||||
<h2 className="text-lg font-semibold mb-2">Brightness Control</h2>
|
<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
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ interface BrightnessContextType {
|
|||||||
setBrightness: (value: number) => void;
|
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);
|
const [brightness, setBrightness] = useState<number>(100);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -45,40 +45,45 @@ export function ContentProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch streams
|
// Fetch streams
|
||||||
const streamsUrl = isLoggedIn
|
const streamsUrl = isLoggedIn
|
||||||
? "/api/streams/recommended"
|
? "/api/streams/recommended"
|
||||||
: "/api/streams/popular/4";
|
: "/api/streams/popular/4";
|
||||||
|
|
||||||
fetch(streamsUrl)
|
fetch(streamsUrl)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: any[]) => {
|
.then((data: any[]) => {
|
||||||
const processedStreams: StreamItem[] = data.map(stream => ({
|
const processedStreams: StreamItem[] = data.map((stream) => ({
|
||||||
type: "stream",
|
type: "stream",
|
||||||
id: stream.user_id,
|
id: stream.user_id,
|
||||||
title: stream.title,
|
title: stream.title,
|
||||||
streamer: stream.username,
|
streamer: stream.username,
|
||||||
streamCategory: stream.category_name,
|
streamCategory: stream.category_name,
|
||||||
viewers: stream.num_viewers,
|
viewers: stream.num_viewers,
|
||||||
thumbnail: stream.thumbnail ||
|
thumbnail:
|
||||||
`/images/category_thumbnails/${stream.category_name.toLowerCase().replace(/ /g, "_")}.webp`
|
stream.thumbnail ||
|
||||||
|
`/images/category_thumbnails/${stream.category_name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "_")}.webp`,
|
||||||
}));
|
}));
|
||||||
setStreams(processedStreams);
|
setStreams(processedStreams);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch categories
|
// Fetch categories
|
||||||
const categoriesUrl = isLoggedIn
|
const categoriesUrl = isLoggedIn
|
||||||
? "/api/categories/recommended"
|
? "/api/categories/recommended"
|
||||||
: "/api/categories/popular/4";
|
: "/api/categories/popular/4";
|
||||||
|
|
||||||
fetch(categoriesUrl)
|
fetch(categoriesUrl)
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: any[]) => {
|
.then((data: any[]) => {
|
||||||
const processedCategories: CategoryItem[] = data.map(category => ({
|
const processedCategories: CategoryItem[] = data.map((category) => ({
|
||||||
type: "category",
|
type: "category",
|
||||||
id: category.category_id,
|
id: category.category_id,
|
||||||
title: category.category_name,
|
title: category.category_name,
|
||||||
viewers: category.num_viewers,
|
viewers: category.num_viewers,
|
||||||
thumbnail: `/images/category_thumbnails/${category.category_name.toLowerCase().replace(/ /g, "_")}.webp`,
|
thumbnail: `/images/category_thumbnails/${category.category_name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "_")}.webp`,
|
||||||
}));
|
}));
|
||||||
setCategories(processedCategories);
|
setCategories(processedCategories);
|
||||||
});
|
});
|
||||||
@@ -114,7 +119,10 @@ export function useCategories() {
|
|||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useCategories must be used within a ContentProvider");
|
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() {
|
export function useUsers() {
|
||||||
|
|||||||
@@ -23,4 +23,4 @@ export function useSidebar() {
|
|||||||
throw new Error("useSidebar must be used within a SidebarProvider");
|
throw new Error("useSidebar must be used within a SidebarProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Defines the Theme (Colour Theme) that would be shared/used
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
theme: string;
|
theme: string;
|
||||||
setTheme: (theme: string) => void;
|
setTheme: (theme: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store theme and provide access to setTheme function
|
// Store theme and provide access to setTheme function
|
||||||
@@ -11,44 +17,45 @@ interface ThemeContextType {
|
|||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
||||||
// Set default theme to dark
|
// Set default theme to dark
|
||||||
|
|
||||||
const [theme, setTheme] = useState<string>(() => {
|
const [theme, setTheme] = useState<string>(() => {
|
||||||
// If exist on user cache, use that instead
|
// If exist on user cache, use that instead
|
||||||
return localStorage.getItem("user-theme") || "dark";
|
return localStorage.getItem("user-theme") || "dark";
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Store current theme set by user
|
// Store current theme set by user
|
||||||
localStorage.setItem("user-theme", theme);
|
localStorage.setItem("user-theme", theme);
|
||||||
|
|
||||||
// Update the theme
|
// Update the theme
|
||||||
document.body.setAttribute("data-theme", theme);
|
document.body.setAttribute("data-theme", theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Sets the selected theme to child component
|
// Sets the selected theme to child component
|
||||||
<ThemeContext.Provider value={{ theme, setTheme }}>
|
<ThemeContext.Provider value={{ theme, setTheme }}>
|
||||||
{children}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Custom Hook which allows any component to access theme & setTheme with "useTheme()"
|
// Custom Hook which allows any component to access theme & setTheme with "useTheme()"
|
||||||
export const useTheme = () => {
|
export const useTheme = () => {
|
||||||
const context = useContext(ThemeContext); //Retrieves current value of context
|
const context = useContext(ThemeContext); //Retrieves current value of context
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error("useTheme must be used within a ThemeProvider"); //If called outside of ThemeContext.tsx, errorHandle
|
throw new Error("useTheme must be used within a ThemeProvider"); //If called outside of ThemeContext.tsx, errorHandle
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
{
|
||||||
{/**
|
/**
|
||||||
|
|
||||||
createContext: Allow components to share data without directly passing props through multiple levels
|
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")
|
useContext: Allows a component to access the current value of a context ("Hook")
|
||||||
useState: Manages state of a component ("Hook")
|
useState: Manages state of a component ("Hook")
|
||||||
ReactNode: Allows to take in HTML / React / Arrays of Component
|
ReactNode: Allows to take in HTML / React / Arrays of Component
|
||||||
|
|
||||||
*/}
|
*/
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function useCategoryFollow() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const followCategory = async (categoryName: number) => {
|
const followCategory = async (categoryName: string) => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ export function useCategoryFollow() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const unfollowCategory = async (categoryName: number) => {
|
const unfollowCategory = async (categoryName: string) => {
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import App from "./App.tsx";
|
|||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<App />
|
<App />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,29 +17,31 @@ const AllCategoriesPage: React.FC = () => {
|
|||||||
const [categoryOffset, setCategoryOffset] = useState(0);
|
const [categoryOffset, setCategoryOffset] = useState(0);
|
||||||
const [noCategories, setNoCategories] = useState(12);
|
const [noCategories, setNoCategories] = useState(12);
|
||||||
const [hasMoreData, setHasMoreData] = useState(true);
|
const [hasMoreData, setHasMoreData] = useState(true);
|
||||||
|
|
||||||
const listRowRef = useRef<any>(null);
|
const listRowRef = useRef<any>(null);
|
||||||
const isLoading = useRef(false);
|
const isLoading = useRef(false);
|
||||||
|
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
// If already loading, skip this fetch
|
// If already loading, skip this fetch
|
||||||
if (isLoading.current) return;
|
if (isLoading.current) return;
|
||||||
|
|
||||||
isLoading.current = true;
|
isLoading.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/categories/popular/${noCategories}/${categoryOffset}`);
|
const response = await fetch(
|
||||||
|
`/api/categories/popular/${noCategories}/${categoryOffset}`
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch categories");
|
throw new Error("Failed to fetch categories");
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
setHasMoreData(false);
|
setHasMoreData(false);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setCategoryOffset(prev => prev + data.length);
|
setCategoryOffset((prev) => prev + data.length);
|
||||||
|
|
||||||
const processedCategories = data.map((category: any) => ({
|
const processedCategories = data.map((category: any) => ({
|
||||||
type: "category" as const,
|
type: "category" as const,
|
||||||
@@ -51,7 +53,7 @@ const AllCategoriesPage: React.FC = () => {
|
|||||||
.replace(/ /g, "_")}.webp`,
|
.replace(/ /g, "_")}.webp`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setCategories(prev => [...prev, ...processedCategories]);
|
setCategories((prev) => [...prev, ...processedCategories]);
|
||||||
return processedCategories;
|
return processedCategories;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching categories:", error);
|
console.error("Error fetching categories:", error);
|
||||||
@@ -99,7 +101,7 @@ const AllCategoriesPage: React.FC = () => {
|
|||||||
type="category"
|
type="category"
|
||||||
title="All Categories"
|
title="All Categories"
|
||||||
items={categories}
|
items={categories}
|
||||||
onClick={handleCategoryClick}
|
onItemClick={handleCategoryClick}
|
||||||
extraClasses="bg-[var(--recommend)] text-center"
|
extraClasses="bg-[var(--recommend)] text-center"
|
||||||
wrap={true}
|
wrap={true}
|
||||||
/>
|
/>
|
||||||
@@ -107,4 +109,4 @@ const AllCategoriesPage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AllCategoriesPage;
|
export default AllCategoriesPage;
|
||||||
|
|||||||
@@ -6,16 +6,7 @@ import { fetchContentOnScroll } from "../hooks/fetchContentOnScroll";
|
|||||||
import Button from "../components/Input/Button";
|
import Button from "../components/Input/Button";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useCategoryFollow } from "../hooks/useCategoryFollow";
|
import { useCategoryFollow } from "../hooks/useCategoryFollow";
|
||||||
|
import { ListItemProps as StreamData } from "../components/Layout/ListItem";
|
||||||
interface StreamData {
|
|
||||||
type: "stream";
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
streamer: string;
|
|
||||||
streamCategory: string;
|
|
||||||
viewers: number;
|
|
||||||
thumbnail?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CategoryPage: React.FC = () => {
|
const CategoryPage: React.FC = () => {
|
||||||
const { categoryName } = useParams<{ categoryName: string }>();
|
const { categoryName } = useParams<{ categoryName: string }>();
|
||||||
@@ -26,10 +17,15 @@ const CategoryPage: React.FC = () => {
|
|||||||
const [noStreams, setNoStreams] = useState(12);
|
const [noStreams, setNoStreams] = useState(12);
|
||||||
const [hasMoreData, setHasMoreData] = useState(true);
|
const [hasMoreData, setHasMoreData] = useState(true);
|
||||||
const { isLoggedIn } = useAuth();
|
const { isLoggedIn } = useAuth();
|
||||||
const { isCategoryFollowing, checkCategoryFollowStatus, followCategory, unfollowCategory } = useCategoryFollow()
|
const {
|
||||||
|
isCategoryFollowing,
|
||||||
|
checkCategoryFollowStatus,
|
||||||
|
followCategory,
|
||||||
|
unfollowCategory,
|
||||||
|
} = useCategoryFollow();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkCategoryFollowStatus(categoryName);
|
if (categoryName) checkCategoryFollowStatus(categoryName);
|
||||||
}, [categoryName]);
|
}, [categoryName]);
|
||||||
|
|
||||||
const fetchCategoryStreams = async () => {
|
const fetchCategoryStreams = async () => {
|
||||||
@@ -38,7 +34,9 @@ const CategoryPage: React.FC = () => {
|
|||||||
|
|
||||||
isLoading.current = true;
|
isLoading.current = true;
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}`);
|
const response = await fetch(
|
||||||
|
`/api/streams/popular/${categoryName}/${noStreams}/${streamOffset}`
|
||||||
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error("Failed to fetch category streams");
|
throw new Error("Failed to fetch category streams");
|
||||||
}
|
}
|
||||||
@@ -49,13 +47,13 @@ const CategoryPage: React.FC = () => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
setStreamOffset(prev => prev + data.length);
|
setStreamOffset((prev) => prev + data.length);
|
||||||
|
|
||||||
const processedStreams = data.map((stream: any) => ({
|
const processedStreams = data.map((stream: any) => ({
|
||||||
type: "stream",
|
type: "stream",
|
||||||
id: stream.user_id,
|
id: stream.user_id,
|
||||||
title: stream.title,
|
title: stream.title,
|
||||||
streamer: stream.username,
|
username: stream.username,
|
||||||
streamCategory: categoryName,
|
streamCategory: categoryName,
|
||||||
viewers: stream.num_viewers,
|
viewers: stream.num_viewers,
|
||||||
thumbnail:
|
thumbnail:
|
||||||
@@ -66,8 +64,8 @@ const CategoryPage: React.FC = () => {
|
|||||||
.replace(/ /g, "_")}.webp`),
|
.replace(/ /g, "_")}.webp`),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setStreams(prev => [...prev, ...processedStreams]);
|
setStreams((prev) => [...prev, ...processedStreams]);
|
||||||
return processedStreams
|
return processedStreams;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching category streams:", error);
|
console.error("Error fetching category streams:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -90,7 +88,6 @@ const CategoryPage: React.FC = () => {
|
|||||||
|
|
||||||
fetchContentOnScroll(logOnScroll, hasMoreData);
|
fetchContentOnScroll(logOnScroll, hasMoreData);
|
||||||
|
|
||||||
|
|
||||||
const handleStreamClick = (streamerName: string) => {
|
const handleStreamClick = (streamerName: string) => {
|
||||||
window.location.href = `/${streamerName}`;
|
window.location.href = `/${streamerName}`;
|
||||||
};
|
};
|
||||||
@@ -115,14 +112,18 @@ const CategoryPage: React.FC = () => {
|
|||||||
description={`Live streams in the ${categoryName} category`}
|
description={`Live streams in the ${categoryName} category`}
|
||||||
items={streams}
|
items={streams}
|
||||||
wrap={true}
|
wrap={true}
|
||||||
onClick={handleStreamClick}
|
onItemClick={handleStreamClick}
|
||||||
extraClasses="bg-[var(--recommend)]"
|
extraClasses="bg-[var(--recommend)]"
|
||||||
>
|
>
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<Button
|
<Button
|
||||||
extraClasses="absolute right-10"
|
extraClasses="absolute right-10"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
isCategoryFollowing ? unfollowCategory(categoryName) : followCategory(categoryName)
|
if (categoryName) {
|
||||||
|
isCategoryFollowing
|
||||||
|
? unfollowCategory(categoryName)
|
||||||
|
: followCategory(categoryName);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isCategoryFollowing ? "Unfollow" : "Follow"}
|
{isCategoryFollowing ? "Unfollow" : "Follow"}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useEffect } from "react";
|
import React from "react";
|
||||||
import ListRow from "../components/Layout/ListRow";
|
import ListRow from "../components/Layout/ListRow";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useStreams, useCategories } from "../context/ContentContext";
|
import { useStreams, useCategories } from "../context/ContentContext";
|
||||||
@@ -45,7 +45,7 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
|
|||||||
}
|
}
|
||||||
items={streams}
|
items={streams}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
onClick={handleStreamClick}
|
onItemClick={handleStreamClick}
|
||||||
extraClasses="bg-[var(--liveNow)]"
|
extraClasses="bg-[var(--liveNow)]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
|
|||||||
}
|
}
|
||||||
items={categories}
|
items={categories}
|
||||||
wrap={false}
|
wrap={false}
|
||||||
onClick={handleCategoryClick}
|
onItemClick={handleCategoryClick}
|
||||||
titleClickable={true}
|
titleClickable={true}
|
||||||
extraClasses="bg-[var(--recommend)]"
|
extraClasses="bg-[var(--recommend)]"
|
||||||
>
|
>
|
||||||
@@ -79,4 +79,4 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default HomePage;
|
export default HomePage;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import Button from "../components/Input/Button";
|
|||||||
import ChromeDinoGame from "react-chrome-dino";
|
import ChromeDinoGame from "react-chrome-dino";
|
||||||
|
|
||||||
const NotFoundPage: React.FC = () => {
|
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 starSize = 30;
|
||||||
|
|
||||||
const [score, setScore] = useState(0);
|
const [score, setScore] = useState(0);
|
||||||
@@ -16,9 +18,15 @@ const NotFoundPage: React.FC = () => {
|
|||||||
const loop = setInterval(() => {
|
const loop = setInterval(() => {
|
||||||
if (Math.random() < 0.1) {
|
if (Math.random() < 0.1) {
|
||||||
const newStar = {
|
const newStar = {
|
||||||
x: score > 20000 ? (window.innerWidth + starSize) : Math.random() * (window.innerWidth - starSize),
|
x:
|
||||||
y: score > 20000 ? Math.random() * (window.innerHeight - starSize) : -starSize,
|
score > 20000
|
||||||
xChange: score * .001,
|
? window.innerWidth + starSize
|
||||||
|
: Math.random() * (window.innerWidth - starSize),
|
||||||
|
y:
|
||||||
|
score > 20000
|
||||||
|
? Math.random() * (window.innerHeight - starSize)
|
||||||
|
: -starSize,
|
||||||
|
xChange: score * 0.001,
|
||||||
yChange: 5,
|
yChange: 5,
|
||||||
};
|
};
|
||||||
setStars((prev) => [...prev, newStar]);
|
setStars((prev) => [...prev, newStar]);
|
||||||
@@ -39,7 +47,7 @@ const NotFoundPage: React.FC = () => {
|
|||||||
return newStars.map((star) => ({
|
return newStars.map((star) => ({
|
||||||
x: star.x - star.xChange,
|
x: star.x - star.xChange,
|
||||||
y: star.y + star.yChange,
|
y: star.y + star.yChange,
|
||||||
xChange: score * .001,
|
xChange: score * 0.001,
|
||||||
yChange: star.yChange,
|
yChange: star.yChange,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
@@ -68,7 +76,15 @@ const NotFoundPage: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
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>
|
<div>
|
||||||
{stars.map((star, index) => (
|
{stars.map((star, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -81,7 +97,11 @@ const NotFoundPage: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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="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>
|
<h1 className="text-6xl font-bold mb-4">404</h1>
|
||||||
<p className="text-2xl mb-8">Page Not Found</p>
|
<p className="text-2xl mb-8">Page Not Found</p>
|
||||||
<ChromeDinoGame />
|
<ChromeDinoGame />
|
||||||
|
|||||||
@@ -3,28 +3,29 @@ import PasswordResetForm from "../components/Auth/PasswordResetForm";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
const ResetPasswordPage: React.FC = () => {
|
const ResetPasswordPage: React.FC = () => {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
|
|
||||||
const handlePasswordReset = (success: boolean) => {
|
const handlePasswordReset = (success: boolean) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
alert("Password reset successful!");
|
alert("Password reset successful!");
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
} else {
|
||||||
else {
|
alert("Password reset failed.");
|
||||||
alert("Password reset failed.");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return <p className="text-red-500 text-center mt-4">Invalid or missing token.</p>;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-screen">
|
<p className="text-red-500 text-center mt-4">Invalid or missing token.</p>
|
||||||
<h1 className="text-2xl font-bold mb-4">Forgot Password</h1>
|
|
||||||
<PasswordResetForm onSubmit={handlePasswordReset} token={token} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
export default ResetPasswordPage;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const ResultsPage: React.FC = ({}) => {
|
|||||||
thumbnail: stream.thumbnail_url,
|
thumbnail: stream.thumbnail_url,
|
||||||
}))}
|
}))}
|
||||||
title="Streams"
|
title="Streams"
|
||||||
onClick={(streamer_name: string) =>
|
onItemClick={(streamer_name: string) =>
|
||||||
(window.location.href = `/${streamer_name}`)
|
(window.location.href = `/${streamer_name}`)
|
||||||
}
|
}
|
||||||
itemExtraClasses="min-w-[calc(12vw+12vh/2)]"
|
itemExtraClasses="min-w-[calc(12vw+12vh/2)]"
|
||||||
@@ -92,7 +92,7 @@ const ResultsPage: React.FC = ({}) => {
|
|||||||
.replace(/ /g, "_")}.webp`,
|
.replace(/ /g, "_")}.webp`,
|
||||||
}))}
|
}))}
|
||||||
title="Categories"
|
title="Categories"
|
||||||
onClick={(category_name: string) =>
|
onItemClick={(category_name: string) =>
|
||||||
navigate(`/category/${category_name}`)
|
navigate(`/category/${category_name}`)
|
||||||
}
|
}
|
||||||
titleClickable={true}
|
titleClickable={true}
|
||||||
@@ -114,7 +114,7 @@ const ResultsPage: React.FC = ({}) => {
|
|||||||
thumbnail: user.profile_picture,
|
thumbnail: user.profile_picture,
|
||||||
}))}
|
}))}
|
||||||
title="Users"
|
title="Users"
|
||||||
onClick={(username: string) =>
|
onItemClick={(username: string) =>
|
||||||
(window.location.href = `/user/${username}`)
|
(window.location.href = `/user/${username}`)
|
||||||
}
|
}
|
||||||
amountForScroll={3}
|
amountForScroll={3}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
if (thumbnail) {
|
if (thumbnail) {
|
||||||
formData.append("thumbnail", thumbnail);
|
formData.append("thumbnail", thumbnail);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/update_stream", {
|
const response = await fetch("/api/update_stream", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -461,7 +461,7 @@ const StreamDashboardPage: React.FC = () => {
|
|||||||
type="stream"
|
type="stream"
|
||||||
id={1}
|
id={1}
|
||||||
title={streamData.title || "Stream Title"}
|
title={streamData.title || "Stream Title"}
|
||||||
streamer={username || ""}
|
username={username || ""}
|
||||||
streamCategory={streamData.category_name || "Category"}
|
streamCategory={streamData.category_name || "Category"}
|
||||||
viewers={streamData.viewer_count}
|
viewers={streamData.viewer_count}
|
||||||
thumbnail={thumbnailPreview.url || ""}
|
thumbnail={thumbnailPreview.url || ""}
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ const UserPage: React.FC = () => {
|
|||||||
} text-white flex flex-col`}
|
} 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="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">
|
<div className="grid grid-cols-4 grid-rows-[0.1fr_4fr] w-full gap-8">
|
||||||
{/* Profile Section - TOP */}
|
{/* Profile Section - TOP */}
|
||||||
|
|
||||||
@@ -129,9 +128,9 @@ const UserPage: React.FC = () => {
|
|||||||
{/* Border Overlay (Always on Top) */}
|
{/* Border Overlay (Always on Top) */}
|
||||||
<div className="absolute left-[0px] inset-0 border-[5px] border-[var(--user-borderBg)] rounded-[20px] z-20"></div>
|
<div className="absolute left-[0px] inset-0 border-[5px] border-[var(--user-borderBg)] rounded-[20px] z-20"></div>
|
||||||
|
|
||||||
|
|
||||||
{/* Background Box */}
|
{/* 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"
|
bg-[var(--user-box)] z-10 flex-shrink justify-center"
|
||||||
style={{ boxShadow: "var(--user-box-shadow)" }}
|
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"
|
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") */}
|
{/* 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">
|
<div className="mt-6 text-center">
|
||||||
<h2 className="text-xl font-semibold mb-3">
|
<h2 className="text-xl font-semibold mb-3">
|
||||||
@@ -258,7 +259,6 @@ const UserPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -269,31 +269,39 @@ const UserPage: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
className="bg-[var(--user-follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300
|
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"
|
flex items-center justify-center w-full p-4 content-start"
|
||||||
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
|
onMouseEnter={(e) =>
|
||||||
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
|
(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>
|
||||||
<div
|
<div
|
||||||
className="bg-[var(--user-follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300
|
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"
|
flex items-center justify-center w-full p-4 content-start"
|
||||||
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
|
onMouseEnter={(e) =>
|
||||||
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
|
(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>
|
||||||
<div
|
<div
|
||||||
className="bg-[var(--user-follow-bg)] rounded-[1em] hover:scale-105 transition-all ease-in-out duration-300
|
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"
|
flex items-center justify-center w-full p-4 content-start"
|
||||||
onMouseEnter={(e) => e.currentTarget.style.boxShadow = "var(--follow-shadow)"}
|
onMouseEnter={(e) =>
|
||||||
onMouseLeave={(e) => e.currentTarget.style.boxShadow = "none"}
|
(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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user