Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API
Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each admin route now owns its own form/modal logic. Add DataTable.svelte, superform-errors.ts, and profile-specific modal/field components. Flatten proclamation categories/groups and drop subscription levels; add the v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated Prisma client with the new migrations. Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
This commit is contained in:
+355
-57
@@ -1,79 +1,377 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { page } from '$app/state';
|
||||
import { resolve } from '$app/paths';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { LayoutData } from './$types';
|
||||
import './layout.css';
|
||||
|
||||
let { data, children } = $props();
|
||||
|
||||
let { children, data }: { children: Snippet; data: LayoutData } = $props();
|
||||
const session = authClient.useSession();
|
||||
let isAdmin = $derived(canAccessAdmin(data.userRoleId));
|
||||
|
||||
let dark = $state(false);
|
||||
let menuOpen = $state(false);
|
||||
let adminOpen = $state(false);
|
||||
|
||||
onMount(() => {
|
||||
dark = document.documentElement.classList.contains('dark');
|
||||
});
|
||||
const isAdmin = $derived((data.userRoleId ?? 4) <= 3);
|
||||
const adminModels = [
|
||||
{ slug: 'users', displayName: 'Users' },
|
||||
{ slug: 'profiles', displayName: 'Profiles' },
|
||||
{ slug: 'user-roles', displayName: 'User Roles' },
|
||||
{ slug: 'proclamations', displayName: 'Proclamations' },
|
||||
{ slug: 'solfeggio-frequencies', displayName: 'Solfeggio Frequencies' },
|
||||
{ slug: 'proclamation-categories', displayName: 'Proclamation Categories' },
|
||||
{ slug: 'proclamation-groups', displayName: 'Proclamation Groups' },
|
||||
{ slug: 'license-keys', displayName: 'License Keys' }
|
||||
];
|
||||
const currentPath = $derived(page.url.pathname);
|
||||
|
||||
function toggleTheme() {
|
||||
dark = !dark;
|
||||
if (dark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
}
|
||||
function isActive(path: string): boolean {
|
||||
if (path === '/') return currentPath === '/';
|
||||
return currentPath === path || currentPath.startsWith(`${path}/`);
|
||||
}
|
||||
|
||||
function activeClass(path: string) {
|
||||
return isActive(path)
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900';
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuOpen = false;
|
||||
adminOpen = false;
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
await authClient.signOut();
|
||||
closeMenu();
|
||||
window.location.href = resolve('/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="icon" href={favicon} />
|
||||
<title>Rhema Wave Technologies</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Rhema Wave Technologies User Portal — manage proclamations, audio content, and solfeggio frequencies."
|
||||
/>
|
||||
<meta name="theme-color" content="#6b7280" />
|
||||
<meta property="og:title" content="Rhema Wave Technologies" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Manage proclamations, audio content, and solfeggio frequencies."
|
||||
/>
|
||||
<title>Radiant Wave User Portal</title>
|
||||
<meta name="description" content="Radiant Wave Technologies User Portal" />
|
||||
<meta property="og:title" content="Radiant Wave User Portal" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Rhema Wave Technologies" />
|
||||
<meta property="og:site_name" content="Radiant Wave User Portal" />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
</svelte:head>
|
||||
|
||||
<nav
|
||||
class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100"
|
||||
>
|
||||
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
|
||||
<div class="flex items-center gap-4">
|
||||
<button
|
||||
onclick={toggleTheme}
|
||||
class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300"
|
||||
>
|
||||
{dark ? 'Light' : 'Dark'}
|
||||
</button>
|
||||
{#if $session.data}
|
||||
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
|
||||
<button
|
||||
class="dark:text-gray-300 dark:hover:text-gray-100"
|
||||
onclick={async () => {
|
||||
await authClient.signOut();
|
||||
window.location.href = '/';
|
||||
}}>Log Out</button
|
||||
<header class="sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur">
|
||||
<div class="mx-auto flex h-14 max-w-6xl items-center justify-between gap-4 px-4">
|
||||
<a href={resolve('/')} class="flex shrink-0 items-center gap-2">
|
||||
<span
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-600 to-indigo-400"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M2 12c1.5 0 1.5-2 3-2s1.5 2 3 2 1.5-2 3-2 1.5 2 3 2 1.5-2 3-2 1.5 2 3 2" /><path
|
||||
d="m2 17 1.5-2M22 12c-1.5 0-1.5-2-3-2M22 17l-1.5-2"
|
||||
/></svg
|
||||
>
|
||||
</span>
|
||||
<span class="text-base font-semibold tracking-tight text-slate-900">Radiant Wave</span>
|
||||
</a>
|
||||
|
||||
{#if $session.data?.user}
|
||||
<nav aria-label="Primary" class="hidden items-center gap-1 md:flex">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/dashboard')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/dashboard')}`}
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/account')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/account')}`}
|
||||
>
|
||||
Account
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/license-keys')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/license-keys')}`}
|
||||
>
|
||||
License Keys
|
||||
</a>
|
||||
|
||||
{#if isAdmin}
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={adminOpen}
|
||||
onclick={() => (adminOpen = !adminOpen)}
|
||||
class={`flex items-center gap-1 rounded-md px-3 py-2 text-sm font-medium transition ${
|
||||
isActive('/admin')
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
Admin
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="transition-transform"
|
||||
class:rotate-180={adminOpen}><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if adminOpen}
|
||||
<div
|
||||
class="absolute top-full left-0 mt-1 min-w-56 rounded-lg border border-slate-200 bg-white p-1 shadow-lg"
|
||||
>
|
||||
{#each adminModels as model (model.slug)}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
onclick={() => (adminOpen = false)}
|
||||
class={`block rounded-md px-3 py-2 text-sm transition ${
|
||||
isActive('/admin/' + model.slug)
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href={resolve('/admin/api-keys')}
|
||||
onclick={() => (adminOpen = false)}
|
||||
class={`block rounded-md px-3 py-2 text-sm transition ${
|
||||
isActive('/admin/api-keys')
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="mx-2 h-5 w-px bg-slate-200" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSignOut}
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-slate-500 transition hover:bg-slate-100 hover:text-slate-900"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</nav>
|
||||
{:else}
|
||||
<span class="dark:text-gray-300"
|
||||
><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> |
|
||||
<a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span
|
||||
>
|
||||
<nav aria-label="Primary" class="hidden items-center gap-1 md:flex">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-indigo-700"
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle navigation menu"
|
||||
aria-expanded={menuOpen}
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg text-slate-700 hover:bg-slate-100 md:hidden"
|
||||
>
|
||||
{#if menuOpen}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><line x1="4" x2="20" y1="6" y2="6" /><line x1="4" x2="20" y1="12" y2="12" /><line
|
||||
x1="4"
|
||||
x2="20"
|
||||
y1="18"
|
||||
y2="18"
|
||||
/></svg
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="flex min-h-0 flex-1 px-8 dark:bg-gray-950">
|
||||
{@render children()}
|
||||
</header>
|
||||
|
||||
{#if menuOpen}
|
||||
<div
|
||||
class="fixed inset-0 top-14 z-50 overflow-y-auto border-t border-slate-200 bg-white shadow-lg md:hidden"
|
||||
>
|
||||
<nav class="mx-auto max-w-6xl px-4 py-4">
|
||||
{#if $session.data?.user}
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/dashboard')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/dashboard')}`}
|
||||
>
|
||||
Dashboard
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/account')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/account')}`}
|
||||
>
|
||||
Account
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/license-keys')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/license-keys')}`}
|
||||
>
|
||||
License Keys
|
||||
</a>
|
||||
|
||||
{#if isAdmin}
|
||||
<div class="mt-2 border-t border-slate-200 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={adminOpen}
|
||||
onclick={() => (adminOpen = !adminOpen)}
|
||||
class={`flex w-full items-center justify-between rounded-lg px-4 py-3 text-base font-semibold transition ${
|
||||
isActive('/admin')
|
||||
? 'bg-indigo-50 text-indigo-700'
|
||||
: 'text-slate-700 hover:bg-indigo-50'
|
||||
}`}
|
||||
>
|
||||
<span>Admin</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="transition-transform"
|
||||
class:rotate-180={adminOpen}><path d="m6 9 6 6 6-6" /></svg
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if adminOpen}
|
||||
<div class="mt-1 flex flex-col gap-1">
|
||||
{#each adminModels as model (model.slug)}
|
||||
<a
|
||||
href="/admin/{model.slug}"
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base transition ${activeClass('/admin/' + model.slug)}`}
|
||||
>
|
||||
{model.displayName}
|
||||
</a>
|
||||
{/each}
|
||||
<a
|
||||
href={resolve('/admin/api-keys')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base transition ${activeClass('/admin/api-keys')}`}
|
||||
>
|
||||
API Keys
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSignOut}
|
||||
class="mt-2 rounded-lg border-t border-slate-200 px-4 py-3 text-base font-medium text-slate-500 hover:bg-slate-100"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-1">
|
||||
<a
|
||||
href={resolve('/')}
|
||||
onclick={closeMenu}
|
||||
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/')}`}
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
<a
|
||||
href={resolve('/login')}
|
||||
onclick={closeMenu}
|
||||
class="mt-1 rounded-lg bg-indigo-600 px-4 py-3 text-center text-base font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
Sign In
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex min-h-[calc(100dvh-3.5rem)] flex-col bg-slate-50 text-slate-900">
|
||||
<main class="flex-1">
|
||||
{@render children()}
|
||||
</main>
|
||||
<footer class="sticky bottom-0 z-10 border-t border-slate-200 bg-white">
|
||||
<div
|
||||
class="mx-auto flex max-w-6xl flex-col items-center justify-between gap-2 px-4 py-6 text-sm text-slate-500 sm:flex-row"
|
||||
>
|
||||
<p>© {new Date().getFullYear()} Radiant Wave Technologies. All rights reserved.</p>
|
||||
{#if $session.data?.user}
|
||||
<a href={resolve('/account')} class="transition hover:text-slate-900">
|
||||
Manage your account
|
||||
</a>
|
||||
{:else}
|
||||
<a href={resolve('/login')} class="transition hover:text-slate-900">Sign in</a>
|
||||
{/if}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user