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

@@ -81,6 +81,7 @@ import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import { truncate } from '@/lib/utils'
import { ProjectLogo } from '@/components/shared/project-logo'
import { StatusBadge } from '@/components/shared/status-badge'
import { Pagination } from '@/components/shared/pagination'
import {
ProjectFiltersBar,
@@ -256,6 +257,11 @@ export default function ProjectsPage() {
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [projectToDelete, setProjectToDelete] = useState<{ id: string; title: string } | null>(null)
// Assign to round dialog state
const [assignDialogOpen, setAssignDialogOpen] = useState(false)
const [projectToAssign, setProjectToAssign] = useState<{ id: string; title: string } | null>(null)
const [assignRoundId, setAssignRoundId] = useState('')
const [aiTagDialogOpen, setAiTagDialogOpen] = useState(false)
const [taggingScope, setTaggingScope] = useState<'round' | 'program'>('round')
const [selectedRoundForTagging, setSelectedRoundForTagging] = useState<string>('')
@@ -420,6 +426,19 @@ export default function ProjectsPage() {
? data.projects.some((p) => selectedIds.has(p.id)) && !allVisibleSelected
: false
const assignToRound = trpc.projectPool.assignToRound.useMutation({
onSuccess: () => {
toast.success('Project assigned to round')
utils.project.list.invalidate()
setAssignDialogOpen(false)
setProjectToAssign(null)
setAssignRoundId('')
},
onError: (error) => {
toast.error(error.message || 'Failed to assign project')
},
})
const deleteProject = trpc.project.delete.useMutation({
onSuccess: () => {
toast.success('Project deleted successfully')
@@ -448,6 +467,12 @@ export default function ProjectsPage() {
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" asChild>
<Link href="/admin/projects/pool">
<Layers className="mr-2 h-4 w-4" />
Project Pool
</Link>
</Button>
<Button variant="outline" onClick={() => setAiTagDialogOpen(true)}>
<Sparkles className="mr-2 h-4 w-4" />
AI Tags
@@ -600,7 +625,13 @@ export default function ProjectsPage() {
<TableCell>
<div>
<div className="flex items-center gap-2">
<p>{project.round?.name ?? '-'}</p>
{project.round ? (
<p>{project.round.name}</p>
) : (
<Badge variant="outline" className="text-xs text-amber-600 border-amber-300 bg-amber-50">
Unassigned
</Badge>
)}
{project.status === 'REJECTED' && (
<Badge variant="destructive" className="text-xs">
Eliminated
@@ -620,11 +651,7 @@ export default function ProjectsPage() {
</div>
</TableCell>
<TableCell>
<Badge
variant={statusColors[project.status ?? 'SUBMITTED'] || 'secondary'}
>
{(project.status ?? 'SUBMITTED').replace('_', ' ')}
</Badge>
<StatusBadge status={project.status ?? 'SUBMITTED'} />
</TableCell>
<TableCell className="relative z-10 text-right">
<DropdownMenu>
@@ -647,6 +674,18 @@ export default function ProjectsPage() {
Edit
</Link>
</DropdownMenuItem>
{!project.round && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
setProjectToAssign({ id: project.id, title: project.title })
setAssignDialogOpen(true)
}}
>
<FolderOpen className="mr-2 h-4 w-4" />
Assign to Round
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
@@ -697,14 +736,10 @@ export default function ProjectsPage() {
<CardTitle className="text-base line-clamp-2">
{project.title}
</CardTitle>
<Badge
variant={
statusColors[project.status ?? 'SUBMITTED'] || 'secondary'
}
<StatusBadge
status={project.status ?? 'SUBMITTED'}
className="shrink-0"
>
{(project.status ?? 'SUBMITTED').replace('_', ' ')}
</Badge>
/>
</div>
<CardDescription>{project.teamName}</CardDescription>
</div>
@@ -857,6 +892,59 @@ export default function ProjectsPage() {
</AlertDialogContent>
</AlertDialog>
{/* Assign to Round Dialog */}
<Dialog open={assignDialogOpen} onOpenChange={(open) => {
setAssignDialogOpen(open)
if (!open) { setProjectToAssign(null); setAssignRoundId('') }
}}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Assign to Round</DialogTitle>
<DialogDescription>
Assign &quot;{projectToAssign?.title}&quot; to a round.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Select Round</Label>
<Select value={assignRoundId} onValueChange={setAssignRoundId}>
<SelectTrigger>
<SelectValue placeholder="Choose a round..." />
</SelectTrigger>
<SelectContent>
{programs?.flatMap((p) =>
(p.rounds || []).map((r: { id: string; name: string }) => (
<SelectItem key={r.id} value={r.id}>
{p.name} {p.year} - {r.name}
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setAssignDialogOpen(false)}>
Cancel
</Button>
<Button
onClick={() => {
if (projectToAssign && assignRoundId) {
assignToRound.mutate({
projectIds: [projectToAssign.id],
roundId: assignRoundId,
})
}
}}
disabled={!assignRoundId || assignToRound.isPending}
>
{assignToRound.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Assign
</Button>
</div>
</DialogContent>
</Dialog>
{/* AI Tagging Dialog */}
<Dialog open={aiTagDialogOpen} onOpenChange={handleCloseTaggingDialog}>
<DialogContent className="sm:max-w-lg">