ui(step-0): rewrite sidebar/shell to match reference admin panel

- AdminSidebar: flat nav list, orange right-border active indicator,
  ACTIVE pill badge, ChevronLeft collapse (rotates 180° when collapsed)
- AdminShell: h-16 header with logo+title left, Bell+avatar+logout right,
  sidebar collapse state, bg-gray-50 main area
- app.css: shared classes — data-table (navy header), table-card,
  btn-primary (navy), btn-secondary, search-input, action-btn,
  status-badge variants, page-title

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-24 04:25:17 +01:00
parent ab15466368
commit c44152154f
3 changed files with 373 additions and 193 deletions

View file

@ -88,6 +88,173 @@ body {
.auth-inline-msg { margin-top: 12px; }
.hint { margin: 6px 0 0; color: #64748b; font-size: 12px; }
/* ===== Shared Page Components ===== */
/* Page header */
.page-title { font-size: 1.5rem; font-weight: 700; color: #0a1d37; line-height: 1.2; }
.page-subtitle { font-size: 0.875rem; color: #64748b; margin-top: 4px; }
/* Data table */
.data-table { width: 100%; border-collapse: collapse; }
.data-table thead th {
background: #0a1d37;
color: rgba(255,255,255,0.9);
font-size: 0.75rem;
font-weight: 700;
text-align: left;
padding: 11px 14px;
white-space: nowrap;
user-select: none;
}
.data-table thead th:first-child { border-radius: 0; }
.data-table thead th:last-child { border-radius: 0; }
.data-table tbody td {
padding: 12px 14px;
font-size: 0.8125rem;
color: #0f172a;
vertical-align: middle;
border-bottom: 1px solid #f1f5f9;
}
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:hover td { background: #fafbff; }
.data-table-empty {
text-align: center;
padding: 32px 16px;
font-size: 0.875rem;
color: #94a3b8;
}
/* Table card wrapper */
.table-card {
background: #fff;
border-radius: 12px;
border: 1px solid #e2e8f0;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
}
/* Sort controls row */
.sort-controls {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid #f1f5f9;
}
.sort-select {
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
color: #0f172a;
font-size: 0.8125rem;
padding: 7px 28px 7px 10px;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
cursor: pointer;
}
.sort-select:focus { outline: none; border-color: #0a1d37; box-shadow: 0 0 0 3px rgba(10,29,55,0.08); }
/* Primary button (navy) */
.btn-primary {
display: inline-flex;
align-items: center;
gap: 6px;
background: #0a1d37;
color: #fff;
font-size: 0.875rem;
font-weight: 600;
padding: 9px 18px;
border-radius: 10px;
border: none;
cursor: pointer;
transition: background 150ms, box-shadow 150ms;
text-decoration: none;
}
.btn-primary:hover { background: #0f2a4e; box-shadow: 0 4px 12px rgba(10,29,55,0.25); }
/* Secondary button */
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 6px;
background: #fff;
color: #0a1d37;
font-size: 0.875rem;
font-weight: 600;
padding: 9px 18px;
border-radius: 10px;
border: 1px solid #e2e8f0;
cursor: pointer;
transition: background 150ms, border-color 150ms;
text-decoration: none;
}
.btn-secondary:hover { background: #f8fafc; border-color: #cbd5e1; }
/* Search input */
.search-input {
width: 100%;
max-width: 320px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
color: #0f172a;
font-size: 0.8125rem;
padding: 8px 12px;
transition: border-color 160ms, box-shadow 160ms;
}
.search-input::placeholder { color: #94a3b8; }
.search-input:focus { outline: none; border-color: rgba(10,29,55,0.45); box-shadow: 0 0 0 3px rgba(10,29,55,0.08); }
/* Tab bar (orange underline style) */
.tab-bar { display: flex; gap: 4px; border-bottom: 1px solid #e2e8f0; margin-bottom: 20px; }
.tab-link {
padding: 10px 16px;
font-size: 0.875rem;
font-weight: 600;
color: #64748b;
text-decoration: none;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 150ms, border-color 150ms;
}
.tab-link:hover { color: #0a1d37; }
.tab-link[aria-current='page'] { color: #fd6216; border-bottom-color: #fd6216; }
/* Action icon buttons in tables */
.action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
width: 32px;
border-radius: 7px;
border: 1px solid #e2e8f0;
background: #fff;
color: #475569;
cursor: pointer;
transition: background 120ms, color 120ms, border-color 120ms;
}
.action-btn:hover { background: #f8fafc; color: #0a1d37; border-color: #cbd5e1; }
.action-btn.danger:hover { background: #fff1f2; color: #e11d48; border-color: #fecdd3; }
/* Status badge */
.status-badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 2px 10px;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-active { background: #dcfce7; color: #166534; }
.status-pending { background: #fef9c3; color: #854d0e; }
.status-draft { background: #f1f5f9; color: #475569; }
.status-error { background: #fee2e2; color: #991b1b; }
/* ===== Admin Shell + Shared UI ===== */
.admin-shell-root {
background:

View file

@ -3,6 +3,7 @@ import { For, createEffect, createMemo, createSignal, onCleanup, onMount, type J
import AdminSidebar from './AdminSidebar';
import { isExternalIdentity } from '~/lib/admin-auth';
import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session';
import { Bell } from 'lucide-solid';
type Tab = { href: string; label: string; exact?: boolean };
@ -25,43 +26,6 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [
},
];
function IconBell() {
return (
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
);
}
function IconSearch() {
return (
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
<circle cx="11" cy="11" r="7.5" />
<path d="m20 20-3.8-3.8" />
</svg>
);
}
function IconHelp() {
return (
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="9" />
<path d="M9.3 9a2.8 2.8 0 1 1 4.9 2c-.8.9-1.7 1.4-1.7 2.5" />
<path d="M12 17h.01" />
</svg>
);
}
function IconCog() {
return (
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 15.5A3.5 3.5 0 1 0 12 8.5a3.5 3.5 0 0 0 0 7Z" />
<path d="M19.4 15a1.6 1.6 0 0 0 .3 1.7l.1.1a2 2 0 0 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-1.7-.3 1.6 1.6 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.2a1.6 1.6 0 0 0-1-1.5 1.6 1.6 0 0 0-1.7.3l-.1.1a2 2 0 0 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0 .3-1.7 1.6 1.6 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.2a1.6 1.6 0 0 0 1.5-1 1.6 1.6 0 0 0-.3-1.7l-.1-.1a2 2 0 0 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.7.3h.1a1.6 1.6 0 0 0 .9-1.5V3a2 2 0 1 1 4 0v.2a1.6 1.6 0 0 0 .9 1.5h.1a1.6 1.6 0 0 0 1.7-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.7v.1a1.6 1.6 0 0 0 1.5.9H21a2 2 0 0 1 0 4h-.2a1.6 1.6 0 0 0-1.5 1Z" />
</svg>
);
}
export default function AdminShell(props: { children: JSX.Element }) {
const location = useLocation();
const navigate = useNavigate();
@ -69,6 +33,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
const [checkedSession, setCheckedSession] = createSignal(false);
const [adminName, setAdminName] = createSignal('Admin');
const [sidebarOpen, setSidebarOpen] = createSignal(false);
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
const [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>();
const [tabRefs, setTabRefs] = createSignal<Record<string, HTMLAnchorElement>>({});
const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false });
@ -81,7 +46,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
return [];
});
const isTabActive = (tab: Tab) => (tab.exact ? location.pathname === tab.href : location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`));
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));
@ -107,13 +75,16 @@ export default function AdminShell(props: { children: JSX.Element }) {
onCleanup(() => window.removeEventListener('resize', onResize));
const isLocalDev =
typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
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');
(typeof sessionStorage !== 'undefined' &&
sessionStorage.getItem('nxtgauge_admin_preview') === '1');
if (isPreview || isLocalDev) {
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1');
if (typeof sessionStorage !== 'undefined')
sessionStorage.setItem('nxtgauge_admin_preview', '1');
setAdminSession();
setCheckedSession(true);
return;
@ -126,7 +97,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
return;
}
try {
const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
const accessToken =
typeof sessionStorage !== 'undefined'
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
: '';
const response = await fetch('/api/gateway/users/auth/me', {
method: 'GET',
headers: {
@ -166,73 +140,100 @@ export default function AdminShell(props: { children: JSX.Element }) {
const adminInitials = createMemo(() => {
const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean);
if (parts.length === 0) return 'AD';
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase();
if (parts.length === 0) return 'U';
if (parts.length === 1) return parts[0].slice(0, 1).toUpperCase();
return `${parts[0][0] || ''}`.toUpperCase();
});
const sidebarWidth = () => (sidebarCollapsed() ? 'w-20' : 'w-64');
return (
<div class="min-h-screen bg-[#f0f1f6]">
<header class="fixed inset-x-0 top-0 z-50 border-b border-[#d8dbe3] bg-white">
<div class="flex h-[64px] items-center justify-between px-6">
<div class="flex min-w-0 items-center gap-6">
<div class="min-h-screen bg-white">
{/* ── Header ── */}
<header class="fixed inset-x-0 top-0 z-50 flex h-16 w-full items-center justify-between border-b border-gray-200 bg-white px-6 shadow-sm">
{/* Left: logo + role title */}
<div class="flex items-center gap-8">
<A href="/admin" class="flex h-10 items-center">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-9 w-auto object-contain" />
</A>
<h1 class="text-base font-semibold text-gray-800">Super Admin</h1>
</div>
{/* Mobile menu button */}
<button
type="button"
class="inline-flex h-9 w-9 items-center justify-center rounded-lg text-slate-600 hover:bg-slate-100 lg:hidden"
onClick={() => setSidebarOpen((v) => !v)}
aria-label="Toggle sidebar"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" d="M4 7h16M4 12h16M4 17h16" />
</svg>
</button>
{/* Right: bell + avatar + logout */}
<div class="hidden items-center gap-6 lg:flex">
{/* Bell */}
<div class="relative">
<button
type="button"
class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 lg:hidden"
onClick={() => setSidebarOpen((v) => !v)}
aria-label="Toggle sidebar"
aria-label="Notifications"
class="relative rounded-full p-2 text-gray-500 transition-colors hover:bg-gray-100"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" d="M4 7h16M4 12h16M4 17h16" />
</svg>
<Bell size={20} />
</button>
<A href="/admin" class="flex shrink-0 items-center">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-12 w-auto object-contain" />
</A>
</div>
<div class="flex items-center gap-4">
<div class="hidden h-[40px] w-[560px] items-center gap-3 rounded-lg border border-[#daddE8] bg-[#f3f4f8] px-4 text-[13px] text-[#6a7285] lg:flex">
<IconSearch />
<span>Search system operations...</span>
</div>
<div class="hidden h-10 w-px bg-[#d9dde7] lg:block" />
<button type="button" aria-label="Notifications" class="relative inline-flex h-10 w-10 items-center justify-center rounded-full text-[#3e4559] hover:bg-white">
<span class="absolute right-2 top-2 h-1.5 w-1.5 rounded-full bg-[#fd6216]" />
<IconBell />
</button>
<button type="button" aria-label="Help" class="hidden h-10 w-10 items-center justify-center rounded-full text-[#3e4559] hover:bg-white lg:inline-flex">
<IconHelp />
</button>
<button type="button" aria-label="Settings" class="hidden h-10 w-10 items-center justify-center rounded-full text-[#3e4559] hover:bg-white lg:inline-flex">
<IconCog />
</button>
<div class="hidden h-10 w-px bg-[#d9dde7] lg:block" />
<div class="hidden items-center gap-2.5 lg:flex">
<div class="text-right leading-tight">
<p class="text-[14px] font-semibold text-[#0a1d37]">{adminName()}</p>
<p class="text-[11px] text-[#6b7280]">Administrator</p>
</div>
<div class="flex h-9 w-9 items-center justify-center overflow-hidden rounded-lg border border-[#d9dce7] bg-[#0a1d37] text-[13px] font-bold text-white">
{/* Avatar + name */}
<div class="flex items-center gap-2">
<button class="flex items-center gap-3 rounded-lg p-1 pr-2 transition-colors hover:bg-gray-100">
<div class="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full border border-orange-200 bg-orange-100 text-sm font-semibold text-orange-700">
{adminInitials()}
</div>
</div>
<div class="flex flex-col items-start">
<span class="mb-0.5 text-xs font-semibold leading-none text-gray-700">{adminName()}</span>
<span class="text-[10px] leading-none text-gray-500">Super Admin</span>
</div>
</button>
{/* Logout */}
<button
type="button"
onClick={onLogout}
class="rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600"
aria-label="Sign out"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</button>
</div>
</div>
</header>
{/* ── Body ── */}
{checkedSession() ? (
<div class="fixed inset-0 top-[64px] flex">
<div class="fixed inset-0 top-16 flex">
{/* Mobile overlay */}
<div
class={`absolute inset-0 z-20 bg-[#0a1d37]/35 transition-opacity lg:hidden ${sidebarOpen() ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
class={`absolute inset-0 z-20 bg-black/30 transition-opacity lg:hidden ${sidebarOpen() ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
onClick={() => setSidebarOpen(false)}
/>
<div class={`absolute inset-y-0 left-0 z-30 w-[268px] -translate-x-full transition-transform duration-200 lg:static lg:translate-x-0 ${sidebarOpen() ? 'translate-x-0' : ''}`}>
<AdminSidebar onNavigate={() => setSidebarOpen(false)} onLogout={onLogout} />
{/* Sidebar */}
<div
class={`absolute inset-y-0 left-0 z-30 -translate-x-full transition-transform duration-200 lg:static lg:translate-x-0 ${sidebarOpen() ? 'translate-x-0' : ''}`}
>
<AdminSidebar
collapsed={sidebarCollapsed()}
onToggle={() => setSidebarCollapsed((v) => !v)}
onNavigate={() => setSidebarOpen(false)}
onLogout={onLogout}
/>
</div>
<main class="scrollbar min-w-0 flex-1 overflow-y-auto bg-[#f3f4f8] px-7 pb-8 pt-7">
{/* Main */}
<main class="scrollbar min-w-0 flex-1 overflow-y-auto bg-gray-50 p-6">
<ShowTabs
tabs={tabs()}
isTabActive={isTabActive}
@ -244,10 +245,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
</main>
</div>
) : (
<div class="fixed inset-0 top-[64px] flex">
<div class="hidden w-[268px] border-r border-[#d7d8df] bg-white lg:block" />
<main class="flex flex-1 items-center justify-center bg-[#f3f4f8]">
<p class="text-sm text-gray-500">Checking session...</p>
<div class="fixed inset-0 top-16 flex">
<div class="hidden w-64 border-r border-slate-200 bg-[#fcfcfd] lg:block" />
<main class="flex flex-1 items-center justify-center bg-gray-50">
<p class="text-sm text-gray-400">Checking session</p>
</main>
</div>
)}
@ -265,15 +266,17 @@ function ShowTabs(props: {
if (props.tabs.length === 0) return null;
return (
<div ref={props.setTabsTrackEl} class="relative mb-6 flex items-center gap-1 border-b border-[#d8dbe3]">
<div ref={props.setTabsTrackEl} class="relative mb-6 flex items-center gap-1 border-b border-gray-200">
<For each={props.tabs}>
{(tab) => (
<A
href={tab.href}
ref={(el) => props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))}
aria-current={props.isTabActive(tab) ? 'page' : undefined}
class={`relative px-4 pb-3 pt-3 text-[13.5px] font-semibold transition-colors ${
props.isTabActive(tab) ? 'text-[#0a1d37]' : 'text-[#636b7f] hover:text-[#0a1d37]'
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}
@ -281,7 +284,7 @@ function ShowTabs(props: {
)}
</For>
<div
class={`absolute bottom-0 h-[2px] bg-[#0a1d37] transition-all duration-300 ease-out ${props.tabIndicator().ready ? 'opacity-100' : 'opacity-0'}`}
class={`absolute bottom-0 h-[2px] bg-[#fd6216] transition-all duration-300 ease-out ${props.tabIndicator().ready ? 'opacity-100' : 'opacity-0'}`}
style={{ left: `${props.tabIndicator().left}px`, width: `${props.tabIndicator().width}px` }}
/>
</div>

View file

@ -1,68 +1,63 @@
import { A, useLocation } from '@solidjs/router';
import { For, Show } from 'solid-js';
import { For, Show, createSignal } from 'solid-js';
import {
Bell,
Briefcase,
ClipboardList,
FileText,
FolderCog,
HandHelping,
LayoutGrid,
Percent,
Receipt,
Sparkles,
UserCircle2,
Users,
WalletCards,
LayoutGrid, Building2, Briefcase, Users, ShieldCheck, FileText,
LayoutDashboard, ClipboardList, UserRoundSearch, UserCircle,
Camera, Palette, BookOpen, Code2, BriefcaseBusiness, HandHelping,
WalletCards, CreditCard, Tag, Percent, Receipt, ShoppingCart,
FileCheck, Star, HeadphonesIcon, BarChart3, BookMarked, Bell,
ChevronLeft,
} from 'lucide-solid';
type Item = {
href: string;
label: string;
iconPath?: string;
icon?: any;
icon: any;
aliasPrefix?: string;
separatorBefore?: boolean;
group?: string;
};
const items: Item[] = [
{ href: '/admin', label: 'Dashboard', icon: LayoutGrid },
{ href: '/admin/roles', label: 'Internal Role Management', icon: FolderCog, group: 'Management' },
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: Users },
{ href: '/admin/onboarding-management', label: 'External Onboarding', icon: Users, aliasPrefix: '/admin/onboarding-schemas' },
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboards', icon: LayoutGrid },
{ href: '/admin/external-dashboard-management', label: 'External Dashboards', icon: LayoutGrid, aliasPrefix: '/admin/role-ui-configs' },
{ href: '/admin/approval', label: 'Approval Management', icon: ClipboardList },
{ href: '/admin/department', label: 'Department Management', icon: Briefcase, group: 'Organisation' },
{ href: '/admin/designation', label: 'Designation Management', icon: Briefcase },
{ href: '/admin/employees', label: 'Employee Management', icon: UserCircle2 },
{ href: '/admin/users', label: 'Users Management', icon: Users },
{ href: '/admin/company', label: 'Company Management', icon: Briefcase },
{ href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle2 },
{ href: '/admin/customer', label: 'Customer Management', icon: UserCircle2 },
{ href: '/admin/photographer', label: 'Photographer Management', icon: Sparkles, group: 'Service Providers' },
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Sparkles },
{ href: '/admin/tutors', label: 'Tutors Management', icon: Sparkles },
{ href: '/admin/developers', label: 'Developers Management', icon: Sparkles },
{ href: '/admin/jobs', label: 'Jobs Management', icon: Briefcase, group: 'Operations' },
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping },
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards, group: 'Finance' },
{ href: '/admin/credit', label: 'Credit Management', icon: WalletCards },
{ href: '/admin/coupon', label: 'Coupon Management', icon: Percent },
{ href: '/admin/discount', label: 'Discount Management', icon: Percent },
{ href: '/admin/tax', label: 'Tax Management', icon: Receipt },
{ href: '/admin/order', label: 'Order Management', icon: FileText },
{ href: '/admin/invoice', label: 'Invoice Management', icon: FileText },
{ href: '/admin/review', label: 'Review Management', icon: FileText, group: 'Support' },
{ href: '/admin/support', label: 'Support Management', icon: UserCircle2 },
{ href: '/admin/report', label: 'Report Management', icon: Bell },
{ href: '/admin/ledger', label: 'Ledger Management', icon: Receipt },
{ href: '/admin/kb', label: 'Knowledge Base', icon: FileText },
{ href: '/admin/notifications', label: 'Notifications', icon: Bell },
{ href: '/admin', label: 'Dashboard', icon: LayoutGrid },
{ href: '/admin/department', label: 'Department Management', icon: Building2 },
{ href: '/admin/designation', label: 'Designation Management', icon: Briefcase },
{ href: '/admin/employees', label: 'Internal User Management', icon: Users },
{ href: '/admin/roles', label: 'Internal Role Management', icon: ShieldCheck },
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: ShieldCheck },
{ href: '/admin/onboarding-management', label: 'Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas' },
{ href: '/admin/internal-dashboard-management',label: 'Internal Dashboard Management', icon: LayoutDashboard },
{ href: '/admin/external-dashboard-management',label: 'External Dashboard Management', icon: LayoutDashboard, aliasPrefix: '/admin/role-ui-configs' },
{ href: '/admin/approval', label: 'Approval Management', icon: ClipboardList },
{ href: '/admin/users', label: 'External User Management', icon: UserRoundSearch },
{ href: '/admin/customer', label: 'Customer Management', icon: UserCircle },
{ href: '/admin/company', label: 'Company Management', icon: Building2 },
{ href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle },
{ href: '/admin/photographer', label: 'Photographer Management', icon: Camera },
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette },
{ href: '/admin/tutors', label: 'Tutor Management', icon: BookOpen },
{ href: '/admin/developers', label: 'Developer Management', icon: Code2 },
{ href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness },
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping },
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards },
{ href: '/admin/credit', label: 'Credit Management', icon: CreditCard },
{ href: '/admin/coupon', label: 'Coupon Management', icon: Tag },
{ href: '/admin/discount', label: 'Discount Management', icon: Percent },
{ href: '/admin/tax', label: 'Tax Management', icon: Receipt },
{ href: '/admin/order', label: 'Order Management', icon: ShoppingCart },
{ href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck },
{ href: '/admin/review', label: 'Review Management', icon: Star },
{ href: '/admin/kb', label: 'Knowledge Base Management', icon: BookMarked },
{ href: '/admin/notifications', label: 'Notifications', icon: Bell },
{ href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon },
{ href: '/admin/report', label: 'Report Management', icon: BarChart3 },
{ href: '/admin/ledger', label: 'Ledger Management', icon: Receipt },
];
export default function AdminSidebar(props: { onNavigate?: () => void; onLogout?: () => void }) {
export default function AdminSidebar(props: {
collapsed: boolean;
onToggle: () => void;
onNavigate?: () => void;
onLogout?: () => void;
}) {
const location = useLocation();
const active = (item: Item) => {
@ -72,61 +67,76 @@ export default function AdminSidebar(props: { onNavigate?: () => void; onLogout?
};
return (
<aside class="flex h-full w-[268px] flex-col border-r border-[#d7d8df] bg-white">
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto px-3 py-4">
<aside class={`flex h-full flex-col border-r border-slate-200 bg-[#fcfcfd] transition-all duration-300 ${props.collapsed ? 'w-20' : 'w-64'}`}>
{/* Collapse toggle */}
<div class="flex justify-end px-3 pt-4 pb-3">
<button
type="button"
onClick={() => props.onToggle()}
class="rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700"
>
<ChevronLeft
size={16}
class={`transition-transform duration-300 ${props.collapsed ? 'rotate-180' : ''}`}
/>
</button>
</div>
{/* Nav */}
<nav class="scrollbar min-h-0 flex-1 space-y-1.5 overflow-y-auto px-3 pb-3">
<For each={items}>
{(item, index) => {
{(item) => {
const isActive = () => active(item);
const showGroup = () => item.group && (index() === 0 || items[index() - 1]?.group !== item.group);
const Icon = item.icon || FileText;
const Icon = item.icon;
return (
<>
<Show when={showGroup()}>
<div class={`${index() > 0 ? 'mt-4' : ''} mb-1 px-3 pb-1`}>
<p class="text-[10px] font-bold uppercase tracking-widest text-slate-400">{item.group}</p>
</div>
</Show>
<A
href={item.href}
onClick={() => props.onNavigate?.()}
title={item.label}
class={`group relative mb-0.5 flex min-h-[40px] w-full items-center gap-2.5 overflow-hidden rounded-lg px-3 text-[13.5px] font-semibold leading-tight transition-all duration-150 ${
isActive()
? 'bg-[#e8edf5] text-[#0a1d37]'
: 'text-[#44495a] hover:bg-slate-50 hover:text-[#0a1d37]'
<A
href={item.href}
onClick={() => props.onNavigate?.()}
title={props.collapsed ? item.label : undefined}
aria-current={isActive() ? 'page' : undefined}
class={`group relative flex items-center gap-3 rounded-xl border px-3 py-3 text-[15px] leading-5 transition-all ${
props.collapsed ? 'justify-center px-2' : ''
} ${
isActive()
? 'border-orange-200 bg-gradient-to-r from-orange-50 to-orange-100/70 text-slate-900'
: 'border-transparent text-slate-500 hover:border-slate-200 hover:bg-white hover:text-slate-800'
}`}
>
{/* Right orange accent bar */}
<span
class={`absolute right-0 top-2 bottom-2 w-[3px] rounded-l-full bg-orange-500 transition-opacity ${
isActive() ? 'opacity-100' : 'opacity-0'
}`}
>
{/* Left active indicator */}
<span
class={`absolute bottom-1.5 left-0 top-1.5 w-[3px] rounded-r-full bg-[#0a1d37] transition-opacity duration-150 ${
isActive() ? 'opacity-100' : 'opacity-0'
}`}
/>
<Icon
size={16}
class={`shrink-0 transition-colors ${isActive() ? 'text-[#0a1d37]' : 'text-slate-400 group-hover:text-slate-600'}`}
/>
<span class="min-w-0 flex-1 truncate">{item.label}</span>
</A>
</>
/>
<Icon
size={18}
class={`shrink-0 transition-colors ${
isActive() ? 'text-orange-600' : 'text-slate-500 group-hover:text-slate-700'
}`}
/>
<Show when={!props.collapsed}>
<span class={`min-w-0 flex-1 truncate font-medium ${isActive() ? 'text-slate-900' : 'text-slate-600 group-hover:text-slate-800'}`}>
{item.label}
</span>
<Show when={isActive()}>
<span class="ml-auto shrink-0 rounded-full border border-orange-200 bg-orange-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-orange-700">
Active
</span>
</Show>
</Show>
{/* Collapsed active dot */}
<Show when={props.collapsed && isActive()}>
<span class="absolute -right-0.5 top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-orange-500" />
</Show>
</A>
);
}}
</For>
</nav>
<div class="border-t border-[#e5e7ef] px-3 py-3">
<button
type="button"
onClick={() => props.onLogout?.()}
class="flex h-[40px] w-full items-center gap-2.5 rounded-lg px-3 text-left text-[13.5px] font-semibold text-[#c51d1d] transition hover:bg-red-50"
>
<svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H9m8 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h8a3 3 0 013 3v1" />
</svg>
<span>Sign Out</span>
</button>
</div>
</aside>
);
}