2026-01-30 13:41:32 +01:00
|
|
|
'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'
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
import { CountrySelect } from '@/components/ui/country-select'
|
2026-02-10 23:08:00 +01:00
|
|
|
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'
|
2026-01-30 13:41:32 +01:00
|
|
|
import { toast } from 'sonner'
|
|
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
Save,
|
|
|
|
|
Loader2,
|
|
|
|
|
AlertCircle,
|
|
|
|
|
FolderPlus,
|
2026-02-10 23:08:00 +01:00
|
|
|
Users,
|
|
|
|
|
UserPlus,
|
|
|
|
|
Trash2,
|
|
|
|
|
Mail,
|
2026-01-30 13:41:32 +01:00
|
|
|
} from 'lucide-react'
|
|
|
|
|
|
2026-02-10 23:08:00 +01:00
|
|
|
type TeamMemberEntry = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
email: string
|
|
|
|
|
role: 'LEAD' | 'MEMBER' | 'ADVISOR'
|
|
|
|
|
title: string
|
|
|
|
|
phone: string
|
|
|
|
|
sendInvite: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ROLE_COLORS: Record<string, string> = {
|
|
|
|
|
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<string, string> = {
|
|
|
|
|
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<string, string> = {
|
|
|
|
|
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<string, number> = { LEAD: 0, MEMBER: 1, ADVISOR: 2 }
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
function NewProjectPageContent() {
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
const roundIdParam = searchParams.get('round')
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
const programIdParam = searchParams.get('program')
|
2026-01-30 13:41:32 +01:00
|
|
|
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
const [selectedProgramId, setSelectedProgramId] = useState<string>(programIdParam || '')
|
2026-01-30 13:41:32 +01:00
|
|
|
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 [country, setCountry] = useState('')
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
const [institution, setInstitution] = useState('')
|
|
|
|
|
const [competitionCategory, setCompetitionCategory] = useState<string>('')
|
|
|
|
|
const [oceanIssue, setOceanIssue] = useState<string>('')
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-10 23:08:00 +01:00
|
|
|
// Team members state
|
|
|
|
|
const [teamMembers, setTeamMembers] = useState<TeamMemberEntry[]>([])
|
|
|
|
|
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)
|
|
|
|
|
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
// Fetch programs
|
2026-01-30 13:41:32 +01:00
|
|
|
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery({
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
includeRounds: true,
|
|
|
|
|
})
|
|
|
|
|
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
// Fetch wizard config for selected program (dropdown options)
|
|
|
|
|
const { data: wizardConfig } = trpc.program.getWizardConfig.useQuery(
|
|
|
|
|
{ programId: selectedProgramId },
|
|
|
|
|
{ enabled: !!selectedProgramId }
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
// Create mutation
|
2026-02-02 23:36:46 +01:00
|
|
|
const utils = trpc.useUtils()
|
2026-01-30 13:41:32 +01:00
|
|
|
const createProject = trpc.project.create.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
toast.success('Project created successfully')
|
2026-02-02 23:36:46 +01:00
|
|
|
utils.project.list.invalidate()
|
|
|
|
|
utils.round.get.invalidate()
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
router.push('/admin/projects')
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
toast.error(error.message)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
// Get rounds for selected program
|
|
|
|
|
const selectedProgram = programs?.find((p) => p.id === selectedProgramId)
|
|
|
|
|
const rounds = selectedProgram?.rounds || []
|
2026-01-30 13:41:32 +01:00
|
|
|
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
// Get dropdown options from wizard config
|
|
|
|
|
const categoryOptions = wizardConfig?.competitionCategories || []
|
|
|
|
|
const oceanIssueOptions = wizardConfig?.oceanIssues || []
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-10 23:08:00 +01:00
|
|
|
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)
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
const handleSubmit = () => {
|
|
|
|
|
if (!title.trim()) {
|
|
|
|
|
toast.error('Please enter a project title')
|
|
|
|
|
return
|
|
|
|
|
}
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
if (!selectedProgramId) {
|
|
|
|
|
toast.error('Please select a program')
|
2026-01-30 13:41:32 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createProject.mutate({
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
programId: selectedProgramId,
|
|
|
|
|
roundId: selectedRoundId || undefined,
|
2026-01-30 13:41:32 +01:00
|
|
|
title: title.trim(),
|
|
|
|
|
teamName: teamName.trim() || undefined,
|
|
|
|
|
description: description.trim() || undefined,
|
|
|
|
|
tags: tags.length > 0 ? tags : undefined,
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
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,
|
2026-02-10 23:08:00 +01:00
|
|
|
teamMembers: teamMembers.length > 0
|
|
|
|
|
? teamMembers.map(({ name, email, role, title: t, phone, sendInvite }) => ({
|
|
|
|
|
name,
|
|
|
|
|
email,
|
|
|
|
|
role,
|
|
|
|
|
title: t || undefined,
|
|
|
|
|
phone: phone || undefined,
|
|
|
|
|
sendInvite,
|
|
|
|
|
}))
|
|
|
|
|
: undefined,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
{/* 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>
|
2026-01-30 13:41:32 +01:00
|
|
|
</div>
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Round (optional)</Label>
|
|
|
|
|
<Select value={selectedRoundId || '__none__'} onValueChange={(v) => setSelectedRoundId(v === '__none__' ? '' : v)} disabled={!selectedProgramId}>
|
2026-01-30 13:41:32 +01:00
|
|
|
<SelectTrigger>
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
<SelectValue placeholder="No round assigned" />
|
2026-01-30 13:41:32 +01:00
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
<SelectItem value="__none__">No round assigned</SelectItem>
|
|
|
|
|
{rounds.map((r: { id: string; name: string }) => (
|
|
|
|
|
<SelectItem key={r.id} value={r.id}>
|
|
|
|
|
{r.name}
|
2026-01-30 13:41:32 +01:00
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2026-01-30 13:41:32 +01:00
|
|
|
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
{selectedProgramId && (
|
|
|
|
|
<>
|
2026-01-30 13:41:32 +01:00
|
|
|
<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>
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
|
|
|
|
|
{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>
|
2026-02-10 23:08:00 +01:00
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Country</Label>
|
|
|
|
|
<CountrySelect
|
|
|
|
|
value={country}
|
|
|
|
|
onChange={setCountry}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-01-30 13:41:32 +01:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-02-10 23:08:00 +01:00
|
|
|
{/* Team Members */}
|
2026-01-30 13:41:32 +01:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
2026-02-10 23:08:00 +01:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<CardTitle>Team Members</CardTitle>
|
|
|
|
|
<Badge variant="secondary">{teamMembers.length} / 10</Badge>
|
|
|
|
|
</div>
|
2026-01-30 13:41:32 +01:00
|
|
|
<CardDescription>
|
2026-02-10 23:08:00 +01:00
|
|
|
Add team members and optionally invite them to the platform
|
2026-01-30 13:41:32 +01:00
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
2026-02-10 23:08:00 +01:00
|
|
|
{teamMembers.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center rounded-lg border border-dashed py-8 text-center">
|
|
|
|
|
<Users className="h-10 w-10 text-muted-foreground/40" />
|
|
|
|
|
<p className="mt-2 text-sm font-medium">No team members yet</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Add members below to link them to this project
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{sortedMembers.map((member) => (
|
|
|
|
|
<div
|
|
|
|
|
key={member.id}
|
|
|
|
|
className="flex items-center gap-3 rounded-lg border p-3"
|
|
|
|
|
>
|
|
|
|
|
<Avatar className={`h-9 w-9 ${ROLE_AVATAR_COLORS[member.role]}`}>
|
|
|
|
|
<AvatarFallback className={ROLE_AVATAR_COLORS[member.role]}>
|
|
|
|
|
{getInitials(member.name)}
|
|
|
|
|
</AvatarFallback>
|
|
|
|
|
</Avatar>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="truncate text-sm font-medium">{member.name}</span>
|
|
|
|
|
<Badge variant="outline" className={`text-[10px] px-1.5 py-0 ${ROLE_COLORS[member.role]}`}>
|
|
|
|
|
{ROLE_LABELS[member.role]}
|
|
|
|
|
</Badge>
|
|
|
|
|
{member.title && (
|
|
|
|
|
<span className="hidden truncate text-xs text-muted-foreground sm:inline">
|
|
|
|
|
{member.title}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="truncate text-xs text-muted-foreground">{member.email}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => handleToggleInvite(member.id)}
|
|
|
|
|
className={`flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors ${
|
|
|
|
|
member.sendInvite
|
|
|
|
|
? 'bg-primary/10 text-primary'
|
|
|
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
|
|
|
}`}
|
|
|
|
|
title={member.sendInvite ? 'Invitation will be sent' : 'Click to send invitation'}
|
|
|
|
|
>
|
|
|
|
|
<Mail className="h-3.5 w-3.5" />
|
|
|
|
|
<span className="hidden sm:inline">
|
|
|
|
|
{member.sendInvite ? 'Invite' : 'No invite'}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-8 w-8 text-muted-foreground hover:text-destructive"
|
|
|
|
|
onClick={() => handleRemoveMember(member.id)}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-10 23:08:00 +01:00
|
|
|
{teamMembers.length > 0 && teamMembers.length < 10 && <Separator />}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-10 23:08:00 +01:00
|
|
|
{/* Add member form */}
|
|
|
|
|
{teamMembers.length < 10 && (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<p className="text-sm font-medium flex items-center gap-1.5">
|
|
|
|
|
<UserPlus className="h-4 w-4" />
|
|
|
|
|
Add Member
|
|
|
|
|
</p>
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="memberName" className="text-xs">Name *</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="memberName"
|
|
|
|
|
value={memberName}
|
|
|
|
|
onChange={(e) => setMemberName(e.target.value)}
|
|
|
|
|
placeholder="Full name"
|
|
|
|
|
className="h-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="memberEmail" className="text-xs">Email *</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="memberEmail"
|
|
|
|
|
type="email"
|
|
|
|
|
value={memberEmail}
|
|
|
|
|
onChange={(e) => setMemberEmail(e.target.value)}
|
|
|
|
|
placeholder="email@example.com"
|
|
|
|
|
className="h-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label className="text-xs">Role</Label>
|
|
|
|
|
<Select value={memberRole} onValueChange={(v) => setMemberRole(v as 'LEAD' | 'MEMBER' | 'ADVISOR')}>
|
|
|
|
|
<SelectTrigger className="h-9">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="LEAD">Lead</SelectItem>
|
|
|
|
|
<SelectItem value="MEMBER">Member</SelectItem>
|
|
|
|
|
<SelectItem value="ADVISOR">Advisor</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="memberTitle" className="text-xs">Title (optional)</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="memberTitle"
|
|
|
|
|
value={memberTitle}
|
|
|
|
|
onChange={(e) => setMemberTitle(e.target.value)}
|
|
|
|
|
placeholder="e.g., CEO, CTO"
|
|
|
|
|
className="h-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="memberSendInvite"
|
|
|
|
|
checked={memberSendInvite}
|
|
|
|
|
onCheckedChange={(checked) => setMemberSendInvite(checked === true)}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="memberSendInvite" className="text-xs cursor-pointer">
|
|
|
|
|
Send platform invitation
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleAddMember}
|
|
|
|
|
>
|
|
|
|
|
<UserPlus className="mr-1.5 h-3.5 w-3.5" />
|
|
|
|
|
Add
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
|
2026-02-10 23:08:00 +01:00
|
|
|
{teamMembers.length >= 10 && (
|
|
|
|
|
<p className="text-center text-xs text-muted-foreground">
|
|
|
|
|
Maximum of 10 team members reached
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2026-01-30 13:41:32 +01:00
|
|
|
</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}
|
Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions
Phase 2: Admin UX - search/filter for awards, learning, partners pages
Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions
Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting
Phase 5: Portals - observer charts, mentor search, login/onboarding polish
Phase 6: Messages preview dialog, CsvExportDialog with column selection
Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook
Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:05:01 +01:00
|
|
|
disabled={createProject.isPending || !title.trim() || !selectedProgramId}
|
2026-01-30 13:41:32 +01:00
|
|
|
>
|
|
|
|
|
{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>
|
|
|
|
|
)
|
|
|
|
|
}
|