Files
edr-platform/apps/edr-freight-web/backoffice/src/shared/components/ui/DynamicBreadcrumb.tsx
2026-07-10 10:41:48 +03:00

164 lines
5.8 KiB
TypeScript

import { useMemo } from "react";
import { Link, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Breadcrumb, BreadcrumbList } from "@/shared/common/ui/breadcrumb";
import { ChevronRight, Home } from "lucide-react";
import { capitalize } from "@/shared/lib/utils";
interface BreadcrumbNode {
label: string;
path: string;
isActive: boolean;
isEllipsis?: boolean;
}
const MODULE_LANDING_PATHS = new Set([
"/record-management/dashboard",
"/objective-management/plan-years",
"/performance-management/plan-years",
"/dms/dashboard",
]);
// Module roots and plan-year list pages are reachable from the sidebar/module
// switcher — omit them from breadcrumbs to avoid redundant navigation.
const HIDDEN_BREADCRUMB_SEGMENTS = new Set([
"plan-years",
"record-management",
"objective-management",
"performance-management",
"dms",
"user-management",
]);
const isDynamicId = (segment: string) =>
/^[0-9a-f]{8,}$/i.test(segment) || /^[0-9]+$/.test(segment);
const makeReadableLabel = (segment: string) => {
const withSpaces = segment
.replace(/([a-z])([A-Z])/g, "$1 $2")
.replace(/[_-]/g, " ");
return capitalize(withSpaces);
};
export function DynamicBreadcrumb() {
const location = useLocation();
const { t } = useTranslation();
const breadcrumbs = useMemo(() => {
if (MODULE_LANDING_PATHS.has(location.pathname)) {
return [] as BreadcrumbNode[];
}
const pathSegments = location.pathname.split("/").filter(Boolean);
if (pathSegments.length === 0) return [] as BreadcrumbNode[];
const labelMap: Record<string, string> = {
dashboard: t("header.navigation.dashboard"),
userRecords: t("header.navigation.outgoing"),
userIncoming: t("header.navigation.incoming"),
delegation: t("header.navigation.delegation"),
approval: t("header.navigation.approval"),
pending: t("header.navigation.pending"),
collaborations: t("header.navigation.collaborations"),
recordOfficer: "Record Officer",
incoming: t("header.navigation.incoming"),
outgoing: t("header.navigation.outgoing"),
add: t("common.create"),
new: t("common.create"),
edit: t("common.Edit"),
profile: t("profile.profile"),
"update-profile": t("profile.updateProfile"),
"change-password": t("header.changePassword"),
uploadTeeterandSignature: t("header.addTeeter"),
viewDelegationLetter: t("delegation.viewLetter"),
editDelegation: t("delegation.editDelegation"),
incomingHistory: "Incoming History",
incomingNonSmartExternal: "External Incoming",
viewIncoming: "View Incoming",
goals: t("organization.Goals", "Goals"),
"detail-task": t("organization.detail_task", "Detail Task"),
"Major-Task": t("organization.Major Task", "Major Task"),
expectations: t("organization.expectations", "Expectations"),
"employee-expectations": t(
"organization.employeeExpectations",
"Employee Expectations",
),
achievements: t("organization.achievements", "Achievements"),
"additional-tasks": t("organization.additionalTasks", "Additional Tasks"),
"signature-settings": t(
"organization.Signature Settings",
"Signature Settings",
),
};
let currentPath = "";
const items: BreadcrumbNode[] = [];
for (const segment of pathSegments) {
currentPath += `/${segment}`;
if (HIDDEN_BREADCRUMB_SEGMENTS.has(segment) || isDynamicId(segment)) {
continue;
}
const baseLabel =
labelMap[segment] || makeReadableLabel(segment);
items.push({
label: baseLabel,
path: currentPath,
isActive: false,
});
}
if (items.length > 0) {
items[items.length - 1].isActive = true;
}
return items;
}, [location.pathname, t]);
if (!breadcrumbs.length) {
return null;
}
const compactBreadcrumbs = breadcrumbs;
return (
<div className="max-w-full overflow-hidden rounded-xl bg-white/80 px-2.5 py-1.5 shadow-sm ring-1 ring-black/5 backdrop-blur dark:bg-gray-800/70 dark:ring-white/10">
<Breadcrumb>
<BreadcrumbList className="flex-nowrap gap-0.5 overflow-x-auto whitespace-nowrap pb-0.5 text-xs">
{compactBreadcrumbs.map((breadcrumb, index) => (
<div key={`${breadcrumb.path}-${index}`} className="flex items-center">
{index > 0 && (
<span className="mx-1 text-gray-400 dark:text-gray-500">
<ChevronRight className="h-3.5 w-3.5" />
</span>
)}
{breadcrumb.isEllipsis ? (
<span className="px-2 py-1 text-xs font-semibold text-gray-400 dark:text-gray-500">
...
</span>
) : breadcrumb.isActive ? (
<span className="inline-flex max-w-[320px] items-center truncate rounded-full bg-primary-50 px-2 py-1 text-xs font-semibold text-primary-700 ring-1 ring-inset ring-primary-200/70 dark:bg-primary-900/40 dark:text-white dark:ring-primary-700/50" title={breadcrumb.label}>
{breadcrumb.label}
</span>
) : (
<Link
to={breadcrumb.path}
className="inline-flex max-w-[240px] items-center gap-1 truncate rounded-full px-2 py-1 text-xs font-medium text-gray-500 transition-colors hover:bg-primary-50 hover:text-primary-700 dark:text-gray-300 dark:hover:bg-primary/10 dark:hover:text-primary-foreground"
title={breadcrumb.label}
>
{index === 0 && <Home className="h-3.5 w-3.5" />}
<span className="truncate">{breadcrumb.label}</span>
</Link>
)}
</div>
))}
</BreadcrumbList>
</Breadcrumb>
</div>
);
}