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 { 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 { isExternalIdentity } from '~/lib/admin-auth';
|
||||
import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session';
|
||||
|
|
@ -10,88 +10,57 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [
|
|||
{
|
||||
prefixes: ['/admin/roles'],
|
||||
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/templates', label: 'Role Templates' },
|
||||
{ href: '/admin/roles/templates', label: 'View Roles' },
|
||||
],
|
||||
},
|
||||
{
|
||||
prefixes: ['/admin/runtime-roles'],
|
||||
tabs: [
|
||||
{ href: '/admin/runtime-roles', label: 'External Roles', exact: true },
|
||||
{ href: '/admin/runtime-roles/new', label: 'Create External Role' },
|
||||
],
|
||||
},
|
||||
{
|
||||
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' },
|
||||
{ href: '/admin/runtime-roles', label: 'Roles', exact: true },
|
||||
{ href: '/admin/runtime-roles/new', label: 'Create Role' },
|
||||
{ href: '/admin/role-ui-configs', label: 'View Roles' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const PAGE_TITLES: Array<{ prefix: string; title: string }> = [
|
||||
{ prefix: '/admin/employees', title: 'Employee Management' },
|
||||
{ prefix: '/admin/department', title: 'Department Management' },
|
||||
{ prefix: '/admin/designation', title: 'Designation Management' },
|
||||
{ prefix: '/admin/roles', title: 'Internal Role Management' },
|
||||
{ prefix: '/admin/runtime-roles', title: 'External Role Management' },
|
||||
{ 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' },
|
||||
{ prefix: '/admin/approval', title: 'Approval Management' },
|
||||
{ prefix: '/admin/users', title: 'Users Management' },
|
||||
{ prefix: '/admin/company', title: 'Company Management' },
|
||||
{ prefix: '/admin/candidate', title: 'Candidate Management' },
|
||||
{ prefix: '/admin/customer', title: 'Customer Management' },
|
||||
{ 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' },
|
||||
{ prefix: '/admin/video-editors', title: 'Video Editor Management' },
|
||||
{ prefix: '/admin/fitness-trainers', title: 'Fitness Trainer Management' },
|
||||
{ prefix: '/admin/catering-services', title: 'Catering Services Management' },
|
||||
{ prefix: '/admin/graphic-designers', title: 'Graphics Designer Management' },
|
||||
{ prefix: '/admin/social-media-managers', title: 'Social Media Manager Management' },
|
||||
{ prefix: '/admin/jobs', title: 'Jobs Management' },
|
||||
{ prefix: '/admin/leads', title: 'Leads Management' },
|
||||
{ prefix: '/admin/pricing', title: 'Pricing Management' },
|
||||
{ prefix: '/admin/credit', title: 'Credit Management' },
|
||||
{ prefix: '/admin/coupon', title: 'Coupon Management' },
|
||||
{ prefix: '/admin/discount', title: 'Discount Management' },
|
||||
{ prefix: '/admin/tax', title: 'Tax Management' },
|
||||
{ prefix: '/admin/order', title: 'Order Management' },
|
||||
{ prefix: '/admin/invoice', title: 'Invoice Management' },
|
||||
{ prefix: '/admin/review', title: 'Review Management' },
|
||||
{ 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' },
|
||||
];
|
||||
function IconBell() {
|
||||
return (
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconSearch() {
|
||||
return (
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="11" cy="11" r="7.5" />
|
||||
<path d="m20 20-3.8-3.8" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconHelp() {
|
||||
return (
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M9.3 9a2.8 2.8 0 1 1 4.9 2c-.8.9-1.7 1.4-1.7 2.5" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function IconCog() {
|
||||
return (
|
||||
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 15.5A3.5 3.5 0 1 0 12 8.5a3.5 3.5 0 0 0 0 7Z" />
|
||||
<path d="M19.4 15a1.6 1.6 0 0 0 .3 1.7l.1.1a2 2 0 0 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-1.7-.3 1.6 1.6 0 0 0-1 1.5V21a2 2 0 0 1-4 0v-.2a1.6 1.6 0 0 0-1-1.5 1.6 1.6 0 0 0-1.7.3l-.1.1a2 2 0 0 1-2.8-2.8l.1-.1a1.6 1.6 0 0 0 .3-1.7 1.6 1.6 0 0 0-1.5-1H3a2 2 0 0 1 0-4h.2a1.6 1.6 0 0 0 1.5-1 1.6 1.6 0 0 0-.3-1.7l-.1-.1a2 2 0 0 1 2.8-2.8l.1.1a1.6 1.6 0 0 0 1.7.3h.1a1.6 1.6 0 0 0 .9-1.5V3a2 2 0 1 1 4 0v.2a1.6 1.6 0 0 0 .9 1.5h.1a1.6 1.6 0 0 0 1.7-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.6 1.6 0 0 0-.3 1.7v.1a1.6 1.6 0 0 0 1.5.9H21a2 2 0 0 1 0 4h-.2a1.6 1.6 0 0 0-1.5 1Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminShell(props: { children: JSX.Element }) {
|
||||
const location = useLocation();
|
||||
|
|
@ -99,35 +68,46 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
const [searchParams] = useSearchParams();
|
||||
const [checkedSession, setCheckedSession] = createSignal(false);
|
||||
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 path = location.pathname;
|
||||
for (const set of TAB_SETS) {
|
||||
if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) {
|
||||
return set.tabs;
|
||||
}
|
||||
if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) return set.tabs;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const isTabActive = (tab: Tab) => {
|
||||
if (tab.exact) return location.pathname === tab.href;
|
||||
return location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`);
|
||||
const isTabActive = (tab: Tab) => (tab.exact ? location.pathname === tab.href : location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`));
|
||||
|
||||
const refreshTabIndicator = () => {
|
||||
const activeTab = tabs().find((tab) => isTabActive(tab));
|
||||
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(() => {
|
||||
const path = location.pathname;
|
||||
for (const item of PAGE_TITLES) {
|
||||
if (path === item.prefix || path.startsWith(`${item.prefix}/`)) return item.title;
|
||||
}
|
||||
return 'Dashboard';
|
||||
createEffect(() => {
|
||||
tabs();
|
||||
location.pathname;
|
||||
requestAnimationFrame(refreshTabIndicator);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const onResize = () => refreshTabIndicator();
|
||||
window.addEventListener('resize', onResize);
|
||||
onCleanup(() => window.removeEventListener('resize', onResize));
|
||||
|
||||
const isLocalDev =
|
||||
typeof window !== 'undefined' &&
|
||||
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
||||
typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
||||
const isPreview =
|
||||
searchParams._preview === '1' ||
|
||||
(typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1');
|
||||
|
|
@ -146,10 +126,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
const accessToken =
|
||||
typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
||||
const response = await fetch('/api/gateway/users/auth/me', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
|
|
@ -162,7 +139,6 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized');
|
||||
if (payload?.full_name) setAdminName(payload.full_name);
|
||||
if (payload?.role?.name) setAdminRole(payload.role.name);
|
||||
setCheckedSession(true);
|
||||
} catch {
|
||||
clearAdminSession();
|
||||
|
|
@ -188,110 +164,126 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
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 (
|
||||
<div class="min-h-screen bg-[#f9f9fd]">
|
||||
{/* ── Fixed Header ── */}
|
||||
<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">
|
||||
{/* Left: logo */}
|
||||
<div class="flex w-64 shrink-0 items-center">
|
||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-8 w-auto object-contain" />
|
||||
</div>
|
||||
|
||||
{/* Center: search */}
|
||||
<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"
|
||||
<div class="min-h-screen bg-[#f0f1f6]">
|
||||
<header class="fixed inset-x-0 top-0 z-50 border-b border-[#d8dbe3] bg-[#f6f7fb]">
|
||||
<div class="flex h-[86px] items-center justify-between px-8">
|
||||
<div class="flex min-w-0 items-center gap-6">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 lg:hidden"
|
||||
onClick={() => setSidebarOpen((v) => !v)}
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<svg class="h-5 w-5" 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="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 class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" d="M4 7h16M4 12h16M4 17h16" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="h-8 w-px bg-slate-200" />
|
||||
|
||||
{/* 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>
|
||||
<A href="/admin" class="flex shrink-0 items-center">
|
||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-12 w-auto object-contain" />
|
||||
</A>
|
||||
</div>
|
||||
|
||||
{/* Logout */}
|
||||
<button type="button" onClick={onLogout} aria-label="Logout"
|
||||
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"
|
||||
>
|
||||
<svg class="h-5 w-5" 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" />
|
||||
</svg>
|
||||
<div class="flex items-center gap-4">
|
||||
<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">
|
||||
<IconSearch />
|
||||
<span>Search system operations...</span>
|
||||
</div>
|
||||
<div class="hidden h-10 w-px bg-[#d9dde7] lg:block" />
|
||||
<button type="button" aria-label="Notifications" class="relative inline-flex h-10 w-10 items-center justify-center rounded-full text-[#3e4559] hover:bg-white">
|
||||
<span class="absolute right-2 top-2 h-1.5 w-1.5 rounded-full bg-[#fd6216]" />
|
||||
<IconBell />
|
||||
</button>
|
||||
<button type="button" aria-label="Help" class="hidden h-10 w-10 items-center justify-center rounded-full text-[#3e4559] hover:bg-white lg:inline-flex">
|
||||
<IconHelp />
|
||||
</button>
|
||||
<button type="button" aria-label="Settings" class="hidden h-10 w-10 items-center justify-center rounded-full text-[#3e4559] hover:bg-white lg:inline-flex">
|
||||
<IconCog />
|
||||
</button>
|
||||
<div class="hidden h-10 w-px bg-[#d9dde7] lg:block" />
|
||||
<div class="hidden items-center gap-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>
|
||||
</header>
|
||||
|
||||
{/* ── Body: sidebar + main (fixed, below header) ── */}
|
||||
{checkedSession() ? (
|
||||
<div class="fixed inset-0 top-16 grid grid-cols-[auto_1fr]">
|
||||
{/* Sidebar */}
|
||||
<AdminSidebar />
|
||||
<div class="fixed inset-0 top-[86px] flex">
|
||||
<div
|
||||
class={`absolute inset-0 z-20 bg-[#0a1d37]/35 transition-opacity lg:hidden ${sidebarOpen() ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<main class="scrollbar min-w-0 overflow-y-auto bg-[#f9f9fd] p-8">
|
||||
{/* 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 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' : ''}`}>
|
||||
<AdminSidebar onNavigate={() => setSidebarOpen(false)} onLogout={onLogout} />
|
||||
</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}
|
||||
</main>
|
||||
</div>
|
||||
) : (
|
||||
/* Session check loading state */
|
||||
<div class="fixed inset-0 top-16 grid grid-cols-[auto_1fr]">
|
||||
<div class="w-64 border-r border-slate-100 bg-white" />
|
||||
<main class="flex items-center justify-center bg-gray-50">
|
||||
<p class="text-sm text-gray-400">Checking session…</p>
|
||||
<div class="fixed inset-0 top-[86px] flex">
|
||||
<div class="hidden w-[310px] border-r border-[#d7d8df] bg-[#f6f6f8] lg:block" />
|
||||
<main class="flex flex-1 items-center justify-center bg-[#f0f1f6]">
|
||||
<p class="text-sm text-gray-500">Checking session...</p>
|
||||
</main>
|
||||
</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 { 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;
|
||||
label: string;
|
||||
icon: string;
|
||||
iconPath?: string;
|
||||
icon?: any;
|
||||
aliasPrefix?: string;
|
||||
group: string;
|
||||
separatorBefore?: boolean;
|
||||
};
|
||||
|
||||
const links: LinkItem[] = [
|
||||
{ href: '/admin', label: 'Dashboard', icon: 'dashboard.svg', group: '__top__' },
|
||||
|
||||
{ href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg', group: 'Management' },
|
||||
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg', group: 'Management' },
|
||||
{ href: '/admin/onboarding-schemas', label: 'External Onboarding', icon: 'reviews.svg', group: 'Management' },
|
||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Mgmt.', icon: 'dashboard.svg', group: 'Management' },
|
||||
{ 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', icon: 'approval.svg', group: 'Management' },
|
||||
{ href: '/admin/department', label: 'Department Management', icon: 'department.svg', group: 'Management' },
|
||||
{ href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg', group: 'Management' },
|
||||
{ href: '/admin/employees', label: 'Employee Management', icon: 'users.svg', group: 'Management' },
|
||||
{ href: '/admin/users', label: 'Users Management', icon: 'users.svg', group: 'Management' },
|
||||
{ href: '/admin/company', label: 'Company Management', icon: 'company.svg', group: 'Management' },
|
||||
{ href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg', group: 'Management' },
|
||||
{ href: '/admin/customer', label: 'Customer Management', icon: 'users.svg', group: 'Management' },
|
||||
{ href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg', group: 'Management' },
|
||||
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg',group: 'Management' },
|
||||
{ href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg', group: 'Management' },
|
||||
{ href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg', group: 'Management' },
|
||||
{ href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg', group: 'Management' },
|
||||
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg', group: 'Management' },
|
||||
{ href: '/admin/catering-services', label: 'Catering Services', icon: 'company.svg', group: 'Management' },
|
||||
{ href: '/admin/graphic-designers', label: 'Graphics Designer Mgmt.', icon: 'developers.svg', group: 'Management' },
|
||||
{ href: '/admin/social-media-managers', label: 'Social Media Mgr.', icon: 'developers.svg', group: 'Management' },
|
||||
{ href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg', group: 'Management' },
|
||||
{ href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg', group: 'Management' },
|
||||
|
||||
{ href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg', group: 'Finance' },
|
||||
{ href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg', group: 'Finance' },
|
||||
{ href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg', group: 'Finance' },
|
||||
{ href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg', group: 'Finance' },
|
||||
{ href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg', group: 'Finance' },
|
||||
{ href: '/admin/order', label: 'Order Management', icon: 'order.svg', group: 'Finance' },
|
||||
{ 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' },
|
||||
const items: Item[] = [
|
||||
{ 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/runtime-roles', label: 'External Role Management', iconPath: '/sidebar-icons/users.svg', icon: Users },
|
||||
{ href: '/admin/onboarding-management', label: 'External Onboarding', iconPath: '/sidebar-icons/users.svg', icon: Users, aliasPrefix: '/admin/onboarding-schemas' },
|
||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboards', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid },
|
||||
{ href: '/admin/external-dashboard-management', label: 'External Dashboards', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid, aliasPrefix: '/admin/role-ui-configs' },
|
||||
{ href: '/admin/approval', label: 'Approval Management', iconPath: '/sidebar-icons/approval.svg', icon: ClipboardList },
|
||||
{ href: '/admin/department', label: 'Department Management', iconPath: '/sidebar-icons/department.svg', icon: Briefcase, separatorBefore: true },
|
||||
{ href: '/admin/designation', label: 'Designation Management', iconPath: '/sidebar-icons/designation.svg', icon: Briefcase },
|
||||
{ href: '/admin/employees', label: 'Employee Management', iconPath: '/sidebar-icons/users.svg', icon: UserCircle2 },
|
||||
{ href: '/admin/users', label: 'Users Management', iconPath: '/sidebar-icons/users.svg', icon: Users },
|
||||
{ href: '/admin/company', label: 'Company Management', iconPath: '/sidebar-icons/company.svg', icon: Briefcase },
|
||||
{ href: '/admin/candidate', label: 'Candidate Management', iconPath: '/sidebar-icons/candidate.svg', icon: UserCircle2 },
|
||||
{ href: '/admin/customer', label: 'Customer Management', iconPath: '/sidebar-icons/support.svg', icon: UserCircle2 },
|
||||
{ href: '/admin/photographer', label: 'Photographer Management', iconPath: '/sidebar-icons/photographer.svg', icon: Sparkles, separatorBefore: true },
|
||||
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', iconPath: '/sidebar-icons/makeup-artist.svg', icon: Sparkles },
|
||||
{ href: '/admin/tutors', label: 'Tutors Management', iconPath: '/sidebar-icons/tutor.svg', icon: Sparkles },
|
||||
{ href: '/admin/developers', label: 'Developers Management', iconPath: '/sidebar-icons/developers.svg', icon: Sparkles },
|
||||
{ href: '/admin/jobs', label: 'Jobs Management', iconPath: '/sidebar-icons/jobs.svg', icon: Briefcase },
|
||||
{ href: '/admin/leads', label: 'Leads Management', iconPath: '/sidebar-icons/leads.svg', icon: HandHelping },
|
||||
{ href: '/admin/pricing', label: 'Pricing Management', iconPath: '/sidebar-icons/pricing.svg', icon: WalletCards },
|
||||
{ href: '/admin/credit', label: 'Credit Management', iconPath: '/sidebar-icons/credits.svg', icon: WalletCards },
|
||||
{ href: '/admin/coupon', label: 'Coupon Management', iconPath: '/sidebar-icons/coupon.svg', icon: Percent },
|
||||
{ href: '/admin/discount', label: 'Discount Management', iconPath: '/sidebar-icons/discount.svg', icon: Percent },
|
||||
{ href: '/admin/tax', label: 'Tax Management', iconPath: '/sidebar-icons/tax.svg', icon: Receipt },
|
||||
{ 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/review', label: 'Review Management', iconPath: '/sidebar-icons/reviews.svg', icon: FileText },
|
||||
{ href: '/admin/support', label: 'Support Management', iconPath: '/sidebar-icons/support.svg', icon: UserCircle2 },
|
||||
{ href: '/admin/report', label: 'Report Management', iconPath: '/sidebar-icons/report.svg', icon: Bell },
|
||||
{ href: '/admin/ledger', label: 'Ledger Management', iconPath: '/sidebar-icons/ledger.svg', icon: Receipt },
|
||||
{ href: '/admin/kb', label: 'Knowledge Base', icon: FileText },
|
||||
{ href: '/admin/notifications', label: 'Notifications', icon: Bell },
|
||||
];
|
||||
|
||||
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 isActive = (href: string, aliasPrefix?: string) => {
|
||||
if (href === '/admin') return location.pathname === '/admin';
|
||||
if (aliasPrefix && location.pathname.startsWith(aliasPrefix)) return true;
|
||||
return location.pathname === href || location.pathname.startsWith(`${href}/`);
|
||||
const active = (item: Item) => {
|
||||
if (item.href === '/admin') return location.pathname === '/admin';
|
||||
if (item.aliasPrefix && location.pathname.startsWith(item.aliasPrefix)) return true;
|
||||
return location.pathname === item.href || location.pathname.startsWith(`${item.href}/`);
|
||||
};
|
||||
|
||||
const NavItem = (item: LinkItem) => {
|
||||
const active = isActive(item.href, item.aliasPrefix);
|
||||
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
|
||||
href={item.href}
|
||||
activeClass=""
|
||||
inactiveClass=""
|
||||
title={item.label}
|
||||
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]'
|
||||
onClick={() => props.onNavigate?.()}
|
||||
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 ${
|
||||
isActive ? 'bg-[#ffece3] text-[#fd6216]' : 'text-[#2f3647] hover:bg-white hover:text-[#111827]'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={`/sidebar-icons/${item.icon}`}
|
||||
alt=""
|
||||
class="h-4 w-4 shrink-0"
|
||||
style={active ? 'opacity:0.85' : 'opacity:0.35'}
|
||||
/>
|
||||
<span class={`absolute bottom-0 left-0 top-0 w-[4px] rounded-r-md bg-[#fd6216] transition-opacity ${isActive ? 'opacity-100' : 'opacity-0'}`} />
|
||||
{renderIcon(item, isActive)}
|
||||
<span class="truncate">{item.label}</span>
|
||||
</A>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}}
|
||||
</For>
|
||||
</nav>
|
||||
|
||||
const topLinks = links.filter(l => l.group === '__top__');
|
||||
const mgmtLinks = links.filter(l => l.group === 'Management');
|
||||
const finLinks = links.filter(l => l.group === 'Finance');
|
||||
const platLinks = links.filter(l => l.group === 'Platform');
|
||||
|
||||
return (
|
||||
<aside class="flex h-full w-60 flex-col border-r border-slate-100 bg-white">
|
||||
|
||||
{/* Top: Dashboard link */}
|
||||
<div class="px-3 pt-4 pb-2">
|
||||
{topLinks.map(NavItem)}
|
||||
</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 class="border-t border-[#dfdfe5] px-6 py-5">
|
||||
<button
|
||||
type="button"
|
||||
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]"
|
||||
>
|
||||
<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" />
|
||||
</svg>
|
||||
<span>Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { A } from '@solidjs/router';
|
||||
import { createResource, createSignal, Show } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import { Eye, Pencil, Trash2 } from 'lucide-solid';
|
||||
|
||||
const API = '/api/gateway';
|
||||
|
||||
|
|
@ -50,12 +51,9 @@ export default function InternalRolesPage() {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">Internal Role Management</h1>
|
||||
<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 class="mb-8">
|
||||
<h1 class="text-[52px] font-extrabold tracking-tight text-[#071b3d] sm:text-[36px] lg:text-[52px]">Roles Management</h1>
|
||||
<p class="mt-2 text-[17px] text-[#4b5563]">Configure and maintain internal system roles and access privileges.</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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">
|
||||
<table class="w-full text-sm">
|
||||
<table class="w-full min-w-[860px] text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th class="text-right">Actions</th>
|
||||
<tr class="bg-[#071b3d] text-white">
|
||||
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">ID</th>
|
||||
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Name</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Show when={roles.loading}>
|
||||
<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>
|
||||
</Show>
|
||||
<Show when={!roles.loading && roles.error}>
|
||||
<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>
|
||||
</Show>
|
||||
<Show when={!roles.loading && !roles.error && roles()?.length === 0}>
|
||||
<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>
|
||||
</Show>
|
||||
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
|
||||
{roles()!.map((role) => (
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<p style="margin:0;font-weight:600;color:#0f172a;">{role.name}</p>
|
||||
<p style="margin:2px 0 0;font-size:11px;color:#94a3b8;">{role.code || role.id?.slice(0, 8)}</p>
|
||||
</div>
|
||||
<tr class="border-b border-[#e4e7ef] text-[17px]">
|
||||
<td class="px-8 py-7 font-medium text-[#364152]">{role.code || role.id?.slice(0, 6).toUpperCase() || 'ROL247'}</td>
|
||||
<td class="px-8 py-7 font-semibold text-[#0f172a]">{role.name}</td>
|
||||
<td class="px-8 py-7">
|
||||
<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}`}>
|
||||
<span>View</span>
|
||||
<Eye size={16} />
|
||||
</A>
|
||||
</td>
|
||||
<td style="color:#475569;">{role.description || 'No description added yet.'}</td>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<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 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 class="px-8 py-7 text-center">
|
||||
<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}`}>
|
||||
<Pencil size={17} />
|
||||
</A>
|
||||
</td>
|
||||
<td class="px-8 py-7 text-center">
|
||||
<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}
|
||||
onClick={() => handleDelete(role.id, role.name)}
|
||||
title="Delete Role"
|
||||
aria-label={`Delete ${role.name}`}
|
||||
>
|
||||
{deleting() === role.id ? '...' : '🗑'}
|
||||
{deleting() === role.id ? '...' : <Trash2 size={17} />}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -124,6 +127,16 @@ export default function InternalRolesPage() {
|
|||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { A, useSearchParams } from '@solidjs/router';
|
||||
import { createMemo, createResource, Show } from 'solid-js';
|
||||
import AdminShell from '~/components/AdminShell';
|
||||
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
||||
|
||||
const API = '/api/gateway';
|
||||
|
||||
|
|
@ -77,73 +76,49 @@ export default function RuntimeRolesPage() {
|
|||
|
||||
return (
|
||||
<AdminShell>
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">External Role Management</h1>
|
||||
<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 class="mb-8">
|
||||
<h1 class="text-[52px] font-extrabold tracking-tight text-[#071b3d] sm:text-[36px] lg:text-[52px]">Roles Management</h1>
|
||||
<p class="mt-2 text-[17px] text-[#4b5563]">Configure and maintain external system roles and access privileges.</p>
|
||||
</div>
|
||||
|
||||
<ExternalRoleTabs />
|
||||
|
||||
<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>
|
||||
<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">
|
||||
<table class="w-full text-sm">
|
||||
<table class="w-full min-w-[860px] text-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Type</th>
|
||||
<th>Modules</th>
|
||||
<th>Schema</th>
|
||||
<th>Status</th>
|
||||
<th class="text-right">Actions</th>
|
||||
<tr class="bg-[#071b3d] text-white">
|
||||
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">ID</th>
|
||||
<th class="px-8 py-5 text-left text-[14px] font-semibold uppercase tracking-[0.08em]">Name</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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 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 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 when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
|
||||
{roles()!.map((role) => (
|
||||
<tr class={selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-orange-50' : ''}>
|
||||
<td>
|
||||
<div>
|
||||
<p style="margin:0;font-weight:600;color:#0f172a">{role.displayName}</p>
|
||||
<p style="margin:2px 0 0;font-size:11px;color:#94a3b8">{role.roleKey}</p>
|
||||
</div>
|
||||
<tr class={`border-b border-[#e4e7ef] text-[17px] ${selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-[#fff7f2]' : ''}`}>
|
||||
<td class="px-8 py-7 font-medium text-[#364152]">{role.roleKey || role.id?.slice(0, 6).toUpperCase()}</td>
|
||||
<td class="px-8 py-7 font-semibold text-[#0f172a]">{role.displayName}</td>
|
||||
<td class="px-8 py-7">
|
||||
<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">
|
||||
<span>View</span>
|
||||
<span class="text-[18px]">↗</span>
|
||||
</A>
|
||||
</td>
|
||||
<td style="color:#475569">{role.vertical || '—'}</td>
|
||||
<td style="color:#475569">{role.enabledModules.length}</td>
|
||||
<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 class="px-8 py-7 text-center">
|
||||
<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>
|
||||
<td>
|
||||
<div class="flex items-center justify-end gap-1">
|
||||
<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 class="px-8 py-7 text-center">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -151,6 +126,16 @@ export default function RuntimeRolesPage() {
|
|||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
</AdminShell>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { hasAdminSession, setAdminSession } from '~/lib/admin-session';
|
||||
|
||||
|
|
@ -307,26 +307,27 @@ export default function LoginPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<main class="auth-page auth-page-login">
|
||||
<div class="auth-bg" />
|
||||
<div class="auth-layout">
|
||||
<section class="auth-visual">
|
||||
<p class="auth-visual-kicker">Internal Access</p>
|
||||
<h1>Welcome back to Nxtgauge.</h1>
|
||||
<p>Sign in securely to access the admin control panel.</p>
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1200&q=80"
|
||||
alt="Office workspace"
|
||||
/>
|
||||
<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="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">
|
||||
<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">
|
||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-12 w-auto brightness-0 invert" />
|
||||
<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 class="mt-6 max-w-[520px] text-[52px] font-extrabold leading-[1.05] text-white">Welcome back to Nxtgauge.</h1>
|
||||
<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>
|
||||
<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">
|
||||
<p class="font-semibold">Secure login with internal access policies.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="auth-card auth-form-card">
|
||||
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="auth-logo" />
|
||||
<h2 class="auth-title">{mode() === 'login' ? 'Employee Login' : 'Reset Password'}</h2>
|
||||
<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">
|
||||
<div class="mx-auto w-full max-w-[560px]">
|
||||
<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">
|
||||
<div class="field">
|
||||
<label>Email address</label>
|
||||
<form class="mt-6 space-y-5">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={email()}
|
||||
|
|
@ -335,12 +336,18 @@ export default function LoginPage() {
|
|||
clearMessages();
|
||||
}}
|
||||
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>
|
||||
{mode() === 'login' ? (
|
||||
<>
|
||||
<div class="field">
|
||||
<label>Password</label>
|
||||
|
||||
<Show when={mode() === 'login'}>
|
||||
<div>
|
||||
<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
|
||||
type="password"
|
||||
value={password()}
|
||||
|
|
@ -349,28 +356,15 @@ export default function LoginPage() {
|
|||
clearMessages();
|
||||
}}
|
||||
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 class="auth-switch">
|
||||
<button type="button" class="auth-link-btn" onClick={() => switchMode('reset')}>
|
||||
Forgot password?
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<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>
|
||||
</Show>
|
||||
|
||||
<Show when={mode() === 'reset'}>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">New Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword()}
|
||||
|
|
@ -378,11 +372,12 @@ export default function LoginPage() {
|
|||
setNewPassword(event.currentTarget.value);
|
||||
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 class="field">
|
||||
<label>Confirm password</label>
|
||||
<div>
|
||||
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Confirm Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword()}
|
||||
|
|
@ -390,13 +385,13 @@ export default function LoginPage() {
|
|||
setConfirmPassword(event.currentTarget.value);
|
||||
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>
|
||||
|
||||
{resetStep() === 'verify' ? (
|
||||
<div class="field">
|
||||
<label>Verification code</label>
|
||||
<Show when={resetStep() === 'verify'}>
|
||||
<div>
|
||||
<label class="mb-1 block text-[11px] font-bold uppercase tracking-[0.11em] text-[#4b546f]">Verification Code</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
|
|
@ -406,43 +401,60 @@ export default function LoginPage() {
|
|||
setResetCode(event.currentTarget.value.replace(/\D/g, '').slice(0, 6));
|
||||
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>
|
||||
) : null}
|
||||
|
||||
<div class="auth-switch">
|
||||
<button type="button" class="auth-link-btn" onClick={() => switchMode('login')}>
|
||||
<Show when={mode() === '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
|
||||
</button>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{resetStep() === 'request' ? (
|
||||
<Show when={resetStep() === 'request'} fallback={(
|
||||
<button
|
||||
type="button"
|
||||
class="btn primary block-btn"
|
||||
disabled={isSubmitting() || !canSubmitResetRequest()}
|
||||
onClick={requestResetCode}
|
||||
>
|
||||
{isSubmitting() ? 'Sending code...' : 'Send reset code'}
|
||||
</button>
|
||||
) : (
|
||||
<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() || !canSubmitResetVerify()}
|
||||
onClick={verifyResetCode}
|
||||
>
|
||||
{isSubmitting() ? 'Resetting password...' : 'Verify & reset password'}
|
||||
{isSubmitting() ? 'Resetting Password...' : 'Verify & Reset Password'}
|
||||
</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>
|
||||
{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>
|
||||
</div>
|
||||
</main>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue