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

@@ -5,6 +5,7 @@ import Link from 'next/link'
import type { Route } from 'next'
import { usePathname } from 'next/navigation'
import { signOut } from 'next-auth/react'
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import {
@@ -33,7 +34,7 @@ import {
Trophy,
User,
MessageSquare,
Wand2,
LayoutTemplate,
} from 'lucide-react'
import { getInitials } from '@/lib/utils'
import { Logo } from '@/components/shared/logo'
@@ -41,6 +42,7 @@ import { EditionSelector } from '@/components/shared/edition-selector'
import { useEdition } from '@/contexts/edition-context'
import { UserAvatar } from '@/components/shared/user-avatar'
import { NotificationBell } from '@/components/shared/notification-bell'
import { LanguageSwitcher } from '@/components/shared/language-switcher'
import { useSession } from 'next-auth/react'
import { trpc } from '@/lib/trpc/client'
@@ -120,7 +122,7 @@ const adminNavigation: NavItem[] = [
{
name: 'Apply Page',
href: '/admin/programs',
icon: Wand2,
icon: LayoutTemplate,
activeMatch: 'apply-settings',
},
{
@@ -145,6 +147,7 @@ const roleLabels: Record<string, string> = {
export function AdminSidebar({ user }: AdminSidebarProps) {
const pathname = usePathname()
const tAuth = useTranslations('auth')
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const { status: sessionStatus } = useSession()
const isAuthenticated = sessionStatus === 'authenticated'
@@ -170,6 +173,7 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
<div className="fixed top-0 left-0 right-0 z-40 flex h-16 items-center justify-between border-b bg-card px-4 lg:hidden">
<Logo showText textSuffix="Admin" />
<div className="flex items-center gap-2">
<LanguageSwitcher />
<NotificationBell />
<Button
variant="ghost"
@@ -204,7 +208,8 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
{/* Logo + Notification */}
<div className="flex h-16 items-center justify-between border-b px-6">
<Logo showText textSuffix="Admin" />
<div className="hidden lg:block">
<div className="hidden lg:flex items-center gap-1">
<LanguageSwitcher />
<NotificationBell />
</div>
</div>
@@ -344,7 +349,7 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
className="flex cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-destructive focus:bg-destructive/10 focus:text-destructive"
>
<LogOut className="h-4 w-4" />
<span>Sign out</span>
<span>{tAuth('signOut')}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -2,35 +2,37 @@
import { Home, Users, FileText, MessageSquare } from 'lucide-react'
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
const navigation: NavItem[] = [
{
name: 'Dashboard',
href: '/applicant',
icon: Home,
},
{
name: 'Team',
href: '/applicant/team',
icon: Users,
},
{
name: 'Documents',
href: '/applicant/documents',
icon: FileText,
},
{
name: 'Mentor',
href: '/applicant/mentor',
icon: MessageSquare,
},
]
import { useTranslations } from 'next-intl'
interface ApplicantNavProps {
user: RoleNavUser
}
export function ApplicantNav({ user }: ApplicantNavProps) {
const t = useTranslations('nav')
const navigation: NavItem[] = [
{
name: t('dashboard'),
href: '/applicant',
icon: Home,
},
{
name: t('team'),
href: '/applicant/team',
icon: Users,
},
{
name: t('documents'),
href: '/applicant/documents',
icon: FileText,
},
{
name: t('mentoring'),
href: '/applicant/mentor',
icon: MessageSquare,
},
]
return (
<RoleNav
navigation={navigation}

View File

@@ -1,32 +1,10 @@
'use client'
import { BookOpen, ClipboardList, GitCompare, Home } from 'lucide-react'
import { BookOpen, ClipboardList, GitCompare, Home, Trophy } 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'
const navigation: NavItem[] = [
{
name: 'Dashboard',
href: '/jury',
icon: Home,
},
{
name: 'My Assignments',
href: '/jury/assignments',
icon: ClipboardList,
},
{
name: 'Compare',
href: '/jury/compare',
icon: GitCompare,
},
{
name: 'Learning Hub',
href: '/jury/learning',
icon: BookOpen,
},
]
import { useTranslations } from 'next-intl'
interface JuryNavProps {
user: RoleNavUser
@@ -65,6 +43,35 @@ function RemainingBadge() {
}
export function JuryNav({ user }: JuryNavProps) {
const t = useTranslations('nav')
const navigation: NavItem[] = [
{
name: t('dashboard'),
href: '/jury',
icon: Home,
},
{
name: t('assignments'),
href: '/jury/assignments',
icon: ClipboardList,
},
{
name: t('awards'),
href: '/jury/awards',
icon: Trophy,
},
{
name: t('compare'),
href: '/jury/compare',
icon: GitCompare,
},
{
name: t('learningHub'),
href: '/jury/learning',
icon: BookOpen,
},
]
return (
<RoleNav
navigation={navigation}

View File

@@ -2,30 +2,32 @@
import { BookOpen, Home, Users } from 'lucide-react'
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
const navigation: NavItem[] = [
{
name: 'Dashboard',
href: '/mentor',
icon: Home,
},
{
name: 'My Mentees',
href: '/mentor/projects',
icon: Users,
},
{
name: 'Resources',
href: '/mentor/resources',
icon: BookOpen,
},
]
import { useTranslations } from 'next-intl'
interface MentorNavProps {
user: RoleNavUser
}
export function MentorNav({ user }: MentorNavProps) {
const t = useTranslations('nav')
const navigation: NavItem[] = [
{
name: t('dashboard'),
href: '/mentor',
icon: Home,
},
{
name: t('myProjects'),
href: '/mentor/projects',
icon: Users,
},
{
name: t('learningHub'),
href: '/mentor/resources',
icon: BookOpen,
},
]
return (
<RoleNav
navigation={navigation}

View File

@@ -2,25 +2,27 @@
import { BarChart3, Home } from 'lucide-react'
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
const navigation: NavItem[] = [
{
name: 'Dashboard',
href: '/observer',
icon: Home,
},
{
name: 'Reports',
href: '/observer/reports',
icon: BarChart3,
},
]
import { useTranslations } from 'next-intl'
interface ObserverNavProps {
user: RoleNavUser
}
export function ObserverNav({ user }: ObserverNavProps) {
const t = useTranslations('nav')
const navigation: NavItem[] = [
{
name: t('dashboard'),
href: '/observer',
icon: Home,
},
{
name: t('reports'),
href: '/observer/reports',
icon: BarChart3,
},
]
return (
<RoleNav
navigation={navigation}

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { signOut, useSession } from 'next-auth/react'
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { UserAvatar } from '@/components/shared/user-avatar'
@@ -21,6 +22,7 @@ import { LogOut, Menu, Moon, Settings, Sun, User, X } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Logo } from '@/components/shared/logo'
import { NotificationBell } from '@/components/shared/notification-bell'
import { LanguageSwitcher } from '@/components/shared/language-switcher'
export type NavItem = {
name: string
@@ -49,6 +51,8 @@ function isNavItemActive(pathname: string, href: string, basePath: string): bool
export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: RoleNavProps) {
const pathname = usePathname()
const tCommon = useTranslations('common')
const tAuth = useTranslations('auth')
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
const { status: sessionStatus } = useSession()
const isAuthenticated = sessionStatus === 'authenticated'
@@ -107,6 +111,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
)}
</Button>
)}
<LanguageSwitcher />
<NotificationBell />
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -130,7 +135,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
<DropdownMenuItem asChild>
<Link href={"/settings/profile" as Route} className="flex cursor-pointer items-center">
<Settings className="mr-2 h-4 w-4" />
Profile Settings
{tCommon('settings')}
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
@@ -139,7 +144,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
className="text-destructive focus:text-destructive"
>
<LogOut className="mr-2 h-4 w-4" />
Sign out
{tAuth('signOut')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -191,7 +196,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge }: R
onClick={() => signOut({ callbackUrl: '/login' })}
>
<LogOut className="mr-2 h-4 w-4" />
Sign out
{tAuth('signOut')}
</Button>
</div>
</nav>