From 02280200414f98e6482ffbea464a32a323a2decb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Sat, 7 Feb 2026 08:17:25 +0300 Subject: [PATCH] feat(video-card): add edit, versioning, and delete functionality with dialogs - Implemented edit dialog for updating video title and description. - Added functionality to create new video versions with URL validation. - Included delete confirmation dialog for video removal. - Enhanced UI with loading indicators and error handling. - Updated video card layout for better user interaction. feat(youtube): extend YT namespace with playback rate methods - Added methods to set and get playback rate. - Included method to retrieve available playback rates. --- app/(dashboard)/projects/[projectId]/page.tsx | 4 +- .../videos/[videoId]/compare/page.tsx | 281 +++++ .../[projectId]/videos/[videoId]/page.tsx | 1064 ++++++++++++----- .../projects/[projectId]/videos/new/page.tsx | 103 +- app/api/comments/[commentId]/route.ts | 4 +- .../[projectId]/videos/[videoId]/route.ts | 4 +- .../videos/[videoId]/versions/route.ts | 4 +- app/api/projects/[projectId]/videos/route.ts | 4 +- .../versions/[versionId]/comments/route.ts | 4 +- app/api/watch/[videoId]/route.ts | 64 + app/watch/[videoId]/page.tsx | 757 +++++++----- components/video-card.tsx | 390 +++++- types/youtube.d.ts | 3 + 13 files changed, 1982 insertions(+), 704 deletions(-) create mode 100644 app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx create mode 100644 app/api/watch/[videoId]/route.ts diff --git a/app/(dashboard)/projects/[projectId]/page.tsx b/app/(dashboard)/projects/[projectId]/page.tsx index be42a0b..dcfde3d 100644 --- a/app/(dashboard)/projects/[projectId]/page.tsx +++ b/app/(dashboard)/projects/[projectId]/page.tsx @@ -92,7 +92,7 @@ export default async function ProjectPage({ params }: ProjectPageProps) { // Check access const isOwner = session?.user?.id === project.ownerId; const isMember = project.members.length > 0; - const isPublicOrLink = project.visibility !== 'PRIVATE'; + const isPublic = project.visibility === 'PUBLIC'; // Check workspace membership let isWorkspaceMember = false; @@ -116,7 +116,7 @@ export default async function ProjectPage({ params }: ProjectPageProps) { } } - if (!isOwner && !isMember && !isPublicOrLink && !isWorkspaceMember) { + if (!isOwner && !isMember && !isPublic && !isWorkspaceMember) { redirect('/dashboard'); } diff --git a/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx new file mode 100644 index 0000000..13974a6 --- /dev/null +++ b/app/(dashboard)/projects/[projectId]/videos/[videoId]/compare/page.tsx @@ -0,0 +1,281 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { useParams, useSearchParams } from 'next/navigation'; +import { + ArrowLeft, + MessageSquare, + ChevronDown, + Loader2, + GitCompareArrows, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Separator } from '@/components/ui/separator'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { cn } from '@/lib/utils'; + +interface Version { + id: string; + versionNumber: number; + versionLabel: string | null; + providerId: string; + videoId: string; + originalUrl: string; + title: string | null; + thumbnailUrl: string | null; + duration: number | null; + isActive: boolean; + _count: { comments: number }; +} + +interface VideoData { + id: string; + title: string; + description: string | null; + projectId: string; + project: { + name: string; + }; + versions: Version[]; +} + +function getEmbedUrl(version: Version) { + if (version.providerId === 'youtube') { + return `https://www.youtube.com/embed/${version.videoId}?enablejsapi=1&rel=0&modestbranding=1`; + } + if (version.providerId === 'vimeo') { + return `https://player.vimeo.com/video/${version.videoId}`; + } + return version.originalUrl; +} + +export default function CompareVersionsPage() { + const params = useParams(); + const searchParams = useSearchParams(); + const projectId = params.projectId as string; + const videoId = params.videoId as string; + + const [video, setVideo] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + const [leftVersionId, setLeftVersionId] = useState(null); + const [rightVersionId, setRightVersionId] = useState(null); + + useEffect(() => { + async function fetchVideo() { + try { + const res = await fetch(`/api/projects/${projectId}/videos/${videoId}`); + if (!res.ok) { + setError('Failed to load video'); + setLoading(false); + return; + } + const data = await res.json(); + setVideo(data); + + // Set initial versions from query params or defaults + const leftParam = searchParams.get('left'); + const rightParam = searchParams.get('right'); + + if (data.versions.length >= 2) { + // Sort versions ascending for comparison (older on left, newer on right) + const sorted = [...data.versions].sort( + (a: Version, b: Version) => a.versionNumber - b.versionNumber + ); + setLeftVersionId( + leftParam && sorted.find((v: Version) => v.id === leftParam) + ? leftParam + : sorted[sorted.length - 2].id + ); + setRightVersionId( + rightParam && sorted.find((v: Version) => v.id === rightParam) + ? rightParam + : sorted[sorted.length - 1].id + ); + } else if (data.versions.length === 1) { + setLeftVersionId(data.versions[0].id); + setRightVersionId(data.versions[0].id); + } + } catch { + setError('Failed to load video'); + } finally { + setLoading(false); + } + } + fetchVideo(); + }, [projectId, videoId, searchParams]); + + const leftVersion = video?.versions.find((v) => v.id === leftVersionId); + const rightVersion = video?.versions.find((v) => v.id === rightVersionId); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error || !video || video.versions.length < 2) { + return ( +
+
+

+ {error || 'Need at least 2 versions to compare'} +

+ +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ + + Back to Video + + +
+ + Compare Versions + • {video.title} +
+
+
+ + {/* Side-by-side comparison */} +
+ {/* Left panel */} +
+
+ + + + + + {video.versions.map((v) => ( + setLeftVersionId(v.id)} + disabled={v.id === rightVersionId} + > + + v{v.versionNumber} + + {v.versionLabel || `Version ${v.versionNumber}`} + + ))} + + + +
+ + {leftVersion?._count.comments || 0} comments +
+
+ +
+ {leftVersion && ( +