mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat: Implement new project visibility types, add dedicated project settings and share pages, and enhance the dashboard with dynamic project data and visibility badges.
This commit is contained in:
@@ -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 <Globe className="h-3.5 w-3.5" />;
|
||||
case 'INVITE':
|
||||
return <UserPlus className="h-3.5 w-3.5" />;
|
||||
default:
|
||||
return <Lock className="h-3.5 w-3.5" />;
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="px-6 lg:px-8 py-8 w-full">
|
||||
{/* Back link */}
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4 mr-1" />
|
||||
@@ -81,7 +129,8 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h1 className="text-3xl font-bold tracking-tight">{project.name}</h1>
|
||||
<Badge variant="outline" className="capitalize">
|
||||
<Badge variant="outline" className="flex items-center gap-1">
|
||||
<VisibilityIcon visibility={project.visibility} />
|
||||
{project.visibility.toLowerCase()}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -89,29 +138,37 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
||||
<p className="text-muted-foreground">{project.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Share2 className="h-4 w-4 mr-2" />
|
||||
Share
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Settings
|
||||
</Button>
|
||||
<Button size="sm" asChild>
|
||||
<Link href={`/projects/${projectId}/videos/new`}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Video
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/projects/${projectId}/share`}>
|
||||
<Share2 className="h-4 w-4 mr-2" />
|
||||
Share
|
||||
</Link>
|
||||
</Button>
|
||||
{(isOwner || project.members[0]?.role === 'ADMIN') && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href={`/projects/${projectId}/settings`}>
|
||||
<Settings className="h-4 w-4 mr-2" />
|
||||
Settings
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{canEdit && (
|
||||
<Button size="sm" asChild>
|
||||
<Link href={`/projects/${projectId}/videos/new`}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Video
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Videos Grid */}
|
||||
{project.videos.length > 0 ? (
|
||||
{videos.length > 0 ? (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{project.videos.map((video) => (
|
||||
{videos.map((video) => (
|
||||
<VideoCard key={video.id} video={video} projectId={projectId} />
|
||||
))}
|
||||
</div>
|
||||
@@ -123,12 +180,14 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
|
||||
<p className="text-muted-foreground text-center mb-4">
|
||||
Add your first video to start collecting feedback
|
||||
</p>
|
||||
<Button asChild>
|
||||
<Link href={`/projects/${projectId}/videos/new`}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Video
|
||||
</Link>
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button asChild>
|
||||
<Link href={`/projects/${projectId}/videos/new`}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Video
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user