← back to Prestige Tip Cards
gen.mjs
81 lines
// Generates printable, scannable "Scan to tip me" cards — one per employee.
// QR codes come from `qrencode` (offline, $0). Cards render via headless Chrome.
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const DIR = path.dirname(fileURLToPath(import.meta.url));
const CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
const cfg = JSON.parse(fs.readFileSync(path.join(DIR, 'employees.json'), 'utf8'));
const cardHtml = (e) => `<!doctype html><html><head><meta charset="utf-8"><style>
*{margin:0;box-sizing:border-box}
html,body{width:1000px;height:1400px}
body{font-family:Inter,"SF Pro Display",system-ui,-apple-system,sans-serif;
background:linear-gradient(165deg,#0b1728 0%,#0a1220 55%,#060c16 100%);color:#eaf2ff;
display:flex;flex-direction:column;align-items:center;padding:70px 64px;position:relative;overflow:hidden}
.glow{position:absolute;width:900px;height:900px;border-radius:50%;
background:radial-gradient(circle, ${e.accent}22 0%, transparent 60%);top:-260px;right:-260px}
.brand{display:flex;align-items:center;gap:16px;font-weight:800;font-size:40px;letter-spacing:.5px}
.logo{width:64px;height:64px;border-radius:16px;background:linear-gradient(135deg,${e.accent},#2563eb);
display:flex;align-items:center;justify-content:center;font-size:34px}
.region{margin-top:8px;color:#8ea6c4;font-size:24px;letter-spacing:3px;text-transform:uppercase;font-weight:700}
.who{margin-top:54px;text-align:center}
.avatar{width:150px;height:150px;border-radius:50%;margin:0 auto 22px;
background:linear-gradient(135deg,#1c2c46,#0f1a2b);border:3px solid ${e.accent};
display:flex;align-items:center;justify-content:center;font-size:64px;font-weight:800;color:${e.accent}}
.name{font-size:66px;font-weight:900;line-height:1}
.role{margin-top:12px;font-size:32px;color:#b8c9e0;font-weight:600}
.scan{margin-top:40px;font-size:38px;font-weight:800;letter-spacing:2px;color:${e.accent};text-transform:uppercase}
.qrbox{margin-top:26px;background:#fff;border-radius:28px;padding:34px;box-shadow:0 30px 80px rgba(0,0,0,.55)}
.qrbox img{display:block;width:420px;height:420px;image-rendering:pixelated}
.handle{margin-top:26px;font-size:30px;color:#dbe7f6;font-weight:700}
.thanks{margin-top:10px;font-size:26px;color:#9fb3cd}
.foot{position:absolute;bottom:56px;left:0;right:0;text-align:center;color:#6f88a8;font-size:24px;font-weight:600;letter-spacing:1px}
</style></head><body>
<div class="glow"></div>
<div class="brand"><span class="logo">🚗</span> ${cfg.brand.name}</div>
<div class="region">${cfg.brand.region}</div>
<div class="who">
<div class="avatar">${e.name[0]}</div>
<div class="name">${e.name}</div>
<div class="role">${e.role}</div>
</div>
<div class="scan">Scan to tip me</div>
<div class="qrbox"><img src="../qr/${e.id}.png"></div>
<div class="handle">${e.tip_url.replace(/^https?:\/\//, '')}</div>
<div class="thanks">Every tip goes straight to me — thank you! 🙏</div>
<div class="foot">${cfg.brand.tagline}</div>
</body></html>`;
// 1) QR codes (offline, scannable) — high error-correction, margin quiet-zone.
for (const e of cfg.employees) {
execFileSync('qrencode', ['-o', path.join(DIR, 'qr', `${e.id}.png`), '-s', '14', '-m', '2', '-l', 'H', e.tip_url]);
}
console.log(`QR codes: ${cfg.employees.length}`);
// 2) Card PNGs (one per employee) + a print PDF each.
for (const e of cfg.employees) {
const htmlPath = path.join(DIR, 'html', `${e.id}.html`);
fs.writeFileSync(htmlPath, cardHtml(e));
execFileSync(CHROME, ['--headless=new', '--disable-gpu', '--force-device-scale-factor=2',
'--window-size=1000,1400', `--screenshot=${path.join(DIR, 'cards', e.id + '.png')}`,
'file://' + htmlPath], { stdio: 'ignore' });
console.log(` card: ${e.id}`);
}
// 3) Combined print sheet (all cards, 2 per row) → single PDF.
const sheet = `<!doctype html><meta charset=utf-8><style>
@page{size:letter;margin:0.4in}
body{margin:0;display:flex;flex-wrap:wrap;gap:0.3in;justify-content:center;
font-family:system-ui;background:#fff}
.c{width:3.2in;height:4.48in;page-break-inside:avoid}
.c img{width:100%;height:100%;object-fit:contain}
</style><body>${cfg.employees.map((e) => `<div class="c"><img src="cards/${e.id}.png"></div>`).join('')}</body>`;
const sheetPath = path.join(DIR, 'print-sheet.html');
fs.writeFileSync(sheetPath, sheet);
execFileSync(CHROME, ['--headless=new', '--disable-gpu', '--no-pdf-header-footer',
`--print-to-pdf=${path.join(DIR, 'prestige-tip-cards.pdf')}`, 'file://' + sheetPath], { stdio: 'ignore' });
console.log('PDF: prestige-tip-cards.pdf');