From c98b5c56921b40099fd0d95872cc4702a0c95b9b Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:48:35 +0200 Subject: [PATCH] Load watch progress in one request on dashboard and player Add GET /api/progress/all, which returns the signed-in user's progress and merged watch segments for every video (or one playlist) using two queries. - Dashboard: replaces two sequential requests per video with a single request on load - Player sidebar: replaces one request per playlist video with a single request per playlist Co-Authored-By: Claude Fable 5.2 --- app/api/progress/all/route.ts | 65 +++++++++++++++++++++++++++ app/dashboard/dashboard-client.tsx | 70 +++++++----------------------- app/videoplayer/page.tsx | 36 +++++++-------- 3 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 app/api/progress/all/route.ts diff --git a/app/api/progress/all/route.ts b/app/api/progress/all/route.ts new file mode 100644 index 0000000..7245ea6 --- /dev/null +++ b/app/api/progress/all/route.ts @@ -0,0 +1,65 @@ +// app/api/progress/all/route.ts +// Everything the signed-in user has watched, in one response, so pages do not +// have to ask about each video separately. +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { getViewer } from '@/lib/video-access'; +import { mergeRanges } from '@/lib/watch-progress'; + +type Segment = { startSec: number; endSec: number; watchedAt?: Date }; + +/** + * GET /api/progress/all – all of the user's progress + * GET /api/progress/all?playlistId – only videos in that playlist + * + * Returns { progress: { [videoId]: { percent, watchedSec, segments } } }. + * Videos the user has never played are simply absent. + */ +export async function GET(req: Request) { + try { + const viewer = await getViewer(); + if (!viewer) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + + const playlistId = new URL(req.url).searchParams.get('playlistId'); + const where = { + userId: viewer.id, + ...(playlistId ? { video: { playlistId } } : {}), + }; + + const [progressRows, segmentRows] = await Promise.all([ + prisma.videoProgress.findMany({ + where, + select: { videoId: true, percent: true, watchedSec: true }, + }), + prisma.videoWatchSegment.findMany({ + where, + select: { videoId: true, startSec: true, endSec: true, watchedAt: true }, + }), + ]); + + const segmentsByVideo = new Map(); + for (const { videoId, ...segment } of segmentRows) { + const list = segmentsByVideo.get(videoId); + if (list) list.push(segment); + else segmentsByVideo.set(videoId, [segment]); + } + + const progress: Record< + string, + { percent: number; watchedSec: number; segments: Segment[] } + > = {}; + for (const row of progressRows) { + progress[row.videoId] = { + percent: row.percent, + watchedSec: row.watchedSec, + // Older data can hold many overlapping rows per video; send the merged view. + segments: mergeRanges(segmentsByVideo.get(row.videoId) ?? []), + }; + } + + return NextResponse.json({ progress }); + } catch (err: any) { + console.error('progress/all GET error', err); + return NextResponse.json({ error: err?.message ?? 'server error' }, { status: 500 }); + } +} diff --git a/app/dashboard/dashboard-client.tsx b/app/dashboard/dashboard-client.tsx index e5d82e2..f8a4590 100644 --- a/app/dashboard/dashboard-client.tsx +++ b/app/dashboard/dashboard-client.tsx @@ -173,65 +173,27 @@ export default function DashboardClient() { syncEnrollments(); }, [session]); + // One request for all of this user's progress; videos never played are + // simply absent and render as 0%. React.useEffect(() => { + let cancelled = false; + const fetchProgress = async () => { - const progressMap: Record = {}; - - // Fetch progress for playlist videos - for (const playlist of playlistsToShow) { - for (const video of playlist.videos || []) { - try { - const [progressRes, segmentsRes] = await Promise.all([ - fetch(`/api/progress?videoId=${video.id}`), - fetch(`/api/progress/segments?videoId=${video.id}`), - ]); - - if (progressRes.ok) { - const progressData = await progressRes.json(); - const segmentsData = segmentsRes.ok ? await segmentsRes.json() : { segments: [] }; - progressMap[video.id] = { - percent: progressData.percent ?? 0, - watchedSec: progressData.watchedSec ?? 0, - segments: segmentsData.segments ?? [], - }; - } - } catch (err) { - console.error(`Failed to fetch progress for video ${video.id}`, err); - progressMap[video.id] = { percent: 0, watchedSec: 0, segments: [] }; - } - } + try { + const res = await fetch('/api/progress/all'); + if (!res.ok) return; + const data = await res.json(); + if (!cancelled) setVideoProgress(data.progress ?? {}); + } catch (err) { + console.error('Failed to fetch progress', err); } - - // Fetch progress for latest videos - for (const video of latestVideos) { - try { - const [progressRes, segmentsRes] = await Promise.all([ - fetch(`/api/progress?videoId=${video.id}`), - fetch(`/api/progress/segments?videoId=${video.id}`), - ]); - - if (progressRes.ok) { - const progressData = await progressRes.json(); - const segmentsData = segmentsRes.ok ? await segmentsRes.json() : { segments: [] }; - progressMap[video.id] = { - percent: progressData.percent ?? 0, - watchedSec: progressData.watchedSec ?? 0, - segments: segmentsData.segments ?? [], - }; - } - } catch (err) { - console.error(`Failed to fetch progress for video ${video.id}`, err); - progressMap[video.id] = { percent: 0, watchedSec: 0, segments: [] }; - } - } - - setVideoProgress(progressMap); }; - if (playlistsToShow.length > 0 || latestVideos.length > 0) { - fetchProgress(); - } - }, [playlistsToShow, latestVideos]); + fetchProgress(); + return () => { + cancelled = true; + }; + }, []); const handleOpenVideo = (playlistId: string, videoId: string) => { // navigate to videoplayer page using query params (keeps your current structure) diff --git a/app/videoplayer/page.tsx b/app/videoplayer/page.tsx index ff65a05..6b108c1 100644 --- a/app/videoplayer/page.tsx +++ b/app/videoplayer/page.tsx @@ -303,32 +303,34 @@ React.useEffect(() => { }; }, [videoId]); -// Fetch segments for all playlist videos +// Fetch watched segments for every video in the playlist in one request +const loadedPlaylistId: string | undefined = playlist?.id; React.useEffect(() => { - if (!playlist?.videos || playlist.videos.length === 0) return; + if (!loadedPlaylistId) return; + let cancelled = false; const fetchPlaylistSegments = async () => { - const segments: Record = {}; - - for (const video of playlist.videos) { - try { - const res = await fetch(`/api/progress/segments?videoId=${video.id}`); - if (res.ok) { - const data = await res.json(); - segments[video.id] = data.segments ?? []; - } - } catch (err) { - console.error(`Failed to fetch segments for video ${video.id}:`, err); + try { + const res = await fetch(`/api/progress/all?playlistId=${encodeURIComponent(loadedPlaylistId)}`); + if (!res.ok || cancelled) return; + const data = await res.json(); + const segments: Record = {}; + for (const [id, entry] of Object.entries(data.progress ?? {})) { + segments[id] = entry.segments ?? []; } + // Keep anything newer that arrived from a progress save in the meantime. + if (!cancelled) setPlaylistSegments((prev) => ({ ...segments, ...prev })); + } catch (err) { + console.error('Failed to fetch playlist progress:', err); } - - // Keep anything newer that arrived from a progress save in the meantime. - setPlaylistSegments((prev) => ({ ...segments, ...prev })); }; setPlaylistSegments({}); fetchPlaylistSegments(); -}, [playlist?.videos]); + return () => { + cancelled = true; + }; +}, [loadedPlaylistId]); // Fetch user's VideoUnlock records React.useEffect(() => {