diff --git a/src/app/(admin)/layout.tsx b/src/app/(admin)/layout.tsx
index 7895f60..4de9796 100644
--- a/src/app/(admin)/layout.tsx
+++ b/src/app/(admin)/layout.tsx
@@ -2,6 +2,7 @@ import { prisma } from '@/lib/prisma'
import { requireRole } from '@/lib/auth-redirect'
import { AdminSidebar } from '@/components/layouts/admin-sidebar'
import { AdminEditionWrapper } from '@/components/layouts/admin-edition-wrapper'
+import { RoleSwitcherPill } from '@/components/layouts/role-switcher'
export default async function AdminLayout({
children,
@@ -34,6 +35,12 @@ export default async function AdminLayout({
{/* Spacer for mobile header */}
+ {/* Top-bar — hosts the RoleSwitcherPill so multi-role admins
+ can switch dashboards from the same screen position used on
+ every other layout. Pill auto-hides for single-role users. */}
+
+
+
{children}
diff --git a/src/components/layouts/admin-sidebar.tsx b/src/components/layouts/admin-sidebar.tsx
index 0bb9ca4..3597dd3 100644
--- a/src/components/layouts/admin-sidebar.tsx
+++ b/src/components/layouts/admin-sidebar.tsx
@@ -50,6 +50,7 @@ import { UserAvatar } from '@/components/shared/user-avatar'
import { NotificationBell } from '@/components/shared/notification-bell'
import { useSession } from 'next-auth/react'
import { trpc } from '@/lib/trpc/client'
+import { useRoleSwitcher } from './role-switcher'
interface AdminSidebarProps {
user: {
@@ -157,14 +158,6 @@ const roleLabels: Record = {
AWARD_MASTER: 'Award Master',
}
-// Role switcher config — maps roles to their dashboard views
-const ROLE_SWITCH_OPTIONS: Record = {
- JURY_MEMBER: { label: 'Jury View', path: '/jury', icon: Scale },
- MENTOR: { label: 'Mentor View', path: '/mentor', icon: Handshake },
- OBSERVER: { label: 'Observer View', path: '/observer', icon: Eye },
- AWARD_MASTER: { label: 'Award Master', path: '/award-master', icon: Trophy },
-}
-
export function AdminSidebar({ user }: AdminSidebarProps) {
const pathname = usePathname()
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
@@ -186,11 +179,10 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
const isSuperAdmin = user.role === 'SUPER_ADMIN'
const roleLabel = roleLabels[user.role || ''] || 'User'
- // Roles the user can switch to (non-admin roles they hold)
- const userRoles = (session?.user?.roles as UserRole[] | undefined) ?? []
- const switchableRoles = Object.entries(ROLE_SWITCH_OPTIONS).filter(
- ([role]) => userRoles.includes(role as UserRole)
- )
+ // Roles the user can switch to — shared logic. Admin sidebar dropdown
+ // no longer renders these; the RoleSwitcherPill in the layout's top-bar
+ // handles role switching for admins, matching every other dashboard.
+ const { switchableRoles } = useRoleSwitcher('/admin')
// Build dynamic admin nav with current edition's apply page
const dynamicAdminNav = adminNavigation.map((item) => {
@@ -374,46 +366,9 @@ export function AdminSidebar({ user }: AdminSidebarProps) {
- {switchableRoles.length > 0 && (
- <>
-
- {switchableRoles.length <= 2 ? (
- // Flat list for 1-2 roles
- switchableRoles.map(([, opt]) => (
-
-
-
- {opt.label}
-
-
- ))
- ) : (
- // Submenu for 3+ roles
-
-
-
- Switch View
-
-
- {switchableRoles.map(([, opt]) => (
-
-
-
- {opt.label}
-
-
- ))}
-
-
- )}
- >
- )}
+ {/* Role switcher items moved to the layout's top-bar
+ RoleSwitcherPill — single source of truth across all
+ dashboards. */}
diff --git a/src/components/layouts/role-nav.tsx b/src/components/layouts/role-nav.tsx
index a02861a..3e1c7ff 100644
--- a/src/components/layouts/role-nav.tsx
+++ b/src/components/layouts/role-nav.tsx
@@ -19,14 +19,14 @@ import {
import type { Route } from 'next'
import type { LucideIcon } from 'lucide-react'
import {
- LogOut, Menu, Moon, Settings, Sun, User, X, Trophy,
- LayoutDashboard, Scale, Handshake, Eye, ArrowRightLeft,
+ LogOut, Menu, Moon, Settings, Sun, User, X,
+ ArrowRightLeft,
ExternalLink as ExternalLinkIcon, HelpCircle, Mail,
} from 'lucide-react'
-import type { UserRole } from '@prisma/client'
import { useTheme } from 'next-themes'
import { Logo } from '@/components/shared/logo'
import { NotificationBell } from '@/components/shared/notification-bell'
+import { useRoleSwitcher, RoleSwitcherPill } from './role-switcher'
export type NavItem = {
name: string
@@ -54,16 +54,6 @@ type RoleNavProps = {
helpEmail?: string
}
-// Role switcher config — maps roles to their dashboard views
-const ROLE_SWITCH_OPTIONS: Record = {
- SUPER_ADMIN: { label: 'Admin View', path: '/admin', icon: LayoutDashboard },
- PROGRAM_ADMIN: { label: 'Admin View', path: '/admin', icon: LayoutDashboard },
- JURY_MEMBER: { label: 'Jury View', path: '/jury', icon: Scale },
- MENTOR: { label: 'Mentor View', path: '/mentor', icon: Handshake },
- OBSERVER: { label: 'Observer View', path: '/observer', icon: Eye },
- AWARD_MASTER: { label: 'Award Master', path: '/award-master', icon: Trophy },
-}
-
function isNavItemActive(pathname: string, href: string, basePath: string): boolean {
return pathname === href || (href !== basePath && pathname.startsWith(href))
}
@@ -111,12 +101,8 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated])
- // Roles the user can switch to (excluding current view)
- const userRoles = (session?.user?.roles as UserRole[] | undefined) ?? []
- const switchableRoles = Object.entries(ROLE_SWITCH_OPTIONS)
- .filter(([role, opt]) => userRoles.includes(role as UserRole) && opt.path !== basePath)
- // Deduplicate admin paths (SUPER_ADMIN and PROGRAM_ADMIN both go to /admin)
- .filter((entry, i, arr) => arr.findIndex(([, o]) => o.path === entry[1].path) === i)
+ // Roles the user can switch to (excluding current view) — shared logic
+ const { switchableRoles } = useRoleSwitcher(basePath)
return (
@@ -200,6 +186,7 @@ export function RoleNav({ navigation, roleName, user, basePath, statusBadge, edi
)}
)}
+
diff --git a/src/components/layouts/role-switcher.tsx b/src/components/layouts/role-switcher.tsx
new file mode 100644
index 0000000..6aea070
--- /dev/null
+++ b/src/components/layouts/role-switcher.tsx
@@ -0,0 +1,86 @@
+'use client'
+
+import { useMemo } from 'react'
+import Link from 'next/link'
+import { useSession } from 'next-auth/react'
+import {
+ ArrowRightLeft,
+ Eye,
+ Handshake,
+ LayoutDashboard,
+ Scale,
+ Trophy,
+ type LucideIcon,
+} from 'lucide-react'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu'
+import { Button } from '@/components/ui/button'
+import type { Route } from 'next'
+import type { UserRole } from '@prisma/client'
+
+export type RoleSwitchOption = { label: string; path: string; icon: LucideIcon }
+
+export const ROLE_SWITCH_OPTIONS: Record = {
+ SUPER_ADMIN: { label: 'Admin View', path: '/admin', icon: LayoutDashboard },
+ PROGRAM_ADMIN: { label: 'Admin View', path: '/admin', icon: LayoutDashboard },
+ JURY_MEMBER: { label: 'Jury View', path: '/jury', icon: Scale },
+ MENTOR: { label: 'Mentor View', path: '/mentor', icon: Handshake },
+ OBSERVER: { label: 'Observer View', path: '/observer', icon: Eye },
+ 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): {
+ switchableRoles: Array<[string, RoleSwitchOption]>
+ isImpersonating: boolean
+} {
+ const { data: session } = useSession()
+ 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)
+ .filter((entry, i, arr) => arr.findIndex(([, o]) => o.path === entry[1].path) === i)
+ }, [userRoles, currentBasePath])
+
+ return { switchableRoles, isImpersonating }
+}
+
+/**
+ * Top-right "Switch View" pill. Hidden for single-role users and during
+ * impersonation. Shows a popover listing alternate dashboards.
+ */
+export function RoleSwitcherPill({ currentBasePath }: { currentBasePath: string }) {
+ const { switchableRoles, isImpersonating } = useRoleSwitcher(currentBasePath)
+ if (switchableRoles.length === 0 || isImpersonating) return null
+
+ return (
+
+
+
+
+
+ {switchableRoles.map(([role, opt]) => (
+
+
+
+ {opt.label}
+
+
+ ))}
+
+
+ )
+}