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.
This commit is contained in:
@@ -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 ? (
|
||||
<Link key={key} href={href} className={LINK_CLASS}>
|
||||
{label}
|
||||
</Link>
|
||||
) : (
|
||||
<a key={key} href={href} className={LINK_CLASS}>
|
||||
{label}
|
||||
</a>
|
||||
),
|
||||
);
|
||||
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 (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(structuredData),
|
||||
}}
|
||||
/>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: JSON.stringify(breadcrumbData),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative min-h-screen overflow-x-hidden bg-transparent">
|
||||
<ContentBanner post={post} variant="guide" />
|
||||
|
||||
<article
|
||||
className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]"
|
||||
data-node-id="19003:23574"
|
||||
>
|
||||
<div className={`${ARTICLE_COLUMN_CLASS} -mt-[var(--spacing-scale-048)]`}>
|
||||
<div
|
||||
className="flex flex-wrap items-end gap-[var(--measures-spacing-008,8px)] font-inter font-normal text-[10px] leading-[14px] text-[var(--color-content-default-secondary)] md:text-[12px] md:leading-[16px] lg:text-[14px] lg:leading-[20px] xl:text-[18px] xl:leading-[130%]"
|
||||
data-name="Metadata Container"
|
||||
>
|
||||
<span>{page.banner.author}</span>
|
||||
<span>{page.updated}</span>
|
||||
</div>
|
||||
|
||||
{page.intro.length > 0 ? (
|
||||
<div className="flex flex-col gap-[1em]">
|
||||
{page.intro.map((paragraph) => (
|
||||
<p key={paragraph}>
|
||||
<LegalInlineText text={paragraph} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{page.sections.map((section) => (
|
||||
<section
|
||||
key={section.heading}
|
||||
className="flex w-full flex-col gap-[var(--space-200,8px)]"
|
||||
data-name="TextBlock"
|
||||
>
|
||||
<h2 className="w-full font-bold">{section.heading}</h2>
|
||||
<div className="flex flex-col gap-[1em]">
|
||||
{section.paragraphs.map((paragraph) => (
|
||||
<p key={paragraph}>
|
||||
<LegalInlineText text={paragraph} />
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Figma: "Content page Template" (19003:23305)
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
|
||||
*/
|
||||
import type { Metadata } from "next";
|
||||
import messages from "../../../messages/en/index";
|
||||
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
|
||||
import LegalDocumentPage, {
|
||||
legalPageMetadata,
|
||||
} from "../_components/LegalDocumentPage";
|
||||
|
||||
const SLUG = "cookies" as const;
|
||||
const PATH = legalPathForSlug(SLUG);
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return legalPageMetadata(messages.metadata.cookies, messages.pages.cookies);
|
||||
}
|
||||
|
||||
export default function CookiesPage() {
|
||||
return (
|
||||
<LegalDocumentPage
|
||||
slug={SLUG}
|
||||
path={PATH}
|
||||
page={messages.pages.cookies}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Figma: "Content page Template" (19003:23305)
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
|
||||
*/
|
||||
import type { Metadata } from "next";
|
||||
import messages from "../../../messages/en/index";
|
||||
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
|
||||
import LegalDocumentPage, {
|
||||
legalPageMetadata,
|
||||
} from "../_components/LegalDocumentPage";
|
||||
|
||||
const SLUG = "privacy" as const;
|
||||
const PATH = legalPathForSlug(SLUG);
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return legalPageMetadata(messages.metadata.privacy, messages.pages.privacy);
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<LegalDocumentPage
|
||||
slug={SLUG}
|
||||
path={PATH}
|
||||
page={messages.pages.privacy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Figma: "Content page Template" (19003:23305)
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=19003-23305
|
||||
*/
|
||||
import type { Metadata } from "next";
|
||||
import messages from "../../../messages/en/index";
|
||||
import { legalPathForSlug } from "../../../lib/legalSyntheticPost";
|
||||
import LegalDocumentPage, {
|
||||
legalPageMetadata,
|
||||
} from "../_components/LegalDocumentPage";
|
||||
|
||||
const SLUG = "terms" as const;
|
||||
const PATH = legalPathForSlug(SLUG);
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return legalPageMetadata(messages.metadata.terms, messages.pages.terms);
|
||||
}
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<LegalDocumentPage
|
||||
slug={SLUG}
|
||||
path={PATH}
|
||||
page={messages.pages.terms}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user