feat: name current view in role-switcher pill, add Mentors sidebar entry
- Switcher trigger now shows the current view's icon + label with a chevron (e.g. "Admin View ⌄") instead of the vague "Switch View". Dropdown adds a header, marks the current view with a checkmark, and lists each accessible alternative explicitly. - Adds a "Mentors" entry to the admin sidebar between Juries and Awards so the existing /admin/mentors page is reachable from nav.
This commit is contained in:
@@ -30,6 +30,7 @@ import {
|
||||
LogOut,
|
||||
ChevronRight,
|
||||
BookOpen,
|
||||
GraduationCap,
|
||||
Handshake,
|
||||
History,
|
||||
Trophy,
|
||||
@@ -85,6 +86,11 @@ const navigation: NavItem[] = [
|
||||
href: '/admin/juries',
|
||||
icon: Scale,
|
||||
},
|
||||
{
|
||||
name: 'Mentors',
|
||||
href: '/admin/mentors',
|
||||
icon: GraduationCap,
|
||||
},
|
||||
{
|
||||
name: 'Awards',
|
||||
href: '/admin/awards',
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useSession } from 'next-auth/react'
|
||||
import {
|
||||
ArrowRightLeft,
|
||||
Check,
|
||||
ChevronDown,
|
||||
Eye,
|
||||
Handshake,
|
||||
LayoutDashboard,
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -33,12 +36,8 @@ export const ROLE_SWITCH_OPTIONS: Record<string, RoleSwitchOption> = {
|
||||
AWARD_MASTER: { label: 'Award Master', path: '/award-master', icon: Trophy },
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of dashboards the current user can switch to,
|
||||
* excluding the one matching `currentBasePath`. Deduplicates admin paths
|
||||
* (SUPER_ADMIN + PROGRAM_ADMIN both go to /admin).
|
||||
*/
|
||||
export function useRoleSwitcher(currentBasePath: string): {
|
||||
currentOption: RoleSwitchOption | null
|
||||
switchableRoles: Array<[string, RoleSwitchOption]>
|
||||
isImpersonating: boolean
|
||||
} {
|
||||
@@ -46,32 +45,47 @@ export function useRoleSwitcher(currentBasePath: string): {
|
||||
const userRoles = (session?.user?.roles as UserRole[] | undefined) ?? []
|
||||
const isImpersonating = !!session?.user?.impersonating
|
||||
|
||||
const switchableRoles = useMemo(() => {
|
||||
return Object.entries(ROLE_SWITCH_OPTIONS)
|
||||
.filter(([role, opt]) => userRoles.includes(role as UserRole) && opt.path !== currentBasePath)
|
||||
const { currentOption, switchableRoles } = useMemo(() => {
|
||||
const accessible = Object.entries(ROLE_SWITCH_OPTIONS)
|
||||
.filter(([role]) => userRoles.includes(role as UserRole))
|
||||
.filter((entry, i, arr) => arr.findIndex(([, o]) => o.path === entry[1].path) === i)
|
||||
const current = accessible.find(([, opt]) => opt.path === currentBasePath)?.[1] ?? null
|
||||
const others = accessible.filter(([, opt]) => opt.path !== currentBasePath)
|
||||
return { currentOption: current, switchableRoles: others }
|
||||
}, [userRoles, currentBasePath])
|
||||
|
||||
return { switchableRoles, isImpersonating }
|
||||
return { currentOption, switchableRoles, isImpersonating }
|
||||
}
|
||||
|
||||
/**
|
||||
* Top-right "Switch View" pill. Hidden for single-role users and during
|
||||
* impersonation. Shows a popover listing alternate dashboards.
|
||||
* Top-right view-switcher pill. Trigger names the current view; the dropdown
|
||||
* lists alternative views. Hidden for single-view users and during impersonation.
|
||||
*/
|
||||
export function RoleSwitcherPill({ currentBasePath }: { currentBasePath: string }) {
|
||||
const { switchableRoles, isImpersonating } = useRoleSwitcher(currentBasePath)
|
||||
if (switchableRoles.length === 0 || isImpersonating) return null
|
||||
const { currentOption, switchableRoles, isImpersonating } = useRoleSwitcher(currentBasePath)
|
||||
if (switchableRoles.length === 0 || isImpersonating || !currentOption) return null
|
||||
|
||||
const CurrentIcon = currentOption.icon
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-1.5">
|
||||
<ArrowRightLeft className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">Switch View</span>
|
||||
<Button variant="outline" size="sm" className="gap-1.5" aria-label="Switch view">
|
||||
<CurrentIcon className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">{currentOption.label}</span>
|
||||
<ChevronDown className="h-3.5 w-3.5 opacity-60" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-44">
|
||||
<DropdownMenuContent align="end" className="min-w-52">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs font-normal">
|
||||
Switch to another view
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem disabled className="opacity-100">
|
||||
<CurrentIcon className="mr-2 h-4 w-4" />
|
||||
<span className="flex-1">{currentOption.label}</span>
|
||||
<Check className="text-muted-foreground h-3.5 w-3.5" />
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{switchableRoles.map(([role, opt]) => (
|
||||
<DropdownMenuItem key={role} asChild>
|
||||
<Link href={opt.path as Route} className="flex cursor-pointer items-center">
|
||||
|
||||
Reference in New Issue
Block a user