QA pass: layout, create-flow, and About books #68

Merged
an.di merged 38 commits from adilallo/fix/CR-129-create-rule-button into main 2026-08-26 15:46:56 +00:00
13 changed files with 155 additions and 43 deletions
Showing only changes of commit 63e0f77998 - Show all commits
+3
View File
@@ -47,6 +47,9 @@ npm-cache/
# Per-article body ornaments (Figma Content page Template image fills)
!public/content/blog/*-ornament-*.png
# Marketing book cover (raster; see docs/guides/static-assets.md)
!public/assets/marketing/community-rules-cover.png
# Visual regression snapshots (allow these)
!tests/e2e/visual-regression.spec.ts-snapshots/
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
+19 -5
View File
@@ -1,5 +1,10 @@
import messages from "../../../messages/en/index";
import { getAssetPath, governanceBookletPath } from "../../../lib/assetUtils";
import {
ASSETS,
getAssetPath,
governanceBookletPath,
structureBeforeCrisisPath,
} from "../../../lib/assetUtils";
import { getTranslation } from "../../../lib/i18n/getTranslation";
import AboutHeader from "../../components/type/AboutHeader";
import type { AboutHeaderSegment } from "../../components/type/AboutHeader";
@@ -53,11 +58,20 @@ export default function AboutPage() {
/>
<TripleTextBlock columns={tripleColumns} />
<Book
title={page.book.title}
description={page.book.description}
buttonText={page.book.buttonText}
title={page.structureBeforeCrisis.title}
description={page.structureBeforeCrisis.description}
buttonText={page.structureBeforeCrisis.buttonText}
buttonHref={getAssetPath(structureBeforeCrisisPath())}
imageSrc={getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER)}
imageAlt={page.structureBeforeCrisis.imageAlt}
/>
<Book
title={page.communityRules.title}
description={page.communityRules.description}
buttonText={page.communityRules.buttonText}
buttonHref={getAssetPath(governanceBookletPath())}
imageAlt={page.book.imageAlt}
imageSrc={getAssetPath(ASSETS.COMMUNITY_RULES_COVER)}
imageAlt={page.communityRules.imageAlt}
/>
<FaqAccordion title={page.faq.title} items={faqItems} />
<QuoteBlock
+2 -1
View File
@@ -19,7 +19,8 @@ function BookView({
headingId,
className = "",
}: BookViewProps) {
const coverSrc = imageSrc ?? getAssetPath(ASSETS.COMMUNITYRULES_COVER);
const coverSrc =
imageSrc ?? getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER);
return (
<section
+4 -1
View File
@@ -65,7 +65,10 @@ stage. Raster → SVG conversion is tracked in
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG |
| `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster |
| `marketing/governance-booklet.pdf` | About / Book | **Done**PDF (`governanceBookletPath()`) |
| `marketing/communityrules-cover.svg` | About / Book | **Done**Structure Before Crisis cover (`ASSETS.STRUCTURE_BEFORE_CRISIS_COVER`; Figma **22137:891197**) |
| `marketing/community-rules-cover.png` | About / Book | **Done** — Community Rules (2021) cover (`ASSETS.COMMUNITY_RULES_COVER`; gitignore exception) |
| `marketing/governance-booklet.pdf` | About / Book | **Done** — Community Rules (2021) PDF (`governanceBookletPath()`) |
| `marketing/structure-before-crisis.pdf` | About / Book | **Done** — Structure Before Crisis (2025) PDF (`structureBeforeCrisisPath()`) |
| `logos/community-rule.svg` | Logo + favicon (`ASSETS.LOGO`) | **Done** — SVG |
| `share/*.svg` (×5) | Share modal | **Done** — SVG (`shareIconPath()`) |
| `logos/gitlab.svg` | Footer / social | **Done** — SVG |
+11 -3
View File
@@ -97,11 +97,16 @@ export function sectionNumberPath(n: 1 | 2 | 3): string {
return `assets/marketing/section-number-${n}.svg`;
}
/** Downloadable governance booklet PDF (About / Sections / Book). */
/** Community Rules (2021) booklet PDF (About / Sections / Book). */
export function governanceBookletPath(): string {
return "assets/marketing/governance-booklet.pdf";
}
/** Structure Before Crisis (2025) PDF (About / Sections / Book). */
export function structureBeforeCrisisPath(): string {
return "assets/marketing/structure-before-crisis.pdf";
}
/** Home feature grid panel art in `public/assets/marketing/`. */
export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools";
@@ -245,8 +250,11 @@ export const ASSETS = {
/** Quote block default avatar. */
QUOTE_AVATAR: "assets/marketing/quote-avatar.svg",
/** Sections / Book cover (Figma **22137:891197**). */
COMMUNITYRULES_COVER: "assets/marketing/communityrules-cover.svg",
/** Structure Before Crisis cover (Figma **22137:891197**; file name is historical). */
STRUCTURE_BEFORE_CRISIS_COVER: "assets/marketing/communityrules-cover.svg",
/** Community Rules (2021) cover. */
COMMUNITY_RULES_COVER: "assets/marketing/community-rules-cover.png",
// Marketing
HERO_IMAGE: "assets/marketing/hero-image.png",
+9 -3
View File
@@ -1,5 +1,5 @@
{
"_comment": "About page content. Book download href is wired in app/(marketing)/about/page.tsx via governanceBookletPath().",
"_comment": "About page content. Book download hrefs are wired in app/(marketing)/about/page.tsx (structureBeforeCrisisPath / governanceBookletPath).",
"aboutHeader": {
"segments": [
{ "type": "word", "text": "CommunityRule" },
@@ -69,11 +69,17 @@
}
]
},
"book": {
"structureBeforeCrisis": {
"title": "Get Structure Before Crisis",
"description": "Structure Before Crisis is a collection of organizing patterns that help mutual aid groups lay the groundwork to build new worlds.",
"buttonText": "Download Book",
"imageAlt": "Structure Before Crisis book cover"
},
"communityRules": {
"title": "Get the Community Rules Book",
"description": "Community Rules is a simple tool to help make great communities even better and healthier. It includes nine templates for organizational structures that communities can choose from, combine, or react against.",
"buttonText": "Download Book",
"imageAlt": "Structure Before Crisis book cover"
"imageAlt": "Community Rules book cover"
},
"faq": {
"title": "Get answers to your questions",
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

@@ -1,18 +1,6 @@
<svg width="256" height="370" viewBox="0 0 256 370" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g filter="url(#filter0_d_22851_36281)">
<svg width="208" height="322" viewBox="24 24 208 321.707" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="24" y="24" width="208" height="321.707" rx="4" fill="url(#pattern0_22851_36281)" shape-rendering="crispEdges"/>
</g>
<defs>
<filter id="filter0_d_22851_36281" x="0" y="0" width="256" height="369.707" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="12"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_22851_36281"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_22851_36281" result="shape"/>
</filter>
<pattern id="pattern0_22851_36281" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_22851_36281" transform="matrix(0.00252723 0 0 0.00163399 -0.000392154 0)"/>
</pattern>

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.
Binary file not shown.
+26 -6
View File
@@ -1,5 +1,10 @@
import Book from "../../app/components/sections/Book";
import { getAssetPath, governanceBookletPath } from "../../lib/assetUtils";
import {
ASSETS,
getAssetPath,
governanceBookletPath,
structureBeforeCrisisPath,
} from "../../lib/assetUtils";
import messages from "../../messages/en/pages/about.json";
export default {
@@ -11,12 +16,27 @@ export default {
},
};
const structureBeforeCrisis = messages.structureBeforeCrisis;
const communityRules = messages.communityRules;
export const Default = {
args: {
title: messages.book.title,
description: messages.book.description,
buttonText: messages.book.buttonText,
buttonHref: getAssetPath(governanceBookletPath()),
imageAlt: messages.book.imageAlt,
title: structureBeforeCrisis.title,
description: structureBeforeCrisis.description,
buttonText: structureBeforeCrisis.buttonText,
buttonHref: getAssetPath(structureBeforeCrisisPath()),
imageSrc: getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER),
imageAlt: structureBeforeCrisis.imageAlt,
},
};
export const CommunityRules = {
args: {
title: communityRules.title,
description: communityRules.description,
buttonText: communityRules.buttonText,
buttonHref: getAssetPath(governanceBookletPath()),
imageSrc: getAssetPath(ASSETS.COMMUNITY_RULES_COVER),
imageAlt: communityRules.imageAlt,
},
};
+66
View File
@@ -0,0 +1,66 @@
import { afterEach, describe, expect, test } from "vitest";
import { within } from "@testing-library/react";
import {
cleanup,
renderWithProviders as render,
screen,
} from "../utils/test-utils";
import AboutPage from "../../app/(marketing)/about/page";
import {
ASSETS,
getAssetPath,
governanceBookletPath,
structureBeforeCrisisPath,
} from "../../lib/assetUtils";
import messages from "../../messages/en/index";
afterEach(() => {
cleanup();
});
function bookSection(title: string) {
const heading = screen.getByRole("heading", { name: title });
const section = heading.closest("section");
expect(section).not.toBeNull();
return within(section as HTMLElement);
}
describe("AboutPage", () => {
test("pairs each book cover, heading, alt, and download with the same title", () => {
render(<AboutPage />);
const page = messages.pages.about;
const structureBeforeCrisis = bookSection(page.structureBeforeCrisis.title);
expect(
structureBeforeCrisis.getByText(page.structureBeforeCrisis.description),
).toBeInTheDocument();
expect(
structureBeforeCrisis.getByRole("img", {
name: page.structureBeforeCrisis.imageAlt,
}),
).toHaveAttribute(
"src",
getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER),
);
expect(
structureBeforeCrisis.getByRole("link", {
name: page.structureBeforeCrisis.buttonText,
}),
).toHaveAttribute("href", getAssetPath(structureBeforeCrisisPath()));
const communityRules = bookSection(page.communityRules.title);
expect(
communityRules.getByText(page.communityRules.description),
).toBeInTheDocument();
expect(
communityRules.getByRole("img", {
name: page.communityRules.imageAlt,
}),
).toHaveAttribute("src", getAssetPath(ASSETS.COMMUNITY_RULES_COVER));
expect(
communityRules.getByRole("link", {
name: page.communityRules.buttonText,
}),
).toHaveAttribute("href", getAssetPath(governanceBookletPath()));
});
});
+14 -11
View File
@@ -1,18 +1,21 @@
import { existsSync, readFileSync } from "node:fs";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { getAssetPath, governanceBookletPath } from "../../../lib/assetUtils";
import {
governanceBookletPath,
structureBeforeCrisisPath,
} from "../../../lib/assetUtils";
describe("governance booklet", () => {
it("ships a PDF at the About Book download path", () => {
const relative = governanceBookletPath();
expect(relative).toBe("assets/marketing/governance-booklet.pdf");
expect(getAssetPath(relative)).toBe("/assets/marketing/governance-booklet.pdf");
const publicRoot = join(process.cwd(), "public");
const abs = join(process.cwd(), "public", relative);
expect(existsSync(abs)).toBe(true);
function assertPdf(relativePath: string) {
const bytes = readFileSync(join(publicRoot, relativePath));
expect(bytes.subarray(0, 5).toString("ascii")).toBe("%PDF-");
}
const header = readFileSync(abs).subarray(0, 5).toString("latin1");
expect(header).toBe("%PDF-");
describe("marketing book PDFs", () => {
it("ships both About book downloads as PDFs", () => {
assertPdf(governanceBookletPath());
assertPdf(structureBeforeCrisisPath());
});
});