import { A, useLocation, useNavigate, useSearchParams } from '@solidjs/router'; import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX, } from 'solid-js'; import AdminSidebar from './AdminSidebar'; import { isExternalIdentity } from '~/lib/admin-auth'; import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session'; import { Bell, Search, Settings, LogOut, User, ChevronDown } from 'lucide-solid'; // ── Types ──────────────────────────────────────────────────────────────────── type Tab = { href: string; label: string; exact?: boolean }; type SearchResult = { id: string; title: string; subtitle: string; href: string }; type SearchGroup = { label: string; viewAllHref: string; results: SearchResult[] }; // ── Tab sets ───────────────────────────────────────────────────────────────── const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [ { prefixes: ['/admin/roles'], tabs: [ { href: '/admin/roles', label: 'Roles', exact: true }, { href: '/admin/roles/create', label: 'Create Role' }, { href: '/admin/roles/templates', label: 'View Roles' }, ], }, { prefixes: ['/admin/runtime-roles'], tabs: [ { href: '/admin/runtime-roles', label: 'Roles', exact: true }, { href: '/admin/runtime-roles/new', label: 'Create Role' }, { href: '/admin/role-ui-configs', label: 'View Roles' }, ], }, ]; // ── Global search ───────────────────────────────────────────────────────────── const SEARCH_MODULES = [ { label: 'Users', viewAllHref: '/admin/users', api: '/api/gateway/api/admin/users', listKeys: ['users', 'items'], titleKeys: ['full_name', 'name'], subtitleKeys: ['email', 'phone'], detailBase: '/admin/users', }, { label: 'Companies', viewAllHref: '/admin/company', api: '/api/gateway/api/admin/companies', listKeys: ['companies', 'items'], titleKeys: ['name', 'companyName'], subtitleKeys: ['email', 'phone'], detailBase: '/admin/company', }, { label: 'Employees', viewAllHref: '/admin/employees', api: '/api/gateway/api/admin/employees', listKeys: ['employees', 'items'], titleKeys: ['full_name', 'name'], subtitleKeys: ['email', 'department_name'], detailBase: '/admin/employees', }, { label: 'Jobs', viewAllHref: '/admin/jobs', api: '/api/gateway/api/admin/jobs', listKeys: ['jobs', 'items'], titleKeys: ['title', 'name'], subtitleKeys: ['status', 'company_name'], detailBase: '/admin/jobs', }, { label: 'Leads', viewAllHref: '/admin/leads', api: '/api/gateway/api/admin/leads', listKeys: ['leads', 'items'], titleKeys: ['name', 'full_name'], subtitleKeys: ['email', 'status'], detailBase: '/admin/leads', }, { label: 'Candidates', viewAllHref: '/admin/candidate', api: '/api/gateway/api/admin/candidates', listKeys: ['candidates', 'items'], titleKeys: ['full_name', 'name'], subtitleKeys: ['email', 'phone'], detailBase: '/admin/candidate', }, ]; function pickStr(obj: Record, keys: string[]): string { for (const k of keys) if (obj[k]) return String(obj[k]); return '—'; } function extractList(data: any, keys: string[]): any[] { if (Array.isArray(data)) return data; for (const k of keys) if (Array.isArray(data[k])) return data[k]; return []; } function GlobalSearch() { const [query, setQuery] = createSignal(''); const [open, setOpen] = createSignal(false); const [groups, setGroups] = createSignal([]); const [searching, setSearching] = createSignal(false); let wrapRef!: HTMLDivElement; let timer: ReturnType; const doSearch = async (q: string) => { const trimmed = q.trim(); if (trimmed.length < 2) { setGroups([]); setOpen(false); return; } setSearching(true); const settled = await Promise.allSettled( SEARCH_MODULES.map(async (mod) => { const res = await fetch( `${mod.api}?search=${encodeURIComponent(trimmed)}&limit=4`, ).catch(() => null); if (!res?.ok) return null; const data = await res.json().catch(() => null); if (!data) return null; const items = extractList(data, mod.listKeys).slice(0, 4); if (!items.length) return null; return { label: mod.label, viewAllHref: mod.viewAllHref, results: items.map((item: any) => ({ id: item.id, title: pickStr(item, mod.titleKeys), subtitle: pickStr(item, mod.subtitleKeys), href: `${mod.detailBase}/${item.id}`, })), } satisfies SearchGroup; }), ); setGroups(settled.flatMap((r) => (r.status === 'fulfilled' && r.value ? [r.value] : []))); setOpen(true); setSearching(false); }; const handleInput = (val: string) => { setQuery(val); clearTimeout(timer); if (val.trim().length < 2) { setGroups([]); setOpen(false); return; } timer = setTimeout(() => doSearch(val), 400); }; const close = () => { setOpen(false); setQuery(''); setGroups([]); }; const onOutside = (e: MouseEvent) => { if (!wrapRef.contains(e.target as Node)) setOpen(false); }; onMount(() => document.addEventListener('mousedown', onOutside)); onCleanup(() => document.removeEventListener('mousedown', onOutside)); return (
{/* Search icon */} handleInput(e.currentTarget.value)} onFocus={() => groups().length > 0 && setOpen(true)} onKeyDown={(e) => e.key === 'Escape' && close()} class="h-10 w-full rounded-xl border border-gray-200 bg-gray-50 pl-10 pr-4 text-sm text-gray-700 placeholder:text-gray-400 transition-colors focus:border-gray-300 focus:bg-white focus:outline-none" /> {/* Results dropdown */} 0}> {/* No results */} = 2 && groups().length === 0}>

