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,
|
LogOut,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
|
GraduationCap,
|
||||||
Handshake,
|
Handshake,
|
||||||
History,
|
History,
|
||||||
Trophy,
|
Trophy,
|
||||||
@@ -85,6 +86,11 @@ const navigation: NavItem[] = [
|
|||||||
href: '/admin/juries',
|
href: '/admin/juries',
|
||||||
icon: Scale,
|
icon: Scale,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Mentors',
|
||||||
|
href: '/admin/mentors',
|
||||||
|
icon: GraduationCap,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Awards',
|
name: 'Awards',
|
||||||
href: '/admin/awards',
|
href: '/admin/awards',
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useMemo } from 'react'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
import {
|
import {
|
||||||
ArrowRightLeft,
|
Check,
|
||||||
|
ChevronDown,
|
||||||
Eye,
|
Eye,
|
||||||
Handshake,
|
Handshake,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
@@ -16,6 +17,8 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { Button } from '@/components/ui/button'
|
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 },
|
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): {
|
export function useRoleSwitcher(currentBasePath: string): {
|
||||||
|
currentOption: RoleSwitchOption | null
|
||||||
switchableRoles: Array<[string, RoleSwitchOption]>
|
switchableRoles: Array<[string, RoleSwitchOption]>
|
||||||
isImpersonating: boolean
|
isImpersonating: boolean
|
||||||
} {
|
} {
|
||||||
@@ -46,32 +45,47 @@ export function useRoleSwitcher(currentBasePath: string): {
|
|||||||
const userRoles = (session?.user?.roles as UserRole[] | undefined) ?? []
|
const userRoles = (session?.user?.roles as UserRole[] | undefined) ?? []
|
||||||
const isImpersonating = !!session?.user?.impersonating
|
const isImpersonating = !!session?.user?.impersonating
|
||||||
|
|
||||||
const switchableRoles = useMemo(() => {
|
const { currentOption, switchableRoles } = useMemo(() => {
|
||||||
return Object.entries(ROLE_SWITCH_OPTIONS)
|
const accessible = Object.entries(ROLE_SWITCH_OPTIONS)
|
||||||
.filter(([role, opt]) => userRoles.includes(role as UserRole) && opt.path !== currentBasePath)
|
.filter(([role]) => userRoles.includes(role as UserRole))
|
||||||
.filter((entry, i, arr) => arr.findIndex(([, o]) => o.path === entry[1].path) === i)
|
.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])
|
}, [userRoles, currentBasePath])
|
||||||
|
|
||||||
return { switchableRoles, isImpersonating }
|
return { currentOption, switchableRoles, isImpersonating }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top-right "Switch View" pill. Hidden for single-role users and during
|
* Top-right view-switcher pill. Trigger names the current view; the dropdown
|
||||||
* impersonation. Shows a popover listing alternate dashboards.
|
* lists alternative views. Hidden for single-view users and during impersonation.
|
||||||
*/
|
*/
|
||||||
export function RoleSwitcherPill({ currentBasePath }: { currentBasePath: string }) {
|
export function RoleSwitcherPill({ currentBasePath }: { currentBasePath: string }) {
|
||||||
const { switchableRoles, isImpersonating } = useRoleSwitcher(currentBasePath)
|
const { currentOption, switchableRoles, isImpersonating } = useRoleSwitcher(currentBasePath)
|
||||||
if (switchableRoles.length === 0 || isImpersonating) return null
|
if (switchableRoles.length === 0 || isImpersonating || !currentOption) return null
|
||||||
|
|
||||||
|
const CurrentIcon = currentOption.icon
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" size="sm" className="gap-1.5">
|
<Button variant="outline" size="sm" className="gap-1.5" aria-label="Switch view">
|
||||||
<ArrowRightLeft className="h-3.5 w-3.5" />
|
<CurrentIcon className="h-3.5 w-3.5" />
|
||||||
<span className="hidden sm:inline">Switch View</span>
|
<span className="hidden sm:inline">{currentOption.label}</span>
|
||||||
|
<ChevronDown className="h-3.5 w-3.5 opacity-60" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</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]) => (
|
{switchableRoles.map(([role, opt]) => (
|
||||||
<DropdownMenuItem key={role} asChild>
|
<DropdownMenuItem key={role} asChild>
|
||||||
<Link href={opt.path as Route} className="flex cursor-pointer items-center">
|
<Link href={opt.path as Route} className="flex cursor-pointer items-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user