// 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, skips: 0 }; 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; // signed-out visitors get the sign-in note without a 401 round trip let me = null; try { me = await j('/api/me'); } catch (e) {} if (me && me.signedIn && me.email) { try { r = await j('/api/my/videos?orientation=portrait'); } 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; st.adId = r.ad.id; const v = $('shVideo'); $('shMsg').hidden = true; v.hidden = false; v.src = r.ad.videoUrl; v.currentTime = 0; // safety net: this reel is portrait-only. If a landscape video slips through, skip to the next. v.onloadedmetadata = () => { if (v.videoWidth && v.videoHeight && v.videoWidth > v.videoHeight) { if (st.skips++ < 4) { load(); return; } v.hidden = true; $('shMsg').hidden = false; $('shMsg').textContent = 'No shorts in rotation right now. Check back soon.'; } else { st.skips = 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); // 2026-09-16: was a native prompt() pre-filled with 'inappropriate' (one stray OK = a false report). // Now an inline row of reason buttons: the member picks one on purpose, or cancels. $('shReport').addEventListener('click', e => { e.preventDefault(); if (!st.adId) return; let row = document.getElementById('shReasons'); if (row) { row.remove(); return; } row = document.createElement('div'); row.id = 'shReasons'; row.style.cssText = 'display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;align-items:center'; row.innerHTML = 'What is wrong?' + ['broken', 'inappropriate', 'spam', 'scam', 'other'].map(r => '').join('') + ''; $('shReport').insertAdjacentElement('afterend', row); row.addEventListener('click', ev => { const b = ev.target.closest('button[data-reason]'); if (!b) return; const reason = b.dataset.reason; row.remove(); if (!reason) return; fetch('/api/report-ad', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaignId: st.adId, reason }) }) .then(() => { $('shReport').textContent = 'โœ“ reported โ€” thanks'; }).catch(() => {}); }); }); // presence enforcement: pause when the tab/window loses focus, resume on return document.addEventListener('visibilitychange', () => { const v = $('shVideo'); if (!v || !v.src) return; if (document.hidden) v.pause(); else if (!st.done) v.play().catch(() => {}); }); window.addEventListener('blur', () => { const v = $('shVideo'); if (v && v.src) v.pause(); }); window.addEventListener('focus', () => { const v = $('shVideo'); if (v && v.src && !st.done) v.play().catch(() => {}); }); // tap the video to pause/resume $('shVideo').addEventListener('click', () => { const v = $('shVideo'); if (v.paused) v.play(); else v.pause(); }); load(); })();