2026-01-30 13:41:32 +01:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { Suspense, use, useState, useEffect, useCallback } from 'react'
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import { useRouter } from 'next/navigation'
|
|
|
|
|
import { useForm } from 'react-hook-form'
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from '@/components/ui/card'
|
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
|
import { Input } from '@/components/ui/input'
|
|
|
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
|
|
|
import { Badge } from '@/components/ui/badge'
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select'
|
|
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
FormControl,
|
|
|
|
|
FormDescription,
|
|
|
|
|
FormField,
|
|
|
|
|
FormItem,
|
|
|
|
|
FormLabel,
|
|
|
|
|
FormMessage,
|
|
|
|
|
} from '@/components/ui/form'
|
|
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
AlertDialogTrigger,
|
|
|
|
|
} from '@/components/ui/alert-dialog'
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableCell,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableRow,
|
|
|
|
|
} from '@/components/ui/table'
|
|
|
|
|
import { FileUpload } from '@/components/shared/file-upload'
|
|
|
|
|
import { ProjectLogo } from '@/components/shared/project-logo'
|
|
|
|
|
import { LogoUpload } from '@/components/shared/logo-upload'
|
|
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
Loader2,
|
|
|
|
|
AlertCircle,
|
|
|
|
|
Trash2,
|
|
|
|
|
X,
|
|
|
|
|
Plus,
|
|
|
|
|
FileText,
|
|
|
|
|
Film,
|
|
|
|
|
Presentation,
|
|
|
|
|
FileIcon,
|
|
|
|
|
} from 'lucide-react'
|
|
|
|
|
import { formatFileSize } from '@/lib/utils'
|
|
|
|
|
|
|
|
|
|
interface PageProps {
|
|
|
|
|
params: Promise<{ id: string }>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateProjectSchema = z.object({
|
|
|
|
|
title: z.string().min(1, 'Title is required').max(500),
|
|
|
|
|
teamName: z.string().optional(),
|
|
|
|
|
description: z.string().optional(),
|
|
|
|
|
status: z.enum([
|
|
|
|
|
'SUBMITTED',
|
|
|
|
|
'ELIGIBLE',
|
|
|
|
|
'ASSIGNED',
|
|
|
|
|
'SEMIFINALIST',
|
|
|
|
|
'FINALIST',
|
|
|
|
|
'REJECTED',
|
|
|
|
|
]),
|
|
|
|
|
tags: z.array(z.string()),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
type UpdateProjectForm = z.infer<typeof updateProjectSchema>
|
|
|
|
|
|
|
|
|
|
// File type icons
|
|
|
|
|
const fileTypeIcons: Record<string, React.ReactNode> = {
|
|
|
|
|
EXEC_SUMMARY: <FileText className="h-4 w-4" />,
|
|
|
|
|
PRESENTATION: <Presentation className="h-4 w-4" />,
|
|
|
|
|
VIDEO: <Film className="h-4 w-4" />,
|
|
|
|
|
OTHER: <FileIcon className="h-4 w-4" />,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EditProjectContent({ projectId }: { projectId: string }) {
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const [tagInput, setTagInput] = useState('')
|
|
|
|
|
|
|
|
|
|
// Fetch project data
|
|
|
|
|
const { data: project, isLoading } = trpc.project.get.useQuery({
|
|
|
|
|
id: projectId,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Fetch files
|
|
|
|
|
const { data: files, refetch: refetchFiles } = trpc.file.listByProject.useQuery({
|
|
|
|
|
projectId,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Fetch logo URL
|
|
|
|
|
const { data: logoUrl, refetch: refetchLogo } = trpc.logo.getUrl.useQuery({
|
|
|
|
|
projectId,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Fetch existing tags for suggestions
|
|
|
|
|
const { data: existingTags } = trpc.project.getTags.useQuery({
|
2026-02-02 22:33:55 +01:00
|
|
|
roundId: project?.roundProjects?.[0]?.round?.id,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Mutations
|
|
|
|
|
const updateProject = trpc.project.update.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
router.push(`/admin/projects/${projectId}`)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const deleteProject = trpc.project.delete.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
router.push('/admin/projects')
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const deleteFile = trpc.file.delete.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
refetchFiles()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Initialize form
|
|
|
|
|
const form = useForm<UpdateProjectForm>({
|
|
|
|
|
resolver: zodResolver(updateProjectSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
title: '',
|
|
|
|
|
teamName: '',
|
|
|
|
|
description: '',
|
|
|
|
|
status: 'SUBMITTED',
|
|
|
|
|
tags: [],
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Update form when project loads
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (project) {
|
|
|
|
|
form.reset({
|
|
|
|
|
title: project.title,
|
|
|
|
|
teamName: project.teamName || '',
|
|
|
|
|
description: project.description || '',
|
2026-02-02 22:33:55 +01:00
|
|
|
status: (project.roundProjects?.[0]?.status ?? 'SUBMITTED') as UpdateProjectForm['status'],
|
2026-01-30 13:41:32 +01:00
|
|
|
tags: project.tags || [],
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}, [project, form])
|
|
|
|
|
|
|
|
|
|
const tags = form.watch('tags')
|
|
|
|
|
|
|
|
|
|
// Add tag
|
|
|
|
|
const addTag = useCallback(() => {
|
|
|
|
|
const tag = tagInput.trim()
|
|
|
|
|
if (tag && !tags.includes(tag)) {
|
|
|
|
|
form.setValue('tags', [...tags, tag])
|
|
|
|
|
setTagInput('')
|
|
|
|
|
}
|
|
|
|
|
}, [tagInput, tags, form])
|
|
|
|
|
|
|
|
|
|
// Remove tag
|
|
|
|
|
const removeTag = useCallback(
|
|
|
|
|
(tag: string) => {
|
|
|
|
|
form.setValue(
|
|
|
|
|
'tags',
|
|
|
|
|
tags.filter((t) => t !== tag)
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
[tags, form]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data: UpdateProjectForm) => {
|
|
|
|
|
await updateProject.mutateAsync({
|
|
|
|
|
id: projectId,
|
|
|
|
|
title: data.title,
|
|
|
|
|
teamName: data.teamName || null,
|
|
|
|
|
description: data.description || null,
|
|
|
|
|
status: data.status,
|
2026-02-02 22:33:55 +01:00
|
|
|
roundId: project?.roundProjects?.[0]?.round?.id,
|
2026-01-30 13:41:32 +01:00
|
|
|
tags: data.tags,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDelete = async () => {
|
|
|
|
|
await deleteProject.mutateAsync({ id: projectId })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeleteFile = async (fileId: string) => {
|
|
|
|
|
await deleteFile.mutateAsync({ id: fileId })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return <EditProjectSkeleton />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!project) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Button variant="ghost" asChild className="-ml-4">
|
|
|
|
|
<Link href="/admin/projects">
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
Back to Projects
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
|
|
|
<AlertCircle className="h-12 w-12 text-destructive/50" />
|
|
|
|
|
<p className="mt-2 font-medium">Project Not Found</p>
|
|
|
|
|
<Button asChild className="mt-4">
|
|
|
|
|
<Link href="/admin/projects">Back to Projects</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isPending = updateProject.isPending || deleteProject.isPending
|
|
|
|
|
|
|
|
|
|
// Filter tag suggestions (exclude already selected)
|
|
|
|
|
const tagSuggestions =
|
|
|
|
|
existingTags?.filter((t) => !tags.includes(t)).slice(0, 5) || []
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Button variant="ghost" asChild className="-ml-4">
|
|
|
|
|
<Link href={`/admin/projects/${projectId}`}>
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
Back to Project
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-semibold tracking-tight">Edit Project</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Update project information and manage files
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Form */}
|
|
|
|
|
<Form {...form}>
|
|
|
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
|
|
|
{/* Basic Information */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg">Basic Information</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
{/* Project Logo */}
|
|
|
|
|
<div className="flex items-start gap-4 pb-4 border-b">
|
|
|
|
|
<ProjectLogo
|
|
|
|
|
project={project}
|
|
|
|
|
logoUrl={logoUrl}
|
|
|
|
|
size="lg"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex-1 space-y-1">
|
|
|
|
|
<FormLabel>Project Logo</FormLabel>
|
|
|
|
|
<FormDescription>
|
|
|
|
|
Upload a logo for this project. It will be displayed in project lists and cards.
|
|
|
|
|
</FormDescription>
|
|
|
|
|
<div className="pt-2">
|
|
|
|
|
<LogoUpload
|
|
|
|
|
project={project}
|
|
|
|
|
currentLogoUrl={logoUrl}
|
|
|
|
|
onUploadComplete={() => refetchLogo()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="title"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Title</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input placeholder="Project title" {...field} />
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="teamName"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Team Name</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Team or organization name"
|
|
|
|
|
{...field}
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="description"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Description</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Textarea
|
|
|
|
|
placeholder="Project description..."
|
|
|
|
|
rows={4}
|
|
|
|
|
maxLength={2000}
|
|
|
|
|
{...field}
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="status"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Status</FormLabel>
|
|
|
|
|
<Select
|
|
|
|
|
onValueChange={field.onChange}
|
|
|
|
|
defaultValue={field.value}
|
|
|
|
|
value={field.value}
|
|
|
|
|
>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="SUBMITTED">Submitted</SelectItem>
|
|
|
|
|
<SelectItem value="ELIGIBLE">Eligible</SelectItem>
|
|
|
|
|
<SelectItem value="ASSIGNED">Assigned</SelectItem>
|
|
|
|
|
<SelectItem value="SEMIFINALIST">Semifinalist</SelectItem>
|
|
|
|
|
<SelectItem value="FINALIST">Finalist</SelectItem>
|
|
|
|
|
<SelectItem value="REJECTED">Rejected</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Tags */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg">Tags</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Add tags to categorize this project
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
value={tagInput}
|
|
|
|
|
onChange={(e) => setTagInput(e.target.value)}
|
|
|
|
|
placeholder="Add a tag..."
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
addTag()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<Button type="button" variant="outline" onClick={addTag}>
|
|
|
|
|
<Plus className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{tagSuggestions.length > 0 && tagInput && (
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
Suggestions:
|
|
|
|
|
</span>
|
|
|
|
|
{tagSuggestions
|
|
|
|
|
.filter((t) =>
|
|
|
|
|
t.toLowerCase().includes(tagInput.toLowerCase())
|
|
|
|
|
)
|
|
|
|
|
.map((tag) => (
|
|
|
|
|
<Badge
|
|
|
|
|
key={tag}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="cursor-pointer hover:bg-muted"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (!tags.includes(tag)) {
|
|
|
|
|
form.setValue('tags', [...tags, tag])
|
|
|
|
|
}
|
|
|
|
|
setTagInput('')
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{tag}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{tags.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
{tags.map((tag) => (
|
|
|
|
|
<Badge key={tag} variant="secondary" className="gap-1">
|
|
|
|
|
{tag}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => removeTag(tag)}
|
|
|
|
|
className="ml-1 hover:text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Files */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg">Files</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Manage project documents and materials
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
{files && files.length > 0 ? (
|
|
|
|
|
<Table>
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableHead>File</TableHead>
|
|
|
|
|
<TableHead>Type</TableHead>
|
|
|
|
|
<TableHead>Size</TableHead>
|
|
|
|
|
<TableHead className="w-[50px]"></TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
<TableBody>
|
|
|
|
|
{files.map((file) => (
|
|
|
|
|
<TableRow key={file.id}>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{fileTypeIcons[file.fileType] || (
|
|
|
|
|
<FileIcon className="h-4 w-4" />
|
|
|
|
|
)}
|
|
|
|
|
<span className="text-sm truncate max-w-[200px]">
|
|
|
|
|
{file.fileName}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<Badge variant="outline" className="text-xs">
|
|
|
|
|
{file.fileType.replace('_', ' ')}
|
|
|
|
|
</Badge>
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell className="text-sm text-muted-foreground">
|
|
|
|
|
{formatFileSize(file.size)}
|
|
|
|
|
</TableCell>
|
|
|
|
|
<TableCell>
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Delete file?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
Are you sure you want to delete "{file.fileName}"?
|
|
|
|
|
This action cannot be undone.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={() => handleDeleteFile(file.id)}
|
|
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
No files uploaded yet
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="pt-4 border-t">
|
|
|
|
|
<p className="text-sm font-medium mb-3">Upload New Files</p>
|
|
|
|
|
<FileUpload
|
|
|
|
|
projectId={projectId}
|
|
|
|
|
onUploadComplete={() => refetchFiles()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Error Display */}
|
|
|
|
|
{updateProject.error && (
|
|
|
|
|
<Card className="border-destructive">
|
|
|
|
|
<CardContent className="flex items-center gap-2 py-4">
|
|
|
|
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
|
|
|
|
<p className="text-sm text-destructive">
|
|
|
|
|
{updateProject.error.message}
|
|
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
|
|
|
<div className="flex justify-end gap-3">
|
|
|
|
|
<Button type="button" variant="outline" asChild>
|
|
|
|
|
<Link href={`/admin/projects/${projectId}`}>Cancel</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={isPending}>
|
|
|
|
|
{updateProject.isPending && (
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
)}
|
|
|
|
|
Save Changes
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</Form>
|
|
|
|
|
|
|
|
|
|
{/* Danger Zone */}
|
|
|
|
|
<Card className="border-destructive/50">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg text-destructive">Danger Zone</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Irreversible actions that will permanently affect this project
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<Button variant="destructive" disabled={deleteProject.isPending}>
|
|
|
|
|
{deleteProject.isPending ? (
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
|
|
|
)}
|
|
|
|
|
Delete Project
|
|
|
|
|
</Button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Delete project?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
This will permanently delete "{project.title}" and all
|
|
|
|
|
associated files, assignments, and evaluations. This action
|
|
|
|
|
cannot be undone.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={handleDelete}
|
|
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
|
|
|
>
|
|
|
|
|
Delete Project
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
|
|
|
|
|
{deleteProject.error && (
|
|
|
|
|
<p className="mt-2 text-sm text-destructive">
|
|
|
|
|
{deleteProject.error.message}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function EditProjectSkeleton() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Skeleton className="h-9 w-36" />
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Skeleton className="h-8 w-32" />
|
|
|
|
|
<Skeleton className="h-4 w-64" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<Skeleton className="h-5 w-40" />
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-20" />
|
|
|
|
|
<Skeleton className="h-10 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-24" />
|
|
|
|
|
<Skeleton className="h-10 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-28" />
|
|
|
|
|
<Skeleton className="h-24 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<Skeleton className="h-5 w-16" />
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<Skeleton className="h-10 w-full" />
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function EditProjectPage({ params }: PageProps) {
|
|
|
|
|
const { id } = use(params)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Suspense fallback={<EditProjectSkeleton />}>
|
|
|
|
|
<EditProjectContent projectId={id} />
|
|
|
|
|
</Suspense>
|
|
|
|
|
)
|
|
|
|
|
}
|