Refactor breadcrumbs into a component that uses route handles

This commit is contained in:
Remmy Cat Stock
2022-10-21 00:48:01 +02:00
parent db4f633a47
commit 85fda204c0
7 changed files with 98 additions and 49 deletions

View File

@@ -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 (
<nav className="breadcrumbs">
{breadcrumbs.map((crumb, i) => {
const isLast = i === breadcrumbs.length - 1;
if (isLast) {
return <div key={crumb.path}>{crumb.name}</div>;
}
return (
<Fragment key={crumb.path}>
<div>
<Link to={crumb.path}>{crumb.name}</Link>
</div>
<div>/</div>
</Fragment>
);
})}
</nav>
);
}

View File

@@ -1,16 +1,13 @@
import { type LinksFunction } from "@remix-run/node";
import { Link, Outlet, useMatches, useParams } from "@remix-run/react";
import type * as React from "react";
import { Outlet } from "@remix-run/react";
import { useTranslation } from "react-i18next";
import { LinkButton } from "~/components/Button";
import { Main } from "~/components/Main";
import { useUser } from "~/modules/auth";
import type { MainWeaponId } from "~/modules/in-game-lists";
import { type SendouRouteHandle } from "~/utils/remix";
import styles from "~/styles/builds.css";
import { atOrError } from "~/utils/arrays";
import { BUILDS_PAGE, userNewBuildPage } from "~/utils/urls";
import { userNewBuildPage } from "~/utils/urls";
import { Breadcrumbs } from "~/components/Breadcrumbs";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
@@ -18,34 +15,17 @@ export const links: LinksFunction = () => {
export const handle: SendouRouteHandle = {
i18n: ["weapons", "builds"],
breadcrumb: ({ t }) => t("pages.builds"),
};
export default function BuildsLayoutPage() {
const user = useUser();
const matches = useMatches();
const { t } = useTranslation(["weapons", "common", "builds"]);
const params = useParams();
const weaponId: MainWeaponId | undefined = atOrError(matches, -1).data?.[
"weaponId"
];
return (
<Main className="stack lg">
<div className="builds__top-container">
<nav className="builds__breadcrumbs">
<SometimesLink isLink={Boolean(params["slug"])}>
{t("common:pages.builds")}
</SometimesLink>
{typeof weaponId === "number" && (
<>
<div>/</div>
<SometimesLink isLink={false}>
{t(`weapons:MAIN_${weaponId}`)}
</SometimesLink>
</>
)}
</nav>
<Breadcrumbs />
{user && (
<LinkButton to={userNewBuildPage(user)} tiny>
{t("builds:addBuild")}
@@ -56,17 +36,3 @@ export default function BuildsLayoutPage() {
</Main>
);
}
function SometimesLink({
children,
isLink,
}: {
children: React.ReactNode;
isLink: boolean;
}) {
if (isLink) {
return <Link to={BUILDS_PAGE}>{children}</Link>;
}
return <div>{children}</div>;
}

View File

@@ -11,6 +11,7 @@ import { BUILDS_PAGE_BATCH_SIZE, BUILDS_PAGE_MAX_BUILDS } from "~/constants";
import { db } from "~/db";
import { i18next } from "~/modules/i18n";
import { weaponIdIsNotAlt } from "~/modules/in-game-lists";
import { type SendouRouteHandle } from "~/utils/remix";
import { makeTitle } from "~/utils/strings";
import { weaponNameSlugToId } from "~/utils/unslugify.server";
@@ -40,9 +41,12 @@ export const loader = async ({ request, params }: LoaderArgs) => {
BUILDS_PAGE_MAX_BUILDS
);
const weaponName = t(`weapons:MAIN_${weaponId}`);
return {
weaponId,
title: makeTitle([t(`weapons:MAIN_${weaponId}`), t("common:pages.builds")]),
weaponName,
title: makeTitle([weaponName, t("common:pages.builds")]),
builds: db.builds.buildsByWeaponId({
weaponId,
limit,
@@ -51,6 +55,14 @@ export const loader = async ({ request, params }: LoaderArgs) => {
};
};
export const handle: SendouRouteHandle = {
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | null;
return data ? data.weaponName : "Unknown";
},
};
export default function WeaponsBuildsPage() {
const data = useLoaderData<typeof loader>();
const { t } = useTranslation(["common"]);

View File

@@ -4,13 +4,6 @@
justify-content: space-between;
}
.builds__breadcrumbs {
display: flex;
font-size: var(--fonts-xs);
font-weight: var(--bold);
gap: var(--s-1);
}
.builds__category {
display: flex;
flex-direction: column;

View File

@@ -872,3 +872,10 @@ dialog::backdrop {
.ability-selector__ability-button.is-dragging {
box-shadow: 0 0 100px inset rgb(255 255 255 / 25%);
}
.breadcrumbs {
display: flex;
font-size: var(--fonts-xs);
font-weight: var(--bold);
gap: var(--s-1);
}

View File

@@ -40,3 +40,8 @@ export function normalizeFormFieldArray(
): string[] {
return value == null ? [] : typeof value === "string" ? [value] : value;
}
/** Can be used as a strongly typed array filter */
export function isDefined<T>(value: T | undefined | null): value is T {
return value !== null && value !== undefined;
}

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { type ReactNode } from "react";
import { type Namespace } from "react-i18next";
import { type TFunction, type Namespace } from "react-i18next";
import { type RouteMatch } from "@remix-run/react";
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
if (!value) throw new Response(null, { status: 404 });
@@ -102,4 +102,12 @@ export function validate(condition: any, status = 400): asserts condition {
export type SendouRouteHandle = {
/** The i18n translation files used for this route, via remix-i18next */
i18n?: Namespace;
/**
* A function that returns the breadcrumb text that should be displayed in
* the <Breadcrumb> component
*/
breadcrumb?: (args: {
match: RouteMatch;
t: TFunction<"common", undefined>;
}) => string | undefined;
};