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:
@@ -238,7 +238,7 @@ export default function OnboardingPage() {
|
||||
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">
|
||||
<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" />
|
||||
{/* Progress indicator */}
|
||||
<div className="px-6 pt-6">
|
||||
|
||||
@@ -3,38 +3,59 @@
|
||||
import Link from 'next/link'
|
||||
import type { Route } from 'next'
|
||||
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 { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowLeft, ArrowRight, ClipboardList, Target } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { StatusBadge } from '@/components/shared/status-badge'
|
||||
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() {
|
||||
const { data: competitions, isLoading } = trpc.competition.getMyCompetitions.useQuery()
|
||||
export default function JuryAssignmentsPage() {
|
||||
const { data: assignments, isLoading } = trpc.assignment.myAssignments.useQuery({})
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-40" />
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Skeleton key={i} className="h-24 w-full rounded-xl" />
|
||||
))}
|
||||
</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 (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground">
|
||||
My Competitions
|
||||
My Assignments
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
View competitions and rounds you're assigned to
|
||||
Projects assigned to you for evaluation
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
@@ -45,65 +66,100 @@ export default function JuryCompetitionsPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!competitions || competitions.length === 0 ? (
|
||||
{roundGroups.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-12">
|
||||
<div className="rounded-2xl bg-brand-teal/10 p-4 mb-4">
|
||||
<ClipboardList className="h-8 w-8 text-brand-teal/60" />
|
||||
</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">
|
||||
You don't have any active competition assignments yet.
|
||||
You don't have any active assignments yet.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{competitions.map((competition) => {
|
||||
const activeRounds = competition.rounds?.filter(r => r.status !== 'ROUND_ARCHIVED') || []
|
||||
const totalRounds = competition.rounds?.length || 0
|
||||
<div className="space-y-6">
|
||||
{roundGroups.map(({ round, items }) => {
|
||||
const completed = (items ?? []).filter(
|
||||
(a) => a.evaluation?.status === 'SUBMITTED'
|
||||
).length
|
||||
const total = items?.length ?? 0
|
||||
|
||||
return (
|
||||
<Card key={competition.id} className="flex flex-col transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg">{competition.name}</CardTitle>
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{totalRounds} round{totalRounds !== 1 ? 's' : ''}
|
||||
<Card key={round.id}>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<CardTitle className="text-base">{round.name}</CardTitle>
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
{formatEnumLabel(round.roundType)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col space-y-4">
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<div className="space-y-2">
|
||||
{activeRounds.length > 0 ? (
|
||||
activeRounds.slice(0, 2).map((round) => (
|
||||
<Link
|
||||
key={round.id}
|
||||
href={`/jury/competitions/${round.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"
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<Target className="h-4 w-4 text-brand-teal shrink-0" />
|
||||
<span className="text-sm font-medium truncate">{round.name}</span>
|
||||
<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>
|
||||
<ArrowRight className="h-4 w-4 text-muted-foreground group-hover:text-brand-blue transition-colors shrink-0" />
|
||||
</Link>
|
||||
))
|
||||
</div>
|
||||
</CardHeader>
|
||||
<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'
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={assignment.id}
|
||||
href={`/jury/competitions/${round.id}/projects/${project.id}` as Route}
|
||||
className="block"
|
||||
>
|
||||
<div className="flex items-center gap-3 py-3 px-1 transition-colors hover:bg-muted/40 rounded-lg group">
|
||||
<ProjectLogo
|
||||
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>
|
||||
) : (
|
||||
<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>
|
||||
<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>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'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 { trpc } from '@/lib/trpc/client'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -42,6 +42,11 @@ function RemainingBadge() {
|
||||
}
|
||||
|
||||
export function JuryNav({ user }: JuryNavProps) {
|
||||
const { data: myAwards } = trpc.specialAward.getMyAwards.useQuery(
|
||||
undefined,
|
||||
{ refetchInterval: 60000 }
|
||||
)
|
||||
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
@@ -49,15 +54,19 @@ export function JuryNav({ user }: JuryNavProps) {
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: 'Competitions',
|
||||
name: 'Assignments',
|
||||
href: '/jury/competitions',
|
||||
icon: Layers,
|
||||
icon: ClipboardList,
|
||||
},
|
||||
...(myAwards && myAwards.length > 0
|
||||
? [
|
||||
{
|
||||
name: 'Awards',
|
||||
href: '/jury/awards',
|
||||
icon: Trophy,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Learning Hub',
|
||||
href: '/jury/learning',
|
||||
|
||||
@@ -182,7 +182,7 @@ export function ExpertiseSelect({
|
||||
)}
|
||||
|
||||
{/* 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)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([category, categoryTags]) => {
|
||||
@@ -261,6 +261,10 @@ export function ExpertiseSelect({
|
||||
<Check className="h-2 w-2 text-white" />
|
||||
))}
|
||||
</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>
|
||||
</Button>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user