Platform review round 2: audit logging migration, nav unification, DB indexes, and UI polish
- Migrate ~41 inline audit log calls to shared logAudit() utility across all routers - Add transaction-aware prisma parameter to logAudit() for atomic operations - Unify jury/mentor/observer navigation into shared RoleNav component - Add composite DB indexes (Evaluation, GracePeriod, AuditLog) for query performance - Fix profile page: consolidate dual save buttons, proper useEffect initialization - Enhance auth error page with MOPC branding and navigation - Improve observer dashboard with prominent read-only badge - Fix DI-3: fetch projects before bulk status update for accurate notifications - Remove unused aiBoost field from smart-assignment scoring - Add shared image-upload utility and structured logger module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,183 +1,37 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { signOut } from 'next-auth/react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import type { Route } from 'next'
|
||||
import { BookOpen, ClipboardList, Home, LogOut, Menu, Settings, User, X } from 'lucide-react'
|
||||
import { Logo } from '@/components/shared/logo'
|
||||
import { NotificationBell } from '@/components/shared/notification-bell'
|
||||
import { BookOpen, ClipboardList, Home } from 'lucide-react'
|
||||
import { RoleNav, type NavItem, type RoleNavUser } from '@/components/layouts/role-nav'
|
||||
|
||||
interface JuryNavProps {
|
||||
user: {
|
||||
name?: string | null
|
||||
email?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
const navigation = [
|
||||
const navigation: NavItem[] = [
|
||||
{
|
||||
name: 'Dashboard',
|
||||
href: '/jury' as const,
|
||||
href: '/jury',
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
name: 'My Assignments',
|
||||
href: '/jury/assignments' as const,
|
||||
href: '/jury/assignments',
|
||||
icon: ClipboardList,
|
||||
},
|
||||
{
|
||||
name: 'Learning Hub',
|
||||
href: '/jury/learning' as const,
|
||||
href: '/jury/learning',
|
||||
icon: BookOpen,
|
||||
},
|
||||
]
|
||||
|
||||
interface JuryNavProps {
|
||||
user: RoleNavUser
|
||||
}
|
||||
|
||||
export function JuryNav({ user }: JuryNavProps) {
|
||||
const pathname = usePathname()
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
||||
const { data: avatarUrl } = trpc.avatar.getUrl.useQuery()
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop header */}
|
||||
<header className="sticky top-0 z-40 border-b bg-card">
|
||||
<div className="container-app">
|
||||
<div className="flex h-16 items-center justify-between">
|
||||
{/* Logo */}
|
||||
<Logo showText textSuffix="Jury" />
|
||||
|
||||
{/* Desktop nav */}
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
{navigation.map((item) => {
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(item.href !== '/jury' && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* User menu & mobile toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<NotificationBell />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="gap-2 hidden sm:flex"
|
||||
size="sm"
|
||||
>
|
||||
<UserAvatar user={user} avatarUrl={avatarUrl} size="xs" />
|
||||
<span className="max-w-[120px] truncate">
|
||||
{user.name || user.email}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuItem disabled>
|
||||
<User className="mr-2 h-4 w-4" />
|
||||
{user.email}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href={"/settings/profile" as Route} className="flex cursor-pointer items-center">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Profile Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
className="text-destructive focus:text-destructive"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="md:hidden"
|
||||
aria-label={isMobileMenuOpen ? 'Close menu' : 'Open menu'}
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
>
|
||||
{isMobileMenuOpen ? (
|
||||
<X className="h-5 w-5" />
|
||||
) : (
|
||||
<Menu className="h-5 w-5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{isMobileMenuOpen && (
|
||||
<div className="border-t md:hidden">
|
||||
<nav className="container-app py-4 space-y-1">
|
||||
{navigation.map((item) => {
|
||||
const isActive =
|
||||
pathname === item.href ||
|
||||
(item.href !== '/jury' && pathname.startsWith(item.href))
|
||||
return (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<item.icon className="h-4 w-4" />
|
||||
{item.name}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
<div className="border-t pt-4 mt-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full justify-start text-destructive hover:text-destructive"
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
Sign out
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
</>
|
||||
<RoleNav
|
||||
navigation={navigation}
|
||||
roleName="Jury"
|
||||
user={user}
|
||||
basePath="/jury"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user