nxtgauge-frontend-solid/src/routes/workspace.tsx

156 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createResource, Show, For } from 'solid-js';
import { A, useSearchParams } from '@solidjs/router';
import ProfileWidget from '~/components/dashboard/ProfileWidget';
async function fetchRuntimeConfig(roleKey: string) {
const RUST_API_URL = import.meta.env.VITE_RUST_API_URL || 'http://localhost:8080';
// Try to lookup Role ID first
const roleRes = await fetch(`${RUST_API_URL}/api/admin/roles/${roleKey}`);
if (!roleRes.ok) throw new Error('Role not found');
const role = await roleRes.json();
// Then fetch Dashboard config for that role
const configRes = await fetch(`${RUST_API_URL}/api/admin/dashboard-config/${role.id}?audience=EXTERNAL`);
if (!configRes.ok) throw new Error('Dashboard config not found');
const dashboardConfig = await configRes.json();
return dashboardConfig.config_json; // Returns `{ sidebar: [...], widgets: [...] }`
}
function IconSearch() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="7" />
<path d="M20 20l-3.5-3.5" />
</svg>
);
}
function IconBell() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
);
}
export default function WorkspaceLayout(props: { children?: any }) {
const [searchParams] = useSearchParams();
const rawRoleKey = searchParams.roleKey;
const roleKey = () => (Array.isArray(rawRoleKey) ? rawRoleKey[0] : rawRoleKey) || 'PHOTOGRAPHER';
const [config] = createResource<any, string>(roleKey, fetchRuntimeConfig);
return (
<div class="min-h-screen bg-slate-100 text-slate-900">
<div class="flex min-h-screen">
<aside class="hidden w-72 shrink-0 border-r border-slate-200 bg-white/95 shadow-sm lg:flex lg:flex-col">
<div class="border-b border-slate-200 px-6 py-6">
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-[#fd6116]">Traceworks Jobs</p>
<h1 class="mt-2 text-2xl font-extrabold text-[#100b2f]">Admin Panel</h1>
<p class="mt-2 text-xs font-medium capitalize text-slate-500">{roleKey().replaceAll('_', ' ').toLowerCase()}</p>
</div>
<div class="px-6 pt-5">
<span class="inline-flex rounded-full border border-orange-200 bg-orange-50 px-3 py-1 text-[11px] font-bold uppercase tracking-[0.12em] text-[#fd6116]">
{roleKey().replaceAll('_', ' ')}
</span>
</div>
<nav class="flex-1 space-y-1 overflow-y-auto px-4 py-6">
<Show when={config.loading}>
<p class="px-3 text-sm text-slate-500">Loading modules...</p>
</Show>
<Show when={config.error}>
<p class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">Failed to load shell config.</p>
</Show>
<Show when={config()}>
<For each={config().sidebar}>
{(item: any) => (
<A
href={item.route}
class="group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 hover:text-[#100b2f]"
activeClass="bg-[#fd6116]/10 text-[#fd6116] shadow-sm ring-1 ring-[#fd6116]/20"
>
<span class="h-2.5 w-2.5 rounded-full bg-current/35 transition group-[.bg-[#fd6116]/10]:bg-current" />
<span class="flex-1">{item.label}</span>
<span class="text-base leading-none text-slate-400 transition group-hover:text-[#fd6116]"></span>
</A>
)}
</For>
</Show>
</nav>
</aside>
<main class="flex min-w-0 flex-1 flex-col">
<header class="sticky top-0 z-20 border-b border-slate-200 bg-white/90 px-4 py-4 backdrop-blur sm:px-6 lg:px-8">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-2xl font-extrabold tracking-tight text-[#100b2f]">Admin Panel</h2>
<p class="mt-1 text-sm text-slate-500">Manage modules and role configuration</p>
</div>
<div class="flex items-center gap-2 sm:gap-3">
<label class="flex h-10 w-44 items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 text-slate-500 focus-within:border-[#fd6116] focus-within:ring-2 focus-within:ring-[#fd6116]/20 sm:w-64">
<IconSearch />
<input
type="search"
placeholder="Search"
class="w-full border-0 bg-transparent text-sm text-slate-700 outline-none placeholder:text-slate-400"
/>
</label>
<button
type="button"
aria-label="Notifications"
class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 transition hover:border-[#fd6116]/30 hover:text-[#fd6116]"
>
<IconBell />
</button>
<span class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[#fd6116] text-sm font-bold text-white shadow-sm">
AD
</span>
</div>
</div>
</header>
<section class="mx-auto w-full max-w-7xl flex-1 space-y-6 px-4 py-6 sm:px-6 lg:px-8">
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 class="text-xl font-bold text-[#100b2f]">Role Modules</h3>
<p class="mt-1 text-sm text-slate-500">Dynamic dashboard widgets configured for this role.</p>
</div>
</div>
<Show when={config() && config().widgets}>
<div class="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
<For each={config().widgets}>
{(widget: any) => (
<Show when={widget.enabled}>
<article class="rounded-xl border border-slate-200 bg-gradient-to-br from-white to-slate-50 p-4 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md">
<div class="flex items-start justify-between gap-3">
<h4 class="text-base font-bold text-[#100b2f]">{widget.title}</h4>
<span class="inline-flex rounded-full border border-emerald-200 bg-emerald-50 px-2.5 py-0.5 text-[11px] font-semibold text-emerald-700">
Live
</span>
</div>
<p class="mt-2 text-sm text-slate-500">Dynamic widget module enabled for this role configuration.</p>
</article>
</Show>
)}
</For>
</div>
</Show>
</div>
<ProfileWidget roleKey={roleKey()} />
{props.children}
</section>
</main>
</div>
</div>
);
}