Prettier owns formatting: .prettierrc.json, format and format:check scripts, and a one-off pass over the repo so the check is green from the start. Embedded language formatting is off deliberately - the renderers build HTML in html tagged templates and letting Prettier reflow those would change the markup customers receive. docs/ is excluded because it is hand-wrapped prose, and shell scripts have no Prettier parser. ESLint covers the JavaScript: the two browser scripts, the build and check scripts, and its own config. no-var is scoped out for the browser scripts on purpose; they are plain ES5-shaped IIFEs and a hoisting difference would only show up in a browser. ESLint cannot lint the TypeScript here: typescript@7 is the native port with no JavaScript compiler API, and typescript-eslint requires a 4.8-6.1 peer, so it refuses to install and forcing it through would leave a parser that cannot read the compiler. tsc --noEmit stays the TypeScript gate; README records the two ways to change that. lint and format:check now run in deploy.sh, and VS Code formats on save with Prettier and surfaces ESLint findings.
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
/**
|
|
* 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 `
|
|
<text x="120" y="${y}" font-family="Georgia, serif" font-size="34" fill="#1b1f22">${
|
|
[
|
|
'Wood-fired pizza',
|
|
'Smash burger',
|
|
'Tenderloin steak',
|
|
'Craft platter',
|
|
'Fish and chips',
|
|
'Risotto',
|
|
'Cheesecake',
|
|
][index % 7]
|
|
}</text>
|
|
<text x="120" y="${y + 34}" font-family="Georgia, serif" font-size="24" fill="#6b7075">${
|
|
[
|
|
'thin crust, basil',
|
|
'double patty, cheddar',
|
|
'pepper sauce',
|
|
'for two',
|
|
'tartare',
|
|
'wild mushroom',
|
|
'berry coulis',
|
|
][index % 7]
|
|
}</text>
|
|
<text x="1000" y="${y}" text-anchor="end" font-family="Georgia, serif" font-size="34" fill="#1b1f22">${
|
|
240 + ((seed * 91 + index * 71) % 620)
|
|
}</text>
|
|
<rect x="120" y="${y + 60}" width="${width}" height="2" fill="#cfc9bf" />`;
|
|
}).join('');
|
|
|
|
return `<svg xmlns="http://www.w3.org/2000/svg" width="1240" height="1754" viewBox="0 0 1240 1754">
|
|
<rect width="1240" height="1754" fill="#f7f4ee" />
|
|
<rect x="0" y="0" width="1240" height="14" fill="#0088aa" />
|
|
<text x="120" y="180" font-family="Georgia, serif" font-size="72" fill="#1b1f22">${title}</text>
|
|
<text x="120" y="240" font-family="Helvetica, Arial, sans-serif" font-size="30" fill="#6b7075">${section} · page ${page} of ${total}</text>
|
|
<rect x="120" y="290" width="1000" height="1" fill="#b2703b" />
|
|
${items}
|
|
<text x="120" y="1650" font-family="Helvetica, Arial, sans-serif" font-size="24" fill="#6b7075">Placeholder page for design review</text>
|
|
<text x="1120" y="1650" text-anchor="end" font-family="Helvetica, Arial, sans-serif" font-size="24" fill="#b2703b">${page} / ${total}</text>
|
|
</svg>`;
|
|
}
|
|
|
|
async function pageImage(svg: string): Promise<Buffer> {
|
|
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}`);
|