diff --git a/.eslintrc.js b/.eslintrc.js
index cb9b9e50c..1d0db8d65 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -30,6 +30,7 @@ module.exports = {
"@typescript-eslint/no-unsafe-argument": 0,
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/no-explicit-any": 0,
+ "react/prop-types": 0,
},
settings: {
react: {
diff --git a/app/components/Breadcrumbs.tsx b/app/components/Breadcrumbs.tsx
new file mode 100644
index 000000000..c17d627be
--- /dev/null
+++ b/app/components/Breadcrumbs.tsx
@@ -0,0 +1,58 @@
+import { Link, useMatches } from "@remix-run/react";
+import { useMemo, Fragment } from "react";
+import { useTranslation } from "react-i18next";
+import { isDefined } from "~/utils/arrays";
+import { type SendouRouteHandle } from "~/utils/remix";
+
+type Crumb = {
+ path: string;
+ name: string;
+};
+
+function useBreadcrumbs(): Crumb[] {
+ const matches = useMatches();
+ const { t } = useTranslation("common");
+
+ return useMemo(
+ () =>
+ matches
+ .map((match) => {
+ const handle = match.handle as undefined | SendouRouteHandle;
+ const name = handle?.breadcrumb?.({ match, t });
+ return name ? { path: match.pathname, name } : undefined;
+ })
+ .filter(isDefined),
+ [matches, t]
+ );
+}
+
+export function Breadcrumbs() {
+ const breadcrumbs = useBreadcrumbs();
+
+ const showBreadcrumbs = breadcrumbs.length > 0;
+
+ if (!showBreadcrumbs) {
+ return null;
+ }
+
+ return (
+
+ );
+}
diff --git a/app/components/Label.tsx b/app/components/Label.tsx
index 828456dc7..08e349d28 100644
--- a/app/components/Label.tsx
+++ b/app/components/Label.tsx
@@ -13,6 +13,7 @@ type LabelProps = Pick<
};
required?: boolean;
className?: string;
+ labelClassName?: string;
};
export function Label({
@@ -21,10 +22,11 @@ export function Label({
children,
htmlFor,
className,
+ labelClassName,
}: LabelProps) {
return (
-