From d08809dc4d16e5af8ac4def2e68f72992464da5a Mon Sep 17 00:00:00 2001 From: Chris-1010 <122332721@umail.ucc.ie> Date: Mon, 3 Mar 2025 17:05:18 +0000 Subject: [PATCH 1/3] FIX: Search from `ResultsPage` --- frontend/src/components/Input/SearchBar.tsx | 104 ++++++----- frontend/src/pages/ResultsPage.tsx | 183 ++++++++++---------- 2 files changed, 149 insertions(+), 138 deletions(-) diff --git a/frontend/src/components/Input/SearchBar.tsx b/frontend/src/components/Input/SearchBar.tsx index 708888d..c565d50 100644 --- a/frontend/src/components/Input/SearchBar.tsx +++ b/frontend/src/components/Input/SearchBar.tsx @@ -1,66 +1,76 @@ -import React, { useState } from "react"; +// In SearchBar.tsx +import React, { useState, useEffect } from "react"; import Input from "./Input"; import { SearchIcon } from "lucide-react"; import { useNavigate } from "react-router-dom"; interface SearchBarProps { - value?: string; + value?: string; + onSearchResults?: (results: any, query: string) => void; } -const SearchBar: React.FC = ({ value = "" }) => { - const [searchQuery, setSearchQuery] = useState(value); - //const [debouncedQuery, setDebouncedQuery] = useState(searchQuery); - const navigate = useNavigate(); +const SearchBar: React.FC = ({ value = "", onSearchResults }) => { + const [searchQuery, setSearchQuery] = useState(value); + const navigate = useNavigate(); - const handleSearch = async () => { - if (!searchQuery.trim()) return; + // Update searchQuery when value prop changes + useEffect(() => { + setSearchQuery(value); + }, [value]); - try { - const response = await fetch("/api/search", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ query: searchQuery }), - }); + const handleSearch = async () => { + if (!searchQuery.trim()) return; - const data = await response.json(); + try { + const response = await fetch("/api/search", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ query: searchQuery }), + }); - navigate("/results", { - state: { searchResults: data, query: searchQuery }, - }); + const data = await response.json(); - // Handle the search results here - } catch (error) { - console.error("Error performing search:", error); - } - }; + // If we have a callback for search results, use that instead of navigating + if (onSearchResults) { + onSearchResults(data, searchQuery); + } else { + // Otherwise navigate to results page with the data + navigate("/results", { + state: { searchResults: data, query: searchQuery }, + }); + } + } catch (error) { + console.error("Error performing search:", error); + } + }; - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - handleSearch(); - } - }; + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + handleSearch(); + } + }; - const handleSearchChange = (e: React.ChangeEvent) => { - setSearchQuery(e.target.value); - }; + const handleSearchChange = (e: React.ChangeEvent) => { + setSearchQuery(e.target.value); + }; - return ( - + ); }; export default SearchBar; diff --git a/frontend/src/pages/ResultsPage.tsx b/frontend/src/pages/ResultsPage.tsx index bb3addc..712f400 100644 --- a/frontend/src/pages/ResultsPage.tsx +++ b/frontend/src/pages/ResultsPage.tsx @@ -1,3 +1,4 @@ +// In ResultsPage.tsx import React, { useEffect, useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import SearchBar from "../components/Input/SearchBar"; @@ -5,105 +6,105 @@ import ListRow from "../components/Layout/ListRow"; import DynamicPageContent from "../components/Layout/DynamicPageContent"; import { getCategoryThumbnail } from "../utils/thumbnailUtils"; -const ResultsPage: React.FC = ({ }) => { - const [overflow, setOverflow] = useState(false); - const location = useLocation(); - const navigate = useNavigate(); - const { searchResults, query } = location.state || { - searchResults: { categories: [], users: [], streams: [] }, - query: "", - }; +const ResultsPage: React.FC = () => { + const [overflow, setOverflow] = useState(false); + const location = useLocation(); + const navigate = useNavigate(); - useEffect(() => { - const checkHeight = () => { - setOverflow( - document.documentElement.scrollHeight + 20 > window.innerHeight - ); - }; + // Initialize with state from navigation, or empty defaults + const [searchState, setSearchState] = useState({ + searchResults: location.state?.searchResults || { categories: [], users: [], streams: [] }, + query: location.state?.query || "", + }); - checkHeight(); - window.addEventListener("resize", checkHeight); + // Handle new search results + const handleSearchResults = (results: any, newQuery: string) => { + console.log("New search results:", results); + setSearchState({ + searchResults: results, + query: newQuery, + }); - return () => window.removeEventListener("resize", checkHeight); - }, []); + // Update URL state without navigation + window.history.replaceState({ searchResults: results, query: newQuery }, "", "/results"); + }; - return ( - -
-

