diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx index 8e9d44e..1356e54 100644 --- a/app/(dashboard)/dashboard/page.tsx +++ b/app/(dashboard)/dashboard/page.tsx @@ -1,44 +1,61 @@ import Link from 'next/link'; -import { Plus, FolderOpen, Clock, Users } from 'lucide-react'; +import { Plus, FolderOpen, Clock, Users, Globe, Lock, UserPlus } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -// import { auth } from '@/lib/auth'; -// import { redirect } from 'next/navigation'; +import { Badge } from '@/components/ui/badge'; +import { auth } from '@/lib/auth'; +import { redirect } from 'next/navigation'; +import { db } from '@/lib/db'; -// Placeholder data - will be replaced with real data from database -const mockProjects = [ - { - id: '1', - name: 'Product Demo v2', - description: 'New product walkthrough video for Q1 launch', - videoCount: 3, - lastUpdated: '2 hours ago', - memberCount: 4, - }, - { - id: '2', - name: 'Marketing Campaign', - description: 'Social media ads for summer campaign', - videoCount: 8, - lastUpdated: '1 day ago', - memberCount: 2, - }, - { - id: '3', - name: 'Tutorial Series', - description: 'Getting started tutorials for new users', - videoCount: 12, - lastUpdated: '3 days ago', - memberCount: 1, - }, -]; +function formatRelativeTime(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); +} + +function VisibilityIcon({ visibility }: { visibility: string }) { + switch (visibility) { + case 'PUBLIC': + return ; + case 'INVITE': + return ; + default: + return ; + } +} export default async function DashboardPage() { - // TODO: Uncomment when database is set up - // const session = await auth(); - // if (!session) { - // redirect('/login'); - // } + const session = await auth(); + if (!session?.user?.id) { + redirect('/login'); + } + + // Fetch projects where user is owner or member + const projects = await db.project.findMany({ + where: { + OR: [ + { ownerId: session.user.id }, + { members: { some: { userId: session.user.id } } }, + ], + }, + include: { + _count: { + select: { + videos: true, + members: true, + }, + }, + }, + orderBy: { updatedAt: 'desc' }, + }); return (
@@ -59,31 +76,37 @@ export default async function DashboardPage() {
{/* Projects Grid */} - {mockProjects.length > 0 ? ( + {projects.length > 0 ? (
- {mockProjects.map((project) => ( + {projects.map((project) => ( - - - {project.name} - +
+ + + {project.name} + + + + {project.visibility.toLowerCase()} + +
- {project.description} + {project.description || 'No description'}
- {project.lastUpdated} + {formatRelativeTime(project.updatedAt)} - {project.memberCount} + {project._count.members + 1} - {project.videoCount} videos + {project._count.videos} videos
diff --git a/app/(dashboard)/projects/[projectId]/page.tsx b/app/(dashboard)/projects/[projectId]/page.tsx index 56dfda4..ba6e4f5 100644 --- a/app/(dashboard)/projects/[projectId]/page.tsx +++ b/app/(dashboard)/projects/[projectId]/page.tsx @@ -1,74 +1,122 @@ import Link from 'next/link'; -import { notFound } from 'next/navigation'; -import { - ArrowLeft, - Plus, - Settings, - Share2, +import { notFound, redirect } from 'next/navigation'; +import { + ArrowLeft, + Plus, + Settings, + Share2, Play, + Globe, + Lock, + UserPlus, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { VideoCard } from '@/components/video-card'; +import { auth } from '@/lib/auth'; +import { db } from '@/lib/db'; -// Mock data - will be replaced with real data -const mockProject = { - id: '1', - name: 'Product Demo v2', - description: 'New product walkthrough video for Q1 launch', - visibility: 'PRIVATE', - videos: [ - { - id: 'v1', - title: 'Main Product Walkthrough', - thumbnailUrl: 'https://img.youtube.com/vi/dQw4w9WgXcQ/mqdefault.jpg', - currentVersion: 3, - commentCount: 12, - duration: '5:42', - lastUpdated: '2 hours ago', - }, - { - id: 'v2', - title: 'Feature Highlight - Dashboard', - thumbnailUrl: 'https://img.youtube.com/vi/dQw4w9WgXcQ/mqdefault.jpg', - currentVersion: 1, - commentCount: 5, - duration: '2:18', - lastUpdated: '1 day ago', - }, - { - id: 'v3', - title: 'Onboarding Flow', - thumbnailUrl: 'https://img.youtube.com/vi/dQw4w9WgXcQ/mqdefault.jpg', - currentVersion: 2, - commentCount: 8, - duration: '3:55', - lastUpdated: '3 days ago', - }, - ], -}; +function VisibilityIcon({ visibility }: { visibility: string }) { + switch (visibility) { + case 'PUBLIC': + return ; + case 'INVITE': + return ; + default: + return ; + } +} + +function formatDuration(seconds: number | null): string { + if (!seconds) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; +} + +function formatRelativeTime(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return date.toLocaleDateString(); +} interface ProjectPageProps { params: Promise<{ projectId: string }>; } export default async function ProjectPage({ params }: ProjectPageProps) { + const session = await auth(); const { projectId } = await params; - - // TODO: Fetch real project data - const project = mockProject; - + + // Fetch project with videos + const project = await db.project.findUnique({ + where: { id: projectId }, + include: { + owner: { select: { id: true, name: true } }, + members: { + where: { userId: session?.user?.id || '' }, + select: { role: true }, + }, + videos: { + orderBy: { position: 'asc' }, + include: { + versions: { + where: { isActive: true }, + take: 1, + include: { + _count: { select: { comments: true } }, + }, + }, + _count: { select: { versions: true } }, + }, + }, + }, + }); + if (!project) { notFound(); } + // Check access + const isOwner = session?.user?.id === project.ownerId; + const isMember = project.members.length > 0; + const isPublicOrLink = project.visibility !== 'PRIVATE'; + + if (!isOwner && !isMember && !isPublicOrLink) { + redirect('/dashboard'); + } + + // Transform videos for VideoCard component + const videos = project.videos.map((video) => { + const activeVersion = video.versions[0]; + return { + id: video.id, + title: video.title, + thumbnailUrl: activeVersion?.thumbnailUrl || 'https://via.placeholder.com/320x180?text=No+Thumbnail', + currentVersion: video._count.versions, + commentCount: activeVersion?._count.comments || 0, + duration: formatDuration(activeVersion?.duration), + lastUpdated: formatRelativeTime(video.updatedAt), + }; + }); + + const canEdit = isOwner || project.members[0]?.role === 'ADMIN' || project.members[0]?.role === 'EDITOR'; + return (
{/* Back link */}
- @@ -81,7 +129,8 @@ export default async function ProjectPage({ params }: ProjectPageProps) {

{project.name}

- + + {project.visibility.toLowerCase()}
@@ -89,29 +138,37 @@ export default async function ProjectPage({ params }: ProjectPageProps) {

{project.description}

)}
- +
- - - + {(isOwner || project.members[0]?.role === 'ADMIN') && ( + + )} + {canEdit && ( + + )}
{/* Videos Grid */} - {project.videos.length > 0 ? ( + {videos.length > 0 ? (
- {project.videos.map((video) => ( + {videos.map((video) => ( ))}
@@ -123,12 +180,14 @@ export default async function ProjectPage({ params }: ProjectPageProps) {

Add your first video to start collecting feedback

- + {canEdit && ( + + )} )} diff --git a/app/(dashboard)/projects/[projectId]/settings/page.tsx b/app/(dashboard)/projects/[projectId]/settings/page.tsx new file mode 100644 index 0000000..f19d813 --- /dev/null +++ b/app/(dashboard)/projects/[projectId]/settings/page.tsx @@ -0,0 +1,339 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { ArrowLeft, Loader2, Globe, Lock, UserPlus, Trash2, AlertTriangle, Settings, Save } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; + +type Visibility = 'PRIVATE' | 'INVITE' | 'PUBLIC'; + +const visibilityOptions: { value: Visibility; label: string; description: string; icon: React.ReactNode }[] = [ + { + value: 'PRIVATE', + label: 'Private', + description: 'Only you can access this project', + icon: , + }, + { + value: 'INVITE', + label: 'Invite Only', + description: 'Share with specific people via email', + icon: , + }, + { + value: 'PUBLIC', + label: 'Public', + description: 'Anyone with the link can view', + icon: , + }, +]; + +interface ProjectSettingsPageProps { + params: Promise<{ projectId: string }>; +} + +export default function ProjectSettingsPage({ params }: ProjectSettingsPageProps) { + const router = useRouter(); + const [projectId, setProjectId] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + const [deleteConfirmation, setDeleteConfirmation] = useState(''); + const [formData, setFormData] = useState({ + name: '', + description: '', + visibility: 'PRIVATE' as Visibility, + }); + + useEffect(() => { + params.then(({ projectId: id }) => { + setProjectId(id); + fetch(`/api/projects/${id}`) + .then((res) => res.json()) + .then((data) => { + if (data.error) { + setError(data.error); + } else { + setFormData({ + name: data.name || '', + description: data.description || '', + visibility: data.visibility || 'PRIVATE', + }); + } + }) + .catch(() => setError('Failed to load project')) + .finally(() => setIsLoading(false)); + }); + }, [params]); + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSaving(true); + setError(''); + setSuccess(''); + + try { + const response = await fetch(`/api/projects/${projectId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData), + }); + + const data = await response.json(); + + if (!response.ok) { + setError(data.error || 'Failed to update project'); + return; + } + + setSuccess('Project settings saved successfully'); + setTimeout(() => setSuccess(''), 3000); + } catch { + setError('Something went wrong. Please try again.'); + } finally { + setIsSaving(false); + } + }; + + const handleDelete = async () => { + if (deleteConfirmation !== formData.name) { + setError('Project name does not match'); + return; + } + + setIsDeleting(true); + setError(''); + + try { + const response = await fetch(`/api/projects/${projectId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const data = await response.json(); + setError(data.error || 'Failed to delete project'); + return; + } + + router.push('/dashboard'); + } catch { + setError('Something went wrong. Please try again.'); + } finally { + setIsDeleting(false); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+ + + Back to Project + +
+ +
+ {/* General Settings */} + + +
+ +
+ Project Settings + + Update your project details and access settings + +
+ +
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + required + disabled={isSaving} + className="h-11" + /> +
+ +
+ +