No results for "{query()}"

); } // ── Notification bell ───────────────────────────────────────────────────────── function NotificationBell(props: { count: number }) { const [open, setOpen] = createSignal(false); let ref!: HTMLDivElement; const onOutside = (e: MouseEvent) => { if (!ref.contains(e.target as Node)) setOpen(false); }; onMount(() => document.addEventListener('mousedown', onOutside)); onCleanup(() => document.removeEventListener('mousedown', onOutside)); return (
Notifications 0}> {props.count} new

Real-time notifications

Connecting to live feed…

); } // ── User menu ───────────────────────────────────────────────────────────────── function UserMenu(props: { name: string; initials: string; onLogout: () => void }) { const [open, setOpen] = createSignal(false); let ref!: HTMLDivElement; const onOutside = (e: MouseEvent) => { if (!ref.contains(e.target as Node)) setOpen(false); }; onMount(() => document.addEventListener('mousedown', onOutside)); onCleanup(() => document.removeEventListener('mousedown', onOutside)); return (
); } // ── Tabs ────────────────────────────────────────────────────────────────────── function ShowTabs(props: { tabs: Tab[]; isTabActive: (tab: Tab) => boolean; setTabsTrackEl: (el: HTMLDivElement) => void; setTabRefs: (fn: (prev: Record) => Record) => void; tabIndicator: () => { left: number; width: number; ready: boolean }; }) { if (props.tabs.length === 0) return null; return (
{(tab) => ( props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))} aria-current={props.isTabActive(tab) ? 'page' : undefined} class={`px-4 pb-3 pt-3 text-[14px] font-semibold transition-colors ${ props.isTabActive(tab) ? 'text-[#fd6216]' : 'text-slate-500 hover:text-slate-800' }`} > {tab.label} )}
); } // ── Main shell ──────────────────────────────────────────────────────────────── export default function AdminShell(props: { children: JSX.Element }) { const location = useLocation(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [checkedSession, setCheckedSession] = createSignal(false); const [adminName, setAdminName] = createSignal('Admin User'); const [sidebarOpen, setSidebarOpen] = createSignal(false); const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); const [notifCount] = createSignal(0); // wired in Phase 2 (SSE) const [tabsTrackEl, setTabsTrackEl] = createSignal(); const [tabRefs, setTabRefs] = createSignal>({}); const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false }); // ── Tab logic ────────────────────────────────────────────────────────────── const tabs = createMemo(() => { const path = location.pathname; for (const set of TAB_SETS) { if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) return set.tabs; } return []; }); const isTabActive = (tab: Tab) => tab.exact ? location.pathname === tab.href : location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`); const refreshTabIndicator = () => { const activeTab = tabs().find((tab) => isTabActive(tab)); const track = tabsTrackEl(); if (!activeTab || !track) { setTabIndicator((p) => ({ ...p, ready: false })); return; } const el = tabRefs()[activeTab.href]; if (!el) return; setTabIndicator({ left: el.offsetLeft, width: el.offsetWidth, ready: true }); }; createEffect(() => { tabs(); location.pathname; requestAnimationFrame(refreshTabIndicator); }); onMount(() => { window.addEventListener('resize', refreshTabIndicator); onCleanup(() => window.removeEventListener('resize', refreshTabIndicator)); const isLocalDev = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); const isPreview = searchParams._preview === '1' || (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1'); if (isPreview || isLocalDev) { if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1'); setAdminSession(); setCheckedSession(true); return; } const verify = async () => { if (!hasAdminSession()) { navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true }); return; } try { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const response = await fetch('/api/gateway/users/auth/me', { method: 'GET', headers: { Accept: 'application/json', 'x-portal-target': 'admin', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }); const payload = await response.json().catch(() => ({})); if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized'); if (payload?.full_name) setAdminName(payload.full_name); setCheckedSession(true); } catch { clearAdminSession(); navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true }); } }; void verify(); }); const onLogout = async () => { await fetch('/api/gateway/users/auth/logout', { method: 'POST', headers: { Accept: 'application/json', 'x-portal-target': 'admin' }, credentials: 'include', }).catch(() => {}); clearAdminSession(); if (typeof sessionStorage !== 'undefined') { sessionStorage.removeItem('nxtgauge_admin_access_token'); sessionStorage.removeItem('nxtgauge_admin_preview'); } navigate('/login', { replace: true }); }; const adminInitials = createMemo(() => { const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean); if (parts.length === 0) return 'U'; if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); }); // Sidebar width — synced between header logo section and sidebar const sidebarW = () => (sidebarCollapsed() ? 'w-[72px]' : 'w-64'); return (
{/* ── Header ── */}
{/* Logo section — same width as sidebar */} {/* Search bar */}
{/* Mobile menu button */} {/* Right: Bell + Gear + User */}
{/* ── Body ── */}

Checking session…

} >
{/* Mobile overlay */}
setSidebarOpen(false)} /> {/* Sidebar */}
setSidebarCollapsed((v) => !v)} onNavigate={() => setSidebarOpen(false)} adminName={adminName()} adminInitials={adminInitials()} />
{/* Main content */}
{props.children}
); }