Feat: Update to VideoPage to display stream data;

This commit is contained in:
Chris-1010
2025-01-28 21:06:23 +00:00
parent e384976686
commit a4f66ba321
12 changed files with 215 additions and 155 deletions

View File

@@ -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>

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
});
}, []);

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}`)
}
}, [streamerName]);
// Fetch stream data for this streamer
fetch(
`/api/get_stream_data/${streamerName}${
streamId == 0 ? "" : `/${streamId}`
}`
).then((res) => {
if (!res.ok) {
console.error("Failed to load stream data:", res.statusText);
}
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} />
)}
<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>
{isLoggedIn && (
<Button
onClick={() => setShowCheckout(true)}
extraClasses="mx-auto mb-4"
>
Payment Screen Test
</Button>
)}
</div>
</div>

View File

@@ -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

View File

@@ -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!');

View File

@@ -36,9 +36,9 @@ CREATE TABLE user_preferences
user_id INT NOT NULL,
category_id INT NOT NULL,
favourability INT NOT NULL DEFAULT 0,
PRIMARY KEY(user_id, category_id),
FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY(category_id) REFERENCES categories(category_id) ON DELETE CASCADE
PRIMARY KEY (user_id, category_id),
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(category_id) ON DELETE CASCADE
);
DROP TABLE IF EXISTS subscribes;
@@ -49,8 +49,8 @@ CREATE TABLE subscribes
since DATETIME NOT NULL,
expires DATETIME NOT NULL,
PRIMARY KEY (user_id),
FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY(subscribed_id) REFERENCES users(user_id) ON DELETE CASCADE
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (subscribed_id) REFERENCES users(user_id) ON DELETE CASCADE
);
DROP TABLE IF EXISTS followed_categories;