Create use cases pages

This commit is contained in:
adilallo
2026-05-18 16:50:44 -06:00
parent 40ce5064d6
commit 7c46cbd87b
28 changed files with 836 additions and 58 deletions
+149
View File
@@ -0,0 +1,149 @@
/**
* Figma: use case detail (22015:42619)
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22015-42619
*/
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import messages from "../../../../messages/en/index";
import {
buildUseCaseSyntheticPost,
getUseCaseDetailEntry,
isUseCaseDetailSlug,
USE_CASE_DETAIL_SLUGS,
useCaseContentKeyForSlug,
} from "../../../../lib/useCaseSyntheticPost";
import ContentBanner from "../../../components/sections/ContentBanner";
import AskOrganizer from "../../../components/sections/AskOrganizer";
import type { AskOrganizerVariant } from "../../../components/sections/AskOrganizer/AskOrganizer.types";
import "../use-cases.css";
type PageProps = {
params: Promise<{ slug: string }>;
};
export function generateStaticParams() {
return USE_CASE_DETAIL_SLUGS.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
if (!isUseCaseDetailSlug(slug)) {
return {};
}
const contentKey = useCaseContentKeyForSlug(slug);
const meta = messages.metadata.useCasesDetail[contentKey];
return {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
openGraph: {
title: meta.title,
description: meta.description,
type: "website",
siteName: "CommunityRule",
},
};
}
export default async function UseCaseDetailPage({ params }: PageProps) {
const { slug } = await params;
if (!isUseCaseDetailSlug(slug)) {
notFound();
}
const detail = messages.pages.useCasesDetail;
const entry = getUseCaseDetailEntry(slug, detail);
const syntheticPost = buildUseCaseSyntheticPost(slug, detail);
const { ruleCard, askOrganizer } = entry;
const askVariant = (askOrganizer.variant ?? "use-case-detail") as AskOrganizerVariant;
const structuredData = {
"@context": "https://schema.org",
"@type": "WebPage",
name: entry.banner.title,
description: entry.banner.description,
url: `https://communityrule.com/use-cases/${slug}`,
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: "Use cases",
item: "https://communityrule.com/use-cases",
},
{
"@type": "ListItem",
position: 3,
name: entry.banner.title,
item: `https://communityrule.com/use-cases/${slug}`,
},
],
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(structuredData),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(breadcrumbData),
}}
/>
<div
className="min-h-screen"
style={{ background: entry.pageBackground }}
>
<ContentBanner
post={syntheticPost}
variant="useCase"
rulePreview={{
title: ruleCard.title,
description: ruleCard.description,
backgroundColor: ruleCard.backgroundColor,
iconPath: ruleCard.iconPath,
}}
/>
<article
data-figma-node="22015:42622"
className="flex w-full items-center justify-center self-stretch px-[var(--spacing-scale-024)] py-[var(--spacing-scale-032)] sm:px-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-048)] lg:px-[var(--spacing-scale-064)] xl:px-[256px]"
>
<div
className="use-case-body"
dangerouslySetInnerHTML={{ __html: syntheticPost.htmlContent }}
/>
</article>
<AskOrganizer
title={askOrganizer.title}
subtitle={askOrganizer.subtitle}
buttonText={askOrganizer.buttonText}
variant={askVariant}
/>
</div>
</>
);
}
+42 -12
View File
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import dynamic from "next/dynamic";
import Link from "next/link";
import { Suspense } from "react";
import messages from "../../../messages/en/index";
import { getAllBlogPosts } from "../../../lib/content";
@@ -30,6 +31,16 @@ function asArray<T>(value: unknown): T[] {
return Array.isArray(value) ? value : [];
}
const CASE_STUDY_TILE_RADIUS_CLASS = "rounded-[23.093px]";
const CASE_STUDY_LINK_CLASS = [
CASE_STUDY_TILE_RADIUS_CLASS,
"block shrink-0 cursor-pointer outline-none transition-transform duration-200",
"hover:scale-[1.02] hover:opacity-95",
"focus-visible:ring-2 focus-visible:ring-[var(--color-border-default-brand-primary)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-surface-default-primary)]",
"active:scale-[0.98]",
].join(" ");
/** Matches `pages.useCases.groups.items` order ↔ `public/assets/vector/*.svg`. */
const USE_CASES_GROUP_VECTOR_SLUGS = [
"worker-coop",
@@ -99,6 +110,16 @@ export default function UseCasesPage() {
page.tripleStep.steps,
);
const caseStudyLinks = asArray<{ href: string; ariaLabel: string }>(
page.caseStudyTiles.links,
);
const caseStudySurfaces = ["lavender", "neutral", "rose"] as const;
const caseStudyAlts = [
page.caseStudyTiles.mutualAidColoradoAlt,
page.caseStudyTiles.foodNotBombsAlt,
page.caseStudyTiles.boulderCountyStreetMedicsAlt,
];
return (
<div className="min-h-screen bg-[var(--color-surface-default-primary)]">
<PageHeader
@@ -109,18 +130,27 @@ export default function UseCasesPage() {
/>
<UseCasesOrgs>
<CaseStudy
surface="lavender"
imageAlt={page.caseStudyTiles.mutualAidColoradoAlt}
/>
<CaseStudy
surface="neutral"
imageAlt={page.caseStudyTiles.foodNotBombsAlt}
/>
<CaseStudy
surface="rose"
imageAlt={page.caseStudyTiles.boulderCountyStreetMedicsAlt}
/>
{caseStudySurfaces.map((surface, index) => {
const link = caseStudyLinks[index];
const card = (
<CaseStudy surface={surface} imageAlt={caseStudyAlts[index]} />
);
if (!link?.href) {
return <div key={surface}>{card}</div>;
}
return (
<Link
key={surface}
href={link.href}
aria-label={link.ariaLabel}
className={CASE_STUDY_LINK_CLASS}
>
{card}
</Link>
);
})}
</UseCasesOrgs>
<QuoteBlock
+35
View File
@@ -0,0 +1,35 @@
/**
* Figma: use case detail body text (22015:42622 / 22015:42623)
* X Large/Paragraph — Inter 24px / 32px, inverse primary on brand surface.
*/
.use-case-body {
width: 100%;
max-width: 700px;
font-family: var(--font-inter, Inter, sans-serif);
font-size: 18px;
font-weight: 400;
line-height: 130%;
color: var(--color-content-inverse-primary);
word-break: break-word;
}
@media (min-width: 768px) {
.use-case-body {
font-size: 24px;
line-height: 32px;
}
}
.use-case-body p {
margin-block: 0;
}
.use-case-body p + p {
margin-block-start: 1em;
}
@media (min-width: 768px) {
.use-case-body p + p {
margin-block-start: 32px;
}
}