← back to CelebritySignatures

public/account.js

183 lines

/* Celebrity Signature Murals — accounts + save-your-placement.
   Self-contained, injected by server.js into /murals so murals.html stays untouched.
   Reads the wall-studio's existing globals (cur, pos, wallDims, selectMural, drawScene). */
(function () {
  'use strict';
  const $ = (s, r = document) => r.querySelector(s);
  const el = (t, props = {}, html) => { const e = document.createElement(t); Object.assign(e, props); if (html != null) e.innerHTML = html; return e; };
  const api = (p, opt = {}) => fetch(p, { credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, ...opt }).then(r => r.json());
  let me = null;

  // ---- styles ----
  document.head.appendChild(el('style', {}, `
    .acct-bar{position:fixed;top:14px;right:18px;z-index:60;display:flex;gap:8px;align-items:center;font:14px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
    .acct-bar .pill{background:#fff;border:1px solid #e6e3dc;border-radius:999px;padding:8px 15px;cursor:pointer;color:#1a1a1a;box-shadow:0 1px 4px rgba(0,0,0,.06)}
    .acct-bar .pill.dark{background:#1a1a1a;color:#fff;border-color:#1a1a1a}
    .acct-menu{position:absolute;top:42px;right:0;background:#fff;border:1px solid #e6e3dc;border-radius:10px;box-shadow:0 8px 28px rgba(0,0,0,.14);overflow:hidden;display:none;min-width:170px}
    .acct-menu.open{display:block}
    .acct-menu button{display:block;width:100%;text-align:left;border:0;background:#fff;padding:11px 16px;font:inherit;cursor:pointer;color:#1a1a1a}
    .acct-menu button:hover{background:#f7f5f0}
    .acct-save{display:inline-flex;align-items:center;gap:7px;border:1px solid #1a1a1a;background:#1a1a1a;color:#fff;font:14px/1 inherit;padding:10px 16px;border-radius:9px;cursor:pointer;margin-top:6px}
    .acct-save.saved{background:#1d7a36;border-color:#1d7a36}
    .acct-ovl{position:fixed;inset:0;background:rgba(20,18,14,.45);z-index:70;display:none;align-items:center;justify-content:center}
    .acct-ovl.open{display:flex}
    .acct-modal{background:#fff;border-radius:14px;width:min(380px,92vw);padding:24px;font:15px/1.5 -apple-system,sans-serif;box-shadow:0 20px 60px rgba(0,0,0,.3)}
    .acct-modal h3{font-family:Georgia,serif;margin:0 0 4px;font-size:21px}
    .acct-modal p{color:#6b6b6b;margin:0 0 16px;font-size:13px}
    .acct-modal label{display:block;font-size:12px;color:#6b6b6b;margin:0 0 10px}
    .acct-modal input{width:100%;box-sizing:border-box;margin-top:5px;padding:10px 12px;border:1px solid #e6e3dc;border-radius:8px;font:inherit}
    .acct-modal .go{width:100%;border:0;background:#1a1a1a;color:#fff;padding:12px;border-radius:9px;font:inherit;font-size:15px;cursor:pointer;margin-top:6px}
    .acct-modal .alt{text-align:center;margin-top:12px;font-size:13px;color:#6b6b6b}
    .acct-modal .alt a{color:#1d7a36;cursor:pointer;text-decoration:underline}
    .acct-msg{font-size:13px;margin-top:10px;min-height:16px}
    .acct-msg.err{color:#b3261e}.acct-msg.ok{color:#1d7a36}
    .acct-draw{position:fixed;top:0;right:0;height:100%;width:min(380px,94vw);background:#f7f5f0;z-index:65;box-shadow:-8px 0 30px rgba(0,0,0,.18);transform:translateX(100%);transition:transform .25s;overflow:auto;padding:22px}
    .acct-draw.open{transform:none}
    .acct-draw h3{font-family:Georgia,serif;margin:0 0 14px;font-size:20px;display:flex;justify-content:space-between;align-items:center}
    .acct-draw h3 .x{cursor:pointer;color:#6b6b6b;font-size:22px;border:0;background:0}
    .save-card{background:#fff;border:1px solid #e6e3dc;border-radius:12px;overflow:hidden;margin-bottom:14px}
    .save-card img{width:100%;height:120px;object-fit:cover;display:block;background:#fff;border-bottom:1px solid #eee}
    .save-card .b{padding:11px 13px}
    .save-card .t{font-family:Georgia,serif;font-size:15px}
    .save-card .m{font-size:12px;color:#6b6b6b;margin:3px 0 9px}
    .save-card .row{display:flex;gap:8px}
    .save-card .row button{flex:1;border:1px solid #e6e3dc;background:#fff;border-radius:7px;padding:7px;font:inherit;font-size:13px;cursor:pointer}
    .save-card .row button.load{border-color:#1a1a1a;background:#1a1a1a;color:#fff}
    .acct-empty{color:#6b6b6b;font-size:14px;text-align:center;padding:30px 10px}
    .acct-toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1a1a1a;color:#fff;padding:11px 20px;border-radius:999px;z-index:80;font:14px sans-serif;opacity:0;transition:opacity .2s}
    .acct-toast.show{opacity:1}
  `));

  // ---- elements ----
  const bar = el('div', { className: 'acct-bar' });
  const acctPill = el('button', { className: 'pill' }, 'Sign in');
  const menu = el('div', { className: 'acct-menu' });
  menu.appendChild(el('button', { onclick: openSaves }, '🖼  My saved murals'));
  menu.appendChild(el('button', { onclick: logout }, 'Sign out'));
  bar.appendChild(acctPill); bar.appendChild(menu); document.body.appendChild(bar);

  const ovl = el('div', { className: 'acct-ovl' });
  const modal = el('div', { className: 'acct-modal' });
  ovl.appendChild(modal); document.body.appendChild(ovl);
  ovl.addEventListener('click', e => { if (e.target === ovl) ovl.classList.remove('open'); });

  const draw = el('div', { className: 'acct-draw' });
  document.body.appendChild(draw);
  const toast = el('div', { className: 'acct-toast' }); document.body.appendChild(toast);
  const flash = m => { toast.textContent = m; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 1900); };

  acctPill.onclick = () => { if (me) menu.classList.toggle('open'); else openAuth('login'); };
  document.addEventListener('click', e => { if (!bar.contains(e.target)) menu.classList.remove('open'); });

  // ---- save button into the studio ----
  function mountSaveBtn() {
    const studio = $('#studio'); if (!studio || $('#acctSaveBtn')) return;
    const btn = el('button', { id: 'acctSaveBtn', className: 'acct-save' }, '💾 Save this placement to my account');
    btn.onclick = savePlacement;
    const anchor = $('#readout') || $('.stage-wrap', studio) || studio;
    anchor.parentNode.insertBefore(btn, anchor.nextSibling);
  }

  // ---- read current placement from the studio's globals (resilient) ----
  function snapshot() {
    try {
      if (typeof cur === 'undefined' || !cur) return null;
      const wd = (typeof wallDims === 'function') ? wallDims() : { W: +($('#wallW') || {}).value || 0, H: +($('#wallH') || {}).value || 0 };
      const p = (typeof pos !== 'undefined' && pos) ? pos : { x: 0, y: 0 };
      return { mural: cur.slug, mural_title: cur.title, wall_w: wd.W, wall_h: wd.H,
        placement: { from_left_ft: +(+p.x).toFixed(2), off_floor_ft: +(+p.y).toFixed(2) },
        scene: (typeof curScene !== 'undefined') ? curScene : '' };
    } catch (e) { return null; }
  }

  async function savePlacement() {
    const snap = snapshot();
    if (!snap) return flash('Pick a mural in the studio first');
    if (!me) { openAuth('login', savePlacement); return; }
    const r = await api('/api/saves', { method: 'POST', body: JSON.stringify(snap) });
    if (r.ok) { const b = $('#acctSaveBtn'); b.classList.add('saved'); b.textContent = '✓ Saved to your account'; setTimeout(() => { b.classList.remove('saved'); b.innerHTML = '💾 Save this placement to my account'; }, 2200); flash('Saved to your account'); }
    else flash(r.error || 'Could not save');
  }

  // ---- auth modal ----
  function openAuth(mode, after) {
    modal.innerHTML = '';
    const isLogin = mode === 'login';
    modal.appendChild(el('h3', {}, isLogin ? 'Sign in' : 'Create your account'));
    modal.appendChild(el('p', {}, 'Save mural placements to your account and find them anytime.'));
    const f = el('form');
    if (!isLogin) f.appendChild(el('label', {}, 'Name<input name="name" autocomplete="name">'));
    f.appendChild(el('label', {}, 'Email<input name="email" type="email" required autocomplete="email">'));
    f.appendChild(el('label', {}, 'Password<input name="password" type="password" required minlength="6" autocomplete="' + (isLogin ? 'current-password' : 'new-password') + '">'));
    const go = el('button', { className: 'go', type: 'submit' }, isLogin ? 'Sign in' : 'Create account');
    f.appendChild(go);
    const msg = el('div', { className: 'acct-msg' });
    f.appendChild(msg);
    const alt = el('div', { className: 'alt' }, isLogin ? 'No account? <a>Create one</a>' : 'Have an account? <a>Sign in</a>');
    alt.querySelector('a').onclick = () => openAuth(isLogin ? 'signup' : 'login', after);
    f.appendChild(alt);
    f.onsubmit = async e => {
      e.preventDefault(); msg.className = 'acct-msg'; msg.textContent = 'Working…';
      const body = JSON.stringify({ name: f.name ? f.name.value : '', email: f.email.value, password: f.password.value });
      const r = await api('/api/auth/' + (isLogin ? 'login' : 'signup'), { method: 'POST', body });
      if (r.ok) { me = r.user; refreshBar(); ovl.classList.remove('open'); flash('Signed in as ' + me.email); if (after) after(); }
      else { msg.className = 'acct-msg err'; msg.textContent = r.error || 'Something went wrong'; }
    };
    modal.appendChild(f);
    ovl.classList.add('open');
    setTimeout(() => { const em = f.querySelector('input[type=email]'); em && em.focus(); }, 50);
  }

  async function logout() { await api('/api/auth/logout', { method: 'POST' }); me = null; refreshBar(); menu.classList.remove('open'); flash('Signed out'); }

  // ---- my saved murals drawer ----
  async function openSaves() {
    menu.classList.remove('open');
    draw.innerHTML = '';
    const h = el('h3', {}, 'My saved murals');
    const x = el('button', { className: 'x' }, '×'); x.onclick = () => draw.classList.remove('open'); h.appendChild(x);
    draw.appendChild(h);
    draw.classList.add('open');
    const r = await api('/api/saves');
    if (!r.ok) { draw.appendChild(el('div', { className: 'acct-empty' }, 'Please sign in.')); return; }
    if (!r.saves.length) { draw.appendChild(el('div', { className: 'acct-empty' }, 'No saved murals yet.<br>Place a mural on your wall and tap “Save”.')); return; }
    r.saves.forEach(s => {
      const card = el('div', { className: 'save-card' });
      card.appendChild(el('img', { src: '/output/portrait-' + s.mural + '.png', loading: 'lazy', onerror: function () { this.src = '/output/collage-' + s.mural + '.png'; } }));
      const b = el('div', { className: 'b' });
      b.appendChild(el('div', { className: 't' }, s.mural_title || s.mural));
      const pl = s.placement ? `${s.placement.from_left_ft} ft from left · ${s.placement.off_floor_ft} ft off floor` : '';
      b.appendChild(el('div', { className: 'm' }, `${s.wall_w || '?'}×${s.wall_h || '?'} ft wall · ${pl}`));
      const row = el('div', { className: 'row' });
      const load = el('button', { className: 'load' }, 'Load'); load.onclick = () => loadSave(s);
      const del = el('button', {}, 'Delete'); del.onclick = async () => { await api('/api/saves?id=' + s.id, { method: 'DELETE' }); openSaves(); };
      row.appendChild(load); row.appendChild(del); b.appendChild(row); card.appendChild(b); draw.appendChild(card);
    });
  }

  function loadSave(s) {
    try {
      const code = (typeof CAT !== 'undefined' && CAT) ? (CAT.murals.find(m => m.slug === s.mural) || {}).code : null;
      if (code && $('#selMural')) $('#selMural').value = code;
      if (s.wall_w && $('#wallW')) $('#wallW').value = s.wall_w;
      if (s.wall_h && $('#wallH')) $('#wallH').value = s.wall_h;
      if (typeof selectMural === 'function' && code) selectMural(code);
      if (typeof pos !== 'undefined' && s.placement) { pos.x = s.placement.from_left_ft; pos.y = s.placement.off_floor_ft; }
      if (typeof drawScene === 'function') drawScene();
      draw.classList.remove('open');
      const st = $('#studio'); st && st.scrollIntoView({ behavior: 'smooth' });
      flash('Loaded “' + (s.mural_title || s.mural) + '”');
    } catch (e) { flash('Could not load that placement'); }
  }

  function refreshBar() {
    if (me) { acctPill.classList.add('dark'); acctPill.textContent = '👤 ' + me.email.split('@')[0]; }
    else { acctPill.classList.remove('dark'); acctPill.textContent = 'Sign in'; }
  }

  // ---- init ----
  api('/api/auth/me').then(r => { me = (r && r.ok) ? r.user : null; refreshBar(); });
  const tryMount = () => { mountSaveBtn(); if (!$('#acctSaveBtn')) setTimeout(tryMount, 400); };
  if (document.readyState !== 'loading') tryMount(); else document.addEventListener('DOMContentLoaded', tryMount);
})();