From a4f66ba3214442683fa60ba3e9b39f0546afe85b Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Tue, 28 Jan 2025 21:06:23 +0000 Subject: [PATCH] Feat: Update to VideoPage to display stream data; --- frontend/index.html | 2 +- frontend/src/components/Layout/BaseLayout.tsx | 24 ----- frontend/src/components/Layout/ListRow.tsx | 87 +++++++++++++++++++ .../src/components/Layout/StreamListRow.tsx | 79 ----------------- .../src/components/Stream/StreamerRoute.tsx | 5 +- frontend/src/context/StreamsContext.tsx | 45 ++++++++-- frontend/src/pages/HomePage.tsx | 20 ++--- frontend/src/pages/UserPage.tsx | 13 ++- frontend/src/pages/VideoPage.tsx | 63 ++++++++++---- web_server/blueprints/streams.py | 9 +- web_server/database/testing_data.sql | 13 ++- web_server/database/users.sql | 10 +-- 12 files changed, 215 insertions(+), 155 deletions(-) delete mode 100644 frontend/src/components/Layout/BaseLayout.tsx create mode 100644 frontend/src/components/Layout/ListRow.tsx delete mode 100644 frontend/src/components/Layout/StreamListRow.tsx diff --git a/frontend/index.html b/frontend/index.html index 113daa2..8a2e42c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,7 +7,7 @@ Team Software Project -
+
diff --git a/frontend/src/components/Layout/BaseLayout.tsx b/frontend/src/components/Layout/BaseLayout.tsx deleted file mode 100644 index f8e7ee7..0000000 --- a/frontend/src/components/Layout/BaseLayout.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// base.html -// src/components/Layout/BaseLayout.tsx -import React from 'react'; - -interface BaseLayoutProps { - children: React.ReactNode; -} - -const BaseLayout: React.FC = ({ children }) => { - return ( - - - - - Live Stream - - - {children} - - - ); -}; - -export default BaseLayout; diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx new file mode 100644 index 0000000..75e0aa5 --- /dev/null +++ b/frontend/src/components/Layout/ListRow.tsx @@ -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 = ({ + type, + title, + streamer, + viewers, + thumbnail, + onItemClick, +}) => { + return ( +
+
+ {thumbnail ? ( + {title} + ) : ( +
+ )} +
+
+

{title}

+ {type === "stream" &&

{streamer}

} +

{viewers} viewers

+
+
+ ); +}; + +// Row of entries +const ListRow: React.FC = ({ + title, + description, + items, + onClick, +}) => { + return ( +
+
+

{title}

+

{description}

+
+
+ {items.map((item) => ( + + onClick?.(item.id, item.streamer || item.title) + } + /> + ))} +
+
+ ); +}; + +export default ListRow; diff --git a/frontend/src/components/Layout/StreamListRow.tsx b/frontend/src/components/Layout/StreamListRow.tsx deleted file mode 100644 index be7b279..0000000 --- a/frontend/src/components/Layout/StreamListRow.tsx +++ /dev/null @@ -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 = ({ - stream, - onClick, -}) => { - return ( -
-
- {stream.thumbnail ? ( - {stream.title} - ) : ( -
- )} -
-
-

{stream.title}

-

{stream.streamer}

-

{stream.viewers} viewers

-
-
- ); -}; - -// Row of stream entries -const StreamListRow: React.FC = ({ - title, - description, - streams, - onStreamClick, -}) => { - return ( -
-
-

{title}

-

{description}

-
-
- {streams.map((stream) => ( - onStreamClick?.(stream.id, stream.streamer)} - /> - ))} -
-
- ); -}; - -export default StreamListRow; diff --git a/frontend/src/components/Stream/StreamerRoute.tsx b/frontend/src/components/Stream/StreamerRoute.tsx index 87f254e..3bbcaeb 100644 --- a/frontend/src/components/Stream/StreamerRoute.tsx +++ b/frontend/src/components/Stream/StreamerRoute.tsx @@ -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
Loading...
; // Or your loading component } - return isLive ? : ; + // streamId=0 is a special case for the streamer's latest stream + return isLive ? : ; }; export default StreamerRoute; diff --git a/frontend/src/context/StreamsContext.tsx b/frontend/src/context/StreamsContext.tsx index caa5a7b..ec43c50 100644 --- a/frontend/src/context/StreamsContext.tsx +++ b/frontend/src/context/StreamsContext.tsx @@ -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(undefined); export function StreamsProvider({ children }: { children: React.ReactNode }) { const [featuredStreams, setFeaturedStreams] = useState([]); - const [featuredCategories, setFeaturedCategories] = useState( + const [featuredCategories, setFeaturedCategories] = useState( [] ); 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); }); }, []); diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 1aab188..f04cb79 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -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 = ({ variant = "default" }) => { return (
- {/*//TODO Extract StreamListRow away, to ListRow so that it makes sense for categories to be there also */} - - - {}} //TODO + items={featuredCategories} + onClick={() => {}} //TODO />
diff --git a/frontend/src/pages/UserPage.tsx b/frontend/src/pages/UserPage.tsx index 692aed3..fe0b64f 100644 --- a/frontend/src/pages/UserPage.tsx +++ b/frontend/src/pages/UserPage.tsx @@ -1,7 +1,16 @@ import React from "react"; -const UserPage: React.FC = () => { - return
; +interface UserPageProps { + username: string; +} + +const UserPage: React.FC = ({ username }) => { + return ( +
+

{username}

+

Profile page for {username}

+
+ ); }; export default UserPage; diff --git a/frontend/src/pages/VideoPage.tsx b/frontend/src/pages/VideoPage.tsx index 00fb44b..ec872bd 100644 --- a/frontend/src/pages/VideoPage.tsx +++ b/frontend/src/pages/VideoPage.tsx @@ -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 = ({ 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(); + const navigate = useNavigate(); useEffect(() => { // Prevent scrolling when checkout is open @@ -30,12 +42,30 @@ const VideoPage: React.FC = ({ 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 (
@@ -44,20 +74,21 @@ const VideoPage: React.FC = ({ streamId }) => {
- {isLoggedIn ? ( - - ) : ( - - )} +
- + {isLoggedIn && ( + + )}
diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 9c1cecc..b4c3e4e 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -68,7 +68,8 @@ def get_recommended_categories() -> list | list[dict]: return jsonify({'categories': categories}) -@stream_bp.route('/get_streamer_data/') + +@stream_bp.route('/get_streamer_data/') 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/', methods=['GET']) +@stream_bp.route('/get_stream_data/') 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//', methods=['GET']) +@stream_bp.route('/get_stream_data//') 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 diff --git a/web_server/database/testing_data.sql b/web_server/database/testing_data.sql index ac4f2d3..47df197 100644 --- a/web_server/database/testing_data.sql +++ b/web_server/database/testing_data.sql @@ -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!'); diff --git a/web_server/database/users.sql b/web_server/database/users.sql index ed278a2..c395bb0 100644 --- a/web_server/database/users.sql +++ b/web_server/database/users.sql @@ -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;