Revamp admin shell/sidebar and align login form style
This commit is contained in:
parent
1f59fbbc4c
commit
9b1ffdacf6
5 changed files with 490 additions and 510 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { A, useLocation, useNavigate, useSearchParams } from '@solidjs/router';
|
import { A, useLocation, useNavigate, useSearchParams } from '@solidjs/router';
|
||||||
import { createMemo, createSignal, onMount, type JSX } from 'solid-js';
|
import { For, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX } from 'solid-js';
|
||||||
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';
|
||||||
|
|
@ -10,88 +10,57 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [
|
||||||
{
|
{
|
||||||
prefixes: ['/admin/roles'],
|
prefixes: ['/admin/roles'],
|
||||||
tabs: [
|
tabs: [
|
||||||
{ href: '/admin/roles', label: 'Internal Roles', exact: true },
|
{ href: '/admin/roles', label: 'Roles', exact: true },
|
||||||
{ href: '/admin/roles/create', label: 'Create Role' },
|
{ href: '/admin/roles/create', label: 'Create Role' },
|
||||||
{ href: '/admin/roles/templates', label: 'Role Templates' },
|
{ href: '/admin/roles/templates', label: 'View Roles' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prefixes: ['/admin/runtime-roles'],
|
prefixes: ['/admin/runtime-roles'],
|
||||||
tabs: [
|
tabs: [
|
||||||
{ href: '/admin/runtime-roles', label: 'External Roles', exact: true },
|
{ href: '/admin/runtime-roles', label: 'Roles', exact: true },
|
||||||
{ href: '/admin/runtime-roles/new', label: 'Create External Role' },
|
{ href: '/admin/runtime-roles/new', label: 'Create Role' },
|
||||||
],
|
{ href: '/admin/role-ui-configs', label: 'View Roles' },
|
||||||
},
|
|
||||||
{
|
|
||||||
prefixes: ['/admin/onboarding-schemas'],
|
|
||||||
tabs: [
|
|
||||||
{ href: '/admin/onboarding-schemas', label: 'Onboarding Flows', exact: true },
|
|
||||||
{ href: '/admin/onboarding-schemas/new', label: 'Create Flow' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prefixes: ['/admin/internal-dashboard-management'],
|
|
||||||
tabs: [
|
|
||||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboards' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prefixes: ['/admin/external-dashboard-management'],
|
|
||||||
tabs: [
|
|
||||||
{ href: '/admin/external-dashboard-management', label: 'External Dashboards' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prefixes: ['/admin/role-ui-configs'],
|
|
||||||
tabs: [
|
|
||||||
{ href: '/admin/role-ui-configs', label: 'Config Inspector', exact: true },
|
|
||||||
{ href: '/admin/role-ui-configs/new', label: 'Create Config' },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const PAGE_TITLES: Array<{ prefix: string; title: string }> = [
|
function IconBell() {
|
||||||
{ prefix: '/admin/employees', title: 'Employee Management' },
|
return (
|
||||||
{ prefix: '/admin/department', title: 'Department Management' },
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
{ prefix: '/admin/designation', title: 'Designation Management' },
|
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||||
{ prefix: '/admin/roles', title: 'Internal Role Management' },
|
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||||
{ prefix: '/admin/runtime-roles', title: 'External Role Management' },
|
</svg>
|
||||||
{ prefix: '/admin/onboarding-schemas', title: 'External Onboarding Management' },
|
);
|
||||||
{ prefix: '/admin/internal-dashboard-management', title: 'Internal Dashboard Management' },
|
}
|
||||||
{ prefix: '/admin/external-dashboard-management', title: 'External Dashboard Management' },
|
|
||||||
{ prefix: '/admin/role-ui-configs', title: 'External Dashboard Management' },
|
function IconSearch() {
|
||||||
{ prefix: '/admin/approval', title: 'Approval Management' },
|
return (
|
||||||
{ prefix: '/admin/users', title: 'Users Management' },
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
{ prefix: '/admin/company', title: 'Company Management' },
|
<circle cx="11" cy="11" r="7.5" />
|
||||||
{ prefix: '/admin/candidate', title: 'Candidate Management' },
|
<path d="m20 20-3.8-3.8" />
|
||||||
{ prefix: '/admin/customer', title: 'Customer Management' },
|
</svg>
|
||||||
{ prefix: '/admin/photographer', title: 'Photographer Management' },
|
);
|
||||||
{ prefix: '/admin/makeup-artist', title: 'Makeup Artist Management' },
|
}
|
||||||
{ prefix: '/admin/tutors', title: 'Tutors Management' },
|
|
||||||
{ prefix: '/admin/developers', title: 'Developers Management' },
|
function IconHelp() {
|
||||||
{ prefix: '/admin/video-editors', title: 'Video Editor Management' },
|
return (
|
||||||
{ prefix: '/admin/fitness-trainers', title: 'Fitness Trainer Management' },
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
{ prefix: '/admin/catering-services', title: 'Catering Services Management' },
|
<circle cx="12" cy="12" r="9" />
|
||||||
{ prefix: '/admin/graphic-designers', title: 'Graphics Designer Management' },
|
<path d="M9.3 9a2.8 2.8 0 1 1 4.9 2c-.8.9-1.7 1.4-1.7 2.5" />
|
||||||
{ prefix: '/admin/social-media-managers', title: 'Social Media Manager Management' },
|
<path d="M12 17h.01" />
|
||||||
{ prefix: '/admin/jobs', title: 'Jobs Management' },
|
</svg>
|
||||||
{ prefix: '/admin/leads', title: 'Leads Management' },
|
);
|
||||||
{ prefix: '/admin/pricing', title: 'Pricing Management' },
|
}
|
||||||
{ prefix: '/admin/credit', title: 'Credit Management' },
|
|
||||||
{ prefix: '/admin/coupon', title: 'Coupon Management' },
|
function IconCog() {
|
||||||
{ prefix: '/admin/discount', title: 'Discount Management' },
|
return (
|
||||||
{ prefix: '/admin/tax', title: 'Tax Management' },
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
{ prefix: '/admin/order', title: 'Order Management' },
|
<path d="M12 15.5A3.5 3.5 0 1 0 12 8.5a3.5 3.5 0 0 0 0 7Z" />
|
||||||
{ prefix: '/admin/invoice', title: 'Invoice Management' },
|
<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" />
|
||||||
{ prefix: '/admin/review', title: 'Review Management' },
|
</svg>
|
||||||
{ prefix: '/admin/support', title: 'Support Management' },
|
);
|
||||||
{ prefix: '/admin/kb', title: 'Knowledge Base Management' },
|
}
|
||||||
{ prefix: '/admin/notifications', title: 'Notifications' },
|
|
||||||
{ prefix: '/admin/report', title: 'Report Management' },
|
|
||||||
{ prefix: '/admin/ledger', title: 'Ledger Management' },
|
|
||||||
{ prefix: '/admin/workspace', title: 'Dashboard Workspace' },
|
|
||||||
{ prefix: '/admin', title: 'Dashboard' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function AdminShell(props: { children: JSX.Element }) {
|
export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
@ -99,35 +68,46 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [checkedSession, setCheckedSession] = createSignal(false);
|
const [checkedSession, setCheckedSession] = createSignal(false);
|
||||||
const [adminName, setAdminName] = createSignal('Admin');
|
const [adminName, setAdminName] = createSignal('Admin');
|
||||||
const [adminRole, setAdminRole] = createSignal('Super Admin');
|
const [sidebarOpen, setSidebarOpen] = createSignal(false);
|
||||||
|
const [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>();
|
||||||
|
const [tabRefs, setTabRefs] = createSignal<Record<string, HTMLAnchorElement>>({});
|
||||||
|
const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false });
|
||||||
|
|
||||||
const tabs = createMemo<Tab[]>(() => {
|
const tabs = createMemo<Tab[]>(() => {
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
for (const set of TAB_SETS) {
|
for (const set of TAB_SETS) {
|
||||||
if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
|
if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) return set.tabs;
|
||||||
return set.tabs;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const isTabActive = (tab: Tab) => {
|
const isTabActive = (tab: Tab) => (tab.exact ? location.pathname === tab.href : location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`));
|
||||||
if (tab.exact) return location.pathname === tab.href;
|
|
||||||
return location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`);
|
const refreshTabIndicator = () => {
|
||||||
|
const activeTab = tabs().find((tab) => isTabActive(tab));
|
||||||
|
const track = tabsTrackEl();
|
||||||
|
if (!activeTab || !track) {
|
||||||
|
setTabIndicator((prev) => ({ ...prev, ready: false }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = tabRefs()[activeTab.href];
|
||||||
|
if (!el) return;
|
||||||
|
setTabIndicator({ left: el.offsetLeft, width: el.offsetWidth, ready: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const pageTitle = createMemo(() => {
|
createEffect(() => {
|
||||||
const path = location.pathname;
|
tabs();
|
||||||
for (const item of PAGE_TITLES) {
|
location.pathname;
|
||||||
if (path === item.prefix || path.startsWith(`${item.prefix}/`)) return item.title;
|
requestAnimationFrame(refreshTabIndicator);
|
||||||
}
|
|
||||||
return 'Dashboard';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
const onResize = () => refreshTabIndicator();
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
onCleanup(() => window.removeEventListener('resize', onResize));
|
||||||
|
|
||||||
const isLocalDev =
|
const isLocalDev =
|
||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
||||||
(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');
|
||||||
|
|
@ -146,10 +126,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const accessToken =
|
const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
||||||
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: {
|
||||||
|
|
@ -162,7 +139,6 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const payload = await response.json().catch(() => ({}));
|
const payload = await response.json().catch(() => ({}));
|
||||||
if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized');
|
if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized');
|
||||||
if (payload?.full_name) setAdminName(payload.full_name);
|
if (payload?.full_name) setAdminName(payload.full_name);
|
||||||
if (payload?.role?.name) setAdminRole(payload.role.name);
|
|
||||||
setCheckedSession(true);
|
setCheckedSession(true);
|
||||||
} catch {
|
} catch {
|
||||||
clearAdminSession();
|
clearAdminSession();
|
||||||
|
|
@ -188,110 +164,126 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const initials = () => adminName().charAt(0).toUpperCase() || 'A';
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-[#f9f9fd]">
|
<div class="min-h-screen bg-[#f0f1f6]">
|
||||||
{/* ── Fixed Header ── */}
|
<header class="fixed inset-x-0 top-0 z-50 border-b border-[#d8dbe3] bg-[#f6f7fb]">
|
||||||
<header class="fixed top-0 z-50 flex h-16 w-full items-center justify-between bg-white/80 px-6 shadow-[0_4px_24px_rgba(10,29,55,0.04)] backdrop-blur-xl">
|
<div class="flex h-[86px] items-center justify-between px-8">
|
||||||
{/* Left: logo */}
|
<div class="flex min-w-0 items-center gap-6">
|
||||||
<div class="flex w-64 shrink-0 items-center">
|
<button
|
||||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-8 w-auto object-contain" />
|
type="button"
|
||||||
</div>
|
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)}
|
||||||
{/* Center: search */}
|
aria-label="Toggle sidebar"
|
||||||
<div class="relative mx-8 w-full max-w-xl">
|
|
||||||
<svg class="absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<circle cx="11" cy="11" r="8"/><path stroke-linecap="round" d="M21 21l-4.35-4.35"/>
|
|
||||||
</svg>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search roles, users, or reports…"
|
|
||||||
class="w-full rounded-full border-0 bg-slate-50 py-2 pl-10 pr-4 text-sm text-[#0a1d37] placeholder-slate-400 outline-none ring-0 transition-all focus:bg-white focus:ring-2 focus:ring-[#fd6216]/20"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right: actions + avatar */}
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
{/* Notification bell */}
|
|
||||||
<button type="button" aria-label="Notifications"
|
|
||||||
class="relative flex h-9 w-9 items-center justify-center rounded-full text-[#0a1d37]/60 transition-colors hover:bg-slate-50"
|
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
<svg class="h-4 w-4" 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" d="M4 7h16M4 12h16M4 17h16" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.73 21a2 2 0 0 1-3.46 0" />
|
|
||||||
</svg>
|
|
||||||
<span class="absolute right-2 top-2 h-2 w-2 rounded-full border-2 border-white bg-[#fd6216]" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Settings */}
|
|
||||||
<button type="button" aria-label="Settings"
|
|
||||||
class="flex h-9 w-9 items-center justify-center rounded-full text-[#0a1d37]/60 transition-colors hover:bg-slate-50"
|
|
||||||
>
|
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
||||||
<circle cx="12" cy="12" r="3"/>
|
|
||||||
<path stroke-linecap="round" d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<A href="/admin" class="flex shrink-0 items-center">
|
||||||
<div class="h-8 w-px bg-slate-200" />
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-12 w-auto object-contain" />
|
||||||
|
</A>
|
||||||
{/* Name + avatar */}
|
|
||||||
<div class="flex items-center gap-2.5">
|
|
||||||
<span class="text-sm font-bold text-[#0a1d37]">{adminName()}</span>
|
|
||||||
<div class="flex h-8 w-8 items-center justify-center rounded-full border-2 border-[#fd6216] bg-[#fd6216] text-xs font-black text-white">
|
|
||||||
{initials()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Logout */}
|
<div class="flex items-center gap-4">
|
||||||
<button type="button" onClick={onLogout} aria-label="Logout"
|
<div class="hidden h-[54px] w-[760px] items-center gap-3 rounded-2xl border border-[#daddE8] bg-[#edeef4] px-5 text-[15px] text-[#6a7285] lg:flex">
|
||||||
class="flex h-9 w-9 items-center justify-center rounded-full text-[#0a1d37]/60 transition-colors hover:bg-red-50 hover:text-red-500"
|
<IconSearch />
|
||||||
>
|
<span>Search system operations...</span>
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
</div>
|
||||||
<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" />
|
<div class="hidden h-10 w-px bg-[#d9dde7] lg:block" />
|
||||||
</svg>
|
<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>
|
||||||
|
<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-3 lg:flex">
|
||||||
|
<div class="text-right leading-tight">
|
||||||
|
<p class="text-[17px] font-semibold text-[#111827]">{adminName()}</p>
|
||||||
|
<p class="text-[14px] text-[#6b7280]">Administrator</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex h-11 w-11 items-center justify-center overflow-hidden rounded-xl border border-[#d9dce7] bg-gradient-to-br from-[#fef3eb] to-[#ffd9c4] text-[15px] font-bold text-[#fd6216]">
|
||||||
|
{adminInitials()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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-[86px] flex">
|
||||||
{/* Sidebar */}
|
<div
|
||||||
<AdminSidebar />
|
class={`absolute inset-0 z-20 bg-[#0a1d37]/35 transition-opacity lg:hidden ${sidebarOpen() ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
|
||||||
|
onClick={() => setSidebarOpen(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Main content */}
|
<div class={`absolute inset-y-0 left-0 z-30 w-[310px] -translate-x-full transition-transform duration-200 lg:static lg:translate-x-0 ${sidebarOpen() ? 'translate-x-0' : ''}`}>
|
||||||
<main class="scrollbar min-w-0 overflow-y-auto bg-[#f9f9fd] p-8">
|
<AdminSidebar onNavigate={() => setSidebarOpen(false)} onLogout={onLogout} />
|
||||||
{/* Sub-tabs (shown for multi-tab sections) */}
|
|
||||||
{tabs().length > 0 && (
|
|
||||||
<div class="mb-6 flex gap-1 border-b border-slate-200">
|
|
||||||
{tabs().map((tab) => (
|
|
||||||
<A
|
|
||||||
href={tab.href}
|
|
||||||
class={`relative px-3 pb-2.5 pt-0.5 text-[13px] font-medium transition-colors ${
|
|
||||||
isTabActive(tab)
|
|
||||||
? '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-slate-500 hover:text-slate-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</A>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<main class="scrollbar min-w-0 flex-1 overflow-y-auto bg-[#f0f1f6] px-8 pb-8 pt-9">
|
||||||
|
<ShowTabs
|
||||||
|
tabs={tabs()}
|
||||||
|
isTabActive={isTabActive}
|
||||||
|
setTabsTrackEl={setTabsTrackEl}
|
||||||
|
setTabRefs={setTabRefs}
|
||||||
|
tabIndicator={tabIndicator}
|
||||||
|
/>
|
||||||
{props.children}
|
{props.children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* Session check loading state */
|
<div class="fixed inset-0 top-[86px] flex">
|
||||||
<div class="fixed inset-0 top-16 grid grid-cols-[auto_1fr]">
|
<div class="hidden w-[310px] border-r border-[#d7d8df] bg-[#f6f6f8] lg:block" />
|
||||||
<div class="w-64 border-r border-slate-100 bg-white" />
|
<main class="flex flex-1 items-center justify-center bg-[#f0f1f6]">
|
||||||
<main class="flex 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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ShowTabs(props: {
|
||||||
|
tabs: Tab[];
|
||||||
|
isTabActive: (tab: Tab) => boolean;
|
||||||
|
setTabsTrackEl: (el: HTMLDivElement) => void;
|
||||||
|
setTabRefs: (fn: (prev: Record<string, HTMLAnchorElement>) => Record<string, HTMLAnchorElement>) => void;
|
||||||
|
tabIndicator: () => { left: number; width: number; ready: boolean };
|
||||||
|
}) {
|
||||||
|
if (props.tabs.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={props.setTabsTrackEl} class="relative mb-7 flex items-center gap-8 border-b border-[#d8dbe3] px-1">
|
||||||
|
<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 rounded-t-xl px-6 pb-4 pt-4 text-[16px] font-semibold transition-colors ${
|
||||||
|
props.isTabActive(tab) ? 'bg-white text-[#111827]' : 'text-[#636b7f] hover:text-[#111827]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</A>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<div
|
||||||
|
class={`absolute bottom-0 h-[3px] 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,144 +1,122 @@
|
||||||
import { A, useLocation } from '@solidjs/router';
|
import { A, useLocation } from '@solidjs/router';
|
||||||
|
import { For, Show } from 'solid-js';
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
Briefcase,
|
||||||
|
ClipboardList,
|
||||||
|
FileText,
|
||||||
|
FolderCog,
|
||||||
|
HandHelping,
|
||||||
|
LayoutGrid,
|
||||||
|
Percent,
|
||||||
|
Receipt,
|
||||||
|
Sparkles,
|
||||||
|
UserCircle2,
|
||||||
|
Users,
|
||||||
|
WalletCards,
|
||||||
|
} from 'lucide-solid';
|
||||||
|
|
||||||
type LinkItem = {
|
type Item = {
|
||||||
href: string;
|
href: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
iconPath?: string;
|
||||||
|
icon?: any;
|
||||||
aliasPrefix?: string;
|
aliasPrefix?: string;
|
||||||
group: string;
|
separatorBefore?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const links: LinkItem[] = [
|
const items: Item[] = [
|
||||||
{ href: '/admin', label: 'Dashboard', icon: 'dashboard.svg', group: '__top__' },
|
{ href: '/admin', label: 'Dashboard', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid },
|
||||||
|
{ href: '/admin/roles', label: 'Internal Role Management', iconPath: '/sidebar-icons/role.svg', icon: FolderCog },
|
||||||
{ href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg', group: 'Management' },
|
{ href: '/admin/runtime-roles', label: 'External Role Management', iconPath: '/sidebar-icons/users.svg', icon: Users },
|
||||||
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg', group: 'Management' },
|
{ href: '/admin/onboarding-management', label: 'External Onboarding', iconPath: '/sidebar-icons/users.svg', icon: Users, aliasPrefix: '/admin/onboarding-schemas' },
|
||||||
{ href: '/admin/onboarding-schemas', label: 'External Onboarding', icon: 'reviews.svg', group: 'Management' },
|
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboards', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid },
|
||||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Mgmt.', icon: 'dashboard.svg', group: 'Management' },
|
{ href: '/admin/external-dashboard-management', label: 'External Dashboards', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid, aliasPrefix: '/admin/role-ui-configs' },
|
||||||
{ href: '/admin/external-dashboard-management', label: 'External Dashboard Mgmt.', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs', group: 'Management' },
|
{ href: '/admin/approval', label: 'Approval Management', iconPath: '/sidebar-icons/approval.svg', icon: ClipboardList },
|
||||||
{ href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg', group: 'Management' },
|
{ href: '/admin/department', label: 'Department Management', iconPath: '/sidebar-icons/department.svg', icon: Briefcase, separatorBefore: true },
|
||||||
{ href: '/admin/department', label: 'Department Management', icon: 'department.svg', group: 'Management' },
|
{ href: '/admin/designation', label: 'Designation Management', iconPath: '/sidebar-icons/designation.svg', icon: Briefcase },
|
||||||
{ href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg', group: 'Management' },
|
{ href: '/admin/employees', label: 'Employee Management', iconPath: '/sidebar-icons/users.svg', icon: UserCircle2 },
|
||||||
{ href: '/admin/employees', label: 'Employee Management', icon: 'users.svg', group: 'Management' },
|
{ href: '/admin/users', label: 'Users Management', iconPath: '/sidebar-icons/users.svg', icon: Users },
|
||||||
{ href: '/admin/users', label: 'Users Management', icon: 'users.svg', group: 'Management' },
|
{ href: '/admin/company', label: 'Company Management', iconPath: '/sidebar-icons/company.svg', icon: Briefcase },
|
||||||
{ href: '/admin/company', label: 'Company Management', icon: 'company.svg', group: 'Management' },
|
{ href: '/admin/candidate', label: 'Candidate Management', iconPath: '/sidebar-icons/candidate.svg', icon: UserCircle2 },
|
||||||
{ href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg', group: 'Management' },
|
{ href: '/admin/customer', label: 'Customer Management', iconPath: '/sidebar-icons/support.svg', icon: UserCircle2 },
|
||||||
{ href: '/admin/customer', label: 'Customer Management', icon: 'users.svg', group: 'Management' },
|
{ href: '/admin/photographer', label: 'Photographer Management', iconPath: '/sidebar-icons/photographer.svg', icon: Sparkles, separatorBefore: true },
|
||||||
{ href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg', group: 'Management' },
|
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', iconPath: '/sidebar-icons/makeup-artist.svg', icon: Sparkles },
|
||||||
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg',group: 'Management' },
|
{ href: '/admin/tutors', label: 'Tutors Management', iconPath: '/sidebar-icons/tutor.svg', icon: Sparkles },
|
||||||
{ href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg', group: 'Management' },
|
{ href: '/admin/developers', label: 'Developers Management', iconPath: '/sidebar-icons/developers.svg', icon: Sparkles },
|
||||||
{ href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg', group: 'Management' },
|
{ href: '/admin/jobs', label: 'Jobs Management', iconPath: '/sidebar-icons/jobs.svg', icon: Briefcase },
|
||||||
{ href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg', group: 'Management' },
|
{ href: '/admin/leads', label: 'Leads Management', iconPath: '/sidebar-icons/leads.svg', icon: HandHelping },
|
||||||
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg', group: 'Management' },
|
{ href: '/admin/pricing', label: 'Pricing Management', iconPath: '/sidebar-icons/pricing.svg', icon: WalletCards },
|
||||||
{ href: '/admin/catering-services', label: 'Catering Services', icon: 'company.svg', group: 'Management' },
|
{ href: '/admin/credit', label: 'Credit Management', iconPath: '/sidebar-icons/credits.svg', icon: WalletCards },
|
||||||
{ href: '/admin/graphic-designers', label: 'Graphics Designer Mgmt.', icon: 'developers.svg', group: 'Management' },
|
{ href: '/admin/coupon', label: 'Coupon Management', iconPath: '/sidebar-icons/coupon.svg', icon: Percent },
|
||||||
{ href: '/admin/social-media-managers', label: 'Social Media Mgr.', icon: 'developers.svg', group: 'Management' },
|
{ href: '/admin/discount', label: 'Discount Management', iconPath: '/sidebar-icons/discount.svg', icon: Percent },
|
||||||
{ href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg', group: 'Management' },
|
{ href: '/admin/tax', label: 'Tax Management', iconPath: '/sidebar-icons/tax.svg', icon: Receipt },
|
||||||
{ href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg', group: 'Management' },
|
{ href: '/admin/order', label: 'Order Management', iconPath: '/sidebar-icons/order.svg', icon: FileText },
|
||||||
|
{ href: '/admin/invoice', label: 'Invoice Management', iconPath: '/sidebar-icons/invoice.svg', icon: FileText },
|
||||||
{ href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg', group: 'Finance' },
|
{ href: '/admin/review', label: 'Review Management', iconPath: '/sidebar-icons/reviews.svg', icon: FileText },
|
||||||
{ href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg', group: 'Finance' },
|
{ href: '/admin/support', label: 'Support Management', iconPath: '/sidebar-icons/support.svg', icon: UserCircle2 },
|
||||||
{ href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg', group: 'Finance' },
|
{ href: '/admin/report', label: 'Report Management', iconPath: '/sidebar-icons/report.svg', icon: Bell },
|
||||||
{ href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg', group: 'Finance' },
|
{ href: '/admin/ledger', label: 'Ledger Management', iconPath: '/sidebar-icons/ledger.svg', icon: Receipt },
|
||||||
{ href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg', group: 'Finance' },
|
{ href: '/admin/kb', label: 'Knowledge Base', icon: FileText },
|
||||||
{ href: '/admin/order', label: 'Order Management', icon: 'order.svg', group: 'Finance' },
|
{ href: '/admin/notifications', label: 'Notifications', icon: Bell },
|
||||||
{ href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg', group: 'Finance' },
|
|
||||||
{ href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg', group: 'Finance' },
|
|
||||||
|
|
||||||
{ href: '/admin/review', label: 'Review Management', icon: 'reviews.svg', group: 'Platform' },
|
|
||||||
{ href: '/admin/support', label: 'Support Management', icon: 'support.svg', group: 'Platform' },
|
|
||||||
{ href: '/admin/kb', label: 'Knowledge Base', icon: 'reviews.svg', group: 'Platform' },
|
|
||||||
{ href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg', group: 'Platform' },
|
|
||||||
{ href: '/admin/report', label: 'Report Management', icon: 'report.svg', group: 'Platform' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function AdminSidebar() {
|
function renderIcon(item: Item, isActive: boolean) {
|
||||||
|
if (item.iconPath) {
|
||||||
|
return <img src={item.iconPath} alt="" class={`h-[18px] w-[18px] object-contain ${isActive ? 'opacity-95' : 'opacity-70'}`} />;
|
||||||
|
}
|
||||||
|
const Icon = item.icon || FileText;
|
||||||
|
return <Icon size={18} class={isActive ? 'text-[#0f172a]' : 'text-slate-500'} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdminSidebar(props: { onNavigate?: () => void; onLogout?: () => void }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const isActive = (href: string, aliasPrefix?: string) => {
|
const active = (item: Item) => {
|
||||||
if (href === '/admin') return location.pathname === '/admin';
|
if (item.href === '/admin') return location.pathname === '/admin';
|
||||||
if (aliasPrefix && location.pathname.startsWith(aliasPrefix)) return true;
|
if (item.aliasPrefix && location.pathname.startsWith(item.aliasPrefix)) return true;
|
||||||
return location.pathname === href || location.pathname.startsWith(`${href}/`);
|
return location.pathname === item.href || location.pathname.startsWith(`${item.href}/`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavItem = (item: LinkItem) => {
|
|
||||||
const active = isActive(item.href, item.aliasPrefix);
|
|
||||||
return (
|
return (
|
||||||
|
<aside class="flex h-full w-[310px] flex-col border-r border-[#d7d8df] bg-[#f6f6f8]">
|
||||||
|
<nav class="scrollbar flex-1 overflow-y-auto px-6 py-5">
|
||||||
|
<For each={items}>
|
||||||
|
{(item) => {
|
||||||
|
const isActive = active(item);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={item.separatorBefore}>
|
||||||
|
<div class="my-4 border-t border-[#dfdfe5]" />
|
||||||
|
</Show>
|
||||||
<A
|
<A
|
||||||
href={item.href}
|
href={item.href}
|
||||||
activeClass=""
|
onClick={() => props.onNavigate?.()}
|
||||||
inactiveClass=""
|
class={`group relative mb-1.5 flex min-h-[48px] items-center gap-3 rounded-xl px-4 text-[17px] font-medium leading-tight transition ${
|
||||||
title={item.label}
|
isActive ? 'bg-[#ffece3] text-[#fd6216]' : 'text-[#2f3647] hover:bg-white hover:text-[#111827]'
|
||||||
class={`relative flex min-w-0 items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-semibold transition-colors duration-150 ${
|
|
||||||
active
|
|
||||||
? 'bg-slate-100 text-[#0a1d37] before:absolute before:left-0 before:top-1/2 before:h-5 before:-translate-y-1/2 before:w-[3px] before:rounded-r-full before:bg-[#fd6216] before:content-[\'\']'
|
|
||||||
: 'text-[#0a1d37]/55 hover:bg-slate-50 hover:text-[#0a1d37]'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<span class={`absolute bottom-0 left-0 top-0 w-[4px] rounded-r-md bg-[#fd6216] transition-opacity ${isActive ? 'opacity-100' : 'opacity-0'}`} />
|
||||||
src={`/sidebar-icons/${item.icon}`}
|
{renderIcon(item, isActive)}
|
||||||
alt=""
|
|
||||||
class="h-4 w-4 shrink-0"
|
|
||||||
style={active ? 'opacity:0.85' : 'opacity:0.35'}
|
|
||||||
/>
|
|
||||||
<span class="truncate">{item.label}</span>
|
<span class="truncate">{item.label}</span>
|
||||||
</A>
|
</A>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
}}
|
||||||
|
</For>
|
||||||
|
</nav>
|
||||||
|
|
||||||
const topLinks = links.filter(l => l.group === '__top__');
|
<div class="border-t border-[#dfdfe5] px-6 py-5">
|
||||||
const mgmtLinks = links.filter(l => l.group === 'Management');
|
<button
|
||||||
const finLinks = links.filter(l => l.group === 'Finance');
|
type="button"
|
||||||
const platLinks = links.filter(l => l.group === 'Platform');
|
onClick={() => props.onLogout?.()}
|
||||||
|
class="flex h-[50px] w-full items-center gap-3 rounded-xl px-4 text-left text-[17px] font-semibold text-[#c51d1d] transition hover:bg-[#fff1f1]"
|
||||||
return (
|
>
|
||||||
<aside class="flex h-full w-60 flex-col border-r border-slate-100 bg-white">
|
<svg class="h-[18px] w-[18px]" 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" />
|
||||||
{/* Top: Dashboard link */}
|
</svg>
|
||||||
<div class="px-3 pt-4 pb-2">
|
<span>Sign Out</span>
|
||||||
{topLinks.map(NavItem)}
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Scrollable groups */}
|
|
||||||
<div class="scrollbar flex-1 overflow-y-auto px-3 pb-3" style="min-height:0">
|
|
||||||
|
|
||||||
{/* Management */}
|
|
||||||
<p class="mt-3 mb-1.5 px-2 text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
|
||||||
Management
|
|
||||||
</p>
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
{mgmtLinks.map(NavItem)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Finance */}
|
|
||||||
<p class="mt-4 mb-1.5 px-2 text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
|
||||||
Finance
|
|
||||||
</p>
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
{finLinks.map(NavItem)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Platform */}
|
|
||||||
<p class="mt-4 mb-1.5 px-2 text-[10px] font-bold uppercase tracking-widest text-slate-400">
|
|
||||||
Platform
|
|
||||||
</p>
|
|
||||||
<div class="space-y-0.5">
|
|
||||||
{platLinks.map(NavItem)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* User card */}
|
|
||||||
<div class="border-t border-slate-100 px-3 py-3">
|
|
||||||
<div class="flex items-center gap-2.5 rounded-lg bg-slate-50 px-3 py-2.5">
|
|
||||||
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[#0a1d37] text-xs font-black text-white">
|
|
||||||
A
|
|
||||||
</div>
|
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<p class="truncate text-[12px] font-bold text-[#0a1d37]">Admin User</p>
|
|
||||||
<p class="truncate text-[10px] text-slate-400">master_admin@nxtgauge.io</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { createResource, createSignal, Show } from 'solid-js';
|
import { createResource, createSignal, Show } from 'solid-js';
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
|
import { Eye, Pencil, Trash2 } from 'lucide-solid';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '/api/gateway';
|
||||||
|
|
||||||
|
|
@ -50,12 +51,9 @@ export default function InternalRolesPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="mb-6 flex items-start justify-between gap-4">
|
<div class="mb-8">
|
||||||
<div>
|
<h1 class="text-[52px] font-extrabold tracking-tight text-[#071b3d] sm:text-[36px] lg:text-[52px]">Roles Management</h1>
|
||||||
<h1 class="text-2xl font-bold text-gray-900">Internal Role Management</h1>
|
<p class="mt-2 text-[17px] text-[#4b5563]">Configure and maintain internal system roles and access privileges.</p>
|
||||||
<p class="mt-1 text-sm text-gray-500">Manage internal employee roles and permissions from one clean list.</p>
|
|
||||||
</div>
|
|
||||||
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/roles/create">Create Internal Role</A>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="hidden" aria-label="Role Management Navigation">
|
<nav class="hidden" aria-label="Role Management Navigation">
|
||||||
|
|
@ -68,55 +66,60 @@ export default function InternalRolesPage() {
|
||||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{deleteError()}</div>
|
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{deleteError()}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
|
<section class="overflow-hidden rounded-[22px] border border-[#d8dbe5] bg-white shadow-[0_14px_28px_-20px_rgba(15,23,42,0.35)]">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full min-w-[860px] text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr class="bg-[#071b3d] text-white">
|
||||||
<th>Name</th>
|
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">ID</th>
|
||||||
<th>Description</th>
|
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Name</th>
|
||||||
<th class="text-right">Actions</th>
|
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Issue Type</th>
|
||||||
|
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Edit</th>
|
||||||
|
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Delete</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<Show when={roles.loading}>
|
<Show when={roles.loading}>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" style="text-align:center;padding:32px;color:#64748b;">Loading internal roles...</td>
|
<td colspan="5" style="text-align:center;padding:32px;color:#64748b;">Loading internal roles...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!roles.loading && roles.error}>
|
<Show when={!roles.loading && roles.error}>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" style="text-align:center;padding:32px;color:#b91c1c;">Failed to load roles. Is the backend running?</td>
|
<td colspan="5" style="text-align:center;padding:32px;color:#b91c1c;">Failed to load roles. Is the backend running?</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
|
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3" style="text-align:center;padding:32px;color:#94a3b8;">No internal roles found. Create your first role.</td>
|
<td colspan="5" style="text-align:center;padding:32px;color:#94a3b8;">No internal roles found. Create your first role.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
|
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
|
||||||
{roles()!.map((role) => (
|
{roles()!.map((role) => (
|
||||||
<tr>
|
<tr class="border-b border-[#e4e7ef] text-[17px]">
|
||||||
<td>
|
<td class="px-8 py-7 font-medium text-[#364152]">{role.code || role.id?.slice(0, 6).toUpperCase() || 'ROL247'}</td>
|
||||||
<div>
|
<td class="px-8 py-7 font-semibold text-[#0f172a]">{role.name}</td>
|
||||||
<p style="margin:0;font-weight:600;color:#0f172a;">{role.name}</p>
|
<td class="px-8 py-7">
|
||||||
<p style="margin:2px 0 0;font-size:11px;color:#94a3b8;">{role.code || role.id?.slice(0, 8)}</p>
|
<A class="inline-flex items-center gap-2 font-semibold text-[#fd6216] hover:text-orange-700" href={`/admin/roles/${role.id}`} title="View Role" aria-label={`View ${role.name}`}>
|
||||||
</div>
|
<span>View</span>
|
||||||
|
<Eye size={16} />
|
||||||
|
</A>
|
||||||
</td>
|
</td>
|
||||||
<td style="color:#475569;">{role.description || 'No description added yet.'}</td>
|
<td class="px-8 py-7 text-center">
|
||||||
<td>
|
<A class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#071b3d] hover:bg-slate-100" href={`/admin/roles/${role.id}/edit`} title="Edit Role" aria-label={`Edit ${role.name}`}>
|
||||||
<div class="flex items-center justify-end gap-1">
|
<Pencil size={17} />
|
||||||
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/roles/${role.id}`} title="View Role">👁</A>
|
</A>
|
||||||
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/roles/${role.id}/edit`} title="Edit Role">✎</A>
|
</td>
|
||||||
|
<td class="px-8 py-7 text-center">
|
||||||
<button
|
<button
|
||||||
class="rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm"
|
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#c81e1e] hover:bg-red-50 disabled:opacity-60"
|
||||||
disabled={deleting() === role.id}
|
disabled={deleting() === role.id}
|
||||||
onClick={() => handleDelete(role.id, role.name)}
|
onClick={() => handleDelete(role.id, role.name)}
|
||||||
title="Delete Role"
|
title="Delete Role"
|
||||||
|
aria-label={`Delete ${role.name}`}
|
||||||
>
|
>
|
||||||
{deleting() === role.id ? '...' : '🗑'}
|
{deleting() === role.id ? '...' : <Trash2 size={17} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -124,6 +127,16 @@ export default function InternalRolesPage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between border-t border-[#e4e7ef] px-8 py-5">
|
||||||
|
<p class="text-[14px] font-semibold uppercase tracking-[0.1em] text-[#485163]">Showing 1 to 5 of {(roles()?.length || 0) || 5} entries</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'<'}</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl bg-[#fd6216] px-3 font-bold text-white">1</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">2</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">3</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'>'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { A, useSearchParams } from '@solidjs/router';
|
import { A, useSearchParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, Show } from 'solid-js';
|
import { createMemo, createResource, Show } from 'solid-js';
|
||||||
import AdminShell from '~/components/AdminShell';
|
import AdminShell from '~/components/AdminShell';
|
||||||
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '/api/gateway';
|
||||||
|
|
||||||
|
|
@ -77,73 +76,49 @@ export default function RuntimeRolesPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminShell>
|
<AdminShell>
|
||||||
<div class="mb-6 flex items-start justify-between gap-4">
|
<div class="mb-8">
|
||||||
<div>
|
<h1 class="text-[52px] font-extrabold tracking-tight text-[#071b3d] sm:text-[36px] lg:text-[52px]">Roles Management</h1>
|
||||||
<h1 class="text-2xl font-bold text-gray-900">External Role Management</h1>
|
<p class="mt-2 text-[17px] text-[#4b5563]">Configure and maintain external system roles and access privileges.</p>
|
||||||
<p class="mt-1 text-sm text-gray-500">Manage canonical external runtime roles, enabled modules, onboarding assignment, approval gates, and default runtime destinations from one place.</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/role-ui-configs">Inspector</A>
|
|
||||||
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/runtime-roles/new">Create External Role</A>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ExternalRoleTabs />
|
<section class="overflow-hidden rounded-[22px] border border-[#d8dbe5] bg-white shadow-[0_14px_28px_-20px_rgba(15,23,42,0.35)]">
|
||||||
|
|
||||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0">
|
|
||||||
<div>
|
|
||||||
<h2 style="margin:0;font-size:17px;font-weight:700">Published External Roles</h2>
|
|
||||||
<p style="margin:4px 0 0;font-size:12px;color:#64748b">Only canonical external runtime roles are shown here. Legacy or malformed role rows are hidden from this management surface.</p>
|
|
||||||
</div>
|
|
||||||
<Show when={!roles.loading}>
|
|
||||||
<span style="font-size:13px;color:#64748b">{roles()?.length || 0} roles</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full min-w-[860px] text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr class="bg-[#071b3d] text-white">
|
||||||
<th>Role</th>
|
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">ID</th>
|
||||||
<th>Type</th>
|
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Name</th>
|
||||||
<th>Modules</th>
|
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Issue Type</th>
|
||||||
<th>Schema</th>
|
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Edit</th>
|
||||||
<th>Status</th>
|
<th class="px-8 py-5 text-center text-[14px] font-semibold uppercase tracking-[0.08em]">Delete</th>
|
||||||
<th class="text-right">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<Show when={roles.loading}>
|
<Show when={roles.loading}>
|
||||||
<tr><td colspan="6" style="text-align:center;padding:32px;color:#64748b">Loading external roles...</td></tr>
|
<tr><td colspan="5" style="text-align:center;padding:32px;color:#64748b">Loading external roles...</td></tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!roles.loading && roles.error}>
|
<Show when={!roles.loading && roles.error}>
|
||||||
<tr><td colspan="6" style="text-align:center;padding:32px;color:#b91c1c">Failed to load external roles. Is the backend running?</td></tr>
|
<tr><td colspan="5" style="text-align:center;padding:32px;color:#b91c1c">Failed to load external roles. Is the backend running?</td></tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
|
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
|
||||||
<tr><td colspan="6" style="text-align:center;padding:32px;color:#94a3b8">No external roles configured yet.</td></tr>
|
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No external roles configured yet.</td></tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
|
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
|
||||||
{roles()!.map((role) => (
|
{roles()!.map((role) => (
|
||||||
<tr class={selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-orange-50' : ''}>
|
<tr class={`border-b border-[#e4e7ef] text-[17px] ${selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-[#fff7f2]' : ''}`}>
|
||||||
<td>
|
<td class="px-8 py-7 font-medium text-[#364152]">{role.roleKey || role.id?.slice(0, 6).toUpperCase()}</td>
|
||||||
<div>
|
<td class="px-8 py-7 font-semibold text-[#0f172a]">{role.displayName}</td>
|
||||||
<p style="margin:0;font-weight:600;color:#0f172a">{role.displayName}</p>
|
<td class="px-8 py-7">
|
||||||
<p style="margin:2px 0 0;font-size:11px;color:#94a3b8">{role.roleKey}</p>
|
<A class="inline-flex items-center gap-2 font-semibold text-[#fd6216] hover:text-orange-700" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} target="_blank" rel="noreferrer">
|
||||||
</div>
|
<span>View</span>
|
||||||
|
<span class="text-[18px]">↗</span>
|
||||||
|
</A>
|
||||||
</td>
|
</td>
|
||||||
<td style="color:#475569">{role.vertical || '—'}</td>
|
<td class="px-8 py-7 text-center">
|
||||||
<td style="color:#475569">{role.enabledModules.length}</td>
|
<A class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#071b3d] hover:bg-slate-100" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} title="Edit External Role">✎</A>
|
||||||
<td style="color:#475569;font-size:12px">{role.onboardingSchemaId || '—'}</td>
|
|
||||||
<td>
|
|
||||||
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${role.isActive ? 'active' : ''}`}>
|
|
||||||
{role.isActive ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td class="px-8 py-7 text-center">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<button class="inline-flex h-9 w-9 items-center justify-center rounded-md text-[#c81e1e] hover:bg-red-50" title="Delete External Role" aria-label={`Delete ${role.displayName}`}>🗑</button>
|
||||||
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} target="_blank" rel="noreferrer" title="View External Role">👁</A>
|
|
||||||
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} title="Edit External Role">✎</A>
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
@ -151,6 +126,16 @@ export default function RuntimeRolesPage() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center justify-between border-t border-[#e4e7ef] px-8 py-5">
|
||||||
|
<p class="text-[14px] font-semibold uppercase tracking-[0.1em] text-[#485163]">Showing 1 to 5 of {(roles()?.length || 0) || 5} entries</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'<'}</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl bg-[#fd6216] px-3 font-bold text-white">1</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">2</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 font-bold text-[#111827]">3</button>
|
||||||
|
<button class="h-11 min-w-11 rounded-xl border border-[#d4d8e2] bg-white px-3 text-[#111827]">{'>'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</AdminShell>
|
</AdminShell>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useNavigate } from '@solidjs/router';
|
import { useNavigate } from '@solidjs/router';
|
||||||
import { createMemo, createSignal, onMount } from 'solid-js';
|
import { Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import { isExternalIdentity, pickManagementLoginError } from '~/lib/admin-auth';
|
import { isExternalIdentity, pickManagementLoginError } from '~/lib/admin-auth';
|
||||||
import { hasAdminSession, setAdminSession } from '~/lib/admin-session';
|
import { hasAdminSession, setAdminSession } from '~/lib/admin-session';
|
||||||
|
|
||||||
|
|
@ -307,26 +307,27 @@ export default function LoginPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="auth-page auth-page-login">
|
<main class="min-h-screen bg-[radial-gradient(circle_at_10%_10%,rgba(253,98,22,0.18),transparent_35%),radial-gradient(circle_at_90%_0%,rgba(34,197,94,0.12),transparent_32%),linear-gradient(180deg,#0f1630_0%,#0b1226_100%)]">
|
||||||
<div class="auth-bg" />
|
<div class="mx-auto grid min-h-screen w-full max-w-[1240px] grid-cols-1 items-center gap-6 px-4 py-8 lg:grid-cols-[1.05fr_0.95fr] lg:px-8">
|
||||||
<div class="auth-layout">
|
<section class="relative hidden min-h-[620px] overflow-hidden rounded-[28px] border border-white/20 bg-white/10 p-8 text-white shadow-[0_28px_60px_-34px_rgba(2,6,23,0.88)] backdrop-blur lg:block">
|
||||||
<section class="auth-visual">
|
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-12 w-auto brightness-0 invert" />
|
||||||
<p class="auth-visual-kicker">Internal Access</p>
|
<p class="mt-8 inline-flex rounded-full border border-white/30 bg-white/10 px-3 py-1 text-[11px] font-bold uppercase tracking-[0.1em] text-orange-100">Internal Admin Portal</p>
|
||||||
<h1>Welcome back to Nxtgauge.</h1>
|
<h1 class="mt-6 max-w-[520px] text-[52px] font-extrabold leading-[1.05] text-white">Welcome back to Nxtgauge.</h1>
|
||||||
<p>Sign in securely to access the admin control panel.</p>
|
<p class="mt-4 max-w-[520px] text-[17px] leading-relaxed text-slate-200">Sign in to manage operations, roles, and approval workflows from one secure control panel.</p>
|
||||||
<img
|
<div class="absolute bottom-8 left-8 right-8 rounded-2xl border border-white/20 bg-white/10 p-4 text-[14px] text-slate-100">
|
||||||
src="https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1200&q=80"
|
<p class="font-semibold">Secure login with internal access policies.</p>
|
||||||
alt="Office workspace"
|
</div>
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="auth-card auth-form-card">
|
<section class="rounded-[28px] border border-white/30 bg-white/95 p-6 shadow-[0_28px_60px_-34px_rgba(2,6,23,0.88)] backdrop-blur sm:p-8">
|
||||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="auth-logo" />
|
<div class="mx-auto w-full max-w-[560px]">
|
||||||
<h2 class="auth-title">{mode() === 'login' ? 'Employee Login' : 'Reset Password'}</h2>
|
<h2 class="text-[46px] font-extrabold tracking-tight text-[#101228] sm:text-[38px] lg:text-[46px]">{mode() === 'login' ? 'Sign In' : 'Reset Password'}</h2>
|
||||||
|
<p class="mt-2 text-[15px] text-[#535e7a]">{mode() === 'login' ? 'Internal team access only.' : 'Use your internal email to reset access.'}</p>
|
||||||
|
|
||||||
<form class="auth-form-grid">
|
<form class="mt-6 space-y-5">
|
||||||
<div class="field">
|
<div class="space-y-4">
|
||||||
<label>Email address</label>
|
<div>
|
||||||
|
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Email</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={email()}
|
value={email()}
|
||||||
|
|
@ -335,12 +336,18 @@ export default function LoginPage() {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
}}
|
}}
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
|
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{mode() === 'login' ? (
|
|
||||||
<>
|
<Show when={mode() === 'login'}>
|
||||||
<div class="field">
|
<div>
|
||||||
<label>Password</label>
|
<div class="mb-1 flex items-center justify-between gap-2">
|
||||||
|
<label class="text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Password</label>
|
||||||
|
<button type="button" class="text-[12px] font-bold text-[#fd6216] underline" onClick={() => switchMode('reset')}>
|
||||||
|
Forgot?
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password()}
|
value={password()}
|
||||||
|
|
@ -349,28 +356,15 @@ export default function LoginPage() {
|
||||||
clearMessages();
|
clearMessages();
|
||||||
}}
|
}}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
|
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="auth-switch">
|
</Show>
|
||||||
<button type="button" class="auth-link-btn" onClick={() => switchMode('reset')}>
|
|
||||||
Forgot password?
|
<Show when={mode() === 'reset'}>
|
||||||
</button>
|
<div class="space-y-4">
|
||||||
</div>
|
<div>
|
||||||
<div class="actions">
|
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">New Password</label>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn primary block-btn"
|
|
||||||
disabled={isSubmitting() || !canSubmitLoginCredentials()}
|
|
||||||
onClick={directSignIn}
|
|
||||||
>
|
|
||||||
{isSubmitting() ? 'Signing in...' : 'Sign in'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div class="field">
|
|
||||||
<label>New password</label>
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={newPassword()}
|
value={newPassword()}
|
||||||
|
|
@ -378,11 +372,12 @@ export default function LoginPage() {
|
||||||
setNewPassword(event.currentTarget.value);
|
setNewPassword(event.currentTarget.value);
|
||||||
clearMessages();
|
clearMessages();
|
||||||
}}
|
}}
|
||||||
placeholder="Enter your new password"
|
placeholder="Minimum 8 characters"
|
||||||
|
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div>
|
||||||
<label>Confirm password</label>
|
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Confirm Password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={confirmPassword()}
|
value={confirmPassword()}
|
||||||
|
|
@ -390,13 +385,13 @@ export default function LoginPage() {
|
||||||
setConfirmPassword(event.currentTarget.value);
|
setConfirmPassword(event.currentTarget.value);
|
||||||
clearMessages();
|
clearMessages();
|
||||||
}}
|
}}
|
||||||
placeholder="Confirm your new password"
|
placeholder="Confirm new password"
|
||||||
|
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-[15px] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={resetStep() === 'verify'}>
|
||||||
{resetStep() === 'verify' ? (
|
<div>
|
||||||
<div class="field">
|
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Verification Code</label>
|
||||||
<label>Verification code</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
|
|
@ -406,43 +401,60 @@ export default function LoginPage() {
|
||||||
setResetCode(event.currentTarget.value.replace(/\D/g, '').slice(0, 6));
|
setResetCode(event.currentTarget.value.replace(/\D/g, '').slice(0, 6));
|
||||||
clearMessages();
|
clearMessages();
|
||||||
}}
|
}}
|
||||||
placeholder="Enter 6-digit code"
|
placeholder="000000"
|
||||||
|
class="h-11 w-full rounded-xl border border-[#cfd4e3] bg-white px-3.5 text-center text-[18px] tracking-[0.2em] text-[#101228] outline-none transition focus:border-[#fd6216] focus:ring-2 focus:ring-[#ffd8c3]"
|
||||||
/>
|
/>
|
||||||
<p class="hint">Code sent to {maskedEmail() || email()}.</p>
|
<p class="mt-2 rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-[12px] text-emerald-900">
|
||||||
|
Code sent to <span class="font-semibold">{maskedEmail() || email()}</span>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div class="auth-switch">
|
<Show when={mode() === 'login'}>
|
||||||
<button type="button" class="auth-link-btn" onClick={() => switchMode('login')}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h-11 w-full rounded-xl bg-[#fd6216] text-[15px] font-bold text-white transition hover:bg-[#e4570f] disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled={isSubmitting() || !canSubmitLoginCredentials()}
|
||||||
|
onClick={directSignIn}
|
||||||
|
>
|
||||||
|
{isSubmitting() ? 'Signing In...' : 'Sign In'}
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={mode() === 'reset'}>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<button type="button" class="text-[13px] font-semibold text-[#fd6216] underline" onClick={() => switchMode('login')}>
|
||||||
Back to sign in
|
Back to sign in
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<Show when={resetStep() === 'request'} fallback={(
|
||||||
{resetStep() === 'request' ? (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn primary block-btn"
|
class="h-11 w-full rounded-xl bg-[#fd6216] text-[15px] font-bold text-white transition hover:bg-[#e4570f] disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
disabled={isSubmitting() || !canSubmitResetRequest()}
|
|
||||||
onClick={requestResetCode}
|
|
||||||
>
|
|
||||||
{isSubmitting() ? 'Sending code...' : 'Send reset code'}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn primary block-btn"
|
|
||||||
disabled={isSubmitting() || !canSubmitResetVerify()}
|
disabled={isSubmitting() || !canSubmitResetVerify()}
|
||||||
onClick={verifyResetCode}
|
onClick={verifyResetCode}
|
||||||
>
|
>
|
||||||
{isSubmitting() ? 'Resetting password...' : 'Verify & reset password'}
|
{isSubmitting() ? 'Resetting Password...' : 'Verify & Reset Password'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}>
|
||||||
</div>
|
<button
|
||||||
</>
|
type="button"
|
||||||
)}
|
class="h-11 w-full rounded-xl bg-[#fd6216] text-[15px] font-bold text-white transition hover:bg-[#e4570f] disabled:cursor-not-allowed disabled:opacity-70"
|
||||||
|
disabled={isSubmitting() || !canSubmitResetRequest()}
|
||||||
|
onClick={requestResetCode}
|
||||||
|
>
|
||||||
|
{isSubmitting() ? 'Sending Code...' : 'Send Reset Code'}
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
</form>
|
</form>
|
||||||
{info() ? <p class="inline-note auth-inline-msg">{info()}</p> : null}
|
|
||||||
{error() ? <p class="error-note auth-inline-msg">{error()}</p> : null}
|
{info() ? <p class="mt-4 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-[14px] text-emerald-700">{info()}</p> : null}
|
||||||
|
{error() ? <p class="mt-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-[14px] text-red-700">{error()}</p> : null}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue