Rich solo composer: WYSIWYG editor, sanitized HTML bodies, media uploads, CTA labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+53
-2
@@ -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'
|
||||
? '<video src="' + r.url + '" controls style="max-width:320px;border-radius:10px"></video>'
|
||||
: '<img src="' + r.url + '" alt="attachment preview" style="max-width:320px;border-radius:10px">';
|
||||
} 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'
|
||||
? '<video src="' + r.mediaUrl + '" controls style="max-width:100%;border-radius:12px"></video>'
|
||||
: '<img src="' + r.mediaUrl + '" alt="attachment" style="max-width:100%;border-radius:12px">';
|
||||
$('ibVisit').href = r.url;
|
||||
$('ibVisit').textContent = r.ctaLabel || 'Learn more';
|
||||
const btn = $('ibClaimBtn');
|
||||
clearInterval(ibTimer);
|
||||
if (r.rewarded) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
+31
-7
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Member area | InstantAdPay</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905n">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905o">
|
||||
</head>
|
||||
<body class="bo-body">
|
||||
|
||||
@@ -222,7 +222,30 @@
|
||||
<p id="cImageRow"><input id="cImage" placeholder="Image URL (banner/login ads)" style="width:100%"></p>
|
||||
<p id="cTitleRow" hidden><input id="cTitle" placeholder="Headline (max 60)" style="width:100%"></p>
|
||||
<p id="cBodyRow" hidden><input id="cBody" placeholder="Ad text (max 140)" style="width:100%"></p>
|
||||
<p id="cSoloRow" hidden><textarea id="cSoloBody" placeholder="Your message (40–1000 characters). Write it like an email worth reading." rows="7" style="width:100%"></textarea></p>
|
||||
<div id="cSoloRow" hidden>
|
||||
<div class="ed-bar" aria-label="Formatting">
|
||||
<button type="button" data-cmd="bold" title="Bold"><b>B</b></button>
|
||||
<button type="button" data-cmd="italic" title="Italic"><i>I</i></button>
|
||||
<button type="button" data-cmd="underline" title="Underline"><u>U</u></button>
|
||||
<button type="button" data-block="h3" title="Heading">Heading</button>
|
||||
<button type="button" data-block="p" title="Normal text">Normal</button>
|
||||
<button type="button" data-cmd="insertUnorderedList" title="Bullet list">• List</button>
|
||||
<button type="button" data-cmd="insertOrderedList" title="Numbered list">1. List</button>
|
||||
<button type="button" id="edLinkBtn" title="Insert link">Link</button>
|
||||
<button type="button" data-cmd="removeFormat" title="Clear formatting">Clear</button>
|
||||
</div>
|
||||
<div id="cSoloEd" class="ed-body" contenteditable="true"
|
||||
data-ph="Write it like an email worth reading. Bold the promise, list the proof, link the receipts."></div>
|
||||
<p style="margin:10px 0 0"><input id="cCtaLabel" maxlength="30" style="width:100%"
|
||||
placeholder="Call-to-action button label (default: Learn more) — the button opens your target URL"></p>
|
||||
<p class="ed-media">
|
||||
<input type="file" id="cSoloFile" accept="image/png,image/jpeg,image/webp,image/gif,video/mp4,video/webm" hidden>
|
||||
<button type="button" class="btn small sec" id="edAttachBtn">Attach image or video</button>
|
||||
<span id="edMediaInfo" class="small muted"></span>
|
||||
<button type="button" class="btn small sec" id="edMediaRemove" hidden>Remove</button>
|
||||
</p>
|
||||
<div id="edMediaPrev" hidden style="margin-top:8px"></div>
|
||||
</div>
|
||||
<p class="small muted" id="cSoloHint" hidden></p>
|
||||
<button class="btn" id="createCampBtn">Launch campaign</button>
|
||||
</div>
|
||||
@@ -239,7 +262,8 @@
|
||||
<p><a href="#" id="ibBack">← Back to inbox</a></p>
|
||||
<h3 id="ibSubject"></h3>
|
||||
<p class="small muted" id="ibMeta"></p>
|
||||
<div id="ibBody" class="promo-block" style="white-space:pre-wrap"></div>
|
||||
<div id="ibMedia" hidden style="margin:12px 0"></div>
|
||||
<div id="ibBody" class="promo-block ib-rich"></div>
|
||||
<p style="margin-top:14px">
|
||||
<a class="btn" id="ibVisit" target="_blank" rel="noopener nofollow">Visit the advertiser</a>
|
||||
<button class="btn sec" id="ibClaimBtn" type="button" hidden>…</button></p>
|
||||
@@ -355,9 +379,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/common.js?v=20260905n"></script>
|
||||
<script src="/assets/wallet.js?v=20260905n"></script>
|
||||
<script src="/assets/my.js?v=20260905n"></script>
|
||||
<script src="/assets/chat.js?v=20260905n"></script>
|
||||
<script src="/assets/common.js?v=20260905o"></script>
|
||||
<script src="/assets/wallet.js?v=20260905o"></script>
|
||||
<script src="/assets/my.js?v=20260905o"></script>
|
||||
<script src="/assets/chat.js?v=20260905o"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Viewing ad — InstantAdPay</title>
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905n">
|
||||
<link rel="stylesheet" href="/assets/site.css?v=20260905o">
|
||||
<style>
|
||||
html,body{height:100%;margin:0;overflow:hidden}
|
||||
.vw{display:flex;flex-direction:column;height:100vh;height:100dvh;background:var(--bg,#04110c);color:var(--ink,#e8fff7)}
|
||||
@@ -43,6 +43,6 @@
|
||||
</div>
|
||||
<iframe class="vframe" id="vFrame" sandbox="allow-scripts allow-same-origin allow-forms allow-popups" referrerpolicy="no-referrer" title="Advertiser site"></iframe>
|
||||
</div>
|
||||
<script src="/assets/view.js?v=20260905n"></script>
|
||||
<script src="/assets/view.js?v=20260905o"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user