Promo tools built out: posts, text-a-friend, swipes, banner kit, handouts, objections

- Social posts for X / Facebook / LinkedIn / Telegram-WhatsApp with post and
  share buttons; 5 SMS-sized Text-a-friend messages (Text it / WhatsApp /
  Telegram / Copy); email swipes short, standard, long, follow-up.
- Banner kit adds 1080x1080, 1080x1920 and 1280x720 (tools/gen-social-banners.cjs)
  with Download buttons.
- Printable half-sheet handouts at /flyers with the member's QR + invite link
  (qrlib.js, two per letter page, cut line, print-one).
- Objection handling bank: truth + ready-to-send reply per objection.
- Invite-link strip, Branded Voice CTA, pill menu covers every kit; chatbot
  canned answer + prompt facts updated. Angle links carry ?v= for the
  upcoming hook pages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-09 05:54:18 -05:00
parent e7ee7d0614
commit f3f9662d99
13 changed files with 2752 additions and 47 deletions
+67
View File
@@ -0,0 +1,67 @@
// Renders the square / story / Telegram social banners for the promo kit.
// Run from the site dir with the qa-tester playwright:
// node tools/gen-social-banners.cjs
// Output: public/banners/iap-<w>x<h>.png (exact pixel sizes, DSF 1)
const path = require('path');
const fs = require('fs');
const { chromium } = require('D:/Projects/MarketingAgent/qa-tester/node_modules/playwright');
const OUT = path.join(__dirname, '..', 'public', 'banners');
const logo = 'data:image/png;base64,' + fs.readFileSync(path.join(__dirname, '..', 'public', 'logo.png')).toString('base64');
const SIZES = [
{ w: 1080, h: 1080, kind: 'square' },
{ w: 1080, h: 1920, kind: 'story' },
{ w: 1280, h: 720, kind: 'telegram' }
];
function html({ w, h, kind }) {
const story = kind === 'story', tg = kind === 'telegram';
const pad = Math.round(w * 0.075);
const h1 = story ? 92 : tg ? 66 : 84;
const lead = story ? 36 : tg ? 27 : 32;
const logoH = story ? 120 : tg ? 84 : 104;
const cta = tg ? 'Tap the link below' : 'Join free';
return `<!doctype html><html><head><meta charset="utf-8"><style>
@import url('https://fonts.googleapis.com/css2?family=Sora:wght@700;800&display=swap');
html,body{margin:0;width:${w}px;height:${h}px;overflow:hidden}
body{font-family:"Sora","Segoe UI",system-ui,sans-serif;color:#eef7f3;
background:radial-gradient(120% 90% at 8% 0%,#16352c 0%,#0b1512 58%,#07100d 100%);position:relative}
.lines{position:absolute;inset:0;background:repeating-linear-gradient(0deg,rgba(67,232,195,.06) 0 1px,transparent 1px 34px)}
.glow{position:absolute;right:-18%;top:-22%;width:70%;height:70%;border-radius:50%;background:radial-gradient(circle,rgba(67,232,195,.16),transparent 62%)}
.wrap{position:absolute;inset:0;padding:${pad}px;display:flex;flex-direction:column;justify-content:${story ? 'center' : 'space-between'};gap:${story ? 48 : 24}px}
img.logo{height:${logoH}px;display:block}
h1{font-size:${h1}px;line-height:1.05;margin:0;letter-spacing:-.02em;font-weight:800;text-wrap:balance}
h1 em{font-style:normal;color:#43e8c3}
p{font-size:${lead}px;line-height:1.35;margin:0;color:#a9c4ba;max-width:${Math.round(w * 0.86)}px}
.row{display:flex;align-items:center;justify-content:space-between;gap:20px}
.cta{display:inline-block;background:#43e8c3;color:#03211a;font-weight:800;font-size:${story ? 40 : tg ? 30 : 34}px;padding:${story ? 26 : 18}px ${story ? 54 : 40}px;border-radius:999px}
.url{font-weight:700;font-size:${story ? 34 : tg ? 26 : 28}px;color:#8ba69c}
.tag{display:inline-block;border:1px solid rgba(67,232,195,.45);color:#43e8c3;border-radius:999px;padding:8px 20px;font-size:${story ? 28 : 22}px;font-weight:700;letter-spacing:.06em;text-transform:uppercase}
</style></head><body><div class="lines"></div><div class="glow"></div>
<div class="wrap">
<div><img class="logo" src="${logo}" alt=""></div>
<div>
<span class="tag">Polygon · same-transaction payouts</span>
<h1 style="margin-top:${story ? 30 : 18}px">Advertise &amp; earn.<br>Paid on-chain, <em>instantly.</em></h1>
<p style="margin-top:${story ? 26 : 16}px">Every ad package splits to real wallets the second it sells. Public ledger. Nothing to claim.</p>
</div>
<div class="row"><span class="cta">${cta} →</span><span class="url">instantadpay.com</span></div>
</div></body></html>`;
}
(async () => {
const browser = await chromium.launch();
for (const s of SIZES) {
const page = await browser.newPage({ viewport: { width: s.w, height: s.h }, deviceScaleFactor: 1 });
await page.setContent(html(s), { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.waitForFunction(() => { const i = document.querySelector('img.logo'); return i && i.complete && i.naturalWidth > 0; });
await page.waitForTimeout(400);
const file = path.join(OUT, `iap-${s.w}x${s.h}.png`);
await page.screenshot({ path: file, clip: { x: 0, y: 0, width: s.w, height: s.h } });
console.log('wrote', file);
await page.close();
}
await browser.close();
})();