'use client' import { useState, useEffect } from 'react' import { useSession } from 'next-auth/react' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { Textarea } from '@/components/ui/textarea' import { CompetitionTimelineSidebar } from '@/components/applicant/competition-timeline' import { MentoringRequestCard } from '@/components/applicant/mentoring-request-card' import { MentorConversationCard } from '@/components/applicant/mentor-conversation-card' import { AttendingMembersCard } from '@/components/applicant/attending-members-card' import { LunchBanner } from '@/components/applicant/lunch-banner' import { ExternalAttendeesStrip } from '@/components/applicant/external-attendees-strip' import { AnimatedCard } from '@/components/shared/animated-container' import { ProjectLogoUpload } from '@/components/shared/project-logo-upload' import { Progress } from '@/components/ui/progress' import { FileText, Calendar, CheckCircle, Users, Crown, MessageSquare, Upload, ArrowRight, Star, AlertCircle, Pencil, Loader2, Check, X, UserCircle, Trophy, Vote, Clock, } from 'lucide-react' import { toast } from 'sonner' function formatCountdown(ms: number): string { if (ms <= 0) return 'Closed' const days = Math.floor(ms / (1000 * 60 * 60 * 24)) const hours = Math.floor((ms % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)) const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)) const parts: string[] = [] if (days > 0) parts.push(`${days}d`) if (hours > 0) parts.push(`${hours}h`) parts.push(`${minutes}m`) return parts.join(' ') } const statusColors: Record = { DRAFT: 'secondary', SUBMITTED: 'default', UNDER_REVIEW: 'default', ELIGIBLE: 'default', SEMIFINALIST: 'success', FINALIST: 'success', WINNER: 'success', REJECTED: 'destructive', } // Keys to hide from the metadata display (shown elsewhere or internal) const HIDDEN_METADATA_KEYS = new Set(['TeamMembers', 'teammembers', 'team_members']) export default function ApplicantDashboardPage() { const { data: session, status: sessionStatus } = useSession() const isAuthenticated = sessionStatus === 'authenticated' const utils = trpc.useUtils() const { data, isLoading } = trpc.applicant.getMyDashboard.useQuery(undefined, { enabled: isAuthenticated, }) const { data: deadlines } = trpc.applicant.getUpcomingDeadlines.useQuery(undefined, { enabled: isAuthenticated, }) const { data: docCompleteness } = trpc.applicant.getDocumentCompleteness.useQuery(undefined, { enabled: isAuthenticated, }) const { data: evaluations } = trpc.applicant.getMyEvaluations.useQuery(undefined, { enabled: isAuthenticated, }) const { data: flags } = trpc.settings.getFeatureFlags.useQuery(undefined, { enabled: isAuthenticated, }) // Live countdown timer for open rounds const [now, setNow] = useState(() => Date.now()) useEffect(() => { const interval = setInterval(() => setNow(Date.now()), 60_000) return () => clearInterval(interval) }, []) if (sessionStatus === 'loading' || (isAuthenticated && isLoading)) { return (
) } // No project yet if (!data?.project) { return (

My Project

Your applicant dashboard

No Project Yet

You haven't submitted a project yet. Check for open application rounds on the MOPC website.

) } const { project, timeline, currentStatus, openRounds, hasPassedIntake, isRejected } = data const programYear = project.program?.year const programName = project.program?.name const totalEvaluations = evaluations?.reduce((sum, r) => sum + r.evaluationCount, 0) ?? 0 const canEditDescription = flags?.applicantAllowDescriptionEdit && !isRejected return (
{/* Header — no withdraw button here */}
{/* Project logo — clickable for any team member to change */} utils.applicant.getMyDashboard.invalidate()} >

{project.title}

{currentStatus && ( {currentStatus.replace('_', ' ')} )}

{programYear ? `${programYear} Edition` : ''}{programName ? ` - ${programName}` : ''}

