import { Link, useLocation, useNavigate } from '@tanstack/react-router' import { Avatar, AppShell, Burger, Divider, Group, Menu, NavLink, ScrollArea, Stack, Text, Title, UnstyledButton, } from '@pikku/mantine/core' import { useDisclosure } from '@mantine/hooks' import { useQueryClient } from '@tanstack/react-query' import { m, mKey } from '@/i18n/messages' import { useLocale } from '@/i18n/config' import { asI18n } from '@pikku/react' import { createContext, useContext, useEffect, useState } from 'react' import { signOut } from '../lib/auth' import { BrandMark } from './Brand' /** * Per-page title shown in the app header next to the brand mark. Pages opt in * via `usePageTitle(...)`; pages that don't call it leave the header showing * only the brand, so existing pages keep working unchanged. */ const PageTitleContext = createContext< ((title: string | undefined) => void) | null >(null) export function PageTitle({ title }: { title: string }) { usePageTitle(title) return null } export function usePageTitle(title: string | undefined) { const setTitle = useContext(PageTitleContext) useEffect(() => { // No-op safely if used outside the provider (e.g. unauthenticated pages). if (!setTitle) return setTitle(title) return () => setTitle(undefined) }, [setTitle, title]) } type Role = 'client' | 'admin' | 'owner' type NavItem = { to: string labelKey: string roles: Role[] } type Section = { titleKey: string items: NavItem[] } const SECTIONS: Section[] = [ { titleKey: 'common.nav.sections.workspace', items: [ { to: '/admin', labelKey: 'common.nav.items.dashboard', roles: ['admin', 'owner'] }, { to: '/admin/bookings', labelKey: 'common.nav.items.bookings', roles: ['admin', 'owner'] }, { to: '/admin/enquiries', labelKey: 'common.nav.items.enquiries', roles: ['admin', 'owner'] }, { to: '/kanban', labelKey: 'common.nav.items.kanban', roles: ['admin', 'owner'] }, ], }, { titleKey: 'common.nav.sections.public', items: [ { to: '/availability', labelKey: 'common.nav.items.availability', roles: ['client', 'admin', 'owner'] }, { to: '/enquiry', labelKey: 'common.nav.items.enquiry', roles: ['admin', 'owner'] }, { to: '/events', labelKey: 'common.nav.items.events', roles: ['client', 'admin', 'owner'] }, ], }, ] const initials = (name: string) => name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((p) => p[0]?.toUpperCase() ?? '') .join('') export function AppLayout({ user, children, }: { user: { name: string; role: string; email: string } children: React.ReactNode }) { useLocale() const [opened, { toggle, close }] = useDisclosure() const [pageTitle, setPageTitle] = useState(undefined) const location = useLocation() const navigate = useNavigate() const qc = useQueryClient() const role = user.role as Role const [logoutPending, setLogoutPending] = useState(false) const handleLogout = async () => { setLogoutPending(true) try { await signOut() } finally { await qc.invalidateQueries({ queryKey: ['me'] }) navigate({ to: '/login' }) } } return ( {m.common_brand_name()} {pageTitle && ( <> {asI18n(`${pageTitle}`)} )} {asI18n(`${user.name}`)} {mKey(`common.nav.role.${user.role}` as any)} {initials(user.name)} {asI18n(`${user.email}`)} {m.common_nav_logout()} {SECTIONS.map((section) => { const visible = section.items.filter((n) => n.roles.includes(role)) if (visible.length === 0) return null return ( {asI18n(`${mKey(section.titleKey as any)}`)} {visible.map((item) => { // '/admin' is the dashboard landing — match it exactly so it // doesn't stay highlighted across every /admin/* sub-page. const active = item.to === '/admin' ? location.pathname === '/admin' || location.pathname === '/admin/' : location.pathname.startsWith(item.to) return ( ) })} ) })} {m.common_brand_tagline()} {children} ) }