feat: add dashboard shell and shared components

This commit is contained in:
2026-06-05 23:25:30 +01:00
parent 7bb19e1be3
commit 930e83344c
7 changed files with 473 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
<script lang="ts">
import { page } from '$app/state';
import AppSidebar from '$lib/components/app-sidebar.svelte';
import * as Breadcrumb from '$lib/components/ui/breadcrumb/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
const { children } = $props();
type BreadcrumbItem = {
label: string;
href?: string;
};
const segmentLabels: Record<string, string> = {
dashboard: 'Dashboard',
admins: 'Admins',
clients: 'Clients',
addresses: 'Addresses',
contacts: 'Contacts',
bookings: 'Bookings',
rooms: 'Rooms',
services: 'Services',
invoices: 'Invoices',
contracts: 'Contracts'
};
function titleCase(segment: string) {
return segment
.split('-')
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
function recordLabel(value: unknown) {
if (typeof value !== 'object' || value === null) return null;
const record = value as Record<string, unknown>;
for (const key of ['name', 'title', 'invoiceNumber', 'label'] as const) {
if (key in record) return String(record[key]);
}
return null;
}
function dynamicSegmentLabel(
paramName: string,
fallback: string,
routeSegments: string[],
index: number
) {
const routeParent = routeSegments[index - 1];
const singularRouteParent = routeParent?.replace(/s$/, '');
const data = page.data as Record<string, unknown>;
const params = page.params as Record<string, string | undefined>;
const routeRecord = singularRouteParent ? data[singularRouteParent] : undefined;
return recordLabel(routeRecord) ?? params[paramName] ?? fallback;
}
let breadcrumbs = $derived.by<BreadcrumbItem[]>(() => {
const pathSegments = page.url.pathname.split('/').filter(Boolean);
const routeSegments = page.route.id?.split('/').filter(Boolean) ?? pathSegments;
return pathSegments.map((segment, index) => {
const routeSegment = routeSegments[index];
const dynamicMatch = routeSegment?.match(/^\[(.+)\]$/);
const label = dynamicMatch
? dynamicSegmentLabel(dynamicMatch[1], segment, routeSegments, index)
: (segmentLabels[segment] ?? titleCase(segment));
const href = `/${pathSegments.slice(0, index + 1).join('/')}`;
const isLast = index === pathSegments.length - 1;
return isLast ? { label } : { label, href };
});
});
</script>
<Sidebar.Provider>
<AppSidebar />
<Sidebar.Inset>
<header class="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<Sidebar.Trigger class="-ms-1" />
<Separator orientation="vertical" class="me-2 h-4" />
<Breadcrumb.Root>
<Breadcrumb.List>
{#each breadcrumbs as item, index (`${item.href ?? page.url.pathname}:${item.label}`)}
{#if index > 0}
<Breadcrumb.Separator class="hidden md:block" />
{/if}
<Breadcrumb.Item class={index === 0 ? 'hidden md:block' : undefined}>
{#if item.href}
<Breadcrumb.Link href={item.href}>{item.label}</Breadcrumb.Link>
{:else}
<Breadcrumb.Page>{item.label}</Breadcrumb.Page>
{/if}
</Breadcrumb.Item>
{/each}
</Breadcrumb.List>
</Breadcrumb.Root>
</header>
<div class="flex flex-1 flex-col gap-4 p-4">
{@render children()}
</div>
</Sidebar.Inset>
</Sidebar.Provider>