Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions components/navigation/NavbarWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { NO_NAVBAR_PAGES } from "@/lib/navbar";
import { isNoNavbarPage } from "@/lib/navbar";
import { usePathname } from "next/navigation";
import React from "react";

Expand All @@ -13,7 +13,7 @@ export default function NavbarWrapper({
}) {
const pathname = usePathname();

if (NO_NAVBAR_PAGES.includes(pathname)) {
if (isNoNavbarPage(pathname)) {
return <>{noNavbarChildren}</>;
}

Expand Down
16 changes: 16 additions & 0 deletions lib/navbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const NAVBAR_ITEMS: NavbarItem[] = [
{ label: "Events", href: "/events", icon: "calendar" },
];

// Entries ending in "/" also match every path under them (see matchesPage).
export const NO_NAVBAR_PAGES = [
ORGANIZATION_NOT_FOUND_PATH,
"/login",
Expand All @@ -56,4 +57,19 @@ export const NO_NAVBAR_PAGES = [
"/forgotpassword",
];

/**
* Whether `pathname` is one of `pages`. An entry ending in "/" matches any
* path under it: "/forms/" matches "/forms/abc" but not "/forms" or
* "/formsabc". Other entries match only the exact path.
*/
export function matchesPage(pathname: string, pages: readonly string[]) {
return pages.some((page) =>
page.endsWith("/") ? pathname.startsWith(page) : pathname === page,
);
}

export function isNoNavbarPage(pathname: string) {
return matchesPage(pathname, NO_NAVBAR_PAGES);
}

export const MEMBERSHIP_REDIRECT_EXCLUDED_PAGES = NO_NAVBAR_PAGES;
10 changes: 10 additions & 0 deletions styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@
transparent
);

/* Theme tokens that will follow each org's theme (#300). They hold today's
values, so using them changes nothing yet. */
--color-page-bg: #ffffff;
--color-page-text: var(--color-grey-text-strong);
/* Text on brand-colored surfaces, e.g. a primary button's label. */
--color-brand-foreground: #ffffff;
--font-display: var(--font-heading);
/* Corner radius for inputs and buttons (Tailwind's `rounded`). */
--radius-control: 0.25rem;

--text-desktop-display: 48px;
--text-desktop-heading-1: 40px;
--text-desktop-heading-2: 32px;
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/components/navigation/NavbarWrapper.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import NavbarWrapper from "@/components/navigation/NavbarWrapper";

const pathname = vi.hoisted(() => ({ current: "/" }));

vi.mock("next/navigation", () => ({
usePathname: () => pathname.current,
}));

// No real entry is a prefix yet (#299 and #301 add them), so add one.
vi.mock("@/lib/navbar", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/navbar")>();
const pages = [...actual.NO_NAVBAR_PAGES, "/forms/"];
return {
...actual,
NO_NAVBAR_PAGES: pages,
isNoNavbarPage: (path: string) => actual.matchesPage(path, pages),
};
});

function renderAt(path: string) {
pathname.current = path;
render(
<NavbarWrapper noNavbarChildren={<p>without navbar</p>}>
<p>with navbar</p>
</NavbarWrapper>,
);
}

describe("NavbarWrapper", () => {
afterEach(cleanup);

it("renders the navbar layout on regular pages", () => {
renderAt("/events");
expect(screen.getByText("with navbar")).toBeTruthy();
});

it("renders without the navbar on excluded pages", () => {
renderAt("/login");
expect(screen.getByText("without navbar")).toBeTruthy();
});
Comment thread
shaply marked this conversation as resolved.

it("renders without the navbar under a prefix entry", () => {
renderAt("/forms/camp-application-2027");
expect(screen.getByText("without navbar")).toBeTruthy();
});

it("renders the navbar on a path that only shares the prefix's letters", () => {
renderAt("/formsabc");
expect(screen.getByText("with navbar")).toBeTruthy();
});
});
34 changes: 34 additions & 0 deletions tests/unit/lib/navbar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @vitest-environment node
import { describe, expect, it } from "vitest";
import { isNoNavbarPage, matchesPage, NO_NAVBAR_PAGES } from "@/lib/navbar";

describe(matchesPage, () => {
const pages = ["/login", "/forms/"];

it("matches any path under an entry ending in /", () => {
expect(matchesPage("/forms/abc", pages)).toBe(true);
expect(matchesPage("/forms/camp-application-2027/edit", pages)).toBe(true);
});

it("doesn't match a path that only shares the prefix's letters", () => {
expect(matchesPage("/formsabc", pages)).toBe(false);
expect(matchesPage("/forms", pages)).toBe(false);
});

it("matches other entries exactly", () => {
expect(matchesPage("/login", pages)).toBe(true);
expect(matchesPage("/login/help", pages)).toBe(false);
expect(matchesPage("/loginx", pages)).toBe(false);
});
});

describe(isNoNavbarPage, () => {
it.each(NO_NAVBAR_PAGES)("hides the navbar on %s", (page) => {
expect(isNoNavbarPage(page)).toBe(true);
});

it("shows the navbar elsewhere", () => {
expect(isNoNavbarPage("/")).toBe(false);
expect(isNoNavbarPage("/events")).toBe(false);
});
});
Loading