From 3ce7c00d5bbe2f089a69db6c6a866ac2d0d161eb Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Fri, 21 Oct 2022 18:29:23 +0200 Subject: [PATCH] Detect active nav item via route handle instead of path matching --- app/components/layout/index.tsx | 34 ++++++++++++++++++++++++--------- app/routes/admin.tsx | 10 +++++++++- app/routes/analyzer.tsx | 1 + app/routes/badges.tsx | 1 + app/routes/builds.tsx | 1 + app/routes/calendar/index.tsx | 1 + app/routes/maps.tsx | 1 + app/routes/plus.tsx | 5 +++++ app/utils/remix.ts | 4 ++++ 9 files changed, 48 insertions(+), 10 deletions(-) diff --git a/app/components/layout/index.tsx b/app/components/layout/index.tsx index b05710e8d..b04831c4d 100644 --- a/app/components/layout/index.tsx +++ b/app/components/layout/index.tsx @@ -1,7 +1,8 @@ -import { Link, useLocation } from "@remix-run/react"; +import { Link, useMatches } from "@remix-run/react"; import * as React from "react"; import { useTranslation } from "react-i18next"; import type { RootLoaderData } from "~/root"; +import { type SendouRouteHandle } from "~/utils/remix"; import { LOGO_PATH, navIconUrl } from "~/utils/urls"; import { Image } from "../Image"; import { ColorModeToggle } from "./ColorModeToggle"; @@ -12,6 +13,25 @@ import { Menu } from "./Menu"; import navItems from "./nav-items.json"; import { UserItem } from "./UserItem"; +function useActiveNavItem() { + const matches = useMatches(); + + return React.useMemo(() => { + let activeItem: { name: string; url: string } | undefined = undefined; + + for (const match of matches.reverse()) { + const handle = match.handle as SendouRouteHandle | undefined; + + if (handle?.navItemName) { + activeItem = navItems.find(({ name }) => name === handle.navItemName); + break; + } + } + + return activeItem; + }, [matches]); +} + export const Layout = React.memo(function Layout({ children, patrons, @@ -22,12 +42,8 @@ export const Layout = React.memo(function Layout({ isCatchBoundary?: boolean; }) { const { t } = useTranslation(); - const location = useLocation(); const [menuOpen, setMenuOpen] = React.useState(false); - - const currentPagesNavItem = navItems.find((navItem) => - location.pathname.includes(navItem.name) - ); + const activeNavItem = useActiveNavItem(); return (
@@ -51,15 +67,15 @@ export const Layout = React.memo(function Layout({
setMenuOpen(false)} /> - {currentPagesNavItem && ( + {activeNavItem && (
- {t(`pages.${currentPagesNavItem.name}` as any)} + {t(`pages.${activeNavItem.name}` as any)}
)} {children} diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx index 768532035..8490107ac 100644 --- a/app/routes/admin.tsx +++ b/app/routes/admin.tsx @@ -18,7 +18,11 @@ import { Main } from "~/components/Main"; import { requireUser } from "~/modules/auth"; import { getUser, isImpersonating } from "~/modules/auth/user.server"; import { canPerformAdminActions } from "~/permissions"; -import { parseRequestFormData, validate } from "~/utils/remix"; +import { + parseRequestFormData, + type SendouRouteHandle, + validate, +} from "~/utils/remix"; import { makeTitle } from "~/utils/strings"; import { impersonateUrl, SEED_URL, STOP_IMPERSONATING_URL } from "~/utils/urls"; import { db } from "~/db"; @@ -69,6 +73,10 @@ export const loader: LoaderFunction = async ({ request }) => { }); }; +export const handle: SendouRouteHandle = { + navItemName: "admin", +}; + export default function AdminPage() { return (
diff --git a/app/routes/analyzer.tsx b/app/routes/analyzer.tsx index 8253e30c1..c1bb42f10 100644 --- a/app/routes/analyzer.tsx +++ b/app/routes/analyzer.tsx @@ -52,6 +52,7 @@ export const links: LinksFunction = () => { export const handle: SendouRouteHandle = { i18n: ["weapons", "analyzer"], + navItemName: "analyzer", }; export default function BuildAnalyzerPage() { diff --git a/app/routes/badges.tsx b/app/routes/badges.tsx index 0106ce443..8b64d95ed 100644 --- a/app/routes/badges.tsx +++ b/app/routes/badges.tsx @@ -21,6 +21,7 @@ export interface BadgesLoaderData { export const handle: SendouRouteHandle = { i18n: "badges", + navItemName: "badges", }; export const loader: LoaderFunction = () => { diff --git a/app/routes/builds.tsx b/app/routes/builds.tsx index 6eddd29c6..87c234620 100644 --- a/app/routes/builds.tsx +++ b/app/routes/builds.tsx @@ -16,6 +16,7 @@ export const links: LinksFunction = () => { export const handle: SendouRouteHandle = { i18n: ["weapons", "builds"], breadcrumb: ({ t }) => t("pages.builds"), + navItemName: "builds", }; export default function BuildsLayoutPage() { diff --git a/app/routes/calendar/index.tsx b/app/routes/calendar/index.tsx index ec8983c2f..841e684f6 100644 --- a/app/routes/calendar/index.tsx +++ b/app/routes/calendar/index.tsx @@ -48,6 +48,7 @@ export const meta: MetaFunction = (args) => { export const handle: SendouRouteHandle = { i18n: "calendar", + navItemName: "calendar", }; const loaderSearchParamsSchema = z.object({ diff --git a/app/routes/maps.tsx b/app/routes/maps.tsx index 3b7d8b661..ff152df1a 100644 --- a/app/routes/maps.tsx +++ b/app/routes/maps.tsx @@ -66,6 +66,7 @@ export const meta: MetaFunction = (args) => { export const handle: SendouRouteHandle = { i18n: "game-misc", + navItemName: "maps", }; export const loader = async ({ request }: LoaderArgs) => { diff --git a/app/routes/plus.tsx b/app/routes/plus.tsx index 1425f0f9e..66b061bf8 100644 --- a/app/routes/plus.tsx +++ b/app/routes/plus.tsx @@ -3,11 +3,16 @@ import { Outlet } from "@remix-run/react"; import { Main } from "~/components/Main"; import { SubNav, SubNavLink } from "~/components/SubNav"; import styles from "~/styles/plus.css"; +import { type SendouRouteHandle } from "~/utils/remix"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; }; +export const handle: SendouRouteHandle = { + navItemName: "plus", +}; + export default function PlusPageLayout() { return ( <> diff --git a/app/utils/remix.ts b/app/utils/remix.ts index be1be09f9..3bf68b402 100644 --- a/app/utils/remix.ts +++ b/app/utils/remix.ts @@ -102,6 +102,7 @@ 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 @@ -110,4 +111,7 @@ export type SendouRouteHandle = { match: RouteMatch; t: TFunction<"common", undefined>; }) => string | undefined; + + /** The name of a navItem that is active on this route. See nav-items.json */ + navItemName?: string; };