Feat: Update to VideoPage to display stream data;
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<title>Team Software Project</title>
|
||||
</head>
|
||||
<body class="h-screen">
|
||||
<div id="root" class="bg-gradient-to-tr from-[#07001F] via-[#1D0085] to-[#CC00AF]"></div>
|
||||
<div id="root" class="h-full bg-gradient-to-tr from-[#07001F] via-[#1D0085] to-[#CC00AF]"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// base.html
|
||||
// src/components/Layout/BaseLayout.tsx
|
||||
import React from 'react';
|
||||
|
||||
interface BaseLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const BaseLayout: React.FC<BaseLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Live Stream</title>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseLayout;
|
||||
87
frontend/src/components/Layout/ListRow.tsx
Normal file
87
frontend/src/components/Layout/ListRow.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
|
||||
interface ListItemProps {
|
||||
type: "stream" | "category";
|
||||
id: number;
|
||||
title: string;
|
||||
streamer?: string;
|
||||
viewers: number;
|
||||
thumbnail?: string;
|
||||
onItemClick?: () => void;
|
||||
}
|
||||
|
||||
interface ListRowProps {
|
||||
type: "stream" | "category";
|
||||
title: string;
|
||||
description: string;
|
||||
items: ListItemProps[];
|
||||
onClick: (itemId: number, itemName: string) => void;
|
||||
}
|
||||
|
||||
// Individual list entry component
|
||||
const ListItem: React.FC<ListItemProps> = ({
|
||||
type,
|
||||
title,
|
||||
streamer,
|
||||
viewers,
|
||||
thumbnail,
|
||||
onItemClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 transition-colors"
|
||||
onClick={onItemClick}
|
||||
>
|
||||
<div className="relative w-full pt-[56.25%]">
|
||||
{thumbnail ? (
|
||||
<img
|
||||
src={`images/` + thumbnail}
|
||||
alt={title}
|
||||
className="absolute top-0 left-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-gray-600" />
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="font-semibold text-lg">{title}</h3>
|
||||
{type === "stream" && <p className="text-gray-400">{streamer}</p>}
|
||||
<p className="text-sm text-gray-500">{viewers} viewers</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Row of entries
|
||||
const ListRow: React.FC<ListRowProps> = ({
|
||||
title,
|
||||
description,
|
||||
items,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-4 py-6">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-bold">{title}</h2>
|
||||
<p className="text-gray-400">{description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{items.map((item) => (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
type={item.type}
|
||||
title={item.title}
|
||||
streamer={item.type === "stream" ? (item.streamer) : undefined}
|
||||
viewers={item.viewers}
|
||||
onItemClick={() =>
|
||||
onClick?.(item.id, item.streamer || item.title)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListRow;
|
||||
@@ -1,79 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface StreamItem {
|
||||
id: number;
|
||||
title: string;
|
||||
streamer: string;
|
||||
viewers: number;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
interface StreamListEntryProps {
|
||||
stream: StreamItem;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface StreamListRowProps {
|
||||
title: string;
|
||||
description: string;
|
||||
streams: StreamItem[];
|
||||
onStreamClick: (streamId: number, streamerName: string) => void;
|
||||
}
|
||||
|
||||
// Individual stream entry component
|
||||
const StreamListEntry: React.FC<StreamListEntryProps> = ({
|
||||
stream,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col bg-gray-800 rounded-lg overflow-hidden cursor-pointer hover:bg-gray-700 transition-colors"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="relative w-full pt-[56.25%]">
|
||||
{stream.thumbnail ? (
|
||||
<img
|
||||
src={`images/` + stream.thumbnail}
|
||||
alt={stream.title}
|
||||
className="absolute top-0 left-0 w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-gray-600" />
|
||||
)}
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<h3 className="font-semibold text-lg">{stream.title}</h3>
|
||||
<p className="text-gray-400">{stream.streamer}</p>
|
||||
<p className="text-sm text-gray-500">{stream.viewers} viewers</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Row of stream entries
|
||||
const StreamListRow: React.FC<StreamListRowProps> = ({
|
||||
title,
|
||||
description,
|
||||
streams,
|
||||
onStreamClick,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-4 py-6">
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-2xl font-bold">{title}</h2>
|
||||
<p className="text-gray-400">{description}</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{streams.map((stream) => (
|
||||
<StreamListEntry
|
||||
key={stream.id}
|
||||
stream={stream}
|
||||
onClick={() => onStreamClick?.(stream.id, stream.streamer)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StreamListRow;
|
||||
@@ -26,7 +26,7 @@ const StreamerRoute: React.FC = () => {
|
||||
checkStreamStatus();
|
||||
|
||||
// Poll for live status changes
|
||||
const interval = setInterval(checkStreamStatus, 30000); // Check every 90 seconds
|
||||
const interval = setInterval(checkStreamStatus, 20000); // Check every 20 seconds
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [streamerName]);
|
||||
@@ -35,7 +35,8 @@ const StreamerRoute: React.FC = () => {
|
||||
return <div className="h-screen w-screen flex text-6xl items-center justify-center" >Loading...</div>; // Or your loading component
|
||||
}
|
||||
|
||||
return isLive ? <VideoPage streamId={1} /> : <UserPage />;
|
||||
// streamId=0 is a special case for the streamer's latest stream
|
||||
return isLive ? <VideoPage streamId={0} /> : <UserPage profile={streamerName} />;
|
||||
};
|
||||
|
||||
export default StreamerRoute;
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import { createContext, useContext, useState, useEffect } from "react";
|
||||
import { useAuth } from "./AuthContext";
|
||||
|
||||
interface StreamItem {
|
||||
interface Item {
|
||||
id: number;
|
||||
title: string;
|
||||
streamer: string;
|
||||
viewers: number;
|
||||
thumbnail?: string;
|
||||
}
|
||||
|
||||
interface StreamItem extends Item {
|
||||
type: "stream";
|
||||
streamer: string;
|
||||
}
|
||||
|
||||
interface CategoryItem extends Item {
|
||||
type: "category";
|
||||
}
|
||||
|
||||
interface StreamsContextType {
|
||||
featuredStreams: StreamItem[];
|
||||
featuredCategories: StreamItem[];
|
||||
featuredCategories: CategoryItem[];
|
||||
setFeaturedStreams: (streams: StreamItem[]) => void;
|
||||
setFeaturedCategories: (categories: StreamItem[]) => void;
|
||||
setFeaturedCategories: (categories: CategoryItem[]) => void;
|
||||
}
|
||||
|
||||
const StreamsContext = createContext<StreamsContextType | undefined>(undefined);
|
||||
|
||||
export function StreamsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [featuredStreams, setFeaturedStreams] = useState<StreamItem[]>([]);
|
||||
const [featuredCategories, setFeaturedCategories] = useState<StreamItem[]>(
|
||||
const [featuredCategories, setFeaturedCategories] = useState<CategoryItem[]>(
|
||||
[]
|
||||
);
|
||||
const { isLoggedIn } = useAuth();
|
||||
@@ -30,15 +38,34 @@ export function StreamsProvider({ children }: { children: React.ReactNode }) {
|
||||
: ["/api/get_streams", "/api/get_categories"];
|
||||
|
||||
useEffect(() => {
|
||||
// Streams
|
||||
fetch(fetch_url[0])
|
||||
.then((response) => response.json())
|
||||
.then((data: StreamItem[]) => {
|
||||
setFeaturedStreams(data);
|
||||
.then((data) => {
|
||||
const extractedData: StreamItem[] = data.streams.map((stream: any) => ({
|
||||
type: "stream",
|
||||
id: stream.stream_id,
|
||||
title: stream.title,
|
||||
streamer: stream.user_id,
|
||||
viewers: stream.num_viewers,
|
||||
thumbnail: stream.thumbnail,
|
||||
}));
|
||||
setFeaturedStreams(extractedData);
|
||||
});
|
||||
|
||||
// Categories
|
||||
fetch(fetch_url[1])
|
||||
.then((response) => response.json())
|
||||
.then((data: StreamItem[]) => {
|
||||
setFeaturedCategories(data);
|
||||
.then((data) => {
|
||||
const extractedData: CategoryItem[] = data.categories.map(
|
||||
(category: any) => ({
|
||||
type: "category",
|
||||
id: category.category_id,
|
||||
title: category.category_name,
|
||||
viewers: category.num_viewers,
|
||||
})
|
||||
);
|
||||
setFeaturedCategories(extractedData);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import Navbar from "../components/Layout/Navbar";
|
||||
import StreamListRow from "../components/Layout/StreamListRow";
|
||||
import ListRow from "../components/Layout/ListRow";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useStreams } from "../context/StreamsContext";
|
||||
|
||||
@@ -20,24 +20,24 @@ const HomePage: React.FC<HomePageProps> = ({ variant = "default" }) => {
|
||||
return (
|
||||
<div
|
||||
id="home-page"
|
||||
className="animate-moving_bg"
|
||||
className="animate-moving_bg h-full"
|
||||
style={{ backgroundImage: "url(/images/background-pattern.svg)" }}
|
||||
>
|
||||
<Navbar variant="home" />
|
||||
|
||||
{/*//TODO Extract StreamListRow away, to ListRow so that it makes sense for categories to be there also */}
|
||||
|
||||
<StreamListRow
|
||||
<ListRow
|
||||
type="stream"
|
||||
title={"Live Now" + (variant === "personalised" ? " - Recommended" : "")}
|
||||
description={variant === "personalised" ? "We think you might like these streams - Streamers recommended for you" : "Streamers that are currently live"}
|
||||
streams={featuredStreams}
|
||||
onStreamClick={handleStreamClick}
|
||||
items={featuredStreams}
|
||||
onClick={handleStreamClick}
|
||||
/>
|
||||
<StreamListRow
|
||||
<ListRow
|
||||
type="category"
|
||||
title={variant === "personalised" ? "Followed Categories" : "Trending Categories"}
|
||||
description={variant === "personalised" ? "Current streams from your followed categories" : "Categories that have been 'popping off' lately"}
|
||||
streams={featuredCategories}
|
||||
onStreamClick={() => {}} //TODO
|
||||
items={featuredCategories}
|
||||
onClick={() => {}} //TODO
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import React from "react";
|
||||
|
||||
const UserPage: React.FC = () => {
|
||||
return <div></div>;
|
||||
interface UserPageProps {
|
||||
username: string;
|
||||
}
|
||||
|
||||
const UserPage: React.FC<UserPageProps> = ({ username }) => {
|
||||
return (
|
||||
<div className="bg-[#808080] h-screen w-screen flex flex-col items-center justify-center">
|
||||
<h1>{username}</h1>
|
||||
<p>Profile page for {username}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPage;
|
||||
|
||||
@@ -3,7 +3,7 @@ import Navbar from "../components/Layout/Navbar";
|
||||
import Button from "../components/Layout/Button";
|
||||
import ChatPanel from "../components/Video/ChatPanel";
|
||||
import CheckoutForm, { Return } from "../components/Checkout/CheckoutForm";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import VideoPlayer from "../components/Video/VideoPlayer";
|
||||
|
||||
@@ -11,11 +11,23 @@ interface VideoPageProps {
|
||||
streamId: number;
|
||||
}
|
||||
|
||||
interface StreamDataProps {
|
||||
streamId: number;
|
||||
streamTitle: string;
|
||||
streamerName: string;
|
||||
streamerId: number;
|
||||
startTime: string;
|
||||
viewerCount: number;
|
||||
categoryId: number;
|
||||
}
|
||||
|
||||
const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
const [showCheckout, setShowCheckout] = useState(false);
|
||||
const showReturn = window.location.search.includes("session_id");
|
||||
const { streamerName } = useParams<{ streamerName: string }>();
|
||||
const { isLoggedIn } = useAuth();
|
||||
const [streamData, setStreamData] = useState<StreamDataProps>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent scrolling when checkout is open
|
||||
@@ -30,12 +42,30 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
|
||||
};
|
||||
}, [showCheckout]);
|
||||
useEffect(() => {
|
||||
if (streamerName) {
|
||||
// Fetch stream data for this streamer
|
||||
console.log(`Loading stream for ${streamerName}`);
|
||||
// fetch(`/api/get_stream_data/${streamId}`)
|
||||
fetch(
|
||||
`/api/get_stream_data/${streamerName}${
|
||||
streamId == 0 ? "" : `/${streamId}`
|
||||
}`
|
||||
).then((res) => {
|
||||
if (!res.ok) {
|
||||
console.error("Failed to load stream data:", res.statusText);
|
||||
}
|
||||
}, [streamerName]);
|
||||
res.json().then((data) => {
|
||||
if (!data.validStream) navigate(`/`);
|
||||
console.log(`Loading stream data for ${streamerName}`);
|
||||
setStreamData({
|
||||
streamId: data.streamId,
|
||||
streamTitle: data.streamTitle,
|
||||
streamerName: data.streamerName,
|
||||
streamerId: data.streamerId,
|
||||
startTime: data.startTime,
|
||||
viewerCount: data.viewerCount,
|
||||
categoryId: data.categoryId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [streamId, streamerName]);
|
||||
|
||||
return (
|
||||
<div id="videoPage" className="w-full">
|
||||
@@ -44,20 +74,21 @@ const VideoPage: React.FC<VideoPageProps> = ({ streamId }) => {
|
||||
<div id="container" className="bg-gray-900">
|
||||
<VideoPlayer streamId={streamId} />
|
||||
|
||||
{isLoggedIn ? (
|
||||
<ChatPanel streamId={streamId} />
|
||||
) : (
|
||||
<ChatPanel streamId={streamId} />
|
||||
)}
|
||||
|
||||
<div
|
||||
id="stream-info"
|
||||
className="flex"
|
||||
style={{ gridArea: "3 / 1 / 4 / 2" }}
|
||||
>
|
||||
<Button onClick={() => setShowCheckout(true)} extraClasses="mx-auto mb-4">
|
||||
{isLoggedIn && (
|
||||
<Button
|
||||
onClick={() => setShowCheckout(true)}
|
||||
extraClasses="mx-auto mb-4"
|
||||
>
|
||||
Payment Screen Test
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -68,7 +68,8 @@ def get_recommended_categories() -> list | list[dict]:
|
||||
|
||||
return jsonify({'categories': categories})
|
||||
|
||||
@stream_bp.route('/get_streamer_data/<int:streamer_username>')
|
||||
|
||||
@stream_bp.route('/get_streamer_data/<string:streamer_username>')
|
||||
def get_streamer_data(streamer_username):
|
||||
"""
|
||||
Returns a given streamer's data
|
||||
@@ -98,7 +99,7 @@ def get_streamer_status(streamer_username):
|
||||
})
|
||||
|
||||
|
||||
@stream_bp.route('/get_stream_data/<string:streamer_username>', methods=['GET'])
|
||||
@stream_bp.route('/get_stream_data/<string:streamer_username>')
|
||||
def get_stream(streamer_username):
|
||||
"""
|
||||
Returns a streamer's most recent stream data
|
||||
@@ -119,7 +120,7 @@ def get_following_categories_streams():
|
||||
return jsonify(streams)
|
||||
|
||||
|
||||
@stream_bp.route('/get_stream_data/<string:streamer_username>/<int:stream_id>', methods=['GET'])
|
||||
@stream_bp.route('/get_stream_data/<string:streamer_username>/<int:stream_id>')
|
||||
def get_specific_stream(streamer_username, stream_id):
|
||||
"""
|
||||
Returns a streamer's stream data given stream_id
|
||||
@@ -132,7 +133,7 @@ def get_specific_stream(streamer_username, stream_id):
|
||||
abort(404)
|
||||
|
||||
@login_required
|
||||
@stream_bp.route('/get_followed_streamers', methods=['GET'])
|
||||
@stream_bp.route('/get_followed_streamers')
|
||||
def get_followed_streamers():
|
||||
"""
|
||||
Queries DB to get a list of followed streamers
|
||||
|
||||
@@ -46,12 +46,19 @@ INSERT INTO subscribes (user_id, subscribed_id, since, expires) VALUES
|
||||
(4, 104, '2024-09-12', '2025-01-12'),
|
||||
(5, 105, '2024-08-30', '2025-02-28');
|
||||
|
||||
INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio) VALUES
|
||||
('GamerDude2', 'password123', 'gamerdude3@gmail.com', 3200, '7890', 0, 'Streaming my gaming adventures!');
|
||||
|
||||
SELECT * FROM users;
|
||||
SELECT * FROM streams;
|
||||
SELECT * FROM follows;
|
||||
SELECT * FROM user_preferences;
|
||||
SELECT * FROM subscribes;
|
||||
SELECT * FROM categories;
|
||||
SELECT * FROM streams;
|
||||
SELECT * FROM chat;
|
||||
SELECT * FROM tags;
|
||||
SELECT * FROM stream_tags;
|
||||
|
||||
-- To see all tables in the database
|
||||
SELECT name FROM sqlite_master WHERE type='table';
|
||||
|
||||
INSERT INTO users (username, password, email, num_followers, stream_key, is_partnered, bio) VALUES
|
||||
('GamerDude2', 'password123', 'gamerdude3@gmail.com', 3200, '7890', 0, 'Streaming my gaming adventures!');
|
||||
|
||||
Reference in New Issue
Block a user