Files
familienarchiv/frontend/src/routes/+layout.svelte
Marcel 17db73d900
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
fix(frontend): hide nav header on forgot-password and reset-password routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 09:28:03 +01:00

197 lines
7.1 KiB
Svelte

<script lang="ts">
import './layout.css';
import { enhance } from '$app/forms';
import { page } from '$app/state';
import { onMount } from 'svelte';
import { m } from '$lib/paraglide/messages.js';
import { setLocale, getLocale } from '$lib/paraglide/runtime';
let { children, data } = $props();
const locales = ['DE', 'EN', 'ES'] as const;
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
const activeLocale = $derived(getLocale().toUpperCase());
const isAdmin = $derived(
data?.user?.groups?.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'))
);
// Set after client-side hydration completes. Used by E2E tests to know the
// page is interactive (event handlers registered) before they interact with it.
let hydrated = $state(false);
onMount(() => {
hydrated = true;
});
let userMenuOpen = $state(false);
const userInitials = $derived.by(() => {
const first = data?.user?.firstName?.[0];
const last = data?.user?.lastName?.[0];
if (first && last) return (first + last).toUpperCase();
return null;
});
function clickOutside(node: HTMLElement) {
const handleClick = (event: MouseEvent) => {
if (node && !node.contains(event.target as Node) && !event.defaultPrevented) {
userMenuOpen = false;
}
};
document.addEventListener('click', handleClick, true);
return () => {
document.removeEventListener('click', handleClick, true);
};
}
</script>
<div class="min-h-screen bg-white" data-hydrated={hydrated || undefined}>
{#if !['/login', '/forgot-password', '/reset-password'].some((p) => page.url.pathname.startsWith(p))}
<header class="sticky top-0 z-50 border-b border-gray-100 bg-white">
<!-- De Gruyter Brill purple accent strip -->
<div class="h-1 bg-brand-purple"></div>
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<!-- Logo & Nav -->
<div class="flex">
<div class="mr-10 flex flex-shrink-0 items-center">
<a href="/" class="flex items-center" aria-label="Familienarchiv">
<span class="font-sans text-xl font-bold tracking-widest text-brand-navy uppercase"
>Familienarchiv</span
>
</a>
</div>
<nav class="hidden items-center sm:flex sm:space-x-1">
<a
href="/"
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
{page.url.pathname === '/' || page.url.pathname.startsWith('/documents')
? 'rounded bg-brand-purple/15 text-brand-navy'
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
>
{m.nav_documents()}
</a>
<a
href="/persons"
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
{page.url.pathname.startsWith('/persons')
? 'rounded bg-brand-purple/15 text-brand-navy'
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
>
{m.nav_persons()}
</a>
<a
href="/conversations"
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
{page.url.pathname.startsWith('/conversations')
? 'rounded bg-brand-purple/15 text-brand-navy'
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
>
{m.nav_conversations()}
</a>
{#if isAdmin}
<a
href="/admin"
class="inline-flex items-center px-3 py-1.5 font-sans text-xs font-bold tracking-widest uppercase transition-colors
{page.url.pathname.startsWith('/admin')
? 'rounded bg-brand-purple/15 text-brand-navy'
: 'rounded text-gray-500 hover:bg-brand-sand/60 hover:text-brand-navy'}"
>
{m.nav_admin()}
</a>
{/if}
</nav>
</div>
<!-- Right Side -->
<div class="flex items-center gap-3">
<!-- Language selector -->
<div class="flex items-center gap-1 border-r border-gray-200 pr-3">
{#each locales as locale (locale)}
<button
type="button"
onclick={() => setLocale(localeMap[locale])}
class="px-1.5 py-1 font-sans text-xs tracking-widest transition-colors
{activeLocale === locale
? 'font-bold text-brand-navy'
: 'font-normal text-gray-400 hover:text-brand-navy'}"
>
{locale}
</button>
{/each}
</div>
<!-- User menu -->
<div
class="relative"
{@attach clickOutside}
onkeydown={(e) => { if (e.key === 'Escape') userMenuOpen = false; }}
role="none"
>
{#if userInitials}
<button
type="button"
aria-expanded={userMenuOpen}
aria-haspopup="true"
onclick={() => (userMenuOpen = !userMenuOpen)}
class="flex h-8 w-8 items-center justify-center rounded-full bg-brand-navy font-sans text-xs font-bold text-white transition-opacity hover:opacity-80"
>
{userInitials}
</button>
{:else}
<button
type="button"
aria-label={m.nav_profile()}
aria-expanded={userMenuOpen}
aria-haspopup="true"
onclick={() => (userMenuOpen = !userMenuOpen)}
class="inline-flex items-center gap-1.5 px-3 py-2 font-sans text-xs font-bold tracking-widest text-gray-400 uppercase transition-colors hover:text-brand-navy"
>
<img
src="/degruyter-icons/Simple/Small-16px/SVG/Action/Account-SM.svg"
alt=""
aria-hidden="true"
class="h-4 w-4 opacity-50"
/>
</button>
{/if}
{#if userMenuOpen}
<div
class="absolute top-full right-0 z-50 mt-1 min-w-[10rem] rounded-sm border border-brand-sand bg-white shadow-md"
>
<a
href="/profile"
onclick={() => (userMenuOpen = false)}
class="block px-4 py-2.5 font-sans text-xs font-bold tracking-widest text-gray-600 uppercase transition-colors hover:bg-brand-sand/40 hover:text-brand-navy"
>
{m.nav_profile()}
</a>
<div class="border-t border-brand-sand">
<form action="/logout" method="POST" use:enhance>
<button
type="submit"
class="w-full px-4 py-2.5 text-left font-sans text-xs font-bold tracking-widest text-gray-400 uppercase transition-colors hover:bg-brand-sand/40 hover:text-brand-navy"
>
{m.nav_logout()}
</button>
</form>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
</header>
{/if}
<main class="py-6">
{@render children()}
</main>
</div>