/** * Seeds a local development Site: placeholder Pages that look enough like * printed menus to design against. Destructive: it wipes the data directory * under MENU_ROOT. * * npm run seed */ import { mkdir, readFile, rm } from 'node:fs/promises'; import path from 'node:path'; import sharp from 'sharp'; import { parseSiteConfig } from '../src/config.ts'; import { createImagePipeline } from '../src/images/pipeline.ts'; import { SetStore } from '../src/storage/sets.ts'; import { SectionStore } from '../src/storage/sections.ts'; const root = path.resolve(process.env['MENU_ROOT'] ?? '.dev'); const configPath = path.resolve(process.env['MENU_SITE_CONFIG'] ?? 'config/hopsngrains.site.json'); const dataDir = path.join(root, 'data'); const config = parseSiteConfig(JSON.parse(await readFile(configPath, 'utf8'))); function menuPage( title: string, section: string, page: number, total: number, seed: number, ): string { const items = Array.from({ length: 7 }, (_, index) => { const y = 420 + index * 130; const width = 180 + ((seed * 37 + index * 53) % 220); return ` ${ [ 'Wood-fired pizza', 'Smash burger', 'Tenderloin steak', 'Craft platter', 'Fish and chips', 'Risotto', 'Cheesecake', ][index % 7] } ${ [ 'thin crust, basil', 'double patty, cheddar', 'pepper sauce', 'for two', 'tartare', 'wild mushroom', 'berry coulis', ][index % 7] } ${ 240 + ((seed * 91 + index * 71) % 620) } `; }).join(''); return ` ${title} ${section} ยท page ${page} of ${total} ${items} Placeholder page for design review ${page} / ${total} `; } async function pageImage(svg: string): Promise { return sharp(Buffer.from(svg)).jpeg({ quality: 82 }).toBuffer(); } const plan: { section: string; label: string; validUntil: string | null; pages: number }[] = [ { section: 'food', label: 'Food Menu', validUntil: '2026-12-31', pages: 4 }, { section: 'bar', label: 'Bar Menu', validUntil: '2026-06-26', pages: 3 }, { section: 'wine', label: 'Wine List', validUntil: null, pages: 2 }, { section: 'whats-on', label: "What's On", validUntil: null, pages: 2 }, ]; await mkdir(dataDir, { recursive: true }); for (const dir of ['drafts', 'published', 'originals']) { await rm(path.join(dataDir, dir), { recursive: true, force: true }); } // Seeding replaces the menus, not the category list. const sections = new SectionStore({ dataDir, seed: config.sections }); await sections.load(); const store = new SetStore({ dataDir, pipeline: createImagePipeline(), sections, maxPagesPerSet: config.limits.maxPagesPerSet, }); for (const [index, entry] of plan.entries()) { const section = sections.bySlug(entry.section); if (section === undefined) continue; const set = await store.create(entry.section, { label: entry.label, validUntil: entry.validUntil, }); for (let page = 1; page <= entry.pages; page += 1) { const svg = menuPage(entry.label, section.label, page, entry.pages, index + page); await store.addPage(set.id, { data: await pageImage(svg), originalName: `${entry.section}-${String(page).padStart(2, '0')}.jpg`, }); } const published = await store.publish(set.id); console.log(`published ${published.section}/${published.slug} (${entry.pages} pages)`); } // One archived set, so the "Older menus" fold has something to show. const previous = await store.create('bar', { label: 'Bar Menu, spring', validUntil: '2026-03-31' }); for (let page = 1; page <= 2; page += 1) { await store.addPage(previous.id, { data: await pageImage(menuPage('Bar Menu, spring', 'Bar', page, 2, page)), originalName: `bar-spring-${page}.jpg`, }); } await store.publish(previous.id); // Publishing the spring set archived the June one; put June back as Current so // the archive is the older menu rather than the live one. const june = (await store.list()).find((set) => set.section === 'bar' && set.slug === '2026-06-26'); if (june !== undefined) await store.restore(june.id); console.log(`seeded ${dataDir}`);