Upgrade promo overlay: flyer shown once per session on the dashboard and Mini App (announce.js promo mode, admin-configurable promo* keys)
This commit is contained in:
+54
-4
@@ -9,10 +9,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function start(EV) {
|
function start(EV) {
|
||||||
if (!EV || !EV.enabled || !EV.id || !EV.img) return;
|
if (!EV || !EV.enabled || !EV.id || !EV.img) return false;
|
||||||
if (EV.expiresUTC) { var exp = Date.parse(EV.expiresUTC); if (exp && Date.now() > exp) return; }
|
if (EV.expiresUTC) { var exp = Date.parse(EV.expiresUTC); if (exp && Date.now() > exp) return false; }
|
||||||
var KEY = 'rmc-announce-' + EV.id;
|
var KEY = 'rmc-announce-' + EV.id;
|
||||||
try { if (localStorage.getItem(KEY) === 'done') return; } catch (e) {}
|
try { if (localStorage.getItem(KEY) === 'done') return false; } catch (e) {}
|
||||||
EV.timesArr = parseTimes(EV.times);
|
EV.timesArr = parseTimes(EV.times);
|
||||||
|
|
||||||
var back, card;
|
var back, card;
|
||||||
@@ -131,12 +131,62 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(build, 1100);
|
setTimeout(build, 1100);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Promo overlay: one flyer, one button, once per browser SESSION (sessionStorage), only on the
|
||||||
|
pages listed in promo.pages (path segment after the first slash: 'my', 'app', or 'all'). */
|
||||||
|
function startPromo(P) {
|
||||||
|
if (!P || !P.enabled || !P.id || !P.img) return;
|
||||||
|
if (P.expiresUTC) { var exp = Date.parse(P.expiresUTC); if (exp && Date.now() > exp) return; }
|
||||||
|
var seg = (location.pathname.split('/')[1] || 'home').toLowerCase();
|
||||||
|
var pages = String(P.pages || 'all').split(',').map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean);
|
||||||
|
if (pages.indexOf('all') < 0 && pages.indexOf(seg) < 0) return;
|
||||||
|
var KEY = 'rmc-promo-' + P.id;
|
||||||
|
try { if (sessionStorage.getItem(KEY) === 'seen') return; } catch (e) {}
|
||||||
|
var back;
|
||||||
|
function mark() { try { sessionStorage.setItem(KEY, 'seen'); } catch (e) {} }
|
||||||
|
function close() { if (!back) return; mark(); back.style.opacity = '0'; setTimeout(function () { if (back && back.parentNode) back.parentNode.removeChild(back); back = null; }, 260); document.removeEventListener('keydown', onKey); }
|
||||||
|
function onKey(e) { if (e.key === 'Escape') close(); }
|
||||||
|
function build() {
|
||||||
|
back = document.createElement('div');
|
||||||
|
back.setAttribute('role', 'dialog'); back.setAttribute('aria-label', P.eyebrow || 'Upgrade');
|
||||||
|
back.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(3,7,14,.84);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);opacity:0;transition:opacity .28s ease;overflow:auto;';
|
||||||
|
back.addEventListener('click', function (e) { if (e.target === back) close(); });
|
||||||
|
var card = document.createElement('div');
|
||||||
|
card.style.cssText = 'position:relative;width:100%;max-width:420px;max-height:96vh;overflow:auto;border-radius:20px;background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7);font-family:system-ui,Segoe UI,Arial,sans-serif;';
|
||||||
|
var x = document.createElement('button');
|
||||||
|
x.setAttribute('aria-label', 'Close'); x.innerHTML = '×';
|
||||||
|
x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:38px;height:38px;border-radius:50%;border:none;cursor:pointer;background:rgba(6,10,16,.72);color:#fff;font-size:24px;line-height:1;font-weight:700;display:grid;place-items:center;box-shadow:0 2px 10px rgba(0,0,0,.5);';
|
||||||
|
x.addEventListener('click', close);
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = P.img; img.alt = P.eyebrow || 'Upgrade'; img.style.cssText = 'display:block;width:100%;height:auto;border-radius:20px 20px 0 0;';
|
||||||
|
var foot = document.createElement('div'); foot.style.cssText = 'padding:14px 18px 18px;';
|
||||||
|
var cta = document.createElement('a');
|
||||||
|
cta.href = P.ctaUrl || '/my'; cta.textContent = P.ctaText || 'See my next step \u2192';
|
||||||
|
cta.style.cssText = 'display:block;text-align:center;text-decoration:none;background:linear-gradient(180deg,#e9c249,#d4af37);color:#0a1119;font-size:18px;font-weight:800;padding:15px;border-radius:12px;box-shadow:0 8px 22px rgba(212,175,55,.35);';
|
||||||
|
cta.addEventListener('click', function (e) {
|
||||||
|
// already on the target page: close and scroll to the plan card instead of reloading
|
||||||
|
var here = (P.ctaUrl || '/my').split('#')[0].replace(/\/$/, '') === location.pathname.replace(/\/$/, '');
|
||||||
|
if (here) { e.preventDefault(); close(); var t = document.querySelector('.ns-card, #nextStep, #plan'); if (t) setTimeout(function () { t.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 300); }
|
||||||
|
else mark();
|
||||||
|
});
|
||||||
|
var later = document.createElement('button');
|
||||||
|
later.textContent = 'Not now'; later.style.cssText = 'display:block;margin:10px auto 0;background:none;border:none;color:#7d8ea0;font-size:13px;cursor:pointer;text-decoration:underline;';
|
||||||
|
later.addEventListener('click', close);
|
||||||
|
foot.appendChild(cta); foot.appendChild(later);
|
||||||
|
card.appendChild(x); card.appendChild(img); card.appendChild(foot);
|
||||||
|
back.appendChild(card); document.body.appendChild(back);
|
||||||
|
document.addEventListener('keydown', onKey);
|
||||||
|
requestAnimationFrame(function () { back.style.opacity = '1'; });
|
||||||
|
}
|
||||||
|
setTimeout(build, 900);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchAndGo() {
|
function fetchAndGo() {
|
||||||
fetch('/api/announce', { headers: { 'Accept': 'application/json' } })
|
fetch('/api/announce', { headers: { 'Accept': 'application/json' } })
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
.then(function (r) { return r.ok ? r.json() : null; })
|
||||||
.then(function (EV) { if (EV) start(EV); })
|
.then(function (EV) { if (!EV) return; var shown = start(EV); if (!shown) startPromo(EV.promo); })
|
||||||
.catch(function () {});
|
.catch(function () {});
|
||||||
}
|
}
|
||||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fetchAndGo);
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fetchAndGo);
|
||||||
|
|||||||
+1
-1
@@ -28,5 +28,5 @@
|
|||||||
<a class="btn btn-primary" href="/my">Open the dashboard</a>
|
<a class="btn btn-primary" href="/my">Open the dashboard</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script src="/app.js"></script> <script src="/announce.js"></script>
|
<script src="/app.js"></script> <script src="/announce.js?v=20260916a"></script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
+1
-1
@@ -31,5 +31,5 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer class="wrap disclaimer">All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.<div class="footer-links"><a href="/">Home</a><a href="/contract">Contract Security</a><a href="/disclaimer">Disclaimers</a><a href="/tools">Promo Tools</a><a href="/privacy">Privacy</a><a href="/refunds">Refunds</a></div></footer>
|
<footer class="wrap disclaimer">All figures are read live from the RM Circle smart contract on Polygon and are historical facts, not a promise of future results. Participation involves cryptocurrency and smart-contract risk. Never use funds you cannot afford to lose.<div class="footer-links"><a href="/">Home</a><a href="/contract">Contract Security</a><a href="/disclaimer">Disclaimers</a><a href="/tools">Promo Tools</a><a href="/privacy">Privacy</a><a href="/refunds">Refunds</a></div></footer>
|
||||||
<script src="/track.js"></script><script src="/qrlib.js"></script><script src="/rmc-wallet.js"></script><script src="/inapp-browser.js"></script><script src="/my.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script><script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/wallet-notice.js" defer></script> <script src="/announce.js"></script>
|
<script src="/track.js"></script><script src="/qrlib.js"></script><script src="/rmc-wallet.js"></script><script src="/inapp-browser.js"></script><script src="/my.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script><script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/wallet-notice.js" defer></script> <script src="/announce.js?v=20260916a"></script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
@@ -423,7 +423,17 @@ const ANNOUNCE_DEFAULTS = {
|
|||||||
announceTimes: '7:00 PM CST | USA / Canada\n8:00 PM EST |\n8:00 PM AST | Caribbean\n1:00 AM | UK',
|
announceTimes: '7:00 PM CST | USA / Canada\n8:00 PM EST |\n8:00 PM AST | Caribbean\n1:00 AM | UK',
|
||||||
announceExpiresUTC: '2026-09-09T02:30:00Z',
|
announceExpiresUTC: '2026-09-09T02:30:00Z',
|
||||||
announceStartUTC: '',
|
announceStartUTC: '',
|
||||||
announceDurationMin: 60
|
announceDurationMin: 60,
|
||||||
|
// Promo overlay (announce.js, second mode): a flyer shown ONCE PER SESSION to members, with one button.
|
||||||
|
// Unlike the event pop-up (once per browser, with times + meet link) this is a plain nudge (Marty, 2026-09-16).
|
||||||
|
promoEnabled: true,
|
||||||
|
promoId: 'upgrade-2026-09-16',
|
||||||
|
promoImg: '/upgrade-flyer.jpg',
|
||||||
|
promoEyebrow: 'Your upgrade unlocks your reward',
|
||||||
|
promoCtaText: 'See my next step →',
|
||||||
|
promoCtaUrl: '/my',
|
||||||
|
promoPages: 'my,app',
|
||||||
|
promoExpiresUTC: ''
|
||||||
};
|
};
|
||||||
function seedAnnounceDefaults() {
|
function seedAnnounceDefaults() {
|
||||||
try { const c = readJson(CONFIG_FILE) || {}; let ch = false;
|
try { const c = readJson(CONFIG_FILE) || {}; let ch = false;
|
||||||
@@ -586,7 +596,8 @@ async function handleApi(req,res,pathname){
|
|||||||
id: c.announceId||'', img: c.announceImg||'', meetUrl: c.announceMeetUrl||'',
|
id: c.announceId||'', img: c.announceImg||'', meetUrl: c.announceMeetUrl||'',
|
||||||
eyebrow: c.announceEyebrow||'', dateLabel: c.announceDateLabel||'',
|
eyebrow: c.announceEyebrow||'', dateLabel: c.announceDateLabel||'',
|
||||||
times: c.announceTimes||'', expiresUTC: c.announceExpiresUTC||'',
|
times: c.announceTimes||'', expiresUTC: c.announceExpiresUTC||'',
|
||||||
startUTC: c.announceStartUTC||'', durationMin: Number(c.announceDurationMin)||60
|
startUTC: c.announceStartUTC||'', durationMin: Number(c.announceDurationMin)||60,
|
||||||
|
promo: { enabled: !!c.promoEnabled, id: c.promoId||'', img: c.promoImg||'', eyebrow: c.promoEyebrow||'', ctaText: c.promoCtaText||'', ctaUrl: c.promoCtaUrl||'/my', pages: c.promoPages||'my,app', expiresUTC: c.promoExpiresUTC||'' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// calendar file for the event pop-up (Apple Calendar / Outlook); Google uses a render URL built client-side
|
// calendar file for the event pop-up (Apple Calendar / Outlook); Google uses a render URL built client-side
|
||||||
@@ -1601,7 +1612,7 @@ async function handleApi(req,res,pathname){
|
|||||||
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
|
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
|
||||||
}
|
}
|
||||||
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
|
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
|
||||||
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','telegramProofChatId','telegramProofTopicId','telegramProofEvents','telegramProofCtaUrl','telegramEchoChatId','telegramEchoTopicId','telegramEchoEvents','telegramTeamEvents','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts','suiteLevelOverride','directDefaultIds','announceEnabled','announceId','announceImg','announceMeetUrl','announceEyebrow','announceDateLabel','announceTimes','announceExpiresUTC','announceStartUTC','announceDurationMin'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];if('announceDurationMin' in next)next.announceDurationMin=Math.max(15,Number(next.announceDurationMin)||60);next.premiumEntryPol=Number(next.premiumEntryPol)||362;if('announceEnabled' in next)next.announceEnabled=!!next.announceEnabled;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','telegramProofChatId','telegramProofTopicId','telegramProofEvents','telegramProofCtaUrl','telegramEchoChatId','telegramEchoTopicId','telegramEchoEvents','telegramTeamEvents','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts','suiteLevelOverride','directDefaultIds','announceEnabled','announceId','announceImg','announceMeetUrl','announceEyebrow','announceDateLabel','announceTimes','announceExpiresUTC','announceStartUTC','announceDurationMin','promoEnabled','promoId','promoImg','promoEyebrow','promoCtaText','promoCtaUrl','promoPages','promoExpiresUTC'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];if('announceDurationMin' in next)next.announceDurationMin=Math.max(15,Number(next.announceDurationMin)||60);next.premiumEntryPol=Number(next.premiumEntryPol)||362;if('announceEnabled' in next)next.announceEnabled=!!next.announceEnabled;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
|
||||||
}
|
}
|
||||||
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
|
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
|
||||||
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
|
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});
|
||||||
|
|||||||
Reference in New Issue
Block a user