Files

113 lines
12 KiB
JavaScript

// Invite / lead-capture page: /join/<token>[?v=<angle>]
// Email first (code sign-in creates the account), wallet later inside the
// member area. The angle only changes the hook copy; the sponsor cookie was
// set by the server when this page was served.
(function () {
const $ = id => document.getElementById(id);
const LEG = (brand, seg) => seg === 'adv'
? { eyebrow: 'For former ' + brand + ' advertisers', h: 'Your next ad budget <em>pays you back.</em>',
lead: brand + ' is closed. The people who bought ads there are exactly who LinkSpin was built for: real ad packages from $5, seven formats, and every package in your line paid out by a verified contract on Polygon in the same transaction.',
points: ['Welcome-back credits land the moment your account exists: enough to run a real banner or text campaign today, on us.', 'Seven formats: banners, text ads, login ads, solo ads to member inboxes, video, featured links and verified visits. Views are timed on the server, so a real person saw your ad.', 'When anyone in your line buys ads, the contract pays you in that same transaction. Public on Polygonscan, nothing held, nothing to withdraw.'],
cta: 'Claim your welcome-back credits', sub: 'Free account by email. No password, no wallet today. Use the email you had on ' + brand + ': the credits are tied to it.', video: false }
: { eyebrow: 'For former ' + brand + ' members', h: 'Same daily habit. <em>Real payouts on-chain.</em>',
lead: 'You viewed ads on ' + brand + '. Here you view ads to earn credits, run your own campaign with them for free, and when anyone in your line buys ads you are paid in POL to your own wallet in the same transaction.',
points: ['Welcome-back credits on day one, so your first campaign runs before you have viewed a single ad.', 'Join with just an email. No password, no wallet today. Link a wallet later, only when you want payouts switched on.', 'Every payout is a public transaction on Polygon. Nothing is held, so there is nothing to withdraw and nothing to wait for.'],
cta: 'Claim your welcome-back credits', sub: 'Free account by email. Use the email you had on ' + brand + ': the credits are tied to it.', video: false };
const ANGLES = {
'fw-adv': LEG('Faucet Wave', 'adv'), 'fw-earn': LEG('Faucet Wave', 'earn'), 't1-adv': LEG('Tier One Ads', 'adv'), 't1-earn': LEG('Tier One Ads', 'earn'),
instant: { eyebrow: 'Same-transaction payouts', h: 'Paid before the page <em>reloads.</em>', lead: 'What if your commission landed before the thank-you page finished loading? On LinkSpin that is not a metaphor. A smart contract on Polygon splits every ad package the moment it sells.',
points: ['A verified contract splits every package in the same transaction it sells: 50 percent to the sponsor, 20 and 10 up the line, 20 to the platform.', 'It lands in your own wallet in seconds. There is no balance to withdraw because nothing is ever held.', 'Every payment is public on Polygonscan, so you can check the claim before you spend a dollar.'],
cta: 'See a payout land in seconds', sub: 'Free account by email. No password, no wallet today. Your invite link is live the moment you are in.' },
adspend: { eyebrow: 'For marketers who buy traffic', h: 'You were buying <em>ads anyway.</em>', lead: 'Every ad dollar you ever spent went one direction: out. Here the ad spend in your line pays you, in the same transaction, on a public ledger. Seven formats, dwell-timed views, packages from $5.',
points: ['Seven formats: banners, text ads, login ads, solo ads to member inboxes, video, featured links and verified visits.', 'Views are timed on the server, so a real person saw your ad. Banners and text also run across a partner ad network.', 'When anyone in your line buys ads, the contract pays you in that same transaction.'],
cta: 'Put your next ad dollar where it pays you back', sub: 'Free account by email. See every format and the live rates before you buy anything.' },
free: { eyebrow: 'Costs nothing to try', h: 'Watch first. <em>Spend never.</em>', lead: 'Join free, view a few ads, earn credits, and run your first campaign for zero dollars. Upgrade only if you want more reach.',
points: ['Join with just an email. No password, no wallet today.', 'View a few ads each day and earn credits you can spend on your own banner or text campaign.', 'Buy a package only if you want more reach. They start at $5, and every payout on them is public.'],
cta: 'Start for free. No card, no wallet.', sub: 'Type your email and we send a 6-digit code. That is the whole signup.' },
ledger: { eyebrow: 'No back office', h: 'No back office. <em>No payday.</em>', lead: 'Your last program paid you on the 15th, if it paid you. Here every payout is a public transaction on Polygon you can read yourself, and nothing is ever held.',
points: ['Every payout is a public transaction on Polygon. Click it, read it, verify it yourself.', 'The contract holds zero balance. It splits and sends in the same transaction, with no pause switch and no upgrade path.', 'No back office means nobody can delay, reverse or review your commission.'],
cta: 'Check the ledger yourself, then decide', sub: 'Free account by email. The public ledger and the verified contract are one click from your dashboard.' },
two: { eyebrow: 'The referral side, exactly as coded', h: 'Two buyers open <em>level two.</em>', lead: 'Every direct buyer pays you 50 percent from their first package. Two qualifying buyers open level two, five open level three. Written as constants in a verified contract.',
points: ['Every direct buyer pays you 50 percent from their very first package.', 'Two qualifying buyers open level two at 20 percent. Five open level three at 10 percent. Constants in a verified contract.', 'Until a level opens, its share climbs to the next qualified member above, so the plan rewards the people who build.'],
cta: 'Start your line. Two buyers is the target.', sub: 'Free account by email. Your invite link and the team-building plays are waiting inside.' }
};
// ?name=First personalises the page (site-owner links, Marty 2026-09-12): letters, spaces, hyphens,
// apostrophes only, 30 chars. On the company placement links it says the top spot is reserved.
const who = String(new URLSearchParams(location.search).get('name') || '').replace(/[^A-Za-z\u00C0-\u024F' -]/g, '').trim().slice(0, 30);
if (who) {
const top = /^\/join\/(company|top)$/i.test(location.pathname);
const h = $('jnHello');
if (h) { h.textContent = who + (top ? ', your spot directly under the company at the top is reserved for you. Activate with the $20 package to claim it.' : ', this invitation is for you.'); h.hidden = false; }
}
const v = new URLSearchParams(location.search).get('v') || (document.cookie.match(/(?:^|; )iap\.angle=([^;]+)/) || [])[1] || '';
const a = v && ANGLES[v];
if (a) {
document.body.classList.add('squeeze'); // server sets it too; this covers cached HTML
$('jnEyebrow').textContent = '· ' + a.eyebrow; $('jnHead').innerHTML = a.h; $('jnLead').textContent = a.lead;
if (a.cta) { $('jnCapH').textContent = a.cta; $('jnCapSub').textContent = a.sub || ''; }
// the matching hook video + this angle's three points replace the worked example
const VID = 'https://coolify-saasytop.nyc3.digitaloceanspaces.com/promo/';
const vid = $('jnVideo');
if (a.video === false) vid.hidden = true; else { vid.src = VID + v + '.mp4'; vid.poster = VID + v + '.jpg'; }
$('jnPoints').innerHTML = (a.points || []).map(t => '<li>' + t.replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c])) + '</li>').join('');
$('jnMock').hidden = true; $('jnAngle').hidden = false; $('jnPoints').hidden = false;
}
async function api(path, body) {
const r = await (await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })).json();
if (r.error) throw new Error(r.error);
return r;
}
const err = m => { const e = $('jnErr'); e.textContent = m || ''; e.hidden = !m; };
function busy(btn, fn) {
return async () => { if (btn.disabled) return; btn.disabled = true; err(''); try { await fn(); } catch (e) { err(e.message || 'Something went wrong.'); } finally { btn.disabled = false; } };
}
const codeOpts = () => ({ honeypot: $('jnWebsite'), host: $('jnCheck') });
const send = busy($('jnSend'), async () => {
const r = await IAP.requestCode($('jnEmail').value, codeOpts());
$('jnCodeRow').hidden = false; $('jnVerify').hidden = false; $('jnSend').hidden = true; $('jnResend').hidden = false;
if (r.devCode) $('jnCode').value = r.devCode;
// the last thing they see before the account is created: who they are joining under
if (sponsorName) { $('jnUnder').textContent = 'Joining under ' + sponsorName + '. Not who invited you? Open their invite link first, then come back for the code.'; $('jnUnder').hidden = false; }
IAP.status(r.sent ? 'Code sent. Check your inbox (and spam, the first time).' : 'Dev mode: code filled in.', 'ok');
$('jnCode').focus();
});
$('jnSend').addEventListener('click', send);
$('jnResend').addEventListener('click', busy($('jnResend'), async () => {
const r = await IAP.requestCode($('jnEmail').value, codeOpts());
if (r.devCode) $('jnCode').value = r.devCode;
IAP.status('Fresh code sent.', 'ok');
}));
$('jnVerify').addEventListener('click', busy($('jnVerify'), async () => {
await api('/api/auth/email/verify', { email: $('jnEmail').value, code: $('jnCode').value, newsletter: !!$('jnNews').checked, followups: !!$('jnNews').checked });
IAP.status('You are in. Taking you to your dashboard…', 'ok');
location.href = '/my?welcome=1'; // dashboard runs the username step + welcome tour on arrival
}));
$('jnEmail').addEventListener('keydown', e => { if (e.key === 'Enter') ($('jnVerify').hidden ? $('jnSend') : $('jnVerify')).click(); });
$('jnCode').addEventListener('keydown', e => { if (e.key === 'Enter') $('jnVerify').click(); });
// sponsor line (the link opened most recently sets the sponsor; it locks at account creation)
let sponsorName = '';
const linkTok = (location.pathname.match(/^\/join\/([^/?#]+)/) || [])[1] || '';
fetch('/api/sponsor' + (linkTok ? '?ref=' + encodeURIComponent(linkTok) : '')).then(r => r.json()).then(sp => {
if (sp && sp.invited && sp.name) {
sponsorName = sp.name;
$('jnSponName').textContent = sp.name + (sp.own ? ' (this is your own invite page; visitors see your name here)' : '');
if (sp.avatarUrl) { $('jnSponImg').src = sp.avatarUrl; $('jnSponImg').hidden = false; }
if (sp.cobrand && sp.bio && $('jnSponBio')) { $('jnSponBio').textContent = sp.bio; $('jnSponBio').hidden = false; $('jnSpon').classList.add('cobrand'); }
$('jnSpon').hidden = false;
}
}).catch(() => {});
// package ladder (dollar constants + live POL quote)
const NAMES = { 1: 'Micro', 2: 'Activation', 3: 'Builder', 4: 'Growth', 5: 'Leader' };
fetch('/api/catalog').then(r => r.json()).then(c => {
const wrap = $('jnLadder'); if (!wrap || !c.products) return;
wrap.innerHTML = c.products.filter(p => p.active !== false).map(p =>
'<div class="jn-pk"><div class="n">' + (NAMES[p.id] || 'Package ' + p.id) + '</div><div class="p">' + IAP.fmtUsd(p.priceCents) + '</div><div class="c">' + Number(p.creditAmount).toLocaleString() + ' credits</div>'
+ (p.costWei ? '<div class="small muted">' + IAP.fmtPol(p.costWei) + ' POL now</div>' : '') + '</div>').join('');
}).catch(() => {});
fetch('/api/stats').then(r => r.json()).then(s => {
if (s && s.onchainMembers) $('jnStats').textContent = s.onchainMembers.toLocaleString() + (s.onchainMembers === 1 ? ' member' : ' members') + ' on-chain so far.';
}).catch(() => {});
})();