c8ae4d55b0
The quota and the capability used to contradict each other. An Ascensus member was allowed 3 page builds a month, but every build overwrote the same file — so they could build three times and never have more than one page. The slug plumbing to do better already existed and was only exposed at level 6, which made the Funnel Factory's differentiator a COUNT rather than a KIND, the same flaw the upper tiers had. Now: hold caps by level (2 at Ascensus rising to 50 at Corona), separate from the monthly build quota. Rebuilding spends a build but needs no hold room; adding a new page needs both. The Page Builder is a list of your pages with view / copy link / rebuild / delete, plus a form that ADDS one instead of silently replacing what you had. Two things kept deliberately fixed. The first page a member builds stays at /p/<id> forever and cannot be deleted — that address ends up on banners, printed flyers and in people's DMs, so it must never move. Extra pages take a slug from the headline the writer produced, de-duplicated, so the address describes the page without anyone having to invent one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
247 lines
9.9 KiB
JavaScript
247 lines
9.9 KiB
JavaScript
// Page Builder client. Loads the member's existing page (if any), takes a
|
||
// four-question brief, and rebuilds at the same /p/<id> URL so shared links
|
||
// never break.
|
||
(function () {
|
||
'use strict';
|
||
var $ = function (id) { return document.getElementById(id); };
|
||
var angle = 'overview', busy = false, myId = null;
|
||
// null = building a NEW page; '' = rebuilding the main one; 'slug' = that page
|
||
var replacing = null, pages = [], cap = 0;
|
||
|
||
function renderAngles(angles) {
|
||
var host = $('pgAngles');
|
||
host.innerHTML = '';
|
||
Object.keys(angles).forEach(function (k) {
|
||
var b = document.createElement('button');
|
||
b.type = 'button';
|
||
b.className = 'pg-angle' + (k === angle ? ' on' : '');
|
||
b.textContent = angles[k].label;
|
||
b.addEventListener('click', function () { angle = k; renderAngles(angles); });
|
||
host.appendChild(b);
|
||
});
|
||
}
|
||
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||
});
|
||
}
|
||
|
||
function setMode(mode, label) {
|
||
replacing = mode;
|
||
$('pgGo').textContent = mode === null ? '🧱 Build a new page' : '🔁 Rebuild ' + (label || 'this page');
|
||
if (mode !== null) $('pgCard').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
|
||
function renderPages() {
|
||
var wrap = $('pgPagesWrap'), host = $('pgPages');
|
||
if (!pages.length) { wrap.style.display = 'none'; return; }
|
||
wrap.style.display = 'block';
|
||
host.innerHTML = '';
|
||
pages.forEach(function (p) {
|
||
var row = document.createElement('div');
|
||
row.className = 'pg-page' + (p.slug ? '' : ' main');
|
||
var left = document.createElement('div');
|
||
left.style.minWidth = '210px';
|
||
left.innerHTML = '<div class="nm">' + esc(p.name || 'Your page') +
|
||
(p.slug ? '' : ' <span style="color:var(--gold);font-size:11px;font-weight:900">MAIN</span>') + '</div>' +
|
||
'<div class="u">rmcircle.team' + esc(p.url) + '</div>' +
|
||
'<div class="meta">' + esc(p.angle || '') +
|
||
(p.updatedAt ? ' · updated ' + esc(String(p.updatedAt).slice(0, 10)) : '') + '</div>';
|
||
row.appendChild(left);
|
||
|
||
var acts = document.createElement('div');
|
||
acts.className = 'pg-acts';
|
||
var view = document.createElement('a');
|
||
view.className = 'btn btn-secondary btn-sm';
|
||
view.href = p.url; view.target = '_blank'; view.rel = 'noopener';
|
||
view.textContent = 'View';
|
||
acts.appendChild(view);
|
||
|
||
var copy = document.createElement('button');
|
||
copy.className = 'btn btn-secondary btn-sm';
|
||
copy.textContent = 'Copy link';
|
||
copy.addEventListener('click', function () {
|
||
if (navigator.clipboard) navigator.clipboard.writeText('https://rmcircle.team' + p.url);
|
||
copy.textContent = 'Copied';
|
||
setTimeout(function () { copy.textContent = 'Copy link'; }, 1400);
|
||
});
|
||
acts.appendChild(copy);
|
||
|
||
var re = document.createElement('button');
|
||
re.className = 'btn btn-secondary btn-sm';
|
||
re.textContent = 'Rebuild';
|
||
re.addEventListener('click', function () { setMode(p.slug || '', p.name || 'this page'); });
|
||
acts.appendChild(re);
|
||
|
||
// The main page keeps its address forever — it is on banners and printed
|
||
// flyers — so only the extra pages can be removed.
|
||
if (p.slug) {
|
||
var del = document.createElement('button');
|
||
del.className = 'btn btn-secondary btn-sm';
|
||
del.textContent = 'Delete';
|
||
del.addEventListener('click', function () { removePage(p.slug, p.name, del); });
|
||
acts.appendChild(del);
|
||
}
|
||
row.appendChild(acts);
|
||
host.appendChild(row);
|
||
});
|
||
|
||
var left = Math.max(0, cap - pages.length);
|
||
$('pgCapNote').innerHTML = 'Holding <b>' + pages.length + '</b> of <b>' + cap + '</b> pages your level allows' +
|
||
(left ? ' — room for ' + left + ' more.' : ' — delete one to add another, or upgrade to hold more.');
|
||
if (!left && replacing === null) setMode(pages.length ? (pages[0].slug || '') : null, pages[0] && pages[0].name);
|
||
}
|
||
|
||
async function removePage(slug, name, btn) {
|
||
if (!window.confirm('Delete "' + (name || slug) + '"? Anything pointing at that address will fall back to your main page.')) return;
|
||
btn.disabled = true;
|
||
try {
|
||
var r = await fetch('/api/public/suite-page', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ remove: slug })
|
||
});
|
||
var d = await r.json();
|
||
if (r.ok) { pages = d.pages || []; cap = d.cap || cap; renderPages(); }
|
||
else btn.disabled = false;
|
||
} catch (e) { btn.disabled = false; }
|
||
}
|
||
|
||
function showMeter(m) {
|
||
if (!m || !m.limit) { $('pgMeter').textContent = ''; return; }
|
||
$('pgMeter').innerHTML = '<b>' + m.remaining + '</b> of ' + m.limit + ' page builds left this month';
|
||
}
|
||
|
||
var built = false;
|
||
|
||
function showLive(url) {
|
||
built = true;
|
||
$('pgUrl').textContent = url.replace(/^https:\/\//, '');
|
||
$('pgOpen').href = url;
|
||
$('pgLive').style.display = 'block';
|
||
// cache-bust so a rebuild always shows the NEW page, not the old one
|
||
$('pgFrame').src = url + '?preview=' + Date.now();
|
||
$('pgPrev').style.display = 'block';
|
||
}
|
||
|
||
// Live status under the button. A silent 40-second wait reads as "frozen",
|
||
// so we narrate it and count the seconds.
|
||
var progTimer = null;
|
||
function startProgress() {
|
||
var el = $('pgProg'), t0 = Date.now();
|
||
var steps = [
|
||
'Reading your notes…',
|
||
'Writing your headline and story…',
|
||
'Choosing your angle video…',
|
||
'Laying out your page…',
|
||
'Almost there — finishing up…'
|
||
];
|
||
el.style.display = 'block';
|
||
var tick = function () {
|
||
var s = Math.round((Date.now() - t0) / 1000);
|
||
var i = Math.min(steps.length - 1, Math.floor(s / 9));
|
||
el.innerHTML = '<span class="spin"></span>' + steps[i] + ' <span style="opacity:.65">(' + s + 's)</span>';
|
||
};
|
||
tick();
|
||
progTimer = setInterval(tick, 1000);
|
||
}
|
||
function stopProgress() {
|
||
if (progTimer) clearInterval(progTimer);
|
||
progTimer = null;
|
||
$('pgProg').style.display = 'none';
|
||
}
|
||
|
||
function gate(msg) {
|
||
var g = $('pgGate');
|
||
g.style.display = 'block';
|
||
g.innerHTML = msg;
|
||
$('pgCard').style.opacity = '.55';
|
||
$('pgGo').disabled = true;
|
||
}
|
||
|
||
async function boot() {
|
||
try {
|
||
var r = await fetch('/api/public/suite-page');
|
||
if (r.status === 401) {
|
||
gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> and connect the wallet that holds your position, then come back.');
|
||
renderAngles({ overview: { label: 'General overview' } });
|
||
return;
|
||
}
|
||
if (r.status === 403) { gate('The Page Builder isn’t open for this position yet. <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.'); return; }
|
||
if (!r.ok) return;
|
||
var d = await r.json();
|
||
myId = d.id;
|
||
renderAngles(d.angles || {});
|
||
if (d.meter && d.meter.reason === 'locked') {
|
||
gate('The Page Builder unlocks at <b>Ascensus (level 2)</b>. You’re at level ' + d.level + ' — your next upgrade opens it, along with the Copy Engine. <a href="/how-pay-works#exact-costs" style="color:var(--gold);font-weight:700">See what an upgrade costs →</a>');
|
||
return;
|
||
}
|
||
showMeter(d.meter);
|
||
pages = d.pages || [];
|
||
cap = d.cap || 0;
|
||
// First-time member: no pages yet, so the button builds their main one.
|
||
// Anyone with pages starts in "add another" mode; renderPages flips that
|
||
// to rebuild if they are already at their level's limit.
|
||
setMode(pages.length ? null : '', null);
|
||
renderPages();
|
||
if (d.page) {
|
||
if (d.page.name) $('pgName').value = d.page.name;
|
||
if (d.page.audience) $('pgAudience').value = d.page.audience;
|
||
if (d.page.story) $('pgStory').value = d.page.story;
|
||
if (d.page.angle) { angle = d.page.angle; renderAngles(d.angles || {}); }
|
||
showLive('https://rmcircle.team/p/' + d.id);
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
async function run() {
|
||
if (busy) return;
|
||
busy = true;
|
||
$('pgErr').style.display = 'none';
|
||
$('pgGo').disabled = true;
|
||
// Hide any previous preview while the new one is being written, so a stale
|
||
// or half-loaded frame can't look like a broken result.
|
||
$('pgPrev').style.display = 'none';
|
||
$('pgFrame').removeAttribute('src');
|
||
$('pgGo').innerHTML = '<span class="spin"></span>Writing your page…';
|
||
startProgress();
|
||
try {
|
||
var r = await fetch('/api/public/suite-page', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: $('pgName').value.trim(), audience: $('pgAudience').value.trim(), story: $('pgStory').value.trim(), angle: angle, replace: replacing === null ? undefined : replacing })
|
||
});
|
||
var d = await r.json();
|
||
if (!r.ok) {
|
||
$('pgErr').textContent = d.error || 'That didn’t go through — try again.';
|
||
$('pgErr').style.display = 'block';
|
||
if (d.meter) showMeter(d.meter);
|
||
} else {
|
||
showMeter(d.meter);
|
||
showLive(d.url);
|
||
if (d.pages) { pages = d.pages; cap = d.cap || cap; renderPages(); }
|
||
setMode(d.canAddMore ? null : (d.slug || ''), null);
|
||
$('pgLive').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
}
|
||
} catch (e) {
|
||
$('pgErr').textContent = 'Connection hiccup — try again.';
|
||
$('pgErr').style.display = 'block';
|
||
}
|
||
stopProgress();
|
||
busy = false;
|
||
$('pgGo').disabled = false;
|
||
setMode(replacing, null);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
boot();
|
||
$('pgGo').addEventListener('click', run);
|
||
$('pgCopyUrl').addEventListener('click', function () {
|
||
navigator.clipboard.writeText($('pgOpen').href).then(function () {
|
||
$('pgCopied').style.display = '';
|
||
setTimeout(function () { $('pgCopied').style.display = 'none'; }, 2500);
|
||
});
|
||
});
|
||
});
|
||
})();
|