Files
portal/src/routes/+layout.svelte
T

510 lines
15 KiB
Svelte

<script lang="ts">
import { authClient } from '$lib/auth-client';
import favicon from '$lib/assets/favicon.svg';
import { page } from '$app/state';
import { resolve } from '$app/paths';
import { onMount } from 'svelte';
import type { Snippet } from 'svelte';
import type { LayoutData } from './$types';
import './layout.css';
let { children, data }: { children: Snippet; data: LayoutData } = $props();
const session = authClient.useSession();
let menuOpen = $state(false);
let adminOpen = $state(false);
let dark = $state(false);
onMount(() => {
dark = document.documentElement.classList.contains('dark');
});
function toggleTheme() {
dark = !dark;
document.documentElement.classList.toggle('dark', dark);
localStorage.setItem('theme', dark ? 'dark' : 'light');
}
const isAdmin = $derived((data.userRoleId ?? 4) <= 3);
const adminModels = [
{ slug: 'users', href: '/admin/users', displayName: 'Users' },
{ slug: 'profiles', href: '/admin/profiles', displayName: 'Profiles' },
{
slug: 'proclamations',
href: '/admin/proclamations',
displayName: 'Proclamations'
},
{
slug: 'solfeggio-frequencies',
href: '/admin/solfeggio-frequencies',
displayName: 'Solfeggio Frequencies'
},
{
slug: 'proclamation-categories',
href: '/admin/proclamation-categories',
displayName: 'Proclamation Categories'
},
{
slug: 'proclamation-groups',
href: '/admin/proclamation-groups',
displayName: 'Proclamation Groups'
},
{ slug: 'license-keys', href: '/admin/license-keys', displayName: 'License Keys' },
{ slug: 'audit-log', href: '/admin/audit-log', displayName: 'Audit Log' }
] as const;
const currentPath = $derived(page.url.pathname);
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 dark:bg-indigo-950 dark:text-indigo-300'
: 'text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-slate-100';
}
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>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="Radiant Wave User Portal" />
<meta name="twitter:card" content="summary" />
</svelte:head>
<header
class="sticky top-0 z-40 border-b border-slate-200 bg-white/90 backdrop-blur dark:border-slate-800 dark:bg-slate-900/90"
>
<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 dark:text-slate-100"
>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('/schedule')}
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/schedule')}`}
>
Schedule
</a>
<a
href={resolve('/profile')}
class={`rounded-md px-3 py-2 text-sm font-medium transition ${activeClass('/profile')}`}
>
Profile
</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 dark:bg-indigo-950 dark:text-indigo-300'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100'
}`}
>
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 dark:border-slate-800 dark:bg-slate-900"
>
{#each adminModels as model (model.slug)}
<a
href={resolve(model.href)}
onclick={() => (adminOpen = false)}
class={`block rounded-md px-3 py-2 text-sm transition ${
isActive('/admin/' + model.slug)
? 'bg-indigo-50 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100'
}`}
>
{model.displayName}
</a>
{/each}
</div>
{/if}
</div>
{/if}
<button
type="button"
aria-label="Toggle dark mode"
aria-pressed={dark}
onclick={toggleTheme}
class="flex h-9 w-9 items-center justify-center rounded-md text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
>
{#if dark}
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><circle cx="12" cy="12" r="4" /><path
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"
/></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" /></svg
>
{/if}
</button>
<span class="mx-2 h-5 w-px bg-slate-200 dark:bg-slate-700" 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 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
>
Sign Out
</button>
</nav>
{:else}
<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>
<button
type="button"
aria-label="Toggle dark mode"
aria-pressed={dark}
onclick={toggleTheme}
class="ml-1 flex h-9 w-9 items-center justify-center rounded-md text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100"
>
{#if dark}
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><circle cx="12" cy="12" r="4" /><path
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"
/></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" /></svg
>
{/if}
</button>
</nav>
{/if}
<button
type="button"
aria-label="Toggle dark mode"
aria-pressed={dark}
onclick={toggleTheme}
class="flex h-10 w-10 items-center justify-center rounded-lg text-slate-700 hover:bg-slate-100 md:hidden dark:text-slate-300 dark:hover:bg-slate-800"
>
{#if dark}
<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"
><circle cx="12" cy="12" r="4" /><path
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"
/></svg
>
{:else}
<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"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" /></svg
>
{/if}
</button>
<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 dark:text-slate-300 dark:hover:bg-slate-800"
>
{#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>
</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 dark:border-slate-800 dark:bg-slate-900"
>
<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('/schedule')}
onclick={closeMenu}
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/schedule')}`}
>
Schedule
</a>
<a
href={resolve('/profile')}
onclick={closeMenu}
class={`rounded-lg px-4 py-3 text-base font-medium transition ${activeClass('/profile')}`}
>
Profile
</a>
{#if isAdmin}
<div class="mt-2 border-t border-slate-200 pt-2 dark:border-slate-800">
<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 dark:bg-indigo-950 dark:text-indigo-300'
: 'text-slate-700 hover:bg-indigo-50 dark:text-slate-300 dark:hover:bg-indigo-950'
}`}
>
<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={resolve(model.href)}
onclick={closeMenu}
class={`rounded-lg px-4 py-3 text-base transition ${activeClass('/admin/' + model.slug)}`}
>
{model.displayName}
</a>
{/each}
</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 dark:border-slate-800 dark:text-slate-400 dark:hover:bg-slate-800"
>
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 dark:bg-slate-950 dark:text-slate-100"
>
<main class="flex-1">
{@render children()}
</main>
<footer
class="sticky bottom-0 z-10 border-t border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900"
>
<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 dark:text-slate-400"
>
<p>&copy; {new Date().getFullYear()} Radiant Wave Technologies. All rights reserved.</p>
{#if $session.data?.user}
<a
href={resolve('/profile')}
class="transition hover:text-slate-900 dark:hover:text-slate-100"
>
Manage your profile
</a>
{:else}
<a
href={resolve('/login')}
class="transition hover:text-slate-900 dark:hover:text-slate-100">Sign in</a
>
{/if}
</div>
</footer>
</div>