feat: add dashboard shell and shared components
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user