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 0000000..548a732 Binary files /dev/null and b/frontend/public/images/favicon.ico differ 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/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; 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;