From 85fda204c0efc162bb38d9c7d69109ecf662fdb2 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Fri, 21 Oct 2022 00:48:01 +0200 Subject: [PATCH] Refactor breadcrumbs into a component that uses route handles --- app/components/Breadcrumbs.tsx | 58 ++++++++++++++++++++++++++++++++++ app/routes/builds.tsx | 44 +++----------------------- app/routes/builds/$slug.tsx | 14 +++++++- app/styles/builds.css | 7 ---- app/styles/common.css | 7 ++++ app/utils/arrays.ts | 5 +++ app/utils/remix.ts | 12 +++++-- 7 files changed, 98 insertions(+), 49 deletions(-) create mode 100644 app/components/Breadcrumbs.tsx 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/routes/builds.tsx b/app/routes/builds.tsx index 3841500d5..6eddd29c6 100644 --- a/app/routes/builds.tsx +++ b/app/routes/builds.tsx @@ -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 (
- + {user && ( {t("builds:addBuild")} @@ -56,17 +36,3 @@ export default function BuildsLayoutPage() {
); } - -function SometimesLink({ - children, - isLink, -}: { - children: React.ReactNode; - isLink: boolean; -}) { - if (isLink) { - return {children}; - } - - return
{children}
; -} diff --git a/app/routes/builds/$slug.tsx b/app/routes/builds/$slug.tsx index 3fe2fc6bf..b0f716385 100644 --- a/app/routes/builds/$slug.tsx +++ b/app/routes/builds/$slug.tsx @@ -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 | null; + + return data ? data.weaponName : "Unknown"; + }, +}; + export default function WeaponsBuildsPage() { const data = useLoaderData(); const { t } = useTranslation(["common"]); diff --git a/app/styles/builds.css b/app/styles/builds.css index 34d8fa9f9..3960f1f83 100644 --- a/app/styles/builds.css +++ b/app/styles/builds.css @@ -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; diff --git a/app/styles/common.css b/app/styles/common.css index ab61e10dc..2aa9a71f0 100644 --- a/app/styles/common.css +++ b/app/styles/common.css @@ -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); +} diff --git a/app/utils/arrays.ts b/app/utils/arrays.ts index 3c29f98a7..2ab5b0efd 100644 --- a/app/utils/arrays.ts +++ b/app/utils/arrays.ts @@ -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(value: T | undefined | null): value is T { + return value !== null && value !== undefined; +} diff --git a/app/utils/remix.ts b/app/utils/remix.ts index 6ebce50d6..be1be09f9 100644 --- a/app/utils/remix.ts +++ b/app/utils/remix.ts @@ -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(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 component + */ + breadcrumb?: (args: { + match: RouteMatch; + t: TFunction<"common", undefined>; + }) => string | undefined; };