Activity Guide
From 583231af37a9837d90ebab006d71913cea664c23 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=
+
+ Submit an activity guide ]+src="([^"]+)"/);
+ assert.ok(pageCover, 'the zine page should render a cover image');
+ assert.ok(existsSync(emittedPath(pageCover[1])), 'the zine cover should be emitted');
+
+ const library = readFileSync(join(DIST, 'organize/activity-guides/library/index.html'), 'utf8');
+ assert.match(library, new RegExp(`href="/activity-guide/${SLUG}/"`));
+ assert.match(library, /A compact guide to making patterns with repeated shapes\./);
+ assert.doesNotMatch(library, /Loops<\/strong>\s*Guide wanted<\/span>/);
+ const grid = library.match(/
([\s\S]*?)<\/ul>/);
+ assert.ok(grid, 'the library should render its grid');
+ assert.equal((grid[1].match(/
]+src="([^"]+)"/);
+ assert.ok(gridCover, 'the zine card should render a cover image');
+ assert.ok(existsSync(emittedPath(gridCover[1])), 'the card cover should be emitted');
+ } finally {
+ if (created) rmSync(DEST, { recursive: true, force: true });
+ }
+});
diff --git a/.github/scripts/zines.test.mjs b/.github/scripts/zines.test.mjs
new file mode 100644
index 00000000..e5dcf79b
--- /dev/null
+++ b/.github/scripts/zines.test.mjs
@@ -0,0 +1,76 @@
+import { describe, test } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ assertIdentity, assertUniqueIds, assertUniqueTopics, parseZineMetadata,
+ resolveZineAssets, zineMetadataSchema,
+} from '../../pcd-website/src/lib/zine-metadata.js';
+
+const valid = () => ({
+ id: 'loops-with-shapes', title: 'Loops with Shapes', topic: 'Loops',
+ created_by: 'Guide Author', summary: 'Make patterns with repeated shapes.',
+ cover: 'cover.png', pdfs: [{ file: 'guide.pdf', label: 'Read on screen' }],
+ license: 'CC BY-SA 4.0',
+});
+
+describe('zine metadata', () => {
+ test('accepts valid metadata', () => {
+ assert.deepEqual(parseZineMetadata(valid(), 'loops-with-shapes'), valid());
+ });
+
+ test('rejects non-object metadata cleanly', () => {
+ for (const input of [null, 'metadata', []]) {
+ assert.throws(() => parseZineMetadata(input, 'loops-with-shapes'), /Invalid metadata/);
+ }
+ });
+
+ test('validates required values, topics, ids, and strict object keys', () => {
+ const cases = [
+ [{ ...valid(), topic: 'Physics' }, /topic/],
+ [{ ...valid(), title: ' ' }, /title/],
+ [{ ...valid(), id: 'Not Kebab' }, /id/],
+ [{ ...valid(), unexpected: true }, /Unrecognized key/],
+ [{ ...valid(), cover: undefined }, /cover/],
+ [{ ...valid(), cover: 'cover.PNG' }, /cover/],
+ [{ ...valid(), pdfs: [] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.PDF', label: 'PDF' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.txt', label: 'Text' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.pdf' }] }, /pdfs/],
+ [{ ...valid(), pdfs: [{ file: 'guide.pdf', label: 'PDF', extra: true }] }, /Unrecognized key/],
+ [{ ...valid(), license: 'CC BY 4.0' }, /license/],
+ ];
+ for (const [input, message] of cases) {
+ assert.throws(() => parseZineMetadata(input, 'loops-with-shapes'), message);
+ }
+ });
+
+ test('rejects draft zines with the safe unpublished location', () => {
+ assert.throws(() => parseZineMetadata({ ...valid(), draft: true }, 'loops-with-shapes'), /zines-drafts/);
+ });
+
+ test('guards source URLs without allowing malformed values to escape', () => {
+ for (const source_url of ['https://example.com', 'http://example.com/path']) {
+ assert.equal(zineMetadataSchema.safeParse({ ...valid(), source_url }).success, true);
+ }
+ for (const source_url of ['javascript:alert(1)', 'data:text/html,test', 'not a url']) {
+ assert.doesNotThrow(() => zineMetadataSchema.safeParse({ ...valid(), source_url }));
+ assert.equal(zineMetadataSchema.safeParse({ ...valid(), source_url }).success, false);
+ }
+ });
+
+ test('checks asset existence', () => {
+ assert.deepEqual(resolveZineAssets('loops-with-shapes', valid(), ['cover.png', 'guide.pdf']), {
+ cover: 'cover.png', pdfs: ['guide.pdf'],
+ });
+ assert.throws(() => resolveZineAssets('loops-with-shapes', valid(), ['cover.png']), /guide.pdf/);
+ assert.throws(() => resolveZineAssets('loops-with-shapes', valid(), ['guide.pdf']), /cover.png/);
+ });
+
+ test('checks unique topics, unique ids, and three-way identity', () => {
+ const first = valid();
+ const sameTopic = { ...valid(), id: 'other-loops' };
+ assert.throws(() => assertUniqueTopics([first, sameTopic]), /both claim/);
+ assert.throws(() => assertUniqueIds([first, { ...valid() }]), /Duplicate zine id/);
+ assert.throws(() => assertIdentity({ slug: 'loops-with-shapes', frontmatterId: 'loops', metadataId: 'loops-with-shapes' }), /identity mismatch/);
+ assert.doesNotThrow(() => assertIdentity({ slug: 'loops-with-shapes', frontmatterId: 'loops-with-shapes', metadataId: 'loops-with-shapes' }));
+ });
+});
diff --git a/AGENTS.md b/AGENTS.md
index 2fc07191..49305833 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -34,12 +34,14 @@ node --test .github/scripts/event-issue-helpers.test.mjs
node --test .github/scripts/process-new-event-issue.test.mjs
node --test .github/scripts/process-edit-event-issue.test.mjs
node --test .github/scripts/plus-code.test.mjs
+node --test .github/scripts/zines.test.mjs
+node --test .github/scripts/zine-build.test.mjs
# Requires npm run build from pcd-website/ first:
node --test .github/scripts/data-json.test.mjs
```
-Need to run the tests end-to-end? `./scripts/run-tests.sh` executes the helper, intake, and plus-code suites, builds the Astro site via `npm --prefix pcd-website run build`, and then runs `data-json.test.mjs` in sequence. Run this script from the repo root after installing dependencies so you get the full battery of checks in one shot.
+Need to run the tests end-to-end? `./scripts/run-tests.sh` executes the helper, intake, plus-code, and zine metadata suites; runs the zine fixture build; builds the Astro site via `npm --prefix pcd-website run build`; and then runs `data-json.test.mjs` in sequence. Run this script from the repo root after installing dependencies so you get the full battery of checks in one shot.
No install needed — `open-location-code` is already available at `pcd-website/node_modules/`.
@@ -66,6 +68,10 @@ Event data lives in `src/content/events/
+ {ZINE_TOPICS.map((topic) => {
+ const zine = zinesByTopic.get(topic);
+ return zine ? (
+
diff --git a/pcd-website/src/components/ActivityGuideSubmitButton.astro b/pcd-website/src/components/ActivityGuideSubmitButton.astro
new file mode 100644
index 00000000..51e9454f
--- /dev/null
+++ b/pcd-website/src/components/ActivityGuideSubmitButton.astro
@@ -0,0 +1,10 @@
+---
+import { ACTIVITY_GUIDE_SUBMIT_URL } from '../config';
+import ExternalLinkIcon from './ExternalLinkIcon.astro';
+---
+
+
Back to the Activity Guide Library
@@ -61,6 +65,19 @@ const { Content } = await render(zine.entry); border-radius: var(--border-radius); } + .activity-guide__cover--placeholder { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 4 / 3; + padding: var(--spacing-lg); + color: var(--color-text-subtle); + font-size: clamp(1.5rem, 5vw, 3rem); + font-weight: 600; + text-align: center; + background: var(--color-border); + } + .activity-guide__metadata { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); diff --git a/pcd-website/src/styles/docs/components.css b/pcd-website/src/styles/docs/components.css index 524d801a..26159b49 100644 --- a/pcd-website/src/styles/docs/components.css +++ b/pcd-website/src/styles/docs/components.css @@ -264,8 +264,7 @@ color: var(--color-text-subtle); border: 1px dashed var(--color-border); } -.guide-card--empty span, -.guide-card__topic { +.guide-card--empty span { font-size: var(--step-2); font-weight: 600; text-transform: uppercase; @@ -290,6 +289,17 @@ aspect-ratio: 4 / 3; object-fit: cover; } +.guide-card__cover-placeholder { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + aspect-ratio: 4 / 3; + padding: var(--sp-4); + color: var(--color-text-subtle); + font-weight: 600; + background: var(--color-border); +} .guide-card__body { display: flex; flex: 1; From 1147cfa5290d8899548ebf790b5b3cb1960bb814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20de=20Courville?=Activity Guide