174 lines
5.4 KiB
Svelte
174 lines
5.4 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/state';
|
|
import { PUBLIC_CONVEX_URL } from '$env/static/public';
|
|
import { api } from '$convex/_generated/api.js';
|
|
import { onMount } from 'svelte';
|
|
import { BookOpenText, LayoutDashboard, LogOut, Menu, PenLine, Settings } from '@lucide/svelte';
|
|
import { setupConvexAuth, useAuth } from '@mmailaender/convex-auth-svelte/sveltekit';
|
|
import { useConvexClient } from 'convex-svelte';
|
|
import Logo from '$lib/components/Logo.svelte';
|
|
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
|
import './layout.css';
|
|
|
|
let { children, data } = $props();
|
|
|
|
setupConvexAuth({
|
|
getServerState: () => data.authState,
|
|
convexUrl: PUBLIC_CONVEX_URL,
|
|
storage: 'localStorage',
|
|
storageNamespace: 'journaley'
|
|
});
|
|
|
|
const auth = useAuth();
|
|
const client = useConvexClient();
|
|
const isAuthenticated = $derived(auth.isAuthenticated);
|
|
const isLoading = $derived(auth.isLoading);
|
|
const isAuthRoute = $derived(page.url.pathname.startsWith('/signin'));
|
|
let navCollapsed = $state(false);
|
|
let guideSeedRequested = $state(false);
|
|
|
|
const navItems = [
|
|
{ href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
|
{ href: '/entries', label: 'Daily Entries', icon: BookOpenText },
|
|
{ href: '/settings', label: 'Settings', icon: Settings }
|
|
];
|
|
|
|
onMount(() => {
|
|
navCollapsed = localStorage.getItem('journaley-nav-collapsed') === 'true';
|
|
});
|
|
|
|
function toggleNav() {
|
|
navCollapsed = !navCollapsed;
|
|
localStorage.setItem('journaley-nav-collapsed', String(navCollapsed));
|
|
}
|
|
|
|
$effect(() => {
|
|
if (isLoading || !isAuthenticated || guideSeedRequested) return;
|
|
|
|
guideSeedRequested = true;
|
|
void client.mutation(api.entries.ensureMarkdownGuideEntry, {}).catch((error) => {
|
|
console.error('Failed to create markdown guide entry', error);
|
|
});
|
|
});
|
|
</script>
|
|
|
|
{#if isLoading && !isAuthRoute}
|
|
<div class="flex min-h-screen items-center justify-center p-6">
|
|
<div class="app-card max-w-sm p-6 text-center">
|
|
<div class="mx-auto mb-4 text-primary"><Logo size={44} /></div>
|
|
<p class="text-sm text-muted-foreground">Opening your journal...</p>
|
|
</div>
|
|
</div>
|
|
{:else if isAuthenticated && !isAuthRoute}
|
|
<div class="min-h-screen">
|
|
<aside
|
|
class="fixed inset-y-0 left-0 z-30 flex flex-col border-r bg-card/90 px-3 py-4 backdrop-blur transition-[width] duration-200 {navCollapsed
|
|
? 'w-[4.75rem]'
|
|
: 'w-72'}"
|
|
>
|
|
<div class="mb-7 flex items-center justify-between gap-2">
|
|
<a href="/dashboard" class="flex min-w-0 items-center gap-3">
|
|
<span class="shrink-0 text-primary"><Logo /></span>
|
|
{#if !navCollapsed}
|
|
<span class="min-w-0">
|
|
<span class="block text-lg font-bold tracking-tight">Journaley</span>
|
|
<span class="text-xs whitespace-nowrap text-muted-foreground">Write, tag, find.</span>
|
|
</span>
|
|
{/if}
|
|
</a>
|
|
<button
|
|
class="app-button ghost shrink-0 px-2"
|
|
type="button"
|
|
onclick={toggleNav}
|
|
aria-label={navCollapsed ? 'Expand navigation' : 'Collapse navigation'}
|
|
>
|
|
<Menu size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
<nav class="space-y-2">
|
|
{#each navItems as item (item.href)}
|
|
{@const Icon = item.icon}
|
|
<a
|
|
href={item.href}
|
|
class="flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-semibold transition hover:bg-secondary {navCollapsed
|
|
? 'justify-center'
|
|
: ''} {page.url.pathname.startsWith(item.href)
|
|
? 'bg-secondary text-foreground'
|
|
: 'text-muted-foreground'}"
|
|
title={navCollapsed ? item.label : undefined}
|
|
>
|
|
<Icon class="shrink-0" size={18} />
|
|
{#if !navCollapsed}
|
|
<span>{item.label}</span>
|
|
{/if}
|
|
</a>
|
|
{/each}
|
|
</nav>
|
|
|
|
{#if !navCollapsed}
|
|
<div class="mt-8 rounded-lg border bg-background/70 p-4">
|
|
<div class="mb-2 flex items-center gap-2 text-sm font-semibold">
|
|
<PenLine size={16} />
|
|
Today counts
|
|
</div>
|
|
<p class="text-sm text-muted-foreground">
|
|
A few honest lines are enough. Keep the thread alive.
|
|
</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="mt-auto space-y-2">
|
|
<div class={navCollapsed ? 'flex justify-center' : ''}>
|
|
<ThemeToggle />
|
|
</div>
|
|
<button
|
|
class="app-button ghost w-full {navCollapsed ? 'justify-center px-2' : ''}"
|
|
type="button"
|
|
onclick={() => auth.signOut()}
|
|
title={navCollapsed ? 'Sign out' : undefined}
|
|
>
|
|
<LogOut size={16} />
|
|
{#if !navCollapsed}
|
|
<span>Sign out</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<div
|
|
class="min-w-0 transition-[padding] duration-200 {navCollapsed ? 'pl-[4.75rem]' : 'pl-72'}"
|
|
>
|
|
<header
|
|
class="sticky top-0 z-20 flex items-center justify-between border-b bg-background/82 px-4 py-3 backdrop-blur lg:px-8"
|
|
>
|
|
<button class="app-button secondary px-3" type="button" onclick={toggleNav}>
|
|
<Menu size={16} />
|
|
<span class="hidden sm:inline">{navCollapsed ? 'Expand' : 'Collapse'} menu</span>
|
|
</button>
|
|
|
|
<nav class="hidden items-center gap-1 lg:flex">
|
|
{#each navItems as item (item.href)}
|
|
<a
|
|
href={item.href}
|
|
class="rounded-md px-3 py-2 text-sm font-semibold text-muted-foreground transition hover:bg-secondary hover:text-foreground"
|
|
>
|
|
{item.label}
|
|
</a>
|
|
{/each}
|
|
</nav>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<span class="hidden text-sm text-muted-foreground md:inline">Your private journal</span>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="mx-auto w-full max-w-7xl px-4 py-6 lg:px-8 lg:py-8">
|
|
{@render children()}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
{@render children()}
|
|
{/if}
|