fixed client review player annotations
Deploy / deploy (push) Successful in 2m46s

This commit is contained in:
twotalesanimation
2026-07-02 15:58:20 +02:00
parent 75d5149bea
commit 7026a5342b
4 changed files with 125 additions and 3 deletions
@@ -0,0 +1,68 @@
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
import { validateReviewToken } from "@/lib/review-auth";
/** Find or create a guest user for the client reviewer based on the session email */
async function getOrCreateClientUser(email: string, label?: string | null) {
const existing = await db.user.findUnique({ where: { email } });
if (existing) return existing;
return db.user.create({
data: {
email,
name: label ?? email.split("@")[0],
role: "CLIENT",
isActive: true,
},
});
}
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ token: string }> }
) {
const { token } = await params;
const result = await validateReviewToken(token, req);
if (result.type === "requiresPassword") {
return NextResponse.json({ requiresPassword: true }, { status: 401 });
}
if (result.type === "invalid") {
return NextResponse.json({ error: "Invalid or expired review link" }, { status: 403 });
}
const session = result.session;
const body = await req.json();
const { versionId, frameNumber, drawingData, color } = body;
if (!versionId || frameNumber == null || !drawingData) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
// Ensure the version belongs to this project
const version = await db.version.findUnique({
where: { id: versionId },
include: {
shot: { select: { projectId: true } },
task: { select: { projectId: true } },
},
});
const projectId = version?.shot?.projectId ?? version?.task?.projectId;
if (!version || projectId !== session.projectId) {
return NextResponse.json({ error: "Version not found" }, { status: 404 });
}
// Resolve commenter identity
const email = session.email ?? `client+${token.slice(0, 8)}@review.external`;
const user = await getOrCreateClientUser(email, session.label);
const annotation = await db.annotation.create({
data: {
versionId,
authorId: user.id,
frameNumber,
drawingData,
color: color ?? "#ef4444",
},
});
return NextResponse.json({ success: true, annotation });
}
@@ -445,6 +445,23 @@ export default function ClientReviewPage({
comments={comments as any}
versionId={version.id}
onAddComment={handlePlayerAddComment}
saveAnnotationFn={async (data) => {
const res = await fetch(`/api/client/${token}/annotation`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Failed to save annotation");
}}
addCommentFn={async (data) => {
const res = await fetch(`/api/client/${token}/comment`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Failed to post comment");
await refreshComments(token, version.id);
}}
/>
</div>
+32 -3
View File
@@ -14,6 +14,20 @@ import { useToast } from "@/components/ui/use-toast";
import type { AnnotationShape, AnnotationDrawingData, AnnotationPoint } from "@/types";
import { v4 as uuidv4 } from "uuid";
interface SaveAnnotationData {
versionId: string;
frameNumber: number;
drawingData: AnnotationDrawingData;
color: string;
}
interface AddCommentData {
versionId: string;
frameNumber: number;
timestamp: number;
text: string;
}
interface AnnotationCanvasProps {
versionId: string;
frameNumber: number;
@@ -22,6 +36,10 @@ interface AnnotationCanvasProps {
showAnnotations: boolean;
existingAnnotations?: unknown[];
onAnnotationSaved?: (frameNumber: number) => void;
/** Override the default server action used to persist annotations (e.g. for client-token routes) */
saveAnnotationFn?: (data: SaveAnnotationData) => Promise<void>;
/** Override the default server action used to create the companion comment */
addCommentFn?: (data: AddCommentData) => Promise<void>;
}
type DrawingState = {
@@ -38,6 +56,8 @@ export function AnnotationCanvas({
showAnnotations,
existingAnnotations = [],
onAnnotationSaved,
saveAnnotationFn,
addCommentFn,
}: AnnotationCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const { selectedTool, selectedColor, strokeWidth } = useReviewStore();
@@ -334,7 +354,16 @@ export function AnnotationCanvas({
canvasHeight: canvas?.offsetHeight ?? 1080,
version: "1.0",
};
await saveAnnotation({
const doSaveAnnotation = saveAnnotationFn
? saveAnnotationFn
: (data: SaveAnnotationData) => saveAnnotation(data).then(() => undefined);
const doAddComment = addCommentFn
? addCommentFn
: (data: AddCommentData) => addComment(data).then(() => undefined);
await doSaveAnnotation({
versionId,
frameNumber,
drawingData,
@@ -344,7 +373,7 @@ export function AnnotationCanvas({
// Create a companion comment ONCE per frame so the annotation appears in the panel.
// Additional strokes on the same frame update the existing comment count silently.
if (!annotationCommentedFrames.has(frameNumber)) {
await addComment({
await doAddComment({
versionId,
frameNumber,
timestamp: frameNumber / fps,
@@ -360,7 +389,7 @@ export function AnnotationCanvas({
variant: "destructive",
});
}
}, [versionId, frameNumber, fps, selectedColor, annotationCommentedFrames, onAnnotationSaved, toast]);
}, [versionId, frameNumber, fps, selectedColor, annotationCommentedFrames, onAnnotationSaved, toast, saveAnnotationFn, addCommentFn]);
// ── Touch events (mirrors mouse events for mobile drawing) ──────────────
const handleTouchStart = useCallback(
+8
View File
@@ -33,6 +33,10 @@ interface ReviewPlayerProps {
className?: string;
onAddComment?: (frameNumber: number, timestamp: number) => void;
onAnnotationSaved?: (frameNumber: number) => void;
/** Override the default server action used to persist annotations (e.g. for client-token routes) */
saveAnnotationFn?: (data: { versionId: string; frameNumber: number; drawingData: unknown; color: string }) => Promise<void>;
/** Override the default server action used to create the companion comment */
addCommentFn?: (data: { versionId: string; frameNumber: number; timestamp: number; text: string }) => Promise<void>;
}
export const ReviewPlayer = forwardRef<ReviewPlayerRef, ReviewPlayerProps>(
@@ -46,6 +50,8 @@ export const ReviewPlayer = forwardRef<ReviewPlayerRef, ReviewPlayerProps>(
className,
onAddComment,
onAnnotationSaved,
saveAnnotationFn,
addCommentFn,
},
ref
) {
@@ -276,6 +282,8 @@ export const ReviewPlayer = forwardRef<ReviewPlayerRef, ReviewPlayerProps>(
showAnnotations={showAnnotations}
existingAnnotations={stableAnnotations}
onAnnotationSaved={onAnnotationSaved}
saveAnnotationFn={saveAnnotationFn as any}
addCommentFn={addCommentFn}
/>
{/* JKL hint overlay — shown briefly when reversing */}