Performance optimization, applicant portal, and missing DB migration
Performance: - Convert admin dashboard from SSR to client-side tRPC (fixes 503/ChunkLoadError) - New dashboard.getStats tRPC endpoint batches 16 queries into single response - Parallelize jury dashboard queries (assignments + gracePeriods via Promise.all) - Add project.getFullDetail combined endpoint (project + assignments + stats) - Configure Prisma connection pool (connection_limit=20, pool_timeout=10) - Add optimizePackageImports for lucide-react tree-shaking - Increase React Query staleTime from 1min to 5min Applicant portal: - Add applicant layout, nav, dashboard, documents, team, and mentor pages - Add applicant router with document and team management endpoints - Add chunk error recovery utility - Update role nav and auth redirect for applicant role Database: - Add migration for missing schema elements (SpecialAward job tracking columns, WizardTemplate table, missing indexes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
234
src/app/(applicant)/applicant/documents/page.tsx
Normal file
234
src/app/(applicant)/applicant/documents/page.tsx
Normal file
@@ -0,0 +1,234 @@
|
||||
'use client'
|
||||
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { RequirementUploadList } from '@/components/shared/requirement-upload-slot'
|
||||
import {
|
||||
FileText,
|
||||
Upload,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Video,
|
||||
File,
|
||||
Download,
|
||||
} from 'lucide-react'
|
||||
|
||||
const fileTypeIcons: Record<string, typeof FileText> = {
|
||||
EXEC_SUMMARY: FileText,
|
||||
BUSINESS_PLAN: FileText,
|
||||
PRESENTATION: FileText,
|
||||
VIDEO_PITCH: Video,
|
||||
VIDEO: Video,
|
||||
OTHER: File,
|
||||
SUPPORTING_DOC: File,
|
||||
}
|
||||
|
||||
const fileTypeLabels: Record<string, string> = {
|
||||
EXEC_SUMMARY: 'Executive Summary',
|
||||
BUSINESS_PLAN: 'Business Plan',
|
||||
PRESENTATION: 'Presentation',
|
||||
VIDEO_PITCH: 'Video Pitch',
|
||||
VIDEO: 'Video',
|
||||
OTHER: 'Other Document',
|
||||
SUPPORTING_DOC: 'Supporting Document',
|
||||
}
|
||||
|
||||
export default function ApplicantDocumentsPage() {
|
||||
const { status: sessionStatus } = useSession()
|
||||
const isAuthenticated = sessionStatus === 'authenticated'
|
||||
|
||||
const { data, isLoading } = trpc.applicant.getMyDashboard.useQuery(undefined, {
|
||||
enabled: isAuthenticated,
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-4 w-64" />
|
||||
</div>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
<Skeleton className="h-48 w-full" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!data?.project) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Documents</h1>
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<FileText className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
||||
<h2 className="text-xl font-semibold mb-2">No Project</h2>
|
||||
<p className="text-muted-foreground text-center">
|
||||
Submit a project first to upload documents.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { project, openRounds } = data
|
||||
const isDraft = !project.submittedAt
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||
<Upload className="h-6 w-6" />
|
||||
Documents
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Upload and manage documents for your project: {project.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Per-round upload sections */}
|
||||
{openRounds.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
{openRounds.map((round) => {
|
||||
const now = new Date()
|
||||
const isLate = round.votingStartAt && now > new Date(round.votingStartAt)
|
||||
const hasDeadline = !!round.submissionDeadline
|
||||
const deadlinePassed = hasDeadline && now > new Date(round.submissionDeadline!)
|
||||
|
||||
return (
|
||||
<Card key={round.id}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{round.name}</CardTitle>
|
||||
<CardDescription>
|
||||
Upload documents for this round
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{isLate && (
|
||||
<Badge variant="warning" className="gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Late submission
|
||||
</Badge>
|
||||
)}
|
||||
{hasDeadline && !deadlinePassed && (
|
||||
<Badge variant="outline" className="gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
Due {new Date(round.submissionDeadline!).toLocaleDateString()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RequirementUploadList
|
||||
projectId={project.id}
|
||||
roundId={round.id}
|
||||
disabled={false}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Original round upload (if not already in openRounds) */}
|
||||
{project.roundId && !openRounds.some((r) => r.id === project.roundId) && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">
|
||||
{project.round?.name || 'Submission Documents'}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Documents uploaded with your original application
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<RequirementUploadList
|
||||
projectId={project.id}
|
||||
roundId={project.roundId}
|
||||
disabled={!isDraft}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Uploaded files list */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Uploaded Documents</CardTitle>
|
||||
<CardDescription>
|
||||
All files associated with your project
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{project.files.length === 0 ? (
|
||||
<p className="text-muted-foreground text-center py-4">
|
||||
No documents uploaded yet
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{project.files.map((file) => {
|
||||
const Icon = fileTypeIcons[file.fileType] || File
|
||||
const fileRecord = file as typeof file & { isLate?: boolean; roundId?: string | null }
|
||||
|
||||
return (
|
||||
<div
|
||||
key={file.id}
|
||||
className="flex items-center justify-between p-3 rounded-lg border"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-sm">{file.fileName}</p>
|
||||
{fileRecord.isLate && (
|
||||
<Badge variant="warning" className="text-xs gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Late
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{fileTypeLabels[file.fileType] || file.fileType}
|
||||
{' - '}
|
||||
{new Date(file.createdAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* No open rounds message */}
|
||||
{openRounds.length === 0 && !project.roundId && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardContent className="p-6 text-center">
|
||||
<Clock className="h-10 w-10 mx-auto text-muted-foreground/50 mb-3" />
|
||||
<p className="text-muted-foreground">
|
||||
No rounds are currently open for document submissions.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user