MOPC-App/src/app/(admin)/admin/projects/new/page.tsx

420 lines
15 KiB
TypeScript
Raw Normal View History

'use client'
import { Suspense, useState } from 'react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Skeleton } from '@/components/ui/skeleton'
import { TagInput } from '@/components/shared/tag-input'
import { CountrySelect } from '@/components/ui/country-select'
import { PhoneInput } from '@/components/ui/phone-input'
import { toast } from 'sonner'
import {
ArrowLeft,
Save,
Loader2,
AlertCircle,
FolderPlus,
} from 'lucide-react'
function NewProjectPageContent() {
const router = useRouter()
const searchParams = useSearchParams()
const roundIdParam = searchParams.get('round')
const programIdParam = searchParams.get('program')
const [selectedProgramId, setSelectedProgramId] = useState<string>(programIdParam || '')
const [selectedRoundId, setSelectedRoundId] = useState<string>(roundIdParam || '')
// Form state
const [title, setTitle] = useState('')
const [teamName, setTeamName] = useState('')
const [description, setDescription] = useState('')
const [tags, setTags] = useState<string[]>([])
const [contactEmail, setContactEmail] = useState('')
const [contactName, setContactName] = useState('')
const [contactPhone, setContactPhone] = useState('')
const [country, setCountry] = useState('')
const [city, setCity] = useState('')
const [institution, setInstitution] = useState('')
const [competitionCategory, setCompetitionCategory] = useState<string>('')
const [oceanIssue, setOceanIssue] = useState<string>('')
// Fetch programs
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery({
status: 'ACTIVE',
includeRounds: true,
})
// Fetch wizard config for selected program (dropdown options)
const { data: wizardConfig } = trpc.program.getWizardConfig.useQuery(
{ programId: selectedProgramId },
{ enabled: !!selectedProgramId }
)
// Create mutation
const utils = trpc.useUtils()
const createProject = trpc.project.create.useMutation({
onSuccess: () => {
toast.success('Project created successfully')
utils.project.list.invalidate()
utils.round.get.invalidate()
router.push('/admin/projects')
},
onError: (error) => {
toast.error(error.message)
},
})
// Get rounds for selected program
const selectedProgram = programs?.find((p) => p.id === selectedProgramId)
const rounds = selectedProgram?.rounds || []
// Get dropdown options from wizard config
const categoryOptions = wizardConfig?.competitionCategories || []
const oceanIssueOptions = wizardConfig?.oceanIssues || []
const handleSubmit = () => {
if (!title.trim()) {
toast.error('Please enter a project title')
return
}
if (!selectedProgramId) {
toast.error('Please select a program')
return
}
createProject.mutate({
programId: selectedProgramId,
roundId: selectedRoundId || undefined,
title: title.trim(),
teamName: teamName.trim() || undefined,
description: description.trim() || undefined,
tags: tags.length > 0 ? tags : undefined,
country: country || undefined,
competitionCategory: competitionCategory as 'STARTUP' | 'BUSINESS_CONCEPT' | undefined || undefined,
oceanIssue: oceanIssue as 'POLLUTION_REDUCTION' | 'CLIMATE_MITIGATION' | 'TECHNOLOGY_INNOVATION' | 'SUSTAINABLE_SHIPPING' | 'BLUE_CARBON' | 'HABITAT_RESTORATION' | 'COMMUNITY_CAPACITY' | 'SUSTAINABLE_FISHING' | 'CONSUMER_AWARENESS' | 'OCEAN_ACIDIFICATION' | 'OTHER' | undefined || undefined,
institution: institution.trim() || undefined,
contactPhone: contactPhone.trim() || undefined,
contactEmail: contactEmail.trim() || undefined,
contactName: contactName.trim() || undefined,
city: city.trim() || undefined,
})
}
if (loadingPrograms) {
return <NewProjectPageSkeleton />
}
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">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Projects
</Link>
</Button>
</div>
<div className="flex items-center gap-2">
<FolderPlus className="h-6 w-6 text-primary" />
<div>
<h1 className="text-2xl font-semibold tracking-tight">Add Project</h1>
<p className="text-muted-foreground">
Manually create a new project submission
</p>
</div>
</div>
{/* Program & Round selection */}
<Card>
<CardHeader>
<CardTitle>Program & Round</CardTitle>
<CardDescription>
Select the program for this project. Round assignment is optional.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!programs || programs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<AlertCircle className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No Active Programs</p>
<p className="text-sm text-muted-foreground">
Create a program first before adding projects
</p>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>Program *</Label>
<Select value={selectedProgramId} onValueChange={(v) => {
setSelectedProgramId(v)
setSelectedRoundId('') // Reset round on program change
}}>
<SelectTrigger>
<SelectValue placeholder="Select a program" />
</SelectTrigger>
<SelectContent>
{programs.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name} {p.year}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Round (optional)</Label>
<Select value={selectedRoundId || '__none__'} onValueChange={(v) => setSelectedRoundId(v === '__none__' ? '' : v)} disabled={!selectedProgramId}>
<SelectTrigger>
<SelectValue placeholder="No round assigned" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">No round assigned</SelectItem>
{rounds.map((r: { id: string; name: string }) => (
<SelectItem key={r.id} value={r.id}>
{r.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
</CardContent>
</Card>
{selectedProgramId && (
<>
<div className="grid gap-6 lg:grid-cols-2">
{/* Basic Info */}
<Card>
<CardHeader>
<CardTitle>Project Details</CardTitle>
<CardDescription>
Basic information about the project
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="title">Project Title *</Label>
<Input
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g., Ocean Cleanup Initiative"
/>
</div>
<div className="space-y-2">
<Label htmlFor="teamName">Team/Organization Name</Label>
<Input
id="teamName"
value={teamName}
onChange={(e) => setTeamName(e.target.value)}
placeholder="e.g., Blue Ocean Foundation"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description of the project..."
rows={4}
maxLength={2000}
/>
</div>
<div className="space-y-2">
<Label>Tags</Label>
<TagInput
value={tags}
onChange={setTags}
placeholder="Select project tags..."
maxTags={10}
/>
</div>
{categoryOptions.length > 0 && (
<div className="space-y-2">
<Label>Competition Category</Label>
<Select value={competitionCategory} onValueChange={setCompetitionCategory}>
<SelectTrigger>
<SelectValue placeholder="Select category..." />
</SelectTrigger>
<SelectContent>
{categoryOptions.map((opt: { value: string; label: string }) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
{oceanIssueOptions.length > 0 && (
<div className="space-y-2">
<Label>Ocean Issue</Label>
<Select value={oceanIssue} onValueChange={setOceanIssue}>
<SelectTrigger>
<SelectValue placeholder="Select ocean issue..." />
</SelectTrigger>
<SelectContent>
{oceanIssueOptions.map((opt: { value: string; label: string }) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<Label htmlFor="institution">Institution</Label>
<Input
id="institution"
value={institution}
onChange={(e) => setInstitution(e.target.value)}
placeholder="e.g., University of Monaco"
/>
</div>
</CardContent>
</Card>
{/* Contact Info */}
<Card>
<CardHeader>
<CardTitle>Contact Information</CardTitle>
<CardDescription>
Contact details for the project team
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="contactName">Contact Name</Label>
<Input
id="contactName"
value={contactName}
onChange={(e) => setContactName(e.target.value)}
placeholder="e.g., John Smith"
/>
</div>
<div className="space-y-2">
<Label htmlFor="contactEmail">Contact Email</Label>
<Input
id="contactEmail"
type="email"
value={contactEmail}
onChange={(e) => setContactEmail(e.target.value)}
placeholder="e.g., john@example.com"
/>
</div>
<div className="space-y-2">
<Label>Contact Phone</Label>
<PhoneInput
value={contactPhone}
onChange={setContactPhone}
defaultCountry="MC"
/>
</div>
<div className="space-y-2">
<Label>Country</Label>
<CountrySelect
value={country}
onChange={setCountry}
/>
</div>
<div className="space-y-2">
<Label htmlFor="city">City</Label>
<Input
id="city"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="e.g., Monaco"
/>
</div>
</CardContent>
</Card>
</div>
{/* Actions */}
<div className="flex justify-end gap-4">
<Button variant="outline" asChild>
<Link href="/admin/projects">Cancel</Link>
</Button>
<Button
onClick={handleSubmit}
disabled={createProject.isPending || !title.trim() || !selectedProgramId}
>
{createProject.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Save className="mr-2 h-4 w-4" />
)}
Create Project
</Button>
</div>
</>
)}
</div>
)
}
function NewProjectPageSkeleton() {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Skeleton className="h-9 w-32" />
</div>
<Skeleton className="h-8 w-48" />
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-64" />
</CardHeader>
<CardContent>
<Skeleton className="h-10 w-full" />
</CardContent>
</Card>
</div>
)
}
export default function NewProjectPage() {
return (
<Suspense fallback={<NewProjectPageSkeleton />}>
<NewProjectPageContent />
</Suspense>
)
}