diff --git a/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx b/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx index 50fb7e7..b78418b 100644 --- a/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx +++ b/app/(dashboard)/projects/[projectId]/videos/[videoId]/page.tsx @@ -25,6 +25,11 @@ import { Link as LinkIcon, AlertCircle, GitCompareArrows, + Reply, + Pencil, + Trash2, + X, + ArrowUpRight, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -131,6 +136,15 @@ export default function VideoPage() { const [selectedTimestamp, setSelectedTimestamp] = useState(null); const [showResolved, setShowResolved] = useState(false); + // Reply/Edit/Delete state + const [replyingTo, setReplyingTo] = useState(null); + const [replyText, setReplyText] = useState(''); + const [isSubmittingReply, setIsSubmittingReply] = useState(false); + const [editingCommentId, setEditingCommentId] = useState(null); + const [editText, setEditText] = useState(''); + const [isSubmittingEdit, setIsSubmittingEdit] = useState(false); + const [deletingCommentId, setDeletingCommentId] = useState(null); + // New version dialog const [showVersionDialog, setShowVersionDialog] = useState(false); const [newVersionUrl, setNewVersionUrl] = useState(''); @@ -393,6 +407,141 @@ export default function VideoPage() { [activeVersionId] ); + // Reply to a comment + const handleReplyComment = useCallback(async (parentId: string) => { + if (!replyText.trim() || !activeVersion) return; + setIsSubmittingReply(true); + try { + const res = await fetch(`/api/versions/${activeVersion.id}/comments`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: replyText, + timestamp: comments.find((c) => c.id === parentId)?.timestamp ?? currentTime, + parentId, + }), + }); + if (res.ok) { + const newReply = await res.json(); + setVideo((prev) => { + if (!prev) return prev; + return { + ...prev, + versions: prev.versions.map((v) => + v.id === activeVersionId + ? { + ...v, + comments: v.comments.map((c) => + c.id === parentId + ? { ...c, replies: [...c.replies, newReply] } + : c + ), + } + : v + ), + }; + }); + setReplyText(''); + setReplyingTo(null); + } + } catch (err) { + console.error('Failed to reply:', err); + } finally { + setIsSubmittingReply(false); + } + }, [replyText, activeVersion, activeVersionId, comments, currentTime]); + + // Edit a comment + const handleEditComment = useCallback(async (commentId: string) => { + if (!editText.trim()) return; + setIsSubmittingEdit(true); + try { + const res = await fetch(`/api/comments/${commentId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ content: editText }), + }); + if (res.ok) { + setVideo((prev) => { + if (!prev) return prev; + return { + ...prev, + versions: prev.versions.map((v) => + v.id === activeVersionId + ? { + ...v, + comments: v.comments.map((c) => { + if (c.id === commentId) return { ...c, content: editText.trim() }; + return { + ...c, + replies: c.replies.map((r) => + r.id === commentId ? { ...r, content: editText.trim() } : r + ), + }; + }), + } + : v + ), + }; + }); + setEditingCommentId(null); + setEditText(''); + } + } catch (err) { + console.error('Failed to edit comment:', err); + } finally { + setIsSubmittingEdit(false); + } + }, [editText, activeVersionId]); + + // Delete a comment + const handleDeleteComment = useCallback(async (commentId: string) => { + setDeletingCommentId(commentId); + try { + const res = await fetch(`/api/comments/${commentId}`, { method: 'DELETE' }); + if (res.ok) { + setVideo((prev) => { + if (!prev) return prev; + return { + ...prev, + versions: prev.versions.map((v) => + v.id === activeVersionId + ? { + ...v, + comments: v.comments + .filter((c) => c.id !== commentId) + .map((c) => ({ + ...c, + replies: c.replies.filter((r) => r.id !== commentId), + })), + } + : v + ), + }; + }); + } + } catch (err) { + console.error('Failed to delete comment:', err); + } finally { + setDeletingCommentId(null); + } + }, [activeVersionId]); + + // Poll for new comments every 10 seconds + useEffect(() => { + if (!activeVersion) return; + const interval = setInterval(async () => { + try { + const res = await fetch(`/api/projects/${projectId}/videos/${videoId}`); + if (res.ok) { + const data = await res.json(); + setVideo(data); + } + } catch { /* silent */ } + }, 10000); + return () => clearInterval(interval); + }, [activeVersion, projectId, videoId]); + // New version URL handler const handleNewVersionUrlChange = (url: string) => { setNewVersionUrl(url); @@ -781,6 +930,8 @@ export default function VideoPage() { .map((comment) => { const authorName = comment.author?.name || comment.guestName || 'Anonymous'; + const isEditing = editingCommentId === comment.id; + const isReplying = replyingTo === comment.id; return (
- {comment.content &&

{comment.content}

} + {isEditing ? ( +
+