'use client'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Fragment } from 'react'; import { ChevronRight } from 'lucide-react'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, } from '@/components/ui/breadcrumb'; import { useUIStore } from '@/stores/ui-store'; import { useBreadcrumbStore } from '@/stores/breadcrumb-store'; // Human-readable labels for route segments const SEGMENT_LABELS: Record = { dashboard: 'Dashboard', clients: 'Clients', interests: 'Interests', berths: 'Berths', documents: 'Documents', files: 'Files', expenses: 'Expenses', invoices: 'Invoices', email: 'Email', reminders: 'Reminders', settings: 'Settings', admin: 'Administration', reports: 'Reports', new: 'New', edit: 'Edit', profile: 'Profile', }; // UUID v4-ish (or any 36-char hex+dash) - used to skip entity-id segments // from the breadcrumbs since the page H1 already shows the entity name. const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; function isIdSegment(segment: string): boolean { return UUID_RE.test(segment); } function formatSegment(segment: string): string { return ( SEGMENT_LABELS[segment] ?? segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) ); } export function Breadcrumbs() { const pathname = usePathname(); const currentPortSlug = useUIStore((s) => s.currentPortSlug); const hint = useBreadcrumbStore((s) => s.hints[pathname]); // Split pathname and filter empty segments const rawSegments = pathname.split('/').filter(Boolean); // Remove the portSlug segment and any UUID-ish entity-id segments - the // page H1 already shows the entity name, no need to leak the raw id. const segments = ( currentPortSlug ? rawSegments.filter((seg) => seg !== currentPortSlug) : rawSegments ).filter((seg) => !isIdSegment(seg)); if (segments.length === 0) return null; // Build href for each segment from the URL. const urlCrumbs = segments.map((segment, index) => { const segmentsUpToHere = rawSegments.slice(0, rawSegments.indexOf(segment, index) + 1); const href = '/' + segmentsUpToHere.join('/'); const label = formatSegment(segment); const isLast = index === segments.length - 1; return { label, href, isLast }; }); // When a detail page registered a hint, splice in the parent crumbs // (e.g. the parent client name) and replace the trailing label with // the entity's actual name (e.g. "B17"). This turns the URL-only // "Clients › Interests" into "Clients › Mary Smith › Interest › B17" // when the rep clicked from a client page. URL-only renders untouched // when no hint is registered. const crumbs = (() => { if (!hint) return urlCrumbs; const head = urlCrumbs.slice(0, -1).map((c) => ({ ...c, isLast: false })); const parents = hint.parents.map((p) => ({ label: p.label, href: p.href ?? pathname, isLast: false, })); const lastUrlCrumb = urlCrumbs[urlCrumbs.length - 1]; const tail = { label: hint.current, href: lastUrlCrumb?.href ?? pathname, isLast: true, }; return [...head, ...parents, tail]; })(); return ( {crumbs.map((crumb, _index) => ( {crumb.isLast ? ( {crumb.label} ) : ( {crumb.label} )} {!crumb.isLast && ( )} ))} ); }