From 0167b03231bbc5dc0b96b3ba68dfe81c8a7f9bc1 Mon Sep 17 00:00:00 2001 From: adilallo <39313955+adilallo@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:15:04 -0600 Subject: [PATCH] feat: add privacy, terms, and cookies pages Footer and login legal links were stubs. Point them at real marketing pages that follow the Figma content-page template. --- .../_components/LegalDocumentPage.tsx | 202 ++++++++++++++++++ app/(marketing)/cookies/page.tsx | 27 +++ app/(marketing)/privacy/page.tsx | 27 +++ app/(marketing)/terms/page.tsx | 27 +++ lib/legalSyntheticPost.ts | 53 +++++ messages/en/components/footer.json | 7 +- messages/en/index.ts | 6 + messages/en/marketing.ts | 6 + messages/en/metadata.json | 15 ++ messages/en/pages/cookies.json | 28 +++ messages/en/pages/privacy.json | 43 ++++ messages/en/pages/terms.json | 36 ++++ tests/components/Footer.test.tsx | 13 +- tests/components/LoginForm.test.tsx | 10 + tests/pages/legal.test.tsx | 112 ++++++++++ tests/unit/legalSyntheticPost.test.ts | 31 +++ 16 files changed, 634 insertions(+), 9 deletions(-) create mode 100644 app/(marketing)/_components/LegalDocumentPage.tsx create mode 100644 app/(marketing)/cookies/page.tsx create mode 100644 app/(marketing)/privacy/page.tsx create mode 100644 app/(marketing)/terms/page.tsx create mode 100644 lib/legalSyntheticPost.ts create mode 100644 messages/en/pages/cookies.json create mode 100644 messages/en/pages/privacy.json create mode 100644 messages/en/pages/terms.json create mode 100644 tests/pages/legal.test.tsx create mode 100644 tests/unit/legalSyntheticPost.test.ts diff --git a/app/(marketing)/_components/LegalDocumentPage.tsx b/app/(marketing)/_components/LegalDocumentPage.tsx new file mode 100644 index 0000000..1e28c3b --- /dev/null +++ b/app/(marketing)/_components/LegalDocumentPage.tsx @@ -0,0 +1,202 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import type { ReactNode } from "react"; +import { + buildLegalSyntheticPost, + type LegalDocumentMessages, + type LegalDocumentSlug, +} from "../../../lib/legalSyntheticPost"; +import ContentBanner from "../../components/sections/ContentBanner"; + +type LegalDocumentPageProps = { + slug: LegalDocumentSlug; + path: string; + page: LegalDocumentMessages; +}; + +const INLINE_LINK = /\[([^\]]+)\]\(([^)]+)\)/g; + +const ARTICLE_COLUMN_CLASS = + "mx-auto flex w-full flex-col gap-[var(--space-800,32px)] font-inter text-[var(--color-content-default-primary)] text-[16px] leading-[24px] sm:max-w-[390px] sm:text-[18px] sm:leading-[130%] md:max-w-[472px] lg:max-w-[700px] lg:text-[24px] lg:leading-[32px] xl:max-w-[904px] xl:text-[32px] xl:leading-[40px]"; + +const LINK_CLASS = "underline decoration-solid underline-offset-2"; + +function isSafeHref(href: string): boolean { + return ( + (href.startsWith("/") && !href.startsWith("//")) || + href.startsWith("https://") || + href.startsWith("http://") || + href.startsWith("mailto:") + ); +} + +function LegalInlineText({ text }: { text: string }) { + const nodes: ReactNode[] = []; + let lastIndex = 0; + let key = 0; + + for (const match of text.matchAll(INLINE_LINK)) { + const matchIndex = match.index ?? 0; + if (matchIndex > lastIndex) { + nodes.push(text.slice(lastIndex, matchIndex)); + } + + const label = match[1]; + const href = match[2]; + if (isSafeHref(href)) { + const isInternal = href.startsWith("/") && !href.startsWith("//"); + nodes.push( + isInternal ? ( + + {label} + + ) : ( + + {label} + + ), + ); + key += 1; + } else { + nodes.push(label); + } + + lastIndex = matchIndex + match[0].length; + } + + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)); + } + + return nodes; +} + +export function legalPageMetadata( + meta: { title: string; description: string; keywords: string[] }, + page: { banner: { title: string; description: string } }, +): Metadata { + return { + title: meta.title, + description: meta.description, + keywords: meta.keywords, + openGraph: { + title: page.banner.title, + description: page.banner.description, + type: "website", + siteName: "CommunityRule", + }, + }; +} + +/** + * Public legal documents (`/privacy`, `/terms`, `/cookies`). + * + * Figma: "Content page Template" (19003:23305) — ContentBanner guide + * (22078:806964) + article column (19003:23574). Body stacks follow + * Type / Text Block (22001:29793): heading + paragraphs. Omits Related + * articles, Ask Organizer, and decorative shapes. + * https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305 + */ +export default function LegalDocumentPage({ + slug, + path, + page, +}: LegalDocumentPageProps) { + const post = buildLegalSyntheticPost(slug, page); + + const structuredData = { + "@context": "https://schema.org", + "@type": "WebPage", + name: page.banner.title, + description: page.banner.description, + url: `https://communityrule.com${path}`, + dateModified: page.banner.date, + publisher: { + "@type": "Organization", + name: "CommunityRule", + url: "https://communityrule.com", + }, + }; + + const breadcrumbData = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Home", + item: "https://communityrule.com", + }, + { + "@type": "ListItem", + position: 2, + name: page.banner.title, + item: `https://communityrule.com${path}`, + }, + ], + }; + + return ( + <> +