diff --git a/accounts.js b/accounts.js index 3e88f33..4ceba60 100644 --- a/accounts.js +++ b/accounts.js @@ -82,6 +82,13 @@ const J = { async byEmail(e) { return pub(this.db.byEmail[e]); }, async byAddress(a) { const e = this.db.byAddress[a]; return e ? pub(this.db.byEmail[e]) : null; }, async byCode(c) { const e = this.db.byCode[c]; return e ? pub(this.db.byEmail[e]) : null; }, + async listByReferrer(refs) { + const set = new Set(refs.filter(Boolean).map(String)); + return Object.values(this.db.byEmail) + .filter(a => set.has(String(a.sponsorRef || ''))) + .sort((a, b) => b.created - a.created) + .slice(0, 200).map(pub); + }, async linkWallet(e, a) { const acct = this.db.byEmail[e]; if (!acct) return { error: 'No such account.' }; @@ -133,6 +140,13 @@ const D = { async byEmail(e) { const r = await db.q('SELECT * FROM accounts WHERE email=?', [e]); return rowPub(r[0]); }, async byAddress(a) { const r = await db.q('SELECT * FROM accounts WHERE address=?', [a]); return rowPub(r[0]); }, async byCode(c) { const r = await db.q('SELECT * FROM accounts WHERE code=?', [c]); return rowPub(r[0]); }, + async listByReferrer(refs) { + const clean = refs.filter(Boolean).map(String); + if (!clean.length) return []; + const rows = await db.q('SELECT * FROM accounts WHERE sponsor_ref IN (' + clean.map(() => '?').join(',') + + ') ORDER BY created DESC LIMIT 200', clean); + return rows.map(rowPub); + }, async linkWallet(e, a) { const cur = await this.byEmail(e); if (!cur) return { error: 'No such account.' }; @@ -166,6 +180,7 @@ async function ensure(email, sponsorRef) { async function byEmail(email) { return impl().byEmail(normEmail(email)); } async function byAddress(address) { return impl().byAddress(normAddr(address)); } async function byCode(code) { return impl().byCode(String(code || '').toLowerCase()); } +async function listByReferrer(refs) { return impl().listByReferrer(refs || []); } async function linkWallet(email, address) { const a = normAddr(address); if (!/^0x[0-9a-f]{40}$/.test(a)) return { error: 'Bad wallet address.' }; @@ -173,4 +188,4 @@ async function linkWallet(email, address) { } async function count() { return impl().count(); } -module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, linkWallet, count }; +module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, listByReferrer, linkWallet, count }; diff --git a/public/assets/my.js b/public/assets/my.js index c28f88e..c8563c7 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -11,12 +11,60 @@ return r; } + function nextMove(d) { + if (!d.address) return 'Grab your link below and start sharing today. Then link your wallet (one free signature) so every payment can lock to you before your people start buying.'; + if (!d.memberId) return 'Your wallet is linked. Switch on payouts above (one free transaction) and every purchase in your line pays you the moment it happens.'; + if (d.buyerCount === 0) return 'Payouts are on and your link is live. Your next milestone: your first buyer of $20 or more. Every direct pays you 50 percent from their very first package.'; + if (d.buyerCount === 1) return 'One qualifying buyer down, one to go. Your next $20+ buyer unlocks level 2: 20 percent of everything your people’s people buy.'; + if (d.buyerCount < 5) return 'Level 2 is open. ' + (5 - d.buyerCount) + ' more qualifying buyer(s) unlock level 3 and the full three-level flow.'; + return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.'; + } + async function loadDashboard() { + try { + const d = await (await fetch('/api/my/dashboard')).json(); + if (d.error) return; + $('dbCredits').textContent = (d.credits || 0).toLocaleString(); + $('dbEarned').textContent = IAP.fmtPol(d.earnedWei || '0'); + $('dbBuyers').textContent = d.buyerCount || 0; + $('dbTeam').textContent = (d.referrals || []).length; + $('nextMove').textContent = nextMove(d); + $('qualFill').style.width = Math.min(100, (d.buyerCount || 0) * 20) + '%'; + const wrap = $('rosterWrap'); + if ((d.referrals || []).length) { + $('rosterEmpty').hidden = true; + let t = wrap.querySelector('table'); + if (t) t.remove(); + t = document.createElement('table'); + t.className = 'roster'; + t.innerHTML = d.referrals.map(r => '' + r.email + '' + + '' + new Date(r.joined).toLocaleDateString() + '' + + '' + r.status + '').join(''); + wrap.appendChild(t); + } + // ready-to-send share message + const link = location.origin + '/join/' + (d.refCode || d.memberId || ''); + if (d.refCode || d.memberId) { + const pitch = 'I found an advertising site that pays referrals instantly to your own wallet. ' + + 'No withdrawals, no waiting, and every payment is public on a blockchain ledger you can check yourself. ' + + 'Free to join and look around: ' + link; + $('copyPitch').hidden = false; + $('pitchPreview').hidden = false; + $('pitchPreview').textContent = '"' + pitch + '"'; + $('copyPitch').onclick = async () => { + try { await navigator.clipboard.writeText(pitch); IAP.status('Message copied. Paste it anywhere.', 'ok'); } + catch (e) { IAP.status('Copy failed. Select the preview text instead.', 'bad'); } + }; + } + } catch (e) {} + } + async function render() { const me = await IAP.refreshNavWallet(); const signedIn = me && me.signedIn; $('authArea').hidden = !!signedIn; $('memberArea').hidden = !signedIn; if (!signedIn) return; + loadDashboard(); const who = []; if (me.email) who.push(me.email); diff --git a/public/assets/site.css b/public/assets/site.css index 6eadc43..be195d8 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -220,6 +220,16 @@ input[type=range]{padding:0;border:0;background:transparent;accent-color:var(--m input:focus,select:focus{border-color:var(--mint)} :focus-visible{outline:2px solid var(--mint);outline-offset:2px} .hero-note{font-family:var(--mono);font-size:12px;color:var(--muted);margin-top:26px} +/* member dashboard: qualification progress + roster */ +.qualbar{position:relative;height:12px;border-radius:999px;background:rgba(4,8,7,.7);border:1px solid var(--line-strong); + margin:26px 0 30px;max-width:520px} +.qualbar #qualFill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--mint),var(--mint-hi)); + box-shadow:0 0 18px rgba(67,232,195,.4);width:0;transition:width .6s ease} +.qb-mark{position:absolute;top:16px;transform:translateX(-50%);font-family:var(--mono);font-size:11px;color:var(--muted);white-space:nowrap} +.roster{width:100%;border-collapse:collapse;font-size:13.5px} +.roster td{padding:8px 10px;border-bottom:1px solid var(--line)} +.roster td:first-child{font-family:var(--mono)} +.roster tr:last-child td{border-bottom:0} /* chat widget */ #iapChatBtn{position:fixed;right:20px;bottom:20px;z-index:40;width:56px;height:56px;border-radius:50%; border:0;background:var(--mint);color:var(--mint-ink);font-size:24px;cursor:pointer; diff --git a/public/contract.html b/public/contract.html index d118220..4e825d9 100644 --- a/public/contract.html +++ b/public/contract.html @@ -5,7 +5,7 @@ The contract | InstantAdPay - +
@@ -129,8 +129,8 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
- - - + + + diff --git a/public/index.html b/public/index.html index 60196f0..042539e 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ InstantAdPay: advertise and earn, locked in code - + @@ -398,9 +398,9 @@ - - - - + + + + diff --git a/public/ledger.html b/public/ledger.html index 8506267..851cc2b 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -5,7 +5,7 @@ Live ledger | InstantAdPay - +
@@ -25,8 +25,8 @@
InstantAdPay · how it works · contract source ↗
- - - + + + diff --git a/public/my.html b/public/my.html index f949f82..876193a 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ My account | InstantAdPay - +
@@ -46,6 +46,24 @@
+
+

Your line

+

Everyone who joined through your link, newest first. When they buy their + first package of $20 or more, they count toward your qualification.

+

Nobody yet. Your link is ready below; share it and this list starts filling.

+
+
@@ -131,9 +160,9 @@
- - - - + + + + diff --git a/server.js b/server.js index 4267d5c..6da22ce 100644 --- a/server.js +++ b/server.js @@ -300,6 +300,42 @@ const server = http.createServer(async (req, res) => { return json(res, 200, out); } + if (p === '/api/my/dashboard' && req.method === 'GET') { + const s = await auth.fromRequest(req); + if (!s) return json(res, 401, { error: 'Sign in first.' }); + const memberId = await auth.refreshMemberId(s); + const acct = (s.email && await accounts.byEmail(s.email)) || (s.address && await accounts.byAddress(s.address)) || null; + const out = { memberId, email: s.email || (acct && acct.email) || null, + address: s.address || (acct && acct.address) || null, + refCode: (acct && acct.code) || null, credits: 0, buyerCount: 0, + earnedWei: '0', earnCount: 0, referrals: [] }; + if (memberId) { + try { + const mm = await chain.member(memberId); + out.buyerCount = mm.buyerCount; + out.credits = await ads.availableCredits(memberId); + } catch (e) { out.chainReadError = true; } + let earned = 0n, n = 0; + for (const ev of chain.recentEvents(600)) { + if ((ev.type === 'TierPaid' && ev.recipientId === memberId) || (ev.type === 'AwardPaid' && ev.toId === memberId)) { + earned += BigInt(ev.amountWei); n += 1; + } + } + out.earnedWei = earned.toString(); + out.earnCount = n; + } + // who joined through this member (code and, once on-chain, numeric id) + const refs = []; + if (acct && acct.code) refs.push(acct.code); + if (memberId) refs.push(String(memberId)); + const joined = await accounts.listByReferrer(refs); + out.referrals = joined.map(r => ({ + email: r.email.replace(/^(.).*(@.*)$/, '$1***$2'), // privacy mask + joined: r.created, + status: r.address ? 'wallet linked' : 'joined free' + })); + return json(res, 200, out); + } if (p === '/api/my/activity' && req.method === 'GET') { const s = await auth.fromRequest(req); if (!s) return json(res, 401, { error: 'Sign in first.' });