QA pass: layout, create-flow, and About books #68
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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()));
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user