Ka-Note/ka-note/client/src/routes/+layout.svelte

202 lines
5.4 KiB
Svelte

<script lang="ts">
import "../app.css";
import Sidebar from "$lib/components/Sidebar.svelte";
import BottomTabBar from "$lib/components/BottomTabBar.svelte";
import AiLockBanner from "$lib/components/AiLockBanner.svelte";
import CommandBar from "$lib/components/CommandBar.svelte";
import { commandBarOpen } from "$lib/stores/commandBar";
import { seedIfEmpty } from "$lib/db/seed";
import { sync } from "$lib/sync/syncService";
import { refreshLockStatus } from "$lib/stores/aiLock";
import { onMount, onDestroy } from "svelte";
import { afterNavigate } from "$app/navigation";
import type { Snippet } from "svelte";
import {
handleRedirect,
login,
isAuthenticated,
} from "$lib/auth/authStore.js";
import { scopeColor } from "$lib/stores/scopeContext";
let { children }: { children: Snippet } = $props();
let sidebarOpen = $state(false);
let ready = $state(false);
let authReady = $state(false);
let authenticated = $state(false);
let syncInterval: ReturnType<typeof setInterval> | null = null;
let lockInterval: ReturnType<typeof setInterval> | null = null;
isAuthenticated.subscribe((v) => (authenticated = v));
function startSync() {
sync();
syncInterval = setInterval(sync, 30_000);
refreshLockStatus();
lockInterval = setInterval(refreshLockStatus, 30_000);
document.addEventListener("visibilitychange", onVisibilityChange);
}
function onVisibilityChange() {
if (document.visibilityState === "visible") {
sync();
refreshLockStatus();
}
}
onMount(async () => {
await handleRedirect();
authReady = true;
if (authenticated) {
await seedIfEmpty();
ready = true;
startSync();
}
});
onDestroy(() => {
if (syncInterval) clearInterval(syncInterval);
if (lockInterval) clearInterval(lockInterval);
document.removeEventListener("visibilitychange", onVisibilityChange);
});
$effect(() => {
if (authReady && authenticated && !ready) {
seedIfEmpty().then(() => {
ready = true;
startSync();
});
}
});
function toggleSidebar() {
sidebarOpen = !sidebarOpen;
}
function closeSidebar() {
sidebarOpen = false;
}
function handleGlobalKeydown(e: KeyboardEvent) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
$commandBarOpen = !$commandBarOpen;
}
}
afterNavigate(({ to }) => {
closeSidebar();
if (to?.url.pathname.startsWith("/context/")) {
const id = to.url.pathname.split("/").pop();
if (id && id !== "daily-log") {
// Exclude daily-log from recents
try {
const stored = sessionStorage.getItem("recentContexts");
let recent = stored ? JSON.parse(stored) : [];
recent = [
id,
...recent.filter((x: string) => x !== id),
].slice(0, 5);
sessionStorage.setItem(
"recentContexts",
JSON.stringify(recent),
);
} catch (e) {
console.error("Failed to save recent context", e);
}
}
}
});
$effect(() => {
document.documentElement.style.setProperty(
"--scope-color",
$scopeColor,
);
});
</script>
<svelte:window onkeydown={handleGlobalKeydown} />
{#if !authReady}
<div class="flex h-screen items-center justify-center bg-bg">
<span class="text-muted">Loading...</span>
</div>
{:else if !authenticated}
<div class="flex h-screen items-center justify-center bg-bg">
<div class="text-center">
<h1 class="mb-4 text-2xl font-bold text-accent">KaNote</h1>
<p class="mb-6 text-muted">Sign in to access your notes</p>
<button
class="rounded bg-accent px-6 py-3 font-semibold text-white transition-colors hover:bg-accent/80"
onclick={() => login()}
>
Sign in with Microsoft
</button>
</div>
</div>
{:else if ready}
<div class="flex h-screen overflow-hidden">
<!-- Sidebar: fullscreen on mobile, static on desktop -->
{#if sidebarOpen}
<!-- Backdrop -->
<div
class="fixed inset-0 z-[59] bg-black/50 backdrop-blur-sm md:hidden"
onclick={closeSidebar}
></div>
<!-- Mobile: full-screen overlay -->
<div
class="fixed inset-0 z-[60] flex flex-col bg-sidebar md:hidden"
>
<!-- Header with close button -->
<div
class="flex items-center justify-between border-b border-border px-5 py-4 shrink-0"
style="padding-top: max(1rem, env(safe-area-inset-top));"
>
<span
class="text-lg font-bold text-accent uppercase tracking-wider"
>KaNote</span
>
<button
class="text-2xl text-muted hover:text-white leading-none"
onclick={closeSidebar}
aria-label="Close">&times;</button
>
</div>
<!-- Scrollable content -->
<div
class="flex-1 overflow-y-auto px-5 pb-8"
style="-webkit-overflow-scrolling: touch;"
>
<Sidebar onnavigate={closeSidebar} hideLogo />
</div>
</div>
{/if}
<!-- Desktop sidebar -->
<aside
class="hidden md:flex md:w-[250px] md:flex-col md:border-r md:border-border md:bg-sidebar md:p-5 md:overflow-y-auto"
>
<Sidebar onnavigate={closeSidebar} />
</aside>
<!-- Main content -->
<main class="flex-1 overflow-y-auto p-5 pt-safe md:pt-5 pb-20 md:pb-5">
<div class="mx-auto max-w-[900px]">
<AiLockBanner />
{@render children()}
</div>
</main>
<!-- Bottom tab bar (mobile only, hidden when sidebar open) -->
{#if !sidebarOpen}
<BottomTabBar onsidebaropen={toggleSidebar} />
{/if}
</div>
{:else}
<div class="flex h-screen items-center justify-center bg-bg">
<span class="text-muted">Loading...</span>
</div>
{/if}
<CommandBar />