From a7195b5f78e0f1f715874c495344cdfcc0bd7993 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:29:16 +0200 Subject: [PATCH] delete comments enabled --- actions/comments.ts | 27 +++++ app/layout.tsx | 6 +- app/review/[versionId]/ReviewPageClient.tsx | 5 + app/review/[versionId]/page.tsx | 1 + components/comments/CommentPanel.tsx | 110 +++++++++++++++++++- 5 files changed, 142 insertions(+), 7 deletions(-) diff --git a/actions/comments.ts b/actions/comments.ts index e22f85d..4c2ac06 100644 --- a/actions/comments.ts +++ b/actions/comments.ts @@ -136,6 +136,25 @@ export async function resolveComment(commentId: string, resolved: boolean) { return { success: true }; } +export async function editComment(commentId: string, newText: string) { + const session = await auth(); + if (!session?.user) throw new Error("Unauthorized"); + + const trimmed = newText.trim(); + if (!trimmed) throw new Error("Comment text cannot be empty"); + + const comment = await db.comment.findUnique({ where: { id: commentId } }); + if (!comment) throw new Error("Comment not found"); + + if (comment.authorId !== session.user.id && session.user.role !== "ADMIN") { + throw new Error("Unauthorized"); + } + + await db.comment.update({ where: { id: commentId }, data: { text: trimmed } }); + revalidatePath(`/review/${comment.versionId}`); + return { success: true }; +} + export async function deleteComment(commentId: string) { const session = await auth(); if (!session?.user) throw new Error("Unauthorized"); @@ -149,6 +168,14 @@ export async function deleteComment(commentId: string) { } await db.comment.delete({ where: { id: commentId } }); + + // If this was an annotation companion comment, also delete the canvas drawings at that frame + if (comment.text.startsWith("✏️ Annotation at frame")) { + await db.annotation.deleteMany({ + where: { versionId: comment.versionId, frameNumber: comment.frameNumber }, + }); + } + revalidatePath(`/review/${comment.versionId}`); return { success: true }; } diff --git a/app/layout.tsx b/app/layout.tsx index d902218..d9963a9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,10 +8,10 @@ const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); export const metadata: Metadata = { title: { - template: "%s | FeedBack", - default: "FeedBack — VFX Review Platform", + template: "%s | VFX Review", + default: "VFX Review — Two Tales VFX Review Platform", }, - description: "Frame-accurate review and approval for VFX and animation studios.", + description: "VFX review and approval for Two Tales VFX.", }; export default function RootLayout({ diff --git a/app/review/[versionId]/ReviewPageClient.tsx b/app/review/[versionId]/ReviewPageClient.tsx index 516ced5..1aedad9 100644 --- a/app/review/[versionId]/ReviewPageClient.tsx +++ b/app/review/[versionId]/ReviewPageClient.tsx @@ -90,6 +90,7 @@ interface ReviewPageClientProps { canApprove: boolean; canShare: boolean; currentUserId: string; + userRole?: string; } const APPROVAL_STATUS_STYLES: Record = { @@ -106,6 +107,7 @@ export function ReviewPageClient({ canApprove, canShare, currentUserId, + userRole, }: ReviewPageClientProps) { const [comments, setComments] = useState(initialComments); const [annotations, setAnnotations] = useState(initialAnnotations); @@ -348,9 +350,12 @@ export function ReviewPageClient({ fps={version.fps} comments={comments} onCommentsChange={handleCommentsChange} + onAnnotationDeleted={handleAnnotationSaved} pendingFrame={pendingFrame} onPendingFrameCleared={() => setPendingFrame(null)} onSeekToFrame={(frame) => playerRef.current?.seekToFrame(frame)} + currentUserId={currentUserId} + isAdmin={userRole === "ADMIN"} /> diff --git a/app/review/[versionId]/page.tsx b/app/review/[versionId]/page.tsx index 78a94ad..7e50f9e 100644 --- a/app/review/[versionId]/page.tsx +++ b/app/review/[versionId]/page.tsx @@ -121,6 +121,7 @@ export default async function ReviewPage({ canApprove={canApprove} canShare={canShare} currentUserId={session.user.id} + userRole={session.user.role as string} /> ); } diff --git a/components/comments/CommentPanel.tsx b/components/comments/CommentPanel.tsx index 154fac5..7691eec 100644 --- a/components/comments/CommentPanel.tsx +++ b/components/comments/CommentPanel.tsx @@ -18,8 +18,12 @@ import { ChevronDown, ChevronUp, Filter, + Pencil, + Trash2, + Check, + X, } from "lucide-react"; -import { addComment, addReply, resolveComment } from "@/actions/comments"; +import { addComment, addReply, resolveComment, deleteComment, editComment } from "@/actions/comments"; import { useToast } from "@/components/ui/use-toast"; import type { CommentWithReplies } from "@/types"; @@ -28,9 +32,12 @@ interface CommentPanelProps { fps: number; comments: CommentWithReplies[]; onCommentsChange?: () => void; + onAnnotationDeleted?: () => void; pendingFrame?: number | null; onPendingFrameCleared?: () => void; onSeekToFrame?: (frame: number) => void; + currentUserId?: string; + isAdmin?: boolean; } export function CommentPanel({ @@ -38,9 +45,12 @@ export function CommentPanel({ fps, comments, onCommentsChange, + onAnnotationDeleted, pendingFrame, onPendingFrameCleared, onSeekToFrame, + currentUserId, + isAdmin, }: CommentPanelProps) { const { currentFrame, setCurrentFrame } = useReviewStore(); const [filterResolved, setFilterResolved] = useState<"all" | "unresolved" | "resolved">("all"); @@ -205,6 +215,9 @@ export function CommentPanel({ onSeekToFrame?.(frame); }} onResolved={onCommentsChange} + onAnnotationDeleted={onAnnotationDeleted} + currentUserId={currentUserId} + isAdmin={isAdmin} /> )) )} @@ -222,6 +235,9 @@ interface CommentThreadProps { isActive: boolean; onJumpToFrame: (frame: number) => void; onResolved?: () => void; + onAnnotationDeleted?: () => void; + currentUserId?: string; + isAdmin?: boolean; } function CommentThread({ @@ -230,12 +246,21 @@ function CommentThread({ isActive, onJumpToFrame, onResolved, + onAnnotationDeleted, + currentUserId, + isAdmin, }: CommentThreadProps) { const [showReplies, setShowReplies] = useState(false); const [replyText, setReplyText] = useState(""); const [isSubmittingReply, setIsSubmittingReply] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editText, setEditText] = useState(""); + const [isSavingEdit, setIsSavingEdit] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const { toast } = useToast(); + const canModify = !!currentUserId && (comment.authorId === currentUserId || !!isAdmin); + const handleReply = async () => { if (!replyText.trim()) return; setIsSubmittingReply(true); @@ -259,6 +284,40 @@ function CommentThread({ } }; + const handleStartEdit = () => { + setEditText(comment.text); + setIsEditing(true); + }; + + const handleSaveEdit = async () => { + if (!editText.trim()) return; + setIsSavingEdit(true); + try { + await editComment(comment.id, editText.trim()); + setIsEditing(false); + onResolved?.(); + } catch { + toast({ title: "Failed to update comment", variant: "destructive" }); + } finally { + setIsSavingEdit(false); + } + }; + + const handleDelete = async () => { + if (!confirm("Delete this comment? This cannot be undone.")) return; + setIsDeleting(true); + try { + const isAnnotationComment = comment.text.startsWith("✏️ Annotation at frame"); + await deleteComment(comment.id); + if (isAnnotationComment) onAnnotationDeleted?.(); + onResolved?.(); + } catch { + toast({ title: "Failed to delete comment", variant: "destructive" }); + } finally { + setIsDeleting(false); + } + }; + return (
{formatRelativeDate(comment.createdAt)} + {canModify && !isEditing && ( +
+ + +
+ )}
-

- {comment.text} -

+ {isEditing ? ( +
+