import { useMemo } from 'react' import { NavLink, Navigate, Outlet, useNavigate, useLocation } from 'react-router-dom' import MoonDot from '../../components/MoonDot.jsx' import BrandLogo from '../../components/BrandLogo.jsx' import NotificationBell from '../../components/NotificationBell.jsx' import { useAuth } from '../../contexts/AuthContext.jsx' import { useSite } from '../../contexts/SiteContext.jsx' import { applyNavOverrides } from '../../lib/navOverrides.js' import { firstDestinationFor } from '../../lib/adminNav.js' import { useNavOverrides } from '../../lib/useNavOverrides.js' import { withModuleNav } from '../../modules/nav.js' import { useFeatureGate } from '../../modules/features.jsx' // Shared shell for the logged-in player portal. Uses the same sidebar shell as // Admin (icon nav, sticky content header, footer sign-out) so the two logged-in // experiences read as one app — the portal just carries fewer nav rows. // Small inline stroke icons (16px, currentColor) — same frame as AdminLayout. function Icon({ children, size = 16 }) { return ( ) } const IconGear = () => const IconShield = () => const IconBell = () => // The settings row's own icon: a bell would make the two rows read as the same // destination twice, which is exactly the confusion the split was meant to end. // Participation history (Phase 14a). A calendar rather than a trophy: the row // is every event this account attended, ranked or not, and most of them will // never have a result published against them at all. const IconCalendar = () => const IconBellGear = () => // Exported because Admin -> Navigation edits this list. It stays declared here; // the editor may only relabel, reorder and hide what it finds (§7). No CORE row // carries a gate — every player sees both — but an installed module's rows join // this list before the merge and may carry a `feature`, so the filter after it // is not dead code. // // "Characters" was the first row and left with the client half in slice 3; the // UO module registers it again at `/player/uo/characters`, in this position, // with `order: 0`. export const NAV = [ { to: '/account/events', label: 'Events', icon: IconCalendar }, { to: '/account/appeals', label: 'Appeals', icon: IconShield }, { to: '/account/notifications', label: 'Notifications', end: true, icon: IconBell }, { to: '/account/notifications/settings', label: 'Notification settings', icon: IconBellGear }, { to: '/account', label: 'Account', end: true, icon: IconGear }, ] // The sticky content header mirrors the active page. A module's pages are not // here and cannot be — core does not know what they are called — so they title // from their own nav row, the same rule AdminLayout's `moduleTitle` follows. const TITLES = { '/account': 'Account', '/account/appeals': 'Appeals', '/account/notifications': 'Notifications', '/account/notifications/settings': 'Notification settings', } function moduleTitle(baseNav, pathname) { return baseNav .filter((i) => i.moduleId && (pathname === i.to || pathname.startsWith(`${i.to}/`))) .sort((a, b) => b.to.length - a.to.length)[0]?.label } const navBtnBase = { textAlign: 'left', borderRadius: 8, padding: '10px 14px', fontFamily: 'var(--sans)', fontSize: '0.92rem', textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 10, transition: 'background .15s,color .15s', } export default function PlayerPortalLayout() { const { user, logout } = useAuth() const { siteTitle } = useSite() const navOverrides = useNavOverrides() const isVisible = useFeatureGate() const baseNav = useMemo(() => withModuleNav(NAV, 'player'), []) const nav = useMemo( () => applyNavOverrides(baseNav, navOverrides.nav_player).filter(isVisible), [baseNav, navOverrides.nav_player, isVisible], ) const navigate = useNavigate() const location = useLocation() const title = TITLES[location.pathname] || moduleTitle(baseNav, location.pathname) || 'Player Portal' async function signOut() { await logout() navigate('/account/login', { replace: true }) } return (
{/* Sidebar */} {/* Main */}

{title}

← Site
) } /** * What `/player` renders. * * It used to be `PlayerCharacters`, a UO page, which left the portal with no * index at all when the client half was extracted (slice 3). Rather than pick a * fixed destination or invent a core landing page, the index resolves to the * first row of the portal nav this viewer can actually reach — so with the UO * module installed a player still arrives at their characters, exactly as * before, and with nothing installed they arrive at Account. * * Resolved from the BASE nav, before overrides: where everybody lands is * behaviour, and an override is presentation (`firstDestinationFor`). `replace` * so the back button leaves the portal rather than bouncing off this redirect. * * The same question exists one area over — the admin index is a hardcoded * Dashboard — and if the two logged-in areas ever become one, this is the shape * that answers for both. Nothing here assumes a portal separate from admin. */ export function PlayerIndex() { const { user } = useAuth() const baseNav = useMemo(() => withModuleNav(NAV, 'player'), []) const to = firstDestinationFor(baseNav, user?.role, '/account') return }