feat(ui): premium redesign — Inter font, grouped sidebar, gradient active states
- Switch to Inter font for sharper admin feel - Sidebar: grouped nav sections (Overview/Organization/Access & Roles/etc), orange gradient active fade, tighter spacing, smaller 220px width - Header: slimmer h-14, backdrop-blur, logo+separator+title layout, gradient avatar, notification dot with ring-2 badge - Dashboard: smaller rounded-xl cards, tabular-nums on KPI values, ArrowUpRight hover indicators, refined Control Plane grid - app.css: sidebar/surface CSS design tokens, orange palette updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a272276055
commit
be179a6673
4 changed files with 234 additions and 185 deletions
29
src/app.css
29
src/app.css
|
|
@ -1,13 +1,28 @@
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@400;500;600;700;800&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Exo+2:wght@400;500;600;700;800&display=swap');
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-brand-orange: #fd6216;
|
--color-brand-orange: #fd6216;
|
||||||
--color-brand-navy: #050026;
|
--color-brand-navy: #050026;
|
||||||
--color-orange-50: #fff1e8;
|
--color-orange-50: #fff4ee;
|
||||||
--color-orange-100: #ffe2d2;
|
--color-orange-100: #ffe4d0;
|
||||||
--color-orange-200: #ffc9ac;
|
--color-orange-200: #ffc9a0;
|
||||||
--font-family-sans: 'Exo 2', sans-serif;
|
--color-orange-500: #fd6216;
|
||||||
|
--color-orange-600: #e5560f;
|
||||||
|
|
||||||
|
/* Sidebar tokens */
|
||||||
|
--color-sidebar-bg: #ffffff;
|
||||||
|
--color-sidebar-border: #f1f5f9;
|
||||||
|
--color-sidebar-text: #64748b;
|
||||||
|
--color-sidebar-active-text: #0f172a;
|
||||||
|
|
||||||
|
/* Surface tokens */
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-raised: #f8fafc;
|
||||||
|
--color-border-subtle: #f1f5f9;
|
||||||
|
--color-border: #e2e8f0;
|
||||||
|
|
||||||
|
--font-family-sans: 'Inter', 'Exo 2', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -17,9 +32,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Exo 2', sans-serif;
|
font-family: 'Inter', 'Exo 2', sans-serif;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #050026;
|
color: #0f172a;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,43 +191,48 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const initials = () => adminName().charAt(0).toUpperCase() || 'A';
|
const initials = () => adminName().charAt(0).toUpperCase() || 'A';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="min-h-screen bg-[#f8fafc]">
|
||||||
{/* ── Fixed Header ── */}
|
{/* ── Fixed Header ── */}
|
||||||
<header class="fixed top-0 z-50 flex h-16 w-full items-center justify-between border-b border-gray-200 bg-white px-6 shadow-sm">
|
<header class="fixed top-0 z-50 flex h-14 w-full items-center justify-between border-b border-slate-200/80 bg-white/95 px-5 backdrop-blur-md">
|
||||||
{/* Left: logo + page title */}
|
{/* Left: logo + separator + page title */}
|
||||||
<div class="flex items-center gap-8">
|
<div class="flex items-center gap-4">
|
||||||
<div class="flex h-10 items-center">
|
<div class="flex h-8 items-center">
|
||||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-8 w-auto object-contain" />
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-7 w-auto object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<h1 class="ml-28 text-base font-semibold text-gray-800">{pageTitle()}</h1>
|
<div class="h-5 w-px bg-slate-200" />
|
||||||
|
<span class="text-sm font-medium text-slate-500">{pageTitle()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: notifications + avatar + logout */}
|
{/* Right: notifications + avatar + logout */}
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-1">
|
||||||
{/* Notification bell */}
|
{/* Notification bell */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Notifications"
|
aria-label="Notifications"
|
||||||
class="relative rounded-full p-2 text-gray-500 transition-colors hover:bg-gray-100"
|
class="relative flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 transition-all hover:bg-slate-100 hover:text-slate-600"
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
<svg class="h-[17px] w-[17px]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.73 21a2 2 0 0 1-3.46 0" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||||
</svg>
|
</svg>
|
||||||
|
{/* Badge */}
|
||||||
|
<span class="absolute right-1.5 top-1.5 flex h-[7px] w-[7px] items-center justify-center rounded-full bg-orange-500 ring-2 ring-white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<div class="mx-2 h-5 w-px bg-slate-200" />
|
||||||
|
|
||||||
{/* Avatar + name */}
|
{/* Avatar + name */}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-3 rounded-lg p-1 pr-2 transition-colors hover:bg-gray-100"
|
class="flex items-center gap-2.5 rounded-lg px-2 py-1.5 transition-colors hover:bg-slate-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">
|
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-gradient-to-br from-orange-400 to-orange-600 text-[11px] font-bold text-white shadow-sm">
|
||||||
{initials()}
|
{initials()}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-start">
|
<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-[12px] font-semibold leading-none text-slate-700">{adminName()}</span>
|
||||||
<span class="text-[10px] leading-none text-gray-500">{adminRole()}</span>
|
<span class="mt-0.5 text-[10px] leading-none text-slate-400">{adminRole()}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
@ -236,9 +241,9 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onLogout}
|
onClick={onLogout}
|
||||||
aria-label="Logout"
|
aria-label="Logout"
|
||||||
class="rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600"
|
class="flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 transition-all hover:bg-red-50 hover:text-red-500"
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
<svg class="h-[17px] w-[17px]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" 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" />
|
<path stroke-linecap="round" stroke-linejoin="round" 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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -248,22 +253,22 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
|
|
||||||
{/* ── Body: sidebar + main (fixed, below header) ── */}
|
{/* ── Body: sidebar + main (fixed, below header) ── */}
|
||||||
{checkedSession() ? (
|
{checkedSession() ? (
|
||||||
<div class="fixed inset-0 top-16 grid grid-cols-[auto_1fr]">
|
<div class="fixed inset-0 top-14 grid grid-cols-[auto_1fr]">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<main class="scrollbar min-w-0 overflow-y-auto bg-gray-50 p-6">
|
<main class="scrollbar min-w-0 overflow-y-auto bg-[#f8fafc] p-6">
|
||||||
{/* Sub-tabs (shown for multi-tab sections) */}
|
{/* Sub-tabs (shown for multi-tab sections) */}
|
||||||
{tabs().length > 0 && (
|
{tabs().length > 0 && (
|
||||||
<div class="mb-6 flex gap-6 border-b border-gray-200">
|
<div class="mb-6 flex gap-1 border-b border-slate-200">
|
||||||
{tabs().map((tab) => (
|
{tabs().map((tab) => (
|
||||||
<A
|
<A
|
||||||
href={tab.href}
|
href={tab.href}
|
||||||
class={`pb-3 text-sm font-medium transition-colors ${
|
class={`relative px-3 pb-2.5 pt-0.5 text-[13px] font-medium transition-colors ${
|
||||||
isTabActive(tab)
|
isTabActive(tab)
|
||||||
? 'border-b-2 border-[#fd6216] text-gray-900'
|
? 'text-slate-900 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:rounded-t-full after:bg-orange-500 after:content-[""]'
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
: 'text-slate-500 hover:text-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
|
|
@ -276,8 +281,8 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Session check loading state */
|
/* Session check loading state */
|
||||||
<div class="fixed inset-0 top-16 grid grid-cols-[auto_1fr]">
|
<div class="fixed inset-0 top-14 grid grid-cols-[auto_1fr]">
|
||||||
<div class="w-64 border-r border-slate-200 bg-[#fcfcfd]" />
|
<div class="w-60 border-r border-slate-100 bg-white" />
|
||||||
<main class="flex items-center justify-center bg-gray-50">
|
<main class="flex items-center justify-center bg-gray-50">
|
||||||
<p class="text-sm text-gray-400">Checking session…</p>
|
<p class="text-sm text-gray-400">Checking session…</p>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -6,49 +6,63 @@ type LinkItem = {
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
aliasPrefix?: string;
|
aliasPrefix?: string;
|
||||||
|
group?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const links: LinkItem[] = [
|
const links: LinkItem[] = [
|
||||||
{ href: '/admin', label: 'Dashboard', icon: 'dashboard.svg' },
|
{ href: '/admin', label: 'Dashboard', icon: 'dashboard.svg', group: 'Overview' },
|
||||||
{ href: '/admin/department', label: 'Department Management', icon: 'department.svg' },
|
|
||||||
{ href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg' },
|
{ href: '/admin/department', label: 'Department', icon: 'department.svg', group: 'Organization' },
|
||||||
{ href: '/admin/employees', label: 'Employee Management', icon: 'users.svg' },
|
{ href: '/admin/designation', label: 'Designation', icon: 'designation.svg', group: 'Organization' },
|
||||||
{ href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg' },
|
{ href: '/admin/employees', label: 'Employees', icon: 'users.svg', group: 'Organization' },
|
||||||
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg' },
|
|
||||||
{ href: '/admin/onboarding-schemas', label: 'External Onboarding Management', icon: 'reviews.svg' },
|
{ href: '/admin/roles', label: 'Internal Roles', icon: 'role.svg', group: 'Access & Roles' },
|
||||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: 'dashboard.svg' },
|
{ href: '/admin/runtime-roles', label: 'External Roles', icon: 'role.svg', group: 'Access & Roles' },
|
||||||
{ href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs' },
|
{ href: '/admin/onboarding-schemas', label: 'Onboarding Flows', icon: 'reviews.svg', group: 'Access & Roles' },
|
||||||
{ href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg' },
|
|
||||||
{ href: '/admin/users', label: 'Users Management', icon: 'users.svg' },
|
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboards', icon: 'dashboard.svg', group: 'Dashboards' },
|
||||||
{ href: '/admin/company', label: 'Company Management', icon: 'company.svg' },
|
{ href: '/admin/external-dashboard-management', label: 'External Dashboards', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs', group: 'Dashboards' },
|
||||||
{ href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg' },
|
|
||||||
{ href: '/admin/customer', label: 'Customer Management', icon: 'users.svg' },
|
{ href: '/admin/users', label: 'Users', icon: 'users.svg', group: 'People' },
|
||||||
{ href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg' },
|
{ href: '/admin/company', label: 'Companies', icon: 'company.svg', group: 'People' },
|
||||||
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg' },
|
{ href: '/admin/candidate', label: 'Candidates', icon: 'candidate.svg', group: 'People' },
|
||||||
{ href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg' },
|
{ href: '/admin/customer', label: 'Customers', icon: 'users.svg', group: 'People' },
|
||||||
{ href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg' },
|
{ href: '/admin/photographer', label: 'Photographers', icon: 'photographer.svg', group: 'People' },
|
||||||
{ href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg' },
|
{ href: '/admin/makeup-artist', label: 'Makeup Artists', icon: 'makeup-artist.svg', group: 'People' },
|
||||||
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg' },
|
{ href: '/admin/tutors', label: 'Tutors', icon: 'tutor.svg', group: 'People' },
|
||||||
{ href: '/admin/catering-services', label: 'Catering Services Management', icon: 'company.svg' },
|
{ href: '/admin/developers', label: 'Developers', icon: 'developers.svg', group: 'People' },
|
||||||
{ href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: 'developers.svg' },
|
{ href: '/admin/video-editors', label: 'Video Editors', icon: 'developers.svg', group: 'People' },
|
||||||
{ href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: 'developers.svg' },
|
{ href: '/admin/fitness-trainers', label: 'Fitness Trainers', icon: 'tutor.svg', group: 'People' },
|
||||||
{ href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg' },
|
{ href: '/admin/catering-services', label: 'Catering Services', icon: 'company.svg', group: 'People' },
|
||||||
{ href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg' },
|
{ href: '/admin/graphic-designers', label: 'Graphic Designers', icon: 'developers.svg', group: 'People' },
|
||||||
{ href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg' },
|
{ href: '/admin/social-media-managers', label: 'Social Media Mgr.', icon: 'developers.svg', group: 'People' },
|
||||||
{ href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg' },
|
|
||||||
{ href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg' },
|
{ href: '/admin/jobs', label: 'Jobs', icon: 'jobs.svg', group: 'Content' },
|
||||||
{ href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg' },
|
{ href: '/admin/leads', label: 'Leads', icon: 'leads.svg', group: 'Content' },
|
||||||
{ href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg' },
|
{ href: '/admin/review', label: 'Reviews', icon: 'reviews.svg', group: 'Content' },
|
||||||
{ href: '/admin/order', label: 'Order Management', icon: 'order.svg' },
|
{ href: '/admin/kb', label: 'Knowledge Base', icon: 'reviews.svg', group: 'Content' },
|
||||||
{ href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg' },
|
{ href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg', group: 'Content' },
|
||||||
{ href: '/admin/review', label: 'Review Management', icon: 'reviews.svg' },
|
|
||||||
{ href: '/admin/support', label: 'Support Management', icon: 'support.svg' },
|
{ href: '/admin/pricing', label: 'Pricing', icon: 'pricing.svg', group: 'Finance' },
|
||||||
{ href: '/admin/kb', label: 'Knowledge Base Management', icon: 'reviews.svg' },
|
{ href: '/admin/credit', label: 'Credits', icon: 'credits.svg', group: 'Finance' },
|
||||||
{ href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg' },
|
{ href: '/admin/coupon', label: 'Coupons', icon: 'coupon.svg', group: 'Finance' },
|
||||||
{ href: '/admin/report', label: 'Report Management', icon: 'report.svg' },
|
{ href: '/admin/discount', label: 'Discounts', icon: 'discount.svg', group: 'Finance' },
|
||||||
{ href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg' },
|
{ href: '/admin/tax', label: 'Tax', icon: 'tax.svg', group: 'Finance' },
|
||||||
|
{ href: '/admin/order', label: 'Orders', icon: 'order.svg', group: 'Finance' },
|
||||||
|
{ href: '/admin/invoice', label: 'Invoices', icon: 'invoice.svg', group: 'Finance' },
|
||||||
|
{ href: '/admin/ledger', label: 'Ledger', icon: 'ledger.svg', group: 'Finance' },
|
||||||
|
|
||||||
|
{ href: '/admin/approval', label: 'Approvals', icon: 'approval.svg', group: 'Operations' },
|
||||||
|
{ href: '/admin/support', label: 'Support', icon: 'support.svg', group: 'Operations' },
|
||||||
|
{ href: '/admin/report', label: 'Reports', icon: 'report.svg', group: 'Operations' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Build ordered group list preserving first appearance
|
||||||
|
const GROUP_ORDER: string[] = [];
|
||||||
|
for (const item of links) {
|
||||||
|
if (item.group && !GROUP_ORDER.includes(item.group)) GROUP_ORDER.push(item.group);
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminSidebar() {
|
export default function AdminSidebar() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [collapsed, setCollapsed] = createSignal(false);
|
const [collapsed, setCollapsed] = createSignal(false);
|
||||||
|
|
@ -61,82 +75,86 @@ export default function AdminSidebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
class={`flex h-full flex-col border-r border-slate-200 bg-[#fcfcfd] px-3 pb-3 pt-5 transition-all duration-300 ${
|
class={`flex h-full flex-col border-r border-slate-100 bg-white pb-3 pt-3 transition-all duration-300 ${
|
||||||
collapsed() ? 'w-20' : 'w-64'
|
collapsed() ? 'w-[60px]' : 'w-[220px]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{/* Collapse toggle */}
|
{/* Collapse toggle */}
|
||||||
<div class="mb-4 flex justify-end px-2">
|
<div class={`mb-2 flex px-3 ${collapsed() ? 'justify-center' : 'justify-end'}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setCollapsed((v) => !v)}
|
onClick={() => setCollapsed((v) => !v)}
|
||||||
aria-label={collapsed() ? 'Expand sidebar' : 'Collapse sidebar'}
|
aria-label={collapsed() ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||||
class="rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700"
|
class="flex h-7 w-7 items-center justify-center rounded-md text-slate-400 transition hover:bg-slate-100 hover:text-slate-600"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class={`h-4 w-4 transition-transform ${collapsed() ? 'rotate-180' : ''}`}
|
class={`h-3.5 w-3.5 transition-transform ${collapsed() ? 'rotate-180' : ''}`}
|
||||||
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
|
fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav items */}
|
{/* Nav items grouped */}
|
||||||
<nav class="scrollbar min-h-0 flex-1 space-y-1.5 overflow-y-auto pr-1">
|
<nav class="scrollbar min-h-0 flex-1 overflow-y-auto px-2">
|
||||||
{links.map((item) => {
|
{GROUP_ORDER.map((group) => {
|
||||||
const active = isActive(item.href, item.aliasPrefix);
|
const groupItems = links.filter((l) => l.group === group);
|
||||||
return (
|
return (
|
||||||
<A
|
<div class="mb-3">
|
||||||
href={item.href}
|
{/* Group label — hidden when collapsed */}
|
||||||
activeClass=""
|
|
||||||
inactiveClass=""
|
|
||||||
title={collapsed() ? item.label : undefined}
|
|
||||||
class={`group relative flex items-center gap-3 rounded-xl border px-3 py-3 text-[15px] leading-5 transition-all ${
|
|
||||||
collapsed() ? 'justify-center px-2' : ''
|
|
||||||
} ${
|
|
||||||
active
|
|
||||||
? 'border-orange-200 bg-gradient-to-r from-orange-50 to-orange-100/70 text-slate-900 shadow-[inset_3px_0_0_0_#FD6216]'
|
|
||||||
: 'border-transparent text-slate-500 hover:border-slate-200 hover:bg-white hover:text-slate-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{/* Active left rail */}
|
|
||||||
<span
|
|
||||||
class={`absolute bottom-2 left-0 top-2 w-[3px] rounded-r-full bg-orange-500 transition-opacity ${
|
|
||||||
active ? 'opacity-100' : 'opacity-0'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Icon */}
|
|
||||||
<img
|
|
||||||
src={`/sidebar-icons/${item.icon}`}
|
|
||||||
alt=""
|
|
||||||
class="h-[18px] w-[18px] shrink-0 transition-all"
|
|
||||||
style={active
|
|
||||||
? 'filter: invert(42%) sepia(78%) saturate(1200%) hue-rotate(360deg) brightness(95%) contrast(95%); opacity:1'
|
|
||||||
: 'opacity:0.45'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Label */}
|
|
||||||
{!collapsed() && (
|
{!collapsed() && (
|
||||||
<span class={`truncate font-medium ${active ? 'text-slate-900' : 'text-slate-600 group-hover:text-slate-800'}`}>
|
<p class="mb-1 px-2 text-[10px] font-semibold uppercase tracking-widest text-slate-400">
|
||||||
{item.label}
|
{group}
|
||||||
</span>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<div class="space-y-0.5">
|
||||||
|
{groupItems.map((item) => {
|
||||||
|
const active = isActive(item.href, item.aliasPrefix);
|
||||||
|
return (
|
||||||
|
<A
|
||||||
|
href={item.href}
|
||||||
|
activeClass=""
|
||||||
|
inactiveClass=""
|
||||||
|
title={collapsed() ? item.label : undefined}
|
||||||
|
class={`group relative flex items-center gap-2.5 rounded-lg px-2 py-2 text-[13px] font-medium leading-none transition-all ${
|
||||||
|
collapsed() ? 'justify-center' : ''
|
||||||
|
} ${
|
||||||
|
active
|
||||||
|
? 'bg-gradient-to-r from-orange-500/[0.12] to-orange-500/[0.04] text-orange-600'
|
||||||
|
: 'text-slate-500 hover:bg-slate-50 hover:text-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Active left accent */}
|
||||||
|
{active && !collapsed() && (
|
||||||
|
<span class="absolute bottom-1.5 left-0 top-1.5 w-[2.5px] rounded-r-full bg-orange-500" />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Active badge (expanded) */}
|
{/* Icon */}
|
||||||
{!collapsed() && active && (
|
<img
|
||||||
<span class="ml-auto rounded-full border border-orange-200 bg-orange-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-orange-700">
|
src={`/sidebar-icons/${item.icon}`}
|
||||||
Active
|
alt=""
|
||||||
</span>
|
class="h-[16px] w-[16px] shrink-0"
|
||||||
)}
|
style={active
|
||||||
|
? 'filter: invert(42%) sepia(78%) saturate(1200%) hue-rotate(360deg) brightness(95%) contrast(95%); opacity:1'
|
||||||
|
: 'opacity:0.4'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Active dot (collapsed) */}
|
{/* Label */}
|
||||||
{collapsed() && active && (
|
{!collapsed() && (
|
||||||
<span class="absolute -right-0.5 top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-orange-500" />
|
<span class="truncate">{item.label}</span>
|
||||||
)}
|
)}
|
||||||
</A>
|
|
||||||
|
{/* Active dot (collapsed) */}
|
||||||
|
{collapsed() && active && (
|
||||||
|
<span class="absolute -right-0.5 top-1.5 h-1.5 w-1.5 rounded-full bg-orange-500" />
|
||||||
|
)}
|
||||||
|
</A>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import AdminShell from '~/components/AdminShell';
|
||||||
import {
|
import {
|
||||||
Users, Building2, Briefcase, Clock, Ticket, Receipt,
|
Users, Building2, Briefcase, Clock, Ticket, Receipt,
|
||||||
Package, Megaphone, Shield, UserCog, FormInput, LayoutDashboard,
|
Package, Megaphone, Shield, UserCog, FormInput, LayoutDashboard,
|
||||||
BadgeCheck, ChevronRight, TrendingUp,
|
BadgeCheck, ArrowUpRight, Activity,
|
||||||
} from 'lucide-solid';
|
} from 'lucide-solid';
|
||||||
import type { Component } from 'solid-js';
|
import type { Component } from 'solid-js';
|
||||||
|
|
||||||
|
|
@ -12,8 +12,7 @@ type KpiCard = {
|
||||||
value: string;
|
value: string;
|
||||||
href: string;
|
href: string;
|
||||||
icon: Component<any>;
|
icon: Component<any>;
|
||||||
bg: string;
|
color: string; // tailwind color name (e.g. 'blue')
|
||||||
fg: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type ControlLink = {
|
type ControlLink = {
|
||||||
|
|
@ -21,109 +20,121 @@ type ControlLink = {
|
||||||
href: string;
|
href: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
icon: Component<any>;
|
icon: Component<any>;
|
||||||
accent: string;
|
iconBg: string;
|
||||||
|
iconFg: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const KPI: KpiCard[] = [
|
const KPI: KpiCard[] = [
|
||||||
{ label: 'Total Users', value: '—', href: '/admin/users', icon: Users, bg: 'bg-blue-50', fg: 'text-blue-600' },
|
{ label: 'Total Users', value: '—', href: '/admin/users', icon: Users, color: 'violet' },
|
||||||
{ label: 'Companies', value: '—', href: '/admin/company', icon: Building2, bg: 'bg-violet-50', fg: 'text-violet-600' },
|
{ label: 'Companies', value: '—', href: '/admin/company', icon: Building2, color: 'blue' },
|
||||||
{ label: 'Open Jobs', value: '—', href: '/admin/jobs', icon: Briefcase, bg: 'bg-amber-50', fg: 'text-amber-600' },
|
{ label: 'Open Jobs', value: '—', href: '/admin/jobs', icon: Briefcase, color: 'amber' },
|
||||||
{ label: 'Pending Approvals', value: '—', href: '/admin/approval', icon: Clock, bg: 'bg-orange-50', fg: 'text-orange-600' },
|
{ label: 'Pending Approvals', value: '—', href: '/admin/approval', icon: Clock, color: 'orange' },
|
||||||
{ label: 'Open Tickets', value: '—', href: '/admin/support', icon: Ticket, bg: 'bg-rose-50', fg: 'text-rose-600' },
|
{ label: 'Open Tickets', value: '—', href: '/admin/support', icon: Ticket, color: 'rose' },
|
||||||
{ label: 'Invoices', value: '—', href: '/admin/invoice', icon: Receipt, bg: 'bg-teal-50', fg: 'text-teal-600' },
|
{ label: 'Invoices', value: '—', href: '/admin/invoice', icon: Receipt, color: 'teal' },
|
||||||
{ label: 'Active Orders', value: '—', href: '/admin/order', icon: Package, bg: 'bg-sky-50', fg: 'text-sky-600' },
|
{ label: 'Active Orders', value: '—', href: '/admin/order', icon: Package, color: 'sky' },
|
||||||
{ label: 'Active Leads', value: '—', href: '/admin/leads', icon: Megaphone, bg: 'bg-pink-50', fg: 'text-pink-600' },
|
{ label: 'Active Leads', value: '—', href: '/admin/leads', icon: Megaphone, color: 'pink' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Tailwind doesn't tree-shake dynamic class names, so we map them explicitly
|
||||||
|
const colorMap: Record<string, { chip: string; icon: string; value: string }> = {
|
||||||
|
violet: { chip: 'bg-violet-100', icon: 'text-violet-600', value: 'text-violet-700' },
|
||||||
|
blue: { chip: 'bg-blue-100', icon: 'text-blue-600', value: 'text-blue-700' },
|
||||||
|
amber: { chip: 'bg-amber-100', icon: 'text-amber-600', value: 'text-amber-700' },
|
||||||
|
orange: { chip: 'bg-orange-100', icon: 'text-orange-600', value: 'text-orange-700' },
|
||||||
|
rose: { chip: 'bg-rose-100', icon: 'text-rose-600', value: 'text-rose-700' },
|
||||||
|
teal: { chip: 'bg-teal-100', icon: 'text-teal-600', value: 'text-teal-700' },
|
||||||
|
sky: { chip: 'bg-sky-100', icon: 'text-sky-600', value: 'text-sky-700' },
|
||||||
|
pink: { chip: 'bg-pink-100', icon: 'text-pink-600', value: 'text-pink-700' },
|
||||||
|
};
|
||||||
|
|
||||||
const CONTROLS: ControlLink[] = [
|
const CONTROLS: ControlLink[] = [
|
||||||
{
|
{
|
||||||
label: 'Internal Roles',
|
label: 'Internal Roles',
|
||||||
href: '/admin/roles',
|
href: '/admin/roles',
|
||||||
desc: 'Define permissions and access levels for internal staff roles.',
|
desc: 'Permissions and access levels for internal staff.',
|
||||||
icon: Shield,
|
icon: Shield,
|
||||||
accent: 'bg-blue-50 text-blue-600',
|
iconBg: 'bg-blue-50',
|
||||||
|
iconFg: 'text-blue-600',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'External Roles',
|
label: 'External Roles',
|
||||||
href: '/admin/runtime-roles',
|
href: '/admin/runtime-roles',
|
||||||
desc: 'Configure modules, credit rules, and capabilities per external role.',
|
desc: 'Modules, credits, and capabilities per external role.',
|
||||||
icon: UserCog,
|
icon: UserCog,
|
||||||
accent: 'bg-violet-50 text-violet-600',
|
iconBg: 'bg-violet-50',
|
||||||
|
iconFg: 'text-violet-600',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Onboarding Flows',
|
label: 'Onboarding Flows',
|
||||||
href: '/admin/onboarding-schemas',
|
href: '/admin/onboarding-schemas',
|
||||||
desc: 'Build schema-driven onboarding forms for each external role.',
|
desc: 'Schema-driven onboarding forms per external role.',
|
||||||
icon: FormInput,
|
icon: FormInput,
|
||||||
accent: 'bg-amber-50 text-amber-600',
|
iconBg: 'bg-amber-50',
|
||||||
|
iconFg: 'text-amber-600',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'External Dashboards',
|
label: 'External Dashboards',
|
||||||
href: '/admin/external-dashboard-management',
|
href: '/admin/external-dashboard-management',
|
||||||
desc: 'Configure the runtimeConfig, sidebar, and widgets per external role.',
|
desc: 'Sidebar, widgets, and runtimeConfig per external role.',
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
accent: 'bg-teal-50 text-teal-600',
|
iconBg: 'bg-teal-50',
|
||||||
|
iconFg: 'text-teal-600',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Internal Dashboards',
|
label: 'Internal Dashboards',
|
||||||
href: '/admin/internal-dashboard-management',
|
href: '/admin/internal-dashboard-management',
|
||||||
desc: 'Design home widgets and KPI panels for internal staff dashboards.',
|
desc: 'Home widgets and KPI panels for internal staff.',
|
||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
accent: 'bg-orange-50 text-orange-600',
|
iconBg: 'bg-orange-50',
|
||||||
|
iconFg: 'text-orange-600',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Approval Queue',
|
label: 'Approval Queue',
|
||||||
href: '/admin/approval',
|
href: '/admin/approval',
|
||||||
desc: 'Review, approve, or reject pending platform action requests.',
|
desc: 'Review, approve, or reject pending action requests.',
|
||||||
icon: BadgeCheck,
|
icon: BadgeCheck,
|
||||||
accent: 'bg-rose-50 text-rose-600',
|
iconBg: 'bg-rose-50',
|
||||||
|
iconFg: 'text-rose-600',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="space-y-8">
|
<div class="mx-auto max-w-6xl space-y-7">
|
||||||
|
|
||||||
{/* ── Page header ── */}
|
{/* ── Page header ── */}
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-[22px] font-bold tracking-tight text-gray-900">Platform Overview</h1>
|
<h1 class="text-xl font-bold tracking-tight text-slate-900">Platform Overview</h1>
|
||||||
<p class="mt-0.5 text-sm text-gray-400">Real-time snapshot of the Nxtgauge platform.</p>
|
<p class="mt-0.5 text-sm text-slate-400">Real-time snapshot of the Nxtgauge platform.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 rounded-xl border border-gray-200 bg-white px-3 py-1.5 shadow-sm">
|
<div class="flex items-center gap-1.5 rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-1.5">
|
||||||
<TrendingUp class="h-4 w-4 text-emerald-500" />
|
<Activity class="h-3.5 w-3.5 text-emerald-500" />
|
||||||
<span class="text-xs font-medium text-gray-500">Live data</span>
|
<span class="text-[11px] font-semibold text-emerald-600">Live</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── KPI grid ── */}
|
{/* ── KPI grid ── */}
|
||||||
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
<div class="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||||
{KPI.map((kpi) => {
|
{KPI.map((kpi) => {
|
||||||
const Icon = kpi.icon;
|
const Icon = kpi.icon;
|
||||||
|
const c = colorMap[kpi.color];
|
||||||
return (
|
return (
|
||||||
<A
|
<A
|
||||||
href={kpi.href}
|
href={kpi.href}
|
||||||
class="group relative overflow-hidden rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-gray-200 hover:shadow-md"
|
class="group flex flex-col gap-3 rounded-xl border border-slate-100 bg-white p-4 shadow-sm transition-all duration-200 hover:-translate-y-px hover:border-slate-200 hover:shadow-md"
|
||||||
>
|
>
|
||||||
{/* Subtle background circle */}
|
<div class="flex items-center justify-between">
|
||||||
<div class="absolute -right-4 -top-4 h-20 w-20 rounded-full bg-gray-50 opacity-60" />
|
<div class={`flex h-8 w-8 items-center justify-center rounded-lg ${c.chip}`}>
|
||||||
|
<Icon class={`h-4 w-4 ${c.icon}`} />
|
||||||
<div class="relative flex flex-col gap-4">
|
|
||||||
{/* Icon */}
|
|
||||||
<div class={`flex h-10 w-10 items-center justify-center rounded-xl ${kpi.bg}`}>
|
|
||||||
<Icon class={`h-5 w-5 ${kpi.fg}`} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Metric */}
|
|
||||||
<div>
|
|
||||||
<p class="text-[28px] font-bold leading-none tracking-tight text-gray-900">{kpi.value}</p>
|
|
||||||
<p class="mt-1.5 text-[13px] font-medium text-gray-400">{kpi.label}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ArrowUpRight class="h-3.5 w-3.5 text-slate-200 transition-colors group-hover:text-slate-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-2xl font-bold tabular-nums tracking-tight text-slate-900">{kpi.value}</p>
|
||||||
|
<p class="mt-0.5 text-xs font-medium text-slate-400">{kpi.label}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Hover arrow */}
|
|
||||||
<ChevronRight class="absolute bottom-4 right-4 h-4 w-4 text-gray-300 opacity-0 transition-opacity group-hover:opacity-100" />
|
|
||||||
</A>
|
</A>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -131,28 +142,28 @@ export default function AdminDashboard() {
|
||||||
|
|
||||||
{/* ── Control Plane ── */}
|
{/* ── Control Plane ── */}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<h2 class="text-[15px] font-semibold text-gray-700">Control Plane</h2>
|
<h2 class="text-sm font-semibold text-slate-700">Control Plane</h2>
|
||||||
<span class="rounded-full border border-gray-200 bg-white px-2.5 py-0.5 text-[11px] font-medium text-gray-400 shadow-sm">
|
<span class="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-slate-400">
|
||||||
{CONTROLS.length} modules
|
{CONTROLS.length} modules
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{CONTROLS.map((item) => {
|
{CONTROLS.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
return (
|
return (
|
||||||
<A
|
<A
|
||||||
href={item.href}
|
href={item.href}
|
||||||
class="group flex items-start gap-4 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-orange-100 hover:shadow-md"
|
class="group flex items-start gap-3.5 rounded-xl border border-slate-100 bg-white p-4 shadow-sm transition-all duration-200 hover:-translate-y-px hover:border-orange-100 hover:shadow-md"
|
||||||
>
|
>
|
||||||
<div class={`mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${item.accent}`}>
|
<div class={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${item.iconBg}`}>
|
||||||
<Icon class="h-4 w-4" />
|
<Icon class={`h-4 w-4 ${item.iconFg}`} />
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<p class="text-[14px] font-semibold text-gray-800 group-hover:text-[#fd6216] transition-colors">{item.label}</p>
|
<p class="text-[13px] font-semibold text-slate-800 transition-colors group-hover:text-[#fd6216]">{item.label}</p>
|
||||||
<p class="mt-0.5 text-[12px] leading-relaxed text-gray-400">{item.desc}</p>
|
<p class="mt-0.5 text-[11px] leading-relaxed text-slate-400">{item.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight class="mt-1 h-4 w-4 shrink-0 text-gray-300 transition-transform group-hover:translate-x-0.5 group-hover:text-orange-400" />
|
<ArrowUpRight class="mt-0.5 h-3.5 w-3.5 shrink-0 text-slate-200 transition-all group-hover:text-orange-400" />
|
||||||
</A>
|
</A>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue