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:
parent
ab15466368
commit
c44152154f
3 changed files with 373 additions and 193 deletions
167
src/app.css
167
src/app.css
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue