- Create gotoLogin() helper for login redirects with search params - Add /terms and /privacy public routes with Tailwind typography - Update auth-guard to allow unauthenticated access to public pages - Fix resolve() usage across navigation components using as const pattern - Fix eslint-disable-next-line placement for svelte/no-navigation-without-resolve - Document SvelteKit resolve() patterns in CLAUDE.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
191 lines
7.5 KiB
Svelte
191 lines
7.5 KiB
Svelte
<script lang="ts">
|
|
import { Settings } from "@lucide/svelte";
|
|
import { getContext } from "svelte";
|
|
import { resolve } from "$app/paths";
|
|
import { page } from "$app/stores";
|
|
import { cn } from "$lib/utils.js";
|
|
import OrgSwitcher from "./org-switcher.svelte";
|
|
import UserMenu from "./user-menu.svelte";
|
|
|
|
interface Props {
|
|
class?: string;
|
|
}
|
|
|
|
let { class: className }: Props = $props();
|
|
|
|
// Get optional org context (undefined outside org routes)
|
|
const orgContext = getContext<{ slug: string } | undefined>("orgContext");
|
|
const currentSlug = $derived(orgContext?.slug);
|
|
|
|
// Nav items depend on whether we're in an org context
|
|
const navItems = $derived.by(() => {
|
|
if (currentSlug) {
|
|
// In org context - org-specific navigation
|
|
return [
|
|
{
|
|
icon: "home",
|
|
href: `/dashboard/${currentSlug}`,
|
|
label: "Home",
|
|
},
|
|
{
|
|
icon: "chart",
|
|
href: `/dashboard/${currentSlug}/performance`,
|
|
label: "Performance",
|
|
},
|
|
{
|
|
icon: "document",
|
|
href: `/dashboard/${currentSlug}/reports`,
|
|
label: "Reports",
|
|
},
|
|
];
|
|
}
|
|
// Outside org context - general navigation
|
|
return [
|
|
{
|
|
icon: "home",
|
|
href: "/",
|
|
label: "Home",
|
|
},
|
|
{
|
|
icon: "building",
|
|
href: "/dashboard",
|
|
label: "Organizations",
|
|
},
|
|
];
|
|
});
|
|
</script>
|
|
|
|
<aside
|
|
class={cn(
|
|
"flex h-screen w-[80px] flex-col items-center bg-sidebar glass",
|
|
className,
|
|
)}
|
|
>
|
|
<!-- Org Switcher -->
|
|
<div class="flex h-[94px] items-center justify-center">
|
|
<OrgSwitcher />
|
|
</div>
|
|
|
|
<!-- Main Navigation -->
|
|
<nav class="flex flex-1 flex-col items-center gap-3">
|
|
{#each navItems as item (item.href)}
|
|
{@const isActive =
|
|
item.icon === "home"
|
|
? $page.url.pathname === item.href
|
|
: $page.url.pathname === item.href ||
|
|
$page.url.pathname.startsWith(item.href + "/")}
|
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
|
<a href={item.href}
|
|
class={cn(
|
|
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
|
isActive
|
|
? "bg-sidebar-accent text-sidebar-foreground"
|
|
: "text-sidebar-muted hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
|
|
)}
|
|
aria-label={item.label}
|
|
aria-current={isActive ? "page" : undefined}
|
|
>
|
|
{#if item.icon === "home"}
|
|
{#if isActive}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path
|
|
d="M12.97 2.59a1.5 1.5 0 00-1.94 0l-7.5 6.363A1.5 1.5 0 003 10.097V19.5A1.5 1.5 0 004.5 21h4.75a.75.75 0 00.75-.75V14h4v6.25c0 .414.336.75.75.75h4.75a1.5 1.5 0 001.5-1.5v-9.403a1.5 1.5 0 00-.53-1.144l-7.5-6.363z"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
|
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" stroke-linecap="round" stroke-linejoin="round" />
|
|
<path d="M9 22V12h6v10" stroke-linecap="round" stroke-linejoin="round" />
|
|
</svg>
|
|
{/if}
|
|
{:else if item.icon === "chart"}
|
|
{#if isActive}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path
|
|
d="M18 3a1 1 0 011 1v16a1 1 0 11-2 0V4a1 1 0 011-1zM12 7a1 1 0 011 1v12a1 1 0 11-2 0V8a1 1 0 011-1zM6 11a1 1 0 011 1v8a1 1 0 11-2 0v-8a1 1 0 011-1z"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
|
<path d="M18 20V10M12 20V4M6 20v-6" stroke-linecap="round" stroke-linejoin="round" />
|
|
</svg>
|
|
{/if}
|
|
{:else if item.icon === "document"}
|
|
{#if isActive}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V7.875L14.25 1.5H5.625zM14.25 3v4.5a.75.75 0 00.75.75h4.5L14.25 3zM8.25 12.75a.75.75 0 000 1.5h7.5a.75.75 0 000-1.5h-7.5zm0 3a.75.75 0 000 1.5h7.5a.75.75 0 000-1.5h-7.5z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
|
<path
|
|
d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
<path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke-linecap="round" stroke-linejoin="round" />
|
|
</svg>
|
|
{/if}
|
|
{:else if item.icon === "building"}
|
|
{#if isActive}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M4.5 2.25a.75.75 0 000 1.5v16.5h-.75a.75.75 0 000 1.5h16.5a.75.75 0 000-1.5h-.75V3.75a.75.75 0 000-1.5h-15zM9 6a.75.75 0 000 1.5h1.5a.75.75 0 000-1.5H9zm-.75 3.75A.75.75 0 019 9h1.5a.75.75 0 010 1.5H9a.75.75 0 01-.75-.75zM9 12a.75.75 0 000 1.5h1.5a.75.75 0 000-1.5H9zm3.75-5.25A.75.75 0 0113.5 6H15a.75.75 0 010 1.5h-1.5a.75.75 0 01-.75-.75zM13.5 9a.75.75 0 000 1.5H15A.75.75 0 0015 9h-1.5zm-.75 3.75a.75.75 0 01.75-.75H15a.75.75 0 010 1.5h-1.5a.75.75 0 01-.75-.75zM9 19.5v-2.25a.75.75 0 01.75-.75h4.5a.75.75 0 01.75.75v2.25H9z"
|
|
clip-rule="evenodd"
|
|
/>
|
|
</svg>
|
|
{:else}
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
|
|
<path d="M3 21h18M5 21V5a2 2 0 012-2h10a2 2 0 012 2v16" stroke-linecap="round" stroke-linejoin="round" />
|
|
<path d="M9 6.5h1.5M9 10h1.5M9 13.5h1.5M13.5 6.5H15M13.5 10H15M13.5 13.5H15M9 21v-4h6v4" stroke-linecap="round" stroke-linejoin="round" />
|
|
</svg>
|
|
{/if}
|
|
{/if}
|
|
|
|
<!-- Tooltip -->
|
|
<span
|
|
class="pointer-events-none absolute left-full ml-3 whitespace-nowrap rounded-md bg-foreground px-2.5 py-1.5 text-xs font-medium text-background opacity-0 shadow-lg transition-all duration-150 group-hover:opacity-100"
|
|
>
|
|
{item.label}
|
|
</span>
|
|
</a>
|
|
{/each}
|
|
|
|
</nav>
|
|
|
|
<!-- Bottom section -->
|
|
<div class="flex flex-col items-center gap-3 pb-6">
|
|
<!-- Settings (only in org context) -->
|
|
{#if currentSlug}
|
|
{@const isSettingsActive = $page.url.pathname.startsWith(`/dashboard/${currentSlug}/settings`)}
|
|
<a
|
|
href={resolve("/dashboard/[slug]/settings", { slug: currentSlug })}
|
|
class={cn(
|
|
"group relative flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150",
|
|
isSettingsActive
|
|
? "bg-sidebar-accent text-sidebar-foreground"
|
|
: "text-sidebar-muted hover:bg-sidebar-accent/50 hover:text-sidebar-foreground",
|
|
)}
|
|
aria-label="Settings"
|
|
aria-current={isSettingsActive ? "page" : undefined}
|
|
>
|
|
<Settings class="h-4 w-4" />
|
|
|
|
<!-- Tooltip -->
|
|
<span
|
|
class="pointer-events-none absolute left-full ml-3 whitespace-nowrap rounded-md bg-foreground px-2.5 py-1.5 text-xs font-medium text-background opacity-0 shadow-lg transition-all duration-150 group-hover:opacity-100"
|
|
>
|
|
Settings
|
|
</span>
|
|
</a>
|
|
{/if}
|
|
|
|
<!-- User Menu -->
|
|
<UserMenu />
|
|
</div>
|
|
</aside>
|