2026-01-30 13:41:32 +01:00
|
|
|
'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 { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from '@/components/ui/dropdown-menu'
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
import type { Route } from 'next'
|
|
|
|
|
import { Home, BarChart3, Menu, X, LogOut, Eye, Settings } from 'lucide-react'
|
2026-01-30 13:41:32 +01:00
|
|
|
import { getInitials } from '@/lib/utils'
|
|
|
|
|
import { Logo } from '@/components/shared/logo'
|
|
|
|
|
|
|
|
|
|
interface ObserverNavProps {
|
|
|
|
|
user: {
|
|
|
|
|
name?: string | null
|
|
|
|
|
email?: string | null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const navigation = [
|
|
|
|
|
{
|
|
|
|
|
name: 'Dashboard',
|
|
|
|
|
href: '/observer' as const,
|
|
|
|
|
icon: Home,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Reports',
|
|
|
|
|
href: '/observer/reports' as const,
|
|
|
|
|
icon: BarChart3,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
export function ObserverNav({ user }: ObserverNavProps) {
|
|
|
|
|
const pathname = usePathname()
|
|
|
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<header className="sticky top-0 z-40 border-b bg-card">
|
|
|
|
|
<div className="container-app flex h-16 items-center justify-between">
|
|
|
|
|
{/* Logo */}
|
|
|
|
|
<Logo showText textSuffix="Observer" />
|
|
|
|
|
|
|
|
|
|
{/* Desktop Navigation */}
|
|
|
|
|
<nav className="hidden md:flex items-center gap-1">
|
|
|
|
|
{navigation.map((item) => {
|
|
|
|
|
const isActive =
|
|
|
|
|
pathname === item.href ||
|
|
|
|
|
(item.href !== '/observer' && 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 text-primary-foreground'
|
|
|
|
|
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<item.icon className="h-4 w-4" />
|
|
|
|
|
{item.name}
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
{/* User Menu */}
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button variant="ghost" className="gap-2">
|
|
|
|
|
<Avatar className="h-8 w-8">
|
|
|
|
|
<AvatarFallback className="text-xs">
|
|
|
|
|
{getInitials(user.name || user.email || 'O')}
|
|
|
|
|
</AvatarFallback>
|
|
|
|
|
</Avatar>
|
|
|
|
|
<span className="hidden sm:inline text-sm truncate max-w-[120px]">
|
|
|
|
|
{user.name || user.email}
|
|
|
|
|
</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="end" className="w-56">
|
|
|
|
|
<DropdownMenuItem disabled className="text-xs text-muted-foreground">
|
|
|
|
|
{user.email}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuSeparator />
|
Add profile settings page, mentor management, and S3 email logos
- Add universal /settings/profile page accessible to all roles with
avatar upload, bio, phone, password change, and account deletion
- Expand updateProfile endpoint to accept bio (metadataJson), phone,
and notification preference
- Add deleteAccount endpoint with password confirmation
- Add Profile Settings link to all nav components (admin, jury, mentor,
observer)
- Add /admin/mentors list page and /admin/mentors/[id] detail page for
mentor management
- Add Mentors nav item to admin sidebar
- Update email logo URLs to S3 (s3.monaco-opc.com/public/)
- Add ocean.png background image to email wrapper
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 19:57:12 +01:00
|
|
|
<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 />
|
2026-01-30 13:41:32 +01:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => signOut({ callbackUrl: '/login' })}
|
|
|
|
|
className="text-destructive focus:text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<LogOut className="mr-2 h-4 w-4" />
|
|
|
|
|
Sign out
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
|
|
|
|
|
{/* Mobile menu button */}
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
{/* Mobile Navigation */}
|
|
|
|
|
{isMobileMenuOpen && (
|
|
|
|
|
<div className="border-t md:hidden">
|
|
|
|
|
<nav className="container-app py-3 space-y-1">
|
|
|
|
|
{navigation.map((item) => {
|
|
|
|
|
const isActive =
|
|
|
|
|
pathname === item.href ||
|
|
|
|
|
(item.href !== '/observer' && 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 text-primary-foreground'
|
|
|
|
|
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<item.icon className="h-4 w-4" />
|
|
|
|
|
{item.name}
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
<div className="pt-2 border-t">
|
|
|
|
|
<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>
|
|
|
|
|
)
|
|
|
|
|
}
|