365 lines
16 KiB
EmacsLisp
365 lines
16 KiB
EmacsLisp
// account-dashboard.el -- Account dashboard: session check, plan card, family.
|
|
// Handles onAuthStateChange, plan card rendering, family member management.
|
|
// Compiled with: elc --target=js --bundle --minify --obfuscate
|
|
//
|
|
// Required globals: window.NEURON_CFG.supabase_url, window.NEURON_CFG.supabase_anon_key
|
|
// Required CDN: supabase-js@2
|
|
|
|
fn main() -> Void {
|
|
native_js("(function() {
|
|
'use strict';
|
|
|
|
var cfg = window.NEURON_CFG || {};
|
|
var sb = supabase.createClient(cfg.supabase_url, cfg.supabase_anon_key, {
|
|
auth: { flowType: 'implicit' }
|
|
});
|
|
|
|
function show(id) { var el = document.getElementById(id); if (el) el.style.display = ''; }
|
|
function hide(id) { var el = document.getElementById(id); if (el) el.style.display = 'none'; }
|
|
function setText(id, text) { var el = document.getElementById(id); if (el) el.textContent = text; }
|
|
function setHtml(id, html) { var el = document.getElementById(id); if (el) el.innerHTML = html; }
|
|
|
|
window.signInWith = async function(provider) {
|
|
var btn = document.getElementById('btn-' + provider);
|
|
if (btn) { btn.disabled = true; btn.style.opacity = '0.6'; }
|
|
try {
|
|
var result = await sb.auth.signInWithOAuth({
|
|
provider: provider,
|
|
options: { redirectTo: window.location.origin + '/account' }
|
|
});
|
|
if (result.error) {
|
|
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
|
|
}
|
|
} catch (e) {
|
|
if (btn) { btn.disabled = false; btn.style.opacity = '1'; }
|
|
}
|
|
};
|
|
|
|
window.signInWithEmail = async function() {
|
|
var email = document.getElementById('acct-email-input').value.trim();
|
|
var pass = document.getElementById('acct-pass-input').value;
|
|
var msg = document.getElementById('acct-email-msg');
|
|
var signinBtn = document.getElementById('acct-signin-btn');
|
|
if (!sb) { msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Loading... try again in a moment.'; return; }
|
|
if (!email || !pass) {
|
|
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Please enter your email and password.'; return;
|
|
}
|
|
if (signinBtn) { signinBtn.disabled=true; signinBtn.textContent='Signing in...'; }
|
|
var result = await sb.auth.signInWithPassword({ email: email, password: pass });
|
|
if (result.error) {
|
|
if (result.error.message && result.error.message.toLowerCase().includes('invalid')) {
|
|
var signupResult = await sb.auth.signUp({
|
|
email: email, password: pass,
|
|
options: { emailRedirectTo: window.location.origin + '/account' }
|
|
});
|
|
if (signupResult.error) {
|
|
msg.style.display='block'; msg.style.color='#c44'; msg.textContent=signupResult.error.message; return;
|
|
}
|
|
msg.style.display='block'; msg.style.color='var(--navy)'; msg.textContent='Check your email to confirm your account.'; return;
|
|
}
|
|
if (signinBtn) { signinBtn.disabled=false; signinBtn.textContent='Sign in'; }
|
|
msg.style.display='block'; msg.style.color='#c44'; msg.textContent=result.error.message; return;
|
|
}
|
|
window.location.reload();
|
|
};
|
|
|
|
window.signOut = async function() {
|
|
var btn = document.getElementById('signout-btn');
|
|
var btnTop = document.getElementById('signout-btn-top');
|
|
if (btn) { btn.disabled=true; btn.textContent='Signing out...'; }
|
|
if (btnTop) { btnTop.disabled=true; btnTop.textContent='Signing out...'; }
|
|
await sb.auth.signOut();
|
|
show('signin-section');
|
|
hide('dashboard-section');
|
|
if (btn) { btn.disabled=false; btn.textContent='Sign out'; }
|
|
if (btnTop) { btnTop.disabled=false; btnTop.textContent='Sign out'; }
|
|
};
|
|
|
|
async function renderPlanCard(row) {
|
|
var plan = (row && row.plan) ? row.plan : 'free';
|
|
var memberNum = (row && row.member_number) ? row.member_number : null;
|
|
var createdAt = (row && row.created_at) ? row.created_at : null;
|
|
|
|
var planNames = { 'founding': 'Founding Member', 'professional': 'Professional', 'free': 'Free' };
|
|
setText('plan-name-el', planNames[plan] || 'Free');
|
|
|
|
var statusHtml = '';
|
|
if (plan === 'founding' || plan === 'professional') {
|
|
statusHtml = '<span class=\"status-badge-preorder\" style=\"margin-top:.625rem;display:inline-flex\">'
|
|
+ '<svg width=\"10\" height=\"10\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><polyline points=\"12 6 12 12 16 14\"/></svg>'
|
|
+ 'Launching within 30 days</span>';
|
|
} else {
|
|
statusHtml = '<span class=\"plan-status\" style=\"margin-top:.625rem;display:inline-flex\"><span class=\"plan-status-dot\"></span>Active</span>';
|
|
}
|
|
setHtml('plan-status-el', statusHtml);
|
|
|
|
var billingNote = '';
|
|
if (plan === 'founding') {
|
|
billingNote = '<p class=\"plan-billing-note\">Lifetime · Never billed again</p>';
|
|
} else if (plan === 'professional') {
|
|
billingNote = '<p class=\"plan-billing-note\">Billed monthly · <button class=\"plan-billing-link\" onclick=\"window.location.href=\'/contact\'\">Cancel</button></p>';
|
|
} else {
|
|
billingNote = '<p class=\"plan-billing-note\">On the waitlist</p>';
|
|
}
|
|
setHtml('plan-billing-note-el', billingNote);
|
|
|
|
var meta = '';
|
|
if (createdAt) {
|
|
var d = new Date(createdAt);
|
|
var dateStr = d.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
|
meta += '<div class=\"plan-meta-item\"><span class=\"plan-meta-label\">Joined</span><span class=\"plan-meta-value\">' + dateStr + '</span></div>';
|
|
}
|
|
if (memberNum) {
|
|
meta += '<div class=\"plan-meta-item\"><span class=\"plan-meta-label\">Member number</span><span class=\"plan-meta-value\">#' + memberNum + ' of 1,000</span></div>';
|
|
}
|
|
if (meta) setHtml('plan-meta-el', meta);
|
|
|
|
if (plan === 'founding') {
|
|
var badgeSection = document.getElementById('badge-section');
|
|
var badgeContainer = document.getElementById('badge-html-container');
|
|
if (badgeSection) badgeSection.style.display = '';
|
|
var badgeN = memberNum || 0;
|
|
fetch('/api/founding-badge?n=' + badgeN)
|
|
.then(function(r) { return r.text(); })
|
|
.then(function(html) { if (badgeContainer) badgeContainer.innerHTML = html; })
|
|
.catch(function() {});
|
|
}
|
|
|
|
var roadmapSection = document.getElementById('roadmap-section');
|
|
if (plan === 'founding' && roadmapSection) roadmapSection.style.display = '';
|
|
|
|
if (plan === 'founding') {
|
|
var famSection = document.getElementById('family-section');
|
|
if (famSection) famSection.style.display = 'block';
|
|
var session = await sb.auth.getSession();
|
|
var userEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
|
if (userEmail) loadFamilyMembers(userEmail);
|
|
}
|
|
}
|
|
|
|
async function loadFamilyMembers(parentEmail) {
|
|
var r = await fetch('/api/family/members?parent_email=' + encodeURIComponent(parentEmail));
|
|
var members = await r.json();
|
|
var list = document.getElementById('family-list');
|
|
if (!list) return;
|
|
if (!members || !members.length) {
|
|
list.innerHTML = '<p style=\"color:var(--t3);font-size:.875rem;margin-bottom:1rem\">No family members yet.</p>';
|
|
return;
|
|
}
|
|
list.innerHTML = members.map(function(m) {
|
|
return '<div style=\"display:flex;justify-content:space-between;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--border)\">'
|
|
+ '<div><p style=\"font-size:.875rem;color:var(--t1)\">' + m.child_email + '</p>'
|
|
+ '<p style=\"font-size:.75rem;color:var(--t3);text-transform:uppercase;letter-spacing:.06em\">' + m.status + '</p></div>'
|
|
+ '<button onclick=\"removeFamilyMember(\'' + m.child_email + '\')\" style=\"background:none;border:none;color:var(--t3);cursor:pointer;font-size:.75rem\">Remove</button>'
|
|
+ '</div>';
|
|
}).join('');
|
|
}
|
|
|
|
window.addFamilyMember = async function() {
|
|
var email = document.getElementById('child-email').value.trim();
|
|
var year = document.getElementById('child-dob-year').value;
|
|
var attest = document.getElementById('family-attest').checked;
|
|
var msg = document.getElementById('family-msg');
|
|
if (!email || !year || !attest) {
|
|
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Please fill in all fields and confirm the attestation.'; return;
|
|
}
|
|
if (parseInt(year) < 2008) {
|
|
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Child must be under 18. Birth year must be 2008 or later.'; return;
|
|
}
|
|
var session = await sb.auth.getSession();
|
|
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
|
var r = await fetch('/api/family/invite', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({parent_email: parentEmail, child_email: email, child_dob_year: parseInt(year), attested: true})
|
|
});
|
|
var d = await r.json();
|
|
msg.style.display = 'block';
|
|
if (d.ok) {
|
|
msg.style.color = 'var(--navy)';
|
|
msg.textContent = 'Invitation sent to ' + email + '. They will receive an email to set up their account.';
|
|
document.getElementById('child-email').value = '';
|
|
document.getElementById('child-dob-year').value = '';
|
|
document.getElementById('family-attest').checked = false;
|
|
loadFamilyMembers(parentEmail);
|
|
} else {
|
|
msg.style.color = '#c44';
|
|
msg.textContent = d.error || 'Something went wrong.';
|
|
}
|
|
};
|
|
|
|
window.removeFamilyMember = async function(childEmail) {
|
|
var session = await sb.auth.getSession();
|
|
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
|
await fetch('/api/family/remove', {
|
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({parent_email: parentEmail, child_email: childEmail})
|
|
});
|
|
loadFamilyMembers(parentEmail);
|
|
};
|
|
|
|
function renderUserChip(user) {
|
|
var email = user.email || '';
|
|
var avatarEl = document.getElementById('user-avatar-el');
|
|
var emailEl = document.getElementById('user-email-el');
|
|
var headerEmailEl = document.getElementById('acct-header-email');
|
|
if (emailEl) emailEl.textContent = email;
|
|
if (headerEmailEl) headerEmailEl.textContent = email;
|
|
var avatarUrl = user.user_metadata && user.user_metadata.avatar_url;
|
|
if (avatarEl) {
|
|
if (avatarUrl) {
|
|
avatarEl.innerHTML = '<img src=\"' + avatarUrl + '\" alt=\"\" referrerpolicy=\"no-referrer\">';
|
|
} else {
|
|
avatarEl.textContent = email ? email.charAt(0).toUpperCase() : '?';
|
|
}
|
|
}
|
|
}
|
|
|
|
function showNoPlan() {
|
|
var el = document.getElementById('plan-card');
|
|
if (!el) return;
|
|
el.innerHTML = '<div class=\"card-label\">Your plan</div>'
|
|
+ '<p style=\"font-family:var(--body);font-weight:500;font-size:1.125rem;color:var(--t1);margin-bottom:.75rem\">No active plan</p>'
|
|
+ '<p style=\"font-family:var(--body);font-weight:300;font-size:.9rem;color:var(--t2);line-height:1.7;margin-bottom:1.5rem\">You have an account but no plan selected yet. Pick one below to preorder.</p>'
|
|
+ '<div style=\"display:flex;gap:1rem;flex-wrap:wrap\">'
|
|
+ '<a href=\"/checkout?plan=founding\" class=\"btn-primary\" style=\"padding:.75rem 1.5rem\">Founding Member - $199 →</a>'
|
|
+ '<a href=\"/checkout?plan=professional\" class=\"btn-ghost\" style=\"padding:.75rem 1.5rem\">Professional - $19/mo</a>'
|
|
+ '<a href=\"/checkout?plan=free\" class=\"btn-ghost\" style=\"padding:.75rem 1.5rem\">Free tier</a>'
|
|
+ '</div>';
|
|
}
|
|
|
|
async function loadWaitlistData() {
|
|
try {
|
|
var sess = await sb.auth.getSession();
|
|
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
|
|
if (!token) { showNoPlan(); return; }
|
|
var r = await fetch('/api/my-plan', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({access_token: token})
|
|
});
|
|
var row = await r.json();
|
|
if (!row || !row.plan) { showNoPlan(); return; }
|
|
renderPlanCard(row);
|
|
} catch (e) {
|
|
showNoPlan();
|
|
}
|
|
}
|
|
|
|
async function loadApiKeys() {
|
|
var apiSection = document.getElementById('api-keys-section');
|
|
if (apiSection) apiSection.style.display = '';
|
|
try {
|
|
var sess = await sb.auth.getSession();
|
|
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
|
|
if (!token) return;
|
|
var r = await fetch('/api/api-keys', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({access_token: token})
|
|
});
|
|
var d = await r.json();
|
|
if (!d.rows || !d.rows.length) return;
|
|
d.rows.forEach(function(row) {
|
|
var provider = row.provider;
|
|
var keyVal = row.key_value || '';
|
|
var maskedEl = document.getElementById('apikey-masked-' + provider);
|
|
var delBtn = document.getElementById('apikey-del-' + provider);
|
|
if (keyVal) {
|
|
var klen = keyVal.length;
|
|
var masked = klen <= 10 ? '••••••••' : keyVal.slice(0, 6) + '••••' + keyVal.slice(-4);
|
|
if (maskedEl) maskedEl.textContent = masked;
|
|
if (delBtn) delBtn.style.display = '';
|
|
}
|
|
});
|
|
} catch (e) {}
|
|
}
|
|
|
|
window.saveApiKey = async function(provider) {
|
|
var input = document.getElementById('apikey-input-' + provider);
|
|
var msg = document.getElementById('api-keys-msg');
|
|
if (!input || !input.value.trim()) {
|
|
if (msg) { msg.style.display = 'block'; msg.style.color = '#c44'; msg.textContent = 'Enter a key first.'; }
|
|
return;
|
|
}
|
|
var keyVal = input.value.trim();
|
|
var sess = await sb.auth.getSession();
|
|
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
|
|
if (!token) return;
|
|
try {
|
|
var r = await fetch('/api/api-keys/save', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({access_token: token, provider: provider, key: keyVal})
|
|
});
|
|
var d = await r.json();
|
|
if (d.ok) {
|
|
var maskedEl = document.getElementById('apikey-masked-' + provider);
|
|
var delBtn = document.getElementById('apikey-del-' + provider);
|
|
if (maskedEl) maskedEl.textContent = d.masked;
|
|
if (delBtn) delBtn.style.display = '';
|
|
input.value = '';
|
|
if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--navy)'; msg.textContent = provider.charAt(0).toUpperCase() + provider.slice(1) + ' key saved.'; }
|
|
setTimeout(function() { if (msg) msg.style.display = 'none'; }, 3000);
|
|
} else {
|
|
if (msg) { msg.style.display = 'block'; msg.style.color = '#c44'; msg.textContent = d.error || 'Save failed.'; }
|
|
}
|
|
} catch (e) {
|
|
if (msg) { msg.style.display = 'block'; msg.style.color = '#c44'; msg.textContent = 'Network error.'; }
|
|
}
|
|
};
|
|
|
|
window.deleteApiKey = async function(provider) {
|
|
var sess = await sb.auth.getSession();
|
|
var token = sess.data && sess.data.session ? sess.data.session.access_token : '';
|
|
if (!token) return;
|
|
try {
|
|
var r = await fetch('/api/api-keys/delete', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({access_token: token, provider: provider})
|
|
});
|
|
var d = await r.json();
|
|
if (d.ok) {
|
|
var maskedEl = document.getElementById('apikey-masked-' + provider);
|
|
var delBtn = document.getElementById('apikey-del-' + provider);
|
|
if (maskedEl) maskedEl.textContent = 'Not configured';
|
|
if (delBtn) delBtn.style.display = 'none';
|
|
var msg = document.getElementById('api-keys-msg');
|
|
if (msg) { msg.style.display = 'block'; msg.style.color = 'var(--t3)'; msg.textContent = provider.charAt(0).toUpperCase() + provider.slice(1) + ' key removed.'; }
|
|
setTimeout(function() { if (msg) msg.style.display = 'none'; }, 3000);
|
|
}
|
|
} catch (e) {}
|
|
};
|
|
|
|
function showDashboard(user) {
|
|
hide('signin-section');
|
|
show('dashboard-section');
|
|
renderUserChip(user);
|
|
loadWaitlistData();
|
|
loadApiKeys();
|
|
}
|
|
|
|
async function init() {
|
|
var result = await sb.auth.getSession();
|
|
var session = result.data && result.data.session;
|
|
if (session && session.user) {
|
|
showDashboard(session.user);
|
|
} else {
|
|
show('signin-section');
|
|
hide('dashboard-section');
|
|
}
|
|
sb.auth.onAuthStateChange(function(event, session) {
|
|
if (session && session.user) {
|
|
showDashboard(session.user);
|
|
} else {
|
|
show('signin-section');
|
|
hide('dashboard-section');
|
|
}
|
|
});
|
|
}
|
|
|
|
init();
|
|
})()")
|
|
}
|