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")}
>
-
+
+
{
+ 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 */}
+

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