Files
community-rule/app/not-found.tsx
T
adilallo ca928e306b Match the 3 Nov color ramps and size-based strokes, and replace leftover palette hex with tokens.
Drop Cookies Settings from the footer, link the Home features that already have pages, and give leftover routes their own descriptions.
2026-11-05 15:37:55 -07:00

141 lines
7.0 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import messages from "../messages/en/index";
import { getTranslation } from "../lib/i18n/getTranslation";
import { NO_INDEX_ROBOTS, routeMetadata } from "../lib/siteMetadata";
import { getGovernanceTemplateCatalogEntry } from "../lib/templates/governanceTemplateCatalog";
import Icon from "./components/asset/icon";
import Button from "./components/buttons/Button";
import HeroDecor from "./components/sections/HeroBanner/HeroDecor";
export const metadata: Metadata = routeMetadata("/404", {
title: messages.metadata.notFound.title,
description: messages.metadata.notFound.description,
robots: NO_INDEX_ROBOTS,
});
const NOT_FOUND_TEMPLATE_SLUGS = [
"consensus",
"do-ocracy",
"devolution",
"solidarity-network",
] as const;
/**
* Figma: 404 page frame 22078-808557; 480px lockup 22078-808903; title + CTA group 22078-808908
* (filled / Go home left, outline / Browse right, 16px between).
* Same [HeroDecor](app/components/sections/HeroBanner/HeroDecor.tsx) SVG as home; 404 places it only behind the title stack.
* Shell: [app/layout.tsx](app/layout.tsx) `Top` only — no site footer.
* Template chip row: Figma 22078-809968 — one row, 16px gaps, 20px to hint (no inner scroll; page handles overflow if needed).
* Hero pattern: behind the 404 + bar + h1; wide SVG is painted with overflow-x-clip on `main` so it does not widen the scrollport.
*/
export default function NotFound() {
const t = (key: string) => getTranslation(messages, key);
const templateEntries = NOT_FOUND_TEMPLATE_SLUGS.map((slug) =>
getGovernanceTemplateCatalogEntry(slug),
).filter(
(e): e is NonNullable<typeof e> => e != null,
);
return (
<main
className="relative flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col overflow-x-clip bg-[var(--color-surface-default-primary)]"
aria-labelledby="not-found-heading"
>
<div
className="relative flex min-h-0 w-full min-w-0 max-w-full flex-1 flex-col overflow-x-clip px-[var(--space-200)] sm:px-[var(--space-250)] md:px-[var(--space-400)] lg:px-[var(--space-600)] xl:px-[var(--space-1200)]"
>
<div className="relative z-10 flex min-h-0 w-full max-w-full flex-1 flex-col items-center justify-center py-[var(--space-1000)] sm:py-[var(--space-1200)]">
{/*
Vertical rhythm: 22078-808903 + 22078-808908 — 404→bar 8px, bar→h1 32px, h1→body 16px,
body→CTAs 48px, CTA→templates 40px (lockup flex gap), template→hint 20px
*/}
<div className="mx-auto flex w-full max-w-[480px] flex-col items-center gap-[var(--space-1000)] text-center">
<div className="flex w-full min-w-0 flex-col items-center">
<div className="relative flex w-full flex-col items-center">
<HeroDecor
className="pointer-events-none absolute left-1/2 top-[40%] -z-10 h-[645px] w-[1540px] max-w-none
-translate-x-1/2 -translate-y-1/2
scale-[0.41] sm:scale-[0.45] md:scale-[0.47] lg:scale-[0.5] xl:scale-[0.53]"
/>
<p
className="w-full text-center font-bricolage-grotesque font-extrabold leading-none tracking-[-0.04em] text-[clamp(5.5rem,16vw,13.75rem)] text-[var(--color-content-default-brand-primary)]"
aria-hidden="true"
>
{t("pages.notFoundPage.codeTitle")}
</p>
<div
className="mt-[var(--space-200)] h-1.5 w-[120px] shrink-0 rounded-full bg-[var(--color-yellow-yellow200)]"
aria-hidden="true"
/>
<h1
id="not-found-heading"
className="mt-[var(--space-800)] max-w-full text-center font-bricolage-grotesque font-medium leading-[1.1] text-[40px] text-[var(--color-content-default-primary)] min-[400px]:text-medium-display"
>
{t("pages.notFoundPage.heading")}
</h1>
</div>
<p className="mt-[var(--space-400)] w-full max-w-[443px] text-center text-large-paragraph text-[var(--color-content-default-secondary)]">
{t("pages.notFoundPage.description")}
</p>
<div
dir="ltr"
className="mt-[var(--space-1200)] flex w-full min-w-0 flex-col items-center justify-center gap-[var(--space-400)] min-[400px]:flex-nowrap min-[400px]:flex-row min-[400px]:items-center min-[400px]:justify-center"
>
<Button
href="/"
size="large"
buttonType="filled"
palette="default"
className="inline-flex w-max max-w-full shrink-0 items-center justify-center gap-[var(--space-250)]"
>
<Icon
name="arrow_back"
size={20}
className="shrink-0"
/>
{t("pages.notFoundPage.goHomeCta")}
</Button>
<Button
href="/templates"
size="large"
buttonType="outline"
palette="default"
className="inline-flex w-max max-w-full shrink-0 items-center justify-center"
>
{t("pages.notFoundPage.browseTemplatesCta")}
</Button>
</div>
</div>
{templateEntries.length > 0 ? (
<div className="flex w-full min-w-0 max-w-[36rem] flex-col items-center gap-[var(--space-500)] self-stretch">
<div
className="flex w-full min-w-0 max-md:flex-wrap md:flex-nowrap items-center justify-center gap-x-[var(--space-400)] gap-y-[var(--space-300)]"
role="list"
>
{templateEntries.map((entry) => (
<Link
key={entry.slug}
href={`/create/review-template/${entry.slug}`}
role="listitem"
className={`${entry.backgroundColor} inline-flex h-[37px] shrink-0 items-center justify-center rounded-full px-[20px] py-0 text-center font-bricolage-grotesque text-sm font-extrabold leading-[21px] text-[var(--color-content-invert-primary)] no-underline transition-opacity hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-border-invert-primary)]`}
>
{entry.title}
</Link>
))}
</div>
<p className="w-full text-center text-[13px] font-normal leading-[1.2] text-[var(--color-gray-500)]">
{t("pages.notFoundPage.templateHint")}
</p>
</div>
) : null}
</div>
</div>
</div>
</main>
);
}