FEAT: Added more info & functionality to UserPage & Added ability to follow streamers on both UserPage and VideoPage;
Added shortcut to toggle chat;
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --config vite.config.dev.ts",
|
"dev": "vite --config vite.config.dev.ts",
|
||||||
|
|||||||
@@ -28,66 +28,61 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
|
|||||||
id="blurring-layer"
|
id="blurring-layer"
|
||||||
className="fixed z-50 inset-0 w-screen h-screen backdrop-blur-sm group-has-[input:focus]:backdrop-blur-[5px]"
|
className="fixed z-50 inset-0 w-screen h-screen backdrop-blur-sm group-has-[input:focus]:backdrop-blur-[5px]"
|
||||||
></div>
|
></div>
|
||||||
<div>
|
|
||||||
{/*Container*/}
|
|
||||||
|
|
||||||
<div className="fixed inset-0 flex flex-col items-center justify-around z-[9000]
|
{/*Container*/}
|
||||||
h-[95vh] m-auto min-w-[65vw] w-fit py-[80px] rounded-[5rem] transition-all animate-floating">
|
<div id="auth-modal"
|
||||||
|
className="fixed inset-0 flex flex-col items-center justify-around z-[9000]
|
||||||
{/* Login/Register Buttons Container */}
|
h-[95vh] m-auto min-w-[65vw] w-fit py-[80px] rounded-[5rem] transition-all animate-floating"
|
||||||
<div
|
>
|
||||||
className="absolute top-[60px] left-1/2 transform -translate-x-1/2 w-[300px] flex justify-center gap-8 transition-transform overflow-visible "
|
{/* Login/Register Buttons Container */}
|
||||||
>
|
<div className="absolute top-[60px] left-1/2 transform -translate-x-1/2 w-[300px] flex justify-center gap-8 transition-transform overflow-visible ">
|
||||||
{/* Login Toggle */}
|
{/* Login Toggle */}
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
toggled={selectedTab === "Login"}
|
toggled={selectedTab === "Login"}
|
||||||
extraClasses="flex flex-col items-center px-8 duration-250 transition-transform hover:translate-y-[-50px] z-[9001]"
|
extraClasses="flex flex-col items-center px-8 duration-250 transition-transform hover:translate-y-[-50px] z-[9001]"
|
||||||
onClick={() => setSelectedTab("Login")}
|
onClick={() => setSelectedTab("Login")}
|
||||||
>
|
|
||||||
<LogInIcon className="h-[40px] w-[40px] mr-1" />
|
|
||||||
Login
|
|
||||||
</ToggleButton>
|
|
||||||
|
|
||||||
{/* Register Toggle */}
|
|
||||||
<ToggleButton
|
|
||||||
toggled={selectedTab === "Register"}
|
|
||||||
extraClasses="flex flex-col items-center px-8 duration-250 transition-transform hover:translate-y-[-50px] z-[9001]"
|
|
||||||
onClick={() => setSelectedTab("Register")}
|
|
||||||
>
|
|
||||||
<UserIcon className="h-[40px] w-[40px] mr-1" />
|
|
||||||
Register
|
|
||||||
</ToggleButton>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="container fixed inset-0 flex flex-col items-center justify-around z-[9999]
|
|
||||||
h-[75vh] m-auto min-w-[45vw] w-fit py-[50px] rounded-[5rem]"
|
|
||||||
style={{ "--spin-duration": spinDuration } as React.CSSProperties}
|
|
||||||
>
|
>
|
||||||
|
<LogInIcon className="h-[40px] w-[40px] mr-1" />
|
||||||
|
Login
|
||||||
|
</ToggleButton>
|
||||||
|
|
||||||
{/*Border Container*/}
|
{/* Register Toggle */}
|
||||||
<div
|
<ToggleButton
|
||||||
id="border-container"
|
toggled={selectedTab === "Register"}
|
||||||
className="front-content fixed inset-0 bg-gradient-to-br from-blue-950 via-purple-500 to-violet-800 flex flex-col justify-center
|
extraClasses="flex flex-col items-center px-8 duration-250 transition-transform hover:translate-y-[-50px] z-[9001]"
|
||||||
|
onClick={() => setSelectedTab("Register")}
|
||||||
|
>
|
||||||
|
<UserIcon className="h-[40px] w-[40px] mr-1" />
|
||||||
|
Register
|
||||||
|
</ToggleButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="container fixed inset-0 flex flex-col items-center justify-around z-[9999]
|
||||||
|
h-[75vh] m-auto min-w-[45vw] w-fit py-[50px] rounded-[5rem]"
|
||||||
|
style={{ "--spin-duration": spinDuration } as React.CSSProperties}
|
||||||
|
>
|
||||||
|
{/*Border Container*/}
|
||||||
|
<div
|
||||||
|
id="border-container"
|
||||||
|
className="front-content fixed inset-0 bg-gradient-to-br from-blue-950 via-purple-500 to-violet-800 flex flex-col justify-center
|
||||||
z-50 h-[70vh] mr-0.5 mb-0.5 m-auto min-w-[40vw] w-fit py-[50px] rounded-[2rem] transition-all"
|
z-50 h-[70vh] mr-0.5 mb-0.5 m-auto min-w-[40vw] w-fit py-[50px] rounded-[2rem] transition-all"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="login-methods"
|
||||||
|
className=" w-full flex flex-row items-center justify-evenly"
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
id="login-methods"
|
onClick={onClose}
|
||||||
className=" w-full flex flex-row items-center justify-evenly"
|
className="absolute top-[1rem] right-[2rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
||||||
>
|
>
|
||||||
<button
|
✕
|
||||||
onClick={onClose}
|
</button>
|
||||||
className="absolute top-[1rem] right-[2rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{selectedTab === "Login" ? (
|
|
||||||
<LoginForm onSubmit={handleSubmit} />
|
|
||||||
) : (
|
|
||||||
<RegisterForm onSubmit={handleSubmit} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{selectedTab === "Login" ? (
|
||||||
|
<LoginForm onSubmit={handleSubmit} />
|
||||||
|
) : (
|
||||||
|
<RegisterForm onSubmit={handleSubmit} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface ListItemProps {
|
|||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
streamer?: string;
|
streamer?: string;
|
||||||
|
streamCategory?: string;
|
||||||
viewers: number;
|
viewers: number;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
onItemClick?: () => void;
|
onItemClick?: () => void;
|
||||||
@@ -41,6 +42,7 @@ const ListRow: React.FC<ListRowProps> = ({
|
|||||||
type={item.type}
|
type={item.type}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
streamer={item.type === "stream" ? item.streamer : undefined}
|
streamer={item.type === "stream" ? item.streamer : undefined}
|
||||||
|
streamCategory={item.type === "stream" ? item.streamCategory : undefined}
|
||||||
viewers={item.viewers}
|
viewers={item.viewers}
|
||||||
thumbnail={item.thumbnail}
|
thumbnail={item.thumbnail}
|
||||||
onItemClick={() => onClick?.(item.id, item.streamer || item.title)}
|
onItemClick={() => onClick?.(item.id, item.streamer || item.title)}
|
||||||
@@ -52,10 +54,11 @@ const ListRow: React.FC<ListRowProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Individual list entry component
|
// Individual list entry component
|
||||||
const ListItem: React.FC<ListItemProps> = ({
|
export const ListItem: React.FC<ListItemProps> = ({
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
streamer,
|
streamer,
|
||||||
|
streamCategory,
|
||||||
viewers,
|
viewers,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
onItemClick,
|
onItemClick,
|
||||||
@@ -79,6 +82,7 @@ const ListItem: React.FC<ListItemProps> = ({
|
|||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<h3 className="font-semibold text-lg text-center">{title}</h3>
|
<h3 className="font-semibold text-lg text-center">{title}</h3>
|
||||||
{type === "stream" && <p className="font-bold">{streamer}</p>}
|
{type === "stream" && <p className="font-bold">{streamer}</p>}
|
||||||
|
{type === "stream" && <p className="text-sm text-gray-300">{streamCategory}</p>}
|
||||||
<p className="text-sm text-gray-300">{viewers} viewers</p>
|
<p className="text-sm text-gray-300">{viewers} viewers</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
@@ -11,29 +11,17 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Input from "./Input";
|
import Input from "./Input";
|
||||||
import AuthModal from "../Auth/AuthModal";
|
import AuthModal from "../Auth/AuthModal";
|
||||||
|
import { useAuthModal } from "../../hooks/useAuthModal";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
variant?: "home" | "default";
|
variant?: "home" | "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
const Navbar: React.FC<NavbarProps> = ({
|
const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||||
variant = "default",
|
|
||||||
}) => {
|
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
|
||||||
const { isLoggedIn } = useAuth();
|
const { isLoggedIn } = useAuth();
|
||||||
|
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||||
const [showSideBar, setShowSideBar] = useState(false);
|
const [showSideBar, setShowSideBar] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showAuthModal) {
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = "unset";
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = "unset";
|
|
||||||
};
|
|
||||||
}, [showAuthModal]);
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
console.log("Logging out...");
|
console.log("Logging out...");
|
||||||
@@ -47,12 +35,16 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
|
|
||||||
const handleSideBar = () => {
|
const handleSideBar = () => {
|
||||||
setShowSideBar(!showSideBar);
|
setShowSideBar(!showSideBar);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="navbar"
|
id="navbar"
|
||||||
className={`flex justify-center items-center ${variant === "home" ? "h-[45vh] flex-col" : "h-[15vh] col-span-2 flex-row"}`}
|
className={`flex justify-center items-center ${
|
||||||
|
variant === "home"
|
||||||
|
? "h-[45vh] flex-col"
|
||||||
|
: "h-[15vh] col-span-2 flex-row"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Logo variant={variant} />
|
<Logo variant={variant} />
|
||||||
<Button
|
<Button
|
||||||
@@ -74,26 +66,29 @@ const Navbar: React.FC<NavbarProps> = ({
|
|||||||
|
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
<>
|
<>
|
||||||
<Button onClick={() => handleSideBar()}
|
<Button
|
||||||
extraClasses={`absolute ${showSideBar ? `fixed top-[20px] left-[20px] p-2 text-[1.5rem] text-white hover:text-white
|
onClick={() => handleSideBar()}
|
||||||
|
extraClasses={`absolute ${
|
||||||
|
showSideBar
|
||||||
|
? `fixed top-[20px] left-[20px] p-2 text-[1.5rem] text-white hover:text-white
|
||||||
bg-black/30 hover:bg-purple-500/80 rounded-md border border-gray-300 hover:border-white h
|
bg-black/30 hover:bg-purple-500/80 rounded-md border border-gray-300 hover:border-white h
|
||||||
over:border-b-4 hover:border-l-4 active:border-b-2 active:border-l-2 transition-all ` :
|
over:border-b-4 hover:border-l-4 active:border-b-2 active:border-l-2 transition-all `
|
||||||
"top-[75px] left-[20px]"
|
: "top-[75px] left-[20px]"
|
||||||
} transition-all duration-300 z-[99]`}
|
} transition-all duration-300 z-[99]`}
|
||||||
>
|
>
|
||||||
<SidebarIcon className="top-[0.20em] left-[10em] mr-1 z-[90]" />
|
<SidebarIcon className="top-[0.20em] left-[10em] mr-1 z-[90]" />
|
||||||
</Button>
|
</Button>
|
||||||
<div
|
<div
|
||||||
className={`fixed top-0 left-0 w-[250px] h-screen bg-[var(--sideBar-LightBG)] text-[var(--sideBar-LightText)] z-[90] overflow-y-auto scrollbar-hide
|
className={`fixed top-0 left-0 w-[250px] h-screen bg-[var(--sideBar-LightBG)] text-[var(--sideBar-LightText)] z-[90] overflow-y-auto scrollbar-hide
|
||||||
transition-transform transition-opacity duration-500 ease-in-out ${showSideBar ? "translate-x-0 opacity-100" : "-translate-x-full opacity-0"
|
transition-transform transition-opacity duration-500 ease-in-out ${
|
||||||
}`}
|
showSideBar ? "translate-x-0 opacity-100" : "-translate-x-full opacity-0"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
extraClasses="absolute top-[20px] right-[20px] text-[1rem] flex items-center flex-nowrap"
|
extraClasses="absolute top-[20px] right-[20px] text-[1rem] flex items-center flex-nowrap"
|
||||||
onClick={() => console.log("Settings - TODO")}
|
onClick={() => console.log("Settings - TODO")}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import VideoPage from "../../pages/VideoPage";
|
import VideoPage from "../../pages/VideoPage";
|
||||||
import UserPage from "../../pages/UserPage";
|
import UserPage from "../../pages/UserPage";
|
||||||
|
|
||||||
const StreamerRoute: React.FC = () => {
|
const StreamerRoute: React.FC = () => {
|
||||||
const { streamerName } = useParams<{ streamerName: string }>();
|
const { streamerName } = useParams();
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [isLive, setIsLive] = useState<boolean>(false);
|
const [isLive, setIsLive] = useState<boolean>(false);
|
||||||
const [streamId, setStreamId] = useState<number>(0);
|
const [streamId, setStreamId] = useState<number>(0);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkStreamStatus = async () => {
|
const checkStreamStatus = async () => {
|
||||||
@@ -27,7 +28,7 @@ const StreamerRoute: React.FC = () => {
|
|||||||
checkStreamStatus();
|
checkStreamStatus();
|
||||||
|
|
||||||
// Poll for live status changes
|
// Poll for live status changes
|
||||||
const interval = setInterval(checkStreamStatus, 10000); // Check every 10 second
|
const interval = setInterval(checkStreamStatus, 60000); // Check every minute
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [streamerName]);
|
}, [streamerName]);
|
||||||
@@ -38,17 +39,15 @@ const StreamerRoute: React.FC = () => {
|
|||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Or your loading component
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// streamId=0 is a special case for the streamer's latest stream
|
// streamId=0 is a special case for the streamer's latest stream
|
||||||
return isLive ? (
|
return isLive ? (
|
||||||
<VideoPage streamerId={streamId} />
|
<VideoPage streamerId={streamId} />
|
||||||
) : streamerName ? (
|
) : streamerName ? (
|
||||||
<UserPage />
|
navigate(`/user/${streamerName}`)
|
||||||
) : (
|
) : (
|
||||||
<div>Error: Streamer not found</div>
|
<div>Streamer not found</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import Input from "../Layout/Input";
|
import Input from "../Layout/Input";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
|
||||||
import { useSocket } from "../../context/SocketContext";
|
|
||||||
import Button from "../Layout/Button";
|
import Button from "../Layout/Button";
|
||||||
import AuthModal from "../Auth/AuthModal";
|
import AuthModal from "../Auth/AuthModal";
|
||||||
|
import { useAuthModal } from "../../hooks/useAuthModal";
|
||||||
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
import { useSocket } from "../../context/SocketContext";
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
chatter_username: string;
|
chatter_username: string;
|
||||||
@@ -16,12 +17,16 @@ interface ChatPanelProps {
|
|||||||
onViewerCountChange?: (count: number) => void;
|
onViewerCountChange?: (count: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, onViewerCountChange }) => {
|
const ChatPanel: React.FC<ChatPanelProps> = ({
|
||||||
|
streamId,
|
||||||
|
onViewerCountChange,
|
||||||
|
}) => {
|
||||||
|
const { isLoggedIn, username } = useAuth();
|
||||||
|
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||||
const { socket, isConnected } = useSocket();
|
const { socket, isConnected } = useSocket();
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [inputMessage, setInputMessage] = useState("");
|
const [inputMessage, setInputMessage] = useState("");
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const { isLoggedIn, username } = useAuth();
|
|
||||||
|
|
||||||
// Join chat room when component mounts
|
// Join chat room when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,17 +62,16 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, onViewerCountChange })
|
|||||||
|
|
||||||
// Handle incoming messages
|
// Handle incoming messages
|
||||||
socket.on("new_message", (data: ChatMessage) => {
|
socket.on("new_message", (data: ChatMessage) => {
|
||||||
console.log("New message:", data);
|
|
||||||
setMessages((prev) => [...prev, data]);
|
setMessages((prev) => [...prev, data]);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle live viewership
|
// Handle live viewership
|
||||||
socket.on("status", (data: any) => {
|
socket.on("status", (data: any) => {
|
||||||
console.log("Live viewership: ", data) // returns dictionary {message: message, num_viewers: num_viewers}
|
console.log("Live viewership: ", data); // returns dictionary {message: message, num_viewers: num_viewers}
|
||||||
if (onViewerCountChange && data.num_viewers) {
|
if (onViewerCountChange && data.num_viewers) {
|
||||||
onViewerCountChange(data.num_viewers);
|
onViewerCountChange(data.num_viewers);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Cleanup function
|
// Cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
@@ -108,20 +112,6 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, onViewerCountChange })
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//added to show login/reg if not
|
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (showAuthModal) {
|
|
||||||
document.body.style.overflow = "hidden";
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = "unset";
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = "unset";
|
|
||||||
};
|
|
||||||
}, [showAuthModal]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="chat-panel"
|
id="chat-panel"
|
||||||
@@ -166,8 +156,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, onViewerCountChange })
|
|||||||
value={inputMessage}
|
value={inputMessage}
|
||||||
onChange={(e) => setInputMessage(e.target.value)}
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
onKeyDown={handleKeyPress}
|
onKeyDown={handleKeyPress}
|
||||||
placeholder={isLoggedIn ? "Type a message..." : "Login to chat"}
|
placeholder="Type a message..."
|
||||||
disabled={!isLoggedIn}
|
|
||||||
extraClasses="flex-grow w-full focus:w-full"
|
extraClasses="flex-grow w-full focus:w-full"
|
||||||
onClick={() => !isLoggedIn && setShowAuthModal(true)}
|
onClick={() => !isLoggedIn && setShowAuthModal(true)}
|
||||||
/>
|
/>
|
||||||
@@ -181,18 +170,14 @@ const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, onViewerCountChange })
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
extraClasses="text-[1rem] flex items-center flex-nowrap z-[999]"
|
extraClasses="text-[1rem] flex items-center flex-nowrap"
|
||||||
onClick={() => setShowAuthModal(true)}
|
onClick={() => setShowAuthModal(true)}
|
||||||
>
|
>
|
||||||
Login to Chat
|
Login to Chat
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showAuthModal && (
|
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
|
||||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
|
||||||
<AuthModal onClose={() => setShowAuthModal(false)} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ streamId }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="video-container"
|
id="video-container"
|
||||||
className="min-w-[65vw] w-full h-full flex items-center bg-gray-900 rounded-lg"
|
className="min-w-[65vw] w-full h-full flex justify-center items-center bg-gray-900 rounded-lg"
|
||||||
style={{ gridArea: "1 / 1 / 2 / 2" }}
|
style={{ gridArea: "1 / 1 / 2 / 2" }}
|
||||||
>
|
>
|
||||||
<div ref={videoRef} id="video-player" className="w-full" />
|
<div ref={videoRef} id="video-player" className="w-full max-w-[80vw] self-center" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface Item {
|
|||||||
interface StreamItem extends Item {
|
interface StreamItem extends Item {
|
||||||
type: "stream";
|
type: "stream";
|
||||||
streamer: string;
|
streamer: string;
|
||||||
|
streamCategory: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryItem extends Item {
|
interface CategoryItem extends Item {
|
||||||
@@ -47,13 +48,13 @@ export function StreamsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
id: stream.user_id,
|
id: stream.user_id,
|
||||||
title: stream.title,
|
title: stream.title,
|
||||||
streamer: stream.username,
|
streamer: stream.username,
|
||||||
|
streamCategory: stream.category_name,
|
||||||
viewers: stream.num_viewers,
|
viewers: stream.num_viewers,
|
||||||
thumbnail:
|
thumbnail:
|
||||||
stream.thumbnail ||
|
stream.thumbnail ||
|
||||||
`/images/thumbnails/categories/${stream.category_name
|
`/images/thumbnails/categories/${stream.category_name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/ /g, "_")}.webp`,
|
.replace(/ /g, "_")}.webp`
|
||||||
category: stream.category_name,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setFeaturedStreams(extractedData);
|
setFeaturedStreams(extractedData);
|
||||||
|
|||||||
21
frontend/src/hooks/useAuthModal.ts
Normal file
21
frontend/src/hooks/useAuthModal.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useAuthModal() {
|
||||||
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAuthModal) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [showAuthModal]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
showAuthModal,
|
||||||
|
setShowAuthModal,
|
||||||
|
};
|
||||||
|
}
|
||||||
65
frontend/src/hooks/useFollow.ts
Normal file
65
frontend/src/hooks/useFollow.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
export function useFollow() {
|
||||||
|
const [isFollowing, setIsFollowing] = useState<boolean>(false);
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
|
const checkFollowStatus = async (username: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/user/following/${username}`);
|
||||||
|
const data = await response.json();
|
||||||
|
setIsFollowing(data.following);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking follow status:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const followUser = async (userId: number, setShowAuthModal?: (show: boolean) => void) => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
setShowAuthModal?.(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/user/follow/${userId}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
console.log(`Now following user ${userId}`);
|
||||||
|
setIsFollowing(true);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to follow user ${userId}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error following user:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfollowUser = async (userId: number, setShowAuthModal?: (show: boolean) => void) => {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
setShowAuthModal?.(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/user/unfollow/${userId}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
console.log(`Unfollowed user ${userId}`);
|
||||||
|
setIsFollowing(false);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to unfollow user ${userId}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error unfollowing user:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFollowing,
|
||||||
|
setIsFollowing,
|
||||||
|
checkFollowStatus,
|
||||||
|
followUser,
|
||||||
|
unfollowUser
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,24 +1,45 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import Navbar from "../components/Layout/Navbar";
|
import Navbar from "../components/Layout/Navbar";
|
||||||
import { useParams } from "react-router-dom";
|
import AuthModal from "../components/Auth/AuthModal";
|
||||||
|
import { useAuthModal } from "../hooks/useAuthModal";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { ListItem } from "../components/Layout/ListRow";
|
||||||
|
import { useFollow } from "../hooks/useFollow";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Button from "../components/Layout/Button";
|
||||||
|
|
||||||
interface UserProfileData {
|
interface UserProfileData {
|
||||||
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
bio: string;
|
bio: string;
|
||||||
followerCount: number;
|
followerCount: number;
|
||||||
isPartnered: boolean;
|
isPartnered: boolean;
|
||||||
|
isLive: boolean;
|
||||||
|
currentStreamTitle?: string;
|
||||||
|
currentStreamCategory?: string;
|
||||||
|
currentStreamViewers?: number;
|
||||||
|
currentStreamStartTime?: string;
|
||||||
|
currentStreamThumbnail?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserPage: React.FC = () => {
|
const UserPage: React.FC = () => {
|
||||||
const [profileData, setProfileData] = useState<UserProfileData | null>(null);
|
const [userPageVariant, setUserPageVariant] = useState<
|
||||||
|
"personal" | "streamer" | "user" | "admin"
|
||||||
|
>("user");
|
||||||
|
const [profileData, setProfileData] = useState<UserProfileData>();
|
||||||
|
const { isFollowing, checkFollowStatus, followUser, unfollowUser } = useFollow();
|
||||||
|
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||||
const { username: loggedInUsername } = useAuth();
|
const { username: loggedInUsername } = useAuth();
|
||||||
const { username } = useParams();
|
const { username } = useParams();
|
||||||
let userPageVariant = "user";
|
const navigate = useNavigate();
|
||||||
|
|
||||||
let setUserPageVariant = (currentStream: string) => {
|
const bgColors = {
|
||||||
if (username === loggedInUsername) userPageVariant = "personal";
|
personal: "",
|
||||||
else if (currentStream) userPageVariant = "streamer";
|
streamer: "bg-gradient-radial from-[#ff00f1] via-[#0400ff] to-[#ff0000]", // offline streamer
|
||||||
|
user: "bg-gradient-radial from-[#ff00f1] via-[#0400ff] to-[#ff00f1]",
|
||||||
|
admin:
|
||||||
|
"bg-gradient-to-r from-[rgb(255,0,0)] via-transparent to-[rgb(0,0,255)]",
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -27,15 +48,52 @@ const UserPage: React.FC = () => {
|
|||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setProfileData({
|
setProfileData({
|
||||||
|
id: data.user_id,
|
||||||
username: data.username,
|
username: data.username,
|
||||||
bio: data.bio || "This user hasn't written a bio yet.",
|
bio: data.bio || "This user hasn't written a bio yet.",
|
||||||
followerCount: data.num_followers || 0,
|
followerCount: data.num_followers || 0,
|
||||||
isPartnered: data.isPartnered || false,
|
isPartnered: data.isPartnered || false,
|
||||||
|
isLive: data.is_live,
|
||||||
|
currentStreamTitle: "",
|
||||||
|
currentStreamCategory: "",
|
||||||
|
currentStreamViewers: 0,
|
||||||
|
currentStreamThumbnail: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
setUserPageVariant(data.current_stream_title);
|
if (data.is_live) {
|
||||||
|
// Fetch stream data for this streamer
|
||||||
|
fetch(`/api/streams/${data.user_id}/data`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((streamData) => {
|
||||||
|
setProfileData((prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return {
|
||||||
|
...prevData,
|
||||||
|
currentStreamTitle: streamData.title,
|
||||||
|
currentStreamCategory: streamData.category_id,
|
||||||
|
currentStreamViewers: streamData.num_viewers,
|
||||||
|
currentStreamStartTime: streamData.start_time,
|
||||||
|
currentStreamThumbnail:
|
||||||
|
streamData.thumbnail ||
|
||||||
|
`/images/thumbnails/categories/${streamData.category_name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/ /g, "_")}.webp`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
let variant: "user" | "streamer" | "personal" | "admin";
|
||||||
|
if (username === loggedInUsername) variant = "personal";
|
||||||
|
else if (streamData.title) variant = "streamer";
|
||||||
|
// else if (data.isAdmin) variant = "admin";
|
||||||
|
else variant = "user";
|
||||||
|
setUserPageVariant(variant);
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("Error fetching stream data:", err));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => console.error("Error fetching profile data:", err));
|
.catch((err) => console.error("Error fetching profile data:", err));
|
||||||
|
|
||||||
|
// Check if the *logged-in* user is following this user
|
||||||
|
if (loggedInUsername && username) checkFollowStatus(username);
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
||||||
if (!profileData) {
|
if (!profileData) {
|
||||||
@@ -45,12 +103,17 @@ const UserPage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 text-white">
|
<div
|
||||||
|
className={`min-h-screen ${
|
||||||
|
profileData.isLive
|
||||||
|
? "bg-gradient-radial from-[#ff00f1] via-[#0400ff] to-[#2efd2d]"
|
||||||
|
: bgColors[userPageVariant]
|
||||||
|
} text-white flex flex-col`}
|
||||||
|
>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="mx-auto px-4 py-8">
|
<div className="flex justify-evenly justify-self-center items-center h-full px-4 py-8">
|
||||||
<div className="grid grid-cols-3 gap-8">
|
<div className="grid grid-cols-3 w-full gap-8">
|
||||||
{/* Profile Section - Left Third */}
|
{/* Profile Section - Left Third */}
|
||||||
<div
|
<div
|
||||||
id="profile"
|
id="profile"
|
||||||
@@ -82,26 +145,46 @@ const UserPage: React.FC = () => {
|
|||||||
<h1 className="text-3xl font-bold mb-2">
|
<h1 className="text-3xl font-bold mb-2">
|
||||||
{profileData.username}
|
{profileData.username}
|
||||||
</h1>
|
</h1>
|
||||||
<small className="text-green-400" >{userPageVariant.toUpperCase()}</small>
|
<small className="text-green-400">
|
||||||
|
{userPageVariant.toUpperCase()}
|
||||||
|
</small>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 mb-6">
|
{/* Follower Count */}
|
||||||
<span className="text-gray-400">
|
{userPageVariant === "streamer" && (
|
||||||
{profileData.followerCount.toLocaleString()} followers
|
<>
|
||||||
</span>
|
<div className="flex items-center space-x-2 mb-6">
|
||||||
{profileData.isPartnered && (
|
<span className="text-gray-400">
|
||||||
<span className="bg-purple-600 text-white text-sm px-2 py-1 rounded">
|
{profileData.followerCount.toLocaleString()} followers
|
||||||
Partner
|
</span>
|
||||||
</span>
|
{profileData.isPartnered && (
|
||||||
)}
|
<span className="bg-purple-600 text-white text-sm px-2 py-1 rounded">
|
||||||
</div>
|
Partner
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded-lg mb-4 transition-colors">
|
{/* (Un)Follow Button */}
|
||||||
Follow
|
{!isFollowing ? (
|
||||||
</button>
|
<Button
|
||||||
|
extraClasses="w-full bg-purple-700 hover:bg-[#28005e]"
|
||||||
|
onClick={() => followUser(profileData.id, setShowAuthModal)}
|
||||||
|
>
|
||||||
|
Follow
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
extraClasses="w-full bg-[#a80000]"
|
||||||
|
onClick={() => unfollowUser(profileData?.id, setShowAuthModal)}
|
||||||
|
>
|
||||||
|
Unfollow
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bio Section */}
|
{/* Bio Section */}
|
||||||
<div className="mt-6">
|
<div className="mt-6 text-center">
|
||||||
<h2 className="text-xl font-semibold mb-3">
|
<h2 className="text-xl font-semibold mb-3">
|
||||||
About {profileData.username}
|
About {profileData.username}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -109,34 +192,47 @@ const UserPage: React.FC = () => {
|
|||||||
{profileData.bio}
|
{profileData.bio}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Additional Stats */}
|
|
||||||
<div className="mt-6 pt-6 border-t border-gray-700">
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-bold text-purple-400">0</div>
|
|
||||||
<div className="text-sm text-gray-400">Total Views</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-2xl font-bold text-purple-400">0</div>
|
|
||||||
<div className="text-sm text-gray-400">Following</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<div
|
<div
|
||||||
id="content"
|
id="content"
|
||||||
className="col-span-2 bg-gray-800 rounded-lg p-6 flex flex-col"
|
className="col-span-2 bg-gray-800 rounded-lg p-6 grid grid-rows-[auto_1fr] items-center justify-center"
|
||||||
>
|
>
|
||||||
<h2 className="text-2xl font-bold mb-4">Past Broadcasts</h2>
|
{userPageVariant === "streamer" && (
|
||||||
<div className="text-gray-400 flex h-full rounded-none">
|
<>
|
||||||
No past broadcasts found
|
{/* ↓↓ Current Stream ↓↓ */}
|
||||||
</div>
|
{profileData.isLive && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-2xl bg-[#ff0000] border py-4 px-12 font-black mb-4 rounded-[4rem]">
|
||||||
|
Currently Live!
|
||||||
|
</h2>
|
||||||
|
<ListItem
|
||||||
|
id={profileData.id}
|
||||||
|
type="stream"
|
||||||
|
title={profileData.currentStreamTitle || ""}
|
||||||
|
streamer=""
|
||||||
|
viewers={profileData.currentStreamViewers || 0}
|
||||||
|
thumbnail={profileData.currentStreamThumbnail}
|
||||||
|
onItemClick={() => {
|
||||||
|
navigate(`/${profileData.username}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* ↓↓ VODS ↓↓ */}
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Past Broadcasts</h2>
|
||||||
|
<div className="text-gray-400 rounded-none">
|
||||||
|
No past broadcasts found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import Navbar from "../components/Layout/Navbar";
|
|||||||
import Button, { ToggleButton } from "../components/Layout/Button";
|
import Button, { ToggleButton } from "../components/Layout/Button";
|
||||||
import ChatPanel from "../components/Video/ChatPanel";
|
import ChatPanel from "../components/Video/ChatPanel";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useAuthModal } from "../hooks/useAuthModal";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import { useFollow } from "../hooks/useFollow";
|
||||||
import VideoPlayer from "../components/Video/VideoPlayer";
|
import VideoPlayer from "../components/Video/VideoPlayer";
|
||||||
import { SocketProvider } from "../context/SocketContext";
|
import { SocketProvider } from "../context/SocketContext";
|
||||||
|
import AuthModal from "../components/Auth/AuthModal";
|
||||||
|
|
||||||
interface VideoPageProps {
|
interface VideoPageProps {
|
||||||
streamerId: number;
|
streamerId: number;
|
||||||
@@ -24,6 +27,9 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamerId }) => {
|
|||||||
const [streamData, setStreamData] = useState<StreamDataProps>();
|
const [streamData, setStreamData] = useState<StreamDataProps>();
|
||||||
const [viewerCount, setViewerCount] = useState(0);
|
const [viewerCount, setViewerCount] = useState(0);
|
||||||
const [isChatOpen, setIsChatOpen] = useState(true);
|
const [isChatOpen, setIsChatOpen] = useState(true);
|
||||||
|
const { isFollowing, checkFollowStatus, followUser, unfollowUser } =
|
||||||
|
useFollow();
|
||||||
|
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||||
// const [showCheckout, setShowCheckout] = useState(false);
|
// const [showCheckout, setShowCheckout] = useState(false);
|
||||||
// const showReturn = window.location.search.includes("session_id");
|
// const showReturn = window.location.search.includes("session_id");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -40,6 +46,7 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamerId }) => {
|
|||||||
// document.body.style.overflow = "unset";
|
// document.body.style.overflow = "unset";
|
||||||
// };
|
// };
|
||||||
// }, [showCheckout]);
|
// }, [showCheckout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch stream data for this streamer
|
// Fetch stream data for this streamer
|
||||||
fetch(`/api/streams/${streamerId}/data`).then((res) => {
|
fetch(`/api/streams/${streamerId}/data`).then((res) => {
|
||||||
@@ -57,6 +64,9 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamerId }) => {
|
|||||||
categoryName: data.category_name,
|
categoryName: data.category_name,
|
||||||
};
|
};
|
||||||
setStreamData(transformedData);
|
setStreamData(transformedData);
|
||||||
|
|
||||||
|
// Check if the logged-in user is following this streamer
|
||||||
|
if (isLoggedIn) checkFollowStatus(data.username);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Error fetching stream data:", error);
|
console.error("Error fetching stream data:", error);
|
||||||
@@ -64,6 +74,21 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamerId }) => {
|
|||||||
});
|
});
|
||||||
}, [streamerId]);
|
}, [streamerId]);
|
||||||
|
|
||||||
|
// Keyboard shortcut to toggle chat
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "c") {
|
||||||
|
setIsChatOpen((prev) => !prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyPress);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyPress);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const toggleChat = () => {
|
const toggleChat = () => {
|
||||||
setIsChatOpen((prev) => !prev);
|
setIsChatOpen((prev) => !prev);
|
||||||
};
|
};
|
||||||
@@ -86,9 +111,11 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamerId }) => {
|
|||||||
<ToggleButton
|
<ToggleButton
|
||||||
onClick={toggleChat}
|
onClick={toggleChat}
|
||||||
toggled={isChatOpen}
|
toggled={isChatOpen}
|
||||||
extraClasses="absolute top-[70px] right-[20px] text-[1rem] flex items-center flex-nowrap"
|
extraClasses="group cursor-pointer absolute top-[70px] right-[20px] text-[1rem] flex items-center flex-nowrap"
|
||||||
>
|
>
|
||||||
{isChatOpen ? "Hide Chat" : "Show Chat"}
|
{isChatOpen ? "Hide Chat" : "Show Chat"}
|
||||||
|
|
||||||
|
<small className="absolute right-0 left-0 -bottom-0 group-hover:-bottom-5 opacity-0 group-hover:opacity-100 text-white transition-all">Press C</small>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|
||||||
<ChatPanel
|
<ChatPanel
|
||||||
@@ -98,53 +125,66 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamerId }) => {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
id="stream-info"
|
id="stream-info"
|
||||||
className="flex flex-col gap-2 p-4 text-white"
|
className="flex flex-row items-center justify-evenly gap-2 p-8 text-white text-xl"
|
||||||
style={{ gridArea: "2 / 1 / 3 / 2" }}
|
style={{ gridArea: "2 / 1 / 3 / 2" }}
|
||||||
>
|
>
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-3xl text-center font-bold">
|
||||||
{streamData ? streamData.streamTitle : "Loading..."}
|
{streamData ? streamData.streamTitle : "Loading..."}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-col gap-2">
|
<div>
|
||||||
<div
|
<Button
|
||||||
id="streamer"
|
extraClasses="flex flex-col items-center font-bold bg-[#ff0000] p-4 px-12 rounded-[3rem] cursor-pointer"
|
||||||
className="flex items-center gap-2 cursor-pointer"
|
|
||||||
onClick={() => navigate(`/user/${streamerName}`)}
|
onClick={() => navigate(`/user/${streamerName}`)}
|
||||||
>
|
>
|
||||||
|
<h1>{streamData ? streamData.streamerName : "Loading..."}</h1>
|
||||||
<img
|
<img
|
||||||
src="/images/monkey.png"
|
src="/images/monkey.png"
|
||||||
alt="streamer"
|
alt="streamer"
|
||||||
className="w-10 h-10 bg-indigo-500 rounded-full"
|
className="w-30 h-10 bg-indigo-500 rounded-full"
|
||||||
/>
|
/>
|
||||||
<span>
|
</Button>
|
||||||
{streamData ? streamData.streamerName : "Loading..."}
|
|
||||||
</span>
|
{/* (Un)Follow Button */}
|
||||||
</div>
|
{!isFollowing ? (
|
||||||
<div className="flex items-center gap-2">
|
<Button
|
||||||
<span className="font-semibold">Viewer Count:</span>
|
extraClasses="w-full bg-purple-700 hover:bg-[#28005e]"
|
||||||
<span>{viewerCount}</span>
|
onClick={() => followUser(streamerId, setShowAuthModal)}
|
||||||
</div>
|
>
|
||||||
<div className="flex items-center gap-2">
|
Follow
|
||||||
<span className="font-semibold">Started At:</span>
|
</Button>
|
||||||
<span>
|
) : (
|
||||||
{streamData
|
<Button
|
||||||
? new Date(streamData.startTime).toLocaleString()
|
extraClasses="w-full bg-green-400/30 hover:content-['Unfollow'] hover:border-[#a80000]"
|
||||||
: "Loading..."}
|
onClick={() => unfollowUser(streamerId, setShowAuthModal)}
|
||||||
</span>
|
>
|
||||||
</div>
|
Following
|
||||||
<div className="flex items-center gap-2">
|
</Button>
|
||||||
<span className="font-semibold">Category ID:</span>
|
)}
|
||||||
<span>
|
</div>
|
||||||
{streamData ? streamData.categoryName : "Loading..."}
|
<div className="flex flex-col items-center font-bold">
|
||||||
</span>
|
<span className="font-thin">Viewers</span>
|
||||||
</div>
|
<span>{viewerCount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center font-bold">
|
||||||
|
<span className="font-thin">Started</span>
|
||||||
|
<span>
|
||||||
|
{streamData
|
||||||
|
? new Date(streamData.startTime).toLocaleString()
|
||||||
|
: "Loading..."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center font-bold">
|
||||||
|
<span className="font-thin">Category</span>
|
||||||
|
<span>{streamData ? streamData.categoryName : "Loading..."}</span>
|
||||||
</div>
|
</div>
|
||||||
{isLoggedIn && (
|
|
||||||
<Button extraClasses="mx-auto mb-4">Payment Screen Test</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{isLoggedIn && (
|
||||||
|
<Button extraClasses="mx-auto mb-4">Payment Screen Test</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* {showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />} */}
|
{/* {showCheckout && <CheckoutForm onClose={() => setShowCheckout(false)} />} */}
|
||||||
{/* {showReturn && <Return />} */}
|
{/* {showReturn && <Return />} */}
|
||||||
|
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
|
||||||
</div>
|
</div>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default {
|
|||||||
|
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
logo: "linear-gradient(45deg, #60A5FA, #8B5CF6, #EC4899, #FACC15,#60A5FA, #8B5CF6, #EC4899, #FACC15)",
|
logo: "linear-gradient(45deg, #60A5FA, #8B5CF6, #EC4899, #FACC15,#60A5FA, #8B5CF6, #EC4899, #FACC15)",
|
||||||
|
'gradient-radial': 'radial-gradient(circle, var(--tw-gradient-stops))',
|
||||||
},
|
},
|
||||||
|
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ def get_following_categories_streams():
|
|||||||
@stream_bp.route('/user/<string:username>/status')
|
@stream_bp.route('/user/<string:username>/status')
|
||||||
def get_user_live_status(username):
|
def get_user_live_status(username):
|
||||||
"""
|
"""
|
||||||
Returns a streamer's status, if they are live or not and their most recent stream (their current stream if live)
|
Returns a streamer's status, if they are live or not and their most recent stream (as a vod) (their current stream if live)
|
||||||
"""
|
"""
|
||||||
user_id = get_user_id(username)
|
user_id = get_user_id(username)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ r = redis.from_url(redis_url, decode_responses=True)
|
|||||||
user_bp = Blueprint("user", __name__)
|
user_bp = Blueprint("user", __name__)
|
||||||
|
|
||||||
@user_bp.route('/user/<string:username>')
|
@user_bp.route('/user/<string:username>')
|
||||||
def get_user_data(username):
|
def get_user_data(username: str):
|
||||||
"""
|
"""
|
||||||
Returns a given user's data
|
Returns a given user's data
|
||||||
"""
|
"""
|
||||||
@@ -45,43 +45,38 @@ def user_subscription_expiration(subscribed_id: int):
|
|||||||
return jsonify({"remaining_time": remaining_time})
|
return jsonify({"remaining_time": remaining_time})
|
||||||
|
|
||||||
## Follow Routes
|
## Follow Routes
|
||||||
@user_bp.route('/user/<int:user_id>/follows/<int:followed_id>')
|
@user_bp.route('/user/following/<string:followed_username>')
|
||||||
def user_following(user_id: int, followed_id: int):
|
def user_following(followed_username: str):
|
||||||
"""
|
"""
|
||||||
Checks to see if user is following a streamer
|
Checks to see if user is following another streamer
|
||||||
"""
|
"""
|
||||||
|
user_id = session.get("user_id")
|
||||||
|
followed_id = get_user_id(followed_username)
|
||||||
if is_following(user_id, followed_id):
|
if is_following(user_id, followed_id):
|
||||||
return jsonify({"following": True})
|
return jsonify({"following": True})
|
||||||
return jsonify({"following": False})
|
return jsonify({"following": False})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_bp.route('/user/follow/<string:username>')
|
@user_bp.route('/user/follow/<int:target_user_id>')
|
||||||
def follow_user(username):
|
def follow_user(target_user_id: int):
|
||||||
"""
|
"""
|
||||||
Follows a user
|
Follows a user
|
||||||
"""
|
"""
|
||||||
user_id = session.get("user_id")
|
user_id = session.get("user_id")
|
||||||
following_id = get_user_id(username)
|
return follow(user_id, target_user_id)
|
||||||
if follow(user_id, following_id):
|
|
||||||
return jsonify({"success": True,
|
|
||||||
"already_following": False})
|
|
||||||
return jsonify({"success": True,
|
|
||||||
"already_following": True})
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_bp.route('/user/unfollow/<string:username>')
|
@user_bp.route('/user/unfollow/<int:target_user_id>')
|
||||||
def unfollow_user(username):
|
def unfollow_user(target_user_id: int):
|
||||||
"""
|
"""
|
||||||
Unfollows a user
|
Unfollows a user
|
||||||
"""
|
"""
|
||||||
user_id = session.get("user_id")
|
user_id = session.get("user_id")
|
||||||
followed_id = get_user_id(username)
|
return unfollow(user_id, target_user_id)
|
||||||
unfollow(user_id, followed_id)
|
|
||||||
return jsonify({"success": True})
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@user_bp.route('/user/following')
|
@user_bp.route('/user/following')
|
||||||
def get_followed_streamers_():
|
def get_followed_streamers():
|
||||||
"""
|
"""
|
||||||
Queries DB to get a list of followed streamers
|
Queries DB to get a list of followed streamers
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -88,32 +88,33 @@ def is_following(user_id: int, followed_id: int) -> bool:
|
|||||||
""", (user_id, followed_id))
|
""", (user_id, followed_id))
|
||||||
return bool(result)
|
return bool(result)
|
||||||
|
|
||||||
def follow(user_id: int, following_id: int):
|
def follow(user_id: int, followed_id: int):
|
||||||
"""
|
"""
|
||||||
Follows following_id user from user_id user
|
Follows followed_id user from user_id user
|
||||||
"""
|
"""
|
||||||
if not is_following(user_id, following_id):
|
if is_following(user_id, followed_id):
|
||||||
with Database() as db:
|
return {"success": False, "error": "Already following user"}, 400
|
||||||
db.execute("""
|
|
||||||
INSERT INTO follows (user_id, followed_id)
|
with Database() as db:
|
||||||
VALUES(?,?);
|
db.execute("""
|
||||||
""", (user_id, following_id))
|
INSERT INTO follows (user_id, followed_id)
|
||||||
return True
|
VALUES(?,?);
|
||||||
return False
|
""", (user_id, followed_id))
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
def unfollow(user_id: int, followed_id: int):
|
def unfollow(user_id: int, followed_id: int):
|
||||||
"""
|
"""
|
||||||
Unfollows follow_id user from user_id user
|
Unfollows followed_id user from user_id user
|
||||||
"""
|
"""
|
||||||
if is_following(user_id, followed_id):
|
if not is_following(user_id, followed_id):
|
||||||
with Database() as db:
|
return {"success": False, "error": "Not following user"}, 400
|
||||||
db.execute("""
|
with Database() as db:
|
||||||
DELETE FROM follows
|
db.execute("""
|
||||||
WHERE user_id = ?
|
DELETE FROM follows
|
||||||
AND followed_id = ?
|
WHERE user_id = ?
|
||||||
""", (user_id, followed_id))
|
AND followed_id = ?
|
||||||
return True
|
""", (user_id, followed_id))
|
||||||
return False
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
def subscription_expiration(user_id: int, subscribed_id: int) -> int:
|
def subscription_expiration(user_id: int, subscribed_id: int) -> int:
|
||||||
@@ -197,11 +198,11 @@ def get_followed_streamers(user_id: int) -> Optional[List[dict]]:
|
|||||||
|
|
||||||
def get_user(user_id: int) -> Optional[dict]:
|
def get_user(user_id: int) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Returns username, bio, number of followers, and if user is partnered from user_id
|
Returns information about a user from user_id
|
||||||
"""
|
"""
|
||||||
with Database() as db:
|
with Database() as db:
|
||||||
data = db.fetchone("""
|
data = db.fetchone("""
|
||||||
SELECT username, bio, num_followers, is_partnered FROM users
|
SELECT user_id, username, bio, num_followers, is_partnered, is_live FROM users
|
||||||
WHERE user_id = ?;
|
WHERE user_id = ?;
|
||||||
""", (user_id,))
|
""", (user_id,))
|
||||||
return data
|
return data
|
||||||
Reference in New Issue
Block a user