- Search results for "{query}" -

- + useEffect(() => { + // If location state changes, update our internal state + if (location.state) { + setSearchState({ + searchResults: location.state.searchResults, + query: location.state.query, + }); + } + }, [location.state]); -
- ({ - id: stream.user_id, - type: "stream", - title: stream.title, - username: stream.username, - streamCategory: stream.category_name, - viewers: stream.num_viewers, - thumbnail: stream.thumbnail_url, - }))} - title="Streams" - onItemClick={(streamer_name: string) => - (window.location.href = `/${streamer_name}`) - } - extraClasses="min-h-[calc((20vw+20vh)/4)] bg-[var(--liveNow)]" - itemExtraClasses="min-w-[calc(12vw+12vh/2)]" - amountForScroll={3} - /> + return ( + +
+

Search results for "{searchState.query}"

+ handleSearchResults(results, query)} /> - ({ - id: category.category_id, - type: "category", - title: category.category_name, - viewers: 0, - thumbnail: getCategoryThumbnail(category.category_name), - }))} - title="Categories" - onItemClick={(category_name: string) => - navigate(`/category/${category_name}`) - } - titleClickable={true} - extraClasses="min-h-[calc((20vw+20vh)/4)] bg-[var(--liveNow)]" - itemExtraClasses="min-w-[calc(12vw+12vh/2)]" - amountForScroll={3} - /> +
+ ({ + id: stream.user_id, + type: "stream", + title: stream.title, + username: stream.username, + streamCategory: stream.category_name, + viewers: stream.num_viewers, + thumbnail: stream.thumbnail_url, + }))} + title="Streams" + onItemClick={(streamer_name: string) => (window.location.href = `/${streamer_name}`)} + extraClasses="min-h-[calc((20vw+20vh)/4)] bg-[var(--liveNow)]" + itemExtraClasses="min-w-[calc(12vw+12vh/2)]" + amountForScroll={3} + /> - ({ - id: user.user_id, - type: "user", - title: `${user.is_live ? "🔴" : ""} ${user.username}`, - username: user.username - }))} - title="Users" - onItemClick={(username: string) => - (window.location.href = `/user/${username}`) - } - amountForScroll={3} - extraClasses="min-h-[calc((20vw+20vh)/4)] bg-[var(--liveNow)]" - itemExtraClasses="min-w-[calc(12vw+12vh/2)]" - /> -
+ ({ + id: category.category_id, + type: "category", + title: category.category_name, + viewers: 0, + thumbnail: getCategoryThumbnail(category.category_name), + }))} + title="Categories" + onItemClick={(category_name: string) => navigate(`/category/${category_name}`)} + titleClickable={true} + extraClasses="min-h-[calc((20vw+20vh)/4)] bg-[var(--liveNow)]" + itemExtraClasses="min-w-[calc(12vw+12vh/2)]" + amountForScroll={3} + /> -
-
-
-
- ); + ({ + id: user.user_id, + type: "user", + title: `${user.is_live ? "🔴" : ""} ${user.username}`, + username: user.username, + }))} + title="Users" + onItemClick={(username: string) => (window.location.href = `/user/${username}`)} + amountForScroll={3} + extraClasses="min-h-[calc((20vw+20vh)/4)] bg-[var(--liveNow)]" + itemExtraClasses="min-w-[calc(12vw+12vh/2)]" + /> +
+
+
+ ); }; export default ResultsPage; From ad50a2063725af60731fb192c45f2b21d7a4bc66 Mon Sep 17 00:00:00 2001 From: EvanLin3141 Date: Mon, 3 Mar 2025 21:31:55 +0000 Subject: [PATCH 2/3] ADD: VODS --- frontend/src/App.tsx | 4 ++ frontend/src/pages/UserPage.tsx | 5 +- frontend/src/pages/VodPlayer.tsx | 22 ++++++++ frontend/src/pages/Vods.tsx | 92 ++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 frontend/src/pages/VodPlayer.tsx create mode 100644 frontend/src/pages/Vods.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index baeeb97..96deb94 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,8 @@ import { Brightness } from "./context/BrightnessContext"; import LoadingScreen from "./components/Layout/LoadingScreen"; import Following from "./pages/Following"; import UnsubscribePage from "./pages/UnsubscribePage"; +import Vods from "./pages/Vods"; +import VodPlayer from "./pages/VodPlayer"; function App() { const [isLoggedIn, setIsLoggedIn] = useState(false); @@ -77,6 +79,8 @@ function App() { }> } /> } /> + } /> + } /> } /> diff --git a/frontend/src/pages/UserPage.tsx b/frontend/src/pages/UserPage.tsx index f97a394..f4d12ce 100644 --- a/frontend/src/pages/UserPage.tsx +++ b/frontend/src/pages/UserPage.tsx @@ -278,9 +278,8 @@ const UserPage: React.FC = () => { onMouseEnter={(e) => (e.currentTarget.style.boxShadow = "var(--follow-shadow)")} onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")} > -
    -
  • Streamers
  • -
+ +
{ + const params = useParams<{ vod_id?: string; username?: string }>(); + const vod_id = params.vod_id || "unknown"; + const username = params.username || "unknown"; + + const videoUrl = `/vods/${username}/${vod_id}.mp4`; + + return ( +
+

Watching VOD {vod_id}

+ +
+ ); +}; + +export default VodPlayer; diff --git a/frontend/src/pages/Vods.tsx b/frontend/src/pages/Vods.tsx new file mode 100644 index 0000000..970eaa6 --- /dev/null +++ b/frontend/src/pages/Vods.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useState } from "react"; +import { useAuth } from "../context/AuthContext"; +import { useNavigate, useParams } from "react-router-dom"; +import DynamicPageContent from "../components/Layout/DynamicPageContent"; + +interface Vod { + vod_id: number; + title: string; + datetime: string; + username: string; + category_name: string; + length: number; + views: number; +} + +const Vods: React.FC = () => { + const navigate = useNavigate(); + const { username } = useParams<{ username?: string }>(); + const { isLoggedIn } = useAuth(); + const [ownedVods, setOwnedVods] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + if (!username) return; + + const fetchVods = async () => { + try { + const response = await fetch(`/api/vods/${username}`); + if (!response.ok) throw new Error(`Failed to fetch VODs: ${response.statusText}`); + + const data = await response.json(); + setOwnedVods(data); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : "Error fetching VODs."; + setError(errorMessage); + } finally { + setLoading(false); + } + }; + + fetchVods(); + }, [username]); + + if (loading) return

