- Added Chat frontend, interfaces with backend;
- Updated styles for VideoPage; - Added StreamerRoute component; - Remove unused Login and Signup pages; - Update to Navbar and Logo components for new structure on different pages; - Update to auth flow to display error messages to user;
This commit is contained in:
@@ -3,9 +3,7 @@ import { AuthContext } from "./context/AuthContext";
|
|||||||
import { StreamsProvider } from "./context/StreamsContext";
|
import { StreamsProvider } from "./context/StreamsContext";
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||||
import HomePage, { PersonalisedHomePage } from "./pages/HomePage";
|
import HomePage, { PersonalisedHomePage } from "./pages/HomePage";
|
||||||
import VideoPage from "./pages/VideoPage";
|
import StreamerRoute from "./components/Stream/StreamerRoute";
|
||||||
import LoginPage from "./pages/LoginPage";
|
|
||||||
import SignupPage from "./pages/SignupPage";
|
|
||||||
import NotFoundPage from "./pages/NotFoundPage";
|
import NotFoundPage from "./pages/NotFoundPage";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -32,9 +30,7 @@ function App() {
|
|||||||
path="/"
|
path="/"
|
||||||
element={isLoggedIn ? <PersonalisedHomePage /> : <HomePage />}
|
element={isLoggedIn ? <PersonalisedHomePage /> : <HomePage />}
|
||||||
/>
|
/>
|
||||||
<Route path="/:streamerName" element={<VideoPage />} />
|
<Route path="/:streamerName" element={<StreamerRoute />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
|
||||||
<Route path="/signup" element={<SignupPage />} />
|
|
||||||
|
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background: #ff9900;
|
background: #d5d5d5;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="blurring-layer fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm group-has-[input:focus]:backdrop-blur-[5px]"></div>
|
<div id="blurring-layer" className="fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm group-has-[input:focus]:backdrop-blur-[5px]"></div>
|
||||||
|
|
||||||
<div className="modal-container fixed inset-0 bg-black/30 has-[input:focus]:bg-black/40 flex flex-col items-center justify-around z-50 h-[70vh] m-auto min-w-[40vw] w-fit py-[50px] rounded-[2rem] transition-all">
|
<div id="modal-container" className="fixed inset-0 bg-black/30 has-[input:focus]:bg-black/40 flex flex-col items-center justify-around z-50 h-[70vh] m-auto min-w-[40vw] w-fit py-[50px] rounded-[2rem] transition-all">
|
||||||
<div className="login-methods w-full flex flex-row items-center justify-evenly">
|
<div id="login-methods" className="w-full flex flex-row items-center justify-evenly">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-[1rem] right-[2rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
className="absolute top-[1rem] right-[2rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
||||||
|
|||||||
@@ -61,27 +61,30 @@ const LoginForm: React.FC = () => {
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.message || "Login failed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.logged_in) {
|
if (data.logged_in) {
|
||||||
//TODO: Handle successful login (e.g., redirect to home page)
|
//TODO: Handle successful login (e.g., redirect to home page)
|
||||||
console.log("Login successful");
|
console.log("Login successful");
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
// Handle authentication errors
|
// Handle authentication errors from server
|
||||||
if (data.errors) {
|
if (data.error_fields) {
|
||||||
|
const newErrors: FormErrors = {};
|
||||||
|
for (const field of data.error_fields) {
|
||||||
|
newErrors[field as keyof FormErrors] = data.message;
|
||||||
|
}
|
||||||
|
setErrors(newErrors);
|
||||||
|
} else {
|
||||||
|
// If no specific fields are indicated, set a general error
|
||||||
setErrors({
|
setErrors({
|
||||||
general: "Invalid username or password",
|
general: data.message || "An error occurred during login",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Login error:", error);
|
console.error("Error logging in:", error);
|
||||||
setErrors({
|
setErrors({
|
||||||
general: "An error occurred during login. Please try again.",
|
general: "An error occurred during login",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +93,8 @@ const LoginForm: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="login-form h-[100%] flex flex-col h-full justify-evenly items-center"
|
id="login-form"
|
||||||
|
className="h-[100%] flex flex-col h-full justify-evenly items-center"
|
||||||
>
|
>
|
||||||
{errors.general && (
|
{errors.general && (
|
||||||
<p className="text-red-500 text-sm text-center">{errors.general}</p>
|
<p className="text-red-500 text-sm text-center">{errors.general}</p>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface FormErrors {
|
|||||||
email?: string;
|
email?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
confirmPassword?: string;
|
confirmPassword?: string;
|
||||||
|
general?: string; // For general authentication errors
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegisterForm: React.FC = () => {
|
const RegisterForm: React.FC = () => {
|
||||||
@@ -68,14 +69,8 @@ const RegisterForm: React.FC = () => {
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify(formData),
|
body: JSON.stringify(formData),
|
||||||
});
|
});
|
||||||
console.log(`sending data: ${JSON.stringify(formData)}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const data = await response.json();
|
||||||
throw new Error(
|
|
||||||
data.message || `Registration failed. ${response.body}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.account_created) {
|
if (data.account_created) {
|
||||||
//TODO Handle successful registration (e.g., redirect or show success message)
|
//TODO Handle successful registration (e.g., redirect or show success message)
|
||||||
@@ -84,17 +79,24 @@ const RegisterForm: React.FC = () => {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
} else {
|
} else {
|
||||||
// Handle validation errors from server
|
// Handle validation errors from server
|
||||||
const serverErrors: FormErrors = {};
|
if (data.error_fields) {
|
||||||
if (data.errors) {
|
const newErrors: FormErrors = {};
|
||||||
Object.entries(data.errors).forEach(([field, message]) => {
|
for (const field of data.error_fields) {
|
||||||
serverErrors[field as keyof FormErrors] = message as string;
|
newErrors[field as keyof FormErrors] = data.message;
|
||||||
|
}
|
||||||
|
setErrors(newErrors);
|
||||||
|
} else {
|
||||||
|
// If no specific fields are indicated, set a general error
|
||||||
|
setErrors({
|
||||||
|
general: data.message || "An error occurred during registration",
|
||||||
});
|
});
|
||||||
setErrors(serverErrors);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Registration error:", error);
|
console.error("Error Registering:", error);
|
||||||
//TODO Show user-friendly error message via Alert component maybe
|
setErrors({
|
||||||
|
general: "An error occurred during registration",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -102,8 +104,13 @@ const RegisterForm: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="register-form h-[100%] flex flex-col h-full justify-evenly items-center"
|
id="register-form"
|
||||||
|
className="h-[100%] flex flex-col h-full justify-evenly items-center"
|
||||||
>
|
>
|
||||||
|
{errors.general && (
|
||||||
|
<p className="text-red-500 text-sm text-center">{errors.general}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{errors.username && (
|
{errors.username && (
|
||||||
<p className="text-red-500 mt-3 text-sm">{errors.username}</p>
|
<p className="text-red-500 mt-3 text-sm">{errors.username}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ const CheckoutForm: React.FC<CheckoutFormProps> = ({ onClose }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="blurring-layer fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm"></div>
|
<div id="blurring-layer" className="fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm"></div>
|
||||||
<div className="modal-container fixed inset-0 bg-black/30 flex items-center justify-center z-50 h-[70vh] m-auto w-fit py-[50px] px-[100px] rounded-[2rem]">
|
<div id="modal-container" className="fixed inset-0 bg-black/30 flex items-center justify-center z-50 h-[70vh] m-auto w-fit py-[50px] px-[100px] rounded-[2rem]">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-[1rem] right-[3rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
className="absolute top-[1rem] right-[3rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const Logo: React.FC = () => {
|
interface LogoProps {
|
||||||
|
variant?: "home" | "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Logo: React.FC<LogoProps> = ({ variant = "default" }) => {
|
||||||
const gradient =
|
const gradient =
|
||||||
"bg-gradient-to-br from-yellow-400 via-red-500 to-indigo-500 text-transparent bg-clip-text group-hover:mx-1 transition-all";
|
"bg-gradient-to-br from-yellow-400 via-red-500 to-indigo-500 text-transparent bg-clip-text group-hover:mx-1 transition-all";
|
||||||
return (
|
return (
|
||||||
<Link to="/" className="cursor-pointer">
|
<Link to="/" className="cursor-pointer">
|
||||||
<div className="logo group py-3 text-center text-[12vh] font-bold hover:scale-110 transition-all">
|
<div id="logo" className={`group py-3 text-center font-bold hover:scale-110 transition-all ${variant === "home" ? "text-[12vh]" : "text-[4vh]"}`}>
|
||||||
<h6 className="text-sm bg-gradient-to-br from-blue-400 via-green-500 to-indigo-500 font-black text-transparent bg-clip-text">
|
<h6 className="text-sm bg-gradient-to-br from-blue-400 via-green-500 to-indigo-500 font-black text-transparent bg-clip-text">
|
||||||
Go on, have a...
|
Go on, have a...
|
||||||
</h6>
|
</h6>
|
||||||
<div className="flex w-fit min-w-[30vw] justify-evenly leading-none transition-all">
|
<div className="flex w-fit min-w-[30vw] justify-center leading-none transition-all">
|
||||||
<span className={gradient}>G</span>
|
<span className={gradient}>G</span>
|
||||||
<span className={gradient}>A</span>
|
<span className={gradient}>A</span>
|
||||||
<span className={gradient}>N</span>
|
<span className={gradient}>N</span>
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import Input from "./Input";
|
|||||||
import AuthModal from "../Auth/AuthModal";
|
import AuthModal from "../Auth/AuthModal";
|
||||||
import { useAuth } from "../../context/AuthContext";
|
import { useAuth } from "../../context/AuthContext";
|
||||||
|
|
||||||
const Navbar: React.FC = () => {
|
interface NavbarProps {
|
||||||
|
variant?: "home" | "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||||
const [showAuthModal, setShowAuthModal] = useState(false);
|
const [showAuthModal, setShowAuthModal] = useState(false);
|
||||||
const { isLoggedIn } = useAuth();
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
@@ -39,8 +43,8 @@ const Navbar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-around items-center h-[45vh]">
|
<div id="navbar" className={`flex justify-center items-center ${variant === "home" ? "h-[45vh] flex-col" : "h-[15vh] col-span-2 flex-row"}`}>
|
||||||
<Logo />
|
<Logo variant={variant} />
|
||||||
<Button
|
<Button
|
||||||
extraClasses="absolute top-[20px] left-[20px] text-[1rem] flex items-center flex-nowrap"
|
extraClasses="absolute top-[20px] left-[20px] text-[1rem] flex items-center flex-nowrap"
|
||||||
onClick={() => (isLoggedIn ? handleLogout() : setShowAuthModal(true))}
|
onClick={() => (isLoggedIn ? handleLogout() : setShowAuthModal(true))}
|
||||||
@@ -69,13 +73,13 @@ const Navbar: React.FC = () => {
|
|||||||
|
|
||||||
<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")}
|
onClick={() => console.log("Settings - TODO")}
|
||||||
>
|
>
|
||||||
<SettingsIcon className="h-15 w-15 mr-1" />
|
<SettingsIcon className="h-15 w-15 mr-1" />
|
||||||
Quick Settings
|
Quick Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="search-bar flex items-center">
|
<div id="search-bar" className="flex items-center">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
|
|||||||
@@ -17,11 +17,14 @@ interface StreamListRowProps {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
streams: StreamItem[];
|
streams: StreamItem[];
|
||||||
onStreamClick?: (streamId: string) => void;
|
onStreamClick: (streamId: number, streamerName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Individual stream entry component
|
// Individual stream entry component
|
||||||
const StreamListEntry: React.FC<StreamListEntryProps> = ({ stream, onClick }) => {
|
const StreamListEntry: React.FC<StreamListEntryProps> = ({
|
||||||
|
stream,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 transition-colors"
|
className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 transition-colors"
|
||||||
@@ -30,7 +33,7 @@ const StreamListEntry: React.FC<StreamListEntryProps> = ({ stream, onClick }) =>
|
|||||||
<div className="relative w-full pt-[56.25%]">
|
<div className="relative w-full pt-[56.25%]">
|
||||||
{stream.thumbnail ? (
|
{stream.thumbnail ? (
|
||||||
<img
|
<img
|
||||||
src={`images/`+stream.thumbnail}
|
src={`images/` + stream.thumbnail}
|
||||||
alt={stream.title}
|
alt={stream.title}
|
||||||
className="absolute top-0 left-0 w-full h-full object-cover"
|
className="absolute top-0 left-0 w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
@@ -65,7 +68,7 @@ const StreamListRow: React.FC<StreamListRowProps> = ({
|
|||||||
<StreamListEntry
|
<StreamListEntry
|
||||||
key={stream.id}
|
key={stream.id}
|
||||||
stream={stream}
|
stream={stream}
|
||||||
onClick={() => onStreamClick?.(stream.id)}
|
onClick={() => onStreamClick?.(stream.id, stream.streamer)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
41
frontend/src/components/Stream/StreamerRoute.tsx
Normal file
41
frontend/src/components/Stream/StreamerRoute.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import VideoPage from "../../pages/VideoPage";
|
||||||
|
import UserPage from "../../pages/UserPage";
|
||||||
|
|
||||||
|
const StreamerRoute: React.FC = () => {
|
||||||
|
const { streamerName } = useParams<{ streamerName: string }>();
|
||||||
|
const [isLive, setIsLive] = useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkStreamStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/streamer/${streamerName}/status`);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Stream status:", data);
|
||||||
|
setIsLive(data.status === "live");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking stream status:", error);
|
||||||
|
setIsLive(false);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkStreamStatus();
|
||||||
|
|
||||||
|
// Poll for live status changes
|
||||||
|
const interval = setInterval(checkStreamStatus, 90000); // Check every 90 seconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [streamerName]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>; // Or your loading component
|
||||||
|
}
|
||||||
|
|
||||||
|
return isLive ? <VideoPage streamId={1} /> : <UserPage />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StreamerRoute;
|
||||||
@@ -7,12 +7,12 @@ interface ThumbnailProps {
|
|||||||
|
|
||||||
const Thumbnail = ({ path, alt }: ThumbnailProps) => {
|
const Thumbnail = ({ path, alt }: ThumbnailProps) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div id='stream-thumbnail'>
|
||||||
<img
|
<img
|
||||||
width={300}
|
width={300}
|
||||||
src={path}
|
src={path}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className="stream-thumbnail rounded-md"
|
className="rounded-md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
151
frontend/src/components/Video/ChatPanel.tsx
Normal file
151
frontend/src/components/Video/ChatPanel.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import Input from "../Layout/Input";
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
chatter_id: string;
|
||||||
|
message: string;
|
||||||
|
time_sent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatPanelProps {
|
||||||
|
streamId: number;
|
||||||
|
chatterId?: string; // Optional as user might not be logged in
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatPanel: React.FC<ChatPanelProps> = ({ streamId, chatterId }) => {
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [inputMessage, setInputMessage] = useState("");
|
||||||
|
const lastReceivedRef = useRef<string>("");
|
||||||
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Load initial chat history
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPastChat = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/chat/${streamId}`);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch chat history");
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.chat_history) {
|
||||||
|
setMessages(data.chat_history);
|
||||||
|
if (data.chat_history.length > 0) {
|
||||||
|
lastReceivedRef.current =
|
||||||
|
data.chat_history[data.chat_history.length - 1].time_sent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading chat history:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPastChat();
|
||||||
|
}, [streamId]);
|
||||||
|
|
||||||
|
// Auto-scroll to bottom when new messages arrive
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatContainerRef.current)
|
||||||
|
chatContainerRef.current.scrollTop =
|
||||||
|
chatContainerRef.current.scrollHeight;
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Poll for new messages
|
||||||
|
useEffect(() => {
|
||||||
|
const getRecentChats = async () => {
|
||||||
|
if (!lastReceivedRef.current) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/load_new_chat/${streamId}?last_received=${lastReceivedRef.current}`
|
||||||
|
);
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch recent chats");
|
||||||
|
const newMessages = await response.json();
|
||||||
|
if (newMessages && newMessages.length > 0) {
|
||||||
|
setMessages((prev) => [...prev, ...newMessages]);
|
||||||
|
lastReceivedRef.current =
|
||||||
|
newMessages[newMessages.length - 1].time_sent;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching recent chats:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pollInterval = setInterval(getRecentChats, 3000); // Poll every 3 seconds
|
||||||
|
return () => clearInterval(pollInterval);
|
||||||
|
}, [streamId]);
|
||||||
|
|
||||||
|
const sendChat = async () => {
|
||||||
|
if (!inputMessage.trim() || !chatterId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/send_chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
chatter_id: chatterId,
|
||||||
|
stream_id: streamId,
|
||||||
|
message: inputMessage,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.chat_sent) {
|
||||||
|
setInputMessage("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending chat:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
sendChat();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="chat-panel" className="h-full flex flex-col rounded-lg p-4" >
|
||||||
|
<h2 className="text-xl font-bold mb-4 text-white">Stream Chat</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={chatContainerRef}
|
||||||
|
id="chat-message-list"
|
||||||
|
className="flex-grow w-full max-h-[50vh] overflow-y-auto mb-4 space-y-2"
|
||||||
|
>
|
||||||
|
{messages.map((msg, index) => (
|
||||||
|
<div key={index} className="grid grid-cols-[8%_minmax(15%,_100px)_1fr] items-center bg-gray-700 rounded p-2 text-white">
|
||||||
|
<span className="text-gray-400 text-sm">
|
||||||
|
{new Date(msg.time_sent).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
<span className={`font-bold ${msg.chatter_id === chatterId ? "text-blue-400" : "text-green-400"}`}> {msg.chatter_id}: </span>
|
||||||
|
<span>{msg.message}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center gap-2">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={inputMessage}
|
||||||
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
placeholder={chatterId ? "Type a message..." : "Login to chat"}
|
||||||
|
disabled={!chatterId}
|
||||||
|
extraClasses="flex-grow disabled:cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={sendChat}
|
||||||
|
disabled={!chatterId}
|
||||||
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatPanel;
|
||||||
@@ -1,35 +1,59 @@
|
|||||||
// video.html
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import videojs from "video.js";
|
||||||
|
import type Player from "video.js/dist/types/player";
|
||||||
|
import "video.js/dist/video-js.css";
|
||||||
|
|
||||||
// src/components/Video/VideoPlayer.tsx
|
interface VideoPlayerProps {
|
||||||
import React, { useEffect } from 'react';
|
streamId: number;
|
||||||
import videojs from 'video.js';
|
}
|
||||||
import 'video.js/dist/video-js.css';
|
|
||||||
|
const VideoPlayer: React.FC<VideoPlayerProps> = ({ streamId }) => {
|
||||||
|
const videoRef = useRef<HTMLDivElement>(null);
|
||||||
|
const playerRef = useRef<Player | null>(null);
|
||||||
|
|
||||||
const VideoPlayer: React.FC = () => {
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const player = videojs('player', {
|
// Makes sure Video.js player is only initialized once
|
||||||
width: 1280,
|
if (!playerRef.current && videoRef.current) {
|
||||||
height: 720,
|
const videoElement = document.createElement("video");
|
||||||
controls: true,
|
videoElement.classList.add(
|
||||||
preload: 'auto',
|
"video-js",
|
||||||
sources: [{
|
"vjs-big-play-centered",
|
||||||
src: '/hls/stream.m3u8',
|
"w-full",
|
||||||
type: 'application/x-mpegURL'
|
"h-full"
|
||||||
}]
|
);
|
||||||
});
|
videoRef.current.appendChild(videoElement);
|
||||||
|
|
||||||
|
playerRef.current = videojs(videoElement, {
|
||||||
|
controls: true,
|
||||||
|
fluid: true,
|
||||||
|
responsive: true,
|
||||||
|
aspectRatio: "16:9",
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
src: `/api/hls1/${streamId}`,
|
||||||
|
type: "application/x-mpegURL",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose the Video.js player when the component unmounts
|
||||||
return () => {
|
return () => {
|
||||||
if (player) {
|
if (playerRef.current) {
|
||||||
player.dispose();
|
playerRef.current.dispose();
|
||||||
|
playerRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [streamId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<video
|
<div
|
||||||
id="player"
|
id="video-container"
|
||||||
className="video-js vjs-default-skin"
|
className="h-full flex items-center bg-gray-900 rounded-lg"
|
||||||
/>
|
style={{ gridArea: "2 / 1 / 3 / 2" }}
|
||||||
|
>
|
||||||
|
<div ref={videoRef} id="video-player" className="w-full" />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ const HomePage: React.FC = () => {
|
|||||||
const { featuredStreams, featuredCategories } = useStreams();
|
const { featuredStreams, featuredCategories } = useStreams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleStreamClick = (streamerId: string) => {
|
const handleStreamClick = (streamId: number, streamerName: string) => {
|
||||||
navigate(`/${streamerId}`);
|
console.log(`Navigating to ${streamId}`);
|
||||||
|
navigate(`/${streamerName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="home-page bg-repeat"
|
id="home-page"
|
||||||
|
className="bg-repeat"
|
||||||
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
||||||
>
|
>
|
||||||
<Navbar />
|
<Navbar variant="home" />
|
||||||
|
|
||||||
<StreamListRow
|
<StreamListRow
|
||||||
title="Live Now"
|
title="Live Now"
|
||||||
@@ -39,16 +41,18 @@ export const PersonalisedHomePage: React.FC = () => {
|
|||||||
const { featuredStreams, featuredCategories } = useStreams();
|
const { featuredStreams, featuredCategories } = useStreams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleStreamClick = (streamerId: string) => {
|
const handleStreamClick = (streamId: number, streamerName: string) => {
|
||||||
navigate(`/${streamerId}`);
|
console.log(`Navigating to ${streamId}`);
|
||||||
|
navigate(`/${streamerName}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="home-page bg-repeat"
|
id="personalised-home-page"
|
||||||
|
className="bg-repeat"
|
||||||
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
||||||
>
|
>
|
||||||
<Navbar />
|
<Navbar variant="home" />
|
||||||
{/*//TODO Extract StreamListRow away to ListRow so that it makes sense for categories to be there also */}
|
{/*//TODO Extract StreamListRow away to ListRow so that it makes sense for categories to be there also */}
|
||||||
<StreamListRow
|
<StreamListRow
|
||||||
title="Live Now - Recommended"
|
title="Live Now - Recommended"
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const LoginPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginPage;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const SignupPage: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SignupPage;
|
|
||||||
7
frontend/src/pages/UserPage.tsx
Normal file
7
frontend/src/pages/UserPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const UserPage: React.FC = () => {
|
||||||
|
return <div></div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserPage;
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
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 Button from "../components/Layout/Button";
|
import Button from "../components/Layout/Button";
|
||||||
|
import ChatPanel from "../components/Video/ChatPanel";
|
||||||
import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm";
|
import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
|
import VideoPlayer from "../components/Video/VideoPlayer";
|
||||||
|
|
||||||
const VideoPage: React.FC = () => {
|
interface VideoPageProps {
|
||||||
|
streamId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
|
||||||
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 { streamerName } = useParams<{ streamerName: string }>();
|
const { streamerName } = useParams<{ streamerName: string }>();
|
||||||
|
const { isLoggedIn } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prevent scrolling when checkout is open
|
// Prevent scrolling when checkout is open
|
||||||
@@ -25,19 +33,33 @@ const VideoPage: React.FC = () => {
|
|||||||
if (streamerName) {
|
if (streamerName) {
|
||||||
// Fetch stream data for this streamer
|
// Fetch stream data for this streamer
|
||||||
console.log(`Loading stream for ${streamerName}`);
|
console.log(`Loading stream for ${streamerName}`);
|
||||||
// fetch(`/api/get_stream_data/${streamerName}`)
|
// fetch(`/api/get_stream_data/${streamId}`)
|
||||||
}
|
}
|
||||||
}, [streamerName]);
|
}, [streamerName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-5xl text-red-600 flex flex-col justify-evenly align-center h-screen text-center">
|
<div id="videoPage" className="w-full">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<h1>
|
<div id="container" className="bg-gray-900">
|
||||||
Hello! Welcome to the soon-to-be-awesome Video Page where you'll watch
|
<VideoPlayer streamId={streamId} />
|
||||||
the best streams ever!
|
|
||||||
</h1>
|
{isLoggedIn ? (
|
||||||
<Button onClick={() => setShowCheckout(true)}>Payment Screen Test</Button>
|
<ChatPanel streamId={streamId} chatterId="chatter-man" />
|
||||||
|
) : (
|
||||||
|
<ChatPanel streamId={streamId} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="stream-info"
|
||||||
|
className="flex"
|
||||||
|
style={{ gridArea: "3 / 1 / 4 / 2" }}
|
||||||
|
>
|
||||||
|
<Button onClick={() => setShowCheckout(true)} 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 />}
|
||||||
|
|||||||
243
package-lock.json
generated
243
package-lock.json
generated
@@ -1,19 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "Software Project",
|
"name": "Team-Software-Project",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/video.js": "^7.3.58",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"video.js": "^8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3"
|
"@types/react-dom": "^19.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.26.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
||||||
|
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.0.8",
|
"version": "19.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz",
|
||||||
@@ -34,6 +48,81 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/video.js": {
|
||||||
|
"version": "7.3.58",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.58.tgz",
|
||||||
|
"integrity": "sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/http-streaming": {
|
||||||
|
"version": "3.16.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.16.2.tgz",
|
||||||
|
"integrity": "sha512-fvt4ko7FknxiT9FnjyNQt6q2px+awrkM+Orv7IB/4gldvj94u4fowGfmNHynnvNTPgPkdxHklGmFLGfclYw8HA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"aes-decrypter": "^4.0.2",
|
||||||
|
"global": "^4.4.0",
|
||||||
|
"m3u8-parser": "^7.2.0",
|
||||||
|
"mpd-parser": "^1.3.1",
|
||||||
|
"mux.js": "7.1.0",
|
||||||
|
"video.js": "^7 || ^8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"video.js": "^8.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/vhs-utils": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"global": "^4.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@videojs/xhr": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"global": "~4.4.0",
|
||||||
|
"is-function": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmldom/xmldom": {
|
||||||
|
"version": "0.8.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||||
|
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aes-decrypter": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"global": "^4.4.0",
|
||||||
|
"pkcs7": "^1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -79,6 +168,11 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-walk": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
|
||||||
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.9",
|
"version": "1.15.9",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
@@ -113,6 +207,33 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/global": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"min-document": "^2.19.0",
|
||||||
|
"process": "^0.11.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-function": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/m3u8-parser": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"global": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
@@ -134,6 +255,67 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/min-document": {
|
||||||
|
"version": "2.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
|
||||||
|
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-walk": "^0.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mpd-parser": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/vhs-utils": "^4.0.0",
|
||||||
|
"@xmldom/xmldom": "^0.8.3",
|
||||||
|
"global": "^4.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mpd-to-m3u8-json": "bin/parse.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mux.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.11.2",
|
||||||
|
"global": "^4.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"muxjs-transmux": "bin/transmux.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8",
|
||||||
|
"npm": ">=5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pkcs7": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"pkcs7": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -161,11 +343,68 @@
|
|||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.25.0",
|
"version": "0.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/video.js": {
|
||||||
|
"version": "8.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/video.js/-/video.js-8.21.0.tgz",
|
||||||
|
"integrity": "sha512-zcwerRb257QAuWfi8NH9yEX7vrGKFthjfcONmOQ4lxFRpDAbAi+u5LAjCjMWqhJda6zEmxkgdDpOMW3Y21QpXA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@videojs/http-streaming": "^3.16.2",
|
||||||
|
"@videojs/vhs-utils": "^4.1.1",
|
||||||
|
"@videojs/xhr": "2.7.0",
|
||||||
|
"aes-decrypter": "^4.0.2",
|
||||||
|
"global": "4.4.0",
|
||||||
|
"m3u8-parser": "^7.2.0",
|
||||||
|
"mpd-parser": "^1.3.1",
|
||||||
|
"mux.js": "^7.0.1",
|
||||||
|
"videojs-contrib-quality-levels": "4.1.0",
|
||||||
|
"videojs-font": "4.2.0",
|
||||||
|
"videojs-vtt.js": "0.15.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/videojs-contrib-quality-levels": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"global": "^4.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16",
|
||||||
|
"npm": ">=8"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"video.js": "^8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/videojs-font": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/videojs-vtt.js": {
|
||||||
|
"version": "0.15.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
|
||||||
|
"integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"global": "^4.3.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/video.js": "^7.3.58",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"video.js": "^8.21.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
|
|||||||
@@ -30,11 +30,13 @@ def create_app():
|
|||||||
from blueprints.stripe import stripe_bp
|
from blueprints.stripe import stripe_bp
|
||||||
from blueprints.user import user_bp
|
from blueprints.user import user_bp
|
||||||
from blueprints.streams import stream_bp
|
from blueprints.streams import stream_bp
|
||||||
|
from blueprints.chat import chat_bp
|
||||||
|
|
||||||
app.register_blueprint(auth_bp)
|
app.register_blueprint(auth_bp)
|
||||||
app.register_blueprint(main_bp)
|
app.register_blueprint(main_bp)
|
||||||
app.register_blueprint(stripe_bp)
|
app.register_blueprint(stripe_bp)
|
||||||
app.register_blueprint(user_bp)
|
app.register_blueprint(user_bp)
|
||||||
app.register_blueprint(stream_bp)
|
app.register_blueprint(stream_bp)
|
||||||
|
app.register_blueprint(chat_bp)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@@ -22,8 +22,13 @@ def signup():
|
|||||||
|
|
||||||
# Basic server-side validation
|
# Basic server-side validation
|
||||||
if not all([username, email, password]):
|
if not all([username, email, password]):
|
||||||
|
fields = ["username", "email", "password"]
|
||||||
|
for x in fields:
|
||||||
|
if not [username, email, password][fields.index(x)]:
|
||||||
|
fields.remove(x)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"account_created": False,
|
"account_created": False,
|
||||||
|
"error_fields": fields,
|
||||||
"message": "Missing required fields"
|
"message": "Missing required fields"
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
@@ -45,13 +50,15 @@ def signup():
|
|||||||
if dup_email is not None:
|
if dup_email is not None:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"account_created": False,
|
"account_created": False,
|
||||||
"errors": {"email": "Email already taken"}
|
"error_fields": ["email"],
|
||||||
|
"message": "Email already taken"
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
if dup_username is not None:
|
if dup_username is not None:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"account_created": False,
|
"account_created": False,
|
||||||
"errors": {"username": "Username already taken"}
|
"error_fields": ["username"],
|
||||||
|
"message": "Username already taken"
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Create new user
|
# Create new user
|
||||||
@@ -122,14 +129,16 @@ def login():
|
|||||||
if not user:
|
if not user:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"logged_in": False,
|
"logged_in": False,
|
||||||
"errors": {"general": "Invalid username or password"}
|
"error_fields": ["username", "password"],
|
||||||
|
"message": "Invalid username or password"
|
||||||
}), 401
|
}), 401
|
||||||
|
|
||||||
# Verify password
|
# Verify password
|
||||||
if not check_password_hash(user["password"], password):
|
if not check_password_hash(user["password"], password):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"logged_in": False,
|
"logged_in": False,
|
||||||
"errors": {"general": "Invalid username or password"}
|
"error_fields": ["username", "password"],
|
||||||
|
"message": "Invalid username or password"
|
||||||
}), 401
|
}), 401
|
||||||
|
|
||||||
# Set up session
|
# Set up session
|
||||||
|
|||||||
@@ -139,9 +139,16 @@ def get_streamer(streamer_id):
|
|||||||
"""
|
"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@stream_bp.route('/streamer/<string:streamerName>/status')
|
||||||
|
def get_streamer_status(streamerName):
|
||||||
|
"""
|
||||||
|
Returns a streamer's status, if they are live or not
|
||||||
|
"""
|
||||||
|
return {"status": "live", "streamId": 1}
|
||||||
|
|
||||||
@stream_bp.route('/get_stream_data/<int:streamer_id>', methods=['GET'])
|
|
||||||
def get_stream(streamer_id):
|
@stream_bp.route('/get_stream_data/<int:stream_id>', methods=['GET'])
|
||||||
|
def get_stream(stream_id):
|
||||||
"""
|
"""
|
||||||
Returns a streamer's stream data
|
Returns a streamer's stream data
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user