From 45208a51bef428a7bf79fc8f964170ddadfa065d Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Fri, 7 Feb 2025 03:57:54 +0000 Subject: [PATCH] FEAT: CategoryPage updated to display streams; General fixes and cleanup of unecessary logging; Update to 404 (NotFound) Page; --- frontend/src/App.tsx | 2 +- frontend/src/components/Layout/ListRow.tsx | 22 +++-- .../src/components/Stream/StreamerRoute.tsx | 18 ++-- frontend/src/components/Video/ChatPanel.tsx | 1 - frontend/src/context/SocketContext.tsx | 4 - frontend/src/pages/CategoryPage.tsx | 98 +++++++++++++++++-- frontend/src/pages/HomePage.tsx | 6 +- frontend/src/pages/NotFoundPage.tsx | 65 +++++++++++- frontend/src/pages/UserPage.tsx | 16 ++- web_server/blueprints/streams.py | 11 +++ web_server/utils/recommendation_utils.py | 12 +-- web_server/utils/stream_utils.py | 12 +++ 12 files changed, 225 insertions(+), 42 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 54f0566..7786faf 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -45,7 +45,7 @@ function App() { }> }> - } /> + } /> diff --git a/frontend/src/components/Layout/ListRow.tsx b/frontend/src/components/Layout/ListRow.tsx index 5afc2f2..7af762c 100644 --- a/frontend/src/components/Layout/ListRow.tsx +++ b/frontend/src/components/Layout/ListRow.tsx @@ -17,7 +17,7 @@ interface ListRowProps { description: string; items: ListItemProps[]; extraClasses?: string; - onClick: (itemId: number, itemName: string) => void; + onClick: (itemName: string) => void; } // Row of entries @@ -26,10 +26,12 @@ const ListRow: React.FC = ({ description, items, onClick, - extraClasses="", + extraClasses = "", }) => { return ( -
+

{title}

{description}

