import { useEffect, useState, useCallback } from "react"; import { useParams, useSearchParams, Link } from "react-router-dom"; import { useDocumentTitle } from "../hooks/useDocumentTitle"; import { useMediaSync } from "../hooks/useMediaSync"; import { fetchVideo, fetchTranscript, fetchChapters } from "../api/videos"; import { BASE } from "../api/client"; import type { VideoDetail, TranscriptSegment, Chapter } from "../api/videos"; import { ApiError } from "../api/client"; import VideoPlayer from "../components/VideoPlayer"; import AudioWaveform from "../components/AudioWaveform"; import PlayerControls from "../components/PlayerControls"; import TranscriptSidebar from "../components/TranscriptSidebar"; import { copyToClipboard } from "../utils/clipboard"; export default function WatchPage() { const { videoId } = useParams<{ videoId: string }>(); const [searchParams] = useSearchParams(); // Parse ?t= param — clamp to 0 for NaN/negative const rawT = parseFloat(searchParams.get("t") ?? ""); const startTime = Number.isFinite(rawT) && rawT > 0 ? rawT : 0; const [video, setVideo] = useState(null); const [segments, setSegments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [transcriptError, setTranscriptError] = useState(false); const [chapters, setChapters] = useState([]); const [embedCopied, setEmbedCopied] = useState(false); const mediaSync = useMediaSync(); const handleCopyEmbed = useCallback(async () => { const height = video?.video_url ? 405 : 120; const snippet = ``; const ok = await copyToClipboard(snippet); if (ok) { setEmbedCopied(true); setTimeout(() => setEmbedCopied(false), 2000); } }, [videoId, video?.video_url]); useDocumentTitle(video ? `${video.filename} — Chrysopedia` : "Loading…"); // Fetch video detail useEffect(() => { if (!videoId) return; let cancelled = false; setLoading(true); setError(null); setTranscriptError(false); (async () => { try { const v = await fetchVideo(videoId); if (!cancelled) setVideo(v); } catch (err) { if (!cancelled) { if (err instanceof ApiError && err.status === 404) { setError("Video not found"); } else { setError("Failed to load video"); } } return; } // Fetch transcript separately — player works without it try { const t = await fetchTranscript(videoId); if (!cancelled) setSegments(t.segments); } catch { if (!cancelled) setTranscriptError(true); } // Fetch chapters — non-critical, fail silently try { const c = await fetchChapters(videoId); if (!cancelled) setChapters(c.chapters); } catch { // chapters are optional — ignore errors } if (!cancelled) setLoading(false); })(); return () => { cancelled = true; }; }, [videoId]); if (loading && !video) { return (

Loading video…

); } if (error) { return (

{error}

← Back to home
); } if (!video) return null; const streamUrl = `${BASE}/videos/${videoId}/stream`; return (

{video.filename}

{video.creator_name && video.creator_slug && ( {video.creator_name} )}
{video.video_url ? ( ) : ( )}
{transcriptError && (

Transcript unavailable — playback continues without it.

)}
); }