Partner promo codes: reusable codes that add free ad credits, tracked per redemption

promos.js (promo_codes + promo_redemptions, JSON fallback): create/update codes with
credits, partner, optional cap and expiry, on/off. Redeemed once per account either
from a join link ?promo=CODE (cookie, applied at signup) or the Overview box
'Have a promo code?' (/api/my/promo/redeem). Admin > Traffic gets a Partner promo
codes card (create, toggle, uses, recent redemptions). Chatbot line added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-12 10:53:17 -05:00
parent 9b60e6414e
commit fc55714757
8 changed files with 179 additions and 2 deletions
+19
View File
@@ -2215,3 +2215,22 @@
render();
})();
// ── partner promo codes: typed on the Overview (Marty, 2026-09-12) ──
(function () {
const btn = document.getElementById('promoApply'), inp = document.getElementById('promoCode'), msg = document.getElementById('promoMsg');
if (!btn || !inp) return;
const say = (t, ok) => { msg.hidden = false; msg.textContent = t; msg.style.color = ok ? 'var(--mint)' : '#ff8a8a'; };
const go = async () => {
const code = inp.value.trim(); if (!code) { say('Enter a promo code.', false); return; }
btn.disabled = true;
try {
const r = await (await fetch('/api/my/promo/redeem', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code }) })).json();
if (r.error) { say(r.error, false); return; }
say('Added ' + Number(r.credits).toLocaleString() + ' credits' + (r.partner ? ' from ' + r.partner : '') + '. They are in your balance now.', true);
inp.value = ''; if (typeof loadDashboard === 'function') loadDashboard();
} catch (e) { say('Could not apply that code. Try again.', false); }
finally { btn.disabled = false; }
};
btn.addEventListener('click', go); inp.addEventListener('keydown', e => { if (e.key === 'Enter') go(); });
})();