// RM Circle: OPTIONAL member profile (Marty + Manson, 2026-09-17).
//
// Contact details are never required to hold a position, get paid, read the org,
// use the training or the tools. Nothing on chain depends on them. They are asked
// for in exactly one place where their absence is the whole problem: a message
// cannot be delivered to someone who has given no way to reach them. Everything
// else is an invitation the member can decline, and declining costs them nothing.
//
// Usage: window.RMCProfile.prompt({ reason: 'alerts'|'inbox' }) // dismissable
// window.RMCProfile.edit('username'|'email') // change later
// window.RMCProfile.status() // read-only
(function () {
'use strict';
var back = null, resolveDone = null, state = null, oneShot = false, showSteps = true, reason = 'alerts';
var GOLD = '#d4af37', TEAL = '#4ed6cb';
function el(tag, css, html) {
var e = document.createElement(tag);
if (css) e.style.cssText = css;
if (html != null) e.innerHTML = html;
return e;
}
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); }
async function api(path, body) {
var r = await fetch(path, body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {});
var j = null; try { j = await r.json(); } catch (e) { j = {}; }
if (!r.ok || j.error) throw new Error(j.error || 'Something went wrong. Try again.');
return j;
}
function shell() {
back = el('div', 'position:fixed;inset:0;z-index:2147483100;display:flex;align-items:center;justify-content:center;' +
'padding:20px;background:rgba(3,7,14,.88);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);overflow:auto;');
back.setAttribute('role', 'dialog');
back.setAttribute('aria-label', 'Member profile — optional');
var card = el('div', 'position:relative;width:100%;max-width:440px;max-height:94vh;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;color:#e8eef6;padding:22px 22px 20px;');
card.id = 'pgCard';
{ // every profile dialog can be closed: nothing here is compulsory
var x = el('button', 'position:absolute;top:10px;right:12px;z-index:2;width:34px;height:34px;border-radius:50%;border:none;' +
'cursor:pointer;background:rgba(255,255,255,.08);color:#fff;font-size:20px;line-height:1;font-weight:700;', '×');
x.id = 'pgClose';
x.setAttribute('aria-label', 'Close');
x.addEventListener('click', close);
card.appendChild(x);
back.addEventListener('click', function (e) { if (e.target === back) close(); });
}
back.appendChild(card);
document.body.appendChild(back);
return card;
}
function head(card, step) {
var keep = card.querySelector('button[aria-label="Close"]');
card.innerHTML = '';
if (keep) card.appendChild(keep);
if (!showSteps) return card;
card.appendChild(el('div', 'text-align:center;letter-spacing:2px;text-transform:uppercase;font-size:11px;font-weight:700;color:' + GOLD + ';margin-bottom:8px;',
'Position #' + esc(state.id) + ' · step ' + step + ' of 2'));
// Manson asked for a HUGE asterisk so nobody can claim they did not see it. On a
// decentralized build the burden is on us to make "optional" unmissable, and this
// dialog is the one place a member might feel cornered (Marty + Manson, 2026-09-17).
card.appendChild(el('div', 'display:flex;gap:9px;align-items:flex-start;margin:0 0 14px;padding:10px 12px;' +
'border:1px solid rgba(240,197,109,.55);border-radius:12px;background:rgba(240,197,109,.09);',
'✳️
' +
'
100% OPTIONAL — never required
' +
'
Close this any time. Your position, your payouts and your team are not affected in any way, ' +
'and you can remove whatever you add later.
'));
return card;
}
function note(text, color) {
return el('p', 'margin:0 0 14px;font-size:13.5px;line-height:1.6;color:' + (color || '#9fb3c8') + ';', text);
}
function input(id, placeholder, value, type) {
var i = el('input');
i.id = id; i.type = type || 'text'; i.placeholder = placeholder; i.value = value || '';
i.autocomplete = type === 'email' ? 'email' : 'off';
i.style.cssText = 'width:100%;box-sizing:border-box;padding:12px 14px;border-radius:11px;border:1px solid rgba(255,255,255,.16);' +
'background:rgba(255,255,255,.04);color:#fff;font-size:16px;margin-bottom:10px;';
return i;
}
function button(label, id) {
var b = el('button', 'width:100%;padding:13px 16px;border-radius:11px;border:none;cursor:pointer;font-size:15px;font-weight:700;' +
'background:linear-gradient(180deg,' + GOLD + ',#b8932f);color:#1a1205;', label);
if (id) b.id = id;
return b;
}
function errLine() { return el('p', 'margin:0 0 10px;font-size:13px;color:#ff8b8b;display:none;'); }
// ---- step 1: username ----
function stepUsername() {
var card = head(document.getElementById('pgCard'), 1);
card.appendChild(el('h2', 'margin:0 0 6px;font-size:21px;line-height:1.25;color:#fff;', 'Pick your username'));
card.appendChild(note('How your team sees you instead of a bare member number. Letters, numbers or underscores, 3 to 20 characters. Optional, and changeable any time.'));
var err = errLine(); card.appendChild(err);
var i = input('pgUser', 'e.g. ' + (state.suggest || 'member' + state.id), (state.profile && state.profile.username) || '');
card.appendChild(i);
var b = button('Save and continue', 'pgUserSave');
card.appendChild(b);
var go = async function () {
err.style.display = 'none'; b.disabled = true; b.textContent = 'Saving…';
try {
var r = await api('/api/public/profile/username', { username: i.value });
state.profile = r.profile;
if (oneShot) { close(); return; }
next();
} catch (e) {
err.textContent = e.message; err.style.display = 'block'; b.disabled = false; b.textContent = 'Save and continue';
}
};
b.addEventListener('click', go);
i.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); go(); } });
setTimeout(function () { i.focus(); }, 60);
}
// ---- step 2: email + code ----
function stepEmail() {
var card = head(document.getElementById('pgCard'), 2);
var prefill = (state.profile && state.profile.email) || '';
card.appendChild(el('h2', 'margin:0 0 6px;font-size:21px;line-height:1.25;color:#fff;', 'Confirm your email'));
card.appendChild(note((reason === 'inbox'
? 'Your inbox needs somewhere to reach you. Add an email and your leader\'s messages reach you even when you are not on the site. '
: 'Get an email the moment a payout lands in your wallet, and your leader can reach you when it matters. ') +
'Optional, never shown to other members, never sold, and you can remove it any time.' +
(prefill ? ' We already have this one on file for your payout alerts, so just confirm it.' : '')));
var err = errLine(); card.appendChild(err);
var i = input('pgEmail', 'you@example.com', prefill, 'email');
card.appendChild(i);
var b = button(prefill ? 'Send me the code' : 'Send me a code', 'pgMailSend');
card.appendChild(b);
var codeWrap = el('div', 'display:none;margin-top:14px;padding-top:14px;border-top:1px solid rgba(255,255,255,.1);');
var sentNote = note('', TEAL); codeWrap.appendChild(sentNote);
var ci = input('pgCode', '6-digit code', '');
ci.inputMode = 'numeric'; ci.maxLength = 6;
codeWrap.appendChild(ci);
var cb = button('Confirm and finish', 'pgCodeConfirm');
codeWrap.appendChild(cb);
var again = el('p', 'margin:10px 0 0;font-size:12.5px;color:#7f93a8;text-align:center;cursor:pointer;', 'Wrong address? Change it and send a new code.');
again.addEventListener('click', function () { codeWrap.style.display = 'none'; b.disabled = false; b.textContent = 'Send me a code'; i.focus(); });
codeWrap.appendChild(again);
card.appendChild(codeWrap);
b.addEventListener('click', async function () {
err.style.display = 'none'; b.disabled = true; b.textContent = 'Sending…';
try {
var r = await api('/api/public/profile/email-start', { email: i.value });
sentNote.textContent = 'Code sent to ' + (r.to || i.value) + '. It lasts 15 minutes. Check spam the first time.';
codeWrap.style.display = 'block'; b.textContent = 'Code sent';
setTimeout(function () { ci.focus(); }, 60);
} catch (e) {
err.textContent = e.message; err.style.display = 'block'; b.disabled = false; b.textContent = 'Send me a code';
}
});
var confirm = async function () {
err.style.display = 'none'; cb.disabled = true; cb.textContent = 'Checking…';
try {
var r = await api('/api/public/profile/email-verify', { code: ci.value });
state.profile = r.profile;
if (oneShot) { close(); return; }
done();
} catch (e) {
err.textContent = e.message; err.style.display = 'block'; cb.disabled = false; cb.textContent = 'Confirm and finish';
}
};
cb.addEventListener('click', confirm);
ci.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); confirm(); } });
setTimeout(function () { i.focus(); }, 60);
}
function done() {
var card = head(document.getElementById('pgCard'), 2);
card.innerHTML = '' +
'
✅
' +
'
You are all set, @' + esc(state.profile.username) + '
' +
'
Your leader can reach you now, and every payout to your wallet sends you a note.
';
var b = button('Open my dashboard', 'pgDone');
b.addEventListener('click', function () { close(true); });
card.appendChild(b);
setTimeout(function () { close(true); }, 2600);
}
function close(openDash) {
if (back && back.parentNode) back.parentNode.removeChild(back);
back = null;
if (openDash) { try { var b = document.querySelector('.mp-tab[data-tab="dash"]'); if (b) b.click(); } catch (e) {} }
if (resolveDone) { var r = resolveDone; resolveDone = null; r((state && state.profile) || null); }
}
function next() {
if (!state.profile || !state.profile.username) return stepUsername();
if (!state.profile.emailVerified) return stepEmail();
return done();
}
// An invitation, not a gate: fully dismissable, and declining costs nothing.
// opts.onlyForId - only for the member's own position, never a teammate's page.
// opts.reason - 'alerts' (default) or 'inbox', which picks the copy.
async function prompt_(opts) {
try { state = await api('/api/public/profile'); }
catch (e) { return null; }
if (!state || state.signedIn === false) return null;
var only = opts && opts.onlyForId;
if (only && Number(only) !== Number(state.id)) return null;
if (state.profile && state.profile.complete) return state.profile;
if (back) return null;
reason = (opts && opts.reason) || 'alerts';
oneShot = false; // full two-step opt-in, not a single-field edit
showSteps = true;
shell();
var p = new Promise(function (res) { resolveDone = res; });
next();
var out = await p; oneShot = false; return out;
}
async function status() { try { return await api('/api/public/profile'); } catch (e) { return null; } }
// Change one field. Opens straight at that step and closes on save.
// Resolves with the profile when saved, or null if the member closed it.
async function edit(kind) {
if (back) return null;
try { state = await api('/api/public/profile'); } catch (e) { return null; }
if (!state || state.signedIn === false) return null;
oneShot = true; showSteps = false;
shell();
var p = new Promise(function (res) { resolveDone = res; });
if (kind === 'email') stepEmail(); else stepUsername();
var out = await p; oneShot = false; return out;
}
window.RMCProfile = { prompt: prompt_, status: status, edit: edit };
})();