import { useMatches } from "react-router"; function cleanTitle(raw: unknown): string { return typeof raw === "string" ? (raw.split("|")[0]?.trim() ?? "") : ""; } export function useTitleData(): string { try { const titles = useMatches() .map((match) => match.data) .filter((data: any) => Boolean(data.title) || Boolean(data.meta?.title)) .map((data: any) => data.title ?? data.meta?.title); if (!titles || titles.length === 0) { return ""; } return cleanTitle(titles[titles.length - 1]); } catch { return ""; } } export type BreadcrumbTrailEntry = { label: string; href: string }; function normalizeCrumbPath(path: string): string { return path.length > 1 ? path.replace(/\/+$/, "") : path; } /** * Should the responsible sidebar entry be rendered as an extra parent crumb in * front of the loader-derived trail? * * No, if it is the current page, or if the trail already covers it — either by * label or by target path. The path check matters because a nav entry may carry * a section name ("CRM") while the trail names the page it links to * ("Kontakte"): rendering both yields two crumbs for one route, and the section * crumb then looks like a level that does not exist. * * Also no, when the current page is itself one of the entry's `contextItems` — * i.e. it has its own row in the inner sub-sidenav. That rail already shows * which area the page belongs to, so repeating the area as a crumb adds a level * the user never navigated through (e.g. "Leads > Lead-Statistiken", where * "Leads" just leads back to a sibling of the current page). * * And no, when the trail itself already links to one of those `contextItems`. * A sub-page below such a row is reached through the row, not through the nav * entry, so the trail already names the path the user took — the nav crumb only * adds the rail's own area on top ("Termine > Buchungen > Buchung" for what is * really "Buchungen > Buchung"). */ export function shouldShowParentNavCrumb(args: { navPath: string | null | undefined; navTitle: string; currentPathname: string; currentCrumbLabel: string; trail: readonly BreadcrumbTrailEntry[]; navContextPaths?: readonly (string | undefined)[]; }): boolean { if (!args.navPath) return false; const navPath = normalizeCrumbPath(args.navPath); const currentPath = normalizeCrumbPath(args.currentPathname); if (navPath === currentPath) return false; const contextPaths = (args.navContextPaths ?? []).filter((path): path is string => Boolean(path)).map(normalizeCrumbPath); if (contextPaths.includes(currentPath)) return false; if (args.trail.some((crumb) => contextPaths.includes(normalizeCrumbPath(crumb.href ?? "")))) return false; if (args.navTitle === args.currentCrumbLabel) return false; return !args.trail.some((crumb) => crumb.label === args.navTitle || normalizeCrumbPath(crumb.href ?? "") === navPath); } function pushCrumb(trail: BreadcrumbTrailEntry[], entry: BreadcrumbTrailEntry): void { if (!entry.label) return; // Collapse consecutive duplicates (layout + index route often share a title, // and an ancestor may repeat the parent it points at). if (trail.length > 0 && trail[trail.length - 1].label === entry.label) { trail[trail.length - 1] = entry; return; } trail.push(entry); } /** * Breadcrumb trail derived from route matches that expose a loader `title` * (or `meta.title`). Each entry links to its match's pathname, so sub-pages * get a clickable path back to their parent page. * * A match may also expose `breadcrumbAncestors: { label, href }[]` in its * loader data — extra crumbs inserted before the match's own crumb. Sub-areas * without their own sidebar entry (e.g. the booking-page editor under * /bookings/pages) use this to name the list page they were reached through, * since the flat route convention gives them no shared layout to hang a crumb on. */ export function useBreadcrumbTrail(): BreadcrumbTrailEntry[] { try { const trail: BreadcrumbTrailEntry[] = []; for (const match of useMatches()) { const data = match.data as any; const ancestors = Array.isArray(data?.breadcrumbAncestors) ? (data.breadcrumbAncestors as BreadcrumbTrailEntry[]) : []; for (const ancestor of ancestors) { if (typeof ancestor?.label === "string" && typeof ancestor?.href === "string") { pushCrumb(trail, { label: ancestor.label, href: ancestor.href }); } } const label = cleanTitle(data?.title ?? data?.meta?.title); if (!label) continue; pushCrumb(trail, { label, href: match.pathname }); } return trail; } catch { return []; } }