Members area: weekly webinar replay page at /replays
Wallet-gated like the Method lessons: the API returns titles/dates to everyone (prospects see the shape of the product) and video URLs only with a member session. List lives in the data volume (replays.json) so the weekly add never needs a deploy; videos stream from the DO Spaces bucket (CSP media-src added). Teaser on /training; chatbot + AI prompt updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// Webinar replays — members-area page. The list comes from /api/public/replays:
|
||||
// locked responses carry titles/dates only (prospects see the shape of the
|
||||
// product), a member session gets the video URLs. Unlock is the same
|
||||
// one-signature SIWE flow as the training page (msg-challenge/msg-verify via
|
||||
// RMCWallet); Mini App visitors already carry a session and see no lock.
|
||||
(function () {
|
||||
'use strict';
|
||||
function $(id) { return document.getElementById(id); }
|
||||
function esc(s) { return String(s == null ? '' : s).replace(/[&<>'"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[c]; }); }
|
||||
|
||||
function card(r, locked) {
|
||||
var head = '<div style="padding:6px 6px 0"><div class="eyebrow" style="color:var(--teal)">' + esc(r.date || '') + (r.minutes ? ' · ' + esc(r.minutes) + ' min' : '') + '</div>' +
|
||||
'<h2 style="margin:6px 0 6px">' + esc(r.title || 'Team webinar') + '</h2>' +
|
||||
(r.blurb ? '<p style="color:var(--muted);margin:0 0 12px">' + esc(r.blurb) + '</p>' : '') + '</div>';
|
||||
if (locked || !r.url) {
|
||||
return '<div class="video-card rmc-lock" style="margin:0 0 26px">' + head +
|
||||
'<div style="padding:8px 6px 10px">' +
|
||||
'<p class="micro" style="color:var(--muted);margin:0 0 12px">🔒 Replays come with every team position. Sign in with the wallet that owns your position — one free signature, it can\'t move funds. Linked on Telegram? The ☰ Mini App in @RMCircleBot unlocks everything automatically.</p>' +
|
||||
'<button class="btn btn-primary btn-sm rmc-unlock">🔓 I\'m a member — connect & unlock</button> ' +
|
||||
'<a class="btn btn-secondary btn-sm" href="/start">Not a member? See how to join</a>' +
|
||||
'<div class="micro rmc-lock-err" style="color:#ff7b7b;margin-top:8px"></div></div></div>';
|
||||
}
|
||||
return '<div class="video-card" style="margin:0 0 26px">' + head +
|
||||
'<video class="video-frame" style="display:block;width:100%;height:auto;aspect-ratio:16/9" controls preload="metadata"' + (r.poster ? ' poster="' + esc(r.poster) + '"' : '') + ' src="' + esc(r.url) + '"></video></div>';
|
||||
}
|
||||
|
||||
var UNLOCK_LABEL = '🔓 I\'m a member — connect & unlock';
|
||||
function wireUnlock() {
|
||||
document.querySelectorAll('button.rmc-unlock').forEach(function (b) {
|
||||
b.addEventListener('click', function (ev) {
|
||||
var btn = ev.currentTarget, err = btn.closest('.rmc-lock').querySelector('.rmc-lock-err');
|
||||
btn.disabled = true; btn.textContent = 'Connecting…';
|
||||
(async function () {
|
||||
try {
|
||||
var eth = window.RMCWallet && await window.RMCWallet.pick();
|
||||
if (!eth) { err.textContent = 'No wallet in this browser. On a phone, open this page inside your wallet app\'s browser — or use the Telegram Mini App.'; btn.disabled = false; btn.textContent = UNLOCK_LABEL; return; }
|
||||
var accounts = await eth.request({ method: 'eth_requestAccounts' });
|
||||
var account = accounts && accounts[0];
|
||||
try { await window.RMCWallet.ensureChain(eth); } catch (ce) { err.textContent = 'Switch your wallet to the Polygon network, then tap unlock again.'; btn.disabled = false; btn.textContent = UNLOCK_LABEL; return; }
|
||||
var ch = await (await fetch('/api/public/msg-challenge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: account }) })).json();
|
||||
if (ch.error) throw new Error(ch.error);
|
||||
var sig = await eth.request({ method: 'personal_sign', params: [ch.message, account] });
|
||||
var vr = await (await fetch('/api/public/msg-verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: account, signature: sig }) })).json();
|
||||
if (vr.error) throw new Error(vr.error);
|
||||
load();
|
||||
} catch (e) { err.textContent = e.message || String(e); btn.disabled = false; btn.textContent = UNLOCK_LABEL; }
|
||||
})();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function load() {
|
||||
fetch('/api/public/replays').then(function (r) { return r.json(); }).then(function (d) {
|
||||
var list = d.replays || [];
|
||||
if (!list.length) {
|
||||
$('replayList').innerHTML = '<div class="callout" style="text-align:center">The first replay is on its way — check back after this week\'s webinar.</div>';
|
||||
return;
|
||||
}
|
||||
if (!d.locked) {
|
||||
$('replayStatus').innerHTML = '<p class="micro" style="color:var(--ok);margin:0">✅ Members area unlocked — position #' + esc(d.id) + '</p>';
|
||||
} else {
|
||||
$('replayStatus').innerHTML = '';
|
||||
}
|
||||
$('replayList').innerHTML = list.map(function (r) { return card(r, d.locked); }).join('');
|
||||
if (d.locked) wireUnlock();
|
||||
}).catch(function () {
|
||||
$('replayList').innerHTML = '<div class="callout warning">Could not load the replay list — refresh to try again.</div>';
|
||||
});
|
||||
}
|
||||
load();
|
||||
})();
|
||||
Reference in New Issue
Block a user