From 2cc2855488a8d6e9019f3401333f4e7f66f329f7 Mon Sep 17 00:00:00 2001 From: martbost Date: Sat, 5 Sep 2026 15:38:46 -0500 Subject: [PATCH] Rich solo composer: WYSIWYG editor, sanitized HTML bodies, media uploads, CTA labels Co-Authored-By: Claude Fable 5 --- ads.js | Bin 28608 -> 30784 bytes chatbot.js | 4 +-- db.js | 5 ++-- public/assets/my.js | 55 +++++++++++++++++++++++++++++++++++++++-- public/assets/site.css | 12 +++++++++ public/my.html | 38 ++++++++++++++++++++++------ public/view.html | 4 +-- server.js | 48 ++++++++++++++++++++++++++++++++++- 8 files changed, 150 insertions(+), 16 deletions(-) diff --git a/ads.js b/ads.js index 6c2d74ed9cebda15d78eaf9dbc33f0f6447ffc82..966a09350cd9de7a8e3a9cdf933d64eee6cde841 100644 GIT binary patch delta 2245 zcma)7PjB2r6i<;_+R*k=L5onOR~KS?v%5~xUPyKmRj2}qh?Hz{VRsYFdMCEY*ke57 zZqjb#@)^vP?|_smhn~3e1weueN4^2PH}+;zRY>e%)-&_|{C@A>y#D>l>%Xu3bo*N2 zd7z|_Fp}elD_|Lin8zcYwOFo0nej1XkspHQCmJ%Ij5!@e3GD2B@pQMARrVs<8D(;! zc&6525=NSfNa>bhNu;9}9Pr{f8dcGl!ym7Hfa63?63_(;6v#xJHX)I~w9cYYuDK$6 z4nOgEsv*=mb(9NX#*b7JL5%W9Xz&GRiQ5E@c27a+jGY2Y0>Kdo^N+a(u6&mA@uDvi zrJ=X8y>qztk9~aqaXAdt^7kMs1k{^cB&E?0PW(55)tlKny|XIva)hXqVu5D>JQYQH*9RRTAQ`} z#)Xe#&Qs_X&>zmvv^?esW^0Vz5yRYFXP_F+z}s@^zt37Pp0vJMX?;3eb{%JHy??N2 z*~6*TKd^=kYijoo>|w)p8?N!{#7)eVRJ;kAA!t zmKoN8Mf7t6hcm{O&iuPf(USC!dskGaiQ2(F-yMipE_ z=T)Z2KV=kh6{^C>R2>26f0fgWbD~@B&~0O%#Z+%+=eMr? zc{QB%rfJP6Nps!C>3CYRsX7$f5NBYQ;$tr7K0DYA!$t)PF5?`iWrLp%jmPr-&E2cU zP&v7|f0t5VcI`Skn?NmHet-SW{+HCOI;7dG6bm@yG>FYT_#wl2r*ZItJTh~-ZNphr zW);{tN2K*MsrbZv9I^82>+hHU+_+Z6`I}Ajz&jXtc`76utAR)DPQCte?eW>5?W9RC z{WcAz6Fy3(L3A{Y)5o-l-H*Mhh|ye?2NpJ>hKJwY;2OjKl%!E<~T zB%Wn5530M$J}LWQ#rX!zo-acPU#c;_VHc(kbCmO?$m7HmMpg2ZGGDW8 zHsWNZr|+0CTLorzQU3bwz4soin4V2?Ae z3FD+MSD)Q4UwPNhe|-2`vq3@zxItvF)_RpSK)VJZl+Epp^PivbD}|9lqU&(duGrPq zn^**xy}i?v&+oRt@h~}W<)YR#GsOhibgsdI<73^xrB{u9Gwx*7U(}4|mEr#3?%F$7 ojnfit-zzTruX#~I{tLofeS*B$8iRqL-APQmL1dnwMUZp Inbox). Cost 5 credits per guaranteed delivery, minimum 10 deliveries (50 credits). Each member receives a given solo at most once, and never the sender's own. Readers earn 2 credits per real read (10-second dwell on the open message, up to 5 rewarded reads/day) — claimed right from the message. Compose one in Campaigns > Solo ad. +- Onsite SOLO ADS are live: a solo ad is a full message (subject + up to 2000 characters of formatted text + your link) delivered into members' on-site Inbox (Members > Inbox). The composer in Campaigns > Solo ad has a rich-text editor (bold, headings, lists, links), lets you ATTACH one image (PNG/JPG/WebP/GIF, up to 3MB) or one video (MP4/WebM, up to 25MB), and adds a call-to-action button with a custom label that opens the target URL. Cost 5 credits per guaranteed delivery, minimum 10 deliveries (50 credits). Each member receives a given solo at most once, and never the sender's own. Readers earn 2 credits per real read (10-second dwell on the open message, up to 5 rewarded reads/day) — claimed right from the message. Compose one in Campaigns > Solo ad. - Campaign target URLs are checked the moment they are submitted: the page must be reachable and must ALLOW framing (no X-Frame-Options deny/sameorigin, no blocking CSP frame-ancestors), because surf views show the real site full screen. Frame-blocking or dead URLs are rejected with the exact reason; the fix is a landing page that allows framing. Login-ad targets skip the frame check (they are click-through only). - Every purchase is split by an immutable smart contract in the same transaction: 50% direct sponsor, 20% level 2, 10% level 3, 20% platform. No withdrawals exist; money lands in members' own wallets instantly. - Qualification: level 1 open to all; 2 buyers of $20+ unlock level 2; 5 unlock level 3. Unqualified shares pass up the sponsor line, checking up to 25 positions, else the platform receives them. Qualification cannot be bought and never expires. diff --git a/db.js b/db.js index 7ca1e65..1692a3d 100644 --- a/db.js +++ b/db.js @@ -98,8 +98,9 @@ async function bootstrap() { UNIQUE KEY uq_solo (campaign_id, email), INDEX (email), INDEX (email, rewarded, rewarded_day) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); - // solo message bodies run long; widen the shared campaigns.body column - await alterSafe('ALTER TABLE campaigns MODIFY body VARCHAR(1200) 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'); await q(`CREATE TABLE IF NOT EXISTS burns ( id VARCHAR(32) PRIMARY KEY, member_id INT NOT NULL, diff --git a/public/assets/my.js b/public/assets/my.js index 77a7f5e..c27d5cc 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -330,6 +330,46 @@ + '. Readers earn ' + (lastRates.soloReadCredits || 2) + ' credits for a real read, so your message gets opened.'; } $('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 => + btn.addEventListener('click', () => { $('cSoloEd').focus(); document.execCommand('formatBlock', false, btn.dataset.block); })); + $('edLinkBtn').addEventListener('click', () => { + const url = prompt('Link URL (https://…)'); + if (!url) return; + $('cSoloEd').focus(); + document.execCommand('createLink', false, url); + }); + $('edAttachBtn').addEventListener('click', () => $('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(); + 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'; + } 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 = ''; + }); $('cType').addEventListener('change', () => { const t = $('cType').value; $('cImageRow').hidden = t === 'text' || t === 'solo'; @@ -344,10 +384,14 @@ await api('/api/my/campaigns', { type: $('cType').value, name: $('cName').value, targetUrl: $('cTarget').value, imageUrl: $('cImage').value, title: $('cTitle').value, - body: $('cType').value === 'solo' ? $('cSoloBody').value : $('cBody').value, + body: $('cType').value === 'solo' ? $('cSoloEd').innerHTML : $('cBody').value, + mediaUrl: $('cType').value === 'solo' && soloMedia ? soloMedia.url : '', + 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(); await loadCampaigns(); })); // defers the busy() lookup to click time (busy is declared below) @@ -395,8 +439,15 @@ $('inboxReadCard').hidden = false; $('ibSubject').textContent = r.subject || '(no subject)'; $('ibMeta').textContent = 'from ' + (r.fromName || 'a member') + ' · ' + new Date(r.delivered).toLocaleString(); - $('ibBody').textContent = r.body || ''; + $('ibBody').innerHTML = r.body || ''; // whitelist-sanitized on the server at submit + const mv = $('ibMedia'); + mv.hidden = !r.mediaUrl; + mv.innerHTML = !r.mediaUrl ? '' + : r.mediaType === 'video' + ? '' + : 'attachment'; $('ibVisit').href = r.url; + $('ibVisit').textContent = r.ctaLabel || 'Learn more'; const btn = $('ibClaimBtn'); clearInterval(ibTimer); if (r.rewarded) { diff --git a/public/assets/site.css b/public/assets/site.css index aa3f183..1c1d1d3 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -295,6 +295,18 @@ textarea{resize:vertical;font:inherit} .donut-legend{display:flex;flex-direction:column;gap:8px;font-size:13px} .donut-legend i{display:inline-block;width:10px;height:10px;border-radius:3px;margin-right:8px} .donut-center{font-family:var(--disp);font-weight:800} +/* ── 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; + padding:5px 11px;font-size:12.5px;cursor:pointer} +.ed-bar button:hover,.ed-bar button:focus-visible{border-color:var(--mint);outline:none} +.ed-body{background:rgba(4,8,7,.65);border:1px solid var(--line-strong);border-radius:11px; + 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 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} /* ── solo-ads inbox ── */ .bo-menu .pill{margin-left:auto;background:var(--amber);color:#1a1206;font-size:11px;font-weight:800; border-radius:999px;padding:1px 8px;line-height:1.5} diff --git a/public/my.html b/public/my.html index 0aa1b77..4802321 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ Member area | InstantAdPay - + @@ -222,7 +222,30 @@

- + @@ -239,7 +262,8 @@

← Back to inbox

-
+ +

Visit the advertiser

@@ -355,9 +379,9 @@ - - - - + + + + diff --git a/public/view.html b/public/view.html index e9fe57b..dad4ef7 100644 --- a/public/view.html +++ b/public/view.html @@ -4,7 +4,7 @@ Viewing ad — InstantAdPay - +