+
{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