Paid shorts: full-screen vertical video feed over the video inventory, earn per watched short
- /shorts page: no-seek full-screen player, server-clock watch, credit + swipe to next - Reuses /api/my/videos + /api/my/videowatch (same fraud model, daily cap, self-exclusion) - Linked from Earn credits > Watch videos Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
// Paid shorts: a full-screen vertical feed over the video-ad inventory. You must
|
||||||
|
// watch each short's required time (server-clock enforced, no seek) to earn,
|
||||||
|
// then advance to the next. Reuses /api/my/videos + /api/my/videowatch.
|
||||||
|
(function () {
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const st = { token: null, secs: 0, maxSeen: 0, credited: false, done: false };
|
||||||
|
function j(url, body) {
|
||||||
|
return fetch(url, body
|
||||||
|
? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
|
||||||
|
: undefined).then(r => r.json());
|
||||||
|
}
|
||||||
|
async function load() {
|
||||||
|
st.token = null; st.maxSeen = 0; st.credited = false; st.done = false;
|
||||||
|
$('shOver').hidden = true; $('shCta').hidden = true; $('shNext').hidden = true;
|
||||||
|
let r = null;
|
||||||
|
try { r = await j('/api/my/videos'); } catch (e) {}
|
||||||
|
if (!r || r.error) { $('shVideo').hidden = true; $('shMsg').hidden = false; $('shMsg').textContent = 'Sign in on the dashboard to watch shorts and earn.'; return; }
|
||||||
|
$('shStat').textContent = 'today: ' + (r.status.count || 0) + ' / ' + r.status.cap;
|
||||||
|
if (!r.ad) {
|
||||||
|
$('shVideo').hidden = true; $('shMsg').hidden = false;
|
||||||
|
$('shMsg').textContent = r.status.left <= 0 ? 'That\'s today\'s shorts. Come back tomorrow.' : 'No shorts in rotation right now. Check back soon.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
st.token = r.token; st.secs = r.ad.watchSecs;
|
||||||
|
const v = $('shVideo');
|
||||||
|
$('shMsg').hidden = true; v.hidden = false;
|
||||||
|
v.src = r.ad.videoUrl; v.currentTime = 0;
|
||||||
|
$('shTitle').textContent = r.ad.title || '';
|
||||||
|
$('shCta').href = r.ad.ctaUrl; $('shCta').textContent = r.ad.ctaLabel || 'Learn more';
|
||||||
|
$('shOver').hidden = false; $('shCta').hidden = false;
|
||||||
|
v.onseeking = () => { if (v.currentTime > st.maxSeen + 0.5) v.currentTime = st.maxSeen; };
|
||||||
|
v.ontimeupdate = () => {
|
||||||
|
if (v.currentTime > st.maxSeen) st.maxSeen = v.currentTime;
|
||||||
|
const left = Math.max(0, Math.ceil(st.secs - st.maxSeen));
|
||||||
|
$('shTimer').textContent = left > 0 ? 'Watch ' + left + 's more to earn' : 'Earned — swipe to the next';
|
||||||
|
if (!st.done && st.maxSeen >= st.secs) { st.done = true; credit(); }
|
||||||
|
};
|
||||||
|
v.play().catch(() => {});
|
||||||
|
}
|
||||||
|
async function credit() {
|
||||||
|
if (st.credited) return; st.credited = true;
|
||||||
|
try {
|
||||||
|
const r = await j('/api/my/videowatch', { token: st.token });
|
||||||
|
if (r.credited) { $('shStat').textContent = 'today: ' + (r.status.count || 0) + ' / ' + (r.status.cap || 0) + ' · +' + r.credited; }
|
||||||
|
} catch (e) {}
|
||||||
|
$('shNext').hidden = false;
|
||||||
|
}
|
||||||
|
$('shNext').addEventListener('click', load);
|
||||||
|
// tap the video to pause/resume
|
||||||
|
$('shVideo').addEventListener('click', () => { const v = $('shVideo'); if (v.paused) v.play(); else v.pause(); });
|
||||||
|
load();
|
||||||
|
})();
|
||||||
+2
-1
@@ -394,7 +394,8 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Watch videos, earn credits</h3>
|
<h3>Watch videos, earn credits</h3>
|
||||||
<p class="muted small">Watch a member's video for its required time and earn credits. The player
|
<p class="muted small">Watch a member's video for its required time and earn credits. The player
|
||||||
can't be skipped — the timer runs on our server. You never see your own videos.</p>
|
can't be skipped — the timer runs on our server. You never see your own videos.
|
||||||
|
Prefer a feed? <a href="/shorts">Open Shorts</a> for a full-screen, swipe-to-next experience.</p>
|
||||||
<p><span class="badge" id="vidProgress">…</span></p>
|
<p><span class="badge" id="vidProgress">…</span></p>
|
||||||
<div class="card" style="background:rgba(4,8,7,.5)" id="vidStage">
|
<div class="card" style="background:rgba(4,8,7,.5)" id="vidStage">
|
||||||
<div id="vidBox" class="small muted" style="min-height:120px;display:grid;place-items:center">
|
<div id="vidBox" class="small muted" style="min-height:120px;display:grid;place-items:center">
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Shorts | 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=20260906e">
|
||||||
|
<style>
|
||||||
|
html,body{height:100%;margin:0;overflow:hidden;background:#000}
|
||||||
|
.sh{position:fixed;inset:0;display:flex;flex-direction:column;background:#000;color:var(--ink,#e8fff7)}
|
||||||
|
.sh-bar{flex:0 0 auto;display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--panel-solid,#071a12);
|
||||||
|
border-bottom:1px solid var(--line,rgba(67,232,195,.18));flex-wrap:wrap}
|
||||||
|
.sh-brand{font-family:var(--disp,sans-serif);font-weight:800;font-size:15px}
|
||||||
|
.sh-brand em{color:var(--mint,#43e8c3);font-style:normal}
|
||||||
|
.sh-stat{margin-left:auto;font-family:var(--mono,monospace);font-size:13px;color:var(--muted,#8ba69c)}
|
||||||
|
.sh-stage{flex:1;display:flex;align-items:center;justify-content:center;position:relative;min-height:0}
|
||||||
|
.sh-video{max-height:100%;max-width:min(100%,440px);width:100%;background:#000;border-radius:0}
|
||||||
|
.sh-over{position:absolute;left:0;right:0;bottom:0;padding:18px;background:linear-gradient(transparent,rgba(0,0,0,.75));
|
||||||
|
display:flex;flex-direction:column;gap:10px}
|
||||||
|
.sh-title{font-weight:700}
|
||||||
|
.sh-timer{font-family:var(--mono,monospace);font-size:13px;color:var(--amber,#ffb238)}
|
||||||
|
.sh-row{display:flex;gap:10px;flex-wrap:wrap}
|
||||||
|
.sh-msg{padding:30px;text-align:center;color:var(--muted,#8ba69c);font-size:16px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sh">
|
||||||
|
<div class="sh-bar">
|
||||||
|
<span class="sh-brand">Instant<em>AdPay</em> · Shorts</span>
|
||||||
|
<span class="sh-stat" id="shStat">…</span>
|
||||||
|
<a class="btn small sec" href="/my#earn">Done</a>
|
||||||
|
</div>
|
||||||
|
<div class="sh-stage" id="shStage">
|
||||||
|
<div class="sh-msg" id="shMsg">Loading your first short…</div>
|
||||||
|
<video class="sh-video" id="shVideo" playsinline hidden></video>
|
||||||
|
<div class="sh-over" id="shOver" hidden>
|
||||||
|
<div class="sh-title" id="shTitle"></div>
|
||||||
|
<div class="sh-timer" id="shTimer"></div>
|
||||||
|
<div class="sh-row">
|
||||||
|
<a class="btn small" id="shCta" target="_blank" rel="noopener nofollow" hidden>Learn more</a>
|
||||||
|
<button class="btn small sec" id="shNext" type="button" hidden>Next short →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/assets/shorts.js?v=20260906e"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -939,6 +939,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (p === '/ledger') return sendFile(res, path.join(PUBLIC_DIR, 'ledger.html'));
|
if (p === '/ledger') return sendFile(res, path.join(PUBLIC_DIR, 'ledger.html'));
|
||||||
if (p === '/contract') return sendFile(res, path.join(PUBLIC_DIR, 'contract.html'));
|
if (p === '/contract') return sendFile(res, path.join(PUBLIC_DIR, 'contract.html'));
|
||||||
if (p === '/my') return sendFile(res, path.join(PUBLIC_DIR, 'my.html'));
|
if (p === '/my') return sendFile(res, path.join(PUBLIC_DIR, 'my.html'));
|
||||||
|
if (p === '/shorts') return sendFile(res, path.join(PUBLIC_DIR, 'shorts.html'));
|
||||||
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
|
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
|
||||||
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
|
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
|
||||||
if (m) return sendFile(res, path.join(UPLOADS_DIR, m[1]));
|
if (m) return sendFile(res, path.join(UPLOADS_DIR, m[1]));
|
||||||
|
|||||||
Reference in New Issue
Block a user