From 1ee0b5626e7cdb6169eef6707c1b6644800a519d Mon Sep 17 00:00:00 2001 From: martbost Date: Sun, 6 Sep 2026 07:30:49 -0500 Subject: [PATCH] Viral welcome tour + line banner + wall, milestone stepper, rich solo editor, CTA-gated reads - Welcome tour (3 levels x 10s) unlocks welcome credits; line banner in Profile; public /wall/ - 'Your next move' redesigned as a milestone stepper - Solo composer: BV-style rich editor (H2/H3, inline image+video, undo/redo, raw text) - Solo read reward now requires clicking through to the advertiser, not just dwelling - Sanitizer: inline media whitelist + script/style stripped whole Co-Authored-By: Claude Fable 5 --- accounts.js | 29 +++++- ads.js | Bin 30886 -> 33318 bytes chatbot.js | 3 +- db.js | 4 + public/assets/my.js | 201 ++++++++++++++++++++++++++++++++++------- public/assets/site.css | 38 ++++++++ public/assets/wall.js | 33 +++++++ public/contract.html | 8 +- public/index.html | 11 ++- public/ledger.html | 8 +- public/my.html | 93 ++++++++++++++----- public/tx.html | 6 +- public/view.html | 4 +- public/wall.html | 32 +++++++ server.js | 87 +++++++++++++++++- 15 files changed, 477 insertions(+), 80 deletions(-) create mode 100644 public/assets/wall.js create mode 100644 public/wall.html diff --git a/accounts.js b/accounts.js index 465c108..8b9b02c 100644 --- a/accounts.js +++ b/accounts.js @@ -34,6 +34,7 @@ function newCode(taken) { } const pub = a => a ? { email: a.email, sponsorRef: a.sponsorRef || '', code: a.code || null, username: a.username || null, memberId: a.memberId || 0, + lineBannerUrl: a.lineBannerUrl || null, lineTargetUrl: a.lineTargetUrl || null, address: a.address || null, created: a.created } : null; const USER_RE = /^[a-zA-Z0-9_]{3,20}$/; const normUser = u => String(u || '').trim().toLowerCase(); @@ -98,6 +99,18 @@ const J = { this.save(); return { ok: true, account: pub(acct) }; }, + async setLineBanner(e, bannerUrl, targetUrl) { + const acct = this.db.byEmail[e]; + if (!acct) return { error: 'No such account.' }; + acct.lineBannerUrl = bannerUrl || null; + acct.lineTargetUrl = targetUrl || null; + this.save(); + return { ok: true, account: pub(acct) }; + }, + async byMemberId(id) { + const a = Object.values(this.db.byEmail).find(x => x.memberId === Number(id)); + return a ? pub(a) : null; + }, async setMemberId(e, id) { const acct = this.db.byEmail[e]; if (acct && acct.memberId !== id) { acct.memberId = id; this.save(); } @@ -131,7 +144,9 @@ const J = { // ---- MySQL mode ---- const rowPub = r => r ? pub({ email: r.email, sponsorRef: r.sponsor_ref, code: r.code, - username: r.username, memberId: r.member_id || 0, address: r.address, created: Number(r.created) }) : null; + username: r.username, memberId: r.member_id || 0, + lineBannerUrl: r.line_banner_url, lineTargetUrl: r.line_target_url, + address: r.address, created: Number(r.created) }) : null; const D = { async signup(e, password, ref) { const code = newCode(); @@ -176,6 +191,14 @@ const D = { } return { ok: true, account: await this.byEmail(e) }; }, + async setLineBanner(e, bannerUrl, targetUrl) { + await db.q('UPDATE accounts SET line_banner_url=?, line_target_url=? WHERE email=?', [bannerUrl || null, targetUrl || null, e]); + return { ok: true, account: await this.byEmail(e) }; + }, + async byMemberId(id) { + const r = await db.q('SELECT * FROM accounts WHERE member_id=?', [Number(id)]); + return rowPub(r[0]); + }, async setMemberId(e, id) { await db.q('UPDATE accounts SET member_id=? WHERE email=? AND (member_id IS NULL OR member_id<>?)', [id, e, id]); }, async namesForMembers(ids) { if (!ids.length) return {}; @@ -246,4 +269,6 @@ async function linkWallet(email, address) { async function count() { return impl().count(); } module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername, - setUsername, setMemberId, namesForMembers, listByReferrer, linkWallet, count }; + setUsername, setMemberId, namesForMembers, listByReferrer, linkWallet, count, + setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t), + byMemberId: id => impl().byMemberId(id) }; diff --git a/ads.js b/ads.js index 01546adb4dc0bd0dd71be6756aad63aa93a14a49..915b9e262443c5305b3d63ef0667fc743b1dd959 100644 GIT binary patch delta 1792 zcmcIk&u<$=6jsuMn@A8)OQ~p!o-L@?vEwDChsd!VlF(AAQYxB2)Y?un-JM+zS?{W`q!$UqsY)qJ~BL7qj(=YRyNp38wD#X`TH3&^MaTq6%vG$L{D2xuTti3irP z&|T(IZloHz1NoqKq(@?D96Az&deWBpPySzff&U5Mn6$swGctR4ZTg!l3wtzhc$a=42o9EO z(Jo;@+ERiNcP?%ur$e0`>>Rp_ZR49%sV7zz^l#UvPAL)T4_DsR(8#^=O^}|MHGMm+8`P@{>#w`qgZG3y-BJ48BkX9A4+5K|* za#zU`wt=2&UmAXX=ZDGH$FtZdYeVW{`}+Y-2KGA67jcog6EJ57hu&aa&s?$XFLkB#(j}*iGE)hHHJm4Q2;d*;>Nk3gZufvr~8g9>ww4np_ zCD^=v_10BjE-shX-iNqy={j*r-?)A8%v#jFM}@`QLJ4R)Zb`j;|J)lL68ed;qD(xG zI%xAPz9->G+g?^LG#|W{`n@4dKa7mkzcjP2rq|U0@SQ4P1JL2)_QT0n%*c+{N&j9t zyS8dPN4 — a shareable page showing their line ladder with their join link. Free viral traffic that compounds as the team grows; no credits spent. - Ad packages: Micro $5/500 credits, Activation $20/2,000, Builder $50/5,500, Growth $100/12,000, Leader $250/32,500. Dollar-priced, settled in POL (Polygon) at the live Chainlink rate. 1 credit = 1 cent of ad delivery. - Live formats: display banners (per impression), text ads (per impression), full-screen LOGIN ADS (per day: right after a member signs in they land on a sponsor interstitial — they click "Open Ad", the advertiser's page opens in a NEW tab, a countdown runs on the interstitial, and at zero a "Go to dashboard" button appears. Just a CTA link is enough; an optional banner image can be the clickable creative. No framing requirement since it opens in its own tab), and solo ads. Coming: featured rotation with disclosed rotation size, verified-visit packs. - Members EARN credits by attention: in the Earn credits section of Members, each ad in the daily set opens FULL SCREEN in its own tab, showing the advertiser's real website. A countdown runs while you watch (it pauses if you leave the tab), then a quick human check (click the named icon) must be passed before the view counts. Finish the daily set, claim a small daily credit batch. Earned credits spend on banner and text campaigns; attention earns advertising, referrals earn money, and viewer rewards are never cash. Advertisers get real, verified visits to their site. diff --git a/db.js b/db.js index 1692a3d..3cfa815 100644 --- a/db.js +++ b/db.js @@ -79,6 +79,8 @@ async function bootstrap() { await alterSafe('ALTER TABLE accounts ADD UNIQUE KEY uq_username (username)'); await alterSafe('ALTER TABLE accounts ADD COLUMN member_id INT NULL'); await alterSafe('ALTER TABLE accounts ADD KEY idx_member (member_id)'); + await alterSafe('ALTER TABLE accounts ADD COLUMN line_banner_url VARCHAR(500) NULL'); + await alterSafe('ALTER TABLE accounts ADD COLUMN line_target_url VARCHAR(500) NULL'); await q(`CREATE TABLE IF NOT EXISTS daily_views ( email VARCHAR(190) NOT NULL, day CHAR(10) NOT NULL, @@ -93,11 +95,13 @@ async function bootstrap() { email VARCHAR(190) NOT NULL, delivered BIGINT NOT NULL, read_ts BIGINT NULL, + visited_ts BIGINT NULL, rewarded TINYINT NOT NULL DEFAULT 0, rewarded_day CHAR(10) NULL, UNIQUE KEY uq_solo (campaign_id, email), INDEX (email), INDEX (email, rewarded, rewarded_day) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); + await alterSafe('ALTER TABLE solo_inbox ADD COLUMN visited_ts BIGINT NULL'); // solo message bodies are sanitized rich text; TEXT gives them room await alterSafe('ALTER TABLE campaigns MODIFY body TEXT NULL'); await alterSafe('ALTER TABLE campaigns ADD COLUMN cta_label VARCHAR(40) NULL'); diff --git a/public/assets/my.js b/public/assets/my.js index c902035..e227625 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -26,6 +26,25 @@ 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.'; } + // milestone stepper under "Your next move": lit nodes for what's done, + // amber glow on the current target — the same ladder the contract pays + function renderSteps(d) { + const el = $('ncSteps'); + if (!el) return; + const b = d.buyerCount || 0; + const steps = [ + { label: 'Joined', sub: 'free account', hit: true }, + { label: 'Payouts on', sub: 'wallet linked', hit: !!d.memberId }, + { label: 'First buyer', sub: '50% of every pack', hit: b >= 1 }, + { label: 'Level 2', sub: '2 buyers · +20%', hit: b >= 2 }, + { label: 'Level 3', sub: '5 buyers · +10%', hit: b >= 5 } + ]; + const cur = steps.findIndex(s => !s.hit); + el.innerHTML = steps.map((s, i) => + '
' + + '' + (s.hit ? '✓' : i + 1) + '' + + '' + s.label + '' + s.sub + '
').join(''); + } // ── overview v3 charts: hand-rolled SVG/CSS, real data only ── const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const CH = { mint: '#43e8c3', cyan: '#54ccff', violet: '#9d7dff', amber: '#ffb238', track: 'rgba(139,166,156,.18)' }; @@ -129,7 +148,7 @@ setInboxBadge(d.inboxUnread || 0); loadCharts(d); $('nextMove').textContent = nextMove(d); - $('qualFill').style.width = Math.min(100, (d.buyerCount || 0) * 20) + '%'; + renderSteps(d); const wrap = $('rosterWrap'); if ((d.referrals || []).length) { $('rosterEmpty').hidden = true; @@ -167,6 +186,17 @@ // ready-to-send share message + promo tools, personalized const link = location.origin + '/join/' + (d.username || d.refCode || d.memberId || ''); fillPromo(link); + if (d.username) { // wall link rides the username + const wl = location.origin + '/wall/' + d.username; + $('wallLine').textContent = wl; + $('wallCopy').hidden = false; + $('wallOpen').hidden = false; + $('wallOpen').href = '/wall/' + d.username; + $('wallCopy').onclick = async () => { + try { await navigator.clipboard.writeText(wl); IAP.status('Wall link copied.', 'ok'); } + catch (e) { IAP.status('Copy failed. Select the link text instead.', 'bad'); } + }; + } 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. ' @@ -197,6 +227,7 @@ b.classList.toggle('on', b.dataset.pane === name)); if ($('boTitle')) $('boTitle').textContent = TITLES[name]; if (name === 'inbox') loadInbox(); + if (name === 'profile') loadLineBanner(); document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name); } @@ -332,7 +363,6 @@ $('cBudget').addEventListener('input', soloHint); // rich solo editor: small toolbar over contenteditable (CSP allows no external editor); // the server whitelist-sanitizes whatever HTML arrives, this is just authoring comfort - let soloMedia = null; // { url, type } from /api/my/upload document.querySelectorAll('.ed-bar [data-cmd]').forEach(btn => btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand(btn.dataset.cmd, false, null); })); document.querySelectorAll('.ed-bar [data-block]').forEach(btn => @@ -343,32 +373,34 @@ $('cSoloEd').focus(); document.execCommand('createLink', false, url); }); - $('edAttachBtn').addEventListener('click', () => $('cSoloFile').click()); + // inline media: upload, then drop the element at the cursor (BV-style) + let mediaMode = 'image'; + function insertHtmlAtCursor(html) { + const ed = $('cSoloEd'); + ed.focus(); + if (!document.execCommand('insertHTML', false, html)) ed.insertAdjacentHTML('beforeend', html); + } + $('edImgBtn').addEventListener('click', () => { mediaMode = 'image'; $('cSoloFile').accept = 'image/png,image/jpeg,image/webp,image/gif'; $('cSoloFile').click(); }); + $('edVidBtn').addEventListener('click', () => { mediaMode = 'video'; $('cSoloFile').accept = 'video/mp4,video/webm'; $('cSoloFile').click(); }); $('cSoloFile').addEventListener('change', async () => { const f = $('cSoloFile').files[0]; if (!f) return; $('edMediaInfo').textContent = 'Uploading ' + f.name + '…'; try { - const r = await (await fetch('/api/my/upload', { method: 'POST', - headers: { 'Content-Type': f.type }, body: f })).json(); + const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); if (r.error) { $('edMediaInfo').textContent = r.error; $('cSoloFile').value = ''; return; } - soloMedia = r; - $('edMediaInfo').textContent = f.name + ' attached'; - $('edMediaRemove').hidden = false; - const pv = $('edMediaPrev'); - pv.hidden = false; - pv.innerHTML = r.type === 'video' - ? '' - : 'attachment preview'; + insertHtmlAtCursor(r.type === 'video' + ? '


' + : '


'); + $('edMediaInfo').textContent = f.name + ' inserted'; } catch (e) { $('edMediaInfo').textContent = 'Upload failed. Try again.'; } $('cSoloFile').value = ''; }); - $('edMediaRemove').addEventListener('click', () => { - soloMedia = null; - $('edMediaInfo').textContent = ''; - $('edMediaRemove').hidden = true; - $('edMediaPrev').hidden = true; - $('edMediaPrev').innerHTML = ''; + // raw-text toggle: swap the WYSIWYG surface for the underlying HTML and back + $('edRawBtn').addEventListener('click', () => { + const ed = $('cSoloEd'), raw = $('cSoloRaw'); + if (raw.hidden) { raw.value = ed.innerHTML; raw.hidden = false; ed.hidden = true; $('edRawBtn').textContent = 'Visual'; } + else { ed.innerHTML = raw.value; ed.hidden = false; raw.hidden = true; $('edRawBtn').textContent = 'Raw text'; } }); $('cType').addEventListener('change', () => { const t = $('cType').value; @@ -381,17 +413,19 @@ soloHint(); }); $('createCampBtn').addEventListener('click', busy2($('createCampBtn'), async () => { + // in raw mode the source of truth is the textarea; sync it back first + let soloBody = $('cSoloEd').innerHTML; + if ($('cSoloRaw') && !$('cSoloRaw').hidden) soloBody = $('cSoloRaw').value; await api('/api/my/campaigns', { type: $('cType').value, name: $('cName').value, targetUrl: $('cTarget').value, imageUrl: $('cImage').value, title: $('cTitle').value, - body: $('cType').value === 'solo' ? $('cSoloEd').innerHTML : $('cBody').value, - mediaUrl: $('cType').value === 'solo' && soloMedia ? soloMedia.url : '', + body: $('cType').value === 'solo' ? soloBody : $('cBody').value, ctaLabel: $('cCtaLabel').value, budget: Number($('cBudget').value) }); IAP.status('Campaign is live. It starts serving right away.', 'ok'); $('cName').value = ''; $('cBudget').value = ''; - $('cSoloEd').innerHTML = ''; $('cCtaLabel').value = ''; - if ($('edMediaRemove') && !$('edMediaRemove').hidden) $('edMediaRemove').click(); + $('cSoloEd').innerHTML = ''; $('cSoloRaw').value = ''; $('cCtaLabel').value = ''; + $('edMediaInfo').textContent = ''; await loadCampaigns(); })); // defers the busy() lookup to click time (busy is declared below) @@ -446,29 +480,49 @@ : r.mediaType === 'video' ? '' : 'attachment'; - $('ibVisit').href = r.url; - $('ibVisit').textContent = r.ctaLabel || 'Learn more'; + // the read reward needs BOTH the dwell AND an actual click-through to the + // advertiser — the visit is what makes the ad worth the sender's credits + const visit = $('ibVisit'); + visit.href = r.url; + visit.textContent = r.ctaLabel || 'Learn more'; + visit.target = '_blank'; const btn = $('ibClaimBtn'); clearInterval(ibTimer); if (r.rewarded) { btn.hidden = true; + visit.classList.remove('cta-need'); $('ibHint').textContent = 'Read reward already claimed for this one.'; return; } + let dwellDone = false; + let visited = !!r.visited; btn.hidden = false; btn.disabled = true; + visit.classList.toggle('cta-need', !visited); let left = r.dwell; - btn.textContent = 'Read it — claim in ' + left + 's'; - $('ibHint').textContent = 'Stay on this tab while you read; the claim unlocks when the timer is done.'; + const refresh = () => { + if (!dwellDone) { btn.textContent = 'Read it — claim in ' + left + 's'; return; } + if (!visited) { btn.textContent = 'Claim +' + r.reward + ' — visit the ad first'; btn.disabled = true; return; } + btn.textContent = 'Claim +' + r.reward + ' credits'; btn.disabled = false; + }; + $('ibHint').textContent = 'Read the message, click through to the advertiser, then claim your credits.'; + refresh(); // countdown pauses off-tab; the server separately enforces the dwell on its own clock ibTimer = setInterval(() => { if (document.visibilityState !== 'visible' || !document.hasFocus()) return; left -= 1; - if (left > 0) { btn.textContent = 'Read it — claim in ' + left + 's'; return; } + if (left > 0) { refresh(); return; } clearInterval(ibTimer); - btn.disabled = false; - btn.textContent = 'Claim +' + r.reward + ' credits'; + dwellDone = true; + refresh(); }, 1000); + // clicking the CTA records the visit (and counts the advertiser's click) + visit.onclick = async () => { + visited = true; + visit.classList.remove('cta-need'); + try { await fetch('/api/my/inbox/' + id + '/visit', { method: 'POST' }); } catch (e2) {} + refresh(); + }; btn.onclick = async () => { try { const c = await api('/api/my/inbox/' + id + '/claim'); @@ -644,7 +698,7 @@ $('mcVerifyBtn').addEventListener('click', busy($('mcVerifyBtn'), async () => { await api('/api/auth/email/verify', { email: $('mcEmail').value, code: $('mcCode').value }); IAP.status('You are in.', 'ok'); - await showLoginAd(); + if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); })); })(); @@ -657,7 +711,7 @@ $('loginBtn').addEventListener('click', busy($('loginBtn'), async () => { await api('/api/login', { email: $('liEmail').value, password: $('liPass').value }); IAP.status('Logged in.', 'ok'); - await showLoginAd(); + if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); })); $('walletSigninLink').addEventListener('click', async e => { @@ -666,7 +720,7 @@ IAP.status('Check your wallet for the free sign-in signature…'); await IAPWallet.signIn(); IAP.status('Signed in with your wallet.', 'ok'); - await showLoginAd(); + if (!(await showGauntlet())) await showLoginAd(); // welcome tour outranks the login ad await render(); } catch (err) { IAP.status((err && err.message) || String(err), 'bad'); } }); @@ -695,6 +749,87 @@ await render(); }); + // ── welcome tour (viral banner gauntlet): a new member meets their 3-level + // upline's sites, 10 focus-paused seconds each, then unlocks welcome credits. + // Same three levels the contract pays — the tour IS the org chart. + async function showGauntlet() { + try { + const g = await (await fetch('/api/my/gauntlet')).json(); + if (!g.pending || !g.slides || !g.slides.length) return false; + const gate = $('gauntGate'); + gate.hidden = false; + for (let i = 0; i < g.slides.length; i++) { + const s = g.slides[i]; + $('ggWho').textContent = 'Position ' + (i + 1) + ': ' + s.name + (i === 0 ? ' — the person who invited you' : ''); + $('ggProgress').textContent = 'Meeting your line: ' + (i + 1) + ' of ' + g.slides.length; + $('ggFrame').src = s.targetUrl; + let left = g.dwell || 10; + $('ggTimer').textContent = left + 's'; + await new Promise(done => { + const t = setInterval(() => { + if (document.visibilityState !== 'visible' || !document.hasFocus()) return; + left -= 1; + $('ggTimer').textContent = Math.max(0, left) + 's'; + if (left <= 0) { clearInterval(t); done(); } + }, 1000); + }); + } + $('ggFrame').src = 'about:blank'; + $('ggTimer').textContent = '✓'; + $('ggWho').textContent = 'That is your line. When you grow, they earn — and yours starts the day you share.'; + $('ggClaim').hidden = false; + await new Promise(done => { + $('ggClaim').onclick = async () => { + try { + const r = await api('/api/my/gauntlet/complete', { token: g.token }); + IAP.status('+' + r.credited + ' welcome credits unlocked. They spend on real campaigns.', 'ok'); + } catch (e) { IAP.status(e.message, 'bad'); } + done(); + }; + }); + gate.hidden = true; + $('ggClaim').hidden = true; + return true; + } catch (e) { return false; } + } + + // ── line banner (profile): the member's slot on welcome tours + their wall ── + function fillLineBanner(a) { + if (!a) return; + if (a.lineTargetUrl) $('lbTarget').value = a.lineTargetUrl; + if (a.lineBannerUrl) { + $('lbBanner').value = a.lineBannerUrl; + $('lbPreview').hidden = false; + $('lbPreview').innerHTML = 'line banner'; + } + $('lbCurrent').textContent = a.lineTargetUrl + ? 'Live: your next three levels meet ' + a.lineTargetUrl + ' on their welcome tour.' + : 'Not set yet. Until you set one, your tour slot is skipped.'; + } + async function loadLineBanner() { + try { fillLineBanner(await (await fetch('/api/me')).json()); } catch (e) {} + } + $('lbUploadBtn').addEventListener('click', () => $('lbFile').click()); + $('lbFile').addEventListener('change', async () => { + const f = $('lbFile').files[0]; + if (!f) return; + $('lbUpInfo').textContent = 'Uploading…'; + try { + const r = await (await fetch('/api/my/upload', { method: 'POST', headers: { 'Content-Type': f.type }, body: f })).json(); + if (r.error) { $('lbUpInfo').textContent = r.error; $('lbFile').value = ''; return; } + $('lbBanner').value = r.url; + $('lbUpInfo').textContent = 'Uploaded.'; + $('lbPreview').hidden = false; + $('lbPreview').innerHTML = 'line banner'; + } catch (e) { $('lbUpInfo').textContent = 'Upload failed. Try again.'; } + $('lbFile').value = ''; + }); + $('lbSaveBtn').addEventListener('click', busy2($('lbSaveBtn'), async () => { + const r = await api('/api/my/linebanner', { bannerUrl: $('lbBanner').value, targetUrl: $('lbTarget').value }); + IAP.status('Line banner saved. Your next three levels will meet it.', 'ok'); + if (r.account) fillLineBanner(r.account); + })); + // ── login ad interstitial (ClickBaitPays pattern): after a successful // sign-in the sponsor card appears; "Open Ad" opens the CTA link in a NEW // tab (a real, counted click) while the timer counts down on THIS page — diff --git a/public/assets/site.css b/public/assets/site.css index 4db8298..42d404f 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -259,6 +259,30 @@ textarea{resize:vertical;font:inherit} #boBurger{display:block} .bo-content{padding:18px} } +/* ── "Your next move": milestone stepper card ── */ +.next-card{position:relative;overflow:hidden;border-color:rgba(67,232,195,.28)} +.next-card::before{content:"";position:absolute;inset:0;pointer-events:none;background: + radial-gradient(420px 150px at 10% 0%,rgba(67,232,195,.12),transparent 70%), + radial-gradient(360px 130px at 90% 100%,rgba(157,125,255,.09),transparent 70%)} +.nc-head{display:flex;gap:14px;align-items:flex-start;position:relative} +.nc-head .pl{flex:0 0 auto;width:44px;height:44px;border-radius:12px;display:grid;place-items:center; + background:rgba(67,232,195,.12);border:1px solid rgba(67,232,195,.35)} +.nc-head .pl svg{width:20px;height:20px;stroke:var(--mint);fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round} +.nc-steps{display:flex;margin-top:20px;position:relative;flex-wrap:wrap;row-gap:14px} +.nc-step{flex:1;min-width:88px;display:flex;flex-direction:column;align-items:center;gap:7px; + position:relative;text-align:center;padding:0 4px} +.nc-step::before{content:"";position:absolute;top:13px;left:-50%;width:100%;height:2px;background:var(--line-strong)} +.nc-step:first-child::before{display:none} +.nc-step.hit::before{background:linear-gradient(90deg,var(--mint),var(--mint-hi))} +.nc-step .dot{width:27px;height:27px;border-radius:50%;display:grid;place-items:center;font-size:12px; + font-weight:800;background:var(--panel-solid);border:2px solid var(--line-strong);color:var(--muted); + position:relative;z-index:1} +.nc-step.hit .dot{background:var(--mint);border-color:var(--mint);color:var(--mint-ink); + box-shadow:0 0 14px rgba(67,232,195,.35)} +.nc-step.cur .dot{border-color:var(--amber);color:var(--amber);box-shadow:0 0 12px rgba(255,178,56,.4)} +.nc-step .lb{font-size:12px;font-weight:700;line-height:1.25} +.nc-step .lb i{display:block;font-style:normal;font-weight:500;font-size:10.5px;color:var(--muted);margin-top:2px} +.nc-step.cur .lb{color:var(--amber)} /* ── overview v3: stat cards w/ plates+chips, hand-rolled charts ── */ .statx{display:flex;gap:14px;align-items:flex-start;background:var(--panel);border:1px solid var(--line); border-radius:var(--radius);padding:18px;position:relative;overflow:hidden} @@ -310,6 +334,13 @@ textarea{resize:vertical;font:inherit} .lgate-card img{max-width:100%;max-height:50vh;border-radius:12px;border:1px solid var(--line-strong)} #lgCreative .lg-linkcard{display:inline-block;background:var(--panel);border:1px solid var(--line-strong); border-radius:14px;padding:22px 28px;font-family:var(--disp);font-weight:700;font-size:18px;overflow-wrap:anywhere} +/* ── welcome tour (gauntlet): framed line sites + countdown ── */ +.lgate-frame{flex:1;border:0;width:100%;background:#fff;min-height:0} +.ggmeta{padding:8px 16px;background:var(--panel-solid);border-bottom:1px solid var(--line)} +/* ── wall page ── */ +.wall-card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);padding:16px;text-align:center} +.wall-card img{max-width:100%;border-radius:10px;border:1px solid var(--line-strong)} +.wall-pos{font-family:var(--mono);font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px} /* ── solo composer: toolbar + contenteditable editor ── */ .ed-bar{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px} .ed-bar button{background:var(--panel);color:var(--ink);border:1px solid var(--line-strong);border-radius:8px; @@ -319,6 +350,10 @@ textarea{resize:vertical;font:inherit} min-height:180px;padding:12px 14px;outline:none;overflow-wrap:anywhere} .ed-body:focus{border-color:var(--mint)} .ed-body:empty::before{content:attr(data-ph);color:var(--muted)} +.ed-body img,.ed-body video,.ib-rich img,.ib-rich video{max-width:100%;border-radius:10px;margin:6px 0;display:block} +.ed-sep{width:1px;align-self:stretch;background:var(--line-strong);margin:2px 3px} +.ed-bar sub{font-size:9px} +textarea.ed-body{width:100%;min-height:180px;resize:vertical} .ed-body a,.ib-rich a{color:var(--mint)} .ed-body h3,.ib-rich h3,.ed-body h4,.ib-rich h4{margin:.5em 0 .3em} .ed-media{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:10px 0 0} @@ -331,6 +366,9 @@ textarea{resize:vertical;font:inherit} .ib-row .sub{font-weight:700;flex:1;min-width:160px;overflow-wrap:anywhere} .ib-row.unread .sub{color:var(--mint)} .ib-row .from,.ib-row .when{font-size:12px;color:var(--muted);white-space:nowrap} +.cta-need{box-shadow:0 0 0 2px var(--amber),0 0 16px rgba(255,178,56,.4)!important;animation:ctapulse 1.6s ease-in-out infinite} +@keyframes ctapulse{50%{box-shadow:0 0 0 2px var(--amber),0 0 24px rgba(255,178,56,.65)!important}} +@media (prefers-reduced-motion:reduce){.cta-need{animation:none}} /* ── back-office accent family: green leads, cyan/violet/amber season the cards ── */ .bo .stats .stat:nth-child(2) .n{color:var(--cyan)} .bo .stats .stat:nth-child(2)::before{background:linear-gradient(90deg,transparent,var(--cyan),transparent)} diff --git a/public/assets/wall.js b/public/assets/wall.js new file mode 100644 index 0000000..278c9b3 --- /dev/null +++ b/public/assets/wall.js @@ -0,0 +1,33 @@ +// Public banner wall: a member's line banner plus their upline ladder, with +// their join link. The viral surface: members send traffic here, every visit +// puts eyes on the whole line. +(async function () { + await IAP.renderNav(''); + const name = location.pathname.split('/').pop(); + let w = null; + try { w = await (await fetch('/api/wall/' + encodeURIComponent(name))).json(); } catch (e) {} + const title = IAP.$('wallTitle'); + if (!w || w.error) { + title.textContent = 'No wall under that name.'; + IAP.$('wallJoin').href = '/my'; + return; + } + title.innerHTML = 'The ' + String(w.name).replace(/[&<>]/g, '') + ' line'; + IAP.$('wallCtaHead').textContent = 'Join ' + w.name + '’s line'; + IAP.$('wallJoin').href = w.joinUrl; + const grid = IAP.$('wallGrid'); + grid.innerHTML = ''; + w.ladder.forEach((m, i) => { + const d = document.createElement('div'); + d.className = 'wall-card'; + const safe = String(m.name || 'member').replace(/[&<>]/g, ''); + d.innerHTML = '
Position ' + (i + 1) + (i === 0 ? ' · this wall' : '') + '
' + + (m.bannerUrl + ? '' + safe + ' banner' + : m.targetUrl + ? '' + safe + '
visit their site
' + : '' + safe + '
banner slot open') + + '
' + safe + '
'; + grid.appendChild(d); + }); +})(); diff --git a/public/contract.html b/public/contract.html index 168b080..dd19390 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 93a6996..9f33a4f 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ InstantAdPay: advertise and earn, locked in code - + @@ -260,6 +260,7 @@
  • A member account and the live ledger
  • Welcome credits to taste real ad delivery
  • +
  • Your line banner, shown to your next three levels as they join
  • Your personal referral link, working from day one
  • Earnings from day one on your referrals' package purchases
  • Access to the member dashboard
  • @@ -409,9 +410,9 @@ - - - - + + + + diff --git a/public/ledger.html b/public/ledger.html index 0f6acef..a334663 100644 --- a/public/ledger.html +++ b/public/ledger.html @@ -5,7 +5,7 @@ Live ledger | InstantAdPay - +
    @@ -25,8 +25,8 @@
    - - - + + + diff --git a/public/my.html b/public/my.html index 9d77c02..52e3515 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ Member area | InstantAdPay - + @@ -67,6 +67,21 @@ + + + -
    -

    Your next move

    -

    …

    -
    -
    - 2 · level 2 - 5 · level 3 +
    +
    +
    +
    +

    Your next move

    +

    …

    +
    @@ -247,24 +262,29 @@ - - + + + + + - - + + + + + + + +
    -

    + + + +

    -

    - - - - -

    -
    @@ -350,6 +370,15 @@

    Email swipe

    +
    +

    Your banner wall

    +

    A public page showing your line banner and your upline ladder, with your + join link front and center. Every visit puts eyes on your whole line — send traffic here and + the wall does the pitching.

    +

    Set a username in Profile to claim your wall.

    +

    +

    +

    Banners

    Branded InstantAdPay banner sets are in production. They will land here @@ -366,6 +395,20 @@

    +
    +

    Your line banner

    +

    Set a destination link (and a banner image for your wall), and every new + member joining your line meets it on their welcome tour: position 1 for your directs, position 2 + for their people, position 3 below that — the same three levels the contract pays you on.

    +

    +

    +

    + +

    + +

    +

    +

    Account details

    …

    @@ -399,9 +442,9 @@
    - - - - + + + + diff --git a/public/tx.html b/public/tx.html index 7eef6b3..12fb27e 100644 --- a/public/tx.html +++ b/public/tx.html @@ -4,7 +4,7 @@ Transaction | InstantAdPay - + @@ -33,7 +33,7 @@

    ← Back to the live ledger · Read the contract review

    - - + + diff --git a/public/view.html b/public/view.html index 88ecc26..5643b90 100644 --- a/public/view.html +++ b/public/view.html @@ -4,7 +4,7 @@ Viewing ad — InstantAdPay - +