From ca65cf8f5886aa5761165938bb10ffa58ed97ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Tue, 10 Feb 2026 14:48:15 +0300 Subject: [PATCH] feat: Implement video version management API, enable comment tag editing, and enhance video duration display to include hours. --- app/(dashboard)/projects/[projectId]/page.tsx | 9 +- app/api/comments/[commentId]/route.ts | 42 +- .../[projectId]/videos/[videoId]/route.ts | 1 + .../[videoId]/versions/[versionId]/route.ts | 151 ++++++ app/api/watch/[videoId]/route.ts | 1 + components/video-card.tsx | 2 + components/video-page-content.tsx | 491 +++++++++++++----- 7 files changed, 522 insertions(+), 175 deletions(-) create mode 100644 app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts diff --git a/app/(dashboard)/projects/[projectId]/page.tsx b/app/(dashboard)/projects/[projectId]/page.tsx index 32284a2..aa91e9f 100644 --- a/app/(dashboard)/projects/[projectId]/page.tsx +++ b/app/(dashboard)/projects/[projectId]/page.tsx @@ -33,8 +33,13 @@ function VisibilityIcon({ visibility }: { visibility: string }) { function formatDuration(seconds: number | null): string { if (!seconds) return '0:00'; - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); + const totalSeconds = Math.floor(seconds); + const hrs = Math.floor(totalSeconds / 3600); + const mins = Math.floor((totalSeconds % 3600) / 60); + const secs = totalSeconds % 60; + if (hrs > 0) { + return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } return `${mins}:${secs.toString().padStart(2, '0')}`; } diff --git a/app/api/comments/[commentId]/route.ts b/app/api/comments/[commentId]/route.ts index cf8f4d4..91c2f0c 100644 --- a/app/api/comments/[commentId]/route.ts +++ b/app/api/comments/[commentId]/route.ts @@ -142,10 +142,10 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { } const body = await request.json(); - const { content, isResolved } = body; + const { content, isResolved, tagId } = body; - // Only author can edit content - if (content !== undefined && !isAuthor) { + // Only author can edit content or tag + if ((content !== undefined || tagId !== undefined) && !isAuthor) { return apiErrors.forbidden('Only the author can edit comment content'); } @@ -156,6 +156,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { const updateData: Record = {}; if (content !== undefined) updateData.content = content.trim(); + if (tagId !== undefined) updateData.tagId = tagId; if (isResolved !== undefined) { updateData.isResolved = isResolved; updateData.resolvedAt = isResolved ? new Date() : null; @@ -166,9 +167,11 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) { data: updateData, include: { author: { select: { id: true, name: true, image: true } }, + tag: { select: { id: true, name: true, color: true } }, replies: { include: { author: { select: { id: true, name: true, image: true } }, + tag: { select: { id: true, name: true, color: true } }, }, }, }, @@ -199,15 +202,6 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { where: { id: commentId }, include: { replies: { select: { voiceUrl: true } }, - version: { - include: { - video: { - include: { - project: true, - }, - }, - }, - }, }, }); @@ -215,30 +209,10 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { return apiErrors.notFound('Comment'); } - const project = comment.version.video.project; - const isOwner = project.ownerId === session.user.id; const isAuthor = comment.authorId === session.user.id; - // Check workspace membership for delete permissions - let isWorkspaceMember = false; - if (!isOwner && !isAuthor && session.user.id) { - const wsMember = await db.workspaceMember.findUnique({ - where: { - workspaceId_userId: { - workspaceId: project.workspaceId, - userId: session.user.id, - }, - }, - }); - const wsOwner = await db.workspace.findUnique({ - where: { id: project.workspaceId }, - select: { ownerId: true }, - }); - isWorkspaceMember = !!wsMember || wsOwner?.ownerId === session.user.id; - } - - if (!isOwner && !isAuthor && !isWorkspaceMember) { - return apiErrors.forbidden('Only the author or project owner can delete this comment'); + if (!isAuthor) { + return apiErrors.forbidden('You can only delete your own comments'); } // Collect all voice URLs to delete from R2 (comment + its replies) diff --git a/app/api/projects/[projectId]/videos/[videoId]/route.ts b/app/api/projects/[projectId]/videos/[videoId]/route.ts index ab525b7..33a63a7 100644 --- a/app/api/projects/[projectId]/videos/[videoId]/route.ts +++ b/app/api/projects/[projectId]/videos/[videoId]/route.ts @@ -57,6 +57,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { const response = successResponse({ ...video, isAuthenticated: !!session?.user?.id, + currentUserId: session?.user?.id || null, }); return withCacheControl(response, 'private, no-cache'); diff --git a/app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts b/app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts new file mode 100644 index 0000000..6a674a8 --- /dev/null +++ b/app/api/projects/[projectId]/videos/[videoId]/versions/[versionId]/route.ts @@ -0,0 +1,151 @@ +import { NextRequest } from 'next/server'; +import { db } from '@/lib/db'; +import { auth } from '@/lib/auth'; +import { ProjectMemberRole, WorkspaceMemberRole } from '@prisma/client'; +import { rateLimit } from '@/lib/rate-limit'; +import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response'; + +type RouteParams = { params: Promise<{ projectId: string; videoId: string; versionId: string }> }; + +async function getVersionWithAccess(projectId: string, videoId: string, versionId: string, userId: string) { + const version = await db.videoVersion.findFirst({ + where: { id: versionId, videoParentId: videoId }, + include: { + video: { + include: { + project: { + include: { + members: { where: { userId } }, + workspace: { + include: { + members: { where: { userId } }, + }, + }, + }, + }, + }, + }, + }, + }); + + if (!version || version.video.projectId !== projectId) { + return null; + } + + const project = version.video.project; + const isOwner = project.ownerId === userId; + const membership = project.members[0]; + const workspaceMembership = project.workspace.members[0]; + const canEdit = isOwner || + membership?.role === ProjectMemberRole.ADMIN || + workspaceMembership?.role === WorkspaceMemberRole.ADMIN; + + return { version, canEdit, isOwner }; +} + +// PATCH /api/projects/[projectId]/videos/[videoId]/versions/[versionId] +export async function PATCH(request: NextRequest, { params }: RouteParams) { + try { + const limited = await rateLimit(request, 'mutate'); + if (limited) return limited; + + const session = await auth(); + const { projectId, videoId, versionId } = await params; + + if (!session?.user?.id) { + return apiErrors.unauthorized(); + } + + const result = await getVersionWithAccess(projectId, videoId, versionId, session.user.id); + if (!result) { + return apiErrors.notFound('Version'); + } + if (!result.canEdit) { + return apiErrors.forbidden('Access denied'); + } + + const body = await request.json(); + const { duration, versionLabel, isActive } = body; + + const updateData: Record = {}; + if (duration !== undefined) updateData.duration = duration; + if (versionLabel !== undefined) updateData.versionLabel = versionLabel?.trim() || null; + + if (isActive === true) { + // Deactivate all other versions, then activate this one + await db.videoVersion.updateMany({ + where: { videoParentId: videoId }, + data: { isActive: false }, + }); + updateData.isActive = true; + } + + const updated = await db.videoVersion.update({ + where: { id: versionId }, + data: updateData, + }); + + const response = successResponse(updated); + return withCacheControl(response, 'private, no-store'); + } catch (error) { + console.error('Error updating version:', error); + return apiErrors.internalError('Failed to update version'); + } +} + +// DELETE /api/projects/[projectId]/videos/[videoId]/versions/[versionId] +export async function DELETE(request: NextRequest, { params }: RouteParams) { + try { + const limited = await rateLimit(request, 'mutate'); + if (limited) return limited; + + const session = await auth(); + const { projectId, videoId, versionId } = await params; + + if (!session?.user?.id) { + return apiErrors.unauthorized(); + } + + const result = await getVersionWithAccess(projectId, videoId, versionId, session.user.id); + if (!result) { + return apiErrors.notFound('Version'); + } + if (!result.canEdit) { + return apiErrors.forbidden('Access denied'); + } + + // Check there's more than one version — can't delete the last one + const versionCount = await db.videoVersion.count({ + where: { videoParentId: videoId }, + }); + + if (versionCount <= 1) { + return apiErrors.badRequest('Cannot delete the only version. Delete the video instead.'); + } + + const wasActive = result.version.isActive; + + // Delete the version (cascades to comments) + await db.videoVersion.delete({ where: { id: versionId } }); + + // If the deleted version was active, activate the latest remaining one + if (wasActive) { + const latestVersion = await db.videoVersion.findFirst({ + where: { videoParentId: videoId }, + orderBy: { versionNumber: 'desc' }, + }); + if (latestVersion) { + await db.videoVersion.update({ + where: { id: latestVersion.id }, + data: { isActive: true }, + }); + } + } + + const response = successResponse({ message: 'Version deleted' }); + return withCacheControl(response, 'private, no-store'); + } catch (error) { + console.error('Error deleting version:', error); + return apiErrors.internalError('Failed to delete version'); + } +} diff --git a/app/api/watch/[videoId]/route.ts b/app/api/watch/[videoId]/route.ts index b6f24c3..952dcd1 100644 --- a/app/api/watch/[videoId]/route.ts +++ b/app/api/watch/[videoId]/route.ts @@ -61,6 +61,7 @@ export async function GET(request: NextRequest, { params }: RouteParams) { visibility: project.visibility, }, isAuthenticated: !!session?.user?.id, + currentUserId: session?.user?.id || null, canComment: access.hasAccess, }); diff --git a/components/video-card.tsx b/components/video-card.tsx index d263614..21bed82 100644 --- a/components/video-card.tsx +++ b/components/video-card.tsx @@ -164,6 +164,8 @@ export function VideoCard({ video, projectId }: VideoCardProps) { }); if (res.ok) { setShowDeleteDialog(false); + // Give revalidatePath time to invalidate cache before refreshing + await new Promise((r) => setTimeout(r, 300)); router.refresh(); } } catch (err) { diff --git a/components/video-page-content.tsx b/components/video-page-content.tsx index 9ba723b..4a12905 100644 --- a/components/video-page-content.tsx +++ b/components/video-page-content.tsx @@ -49,6 +49,16 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { DropdownMenu, DropdownMenuContent, @@ -115,12 +125,18 @@ interface VideoData { }; versions: (Version & { comments: Comment[] })[]; isAuthenticated: boolean; + currentUserId: string | null; canComment?: boolean; } function formatTime(seconds: number): string { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); + const totalSeconds = Math.floor(seconds); + const hrs = Math.floor(totalSeconds / 3600); + const mins = Math.floor((totalSeconds % 3600) / 60); + const secs = totalSeconds % 60; + if (hrs > 0) { + return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } return `${mins}:${secs.toString().padStart(2, '0')}`; } @@ -138,6 +154,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi const iframeRef = useRef(null); const playerRef = useRef(null); const timelineRef = useRef(null); + const videoContainerRef = useRef(null); const [video, setVideo] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); @@ -149,6 +166,8 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi const [isMuted, setIsMuted] = useState(false); const [isDragging, setIsDragging] = useState(false); const [playbackSpeed, setPlaybackSpeed] = useState(1); + const [cursorIdle, setCursorIdle] = useState(false); + const cursorIdleTimerRef = useRef | null>(null); const [commentText, setCommentText] = useState(''); const [isSubmittingComment, setIsSubmittingComment] = useState(false); @@ -181,13 +200,14 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi const replyRecordingTimerRef = useRef | null>(null); const [editingCommentId, setEditingCommentId] = useState(null); const [editText, setEditText] = useState(''); + const [editTagId, setEditTagId] = useState(null); const [isSubmittingEdit, setIsSubmittingEdit] = useState(false); const [deletingCommentId, setDeletingCommentId] = useState(null); const isMutatingRef = useRef(false); const [guestName, setGuestName] = useState(''); const [guestNameConfirmed, setGuestNameConfirmed] = useState(mode === 'dashboard'); - + useEffect(() => { const saved = localStorage.getItem('openframe_guest_name'); if (saved) { @@ -210,7 +230,30 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi const projectId = propProjectId || video?.projectId; - const apiBasePath = mode === 'dashboard' + // Cursor idle detection: hide overlay when cursor idle for 3s while playing + const handleVideoMouseMove = useCallback(() => { + setCursorIdle(false); + if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current); + cursorIdleTimerRef.current = setTimeout(() => { + setCursorIdle(true); + }, 3000); + }, []); + + const handleVideoMouseLeave = useCallback(() => { + if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current); + setCursorIdle(false); + }, []); + + useEffect(() => { + return () => { + if (cursorIdleTimerRef.current) clearTimeout(cursorIdleTimerRef.current); + }; + }, []); + + // Determine current user ID for permission checks + const currentUserId = video?.currentUserId || null; + + const apiBasePath = mode === 'dashboard' ? `/api/projects/${propProjectId}/videos/${videoId}` : `/api/watch/${videoId}`; @@ -220,7 +263,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi const res = await fetch(apiBasePath, { cache: 'no-store' }); if (!res.ok) { const errorText = mode === 'dashboard' ? await res.text() : ''; - setError(mode === 'dashboard' + setError(mode === 'dashboard' ? `Failed to load video: ${res.status} ${errorText}` : 'Video not found or access denied' ); @@ -242,8 +285,8 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi fetchVideo(); }, [apiBasePath, mode]); - const activeVersion = video?.versions?.find((v) => v.id === activeVersionId) || - video?.versions?.find((v) => v.isActive) || + const activeVersion = video?.versions?.find((v) => v.id === activeVersionId) || + video?.versions?.find((v) => v.isActive) || video?.versions?.[0]; const comments = activeVersion?.comments || []; const filteredComments = comments.filter((c) => showResolved || !c.isResolved); @@ -324,6 +367,31 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi }; }, [activeVersionId]); + // Save detected duration to DB if the version doesn't have one stored + useEffect(() => { + if (!videoDuration || !activeVersion || !propProjectId) return; + if (activeVersion.duration && activeVersion.duration > 0) return; + + const roundedDuration = Math.round(videoDuration); + // Fire-and-forget PATCH to save duration + fetch(`/api/projects/${propProjectId}/videos/${videoId}/versions/${activeVersion.id}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ duration: roundedDuration }), + }).catch(() => { /* ignore save errors */ }); + + // Also update local state so the version object has the duration + setVideo((prev) => { + if (!prev) return prev; + return { + ...prev, + versions: prev.versions.map((v) => + v.id === activeVersion.id ? { ...v, duration: roundedDuration } : v + ), + }; + }); + }, [videoDuration, activeVersion?.id, activeVersion?.duration, propProjectId, videoId]); + useEffect(() => { if (!isReady || !playerRef.current) return; @@ -824,11 +892,11 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi versions: prev.versions.map((v) => v.id === activeVersionId ? { - ...v, - comments: v.comments.map((c) => - c.id === commentId ? { ...c, isResolved: !c.isResolved } : c - ), - } + ...v, + comments: v.comments.map((c) => + c.id === commentId ? { ...c, isResolved: !c.isResolved } : c + ), + } : v ), }; @@ -849,11 +917,11 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi versions: prev.versions.map((v) => v.id === activeVersionId ? { - ...v, - comments: v.comments.map((c) => - c.id === commentId ? { ...c, isResolved: currentlyResolved } : c - ), - } + ...v, + comments: v.comments.map((c) => + c.id === commentId ? { ...c, isResolved: currentlyResolved } : c + ), + } : v ), }; @@ -868,11 +936,11 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi versions: prev.versions.map((v) => v.id === activeVersionId ? { - ...v, - comments: v.comments.map((c) => - c.id === commentId ? { ...c, isResolved: currentlyResolved } : c - ), - } + ...v, + comments: v.comments.map((c) => + c.id === commentId ? { ...c, isResolved: currentlyResolved } : c + ), + } : v ), }; @@ -909,13 +977,13 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi versions: prev.versions.map((v) => v.id === activeVersionId ? { - ...v, - comments: v.comments.map((c) => - c.id === parentId - ? { ...c, replies: [...c.replies, optimisticReply] } - : c - ), - } + ...v, + comments: v.comments.map((c) => + c.id === parentId + ? { ...c, replies: [...c.replies, optimisticReply] } + : c + ), + } : v ), }; @@ -952,13 +1020,13 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi versions: prev.versions.map((v) => v.id === activeVersionId ? { - ...v, - comments: v.comments.map((c) => - c.id === parentId - ? { ...c, replies: c.replies.map(r => r.id === tempId ? newReply : r) } - : c - ), - } + ...v, + comments: v.comments.map((c) => + c.id === parentId + ? { ...c, replies: c.replies.map(r => r.id === tempId ? newReply : r) } + : c + ), + } : v ), }; @@ -971,13 +1039,13 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi versions: prev.versions.map((v) => v.id === activeVersionId ? { - ...v, - comments: v.comments.map((c) => - c.id === parentId - ? { ...c, replies: c.replies.filter(r => r.id !== tempId) } - : c - ), - } + ...v, + comments: v.comments.map((c) => + c.id === parentId + ? { ...c, replies: c.replies.filter(r => r.id !== tempId) } + : c + ), + } : v ), }; @@ -992,13 +1060,13 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi versions: prev.versions.map((v) => v.id === activeVersionId ? { - ...v, - comments: v.comments.map((c) => - c.id === parentId - ? { ...c, replies: c.replies.filter(r => r.id !== tempId) } - : c - ), - } + ...v, + comments: v.comments.map((c) => + c.id === parentId + ? { ...c, replies: c.replies.filter(r => r.id !== tempId) } + : c + ), + } : v ), }; @@ -1082,12 +1150,15 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi setIsSubmittingEdit(true); isMutatingRef.current = true; try { + const body: Record = { content: editText }; + if (editTagId !== undefined) body.tagId = editTagId; const res = await fetch(`/api/comments/${commentId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content: editText }), + body: JSON.stringify(body), }); if (res.ok) { + const editedTag = editTagId ? availableTags.find(t => t.id === editTagId) || null : null; setVideo((prev) => { if (!prev) return prev; return { @@ -1097,7 +1168,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi ? { ...v, comments: v.comments.map((c) => { - if (c.id === commentId) return { ...c, content: editText.trim() }; + if (c.id === commentId) return { ...c, content: editText.trim(), tag: editTagId !== undefined ? editedTag : c.tag }; return { ...c, replies: c.replies.map((r) => @@ -1112,6 +1183,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi }); setEditingCommentId(null); setEditText(''); + setEditTagId(null); } } catch (err) { console.error('Failed to edit comment:', err); @@ -1119,7 +1191,7 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi setIsSubmittingEdit(false); isMutatingRef.current = false; } - }, [editText, activeVersionId]); + }, [editText, editTagId, activeVersionId, availableTags]); const handleDeleteComment = useCallback(async (commentId: string) => { setDeletingCommentId(commentId); @@ -1217,13 +1289,20 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi }); if (res.ok) { - const videoRes = await fetch(`/api/projects/${propProjectId}/videos/${videoId}`); - if (videoRes.ok) { - const data = await videoRes.json(); - setVideo(data.data); - const active = data.data.versions.find((v: Version) => v.isActive) || data.data.versions[0]; - if (active) setActiveVersionId(active.id); - } + const versionData = await res.json(); + const newVersion = versionData.data; + // Optimistically add the new version to local state instead of refetching + setVideo((prev) => { + if (!prev) return prev; + const updatedVersions = prev.versions.map(v => ({ ...v, isActive: false })); + const createdVersion = { + ...newVersion, + comments: [], + }; + updatedVersions.unshift(createdVersion); + return { ...prev, versions: updatedVersions }; + }); + setActiveVersionId(newVersion.id); setShowVersionDialog(false); setNewVersionUrl(''); setNewVersionLabel(''); @@ -1236,6 +1315,43 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi } }; + // Version deletion + const [showDeleteVersionDialog, setShowDeleteVersionDialog] = useState(false); + const [versionToDelete, setVersionToDelete] = useState(null); + const [isDeletingVersion, setIsDeletingVersion] = useState(false); + + const handleDeleteVersion = async () => { + if (!versionToDelete || !propProjectId) return; + setIsDeletingVersion(true); + try { + const res = await fetch( + `/api/projects/${propProjectId}/videos/${videoId}/versions/${versionToDelete}`, + { method: 'DELETE' } + ); + if (res.ok) { + setVideo((prev) => { + if (!prev) return prev; + const remaining = prev.versions.filter((v) => v.id !== versionToDelete); + return { ...prev, versions: remaining }; + }); + // If deleted version was active, switch to the first remaining + if (activeVersionId === versionToDelete && video) { + const remaining = video.versions.filter((v) => v.id !== versionToDelete); + if (remaining.length > 0) setActiveVersionId(remaining[0].id); + } + setShowDeleteVersionDialog(false); + setVersionToDelete(null); + } else { + const data = await res.json(); + toast.error(data.error || 'Failed to delete version'); + } + } catch { + toast.error('Failed to delete version'); + } finally { + setIsDeletingVersion(false); + } + }; + const getEmbedUrl = (version: Version) => { if (version.providerId === 'youtube') { return `https://www.youtube.com/embed/${version.videoId}?enablejsapi=1&rel=0&modestbranding=1&controls=0&showinfo=0&iv_load_policy=3&disablekb=1`; @@ -1255,8 +1371,8 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi }; const containerHeight = mode === 'dashboard' ? 'h-[calc(100vh-3.5rem)]' : 'h-screen'; - const backHref = mode === 'dashboard' - ? `/projects/${propProjectId}` + const backHref = mode === 'dashboard' + ? `/projects/${propProjectId}` : (video?.projectId ? `/projects/${video.projectId}` : '/'); if (loading) { @@ -1449,9 +1565,47 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi ))} + {mode === 'dashboard' && video.versions.length > 1 && ( + <> + + { + setVersionToDelete(activeVersionId); + setShowDeleteVersionDialog(true); + }} + > + + Delete Current Version + + + )} + {/* Version Delete Confirmation */} + + + + Delete this version? + + This will permanently delete this version and all its comments. This cannot be undone. + + + + Cancel + + {isDeletingVersion && } + Delete Version + + + + + {mode === 'dashboard' && ( <> @@ -1531,8 +1685,14 @@ export function VideoPageContent({ mode, videoId, projectId: propProjectId }: Vi