'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 { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Separator } from '@/components/ui/separator' import { toast } from 'sonner' import { ArrowLeft, Save, Loader2, AlertCircle, FolderPlus, Users, UserPlus, Trash2, Mail, } from 'lucide-react' type TeamMemberEntry = { id: string name: string email: string role: 'LEAD' | 'MEMBER' | 'ADVISOR' title: string phone: string sendInvite: boolean } const ROLE_COLORS: Record = { LEAD: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', MEMBER: 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400', ADVISOR: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', } const ROLE_AVATAR_COLORS: Record = { LEAD: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', MEMBER: 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300', ADVISOR: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300', } const ROLE_LABELS: Record = { LEAD: 'Lead', MEMBER: 'Member', ADVISOR: 'Advisor', } function getInitials(name: string): string { return name .split(' ') .map((w) => w[0]) .filter(Boolean) .slice(0, 2) .join('') .toUpperCase() } const ROLE_SORT_ORDER: Record = { LEAD: 0, MEMBER: 1, ADVISOR: 2 } function NewProjectPageContent() { const router = useRouter() const searchParams = useSearchParams() const roundIdParam = searchParams.get('round') const programIdParam = searchParams.get('program') const [selectedProgramId, setSelectedProgramId] = useState(programIdParam || '') const [selectedRoundId, setSelectedRoundId] = useState(roundIdParam || '') // Form state const [title, setTitle] = useState('') const [teamName, setTeamName] = useState('') const [description, setDescription] = useState('') const [tags, setTags] = useState([]) const [country, setCountry] = useState('') const [institution, setInstitution] = useState('') const [competitionCategory, setCompetitionCategory] = useState('') const [oceanIssue, setOceanIssue] = useState('') // Team members state const [teamMembers, setTeamMembers] = useState([]) const [memberName, setMemberName] = useState('') const [memberEmail, setMemberEmail] = useState('') const [memberRole, setMemberRole] = useState<'LEAD' | 'MEMBER' | 'ADVISOR'>('MEMBER') const [memberTitle, setMemberTitle] = useState('') const [memberPhone, setMemberPhone] = useState('') const [memberSendInvite, setMemberSendInvite] = useState(false) // 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 handleAddMember = () => { if (!memberName.trim()) { toast.error('Please enter a member name') return } if (!memberEmail.trim()) { toast.error('Please enter a member email') return } // Basic email validation if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(memberEmail.trim())) { toast.error('Please enter a valid email address') return } // Check for duplicates if (teamMembers.some((m) => m.email.toLowerCase() === memberEmail.trim().toLowerCase())) { toast.error('A member with this email already exists') return } if (teamMembers.length >= 10) { toast.error('Maximum 10 team members allowed') return } setTeamMembers((prev) => [ ...prev, { id: crypto.randomUUID(), name: memberName.trim(), email: memberEmail.trim(), role: memberRole, title: memberTitle.trim(), phone: memberPhone.trim(), sendInvite: memberSendInvite, }, ]) // Reset form setMemberName('') setMemberEmail('') setMemberRole('MEMBER') setMemberTitle('') setMemberPhone('') setMemberSendInvite(false) } const handleRemoveMember = (id: string) => { setTeamMembers((prev) => prev.filter((m) => m.id !== id)) } const handleToggleInvite = (id: string) => { setTeamMembers((prev) => prev.map((m) => (m.id === id ? { ...m, sendInvite: !m.sendInvite } : m)) ) } const sortedMembers = [...teamMembers].sort( (a, b) => (ROLE_SORT_ORDER[a.role] ?? 9) - (ROLE_SORT_ORDER[b.role] ?? 9) ) 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, teamMembers: teamMembers.length > 0 ? teamMembers.map(({ name, email, role, title: t, phone, sendInvite }) => ({ name, email, role, title: t || undefined, phone: phone || undefined, sendInvite, })) : undefined, }) } if (loadingPrograms) { return } return (
{/* Header */}

Add Project

Manually create a new project submission

{/* Program & Round selection */} Program & Round Select the program for this project. Round assignment is optional. {!programs || programs.length === 0 ? (

No Active Programs

Create a program first before adding projects

) : (
)}
{selectedProgramId && ( <>
{/* Basic Info */} Project Details Basic information about the project
setTitle(e.target.value)} placeholder="e.g., Ocean Cleanup Initiative" />
setTeamName(e.target.value)} placeholder="e.g., Blue Ocean Foundation" />