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 (
- {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}
)}
-
+
-
-
- Share
-
-
-
- Settings
-
-
-
-
- Add Video
+
+
+
+ Share
+ {(isOwner || project.members[0]?.role === 'ADMIN') && (
+
+
+
+ Settings
+
+
+ )}
+ {canEdit && (
+
+
+
+ Add Video
+
+
+ )}
{/* 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
-
-
-
- Add Video
-
-
+ {canEdit && (
+
+
+
+ Add Video
+
+
+ )}
)}
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
+
+
+
+
+
+
+
+ {/* Danger Zone */}
+
+
+
+
+ Danger Zone
+
+
+ Irreversible actions that will permanently affect your project
+
+
+
+
+
+
Delete this project
+
+ This action cannot be undone
+
+
+
+
+
+
+ Delete
+
+
+
+
+ Delete "{formData.name}"?
+
+
+
+ This will permanently delete this project and all of its
+ videos, versions, and comments. This action cannot be undone.
+
+
+
+ Type {formData.name} to confirm
+
+ setDeleteConfirmation(e.target.value)}
+ placeholder="Project name"
+ className="h-11"
+ />
+
+
+
+
+
+ setDeleteConfirmation('')}>
+ Cancel
+
+
+ {isDeleting && }
+ Delete Project
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(dashboard)/projects/[projectId]/share/page.tsx b/app/(dashboard)/projects/[projectId]/share/page.tsx
new file mode 100644
index 0000000..c17e782
--- /dev/null
+++ b/app/(dashboard)/projects/[projectId]/share/page.tsx
@@ -0,0 +1,336 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { ArrowLeft, Copy, Check, Loader2, UserPlus, Trash2, Share2, Globe, Lock, Mail, X } 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 { Badge } from '@/components/ui/badge';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+
+interface ProjectMember {
+ id: string;
+ role: string;
+ user: {
+ id: string;
+ name: string | null;
+ email: string | null;
+ };
+}
+
+interface ProjectSharePageProps {
+ params: Promise<{ projectId: string }>;
+}
+
+export default function ProjectSharePage({ params }: ProjectSharePageProps) {
+ const [projectId, setProjectId] = useState('');
+ const [projectName, setProjectName] = useState('');
+ const [projectVisibility, setProjectVisibility] = useState('');
+ const [isLoading, setIsLoading] = useState(true);
+ const [members, setMembers] = useState([]);
+ const [copied, setCopied] = useState(false);
+ const [error, setError] = useState('');
+ const [inviteEmail, setInviteEmail] = useState('');
+ const [isInviting, setIsInviting] = useState(false);
+ const [inviteSuccess, setInviteSuccess] = useState('');
+
+ useEffect(() => {
+ params.then(({ projectId: id }) => {
+ setProjectId(id);
+ fetch(`/api/projects/${id}`)
+ .then((res) => res.json())
+ .then((data) => {
+ if (data.error) {
+ setError(data.error);
+ } else {
+ setProjectName(data.name || '');
+ setProjectVisibility(data.visibility || 'PRIVATE');
+ setMembers(data.members || []);
+ }
+ })
+ .catch(() => setError('Failed to load project'))
+ .finally(() => setIsLoading(false));
+ });
+ }, [params]);
+
+ const copyToClipboard = async (text: string) => {
+ await navigator.clipboard.writeText(text);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ const getDirectLink = () => {
+ if (typeof window !== 'undefined') {
+ return `${window.location.origin}/projects/${projectId}`;
+ }
+ return `/projects/${projectId}`;
+ };
+
+ const handleInvite = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!inviteEmail.trim()) return;
+
+ setIsInviting(true);
+ setError('');
+ setInviteSuccess('');
+
+ try {
+ // TODO: Implement invite API
+ await new Promise(resolve => setTimeout(resolve, 500));
+ setInviteSuccess(`Invitation sent to ${inviteEmail}`);
+ setInviteEmail('');
+ setTimeout(() => setInviteSuccess(''), 3000);
+ } catch {
+ setError('Failed to send invitation');
+ } finally {
+ setIsInviting(false);
+ }
+ };
+
+ const VisibilityIcon = () => {
+ switch (projectVisibility) {
+ case 'PUBLIC':
+ return ;
+ case 'INVITE':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getVisibilityColor = () => {
+ switch (projectVisibility) {
+ case 'PUBLIC':
+ return 'bg-green-500/10 text-green-500';
+ case 'INVITE':
+ return 'bg-blue-500/10 text-blue-500';
+ default:
+ return 'bg-orange-500/10 text-orange-500';
+ }
+ };
+
+ const getVisibilityLabel = () => {
+ switch (projectVisibility) {
+ case 'PUBLIC':
+ return { title: 'Public', description: 'Anyone with the link can view this project' };
+ case 'INVITE':
+ return { title: 'Invite Only', description: 'Only people you invite can access' };
+ default:
+ return { title: 'Private', description: 'Only you can access this project' };
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ const visibilityInfo = getVisibilityLabel();
+
+ return (
+
+
+
+
+
+ Back to Project
+
+
+
+
+ {/* Header Card */}
+
+
+
+
+
+ Share Project
+
+ Share "{projectName}" with your team or clients
+
+
+
+ {/* Visibility Status */}
+
+
+
+
+
+
{visibilityInfo.title}
+
{visibilityInfo.description}
+
+
+
+ Change
+
+
+
+
+
+
+ {/* Invite People - Only show for INVITE visibility */}
+ {projectVisibility === 'INVITE' && (
+
+
+
+
+ Invite People
+
+
+ Send email invitations to specific people
+
+
+
+
+ setInviteEmail(e.target.value)}
+ placeholder="email@example.com"
+ className="h-11 flex-1"
+ disabled={isInviting}
+ />
+
+ {isInviting ? (
+
+ ) : (
+ <>
+
+ Invite
+ >
+ )}
+
+
+
+ {inviteSuccess && (
+
+ {inviteSuccess}
+
+ )}
+
+ {/* Current Members */}
+ {members.length > 0 && (
+
+
Project Members
+
+ {members.map((member) => (
+
+
+
+
+ {member.user.name?.charAt(0) || member.user.email?.charAt(0) || '?'}
+
+
+
+
+ {member.user.name || 'Unknown'}
+
+
+ {member.user.email}
+
+
+
+
+
+ {member.role.toLowerCase()}
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {members.length === 0 && (
+
+
+
No members yet
+
Invite people to collaborate on this project
+
+ )}
+
+
+ )}
+
+ {/* Public Link - Only show for PUBLIC visibility */}
+ {projectVisibility === 'PUBLIC' && (
+
+
+
+
+ Public Link
+
+
+ Share this link with anyone
+
+
+
+
+
+ copyToClipboard(getDirectLink())}
+ >
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ )}
+
+ {/* Private notice */}
+ {projectVisibility === 'PRIVATE' && (
+
+
+
+
+
+
+
This project is private
+
+ Only you can access this project. Change visibility to share with others.
+
+
+
+ Change Visibility
+
+
+
+
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ );
+}
diff --git a/app/(dashboard)/projects/new/page.tsx b/app/(dashboard)/projects/new/page.tsx
index 0aacb7e..e2416bd 100644
--- a/app/(dashboard)/projects/new/page.tsx
+++ b/app/(dashboard)/projects/new/page.tsx
@@ -3,102 +3,196 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
-import { ArrowLeft, Loader2 } from 'lucide-react';
+import { ArrowLeft, Loader2, Globe, Lock, UserPlus, FolderPlus } 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';
+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: ,
+ },
+];
+
export default function NewProjectPage() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState('');
const [formData, setFormData] = useState({
name: '',
description: '',
+ visibility: 'PRIVATE' as Visibility,
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
+ setError('');
try {
- // TODO: Implement actual project creation
- // const response = await fetch('/api/projects', {
- // method: 'POST',
- // headers: { 'Content-Type': 'application/json' },
- // body: JSON.stringify(formData),
- // });
-
- // Simulate API call for now
- await new Promise(resolve => setTimeout(resolve, 500));
-
- // Redirect to dashboard on success
- router.push('/dashboard');
- } catch (error) {
- console.error('Failed to create project:', error);
+ const response = await fetch('/api/projects', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(formData),
+ });
+
+ const data = await response.json();
+
+ if (!response.ok) {
+ setError(data.error || 'Failed to create project');
+ return;
+ }
+
+ router.push(`/projects/${data.id}`);
+ } catch {
+ setError('Something went wrong. Please try again.');
} finally {
setIsLoading(false);
}
};
return (
-
-
-
-
- Back to Projects
-
+
+
+
+
+
+ Back to Projects
+
+
+
+
+
+
+
+
+ Create New Project
+
+ Set up a new project to organize your videos and collect feedback
+
+
+
+
+
+
+ Project Name
+
+ setFormData(prev => ({ ...prev, name: e.target.value }))}
+ required
+ disabled={isLoading}
+ className="h-11"
+ />
+
+
+
+
+ Description
+ (optional)
+
+ setFormData(prev => ({ ...prev, description: e.target.value }))}
+ rows={3}
+ disabled={isLoading}
+ className="resize-none"
+ />
+
+
+
+
Who can access?
+
+ {visibilityOptions.map((option) => (
+
setFormData(prev => ({ ...prev, visibility: option.value }))}
+ disabled={isLoading}
+ className={`w-full flex items-center gap-4 p-4 rounded-xl border-2 text-left transition-all ${formData.visibility === option.value
+ ? 'border-primary bg-primary/5 ring-1 ring-primary/20'
+ : 'border-border hover:border-border/80 hover:bg-accent/50'
+ }`}
+ >
+
+ {option.icon}
+
+
+
{option.label}
+
+ {option.description}
+
+
+
+ {formData.visibility === option.value && (
+
+ )}
+
+
+ ))}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ {isLoading && }
+ Create Project
+
+ router.back()}
+ className="h-11 px-6"
+ >
+ Cancel
+
+
+
+
+
-
-
-
- Create New Project
-
- Set up a new project to organize your videos and collect feedback
-
-
-
-
-
- Project Name
- setFormData(prev => ({ ...prev, name: e.target.value }))}
- required
- disabled={isLoading}
- />
-
-
-
- Description (optional)
- setFormData(prev => ({ ...prev, description: e.target.value }))}
- rows={3}
- disabled={isLoading}
- />
-
-
-
-
- {isLoading && }
- Create Project
-
- router.back()}>
- Cancel
-
-
-
-
-
);
}
diff --git a/components/ui/radio-group.tsx b/components/ui/radio-group.tsx
new file mode 100644
index 0000000..f51d506
--- /dev/null
+++ b/components/ui/radio-group.tsx
@@ -0,0 +1,45 @@
+"use client"
+
+import * as React from "react"
+import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+import { CircleIcon } from "lucide-react"
+
+function RadioGroup({
+ className,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+ )
+}
+
+function RadioGroupItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { RadioGroup, RadioGroupItem }
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index df3e0ea..26d5e69 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -104,9 +104,9 @@ model Project {
}
enum ProjectVisibility {
- PRIVATE // Only owner and members can access
- LINK_ONLY // Anyone with link can view
- PUBLIC // Listed publicly
+ PRIVATE // Only owner can access
+ INVITE // Owner + specifically invited members
+ PUBLIC // Anyone with the link can access
}
model ProjectMember {
diff --git a/prisma/seed.ts b/prisma/seed.ts
index 7fc0115..2dc401f 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -79,7 +79,7 @@ async function main() {
name: 'Programming Tutorials',
description: 'In-depth programming tutorials covering modern web development.',
slug: 'programming-tutorials',
- visibility: ProjectVisibility.LINK_ONLY,
+ visibility: ProjectVisibility.INVITE,
ownerId: demoUser.id,
},
});