Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/data/useTitleData.ts
T

111 lines
4.6 KiB
TypeScript

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 [];
}
}