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; }
|
.auth-inline-msg { margin-top: 12px; }
|
||||||
.hint { margin: 6px 0 0; color: #64748b; font-size: 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 + Shared UI ===== */
|
||||||
.admin-shell-root {
|
.admin-shell-root {
|
||||||
background:
|
background:
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { For, createEffect, createMemo, createSignal, onCleanup, onMount, type J
|
||||||
import AdminSidebar from './AdminSidebar';
|
import AdminSidebar from './AdminSidebar';
|
||||||
import { isExternalIdentity } from '~/lib/admin-auth';
|
import { isExternalIdentity } from '~/lib/admin-auth';
|
||||||
import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session';
|
import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session';
|
||||||
|
import { Bell } from 'lucide-solid';
|
||||||
|
|
||||||
type Tab = { href: string; label: string; exact?: boolean };
|
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 }) {
|
export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
@ -69,6 +33,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const [checkedSession, setCheckedSession] = createSignal(false);
|
const [checkedSession, setCheckedSession] = createSignal(false);
|
||||||
const [adminName, setAdminName] = createSignal('Admin');
|
const [adminName, setAdminName] = createSignal('Admin');
|
||||||
const [sidebarOpen, setSidebarOpen] = createSignal(false);
|
const [sidebarOpen, setSidebarOpen] = createSignal(false);
|
||||||
|
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
|
||||||
const [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>();
|
const [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>();
|
||||||
const [tabRefs, setTabRefs] = createSignal<Record<string, HTMLAnchorElement>>({});
|
const [tabRefs, setTabRefs] = createSignal<Record<string, HTMLAnchorElement>>({});
|
||||||
const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false });
|
const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false });
|
||||||
|
|
@ -81,7 +46,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
return [];
|
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 refreshTabIndicator = () => {
|
||||||
const activeTab = tabs().find((tab) => isTabActive(tab));
|
const activeTab = tabs().find((tab) => isTabActive(tab));
|
||||||
|
|
@ -107,13 +75,16 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
onCleanup(() => window.removeEventListener('resize', onResize));
|
onCleanup(() => window.removeEventListener('resize', onResize));
|
||||||
|
|
||||||
const isLocalDev =
|
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 =
|
const isPreview =
|
||||||
searchParams._preview === '1' ||
|
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 (isPreview || isLocalDev) {
|
||||||
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1');
|
if (typeof sessionStorage !== 'undefined')
|
||||||
|
sessionStorage.setItem('nxtgauge_admin_preview', '1');
|
||||||
setAdminSession();
|
setAdminSession();
|
||||||
setCheckedSession(true);
|
setCheckedSession(true);
|
||||||
return;
|
return;
|
||||||
|
|
@ -126,7 +97,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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', {
|
const response = await fetch('/api/gateway/users/auth/me', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -166,73 +140,100 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
|
|
||||||
const adminInitials = createMemo(() => {
|
const adminInitials = createMemo(() => {
|
||||||
const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean);
|
const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean);
|
||||||
if (parts.length === 0) return 'AD';
|
if (parts.length === 0) return 'U';
|
||||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
if (parts.length === 1) return parts[0].slice(0, 1).toUpperCase();
|
||||||
return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase();
|
return `${parts[0][0] || ''}`.toUpperCase();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sidebarWidth = () => (sidebarCollapsed() ? 'w-20' : 'w-64');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-[#f0f1f6]">
|
<div class="min-h-screen bg-white">
|
||||||
<header class="fixed inset-x-0 top-0 z-50 border-b border-[#d8dbe3] bg-white">
|
{/* ── Header ── */}
|
||||||
<div class="flex h-[64px] items-center justify-between px-6">
|
<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">
|
||||||
<div class="flex min-w-0 items-center gap-6">
|
{/* 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
|
<button
|
||||||
type="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"
|
aria-label="Notifications"
|
||||||
onClick={() => setSidebarOpen((v) => !v)}
|
class="relative rounded-full p-2 text-gray-500 transition-colors hover:bg-gray-100"
|
||||||
aria-label="Toggle sidebar"
|
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
<Bell size={20} />
|
||||||
<path stroke-linecap="round" d="M4 7h16M4 12h16M4 17h16" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
{/* Avatar + name */}
|
||||||
<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">
|
<div class="flex items-center gap-2">
|
||||||
<IconSearch />
|
<button class="flex items-center gap-3 rounded-lg p-1 pr-2 transition-colors hover:bg-gray-100">
|
||||||
<span>Search system operations...</span>
|
<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">
|
||||||
</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">
|
|
||||||
{adminInitials()}
|
{adminInitials()}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
{/* ── Body ── */}
|
||||||
{checkedSession() ? (
|
{checkedSession() ? (
|
||||||
<div class="fixed inset-0 top-[64px] flex">
|
<div class="fixed inset-0 top-16 flex">
|
||||||
|
{/* Mobile overlay */}
|
||||||
<div
|
<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)}
|
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' : ''}`}>
|
{/* Sidebar */}
|
||||||
<AdminSidebar onNavigate={() => setSidebarOpen(false)} onLogout={onLogout} />
|
<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>
|
</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
|
<ShowTabs
|
||||||
tabs={tabs()}
|
tabs={tabs()}
|
||||||
isTabActive={isTabActive}
|
isTabActive={isTabActive}
|
||||||
|
|
@ -244,10 +245,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="fixed inset-0 top-[64px] flex">
|
<div class="fixed inset-0 top-16 flex">
|
||||||
<div class="hidden w-[268px] border-r border-[#d7d8df] bg-white lg:block" />
|
<div class="hidden w-64 border-r border-slate-200 bg-[#fcfcfd] lg:block" />
|
||||||
<main class="flex flex-1 items-center justify-center bg-[#f3f4f8]">
|
<main class="flex flex-1 items-center justify-center bg-gray-50">
|
||||||
<p class="text-sm text-gray-500">Checking session...</p>
|
<p class="text-sm text-gray-400">Checking session…</p>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -265,15 +266,17 @@ function ShowTabs(props: {
|
||||||
if (props.tabs.length === 0) return null;
|
if (props.tabs.length === 0) return null;
|
||||||
|
|
||||||
return (
|
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}>
|
<For each={props.tabs}>
|
||||||
{(tab) => (
|
{(tab) => (
|
||||||
<A
|
<A
|
||||||
href={tab.href}
|
href={tab.href}
|
||||||
ref={(el) => props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))}
|
ref={(el) => props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))}
|
||||||
aria-current={props.isTabActive(tab) ? 'page' : undefined}
|
aria-current={props.isTabActive(tab) ? 'page' : undefined}
|
||||||
class={`relative px-4 pb-3 pt-3 text-[13.5px] font-semibold transition-colors ${
|
class={`px-4 pb-3 pt-3 text-[14px] font-semibold transition-colors ${
|
||||||
props.isTabActive(tab) ? 'text-[#0a1d37]' : 'text-[#636b7f] hover:text-[#0a1d37]'
|
props.isTabActive(tab)
|
||||||
|
? 'text-[#fd6216]'
|
||||||
|
: 'text-slate-500 hover:text-slate-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
|
@ -281,7 +284,7 @@ function ShowTabs(props: {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<div
|
<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` }}
|
style={{ left: `${props.tabIndicator().left}px`, width: `${props.tabIndicator().width}px` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,63 @@
|
||||||
import { A, useLocation } from '@solidjs/router';
|
import { A, useLocation } from '@solidjs/router';
|
||||||
import { For, Show } from 'solid-js';
|
import { For, Show, createSignal } from 'solid-js';
|
||||||
import {
|
import {
|
||||||
Bell,
|
LayoutGrid, Building2, Briefcase, Users, ShieldCheck, FileText,
|
||||||
Briefcase,
|
LayoutDashboard, ClipboardList, UserRoundSearch, UserCircle,
|
||||||
ClipboardList,
|
Camera, Palette, BookOpen, Code2, BriefcaseBusiness, HandHelping,
|
||||||
FileText,
|
WalletCards, CreditCard, Tag, Percent, Receipt, ShoppingCart,
|
||||||
FolderCog,
|
FileCheck, Star, HeadphonesIcon, BarChart3, BookMarked, Bell,
|
||||||
HandHelping,
|
ChevronLeft,
|
||||||
LayoutGrid,
|
|
||||||
Percent,
|
|
||||||
Receipt,
|
|
||||||
Sparkles,
|
|
||||||
UserCircle2,
|
|
||||||
Users,
|
|
||||||
WalletCards,
|
|
||||||
} from 'lucide-solid';
|
} from 'lucide-solid';
|
||||||
|
|
||||||
type Item = {
|
type Item = {
|
||||||
href: string;
|
href: string;
|
||||||
label: string;
|
label: string;
|
||||||
iconPath?: string;
|
icon: any;
|
||||||
icon?: any;
|
|
||||||
aliasPrefix?: string;
|
aliasPrefix?: string;
|
||||||
separatorBefore?: boolean;
|
|
||||||
group?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
{ href: '/admin', label: 'Dashboard', icon: LayoutGrid },
|
{ href: '/admin', label: 'Dashboard', icon: LayoutGrid },
|
||||||
{ href: '/admin/roles', label: 'Internal Role Management', icon: FolderCog, group: 'Management' },
|
{ href: '/admin/department', label: 'Department Management', icon: Building2 },
|
||||||
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: Users },
|
{ href: '/admin/designation', label: 'Designation Management', icon: Briefcase },
|
||||||
{ href: '/admin/onboarding-management', label: 'External Onboarding', icon: Users, aliasPrefix: '/admin/onboarding-schemas' },
|
{ href: '/admin/employees', label: 'Internal User Management', icon: Users },
|
||||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboards', icon: LayoutGrid },
|
{ href: '/admin/roles', label: 'Internal Role Management', icon: ShieldCheck },
|
||||||
{ href: '/admin/external-dashboard-management', label: 'External Dashboards', icon: LayoutGrid, aliasPrefix: '/admin/role-ui-configs' },
|
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: ShieldCheck },
|
||||||
{ href: '/admin/approval', label: 'Approval Management', icon: ClipboardList },
|
{ href: '/admin/onboarding-management', label: 'Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas' },
|
||||||
{ href: '/admin/department', label: 'Department Management', icon: Briefcase, group: 'Organisation' },
|
{ href: '/admin/internal-dashboard-management',label: 'Internal Dashboard Management', icon: LayoutDashboard },
|
||||||
{ href: '/admin/designation', label: 'Designation Management', icon: Briefcase },
|
{ href: '/admin/external-dashboard-management',label: 'External Dashboard Management', icon: LayoutDashboard, aliasPrefix: '/admin/role-ui-configs' },
|
||||||
{ href: '/admin/employees', label: 'Employee Management', icon: UserCircle2 },
|
{ href: '/admin/approval', label: 'Approval Management', icon: ClipboardList },
|
||||||
{ href: '/admin/users', label: 'Users Management', icon: Users },
|
{ href: '/admin/users', label: 'External User Management', icon: UserRoundSearch },
|
||||||
{ href: '/admin/company', label: 'Company Management', icon: Briefcase },
|
{ href: '/admin/customer', label: 'Customer Management', icon: UserCircle },
|
||||||
{ href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle2 },
|
{ href: '/admin/company', label: 'Company Management', icon: Building2 },
|
||||||
{ href: '/admin/customer', label: 'Customer Management', icon: UserCircle2 },
|
{ href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle },
|
||||||
{ href: '/admin/photographer', label: 'Photographer Management', icon: Sparkles, group: 'Service Providers' },
|
{ href: '/admin/photographer', label: 'Photographer Management', icon: Camera },
|
||||||
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Sparkles },
|
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette },
|
||||||
{ href: '/admin/tutors', label: 'Tutors Management', icon: Sparkles },
|
{ href: '/admin/tutors', label: 'Tutor Management', icon: BookOpen },
|
||||||
{ href: '/admin/developers', label: 'Developers Management', icon: Sparkles },
|
{ href: '/admin/developers', label: 'Developer Management', icon: Code2 },
|
||||||
{ href: '/admin/jobs', label: 'Jobs Management', icon: Briefcase, group: 'Operations' },
|
{ href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness },
|
||||||
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping },
|
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping },
|
||||||
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards, group: 'Finance' },
|
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards },
|
||||||
{ href: '/admin/credit', label: 'Credit Management', icon: WalletCards },
|
{ href: '/admin/credit', label: 'Credit Management', icon: CreditCard },
|
||||||
{ href: '/admin/coupon', label: 'Coupon Management', icon: Percent },
|
{ href: '/admin/coupon', label: 'Coupon Management', icon: Tag },
|
||||||
{ href: '/admin/discount', label: 'Discount Management', icon: Percent },
|
{ href: '/admin/discount', label: 'Discount Management', icon: Percent },
|
||||||
{ href: '/admin/tax', label: 'Tax Management', icon: Receipt },
|
{ href: '/admin/tax', label: 'Tax Management', icon: Receipt },
|
||||||
{ href: '/admin/order', label: 'Order Management', icon: FileText },
|
{ href: '/admin/order', label: 'Order Management', icon: ShoppingCart },
|
||||||
{ href: '/admin/invoice', label: 'Invoice Management', icon: FileText },
|
{ href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck },
|
||||||
{ href: '/admin/review', label: 'Review Management', icon: FileText, group: 'Support' },
|
{ href: '/admin/review', label: 'Review Management', icon: Star },
|
||||||
{ href: '/admin/support', label: 'Support Management', icon: UserCircle2 },
|
{ href: '/admin/kb', label: 'Knowledge Base Management', icon: BookMarked },
|
||||||
{ href: '/admin/report', label: 'Report Management', icon: Bell },
|
{ href: '/admin/notifications', label: 'Notifications', icon: Bell },
|
||||||
{ href: '/admin/ledger', label: 'Ledger Management', icon: Receipt },
|
{ href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon },
|
||||||
{ href: '/admin/kb', label: 'Knowledge Base', icon: FileText },
|
{ href: '/admin/report', label: 'Report Management', icon: BarChart3 },
|
||||||
{ href: '/admin/notifications', label: 'Notifications', icon: Bell },
|
{ 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 location = useLocation();
|
||||||
|
|
||||||
const active = (item: Item) => {
|
const active = (item: Item) => {
|
||||||
|
|
@ -72,61 +67,76 @@ export default function AdminSidebar(props: { onNavigate?: () => void; onLogout?
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside class="flex h-full w-[268px] flex-col border-r border-[#d7d8df] bg-white">
|
<aside class={`flex h-full flex-col border-r border-slate-200 bg-[#fcfcfd] transition-all duration-300 ${props.collapsed ? 'w-20' : 'w-64'}`}>
|
||||||
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto px-3 py-4">
|
{/* 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}>
|
<For each={items}>
|
||||||
{(item, index) => {
|
{(item) => {
|
||||||
const isActive = () => active(item);
|
const isActive = () => active(item);
|
||||||
const showGroup = () => item.group && (index() === 0 || items[index() - 1]?.group !== item.group);
|
const Icon = item.icon;
|
||||||
const Icon = item.icon || FileText;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<A
|
||||||
<Show when={showGroup()}>
|
href={item.href}
|
||||||
<div class={`${index() > 0 ? 'mt-4' : ''} mb-1 px-3 pb-1`}>
|
onClick={() => props.onNavigate?.()}
|
||||||
<p class="text-[10px] font-bold uppercase tracking-widest text-slate-400">{item.group}</p>
|
title={props.collapsed ? item.label : undefined}
|
||||||
</div>
|
aria-current={isActive() ? 'page' : undefined}
|
||||||
</Show>
|
class={`group relative flex items-center gap-3 rounded-xl border px-3 py-3 text-[15px] leading-5 transition-all ${
|
||||||
<A
|
props.collapsed ? 'justify-center px-2' : ''
|
||||||
href={item.href}
|
} ${
|
||||||
onClick={() => props.onNavigate?.()}
|
isActive()
|
||||||
title={item.label}
|
? 'border-orange-200 bg-gradient-to-r from-orange-50 to-orange-100/70 text-slate-900'
|
||||||
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 ${
|
: 'border-transparent text-slate-500 hover:border-slate-200 hover:bg-white hover:text-slate-800'
|
||||||
isActive()
|
}`}
|
||||||
? 'bg-[#e8edf5] text-[#0a1d37]'
|
>
|
||||||
: 'text-[#44495a] hover:bg-slate-50 hover:text-[#0a1d37]'
|
{/* 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
|
<Icon
|
||||||
class={`absolute bottom-1.5 left-0 top-1.5 w-[3px] rounded-r-full bg-[#0a1d37] transition-opacity duration-150 ${
|
size={18}
|
||||||
isActive() ? 'opacity-100' : 'opacity-0'
|
class={`shrink-0 transition-colors ${
|
||||||
}`}
|
isActive() ? 'text-orange-600' : 'text-slate-500 group-hover:text-slate-700'
|
||||||
/>
|
}`}
|
||||||
<Icon
|
/>
|
||||||
size={16}
|
|
||||||
class={`shrink-0 transition-colors ${isActive() ? 'text-[#0a1d37]' : 'text-slate-400 group-hover:text-slate-600'}`}
|
<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'}`}>
|
||||||
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
{item.label}
|
||||||
</A>
|
</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>
|
</For>
|
||||||
</nav>
|
</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>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue