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>
This commit is contained in:
2026-02-08 22:05:01 +01:00
parent e0e4cb2a32
commit e73a676412
33 changed files with 3193 additions and 977 deletions

View File

@@ -1,5 +1,6 @@
'use client'
import { useState, useMemo } from 'react'
import Link from 'next/link'
import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client'
@@ -15,6 +16,14 @@ import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { Progress } from '@/components/ui/progress'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Users,
Briefcase,
@@ -27,6 +36,7 @@ import {
CheckCircle2,
Circle,
Clock,
Search,
} from 'lucide-react'
import { formatDateOnly } from '@/lib/utils'
@@ -72,15 +82,27 @@ function DashboardSkeleton() {
export default function MentorDashboard() {
const { data: assignments, isLoading } = trpc.mentor.getMyProjects.useQuery()
if (isLoading) {
return <DashboardSkeleton />
}
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState('all')
const projects = assignments || []
const completedCount = projects.filter((a) => a.completionStatus === 'completed').length
const inProgressCount = projects.filter((a) => a.completionStatus === 'in_progress').length
const filteredProjects = useMemo(() => {
return projects.filter(a => {
const matchesSearch = !search ||
a.project.title.toLowerCase().includes(search.toLowerCase()) ||
a.project.teamName?.toLowerCase().includes(search.toLowerCase())
const matchesStatus = statusFilter === 'all' || a.completionStatus === statusFilter
return matchesSearch && matchesStatus
})
}, [projects, search, statusFilter])
if (isLoading) {
return <DashboardSkeleton />
}
return (
<div className="space-y-6">
{/* Header */}
@@ -154,10 +176,46 @@ export default function MentorDashboard() {
</Card>
</div>
{/* Quick Actions */}
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" asChild>
<Link href={'/mentor/messages' as Route}>
<Mail className="mr-2 h-4 w-4" />
Messages
</Link>
</Button>
</div>
{/* Projects List */}
<div>
<h2 className="text-lg font-semibold mb-4">Your Mentees</h2>
{/* Search and Filter */}
{projects.length > 0 && (
<div className="flex flex-col gap-3 sm:flex-row sm:items-center mb-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search projects..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
/>
</div>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="All statuses" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All statuses</SelectItem>
<SelectItem value="in_progress">In Progress</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="paused">Paused</SelectItem>
</SelectContent>
</Select>
</div>
)}
{projects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
@@ -171,9 +229,26 @@ export default function MentorDashboard() {
</p>
</CardContent>
</Card>
) : filteredProjects.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-8 text-center">
<Search className="h-8 w-8 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground">
No projects match your search criteria
</p>
<Button
variant="ghost"
size="sm"
className="mt-2"
onClick={() => { setSearch(''); setStatusFilter('all') }}
>
Clear filters
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4">
{projects.map((assignment) => {
{filteredProjects.map((assignment) => {
const project = assignment.project
const teamLead = project.teamMembers?.find(
(m) => m.role === 'LEAD'