Toolkit: ship the rest of the ladder (Spark templates + handout, Circuit Video Maker + split tester, Nexus Leader Ops, grants, co-brand, partner code)
- Spark: one-tap banner/text campaign templates aimed at the member's link (POST /api/my/toolkit/template); printable QR handout at /handout/<username> - Circuit: videomaker.js renders every promo video/short with the member's end card + QR via ffmpeg (Dockerfile adds ffmpeg + ttf-dejavu), hosted in Spaces; split tester compares join angles - Nexus: Leader Ops (three-level triage, stalled flags, one-click nudges), AI team broadcast kind + Send to my team, credit grants from the leader's earned pool, co-branded join page (sponsor bio for level-3 sponsors), member-funded partner code (promo_codes.funder; redemption charges the funder) + /partners?ref=<username> - toolkit ladder all live; chatbot updated; my.js v20260914e, site.css v20260914c, join.js/partners.js v20260914a Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -27,8 +27,8 @@ const D = {
|
||||
async get(code) { const r = await db.q('SELECT * FROM promo_codes WHERE code=?', [code]); return r[0] ? row(r[0]) : null; },
|
||||
async list() { return (await db.q('SELECT * FROM promo_codes ORDER BY created DESC')).map(row); },
|
||||
async put(c) {
|
||||
await db.q('INSERT INTO promo_codes (code,credits,partner,note,max_uses,expires,active,created) VALUES (?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE credits=VALUES(credits), partner=VALUES(partner), note=VALUES(note), max_uses=VALUES(max_uses), expires=VALUES(expires), active=VALUES(active)',
|
||||
[c.code, c.credits, c.partner || null, c.note || null, c.maxUses || 0, c.expires || 0, c.active ? 1 : 0, c.created || Date.now()]);
|
||||
await db.q('INSERT INTO promo_codes (code,credits,partner,note,max_uses,expires,active,created,funder) VALUES (?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE funder=VALUES(funder), credits=VALUES(credits), partner=VALUES(partner), note=VALUES(note), max_uses=VALUES(max_uses), expires=VALUES(expires), active=VALUES(active)',
|
||||
[c.code, c.credits, c.partner || null, c.note || null, c.maxUses || 0, c.expires || 0, c.active ? 1 : 0, c.created || Date.now(), c.funder || null]);
|
||||
return this.get(c.code);
|
||||
},
|
||||
async uses(code) { const r = await db.q('SELECT COUNT(*) n FROM promo_redemptions WHERE code=?', [code]); return Number(r[0].n); },
|
||||
@@ -36,7 +36,7 @@ const D = {
|
||||
async addRedemption(r) { await db.q('INSERT IGNORE INTO promo_redemptions (code,email,credits,via,ts) VALUES (?,?,?,?,?)', [r.code, r.email, r.credits, r.via, r.ts]); },
|
||||
async redemptions(code, n) { const rows = code ? await db.q('SELECT * FROM promo_redemptions WHERE code=? ORDER BY ts DESC LIMIT ?', [code, Number(n) || 200]) : await db.q('SELECT * FROM promo_redemptions ORDER BY ts DESC LIMIT ?', [Number(n) || 200]); return rows.map(r => ({ code: r.code, email: r.email, credits: Number(r.credits), via: r.via, ts: Number(r.ts) })); }
|
||||
};
|
||||
const row = r => ({ code: r.code, credits: Number(r.credits), partner: r.partner || '', note: r.note || '', maxUses: Number(r.max_uses) || 0, expires: Number(r.expires) || 0, active: !!Number(r.active), created: Number(r.created) });
|
||||
const row = r => ({ code: r.code, funder: r.funder || null, credits: Number(r.credits), partner: r.partner || '', note: r.note || '', maxUses: Number(r.max_uses) || 0, expires: Number(r.expires) || 0, active: !!Number(r.active), created: Number(r.created) });
|
||||
const impl = () => db.enabled() ? D : J;
|
||||
|
||||
function init(opts) { DATA_DIR = opts.dataDir; if (!db.enabled()) J.load(); }
|
||||
@@ -44,7 +44,7 @@ async function create(c) {
|
||||
const code = norm(c.code); if (!code || code.length < 3) return { error: 'Code must be 3 to 24 letters or numbers.' };
|
||||
const credits = Math.floor(Number(c.credits)); if (!(credits > 0) || credits > 100000) return { error: 'Credits must be between 1 and 100,000.' };
|
||||
const cur = await impl().get(code);
|
||||
const saved = await impl().put({ code, credits, partner: String(c.partner || '').slice(0, 80), note: String(c.note || '').slice(0, 200), maxUses: Math.max(0, Math.floor(Number(c.maxUses) || 0)), expires: c.expires ? Number(new Date(c.expires)) || 0 : 0, active: c.active !== false && c.active !== 0 && c.active !== '0', created: cur ? cur.created : Date.now() });
|
||||
const saved = await impl().put({ code, credits, funder: c.funder ? String(c.funder).toLowerCase() : (cur && cur.funder) || null, partner: String(c.partner || '').slice(0, 80), note: String(c.note || '').slice(0, 200), maxUses: Math.max(0, Math.floor(Number(c.maxUses) || 0)), expires: c.expires ? Number(new Date(c.expires)) || 0 : 0, active: c.active !== false && c.active !== 0 && c.active !== '0', created: cur ? cur.created : Date.now() });
|
||||
return { ok: true, code: saved };
|
||||
}
|
||||
async function setActive(code, active) { const c = await impl().get(norm(code)); if (!c) return { error: 'No such code.' }; await impl().put(Object.assign(c, { active: !!active })); return { ok: true }; }
|
||||
@@ -63,11 +63,12 @@ async function redeem(code, email, via) {
|
||||
const bad = await check(k, e); if (bad) return bad;
|
||||
const c = await impl().get(k);
|
||||
await impl().addRedemption({ code: k, email: e, credits: c.credits, via: via || 'dashboard', ts: Date.now() });
|
||||
return { ok: true, credits: c.credits, code: k, partner: c.partner };
|
||||
return { ok: true, credits: c.credits, code: k, partner: c.partner, funder: c.funder || null };
|
||||
}
|
||||
async function adminView() {
|
||||
const codes = await impl().list();
|
||||
for (const c of codes) c.uses = await impl().uses(c.code);
|
||||
return { codes, recent: await impl().redemptions(null, 100) };
|
||||
}
|
||||
module.exports = { init, create, setActive, check, redeem, adminView, norm };
|
||||
async function byFunder(email) { return (await impl().list()).filter(c => c.funder === String(email || '').toLowerCase()); }
|
||||
module.exports = { init, create, setActive, check, redeem, adminView, norm, byFunder };
|
||||
|
||||
Reference in New Issue
Block a user