{/* Active round deadline banner */} {!isRejected && openRounds.length > 0 && (() => { const submissionTypes = new Set(['INTAKE', 'SUBMISSION', 'MENTORING']) const roundsWithDeadline = openRounds.filter((r) => r.windowCloseAt && submissionTypes.has(r.roundType)) if (roundsWithDeadline.length === 0) return null return roundsWithDeadline.map((round) => { const closeAt = new Date(round.windowCloseAt!).getTime() const remaining = closeAt - now const isUrgent = remaining > 0 && remaining < 1000 * 60 * 60 * 24 * 3 // < 3 days return (
{round.name} {remaining > 0 ? formatCountdown(remaining) + ' left' : 'Closed'}
Closes {new Date(round.windowCloseAt!).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', })}{' '} at {new Date(round.windowCloseAt!).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', })}
) }) })()}
{/* Main content */}
{/* Project details */} Project Details {project.teamName && (

Team/Organization

{project.teamName}

)} {/* Description — editable if admin allows */} {project.description && !canEditDescription && (

Description

{project.description}

)} {canEditDescription && ( )} {project.tags && project.tags.length > 0 && (

Tags

{project.tags.map((tag) => ( {tag} ))}
)} {/* Metadata — filter out team members (shown in sidebar) */} {project.metadataJson && (() => { const entries = Object.entries(project.metadataJson as Record) .filter(([key]) => !HIDDEN_METADATA_KEYS.has(key)) if (entries.length === 0) return null return (

Additional Information

{entries.map(([key, value]) => (
{key.replace(/_/g, ' ')}
{String(value)}
))}
) })()} {/* Meta info row */}
Created {new Date(project.createdAt).toLocaleDateString()}
{project.submittedAt && (
Submitted {new Date(project.submittedAt).toLocaleDateString()}
)}
{project.files.length} file(s)
{/* Rejected banner */} {isRejected && (

Your project was not selected to advance. Your project space is now read-only.

)} {/* Document Completeness */} {docCompleteness && docCompleteness.length > 0 && ( Document Progress {docCompleteness.map((dc) => (
{dc.roundName} {dc.uploaded}/{dc.required} files
0 ? Math.round((dc.uploaded / dc.required) * 100) : 0}%` }} />
))} )}
{/* Sidebar */}
{/* Competition timeline */} Status Timeline {/* Mentoring Request Card */} {project.isTeamLead && openRounds.filter((r) => r.roundType === 'MENTORING').map((mentoringRound) => ( ))} {/* Lunch banner (auto-hides when lunch event disabled or unconfigured) */} {/* External lunch attendees attached to this team (auto-hides if none) */} {/* Grand finale attendee roster (auto-hides until confirmation status is CONFIRMED) */} {/* Conversation with assigned mentor (auto-hides when no mentor assigned) */} {/* Jury Feedback Card */} {totalEvaluations > 0 && (
Jury Feedback
{evaluations?.map((round) => { const showScore = round.roundType !== 'DELIBERATION' const scores = round.evaluations .map((ev) => ev.globalScore) .filter((s): s is number => s !== null) const avgScore = showScore && scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : null const maxScore = 10 const pct = avgScore !== null ? (avgScore / maxScore) * 100 : 0 const roundIcon = round.roundType === 'LIVE_FINAL' ? : round.roundType === 'DELIBERATION' ? : return (
{roundIcon} {round.roundName} {round.evaluationCount} review{round.evaluationCount !== 1 ? 's' : ''}
{avgScore !== null && (
Avg Score {avgScore.toFixed(1)} / {maxScore}
)}
) })}
)} {/* Team overview — proper cards */}
Team
{project.teamMembers.length > 0 ? ( project.teamMembers.slice(0, 5).map((member) => (
{member.role === 'LEAD' ? ( ) : ( )}

{member.user.name || member.user.email}

{member.role === 'LEAD' ? 'Lead' : member.role === 'ADVISOR' ? 'Advisor' : 'Member'}
)) ) : (

No team members yet

)} {project.teamMembers.length > 5 && (

+{project.teamMembers.length - 5} more

)}
{/* Upcoming Deadlines */} {deadlines && deadlines.length > 0 && ( Upcoming Deadlines {deadlines.map((dl, i) => (
{dl.roundName} {new Date(dl.windowCloseAt).toLocaleDateString()}
))}
)} {/* Key dates */} Key Dates
Created {new Date(project.createdAt).toLocaleDateString()}
{project.submittedAt && (
Submitted {new Date(project.submittedAt).toLocaleDateString()}
)}
Last Updated {new Date(project.updatedAt).toLocaleDateString()}
) } function EditableDescription({ projectId, initialDescription }: { projectId: string; initialDescription: string }) { const [isEditing, setIsEditing] = useState(false) const [description, setDescription] = useState(initialDescription) const utils = trpc.useUtils() const mutation = trpc.applicant.updateDescription.useMutation({ onSuccess: () => { utils.applicant.getMyDashboard.invalidate() setIsEditing(false) toast.success('Description updated') }, onError: (e) => toast.error(e.message), }) const handleSave = () => { mutation.mutate({ projectId, description }) } const handleCancel = () => { setDescription(initialDescription) setIsEditing(false) } if (!isEditing) { return (

Description

{initialDescription || No description yet. Click Edit to add one.}

) } return (

Description