From 72ce7af1ece708648dfb855d0483e1785fbcfb28 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 12 Jun 2026 09:33:54 +0200 Subject: [PATCH] mobile support for client review player2 --- app/api/client/[token]/project/route.ts | 3 +- app/client/[token]/page.tsx | 395 +++++++++++++++--------- components/player/FrameTimeline.tsx | 2 +- 3 files changed, 244 insertions(+), 156 deletions(-) diff --git a/app/api/client/[token]/project/route.ts b/app/api/client/[token]/project/route.ts index 87e0fe4..3b20f8f 100644 --- a/app/api/client/[token]/project/route.ts +++ b/app/api/client/[token]/project/route.ts @@ -34,10 +34,11 @@ export async function GET( projectId: session.projectId, sharedWithClient: true, }, - orderBy: [{ sequence: "asc" }, { shotCode: "asc" }], + orderBy: [{ episode: "asc" }, { sequence: "asc" }, { shotCode: "asc" }], select: { id: true, shotCode: true, + episode: true, sequence: true, description: true, status: true, diff --git a/app/client/[token]/page.tsx b/app/client/[token]/page.tsx index 2ce9110..a39f7a9 100644 --- a/app/client/[token]/page.tsx +++ b/app/client/[token]/page.tsx @@ -10,6 +10,7 @@ import { AlertCircle, Clock, ChevronRight, + ChevronDown, Package, } from 'lucide-react'; import { cn } from '@/lib/utils'; @@ -42,6 +43,7 @@ interface ClientTask { interface ClientShot { id: string; shotCode: string; + episode: string | null; sequence: string | null; description: string | null; status: string; @@ -164,6 +166,15 @@ export default function ClientPortalPage({ ); } + const [collapsedEpisodes, setCollapsedEpisodes] = useState>(new Set()); + const toggleEpisode = (ep: string) => { + setCollapsedEpisodes((prev) => { + const next = new Set(prev); + if (next.has(ep)) next.delete(ep); else next.add(ep); + return next; + }); + }; + const allTasks = [...shots.flatMap((s) => s.tasks), ...assetTasks]; const totalTasks = allTasks.length; const approved = allTasks.filter( @@ -176,11 +187,13 @@ export default function ClientPortalPage({ ).length; const pending = totalTasks - approved - needsChanges; - const shotsBySequence = shots.reduce>( + // Group shots by episode; null episode → "Shots" + const isEpisodic = shots.some((s) => s.episode != null); + const shotsByEpisode = shots.reduce>( (acc, shot) => { - const seq = shot.sequence ?? 'Shots'; - if (!acc[seq]) acc[seq] = []; - acc[seq].push(shot); + const key = shot.episode ?? 'Shots'; + if (!acc[key]) acc[key] = []; + acc[key].push(shot); return acc; }, {}, @@ -248,165 +261,239 @@ export default function ClientPortalPage({ -
- {Object.entries(shotsBySequence).map(([sequence, seqShots]) => ( -
-

- {sequence} -

-
- {seqShots.map((shot) => ( -
-
- {shot.thumbnailUrl && ( -
- {shot.shotCode} -
- )} -

- {shot.shotCode} - {shot.description && ( - - {shot.description} - - )} -

-
-
- {shot.tasks.map((task) => { - const ver = getLatestVersion(task); - const approvalKey = - ver?.approvalStatus ?? 'PENDING_REVIEW'; - const approval = - APPROVAL_STYLES[approvalKey] ?? - APPROVAL_STYLES.PENDING_REVIEW; - const ApprovalIcon = approval.Icon; - return ( - -
-

- {task.title} -

-

- {TASK_TYPE_LABELS[task.type] ?? task.type} -

- {ver?.notes && ( -

- “{ver.notes}” -

- )} +
+ {Object.entries(shotsByEpisode).map(([episode, epShots]) => { + const isCollapsed = collapsedEpisodes.has(episode); + const epTasks = epShots.flatMap((s) => s.tasks); + const epApproved = epTasks.filter( + (t) => getLatestVersion(t)?.approvalStatus === 'APPROVED', + ).length; + const epChanges = epTasks.filter((t) => + ['REJECTED', 'NEEDS_CHANGES'].includes( + getLatestVersion(t)?.approvalStatus ?? '', + ), + ).length; + const epPending = epTasks.length - epApproved - epChanges; + + return ( +
+ {/* Episode header — clickable */} + + + {/* Collapsible shot list */} + {!isCollapsed && ( +
+ {epShots.map((shot) => ( +
+
+ {shot.thumbnailUrl && ( +
+ {shot.shotCode}
- {ver ? ( -
- - v{String(ver.versionNumber).padStart(3, '0')} - - - - {approval.label} - -
- ) : ( - - No versions yet + )} +

+ {shot.shotCode} + {shot.description && ( + + {shot.description} )} - - - ); - })} -

+

+
+
+ {shot.tasks.map((task) => { + const ver = getLatestVersion(task); + const approvalKey = + ver?.approvalStatus ?? 'PENDING_REVIEW'; + const approval = + APPROVAL_STYLES[approvalKey] ?? + APPROVAL_STYLES.PENDING_REVIEW; + const ApprovalIcon = approval.Icon; + return ( + +
+

+ {task.title} +

+

+ {TASK_TYPE_LABELS[task.type] ?? task.type} +

+ {ver?.notes && ( +

+ “{ver.notes}” +

+ )} +
+ {ver ? ( +
+ + v{String(ver.versionNumber).padStart(3, '0')} + + + + {approval.label} + +
+ ) : ( + + No versions yet + + )} + + + ); + })} +
+
+ ))}
- ))} + )}
-
- ))} + ); + })} {assetTasks.length > 0 && ( -
-

- Assets -

-
- {assetTasks.map((task) => { - const ver = getLatestVersion(task); - const approvalKey = ver?.approvalStatus ?? 'PENDING_REVIEW'; - const approval = - APPROVAL_STYLES[approvalKey] ?? - APPROVAL_STYLES.PENDING_REVIEW; - const ApprovalIcon = approval.Icon; - return ( - - -
-

- {task.asset?.assetCode ?? TASK_TYPE_LABELS[task.type]} -

-

- {TASK_TYPE_LABELS[task.type] ?? task.type} -

-
-
-

- {task.title} -

- {ver?.notes && ( -

- “{ver.notes}” -

+
+ {/* Assets header */} + + + {!collapsedEpisodes.has('__assets__') && ( +
+ {assetTasks.map((task) => { + const ver = getLatestVersion(task); + const approvalKey = ver?.approvalStatus ?? 'PENDING_REVIEW'; + const approval = + APPROVAL_STYLES[approvalKey] ?? + APPROVAL_STYLES.PENDING_REVIEW; + const ApprovalIcon = approval.Icon; + return ( + - {ver ? ( -
- - v{String(ver.versionNumber).padStart(3, '0')} - - - - {approval.label} - + > + +
+

+ {task.asset?.assetCode ?? TASK_TYPE_LABELS[task.type]} +

+

+ {TASK_TYPE_LABELS[task.type] ?? task.type} +

- ) : ( - - No versions yet - - )} - - - ); - })} -
+
+

+ {task.title} +

+ {ver?.notes && ( +

+ “{ver.notes}” +

+ )} +
+ {ver ? ( +
+ + v{String(ver.versionNumber).padStart(3, '0')} + + + + {approval.label} + +
+ ) : ( + + No versions yet + + )} + + + ); + })} +
+ )}
)} diff --git a/components/player/FrameTimeline.tsx b/components/player/FrameTimeline.tsx index 1e44aa3..83096b8 100644 --- a/components/player/FrameTimeline.tsx +++ b/components/player/FrameTimeline.tsx @@ -172,7 +172,7 @@ export function FrameTimeline({ fps, comments, annotations = [], videoRef, onSee // ── Touch scrubbing ────────────────────────────────────────────────────── const getFrameFromTouch = useCallback( - (touch: Touch): number => { + (touch: { clientX: number }): number => { const canvas = canvasRef.current; if (!canvas || totalFrames === 0) return 0; const rect = canvas.getBoundingClientRect();