when-am-i / files / scripts / screenshot-readme.mts
screenshot-readme.mts
76ecf55e 2026-06-21 Pavel Safronov | 1 | // Generate the two screenshots embedded in the README. |
| 2 | // | |
| 3 | // Unlike generate-screenshots.mts (full-page app-state captures for internal | |
| 4 | // review) these are deliberately small and cropped — a README hero image | |
| 5 | // should read at a glance, not reproduce the whole page. The landing shot is | |
| 6 | // the full viewport (narrow, so the header/intro/map fit together); the | |
| 7 | // example shot clips to just the map+answers column (`.app-cols`), skipping | |
| 8 | // the header and intro text since by then the reader already has context. | |
| 9 | // | |
| 10 | // npm run screenshot:readme | |
| 11 | // | |
| 12 | // Output: screenshots/readme/*.png — committed, embedded via README.md. | |
| 13 | ||
| 14 | import { chromium } from 'playwright'; | |
| 15 | import { mkdir, rm } from 'node:fs/promises'; | |
| 16 | import path from 'node:path'; | |
| 17 | import { startDevServer, BASE_URL, REPO_ROOT } from './lib/server.mts'; | |
| 18 | ||
| 19 | const OUT_DIR = path.join(REPO_ROOT, 'screenshots', 'readme'); | |
| 20 | const VIEWPORT = { width: 1000, height: 700 }; | |
| 21 | ||
| 22 | async function run() { | |
| 23 | console.log('\n📸 Generating README screenshots…\n'); | |
| 24 | const server = await startDevServer(); | |
| 25 | try { | |
| 26 | await rm(OUT_DIR, { recursive: true, force: true }); | |
| 27 | await mkdir(OUT_DIR, { recursive: true }); | |
| 28 | ||
| 29 | const browser = await chromium.launch(); | |
| 30 | const page = await browser.newPage({ viewport: VIEWPORT, deviceScaleFactor: 2 }); | |
| 31 | ||
| 32 | // 1. Landing: no pin yet, today's date — the first thing a visitor sees. | |
| 33 | await page.goto(BASE_URL, { waitUntil: 'networkidle' }); | |
| 34 | await page.waitForTimeout(500); | |
| 35 | await page.screenshot({ path: path.join(OUT_DIR, '01-landing.png') }); | |
| 36 | console.log(' ✅ 01-landing.png'); | |
| 37 | ||
| 38 | // 2. Example: Boston, 1650 — two coexisting calendars (Massachusett + | |
| 39 | // colonial Julian), cropped to the map+answers column only. | |
| 40 | await page.goto(`${BASE_URL}?lat=42.32&lng=-71.06&zoom=10&year=1650&month=6&day=15`, { | |
| 41 | waitUntil: 'networkidle', | |
| 42 | }); | |
| 43 | await page.waitForTimeout(800); // let map tiles settle | |
| 44 | await page.locator('.app-cols').screenshot({ path: path.join(OUT_DIR, '02-example.png') }); | |
| 45 | console.log(' ✅ 02-example.png'); | |
| 46 | ||
| 47 | await browser.close(); | |
| 48 | console.log('\n✨ Done — screenshots/readme/\n'); | |
| 49 | } finally { | |
| 50 | server.stop(); | |
| 51 | } | |
| 52 | } | |
| 53 | ||
| 54 | run().catch((err) => { | |
| 55 | console.error(err); | |
| 56 | process.exit(1); | |
| 57 | }); |