- Install eslint, prettier and related plugins - Add lint and format npm scripts - Configure eslint.config.js with Svelte + TypeScript rules - Configure .prettierrc with Svelte plugin - Fix code to comply with lint rules: - Use resolve() for navigation links - Use SvelteMap for reactive maps - Use writable instead of + - Remove unused imports and variables Note: ignoreGoto is set to true due to eslint-plugin-svelte#1327
125 lines
4.5 KiB
Svelte
125 lines
4.5 KiB
Svelte
<script lang="ts">
|
|
import '../app.css';
|
|
import { page } from '$app/state';
|
|
import { resolve } from '$app/paths';
|
|
import { m } from '$lib/paraglide/messages.js';
|
|
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte';
|
|
|
|
let { children } = $props();
|
|
|
|
let mobileMenuOpen = $state(false);
|
|
|
|
const navItems = $derived([
|
|
{ href: resolve('/'), label: m.nav_dashboard(), icon: '🏠' },
|
|
{ href: resolve('/products'), label: m.nav_products(), icon: '🧴' },
|
|
{ href: resolve('/routines'), label: m.nav_routines(), icon: '📋' },
|
|
{ href: resolve('/routines/grooming-schedule'), label: m.nav_grooming(), icon: '🪒' },
|
|
{ href: resolve('/health/medications'), label: m.nav_medications(), icon: '💊' },
|
|
{ href: resolve('/health/lab-results'), label: m["nav_labResults"](), icon: '🔬' },
|
|
{ href: resolve('/skin'), label: m.nav_skin(), icon: '✨' }
|
|
]);
|
|
|
|
function isActive(href: string) {
|
|
if (href === '/') return page.url.pathname === '/';
|
|
const pathname = page.url.pathname;
|
|
if (!pathname.startsWith(href)) return false;
|
|
// Don't mark parent as active if a more-specific nav item also matches
|
|
const moreSpecific = navItems.some(
|
|
(item) => item.href !== href && item.href.startsWith(href) && pathname.startsWith(item.href)
|
|
);
|
|
return !moreSpecific;
|
|
}
|
|
</script>
|
|
|
|
<div class="flex min-h-screen flex-col bg-background md:flex-row">
|
|
<!-- Mobile header -->
|
|
<header class="flex items-center justify-between border-b border-border bg-card px-4 py-3 md:hidden">
|
|
<div>
|
|
<span class="text-sm font-semibold tracking-tight">{m["nav_appName"]()}</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
|
|
class="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
aria-label="Toggle menu"
|
|
>
|
|
{#if mobileMenuOpen}
|
|
<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"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></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"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
{/if}
|
|
</button>
|
|
</header>
|
|
|
|
<!-- Mobile drawer overlay -->
|
|
{#if mobileMenuOpen}
|
|
<!-- Backdrop: closes drawer on click -->
|
|
<button
|
|
type="button"
|
|
class="fixed inset-0 z-50 bg-black/50 md:hidden"
|
|
onclick={() => (mobileMenuOpen = false)}
|
|
aria-label={m.common_cancel()}
|
|
></button>
|
|
<!-- Drawer (same z-50 but later in DOM → on top of backdrop) -->
|
|
<nav
|
|
class="fixed inset-y-0 left-0 z-50 w-64 overflow-y-auto bg-card px-3 py-6 md:hidden"
|
|
>
|
|
<div class="mb-8 px-3">
|
|
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
|
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
|
</div>
|
|
<ul class="space-y-1">
|
|
{#each navItems as item}
|
|
<li>
|
|
<a
|
|
href={item.href}
|
|
onclick={() => (mobileMenuOpen = false)}
|
|
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors
|
|
{isActive(item.href)
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
>
|
|
<span class="text-base">{item.icon}</span>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<div class="mt-6 px-3">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</nav>
|
|
{/if}
|
|
|
|
<!-- Desktop Sidebar -->
|
|
<nav class="hidden w-56 shrink-0 flex-col border-r border-border bg-card px-3 py-6 md:flex">
|
|
<div class="mb-8 px-3">
|
|
<h1 class="text-lg font-semibold tracking-tight">{m["nav_appName"]()}</h1>
|
|
<p class="text-xs text-muted-foreground">{m["nav_appSubtitle"]()}</p>
|
|
</div>
|
|
<ul class="space-y-1">
|
|
{#each navItems as item}
|
|
<li>
|
|
<a
|
|
href={item.href}
|
|
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors
|
|
{isActive(item.href)
|
|
? 'bg-accent text-accent-foreground font-medium'
|
|
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
>
|
|
<span class="text-base">{item.icon}</span>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
<div class="mt-6 px-3">
|
|
<LanguageSwitcher />
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main content -->
|
|
<main class="flex-1 overflow-auto p-4 md:p-8">
|
|
{@render children()}
|
|
</main>
|
|
</div>
|