301 lines
9.7 KiB
TypeScript
301 lines
9.7 KiB
TypeScript
|
|
import { Suspense } from 'react'
|
||
|
|
import Link from 'next/link'
|
||
|
|
import { prisma } from '@/lib/prisma'
|
||
|
|
|
||
|
|
export const dynamic = 'force-dynamic'
|
||
|
|
import {
|
||
|
|
Card,
|
||
|
|
CardContent,
|
||
|
|
CardDescription,
|
||
|
|
CardHeader,
|
||
|
|
CardTitle,
|
||
|
|
} from '@/components/ui/card'
|
||
|
|
import { Badge } from '@/components/ui/badge'
|
||
|
|
import { Button } from '@/components/ui/button'
|
||
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
||
|
|
import {
|
||
|
|
Table,
|
||
|
|
TableBody,
|
||
|
|
TableCell,
|
||
|
|
TableHead,
|
||
|
|
TableHeader,
|
||
|
|
TableRow,
|
||
|
|
} from '@/components/ui/table'
|
||
|
|
import {
|
||
|
|
DropdownMenu,
|
||
|
|
DropdownMenuContent,
|
||
|
|
DropdownMenuItem,
|
||
|
|
DropdownMenuTrigger,
|
||
|
|
} from '@/components/ui/dropdown-menu'
|
||
|
|
import {
|
||
|
|
Plus,
|
||
|
|
MoreHorizontal,
|
||
|
|
ClipboardList,
|
||
|
|
Eye,
|
||
|
|
Pencil,
|
||
|
|
FileUp,
|
||
|
|
Users,
|
||
|
|
} from 'lucide-react'
|
||
|
|
import { formatDateOnly, truncate } from '@/lib/utils'
|
||
|
|
import { ProjectLogo } from '@/components/shared/project-logo'
|
||
|
|
|
||
|
|
async function ProjectsContent() {
|
||
|
|
const projects = await prisma.project.findMany({
|
||
|
|
// Note: PROGRAM_ADMIN filtering should be handled via middleware or a separate relation
|
||
|
|
select: {
|
||
|
|
id: true,
|
||
|
|
title: true,
|
||
|
|
teamName: true,
|
||
|
|
status: true,
|
||
|
|
logoKey: true,
|
||
|
|
createdAt: true,
|
||
|
|
round: {
|
||
|
|
select: {
|
||
|
|
id: true,
|
||
|
|
name: true,
|
||
|
|
status: true,
|
||
|
|
program: {
|
||
|
|
select: {
|
||
|
|
name: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
_count: {
|
||
|
|
select: {
|
||
|
|
assignments: true,
|
||
|
|
files: true,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
orderBy: { createdAt: 'desc' },
|
||
|
|
take: 100,
|
||
|
|
})
|
||
|
|
|
||
|
|
if (projects.length === 0) {
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
||
|
|
<ClipboardList className="h-12 w-12 text-muted-foreground/50" />
|
||
|
|
<p className="mt-2 font-medium">No projects yet</p>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
Import projects via CSV or create them manually
|
||
|
|
</p>
|
||
|
|
<div className="mt-4 flex gap-2">
|
||
|
|
<Button asChild>
|
||
|
|
<Link href="/admin/projects/import">
|
||
|
|
<FileUp className="mr-2 h-4 w-4" />
|
||
|
|
Import CSV
|
||
|
|
</Link>
|
||
|
|
</Button>
|
||
|
|
<Button variant="outline" asChild>
|
||
|
|
<Link href="/admin/projects/new">
|
||
|
|
<Plus className="mr-2 h-4 w-4" />
|
||
|
|
Add Project
|
||
|
|
</Link>
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const statusColors: Record<string, 'default' | 'success' | 'secondary' | 'destructive' | 'warning'> = {
|
||
|
|
SUBMITTED: 'secondary',
|
||
|
|
UNDER_REVIEW: 'default',
|
||
|
|
SHORTLISTED: 'success',
|
||
|
|
FINALIST: 'success',
|
||
|
|
WINNER: 'success',
|
||
|
|
REJECTED: 'destructive',
|
||
|
|
WITHDRAWN: 'secondary',
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
{/* Desktop table view */}
|
||
|
|
<Card className="hidden md:block">
|
||
|
|
<Table>
|
||
|
|
<TableHeader>
|
||
|
|
<TableRow>
|
||
|
|
<TableHead>Project</TableHead>
|
||
|
|
<TableHead>Round</TableHead>
|
||
|
|
<TableHead>Files</TableHead>
|
||
|
|
<TableHead>Assignments</TableHead>
|
||
|
|
<TableHead>Status</TableHead>
|
||
|
|
<TableHead className="text-right">Actions</TableHead>
|
||
|
|
</TableRow>
|
||
|
|
</TableHeader>
|
||
|
|
<TableBody>
|
||
|
|
{projects.map((project) => (
|
||
|
|
<TableRow key={project.id} className="group relative cursor-pointer hover:bg-muted/50">
|
||
|
|
<TableCell>
|
||
|
|
<Link href={`/admin/projects/${project.id}`} className="flex items-center gap-3 after:absolute after:inset-0 after:content-['']">
|
||
|
|
<ProjectLogo
|
||
|
|
project={project}
|
||
|
|
size="sm"
|
||
|
|
fallback="initials"
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<p className="font-medium hover:text-primary">
|
||
|
|
{truncate(project.title, 40)}
|
||
|
|
</p>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
{project.teamName}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</Link>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<div>
|
||
|
|
<p>{project.round.name}</p>
|
||
|
|
<p className="text-sm text-muted-foreground">
|
||
|
|
{project.round.program.name}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>{project._count.files}</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<div className="flex items-center gap-1">
|
||
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||
|
|
{project._count.assignments}
|
||
|
|
</div>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell>
|
||
|
|
<Badge variant={statusColors[project.status] || 'secondary'}>
|
||
|
|
{project.status.replace('_', ' ')}
|
||
|
|
</Badge>
|
||
|
|
</TableCell>
|
||
|
|
<TableCell className="relative z-10 text-right">
|
||
|
|
<DropdownMenu>
|
||
|
|
<DropdownMenuTrigger asChild>
|
||
|
|
<Button variant="ghost" size="icon">
|
||
|
|
<MoreHorizontal className="h-4 w-4" />
|
||
|
|
<span className="sr-only">Actions</span>
|
||
|
|
</Button>
|
||
|
|
</DropdownMenuTrigger>
|
||
|
|
<DropdownMenuContent align="end">
|
||
|
|
<DropdownMenuItem asChild>
|
||
|
|
<Link href={`/admin/projects/${project.id}`}>
|
||
|
|
<Eye className="mr-2 h-4 w-4" />
|
||
|
|
View Details
|
||
|
|
</Link>
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem asChild>
|
||
|
|
<Link href={`/admin/projects/${project.id}/edit`}>
|
||
|
|
<Pencil className="mr-2 h-4 w-4" />
|
||
|
|
Edit
|
||
|
|
</Link>
|
||
|
|
</DropdownMenuItem>
|
||
|
|
<DropdownMenuItem asChild>
|
||
|
|
<Link href={`/admin/projects/${project.id}/assignments`}>
|
||
|
|
<Users className="mr-2 h-4 w-4" />
|
||
|
|
Manage Assignments
|
||
|
|
</Link>
|
||
|
|
</DropdownMenuItem>
|
||
|
|
</DropdownMenuContent>
|
||
|
|
</DropdownMenu>
|
||
|
|
</TableCell>
|
||
|
|
</TableRow>
|
||
|
|
))}
|
||
|
|
</TableBody>
|
||
|
|
</Table>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Mobile card view */}
|
||
|
|
<div className="space-y-4 md:hidden">
|
||
|
|
{projects.map((project) => (
|
||
|
|
<Link key={project.id} href={`/admin/projects/${project.id}`} className="block">
|
||
|
|
<Card className="transition-colors hover:bg-muted/50">
|
||
|
|
<CardHeader className="pb-3">
|
||
|
|
<div className="flex items-start gap-3">
|
||
|
|
<ProjectLogo
|
||
|
|
project={project}
|
||
|
|
size="md"
|
||
|
|
fallback="initials"
|
||
|
|
/>
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="flex items-start justify-between gap-2">
|
||
|
|
<CardTitle className="text-base line-clamp-2">
|
||
|
|
{project.title}
|
||
|
|
</CardTitle>
|
||
|
|
<Badge variant={statusColors[project.status] || 'secondary'} className="shrink-0">
|
||
|
|
{project.status.replace('_', ' ')}
|
||
|
|
</Badge>
|
||
|
|
</div>
|
||
|
|
<CardDescription>{project.teamName}</CardDescription>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-3">
|
||
|
|
<div className="flex items-center justify-between text-sm">
|
||
|
|
<span className="text-muted-foreground">Round</span>
|
||
|
|
<span>{project.round.name}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center justify-between text-sm">
|
||
|
|
<span className="text-muted-foreground">Assignments</span>
|
||
|
|
<span>{project._count.assignments} jurors</span>
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
</Link>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
function ProjectsSkeleton() {
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardContent className="p-6">
|
||
|
|
<div className="space-y-4">
|
||
|
|
{[...Array(5)].map((_, i) => (
|
||
|
|
<div key={i} className="flex items-center justify-between">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Skeleton className="h-5 w-64" />
|
||
|
|
<Skeleton className="h-4 w-32" />
|
||
|
|
</div>
|
||
|
|
<Skeleton className="h-9 w-9" />
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function ProjectsPage() {
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h1 className="text-2xl font-semibold tracking-tight">Projects</h1>
|
||
|
|
<p className="text-muted-foreground">
|
||
|
|
Manage submitted projects across all rounds
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Button variant="outline" asChild>
|
||
|
|
<Link href="/admin/projects/import">
|
||
|
|
<FileUp className="mr-2 h-4 w-4" />
|
||
|
|
Import
|
||
|
|
</Link>
|
||
|
|
</Button>
|
||
|
|
<Button asChild>
|
||
|
|
<Link href="/admin/projects/new">
|
||
|
|
<Plus className="mr-2 h-4 w-4" />
|
||
|
|
Add Project
|
||
|
|
</Link>
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Content */}
|
||
|
|
<Suspense fallback={<ProjectsSkeleton />}>
|
||
|
|
<ProjectsContent />
|
||
|
|
</Suspense>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|