feat(layout): expose user data to app shell

Add layout.server.ts to pass event.locals.user to the client.
Header now shows user avatar initial with dropdown menu
(Settings link, Log out button).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-09 01:10:37 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent c9e041bffe
commit a9cb60e852
4 changed files with 65 additions and 5 deletions
+52 -1
View File
@@ -6,12 +6,28 @@
interface Props {
onMenuToggle: () => void;
user: { id: number; email: string; displayName: string; role: 'admin' | 'user' } | null;
}
let { onMenuToggle }: Props = $props();
let { onMenuToggle, user }: Props = $props();
let mobileSearchOpen = $state(false);
let dropdownOpen = $state(false);
function getInitial(user: Props['user']): string {
if (!user) return '?';
if (user.displayName) return user.displayName.charAt(0).toUpperCase();
if (user.email) return user.email.charAt(0).toUpperCase();
return '?';
}
async function handleLogout() {
await fetch('/api/auth/logout', { method: 'POST' });
window.location.href = '/login';
}
</script>
<svelte:window onclick={() => (dropdownOpen = false)} />
<header class="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-[var(--border)] bg-[var(--bg-surface)] px-4">
{#if mobileSearchOpen}
<!-- Mobile expanded search -->
@@ -48,6 +64,41 @@
<div class="flex items-center gap-2">
<SyncIndicator />
{#if user}
<div class="relative">
<button
onclick={(e) => { e.stopPropagation(); dropdownOpen = !dropdownOpen; }}
class="flex h-8 w-8 items-center justify-center rounded-full bg-[var(--primary)] text-sm font-medium text-white hover:bg-[var(--primary-hover)]"
aria-label="User menu"
data-testid="user-menu-btn"
>
{getInitial(user)}
</button>
{#if dropdownOpen}
<div class="absolute right-0 top-10 z-50 w-48 rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-surface)] py-1 shadow-[var(--card-shadow)]">
<div class="border-b border-[var(--border-subtle)] px-4 py-2">
<p class="text-sm font-medium text-[var(--text)]">{user.displayName || user.email}</p>
<p class="text-xs text-[var(--text-muted)]">{user.email}</p>
</div>
<a
href="/settings"
class="block px-4 py-2 text-sm text-[var(--text)] hover:bg-[var(--bg-base)]"
>
Settings
</a>
<button
onclick={handleLogout}
class="block w-full px-4 py-2 text-left text-sm text-[var(--text)] hover:bg-[var(--bg-base)]"
data-testid="logout-btn"
>
Log out
</button>
</div>
{/if}
</div>
{/if}
</div>
{/if}
</header>
+7 -3
View File
@@ -1,5 +1,9 @@
import { version } from '../../../package.json';
import type { LayoutServerLoad } from './$types';
export function load() {
return { appVersion: version };
}
export const load: LayoutServerLoad = (event) => {
return {
appVersion: version,
user: event.locals.user
};
};
+1 -1
View File
@@ -21,7 +21,7 @@
</script>
<div class="flex min-h-screen flex-col bg-[var(--bg-base)] text-[var(--text)]">
<Header onMenuToggle={() => (sidebarOpen = !sidebarOpen)} />
<Header onMenuToggle={() => (sidebarOpen = !sidebarOpen)} user={data.user} />
<Sidebar open={sidebarOpen} onClose={() => (sidebarOpen = false)} appVersion={data.appVersion} />
<main class="flex-1 pt-4 transition-all {sidebarOpen ? 'lg:ml-64' : ''}">
+5
View File
@@ -0,0 +1,5 @@
import type { LayoutServerLoad } from './$types.js';
export const load: LayoutServerLoad = ({ locals }) => {
return { user: locals.user };
};