@@ -42,10 +44,16 @@ const ListRow: React.FC = ({ type={item.type} title={item.title} streamer={item.type === "stream" ? item.streamer : undefined} - streamCategory={item.type === "stream" ? item.streamCategory : undefined} + streamCategory={ + item.type === "stream" ? item.streamCategory : undefined + } viewers={item.viewers} thumbnail={item.thumbnail} - onItemClick={() => onClick?.(item.id, item.streamer || item.title)} + onItemClick={() => + item.type === "stream" && item.streamer + ? onClick?.(item.streamer) + : onClick?.(item.title) + } /> ))}
@@ -82,7 +90,9 @@ export const ListItem: React.FC = ({

{title}

{type === "stream" &&

{streamer}

} - {type === "stream" &&

{streamCategory}

} + {type === "stream" && ( +

{streamCategory}

+ )}

{viewers} viewers

diff --git a/frontend/src/components/Stream/StreamerRoute.tsx b/frontend/src/components/Stream/StreamerRoute.tsx index c7286ff..4ced6b1 100644 --- a/frontend/src/components/Stream/StreamerRoute.tsx +++ b/frontend/src/components/Stream/StreamerRoute.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import VideoPage from "../../pages/VideoPage"; -import UserPage from "../../pages/UserPage"; const StreamerRoute: React.FC = () => { const { streamerName } = useParams(); @@ -42,13 +41,16 @@ const StreamerRoute: React.FC = () => { } // streamId=0 is a special case for the streamer's latest stream - return isLive ? ( - - ) : streamerName ? ( - navigate(`/user/${streamerName}`) - ) : ( -
Streamer not found
- ); + if (isLive) { + return ; + } + + if (streamerName) { + navigate(`/user/${streamerName}`); + return null; + } + + return
Streamer not found
; }; export default StreamerRoute; diff --git a/frontend/src/components/Video/ChatPanel.tsx b/frontend/src/components/Video/ChatPanel.tsx index 2536388..23a77ab 100644 --- a/frontend/src/components/Video/ChatPanel.tsx +++ b/frontend/src/components/Video/ChatPanel.tsx @@ -67,7 +67,6 @@ const ChatPanel: React.FC = ({ // Handle live viewership socket.on("status", (data: any) => { - console.log("Live viewership: ", data); // returns dictionary {message: message, num_viewers: num_viewers} if (onViewerCountChange && data.num_viewers) { onViewerCountChange(data.num_viewers); } diff --git a/frontend/src/context/SocketContext.tsx b/frontend/src/context/SocketContext.tsx index 6f3d85f..84708bb 100644 --- a/frontend/src/context/SocketContext.tsx +++ b/frontend/src/context/SocketContext.tsx @@ -23,15 +23,12 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ const [isLoading, setIsLoading] = useState(true); useEffect(() => { - console.log("Start of useEffect"); - // Check if we already have a socket instance if (socketRef.current) { console.log("Socket already exists, closing existing socket"); socketRef.current.close(); } - console.log("Creating new socket connection"); const newSocket = io("http://localhost:8080", { path: "/socket.io/", transports: ["websocket"], @@ -82,7 +79,6 @@ export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ return () => { if (socketRef.current) { - console.log("Cleaning up socket connection..."); socketRef.current.disconnect(); socketRef.current.close(); socketRef.current = null; diff --git a/frontend/src/pages/CategoryPage.tsx b/frontend/src/pages/CategoryPage.tsx index 00493cf..393e587 100644 --- a/frontend/src/pages/CategoryPage.tsx +++ b/frontend/src/pages/CategoryPage.tsx @@ -1,9 +1,95 @@ -import React from 'react' +import React, { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import Navbar from "../components/Layout/Navbar"; +import ListRow from "../components/Layout/ListRow"; +import { useNavigate } from "react-router-dom"; -const CategoryPage = () => { - return ( -
CategoryPage
- ) +interface StreamData { + type: "stream"; + id: number; + title: string; + streamer: string; + streamCategory: string; + viewers: number; + thumbnail?: string; } -export default CategoryPage \ No newline at end of file +const CategoryPage: React.FC = () => { + const { category_name } = useParams<{ category_name: string }>(); + const [streams, setStreams] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const navigate = useNavigate(); + + useEffect(() => { + const fetchCategoryStreams = async () => { + try { + const response = await fetch(`/api/streams/popular/${category_name}`); + if (!response.ok) { + throw new Error("Failed to fetch category streams"); + } + const data = await response.json(); + const formattedData = data.map((stream: any) => ({ + type: "stream", + id: stream.user_id, + title: stream.title, + streamer: stream.username, + streamCategory: category_name, + viewers: stream.num_viewers, + thumbnail: + stream.thumbnail || + (category_name && + `/images/thumbnails/categories/${category_name + .toLowerCase() + .replace(/ /g, "_")}.webp`), + })); + setStreams(formattedData); + } catch (error) { + console.error("Error fetching category streams:", error); + } finally { + setIsLoading(false); + } + }; + + fetchCategoryStreams(); + }, [category_name]); + + const handleStreamClick = (streamerName: string) => { + navigate(`/${streamerName}`); + }; + + if (isLoading) { + return ( +
+ Loading... +
+ ); + } + + return ( +
+ + +
+ +
+ + {streams.length === 0 && !isLoading && ( +
+ No live streams found in this category +
+ )} +
+ ); +}; + +export default CategoryPage; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index e593f43..d85e606 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -12,13 +12,11 @@ const HomePage: React.FC = ({ variant = "default" }) => { const { featuredStreams, featuredCategories } = useStreams(); const navigate = useNavigate(); - const handleStreamClick = (streamerId: number, streamerName: string) => { - console.log(`Navigating to stream by user ${streamerId}`); + const handleStreamClick = (streamerName: string) => { navigate(`/${streamerName}`); }; - const handleCategoryClick = (categoryID: number, categoryName: string) => { - console.log(`Navigating to category ${categoryID}`); + const handleCategoryClick = (categoryName: string) => { navigate(`category/${categoryName}`); }; diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/NotFoundPage.tsx index 6a85b61..2479003 100644 --- a/frontend/src/pages/NotFoundPage.tsx +++ b/frontend/src/pages/NotFoundPage.tsx @@ -1,7 +1,68 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import Button from "../components/Layout/Button"; const NotFoundPage: React.FC = () => { - return
; + const navigate = useNavigate(); + const [stars, setStars] = useState<{ x: number; y: number }[]>([]); + const starSize = 20; + + useEffect(() => { + const loop = setInterval(() => { + if (Math.random() < 0.1) { + const newStar = { + x: Math.random() * (window.innerWidth - starSize), + y: -starSize, + }; + setStars((prev) => [...prev, newStar]); + } + + setStars((prev) => { + const newStars = prev.filter((star) => { + if ( + star.y > window.innerHeight - starSize && + star.y < window.innerHeight + ) { + return false; + } + if (star.y > window.innerHeight) return false; + return true; + }); + + return newStars.map((star) => ({ + ...star, + y: star.y + 5, + })); + }); + }, 10); + + return () => clearInterval(loop); + }, []); + + return ( +
+
+ {stars.map((star, index) => ( +
+ ★ +
+ ))} +
+
+
+

404

+

Page Not Found

+ +
+
+
+ ); }; export default NotFoundPage; diff --git a/frontend/src/pages/UserPage.tsx b/frontend/src/pages/UserPage.tsx index a7b3ca0..f61bd2c 100644 --- a/frontend/src/pages/UserPage.tsx +++ b/frontend/src/pages/UserPage.tsx @@ -28,7 +28,8 @@ const UserPage: React.FC = () => { "personal" | "streamer" | "user" | "admin" >("user"); const [profileData, setProfileData] = useState(); - const { isFollowing, checkFollowStatus, followUser, unfollowUser } = useFollow(); + const { isFollowing, checkFollowStatus, followUser, unfollowUser } = + useFollow(); const { showAuthModal, setShowAuthModal } = useAuthModal(); const { username: loggedInUsername } = useAuth(); const { username } = useParams(); @@ -90,7 +91,10 @@ const UserPage: React.FC = () => { .catch((err) => console.error("Error fetching stream data:", err)); } }) - .catch((err) => console.error("Error fetching profile data:", err)); + .catch((err) => { + console.error("Error fetching profile data:", err); + navigate("/404"); + }); // Check if the *logged-in* user is following this user if (loggedInUsername && username) checkFollowStatus(username); @@ -167,14 +171,18 @@ const UserPage: React.FC = () => { {!isFollowing ? ( ) : ( diff --git a/web_server/blueprints/streams.py b/web_server/blueprints/streams.py index 660a9a0..4bbf721 100644 --- a/web_server/blueprints/streams.py +++ b/web_server/blueprints/streams.py @@ -31,6 +31,17 @@ def get_popular_streams(no_streams) -> list[dict]: streams = get_highest_view_streams(no_streams) return jsonify(streams) +@stream_bp.route('/streams/popular/') +def get_popular_streams_by_category(category_name) -> list[dict]: + """ + Returns a list of streams live now with the highest viewers in a given category + """ + + category_id = get_category_id(category_name) + print(category_id, flush=True) + streams = get_streams_based_on_category(category_id) + return jsonify(streams) + @login_required @stream_bp.route('/streams/recommended') def get_recommended_streams() -> list[dict]: diff --git a/web_server/utils/recommendation_utils.py b/web_server/utils/recommendation_utils.py index 142450e..f4eac8e 100644 --- a/web_server/utils/recommendation_utils.py +++ b/web_server/utils/recommendation_utils.py @@ -19,7 +19,7 @@ def get_user_preferred_category(user_id: int) -> Optional[int]: def followed_categories_recommendations(user_id: int) -> Optional[List[dict]]: """ - Returns top 25 streams given a users category following + Returns top 25 streams given a user's category following """ with Database() as db: streams = db.fetchall(""" @@ -40,11 +40,11 @@ def get_streams_based_on_category(category_id: int) -> Optional[List[dict]]: """ with Database() as db: streams = db.fetchall(""" - SELECT u.user_id, title, username, num_viewers, category_name - FROM streams - JOIN users u ON streams.user_id = u.user_id - JOIN categories ON streams.category_id = categories.category_id - WHERE categories.category_id = ? + SELECT u.user_id, title, username, num_viewers, c.category_name + FROM streams s + JOIN users u ON s.user_id = u.user_id + JOIN categories c ON s.category_id = c.category_id + WHERE c.category_id = ? ORDER BY num_viewers DESC LIMIT 25 """, (category_id,)) diff --git a/web_server/utils/stream_utils.py b/web_server/utils/stream_utils.py index 2984c70..7328294 100644 --- a/web_server/utils/stream_utils.py +++ b/web_server/utils/stream_utils.py @@ -48,6 +48,18 @@ def get_current_stream_data(user_id: int) -> Optional[dict]: """, (user_id,)) return most_recent_stream +def get_category_id(category_name: str) -> Optional[int]: + """ + Returns the category_id given a category name + """ + with Database() as db: + data = db.fetchone(""" + SELECT category_id + FROM categories + WHERE category_name = ?; + """, (category_name,)) + return data['category_id'] if data else None + def get_vod(vod_id: int) -> dict: """ Returns data of a streamers vod