diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 34ae6cc..da08c32 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,8 +7,9 @@ import Home from './pages/Home'; import {useEffect} from "react"; import MyClips from './pages/MyClips'; import MyVods from './pages/MyVods'; -import VideoPlayer from "./pages/VideoPlayer.tsx"; -import VodPlayer from "./pages/VodPlayer.tsx"; +import MediaPlayer from "./pages/MediaPlayer.tsx"; +import { getVodById, getVodBlob } from "./utils/api/vods.ts"; +import { getClipById, getVideoBlob } from "./utils/api/clips.ts"; import EditClip from "./pages/EditClip.tsx"; import EditVod from "./pages/EditVod.tsx"; import Profile from "./pages/Profile.tsx"; @@ -31,8 +32,8 @@ function App() { } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/VideoPlayer.tsx b/frontend/src/pages/MediaPlayer.tsx similarity index 93% rename from frontend/src/pages/VideoPlayer.tsx rename to frontend/src/pages/MediaPlayer.tsx index 436edb4..ff31ced 100644 --- a/frontend/src/pages/VideoPlayer.tsx +++ b/frontend/src/pages/MediaPlayer.tsx @@ -1,13 +1,26 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ArrowLeft, Lock, Maximize, Minimize, Pause, Play, Volume1, Volume2, VolumeX } from "lucide-react"; -import type { Clip } from "../utils/types"; import { AuthError } from "../utils/api/client.ts"; -import { getClipById, getVideoBlob } from "../utils/api/clips.ts"; import Box from "../components/Box.tsx"; import { dateToTimeAgo, formatTime, stringToDate } from "../utils/utils.ts"; -const VideoPlayer = () => { +// Both VoDs and clips share the same on-screen shape. +type MediaItem = { + title: string, + createdAt: string, +}; + +type MediaPlayerProps = { + /** Human-readable name for the media, e.g. "VoD" or "clip". Used in messages. */ + noun: string, + /** Fetches the raw video blob for the given id. */ + fetchBlob: (id: string) => Promise, + /** Fetches the media metadata for the given id. */ + fetchDetails: (id: string) => Promise, +}; + +const MediaPlayer = ({ noun, fetchBlob, fetchDetails }: MediaPlayerProps) => { const { id } = useParams(); const navigate = useNavigate(); @@ -20,7 +33,7 @@ const VideoPlayer = () => { const [videoUrl, setVideoUrl] = useState(undefined); const [error, setError] = useState(null); const [notAuthenticated, setNotAuthenticated] = useState(false); - const [clip, setClip] = useState(null); + const [item, setItem] = useState(null); const [timeAgo, setTimeAgo] = useState(""); // Playback @@ -41,27 +54,27 @@ const VideoPlayer = () => { // ── Data loading ──────────────────────────────────────────────────────── useEffect(() => { - if (!id) { setError("Clip ID is required."); return; } + if (!id) { setError(`${noun} ID is required.`); return; } - getVideoBlob(id) + fetchBlob(id) .then((blob) => setVideoUrl(URL.createObjectURL(blob))) .catch((e) => { if (e instanceof AuthError) setNotAuthenticated(true); }); - getClipById(id) - .then(setClip) + fetchDetails(id) + .then(setItem) .catch((e) => { if (e instanceof AuthError) setNotAuthenticated(true); - else setError("Failed to load clip details."); + else setError(`Failed to load ${noun.toLowerCase()} details.`); }); - }, [id]); + }, [id, noun, fetchBlob, fetchDetails]); useEffect(() => { - if (!clip?.createdAt) return; - const update = () => setTimeAgo(dateToTimeAgo(stringToDate(clip.createdAt))); + if (!item?.createdAt) return; + const update = () => setTimeAgo(dateToTimeAgo(stringToDate(item.createdAt))); update(); const interval = setInterval(update, 1000); return () => clearInterval(interval); - }, [clip]); + }, [item]); // ── Controls visibility ───────────────────────────────────────────────── const scheduleHide = useCallback(() => { @@ -192,7 +205,7 @@ const VideoPlayer = () => {

Not authenticated

-

You don't have access to this clip.

+

You don't have access to this {noun.toLowerCase()}.

- - ); - } - - return ( -
- - -
isPlaying && setControlsVisible(false)} - onClick={togglePlay} - style={{ cursor: controlsVisible ? 'default' : 'none' }} - > -