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)
|
# Per-article body ornaments (Figma Content page Template image fills)
|
||||||
!public/content/blog/*-ornament-*.png
|
!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)
|
# Visual regression snapshots (allow these)
|
||||||
!tests/e2e/visual-regression.spec.ts-snapshots/
|
!tests/e2e/visual-regression.spec.ts-snapshots/
|
||||||
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
|
!tests/e2e/visual-regression.spec.ts-snapshots/*.png
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import messages from "../../../messages/en/index";
|
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 { getTranslation } from "../../../lib/i18n/getTranslation";
|
||||||
import AboutHeader from "../../components/type/AboutHeader";
|
import AboutHeader from "../../components/type/AboutHeader";
|
||||||
import type { AboutHeaderSegment } from "../../components/type/AboutHeader";
|
import type { AboutHeaderSegment } from "../../components/type/AboutHeader";
|
||||||
@@ -53,11 +58,20 @@ export default function AboutPage() {
|
|||||||
/>
|
/>
|
||||||
<TripleTextBlock columns={tripleColumns} />
|
<TripleTextBlock columns={tripleColumns} />
|
||||||
<Book
|
<Book
|
||||||
title={page.book.title}
|
title={page.structureBeforeCrisis.title}
|
||||||
description={page.book.description}
|
description={page.structureBeforeCrisis.description}
|
||||||
buttonText={page.book.buttonText}
|
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())}
|
buttonHref={getAssetPath(governanceBookletPath())}
|
||||||
imageAlt={page.book.imageAlt}
|
imageSrc={getAssetPath(ASSETS.COMMUNITY_RULES_COVER)}
|
||||||
|
imageAlt={page.communityRules.imageAlt}
|
||||||
/>
|
/>
|
||||||
<FaqAccordion title={page.faq.title} items={faqItems} />
|
<FaqAccordion title={page.faq.title} items={faqItems} />
|
||||||
<QuoteBlock
|
<QuoteBlock
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ function BookView({
|
|||||||
headingId,
|
headingId,
|
||||||
className = "",
|
className = "",
|
||||||
}: BookViewProps) {
|
}: BookViewProps) {
|
||||||
const coverSrc = imageSrc ?? getAssetPath(ASSETS.COMMUNITYRULES_COVER);
|
const coverSrc =
|
||||||
|
imageSrc ?? getAssetPath(ASSETS.STRUCTURE_BEFORE_CRISIS_COVER);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ stage. Raster → SVG conversion is tracked in
|
|||||||
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
|
| `marketing/section-number-*.svg` (×3) | SectionNumber | **Done** — SVG |
|
||||||
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG |
|
| `marketing/avatar-*.svg` (×3) | Avatar / ASSETS | **Done** — SVG |
|
||||||
| `marketing/hero-image.png` | HeroBanner | **Design review** — likely keep raster |
|
| `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 |
|
| `logos/community-rule.svg` | Logo + favicon (`ASSETS.LOGO`) | **Done** — SVG |
|
||||||
| `share/*.svg` (×5) | Share modal | **Done** — SVG (`shareIconPath()`) |
|
| `share/*.svg` (×5) | Share modal | **Done** — SVG (`shareIconPath()`) |
|
||||||
| `logos/gitlab.svg` | Footer / social | **Done** — SVG |
|
| `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`;
|
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 {
|
export function governanceBookletPath(): string {
|
||||||
return "assets/marketing/governance-booklet.pdf";
|
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/`. */
|
/** Home feature grid panel art in `public/assets/marketing/`. */
|
||||||
export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools";
|
export type FeaturePanelKey = "support" | "exercises" | "guidance" | "tools";
|
||||||
|
|
||||||
@@ -245,8 +250,11 @@ export const ASSETS = {
|
|||||||
/** Quote block default avatar. */
|
/** Quote block default avatar. */
|
||||||
QUOTE_AVATAR: "assets/marketing/quote-avatar.svg",
|
QUOTE_AVATAR: "assets/marketing/quote-avatar.svg",
|
||||||
|
|
||||||
/** Sections / Book cover (Figma **22137:891197**). */
|
/** Structure Before Crisis cover (Figma **22137:891197**; file name is historical). */
|
||||||
COMMUNITYRULES_COVER: "assets/marketing/communityrules-cover.svg",
|
STRUCTURE_BEFORE_CRISIS_COVER: "assets/marketing/communityrules-cover.svg",
|
||||||
|
|
||||||
|
/** Community Rules (2021) cover. */
|
||||||
|
COMMUNITY_RULES_COVER: "assets/marketing/community-rules-cover.png",
|
||||||
|
|
||||||
// Marketing
|
// Marketing
|
||||||
HERO_IMAGE: "assets/marketing/hero-image.png",
|
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": {
|
"aboutHeader": {
|
||||||
"segments": [
|
"segments": [
|
||||||
{ "type": "word", "text": "CommunityRule" },
|
{ "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",
|
"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.",
|
"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",
|
"buttonText": "Download Book",
|
||||||
"imageAlt": "Structure Before Crisis book cover"
|
"imageAlt": "Community Rules book cover"
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"title": "Get answers to your questions",
|
"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">
|
<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">
|
||||||
<g filter="url(#filter0_d_22851_36281)">
|
|
||||||
<rect x="24" y="24" width="208" height="321.707" rx="4" fill="url(#pattern0_22851_36281)" shape-rendering="crispEdges"/>
|
<rect x="24" y="24" width="208" height="321.707" rx="4" fill="url(#pattern0_22851_36281)" shape-rendering="crispEdges"/>
|
||||||
</g>
|
|
||||||
<defs>
|
<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">
|
<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)"/>
|
<use xlink:href="#image0_22851_36281" transform="matrix(0.00252723 0 0 0.00163399 -0.000392154 0)"/>
|
||||||
</pattern>
|
</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 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";
|
import messages from "../../messages/en/pages/about.json";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -11,12 +16,27 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const structureBeforeCrisis = messages.structureBeforeCrisis;
|
||||||
|
const communityRules = messages.communityRules;
|
||||||
|
|
||||||
export const Default = {
|
export const Default = {
|
||||||
args: {
|
args: {
|
||||||
title: messages.book.title,
|
title: structureBeforeCrisis.title,
|
||||||
description: messages.book.description,
|
description: structureBeforeCrisis.description,
|
||||||
buttonText: messages.book.buttonText,
|
buttonText: structureBeforeCrisis.buttonText,
|
||||||
buttonHref: getAssetPath(governanceBookletPath()),
|
buttonHref: getAssetPath(structureBeforeCrisisPath()),
|
||||||
imageAlt: messages.book.imageAlt,
|
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 { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { getAssetPath, governanceBookletPath } from "../../../lib/assetUtils";
|
import {
|
||||||
|
governanceBookletPath,
|
||||||
|
structureBeforeCrisisPath,
|
||||||
|
} from "../../../lib/assetUtils";
|
||||||
|
|
||||||
describe("governance booklet", () => {
|
const publicRoot = join(process.cwd(), "public");
|
||||||
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 abs = join(process.cwd(), "public", relative);
|
function assertPdf(relativePath: string) {
|
||||||
expect(existsSync(abs)).toBe(true);
|
const bytes = readFileSync(join(publicRoot, relativePath));
|
||||||
|
expect(bytes.subarray(0, 5).toString("ascii")).toBe("%PDF-");
|
||||||
|
}
|
||||||
|
|
||||||
const header = readFileSync(abs).subarray(0, 5).toString("latin1");
|
describe("marketing book PDFs", () => {
|
||||||
expect(header).toBe("%PDF-");
|
it("ships both About book downloads as PDFs", () => {
|
||||||
|
assertPdf(governanceBookletPath());
|
||||||
|
assertPdf(structureBeforeCrisisPath());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user