Loading VODs...

; + if (error) return

{error}

; + + return ( + +
+

{username}'s VODs

+
+ {ownedVods.length === 0 ? ( +

No VODs available.

+ ) : ( + ownedVods.map((vod) => { + const thumbnailUrl = `/stream/${username}/vods/${vod.vod_id}.png`; + + return ( +
navigate(`/stream/${username}/vods/${vod.vod_id}`)} + > + {/* Thumbnail */} + {`Thumbnail { + e.currentTarget.onerror = null; + e.currentTarget.src = "/default-thumbnail.png"; + }} + /> + + {/* Video Info */} +

{vod.title}

+

📅 {new Date(vod.datetime).toLocaleString()}

+

🎮 {vod.category_name}

+

⏱ {Math.floor(vod.length / 60)} min

+

👀 {vod.views} views

+
+ ); + }) + )} +
+
+
+ ); +}; + +export default Vods; From d4e17838875ef96ad25159dd70bf7d02595a7925 Mon Sep 17 00:00:00 2001 From: white <122345776@umail.ucc.ie> Date: Mon, 3 Mar 2025 21:40:57 +0000 Subject: [PATCH 3/3] FEAT: Added a beautiful favicon --- frontend/index.html | 3 ++- frontend/public/images/favicon.ico | Bin 0 -> 15406 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/public/images/favicon.ico diff --git a/frontend/index.html b/frontend/index.html index ca0383c..c9d6c40 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,7 +2,8 @@ - + + Team Software Project diff --git a/frontend/public/images/favicon.ico b/frontend/public/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..548a732e3af79f9c5757c58cae3f69a82c226393 GIT binary patch literal 15406 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 T{vBk@?Lq(e_YVoYumt`KAQ;7E literal 0 HcmV?d00001