UI fixes: onboarding scroll, expertise tags, jury assignments view

- Fix onboarding card overflow (overflow-hidden → overflow-x-hidden) so
  expertise step can scroll to submit button
- Reduce expertise category list height (max-h-64 → max-h-48)
- Add color dots to expertise tag options matching admin display
- Single-column layout for expertise tags (no truncation)
- Ocean background on onboarding (matches email template)
- Rewrite jury competitions page as assignment-centric grouped by round
- Conditionally show Awards nav item only when juror has award assignments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-17 11:48:14 +01:00
parent a006c6505c
commit f9016168e7
4 changed files with 128 additions and 59 deletions

View File

@@ -238,7 +238,7 @@ export default function OnboardingPage() {
return ( return (
<div className="absolute inset-0 -m-4 flex items-center justify-center p-4 md:p-8 bg-[#053d57] bg-[url('https://s3.monaco-opc.com/public/ocean.png')] bg-cover bg-center bg-no-repeat"> <div className="absolute inset-0 -m-4 flex items-center justify-center p-4 md:p-8 bg-[#053d57] bg-[url('https://s3.monaco-opc.com/public/ocean.png')] bg-cover bg-center bg-no-repeat">
<AnimatedCard> <AnimatedCard>
<Card className="w-full max-w-lg max-h-[85vh] overflow-y-auto overflow-hidden shadow-2xl"> <Card className="w-full max-w-lg max-h-[85vh] overflow-y-auto overflow-x-hidden shadow-2xl">
<div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" /> <div className="h-1 w-full bg-gradient-to-r from-brand-blue via-brand-teal to-brand-blue" />
{/* Progress indicator */} {/* Progress indicator */}
<div className="px-6 pt-6"> <div className="px-6 pt-6">

View File

@@ -3,38 +3,59 @@
import Link from 'next/link' import Link from 'next/link'
import type { Route } from 'next' import type { Route } from 'next'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ArrowLeft, ArrowRight, ClipboardList, Target } from 'lucide-react' import { StatusBadge } from '@/components/shared/status-badge'
import { toast } from 'sonner' import { ProjectLogo } from '@/components/shared/project-logo'
import {
ArrowLeft,
ArrowRight,
ClipboardList,
CheckCircle2,
Clock,
FileEdit,
} from 'lucide-react'
import { formatDateOnly, formatEnumLabel } from '@/lib/utils'
export default function JuryCompetitionsPage() { export default function JuryAssignmentsPage() {
const { data: competitions, isLoading } = trpc.competition.getMyCompetitions.useQuery() const { data: assignments, isLoading } = trpc.assignment.myAssignments.useQuery({})
if (isLoading) { if (isLoading) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Skeleton className="h-8 w-64" /> <Skeleton className="h-8 w-64" />
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="space-y-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-40" /> <Skeleton key={i} className="h-24 w-full rounded-xl" />
))} ))}
</div> </div>
</div> </div>
) )
} }
// Group assignments by round
const byRound = new Map<string, { round: { id: string; name: string; roundType: string; status: string; windowCloseAt: Date | null }; items: typeof assignments }>()
for (const a of assignments ?? []) {
if (!a.round) continue
if (!byRound.has(a.round.id)) {
byRound.set(a.round.id, { round: a.round, items: [] })
}
byRound.get(a.round.id)!.items!.push(a)
}
const roundGroups = Array.from(byRound.values())
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground"> <h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground">
My Competitions My Assignments
</h1> </h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1">
View competitions and rounds you&apos;re assigned to Projects assigned to you for evaluation
</p> </p>
</div> </div>
<Button variant="ghost" size="sm" asChild> <Button variant="ghost" size="sm" asChild>
@@ -45,65 +66,100 @@ export default function JuryCompetitionsPage() {
</Button> </Button>
</div> </div>
{!competitions || competitions.length === 0 ? ( {roundGroups.length === 0 ? (
<Card> <Card>
<CardContent className="flex flex-col items-center justify-center py-12"> <CardContent className="flex flex-col items-center justify-center py-12">
<div className="rounded-2xl bg-brand-teal/10 p-4 mb-4"> <div className="rounded-2xl bg-brand-teal/10 p-4 mb-4">
<ClipboardList className="h-8 w-8 text-brand-teal/60" /> <ClipboardList className="h-8 w-8 text-brand-teal/60" />
</div> </div>
<h2 className="text-xl font-semibold mb-2">No Competitions</h2> <h2 className="text-xl font-semibold mb-2">No Assignments</h2>
<p className="text-muted-foreground text-center max-w-md"> <p className="text-muted-foreground text-center max-w-md">
You don&apos;t have any active competition assignments yet. You don&apos;t have any active assignments yet.
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="space-y-6">
{competitions.map((competition) => { {roundGroups.map(({ round, items }) => {
const activeRounds = competition.rounds?.filter(r => r.status !== 'ROUND_ARCHIVED') || [] const completed = (items ?? []).filter(
const totalRounds = competition.rounds?.length || 0 (a) => a.evaluation?.status === 'SUBMITTED'
).length
const total = items?.length ?? 0
return ( return (
<Card key={competition.id} className="flex flex-col transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md"> <Card key={round.id}>
<CardHeader> <CardHeader className="pb-3">
<div className="flex items-start justify-between"> <div className="flex items-center justify-between">
<div> <div className="flex items-center gap-3">
<CardTitle className="text-lg">{competition.name}</CardTitle> <CardTitle className="text-base">{round.name}</CardTitle>
<Badge variant="secondary" className="text-xs">
{formatEnumLabel(round.roundType)}
</Badge>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{completed}/{total} completed
</span>
{round.windowCloseAt && (
<Badge variant="outline" className="text-xs gap-1">
<Clock className="h-3 w-3" />
Due {formatDateOnly(round.windowCloseAt)}
</Badge>
)}
</div> </div>
<Badge variant="secondary">
{totalRounds} round{totalRounds !== 1 ? 's' : ''}
</Badge>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex-1 flex flex-col space-y-4"> <CardContent>
<div className="divide-y">
{(items ?? []).map((assignment) => {
const project = assignment.project
const evalStatus = assignment.evaluation?.status
const isSubmitted = evalStatus === 'SUBMITTED'
const isDraft = evalStatus === 'DRAFT'
<div className="flex-1" /> return (
<div className="space-y-2">
{activeRounds.length > 0 ? (
activeRounds.slice(0, 2).map((round) => (
<Link <Link
key={round.id} key={assignment.id}
href={`/jury/competitions/${round.id}` as Route} href={`/jury/competitions/${round.id}/projects/${project.id}` as Route}
className="flex items-center justify-between p-3 rounded-lg border border-border/60 hover:border-brand-blue/30 hover:bg-brand-blue/5 transition-all group" className="block"
> >
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-3 py-3 px-1 transition-colors hover:bg-muted/40 rounded-lg group">
<Target className="h-4 w-4 text-brand-teal shrink-0" /> <ProjectLogo
<span className="text-sm font-medium truncate">{round.name}</span> project={project}
size="sm"
fallback="initials"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate group-hover:text-brand-blue transition-colors">
{project.title}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{[project.teamName, project.country].filter(Boolean).join(' \u00b7 ')}
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
{isSubmitted ? (
<Badge variant="success" className="gap-1">
<CheckCircle2 className="h-3 w-3" />
Submitted
</Badge>
) : isDraft ? (
<Badge variant="warning" className="gap-1">
<FileEdit className="h-3 w-3" />
Draft
</Badge>
) : (
<Badge variant="secondary" className="gap-1">
<Clock className="h-3 w-3" />
Pending
</Badge>
)}
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-brand-blue transition-colors" />
</div>
</div> </div>
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-brand-blue transition-colors shrink-0" />
</Link> </Link>
)) )
) : ( })}
<p className="text-sm text-muted-foreground text-center py-2">
No active rounds
</p>
)}
{activeRounds.length > 2 && (
<p className="text-xs text-muted-foreground text-center">
+{activeRounds.length - 2} more round{activeRounds.length - 2 !== 1 ? 's' : ''}
</p>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { BookOpen, Home, Trophy, Layers } from 'lucide-react' import { BookOpen, Home, Trophy, ClipboardList } from 'lucide-react'
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav' import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@@ -42,6 +42,11 @@ function RemainingBadge() {
} }
export function JuryNav({ user }: JuryNavProps) { export function JuryNav({ user }: JuryNavProps) {
const { data: myAwards } = trpc.specialAward.getMyAwards.useQuery(
undefined,
{ refetchInterval: 60000 }
)
const navigation: NavItem[] = [ const navigation: NavItem[] = [
{ {
name: 'Dashboard', name: 'Dashboard',
@@ -49,15 +54,19 @@ export function JuryNav({ user }: JuryNavProps) {
icon: Home, icon: Home,
}, },
{ {
name: 'Competitions', name: 'Assignments',
href: '/jury/competitions', href: '/jury/competitions',
icon: Layers, icon: ClipboardList,
},
{
name: 'Awards',
href: '/jury/awards',
icon: Trophy,
}, },
...(myAwards && myAwards.length > 0
? [
{
name: 'Awards',
href: '/jury/awards',
icon: Trophy,
},
]
: []),
{ {
name: 'Learning Hub', name: 'Learning Hub',
href: '/jury/learning', href: '/jury/learning',

View File

@@ -182,7 +182,7 @@ export function ExpertiseSelect({
)} )}
{/* Categories with expandable tag lists */} {/* Categories with expandable tag lists */}
<div className="space-y-2 max-h-64 overflow-y-auto pr-1"> <div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{Object.entries(filteredTagsByCategory) {Object.entries(filteredTagsByCategory)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([category, categoryTags]) => { .map(([category, categoryTags]) => {
@@ -261,6 +261,10 @@ export function ExpertiseSelect({
<Check className="h-2 w-2 text-white" /> <Check className="h-2 w-2 text-white" />
))} ))}
</div> </div>
<span
className="h-2.5 w-2.5 rounded-full shrink-0 mr-1.5"
style={{ backgroundColor: tag.color || '#6b7280' }}
/>
<span className="text-xs">{tag.name}</span> <span className="text-xs">{tag.name}</span>
</Button> </Button>
) )