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:
2026-02-11 11:04:26 +01:00
parent 09091d7c08
commit 98f4a957cc
32 changed files with 3002 additions and 1121 deletions

View File

@@ -0,0 +1,138 @@
'use client'
import { useSession } from 'next-auth/react'
import { trpc } from '@/lib/trpc/client'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { MentorChat } from '@/components/shared/mentor-chat'
import {
MessageSquare,
UserCircle,
FileText,
} from 'lucide-react'
export default function ApplicantMentorPage() {
const { data: session, status: sessionStatus } = useSession()
const isAuthenticated = sessionStatus === 'authenticated'
const { data: dashboardData, isLoading: dashLoading } = trpc.applicant.getMyDashboard.useQuery(
undefined,
{ enabled: isAuthenticated }
)
const projectId = dashboardData?.project?.id
const { data: mentorMessages, isLoading: messagesLoading } = trpc.applicant.getMentorMessages.useQuery(
{ projectId: projectId! },
{ enabled: !!projectId }
)
const utils = trpc.useUtils()
const sendMessage = trpc.applicant.sendMentorMessage.useMutation({
onSuccess: () => {
utils.applicant.getMentorMessages.invalidate({ projectId: projectId! })
},
})
if (dashLoading) {
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-96 w-full" />
</div>
)
}
if (!projectId) {
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Mentor</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 communicate with your mentor.
</p>
</CardContent>
</Card>
</div>
)
}
const mentor = dashboardData?.project?.mentorAssignment?.mentor
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
<MessageSquare className="h-6 w-6" />
Mentor Communication
</h1>
<p className="text-muted-foreground">
Chat with your assigned mentor
</p>
</div>
{/* Mentor info */}
{mentor ? (
<Card className="bg-muted/50">
<CardContent className="p-4">
<div className="flex items-center gap-3">
<UserCircle className="h-10 w-10 text-muted-foreground" />
<div>
<p className="font-medium">{mentor.name || 'Mentor'}</p>
<p className="text-sm text-muted-foreground">{mentor.email}</p>
</div>
</div>
</CardContent>
</Card>
) : (
<Card className="bg-muted/50">
<CardContent className="flex flex-col items-center justify-center py-8">
<UserCircle className="h-12 w-12 text-muted-foreground/50 mb-3" />
<p className="text-muted-foreground text-center">
No mentor has been assigned to your project yet.
You&apos;ll be notified when a mentor is assigned.
</p>
</CardContent>
</Card>
)}
{/* Chat */}
{mentor && (
<Card>
<CardHeader>
<CardTitle>Messages</CardTitle>
<CardDescription>
Your conversation history with {mentor.name || 'your mentor'}
</CardDescription>
</CardHeader>
<CardContent>
<MentorChat
messages={mentorMessages || []}
currentUserId={session?.user?.id || ''}
onSendMessage={async (message) => {
await sendMessage.mutateAsync({ projectId: projectId!, message })
}}
isLoading={messagesLoading}
isSending={sendMessage.isPending}
/>
</CardContent>
</Card>
)}
</div>
)
}