\n'; } function publish(){ if (!cms.art || !isDirty()) return; var html = buildDocument(); if (html.length > 15 * 1024 * 1024){ cms.msg = 'The site is too large to publish (over 15 MB). Remove or replace some large images and try again.'; cms.msgKind = 'err'; renderCMS(); return; } cms.status = 'Publishing…'; cms.msg = ''; updateStatus(); var snap = JSON.stringify({base:PUBLISHED, data:data}); clearDraft(); cms.art.publish(html).then(function(){ cms.status = ''; cms.msg = 'Published. The page will reload with your changes.'; cms.msgKind = ''; renderCMS(); }, function(e){ try { localStorage.setItem(DRAFT_KEY, snap); } catch (x) {} var code = e && e.code; cms.status = ''; cms.msg = code === 'no_server' ? 'Couldn’t reach save.php, so your updated site was downloaded as index.html. Upload it to public_html to go live.' : code === 'bad_password' ? 'Your editor password wasn’t accepted. Reload with ?edit and sign in again.' : code === 'too_large' ? 'The site is too large for the server to save. Use smaller images and try again.' : code === 'conflict' ? 'Someone else published a newer version first. Your edits are kept as a draft on this device.' : (code === 'not_granted' || code === 'not_writer') ? 'You don’t have permission to publish this site.' : 'Publishing didn’t go through (' + (code || 'unknown error') + '). Your changes are still here — try again in a moment.'; cms.msgKind = 'err'; renderCMS(); }); } function onInput(e){ var t = e.target, p = t.getAttribute('data-p'); if (!p) return; var v = t.getAttribute('data-bool') ? t.checked : t.value; if (t.type === 'color'){ setPath(data, p, v); var tw = t.parentNode.querySelector('[data-hex]'); if (tw) tw.value = v; changed(); return; } if (t.getAttribute('data-hex')){ if (!/^#[0-9a-f]{6}$/i.test(v)) return; setPath(data, p, v); var sw = t.parentNode.querySelector('input[type=color]'); if (sw) sw.value = v; changed(); return; } setPath(data, p, v); changed(); if (e.type === 'change' && (t.getAttribute('data-rerender') || t.getAttribute('data-bool'))) renderCMS(); } function onClick(e){ var b = e.target.closest('[data-act]'); if (!b || !document.getElementById('cms-root').contains(b)) return; var a = b.getAttribute('data-act'), p = b.getAttribute('data-p'), i = +b.getAttribute('data-i'); if (a !== 'del' && a !== 'discard') cms.confirm = null; if (a === 'tab'){ cms.tab = b.getAttribute('data-tab'); cms.exportText = ''; cms.importOpen = false; } else if (a === 'toggle') cms.expanded[p] = !cms.expanded[p]; else if (a === 'close'){ cms.open = false; editing = false; cms.msg = ''; renderSite(); } else if (a === 'vis'){ var vo = getPath(data, p); vo.visible = !vo.visible; changed(); } else if (a === 'page'){ cms.expanded = {}; go(b.getAttribute('data-page'), b.getAttribute('data-page') === 'lofe' ? 'home' : 'valencea'); } else if (a === 'up' || a === 'down'){ var list = getPath(data, p), j = a === 'up' ? i - 1 : i + 1; if (j < 0 || j >= list.length) return; var tmp = list[i]; list[i] = list[j]; list[j] = tmp; var ek = p + '.' + i, ej = p + '.' + j, oi = cms.expanded[ek]; cms.expanded[ek] = cms.expanded[ej]; cms.expanded[ej] = oi; changed(); } else if (a === 'del'){ var key = p + '#' + i; if (cms.confirm !== key){ cms.confirm = key; renderCMS(); setTimeout(function(){ if (cms.confirm === key){ cms.confirm = null; renderCMS(); } }, 3500); return; } cms.confirm = null; getPath(data, p).splice(i, 1); Object.keys(cms.expanded).forEach(function(k){ if (k.indexOf(p + '.') === 0) delete cms.expanded[k]; }); changed(); } else if (a === 'add'){ var l = getPath(data, p); if (!Array.isArray(l)){ setPath(data, p, []); l = getPath(data, p); } l.push(JSON.parse(b.getAttribute('data-blank'))); cms.expanded[p + '.' + (l.length - 1)] = true; changed(); } else if (a === 'addsec'){ var type = document.getElementById('cms-new-type').value, base = JSON.parse(JSON.stringify(NEW_SECTION[type])), pf = page === 'lofe' ? 'lofe.sections' : 'sections', lst = getPath(data, pf); var used = arr(data.sections).concat(lofeSecs()).map(function(s){ return slug(s.key); }), k = type, n = 2; while (used.indexOf(k) > -1) k = type + '-' + (n++); var sec = Object.assign({key:k, type:type, visible:true, nav:'', bg:'night'}, base), ci = lst.findIndex(function(s){ return s.type === 'contact'; }); if (ci < 0) ci = lst.length; lst.splice(ci, 0, sec); cms.expanded = {}; cms.expanded[pf + '.' + ci] = true; changed(); } else if (a === 'clearimg'){ setPath(data, p, ''); changed(); } else if (a === 'resetcolors'){ data.brand.colors = {accent:'#FF5A36', brand:'#3D5BFF', night:'#101433'}; changed(); } else if (a === 'publish'){ publish(); return; } else if (a === 'discard'){ if (cms.confirm !== 'discard'){ cms.confirm = 'discard'; renderCMS(); setTimeout(function(){ if (cms.confirm === 'discard'){ cms.confirm = null; renderCMS(); } }, 3500); return; } cms.confirm = null; data = JSON.parse(PUBLISHED); clearDraft(); cms.msg = 'Changes discarded.'; cms.msgKind = ''; renderSite(); } else if (a === 'export'){ cms.exportText = JSON.stringify(data, null, 1); renderCMS(); if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(cms.exportText).then(function(){ cms.msg = 'Backup copied to your clipboard. Save it somewhere safe.'; cms.msgKind = ''; renderCMS(); }, function(){ cms.msg = 'Select the text below and copy it to keep a backup.'; renderCMS(); }); return; } else if (a === 'showimport') cms.importOpen = !cms.importOpen; else if (a === 'doimport'){ try { var nd = JSON.parse(document.getElementById('cms-import').value); if (!nd || !Array.isArray(nd.sections) || !nd.brand || !nd.seo) throw 0; data = nd; cms.importOpen = false; cms.msg = 'Backup restored. Review it, then publish.'; cms.msgKind = ''; renderSite(); saveDraft(); } catch (x){ cms.msg = 'That doesn’t look like a site backup. Paste the full text you copied with "Copy content backup".'; cms.msgKind = 'err'; } } renderCMS(); } function onFile(e){ var t = e.target, p = t.getAttribute('data-img'); if (!p || !t.files || !t.files[0]) return; var f = t.files[0]; if (!/^image\//.test(f.type)){ cms.msg = 'Choose a PNG, JPG, WebP or SVG image.'; cms.msgKind = 'err'; renderCMS(); return; } compress(f, p.indexOf('logoImage') > -1 ? 600 : 1600, function(url){ if (!url){ cms.msg = 'That image couldn’t be read. Try another file.'; cms.msgKind = 'err'; renderCMS(); return; } if (url.length > 1.5 * 1024 * 1024){ cms.msg = 'That image is still over 1 MB after resizing. Try a smaller file.'; cms.msgKind = 'err'; renderCMS(); return; } setPath(data, p, url); if (p === 'brand.logoImage') data.brand.logoMode = 'image'; cms.msg = ''; changed(); renderCMS(); }); } document.addEventListener('input', function(e){ if (e.target.closest('#cms-root') && e.target.type !== 'file') onInput(e); }); document.addEventListener('change', function(e){ if (!e.target.closest('#cms-root')) return; if (e.target.type === 'file') onFile(e); else onInput(e); }); document.addEventListener('click', function(e){ if (e.target.closest('#cms-root')) onClick(e); }); /* ---------- boot ---------- */ (function(){ var h = (location.hash || '').slice(1); page = pageOfId(h) || 'lofe'; renderSite(); var el = h && document.getElementById(h); if (el && h !== 'top' && h !== 'home') setTimeout(function(){ el.scrollIntoView(); }, 60); })(); if (window.claude && typeof window.claude.use === 'function'){ Promise.all([window.claude.use('artifact'), window.claude.use('user')]).then(function(r){ var art = r[0], user = r[1]; if (!art || !user) return; return user.canEdit().then(function(can){ if (!can) return; cms.art = art; try { var d = JSON.parse(localStorage.getItem(DRAFT_KEY) || 'null'); if (d && d.base === PUBLISHED && d.data && Array.isArray(d.data.sections)){ data = d.data; renderSite(); cms.msg = 'Restored your unpublished draft from this device.'; } else if (d) localStorage.removeItem(DRAFT_KEY); } catch (e) {} renderCMS(); }); }).catch(function(){}); } else selfHostEditor(); })();