Platform-wide visual overhaul, team invites, analytics improvements, and deployment hardening

UI overhaul applying jury dashboard design patterns across all pages:
- Stat cards with border-l-4 accent + icon pills on admin, observer, mentor, applicant dashboards and reports
- Card section headers with color-coded icon pills throughout
- Hover lift effects (translate-y + shadow) on cards and list items
- Gradient progress bars (brand-teal to brand-blue) platform-wide
- AnimatedCard stagger animations on all dashboard sections
- Auth pages with gradient accent strip and polished icon containers
- EmptyState component upgraded with rounded icon pill containers
- Replaced AI-looking icons (Brain/Sparkles/Bot/Wand2/Cpu) with descriptive alternatives across 12 files
- Removed gradient overlay from jury dashboard header
- Quick actions restyled as card links with group hover effects

Backend improvements:
- Team member invite emails with account setup flow and notification logging
- Analytics routers accept edition-wide queries (programId) in addition to roundId
- Round detail endpoint returns inline progress data (eliminates extra getProgress call)
- Award voting endpoints parallelized with Promise.all
- Bulk invite supports optional sendInvitation flag
- AwardVote composite index migration for query performance

Infrastructure:
- Docker entrypoint with migration retry loop (configurable retries/delay)
- docker-compose pull_policy: always for automatic image refresh
- Simplified deploy/update scripts using docker compose up -d --pull always
- Updated deployment documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 13:20:52 +01:00
parent 98f4a957cc
commit ce4069bf92
59 changed files with 1949 additions and 913 deletions

View File

@@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { toast } from 'sonner'
import { Bot, Loader2, Zap, AlertCircle, RefreshCw, Brain } from 'lucide-react'
import { Cog, Loader2, Zap, AlertCircle, RefreshCw, SlidersHorizontal } from 'lucide-react'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -264,7 +264,7 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
<SelectItem key={model.id} value={model.id}>
<div className="flex items-center gap-2">
{model.isReasoning && (
<Brain className="h-3 w-3 text-purple-500" />
<SlidersHorizontal className="h-3 w-3 text-purple-500" />
)}
<span>{model.name}</span>
</div>
@@ -278,7 +278,7 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
<FormDescription>
{form.watch('ai_model')?.startsWith('o') ? (
<span className="flex items-center gap-1 text-purple-600">
<Brain className="h-3 w-3" />
<SlidersHorizontal className="h-3 w-3" />
Reasoning model - optimized for complex analysis tasks
</span>
) : (
@@ -323,7 +323,7 @@ export function AISettingsForm({ settings }: AISettingsFormProps) {
</>
) : (
<>
<Bot className="mr-2 h-4 w-4" />
<Cog className="mr-2 h-4 w-4" />
Save AI Settings
</>
)}

View File

@@ -15,7 +15,7 @@ import {
Zap,
TrendingUp,
Activity,
Brain,
SlidersHorizontal,
Filter,
Users,
Award,
@@ -26,7 +26,7 @@ const ACTION_ICONS: Record<string, typeof Zap> = {
ASSIGNMENT: Users,
FILTERING: Filter,
AWARD_ELIGIBILITY: Award,
MENTOR_MATCHING: Brain,
MENTOR_MATCHING: SlidersHorizontal,
}
const ACTION_LABELS: Record<string, string> = {
@@ -235,7 +235,7 @@ export function AIUsageCard() {
variant="outline"
className="flex items-center gap-2"
>
<Brain className="h-3 w-3" />
<SlidersHorizontal className="h-3 w-3" />
<span>{model}</span>
<span className="text-muted-foreground">
{(data as { costFormatted?: string }).costFormatted}

View File

@@ -11,7 +11,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Skeleton } from '@/components/ui/skeleton'
import {
Bot,
Cog,
Palette,
Mail,
HardDrive,
@@ -29,6 +29,7 @@ import {
} from 'lucide-react'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { AnimatedCard } from '@/components/shared/animated-container'
import { AISettingsForm } from './ai-settings-form'
import { AIUsageCard } from './ai-usage-card'
import { BrandingSettingsForm } from './branding-settings-form'
@@ -195,7 +196,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
</TabsTrigger>
{isSuperAdmin && (
<TabsTrigger value="ai" className="gap-2 shrink-0">
<Bot className="h-4 w-4" />
<Cog className="h-4 w-4" />
AI
</TabsTrigger>
)}
@@ -275,7 +276,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<TabsList className="flex flex-col items-stretch h-auto w-full bg-transparent p-0 gap-0.5">
{isSuperAdmin && (
<TabsTrigger value="ai" className="justify-start gap-2 w-full px-3 py-2 h-auto data-[state=active]:bg-muted">
<Bot className="h-4 w-4" />
<Cog className="h-4 w-4" />
AI
</TabsTrigger>
)}
@@ -308,6 +309,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
{isSuperAdmin && (
<TabsContent value="ai" className="space-y-6">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>AI Configuration</CardTitle>
@@ -319,11 +321,13 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<AISettingsForm settings={aiSettings} />
</CardContent>
</Card>
</AnimatedCard>
<AIUsageCard />
</TabsContent>
)}
<TabsContent value="tags">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -353,9 +357,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
</Button>
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
<TabsContent value="branding">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Platform Branding</CardTitle>
@@ -367,10 +373,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<BrandingSettingsForm settings={brandingSettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
{isSuperAdmin && (
<TabsContent value="email">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Email Configuration</CardTitle>
@@ -382,10 +390,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<EmailSettingsForm settings={emailSettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
)}
<TabsContent value="notifications">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Notification Email Settings</CardTitle>
@@ -397,10 +407,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<NotificationSettingsForm />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
{isSuperAdmin && (
<TabsContent value="storage">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>File Storage</CardTitle>
@@ -412,11 +424,13 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<StorageSettingsForm settings={storageSettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
)}
{isSuperAdmin && (
<TabsContent value="security">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Security Settings</CardTitle>
@@ -428,10 +442,12 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<SecuritySettingsForm settings={securitySettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
)}
<TabsContent value="defaults">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Default Settings</CardTitle>
@@ -443,9 +459,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<DefaultsSettingsForm settings={defaultsSettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
<TabsContent value="digest" className="space-y-6">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Digest Configuration</CardTitle>
@@ -457,9 +475,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<DigestSettingsSection settings={digestSettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
<TabsContent value="analytics" className="space-y-6">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Analytics & Reports</CardTitle>
@@ -471,9 +491,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<AnalyticsSettingsSection settings={analyticsSettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
<TabsContent value="audit" className="space-y-6">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Audit & Security</CardTitle>
@@ -485,9 +507,11 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<AuditSettingsSection settings={auditSecuritySettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
<TabsContent value="localization" className="space-y-6">
<AnimatedCard>
<Card>
<CardHeader>
<CardTitle>Localization</CardTitle>
@@ -499,6 +523,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
<LocalizationSettingsSection settings={localizationSettings} />
</CardContent>
</Card>
</AnimatedCard>
</TabsContent>
</div>{/* end content area */}
</div>{/* end lg:flex */}
@@ -506,7 +531,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
{/* Quick Links to sub-pages */}
<div className="grid gap-4 sm:grid-cols-2">
<Card>
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<LayoutTemplate className="h-4 w-4" />
@@ -528,7 +553,7 @@ export function SettingsContent({ initialSettings, isSuperAdmin = true }: Settin
</Card>
{isSuperAdmin && (
<Card>
<Card className="transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<Webhook className="h-4 w-4" />