112 lines
3.4 KiB
Svelte
112 lines
3.4 KiB
Svelte
<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 { data, children } = $props();
|
|
|
|
type BreadcrumbItem = {
|
|
label: string;
|
|
href?: string;
|
|
};
|
|
|
|
const segmentLabels: Record<string, string> = {
|
|
dashboard: 'Dashboard',
|
|
organization: 'Organization',
|
|
details: 'Details',
|
|
admins: 'Admins',
|
|
clients: 'Clients',
|
|
addresses: 'Addresses',
|
|
contacts: 'Contacts',
|
|
bookings: 'Bookings',
|
|
rooms: 'Rooms',
|
|
services: 'Services',
|
|
billing: 'Billing',
|
|
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 organizations={data.organizations} activeOrganization={data.activeOrganization} />
|
|
<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>
|