/* ========================================================================= * Personal portfolio Q-App — create your site, publish WEBSITE from here * * For anyone: on-chain portfolio, Qortal ecosystem contribution, and * real-life achievements. Config: localStorage + DOCUMENT + WEBSITE ZIP. * ======================================================================= */ (function () { 'use strict'; const DEFAULT_PUBLISHER = 'Your Name'; const SITE_STORAGE_KEY = 'portfolio_sovereign_site_v3'; const SITE_DOCUMENT_ID = 'portfolio_sovereign_site_config_v3'; /** v2 localStorage + DOCUMENT id (legacy prefix; kept so older saves still migrate) */ const LEGACY_SITE_STORAGE_KEY = 'sj_sovereign_site_v2'; const LEGACY_SITE_DOCUMENT_ID = 'sj_sovereign_site_config_v2'; /** * Lowercase registered names that skip on-chain DOCUMENT (WEBSITE / “Publish my website” only). * Add only if you need that behaviour; leave empty in the template. */ const NO_DOCUMENT_QDN_NAME_KEYS = new Set([]); /** Reorderable page regions under #sn-app (header stays fixed at top). */ const LAYOUT_DEFAULT_ORDER = ['site', 'hero', 'apps', 'feed', 'pages', 'community', 'about', 'poi', 'editor', 'foot']; const LAYOUT_BLOCK_ELEMENTS = { site: 'sn-lane-site', hero: 'sn-lane-hero', apps: 'sn-lane-apps', feed: 'sn-lane-feed', pages: 'sn-lane-pages', community: 'sn-lane-community', about: 'sn-about-section', poi: 'sn-lane-poi', editor: 'sn-site-editor', foot: 'sn-lane-foot', }; const LAYOUT_LABELS = { site: 'Kicker line', hero: 'Hero and identity', apps: 'Featured Q-App', feed: 'Published Q-Apps embeds', pages: 'More pages', community: 'Community', about: 'Story and links', poi: 'World Map photos', editor: 'Site builder', foot: 'Footer', }; const LAYOUT_KEYS_NO_DELETE = new Set(['editor']); function isDocumentQdnEnabledForPublisher() { const k = String(publisherName() || '') .trim() .toLowerCase(); if (!k) return true; return !NO_DOCUMENT_QDN_NAME_KEYS.has(k); } function defaultUiTemplate() { return { docTitle: 'My Personal Website — Qortal', heroEyebrow: 'PORTFOLIO · QORTAL & REAL LIFE', nameFirst: 'Your', nameLast: 'Name', tagline: 'Your corner of the chain — apps you ship, how you help the network, and the achievements that matter offline. Edit everything in Site settings, then publish your own WEBSITE.', heroBtnWorkshop: 'Open featured Q-App →', heroBtnQapps: 'My Q-Apps on Qortal', heroBtnCommunity: 'Groups & life', identityCardTitle: 'Your Name', ownerLinePrefix: 'Name owner ·', visitorEyebrow: 'Your wallet', statQdn: 'QDN items', statApps: 'Q-Apps', statArb: 'Arb. txs', statMinted: 'Minted', statQort: 'QORT', statLinks: 'Links', sec01Eyebrow: 'SECTION · 01', sec01Title: 'Featured Q-App', sec01Sub: 'Pick one app you are proud of — live preview here, full screen in Qortal. A single flagship for your portfolio.', sec02Eyebrow: 'ON THE CHAIN', sec02Title: 'Published Q-Apps', sec02Sub: 'Tabs for Q-Apps you publish — your visible contribution to the Qortal ecosystem. Replace URLs in Site settings with your channels and apps.', embedOpenLabel: 'open in Qortal ↗', embedFallbackText: '$ connect in Qortal to load embedded Q-Apps', embedTabsAria: 'Your published Q-Apps', sec03Eyebrow: 'SECTION · 03', sec03Title: 'More pages & websites', sec03Sub: 'Optional extra pages (long stories, timelines, or nested sites) published from Site settings.', sec04Eyebrow: 'SECTION · 04', sec04Title: 'Story & achievements', linksAsideHead: '// quick links', linksAsideIntro: 'Saved qortal:// links with live previews via /render/ inside the Hub.', linkPreviewsAria: 'Portfolio links with live previews', footerLeft: '// your portfolio · sovereign', footerBuild: 'portfolio · qortal template', siteSettingsH3: 'My Personal Website', siteEditorLeadHtml: 'Build your own website here — the form below changes this page in real time. Set your registered name and fill in each section. Connect in the header, then Publish my website to put your public site on qortal://WEBSITE/yourName. Use Publish to QDN to save this Q-App’s config (DOCUMENT) on-chain; that button appears in the corner when you have unsaved config changes.', communityEyebrow: 'COMMUNITY', communityTitle: 'Groups & life counter', communitySub: 'Qortal group identifiers and a life stat, plus private listings you name yourself — the same class of data Crowetic’s Q-Community / importer tools use (e.g. publicGroupId / adminGroupId in a baked WEBSITE). Edit the JSON-like fields in the site builder; they ship inside your WEBSITE ZIP.', poiPhotosEyebrow: 'WORLD MAP', poiPhotosTitle: 'Photos from my public POIs', poiPhotosSub: 'Pictures already on your POI “wall” in World Map live on QDN as IMAGE resources — list them here for your portfolio.', }; } /** Default HTML for Section 04 — edit under Site settings → Story & achievements */ const DEFAULT_ABOUT_HTML = '

On Qortal — describe the apps you publish, channels you run, and how you contribute to the network: minting, tools, content, or community building. Link to your qortal://APP/… resources so visitors can open them in-app.

' + '

World Map photos — add pictures from your public POI “wall” in Site settings → World Map photos (paste each qortal://IMAGE/… link — same files World Map already published).

' + '

In real life — work, study, crafts, or causes you care about. Milestones or a short bio that does not have to mirror your on-chain identity.

' + '

This template is customized in Site settings, then Publish my website in the header on a name you own.

'; function defaultSiteConfig() { return { version: 3, /** Registered Qortal name this site tracks (QDN stats, DOCUMENT config, avatar). */ publisherName: DEFAULT_PUBLISHER, /** Flat copy map — every visible title / blurb (see applyChromeUi). */ ui: defaultUiTemplate(), aboutHtml: DEFAULT_ABOUT_HTML, workshop: { label: 'Open featured Q-App', qortalHref: 'qortal://APP/The People of Qortal', }, links: [ { id: uid(), label: 'The People of Qortal — hub', href: 'qortal://APP/The People of Qortal' }, { id: uid(), label: 'Q-Mail', href: 'qortal://APP/Q-Mail' }, { id: uid(), label: 'Q-Minter — governance', href: 'qortal://APP/Q-Minter' }, { id: uid(), label: 'ThankQ', href: 'qortal://APP/ThankQ' }, { id: uid(), label: 'World Map', href: 'qortal://APP/World Map' }, { id: uid(), label: 'Quitter', href: 'qortal://APP/Quitter' }, ], pages: [], embeds: [ { id: uid(), label: 'Q-Tube', qortalHref: 'qortal://APP/Q-Tube' }, { id: uid(), label: 'Quitter', qortalHref: 'qortal://APP/Quitter' }, { id: uid(), label: 'ThankQ', qortalHref: 'qortal://APP/ThankQ' }, ], /** Public POI wall photos: qortal://IMAGE/Name/worldmap_marker_*_photo_0 … (same identifiers World Map publishes) */ poiPhotos: [], /** * Qommunity / Crowetic-style: public & admin group IDs, life stat, private group rows. * Shipped in DOCUMENT + in WEBSITE \n' ); } /** * Pack static assets + embedded STATE.site (with chosen publisherName) into a STORE ZIP * for qortalRequest({ service: 'WEBSITE', data64 }). */ async function buildWebsiteZipBytes(siteSnapshot) { const base = ''; const fetchText = (path) => fetch(base + path, { cache: 'no-store' }).then((r) => { if (!r.ok) throw new Error('Missing ' + path + ' (' + r.status + ')'); return r.text(); }); const [htmlRaw, appPart, stylePart, manifestRaw, zipPart] = await Promise.all([ fetchText('index.html'), fetchText('app.js'), fetchText('style.css'), fetchText('manifest.json'), fetchText('zip-store.js'), ]); let favBytes; try { const fr = await fetch(base + 'favicon.svg', { cache: 'no-store' }); if (!fr.ok) throw new Error('no favicon'); favBytes = new Uint8Array(await fr.arrayBuffer()); } catch (_) { favBytes = new TextEncoder().encode(''); } const cfg = jsonForScriptTag(siteSnapshot); const htmlOut = injectEmbedConfig(htmlRaw, cfg); let manifestStr = manifestRaw; try { const m = JSON.parse(manifestRaw); const t = siteSnapshot && siteSnapshot.ui && siteSnapshot.ui.docTitle; if (t) m.title = String(t); m.description = (m.description || '') + ' Published as WEBSITE from the portfolio template.'; manifestStr = JSON.stringify(m, null, 2); } catch (_) {} if (typeof buildZipStore !== 'function') { throw new Error('zip-store.js did not define buildZipStore'); } return buildZipStore({ 'index.html': htmlOut, 'app.js': appPart, 'style.css': stylePart, 'manifest.json': manifestStr, 'favicon.svg': favBytes, 'zip-store.js': zipPart, }); } async function pickOwnedRegisteredName() { if (!STATE.inQortal) return null; let acc = STATE.userAccount; if (!acc || !acc.address) { try { acc = await qReq({ action: 'GET_USER_ACCOUNT' }); STATE.userAccount = acc; } catch (_) { toast('Connect your wallet first', 'warn'); return null; } } const raw = await qReq({ action: 'GET_ACCOUNT_NAMES', address: acc.address, limit: 200, offset: 0, }); const names = accountNamesList(raw); if (!names.length) { toast('You need at least one registered Qortal name to publish a WEBSITE', 'warn'); return null; } if (names.length === 1) return String(names[0]); const lines = names.slice(0, 40).join(', '); const chosen = window.prompt('Enter the registered name to publish this WEBSITE under:\n\n' + lines, names[0] || ''); if (chosen == null || !String(chosen).trim()) return null; const t = String(chosen).trim(); if (!names.includes(t)) { toast('That name is not registered to your wallet', 'warn'); return null; } return t; } async function onPublishMyWebsite() { if (!STATE.inQortal) { toast('Open inside Qortal to publish', 'warn'); return; } if (typeof buildZipStore !== 'function') { toast('ZIP builder missing — ensure zip-store.js is loaded', 'err'); return; } const publishName = await pickOwnedRegisteredName(); if (!publishName) return; try { toast('Building ZIP…'); const snap = mergeSite(JSON.parse(JSON.stringify(STATE.site))); snap.publisherName = publishName; const zipBytes = await buildWebsiteZipBytes(snap); const title = (snap.ui && snap.ui.docTitle && String(snap.ui.docTitle).trim()) || publishName + ' — website'; const websiteFile = new File([zipBytes], 'website.zip', { type: 'application/zip' }); await qReq({ action: 'PUBLISH_QDN_RESOURCE', name: publishName, service: 'WEBSITE', file: websiteFile, filename: 'website.zip', isMultiFileZip: true, title, description: 'My Personal Website — portfolio + Qommunity-style community (groups, life counter, private rows) in embedded site config', }); STATE.site.publisherName = publishName; await loadEverything(); saveLocalSite(); toast('Published WEBSITE on «' + publishName + '» — public site: ' + 'qortal://WEBSITE/' + publishName); } catch (e) { toast('Website publish failed: ' + (e && e.message ? e.message : e), 'err'); } } function base64ToObject(b64) { const bin = atob(b64); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); const json = new TextDecoder('utf-8').decode(bytes); return JSON.parse(json); } async function qReq(payload) { if (typeof qortalRequest !== 'function') { throw new Error('Qortal API unavailable (preview mode)'); } return qortalRequest(payload); } function normList(batch) { if (Array.isArray(batch)) return batch; if (batch && Array.isArray(batch.resources)) return batch.resources; return []; } /** qortal://APP/AppName/path/with/segments */ function parseQortalAppHref(href) { const s = String(href || '').trim(); const m = /^qortal:\/\/APP\/([^/]+)\/(.+)$/i.exec(s); if (m) { return { name: decodeURIComponentSafe(m[1]), path: decodeURIComponentSafe(m[2].replace(/\+/g, '%20')), }; } const simple = /^qortal:\/\/APP\/([^/?#]+)\/?$/i.exec(s); if (simple) return { name: decodeURIComponentSafe(simple[1]), path: null }; return null; } function decodeURIComponentSafe(s) { try { return decodeURIComponent(s); } catch (_) { return s; } } /** World Map POI wall photos: qortal://IMAGE/PublisherName/worldmap_marker_…_photo_0 */ function parseQortalImageHref(href) { const s = String(href || '').trim(); const m = /^qortal:\/\/IMAGE\/([^/]+)\/([^?#]+)$/i.exec(s); if (!m) return null; return { name: decodeURIComponentSafe(m[1].replace(/\+/g, '%20')), identifier: decodeURIComponentSafe(m[2].replace(/\+/g, '%20')), }; } async function resolvePoiImageSrc(parsed) { if (!parsed || !parsed.name || !parsed.identifier) return null; if (STATE.inQortal) { try { const u = await qReq({ action: 'GET_QDN_RESOURCE_URL', service: 'IMAGE', name: parsed.name, identifier: parsed.identifier, }); if (u && typeof u === 'string') return u; } catch (_) {} } return qdnUrl('IMAGE', parsed.name, parsed.identifier, null); } function mergeSite(raw) { const d = defaultSiteConfig(); if (!raw || typeof raw !== 'object') return d; const out = Object.assign({}, d, raw); if (!Array.isArray(out.links)) out.links = d.links; if (!Array.isArray(out.pages)) out.pages = d.pages; if (!Array.isArray(out.embeds) || !out.embeds.length) out.embeds = d.embeds; if (!Array.isArray(out.poiPhotos)) out.poiPhotos = []; if (typeof out.aboutHtml !== 'string' || !String(out.aboutHtml).trim()) out.aboutHtml = d.aboutHtml; if (typeof out.publisherName !== 'string' || !String(out.publisherName).trim()) { out.publisherName = d.publisherName; } const dui = d.ui || defaultUiTemplate(); out.ui = Object.assign({}, dui, typeof out.ui === 'object' && out.ui ? out.ui : {}); Object.keys(dui).forEach((k) => { if (out.ui[k] == null || out.ui[k] === '') out.ui[k] = dui[k]; }); if (out.ui.siteSettingsH3 === 'Visual site builder' || out.ui.siteSettingsH3 === 'My Website Builder') { out.ui.siteSettingsH3 = dui.siteSettingsH3; } const dcomm = d.community; if (!out.community || typeof out.community !== 'object') { out.community = Object.assign({}, dcomm); } else { out.community = Object.assign({}, dcomm, out.community); } if (!Array.isArray(out.community.privateGroups)) out.community.privateGroups = []; const dw = d.workshop; if (!out.workshop || typeof out.workshop !== 'object') { out.workshop = { label: dw.label, qortalHref: dw.qortalHref }; } else { if (typeof out.workshop.label !== 'string' || !out.workshop.label.trim()) out.workshop.label = dw.label; if (typeof out.workshop.qortalHref !== 'string' || !out.workshop.qortalHref.trim()) { out.workshop.qortalHref = dw.qortalHref; } } const dLay = d.layout; if (!out.layout || typeof out.layout !== 'object') { out.layout = { order: dLay.order.slice(), hidden: { ...dLay.hidden } }; } else { if (!Array.isArray(out.layout.order)) { out.layout.order = dLay.order.slice(); } else { out.layout.order = normalizeLayoutOrder(out.layout.order); } if (!out.layout.hidden || typeof out.layout.hidden !== 'object') { out.layout.hidden = {}; } else { const h = {}; LAYOUT_DEFAULT_ORDER.forEach((k) => { if (out.layout.hidden[k] === true) h[k] = true; }); out.layout.hidden = h; } } out.version = 3; return out; } function normalizeLayoutOrder(userOrder) { const d = LAYOUT_DEFAULT_ORDER; const seen = new Set(); const out = []; (Array.isArray(userOrder) ? userOrder : []).forEach((k) => { if (d.indexOf(k) === -1) return; if (seen.has(k)) return; seen.add(k); out.push(k); }); d.forEach((k) => { if (!seen.has(k)) out.push(k); }); return out; } function getLayoutNodeByKey(key) { const elId = LAYOUT_BLOCK_ELEMENTS[key]; if (!elId) return null; return document.getElementById(elId); } function ensureLayoutState() { if (!STATE.site.layout || typeof STATE.site.layout !== 'object') { STATE.site.layout = { order: LAYOUT_DEFAULT_ORDER.slice(), hidden: {} }; } if (!Array.isArray(STATE.site.layout.order)) { STATE.site.layout.order = LAYOUT_DEFAULT_ORDER.slice(); } STATE.site.layout.order = normalizeLayoutOrder(STATE.site.layout.order); if (!STATE.site.layout.hidden || typeof STATE.site.layout.hidden !== 'object') { STATE.site.layout.hidden = {}; } } function applyPageLayout() { const app = document.getElementById('sn-app'); if (!app) return; ensureLayoutState(); const order = STATE.site.layout.order; const hidden = STATE.site.layout.hidden; const header = app.querySelector('header.sn-top'); if (!header) return; let prev = header; order.forEach((key) => { const el = getLayoutNodeByKey(key); if (!el || el.parentNode !== app) return; if (hidden[key] === true) { el.hidden = true; el.setAttribute('aria-hidden', 'true'); el.setAttribute('data-sn-layout-off', '1'); } else { el.hidden = false; el.setAttribute('aria-hidden', 'false'); el.removeAttribute('data-sn-layout-off'); } app.insertBefore(el, prev.nextSibling); prev = el; }); syncLayoutBlockChrome(); renderLayoutRestoreList(); } function renderLayoutRestoreList() { const ul = document.getElementById('sn-layout-restore-list'); const details = document.getElementById('sn-layout-restore-wrap'); if (!ul) return; ensureLayoutState(); const h = STATE.site.layout.hidden || {}; const keys = LAYOUT_DEFAULT_ORDER.filter((k) => h[k] === true); ul.innerHTML = ''; if (!keys.length) { const li = document.createElement('li'); li.className = 'sn-mono sn-dim'; li.textContent = 'No sections are hidden. Use the × on a block in the page (Hub) to remove one from the public view.'; ul.appendChild(li); if (details) details.open = false; return; } if (details) details.open = true; keys.forEach((k) => { const li = document.createElement('li'); li.className = 'sn-layout-restore-item'; const label = LAYOUT_LABELS[k] || k; li.appendChild(document.createTextNode(label + ' ')); const b = document.createElement('button'); b.type = 'button'; b.className = 'sn-btn sn-btn-ghost sn-btn-sm'; b.textContent = 'Show again'; b.setAttribute('data-sn-lc-restore', k); b.title = 'Show this block on the page again'; li.appendChild(b); ul.appendChild(li); }); } function syncLayoutBlockChrome() { const app = document.getElementById('sn-app'); if (!app) return; ensureLayoutState(); const order = STATE.site.layout.order; LAYOUT_DEFAULT_ORDER.forEach((key) => { const node = getLayoutNodeByKey(key); if (!node) return; let chrome = node.querySelector(':scope > .sn-layout-chrome'); if (chrome) chrome.remove(); if (!STATE.inQortal) { return; } chrome = document.createElement('div'); chrome.className = 'sn-layout-chrome'; chrome.setAttribute('data-sn-lc', key); const drag = document.createElement('span'); drag.className = 'sn-layout-chrome__drag'; drag.textContent = '⋮⋮'; drag.title = 'Drag to reorder with another block'; drag.setAttribute('draggable', 'true'); const up = document.createElement('button'); up.type = 'button'; up.className = 'sn-btn sn-btn-ghost sn-btn-sm sn-layout-chrome__btn'; up.textContent = '↑'; up.setAttribute('data-sn-lc-act', 'up'); up.title = 'Move this block up'; const down = document.createElement('button'); down.type = 'button'; down.className = 'sn-btn sn-btn-ghost sn-btn-sm sn-layout-chrome__btn'; down.textContent = '↓'; down.setAttribute('data-sn-lc-act', 'down'); down.title = 'Move this block down'; chrome.appendChild(drag); chrome.appendChild(up); chrome.appendChild(down); if (!LAYOUT_KEYS_NO_DELETE.has(key)) { const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'sn-btn sn-btn-ghost sn-btn-sm sn-layout-chrome__btn'; rm.textContent = '×'; rm.setAttribute('data-sn-lc-act', 'rm'); rm.title = 'Remove this block from the public page (restore below)'; chrome.appendChild(rm); } const idx = order.indexOf(key); if (idx <= 0) up.disabled = true; if (idx < 0 || idx >= order.length - 1) down.disabled = true; if (node.firstChild) { node.insertBefore(chrome, node.firstChild); } else { node.appendChild(chrome); } node.classList.add('sn-layout-block'); }); } function layoutActionMove(key, dir) { ensureLayoutState(); const o = STATE.site.layout.order.slice(); const i = o.indexOf(key); if (i < 0) return; if (dir === 'up' && i > 0) { const t = o[i - 1]; o[i - 1] = o[i]; o[i] = t; } else if (dir === 'down' && i < o.length - 1) { const t = o[i + 1]; o[i + 1] = o[i]; o[i] = t; } else { return; } STATE.site.layout.order = o; saveLocalSite(); applyPageLayout(); } function layoutActionHide(key) { if (LAYOUT_KEYS_NO_DELETE.has(key)) { toast('The builder panel always stays on the page so you can edit and restore other sections.', 'warn'); return; } ensureLayoutState(); STATE.site.layout.hidden[key] = true; saveLocalSite(); applyPageLayout(); } function layoutMoveKeyBefore(from, to) { ensureLayoutState(); if (from === to) return; const o = STATE.site.layout.order.slice(); const iFrom = o.indexOf(from); const iTo = o.indexOf(to); if (iFrom < 0 || iTo < 0) return; o.splice(iFrom, 1); const j = o.indexOf(to); o.splice(j, 0, from); STATE.site.layout.order = o; saveLocalSite(); applyPageLayout(); } function layoutRestoreKey(key) { ensureLayoutState(); if (STATE.site.layout.hidden[key] === true) { delete STATE.site.layout.hidden[key]; } saveLocalSite(); applyPageLayout(); } function wirePageLayoutOnce() { const app = document.getElementById('sn-app'); if (!app || app.dataset.snLayoutControls) return; app.dataset.snLayoutControls = '1'; app.addEventListener('click', (e) => { const r = e.target.closest('[data-sn-lc-restore]'); if (r && app.contains(r)) { e.preventDefault(); e.stopPropagation(); layoutRestoreKey(r.getAttribute('data-sn-lc-restore') || ''); return; } const btn = e.target.closest('[data-sn-lc-act]'); if (!btn || !app.contains(btn)) return; e.preventDefault(); e.stopPropagation(); const wrap = btn.closest('.sn-layout-chrome'); const key = wrap && wrap.getAttribute('data-sn-lc'); if (!key) return; const act = btn.getAttribute('data-sn-lc-act'); if (act === 'up') layoutActionMove(key, 'up'); else if (act === 'down') layoutActionMove(key, 'down'); else if (act === 'rm') layoutActionHide(key); }); app.addEventListener('dragstart', (e) => { const h = e.target.closest('.sn-layout-chrome__drag'); if (!h || !app.contains(h)) return; const block = h.closest('[data-sn-layout-block]'); if (!block) return; const k = block.getAttribute('data-sn-layout-block'); if (!k) return; e.dataTransfer.setData('text/plain', k); e.dataTransfer.effectAllowed = 'move'; }); app.addEventListener('dragover', (e) => { if (!e.target.closest('[data-sn-layout-block]')) return; e.preventDefault(); }); app.addEventListener('drop', (e) => { const block = e.target.closest('[data-sn-layout-block]'); if (!block || !app.contains(block)) return; e.preventDefault(); const from = e.dataTransfer.getData('text/plain'); const to = block.getAttribute('data-sn-layout-block'); if (!from || !to || from === to) return; layoutMoveKeyBefore(from, to); }); } function loadLocalSite() { try { let t = localStorage.getItem(SITE_STORAGE_KEY); if (!t) { t = localStorage.getItem(LEGACY_SITE_STORAGE_KEY); if (t) { try { localStorage.setItem(SITE_STORAGE_KEY, t); localStorage.removeItem(LEGACY_SITE_STORAGE_KEY); } catch (_) {} } } if (!t) return null; return mergeSite(JSON.parse(t)); } catch (_) { return null; } } function saveLocalSite() { try { localStorage.setItem(SITE_STORAGE_KEY, JSON.stringify(STATE.site)); } catch (e) { toast('Could not save locally: ' + (e.message || e), 'err'); } updateFloatingQdnBar(); } async function fetchSiteFromQDN() { if (!isDocumentQdnEnabledForPublisher()) return null; const ids = [SITE_DOCUMENT_ID, LEGACY_SITE_DOCUMENT_ID]; for (let i = 0; i < ids.length; i++) { try { const raw = await qReq({ action: 'FETCH_QDN_RESOURCE', name: publisherName(), service: 'DOCUMENT', identifier: ids[i], }); if (raw == null || raw === '') continue; if (typeof raw === 'object' && raw !== null && !ArrayBuffer.isView(raw) && !(raw instanceof ArrayBuffer)) { return mergeSite(raw); } const str = typeof raw === 'string' ? raw : new TextDecoder().decode(new Uint8Array(raw)); if (str && str.trim()) return mergeSite(JSON.parse(str)); } catch (_) {} } return null; } async function tryLoadSiteConfig() { const embedEl = document.getElementById('sn-embed-config'); if (embedEl && embedEl.textContent && embedEl.textContent.trim()) { try { STATE.site = mergeSite(JSON.parse(embedEl.textContent.trim())); return; } catch (e) { console.warn('Embedded site config', e); } } const local = loadLocalSite(); if (local) { STATE.site = local; return; } let merged = mergeSite(null); if (STATE.inQortal) { try { const remote = await fetchSiteFromQDN(); if (remote) merged = remote; } catch (_) { /* no published config yet */ } } STATE.site = merged; } function uiStr(key, fallback) { const u = STATE.site && STATE.site.ui; const v = u && u[key]; return v != null && String(v).length ? String(v) : fallback || ''; } function ensureCommunity() { const s = STATE.site; if (!s.community || typeof s.community !== 'object') { s.community = Object.assign({}, defaultSiteConfig().community); } if (!Array.isArray(s.community.privateGroups)) s.community.privateGroups = []; return s.community; } function applyCommunityDataToDom() { const c = ensureCommunity(); const sec = document.getElementById('sn-community-section'); if (sec) sec.hidden = c.sectionHidden === true; const setN = (id, v) => { const n = document.getElementById(id); if (!n) return; if (wysiIsEditingNode(n)) return; const t = v == null ? '' : String(v).trim(); n.textContent = t === '' ? '—' : t; }; setN('sn-life-label', c.lifeLabel); setN('sn-life-value', c.lifeValue); setN('sn-pub-gid', c.publicGroupId); setN('sn-pub-gname', c.publicGroupName); setN('sn-adm-gid', c.adminGroupId); setN('sn-adm-gname', c.adminGroupName); const host = document.getElementById('sn-private-groups'); if (!host) return; host.innerHTML = ''; const list = c.privateGroups || []; if (!list.length) { const p = el('p', 'sn-mono sn-dim sn-community-empty'); p.textContent = 'No private groups listed — add rows in the site builder.'; host.appendChild(p); return; } list.forEach((row) => { const card = el('div', 'sn-private-group-card'); const name = el('div', 'sn-private-group-name', row.name || 'Unnamed'); const gid = row.groupId != null ? String(row.groupId).trim() : ''; const idl = el('div', 'sn-private-group-id sn-mono sn-dim', gid ? 'Group ID: ' + gid : ''); const badge = el('div', 'sn-private-group-badge sn-mono', 'private'); const top = el('div', 'sn-private-group-top'); top.appendChild(badge); top.appendChild(name); card.appendChild(top); if (gid) card.appendChild(idl); if (row.note && String(row.note).trim()) { const note = el('p', 'sn-private-group-note sn-dim', String(row.note).trim()); card.appendChild(note); } host.appendChild(card); }); } function isWysiwygContextActive() { return !!( STATE.wysiwygMode !== false && STATE.inQortal && STATE.userAccount ); } function wysiIsEditingNode(n) { if (!n || !isWysiwygContextActive()) return false; const ae = document.activeElement; if (!ae) return false; return n === ae || n.contains(ae); } const WYSIWYG_PAGE_CONFIG = [ { id: 'sn-hero-eyebrow', key: 'heroEyebrow', html: false, kind: 'ui' }, { id: 'sn-name-first', key: 'nameFirst', html: false, kind: 'ui' }, { id: 'sn-name-last', key: 'nameLast', html: false, kind: 'ui' }, { id: 'sn-tagline', key: 'tagline', html: true, kind: 'ui' }, { id: 'sn-hero-lbl-apps', key: 'heroBtnWorkshop', html: false, kind: 'ui' }, { id: 'sn-hero-lbl-feed', key: 'heroBtnQapps', html: false, kind: 'ui' }, { id: 'sn-hero-lbl-community', key: 'heroBtnCommunity', html: false, kind: 'ui' }, { id: 'sn-id-name', key: 'identityCardTitle', html: false, kind: 'ui' }, { id: 'sn-owner-line-prefix', key: 'ownerLinePrefix', html: false, kind: 'ui' }, { id: 'sn-visitor-eyebrow', key: 'visitorEyebrow', html: false, kind: 'ui' }, { id: 'sn-stat-l-qdn', key: 'statQdn', html: false, kind: 'ui' }, { id: 'sn-stat-l-apps', key: 'statApps', html: false, kind: 'ui' }, { id: 'sn-stat-l-arb', key: 'statArb', html: false, kind: 'ui' }, { id: 'sn-stat-l-blocks', key: 'statMinted', html: false, kind: 'ui' }, { id: 'sn-stat-l-balance', key: 'statQort', html: false, kind: 'ui' }, { id: 'sn-stat-l-links', key: 'statLinks', html: false, kind: 'ui' }, { id: 'sn-sec01-eyebrow', key: 'sec01Eyebrow', html: false, kind: 'ui' }, { id: 'sn-sec01-title', key: 'sec01Title', html: false, kind: 'ui' }, { id: 'sn-sec01-sub', key: 'sec01Sub', html: true, kind: 'ui' }, { id: 'sn-sec02-eyebrow', key: 'sec02Eyebrow', html: false, kind: 'ui' }, { id: 'sn-sec02-title', key: 'sec02Title', html: false, kind: 'ui' }, { id: 'sn-sec02-sub', key: 'sec02Sub', html: true, kind: 'ui' }, { id: 'sn-embed-open-label', key: 'embedOpenLabel', html: false, kind: 'ui' }, { id: 'sn-embed-fallback-text', key: 'embedFallbackText', html: false, kind: 'ui' }, { id: 'sn-sec03-eyebrow', key: 'sec03Eyebrow', html: false, kind: 'ui' }, { id: 'sn-sec03-title', key: 'sec03Title', html: false, kind: 'ui' }, { id: 'sn-sec03-sub', key: 'sec03Sub', html: true, kind: 'ui' }, { id: 'sn-comm-eyebrow', key: 'communityEyebrow', html: false, kind: 'ui' }, { id: 'sn-comm-title', key: 'communityTitle', html: false, kind: 'ui' }, { id: 'sn-comm-sub', key: 'communitySub', html: true, kind: 'ui' }, { id: 'sn-life-label', key: 'lifeLabel', html: false, kind: 'community' }, { id: 'sn-life-value', key: 'lifeValue', html: false, kind: 'community' }, { id: 'sn-pub-gname', key: 'publicGroupName', html: false, kind: 'community' }, { id: 'sn-pub-gid', key: 'publicGroupId', html: false, kind: 'community' }, { id: 'sn-adm-gname', key: 'adminGroupName', html: false, kind: 'community' }, { id: 'sn-adm-gid', key: 'adminGroupId', html: false, kind: 'community' }, { id: 'sn-sec04-eyebrow', key: 'sec04Eyebrow', html: false, kind: 'ui' }, { id: 'sn-sec04-title', key: 'sec04Title', html: false, kind: 'ui' }, { id: 'sn-poi-eyebrow', key: 'poiPhotosEyebrow', html: false, kind: 'ui' }, { id: 'sn-poi-title', key: 'poiPhotosTitle', html: false, kind: 'ui' }, { id: 'sn-poi-sub', key: 'poiPhotosSub', html: true, kind: 'ui' }, { id: 'sn-links-aside-head', key: 'linksAsideHead', html: false, kind: 'ui' }, { id: 'sn-links-aside-intro', key: 'linksAsideIntro', html: true, kind: 'ui' }, { id: 'sn-foot-left', key: 'footerLeft', html: false, kind: 'ui' }, { id: 'sn-build-tag', key: 'footerBuild', html: false, kind: 'ui' }, { id: 'sn-site-settings-h3', key: 'siteSettingsH3', html: false, kind: 'ui' }, { id: 'sn-site-editor-lead', key: 'siteEditorLeadHtml', html: true, kind: 'ui' }, { id: 'sn-about-prose', key: 'aboutHtml', html: true, kind: 'about' }, ]; const WYSIWYG_BY_ID = (() => { const m = {}; WYSIWYG_PAGE_CONFIG.forEach((r) => { m[r.id] = r; }); return m; })(); function wysiFlushFromNode(el) { const row = WYSIWYG_BY_ID[el.id]; if (!row) return; if (!STATE.site.ui) STATE.site.ui = defaultUiTemplate(); const v = row.html ? el.innerHTML : el.textContent; if (row.kind === 'about') { STATE.site.aboutHtml = v; } else if (row.kind === 'community') { const c = ensureCommunity(); c[row.key] = v; } else { STATE.site.ui[row.key] = v; } try { saveLocalSite(); } catch (_) {} if (row.kind === 'ui') { const inp = document.querySelector(`[data-vi-key="${row.key}"]`); if (inp && document.activeElement !== inp) { inp.value = v; } } else if (row.kind === 'about') { const ta = document.getElementById('sn-editor-about-html'); if (ta && document.activeElement !== ta) { ta.value = STATE.site.aboutHtml || DEFAULT_ABOUT_HTML; } } else if (row.kind === 'community') { const map = { lifeLabel: 'sn-comm-inp-life-label', lifeValue: 'sn-comm-inp-life-value', publicGroupName: 'sn-comm-inp-pub-gname', publicGroupId: 'sn-comm-inp-pub-gid', adminGroupName: 'sn-comm-inp-adm-gname', adminGroupId: 'sn-comm-inp-adm-gid', }; const iid = map[row.key]; if (iid) { const ii = document.getElementById(iid); if (ii && document.activeElement !== ii) ii.value = cVal(v); } } try { const uj = document.getElementById('sn-editor-ui-json'); if (uj && document.activeElement !== uj) uj.value = JSON.stringify(STATE.site.ui || {}, null, 2); } catch (_) {} } function cVal(x) { return x == null ? '' : String(x); } function updateWysiwygLayer() { const app = document.getElementById('sn-app'); const tgl = document.getElementById('sn-wysiwyg-toggle'); const tlab = document.getElementById('sn-wysiwyg-toggle-label'); if (tgl) { tgl.hidden = !STATE.inQortal || !STATE.userAccount; tgl.setAttribute('aria-hidden', tgl.hidden ? 'true' : 'false'); } if (tlab) { tlab.textContent = STATE.wysiwygMode !== false ? 'Visual page · on' : 'Visual page · off'; } if (tgl) { tgl.setAttribute('aria-pressed', STATE.wysiwygMode !== false ? 'true' : 'false'); } if (!isWysiwygContextActive()) { if (app) app.classList.remove('sn-app--wysiwyg'); WYSIWYG_PAGE_CONFIG.forEach((row) => { const n = document.getElementById(row.id); if (!n) return; n.removeAttribute('contenteditable'); n.classList.remove('sn-wysi-editable'); n.removeAttribute('data-sn-wysi-key'); n.removeAttribute('data-sn-wysi-html'); }); return; } if (app) app.classList.add('sn-app--wysiwyg'); WYSIWYG_PAGE_CONFIG.forEach((row) => { const n = document.getElementById(row.id); if (!n) return; n.setAttribute('data-sn-wysi-key', row.key); n.setAttribute('data-sn-wysi-html', row.html ? '1' : '0'); n.contentEditable = 'true'; n.setAttribute('spellcheck', 'true'); n.classList.add('sn-wysi-editable'); }); } function wireWysiwygToolbarOnce() { if (document.body.dataset.snWysiTb) return; document.body.dataset.snWysiTb = '1'; const tb = document.getElementById('sn-wysi-toolbar'); if (!tb) return; tb.addEventListener('mousedown', (e) => { e.preventDefault(); }); tb.addEventListener('click', (e) => { const b = e.target.closest('[data-wysi-cmd]'); if (!b) return; e.preventDefault(); const cmd = b.getAttribute('data-wysi-cmd') || ''; if (cmd === 'link') { const u = window.prompt('Link URL (qortal://):', 'qortal://'); if (u) document.execCommand('createLink', false, u); } else { try { document.execCommand(cmd, false, null); } catch (err) { console.warn('execCommand', err); } } }); } function positionWysiwygToolbar() { const tb = document.getElementById('sn-wysi-toolbar'); if (!tb || tb.hidden) return; const sel = window.getSelection(); if (!sel || !sel.rangeCount) return; const r = sel.getRangeAt(0); if (r.collapsed) { const el = r.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? r.commonAncestorContainer : r.commonAncestorContainer.parentElement; const h = el && el.closest && el.closest('[data-sn-wysi-html="1"]'); if (h) { const rect = h.getBoundingClientRect(); const tw = Math.min(280, window.innerWidth - 16); tb.style.left = Math.max(8, Math.min(window.innerWidth - tw - 8, rect.left)) + 'px'; tb.style.top = Math.max(8, rect.top - 44) + 'px'; return; } } } function wireWysiwygInputOnce() { if (document.body.dataset.snWysiIn) return; document.body.dataset.snWysiIn = '1'; let deb; const go = (t) => { if (t && t.id && WYSIWYG_BY_ID[t.id] && t.isContentEditable) { wysiFlushFromNode(t); if (WYSIWYG_BY_ID[t.id].html) { const tb = document.getElementById('sn-wysi-toolbar'); if (tb) { tb.hidden = false; positionWysiwygToolbar(); } } updateBrandLine(); updateFloatingQdnBar(); applyCommunityDataToDom(); } }; document.addEventListener('input', (e) => { const t = e.target; if (!t || !t.id || !WYSIWYG_BY_ID[t.id]) return; if (deb) clearTimeout(deb); deb = setTimeout(() => go(t), 120); }, true); document.addEventListener( 'blur', (e) => { const t = e.target; if (t && t.id && WYSIWYG_BY_ID[t.id] && t.isContentEditable) { wysiFlushFromNode(t); } setTimeout(() => { const tb = document.getElementById('sn-wysi-toolbar'); if (tb && !tb.matches(':hover')) { const ae = document.activeElement; if (!ae || !ae.closest || !ae.closest('.sn-wysi-toolbar')) { if (tb) tb.hidden = true; } } }, 0); }, true ); document.addEventListener('selectionchange', () => { if (!isWysiwygContextActive()) return; const sel = window.getSelection(); if (!sel || !sel.rangeCount) return; const r = sel.getRangeAt(0); const el = r.commonAncestorContainer.nodeType === Node.ELEMENT_NODE ? r.commonAncestorContainer : r.commonAncestorContainer.parentElement; const h = el && el.closest && el.closest('[data-sn-wysi-html="1"]'); const tb = document.getElementById('sn-wysi-toolbar'); if (!h || h !== document.activeElement) { if (tb) tb.hidden = !h; } if (h) { if (tb) { tb.hidden = false; const rect = h.getBoundingClientRect(); const tw = Math.min(280, window.innerWidth - 16); tb.style.left = Math.max(8, Math.min(window.innerWidth - tw - 8, rect.left)) + 'px'; tb.style.top = Math.max(8, rect.top - 40) + 'px'; } } }); wireWysiwygToolbarOnce(); } /** Push `ui` strings into the static shell (ids in index.html). */ function applyChromeUi() { const t = uiStr('docTitle', ''); if (t) document.title = t; const set = (id, text, asHtml) => { const n = document.getElementById(id); if (!n) return; if (wysiIsEditingNode(n)) return; if (asHtml) n.innerHTML = text; else n.textContent = text; }; const setHtml = (id, h) => set(id, h, true); updateBrandLine(); set('sn-hero-eyebrow', uiStr('heroEyebrow')); set('sn-name-first', uiStr('nameFirst')); set('sn-name-last', uiStr('nameLast')); setHtml('sn-tagline', uiStr('tagline')); set('sn-hero-lbl-apps', uiStr('heroBtnWorkshop')); set('sn-hero-lbl-feed', uiStr('heroBtnQapps')); set('sn-hero-lbl-community', uiStr('heroBtnCommunity')); set('sn-id-name', uiStr('identityCardTitle')); set('sn-owner-line-prefix', uiStr('ownerLinePrefix')); set('sn-visitor-eyebrow', uiStr('visitorEyebrow')); document.querySelectorAll('[data-sn-stat-l="qdn"]').forEach((e) => { if (!wysiIsEditingNode(e)) e.textContent = uiStr('statQdn'); }); document.querySelectorAll('[data-sn-stat-l="apps"]').forEach((e) => { if (!wysiIsEditingNode(e)) e.textContent = uiStr('statApps'); }); document.querySelectorAll('[data-sn-stat-l="arb"]').forEach((e) => { if (!wysiIsEditingNode(e)) e.textContent = uiStr('statArb'); }); document.querySelectorAll('[data-sn-stat-l="blocks"]').forEach((e) => { if (!wysiIsEditingNode(e)) e.textContent = uiStr('statMinted'); }); document.querySelectorAll('[data-sn-stat-l="balance"]').forEach((e) => { if (!wysiIsEditingNode(e)) e.textContent = uiStr('statQort'); }); document.querySelectorAll('[data-sn-stat-l="links"]').forEach((e) => { if (!wysiIsEditingNode(e)) e.textContent = uiStr('statLinks'); }); set('sn-sec01-eyebrow', uiStr('sec01Eyebrow')); set('sn-sec01-title', uiStr('sec01Title')); setHtml('sn-sec01-sub', uiStr('sec01Sub')); set('sn-sec02-eyebrow', uiStr('sec02Eyebrow')); set('sn-sec02-title', uiStr('sec02Title')); setHtml('sn-sec02-sub', uiStr('sec02Sub')); set('sn-embed-open-label', uiStr('embedOpenLabel')); set('sn-embed-fallback-text', uiStr('embedFallbackText')); const tabs = $('#sn-embed-tabs'); if (tabs) tabs.setAttribute('aria-label', uiStr('embedTabsAria')); set('sn-sec03-eyebrow', uiStr('sec03Eyebrow')); set('sn-sec03-title', uiStr('sec03Title')); setHtml('sn-sec03-sub', uiStr('sec03Sub')); set('sn-comm-eyebrow', uiStr('communityEyebrow')); set('sn-comm-title', uiStr('communityTitle')); setHtml('sn-comm-sub', uiStr('communitySub')); applyCommunityDataToDom(); set('sn-sec04-eyebrow', uiStr('sec04Eyebrow')); set('sn-sec04-title', uiStr('sec04Title')); set('sn-poi-eyebrow', uiStr('poiPhotosEyebrow')); set('sn-poi-title', uiStr('poiPhotosTitle')); setHtml('sn-poi-sub', uiStr('poiPhotosSub')); set('sn-links-aside-head', uiStr('linksAsideHead')); setHtml('sn-links-aside-intro', uiStr('linksAsideIntro')); const lgrid = $('#sn-link-previews'); if (lgrid) lgrid.setAttribute('aria-label', uiStr('linkPreviewsAria')); set('sn-foot-left', uiStr('footerLeft')); set('sn-build-tag', uiStr('footerBuild')); set('sn-site-settings-h3', uiStr('siteSettingsH3')); setHtml('sn-site-editor-lead', uiStr('siteEditorLeadHtml')); updateWysiwygLayer(); updateFloatingQdnBar(); } /** * Site builder is always shown; Connect unlocks publish and ✎ site (in-place panes). * "Publish to QDN" (DOCUMENT) only if this wallet also owns the tracked registered name. */ function updateBuilderChrome() { const connected = STATE.inQortal && !!STATE.userAccount; const hadSegment = !!STATE.segmentEditMode; if (!connected) STATE.segmentEditMode = false; const publishDoc = connected && !!STATE.canPublishDocument; const publishBtn = $('#sn-site-publish'); const editJump = $('#sn-edit-jump'); const editor = $('#sn-site-editor'); const connectBtn = $('#sn-connect'); const pubWww = document.getElementById('sn-publish-website'); const reloadQdn = document.getElementById('sn-site-reload-qdn'); if (reloadQdn) { const docQdn = isDocumentQdnEnabledForPublisher(); reloadQdn.hidden = !docQdn; reloadQdn.setAttribute('aria-hidden', docQdn ? 'false' : 'true'); } if (publishBtn) { if (connected) { publishBtn.hidden = true; publishBtn.setAttribute('aria-hidden', 'true'); } else { publishBtn.hidden = !publishDoc; publishBtn.setAttribute('aria-hidden', !publishDoc ? 'true' : 'false'); } } if (editJump) { editJump.hidden = !connected; editJump.setAttribute('aria-hidden', connected ? 'false' : 'true'); if (!connected) { editJump.setAttribute('aria-pressed', 'false'); editJump.title = 'Connect to toggle in-place panels next to each section'; } else { editJump.setAttribute('aria-pressed', STATE.segmentEditMode ? 'true' : 'false'); editJump.title = STATE.segmentEditMode ? 'Single form in the site builder (turn off in-place panels)' : 'In-place side panels next to each section (or single form below)'; } } if (editor) { editor.hidden = false; editor.removeAttribute('hidden'); } if (connectBtn) { connectBtn.classList.toggle('sn-btn-primary', !connected); connectBtn.classList.toggle('sn-btn-ghost', connected); } if (pubWww) { pubWww.classList.toggle('sn-btn-primary', connected); pubWww.classList.toggle('sn-btn-ghost', !connected); } if (visualDesignerBuilt && hadSegment && !STATE.segmentEditMode) { rebuildVisualFormLayout(); } updateFloatingQdnBar(); updateWysiwygLayer(); } /** * Whether the connected wallet may publish the DOCUMENT for publisherName() (on-chain name owner). */ async function syncDocumentPublishEligibility() { STATE.canPublishDocument = false; if (!STATE.inQortal || !STATE.userAccount) return; const userAddr = String(STATE.userAccount.address || ''); const ownerAddr = STATE.ownerAddress != null ? String(STATE.ownerAddress) : ''; if (ownerAddr && userAddr && userAddr === ownerAddr) { STATE.canPublishDocument = true; } else { try { const names = await qReq({ action: 'GET_ACCOUNT_NAMES', address: STATE.userAccount.address, limit: 200, offset: 0, }); let list = []; if (Array.isArray(names)) list = names; else if (names && Array.isArray(names.names)) list = names.names; else list = normList(names); const owns = list.some((n) => { const nm = n && (n.name || n); return String(nm || '') === publisherName(); }); STATE.canPublishDocument = owns; } catch (_) {} } if (!isDocumentQdnEnabledForPublisher()) { STATE.canPublishDocument = false; } } /** * Load wallet session (if any), align default registered name, resolve on-chain name owner, then editor + publish buttons. * Editor opens whenever a wallet is connected; DOCUMENT publish only if the wallet owns the tracked name. */ async function applyWalletSessionForBuilder() { if (!STATE.inQortal) return; try { if (!STATE.userAccount) { const acc = await qReq({ action: 'GET_USER_ACCOUNT' }); if (acc && acc.address) STATE.userAccount = acc; } } catch (_) { STATE.userAccount = null; } if (STATE.userAccount) { await maybeSetPublisherNameFromWallet(); } await resolveOwner(); if (STATE.userAccount) { await syncDocumentPublishEligibility(); } else { STATE.canPublishDocument = false; } updateBuilderChrome(); } async function publishSiteToQDN() { if (!isDocumentQdnEnabledForPublisher()) { toast('This name is website-only — use “Publish my website” (qortal://WEBSITE/…), not on-chain DOCUMENT config.', 'warn'); return; } if (!STATE.canPublishDocument) { toast('Wallet must own the tracked name «' + publisherName() + '» to publish this config to QDN', 'warn'); return; } try { const payload = JSON.stringify(STATE.site); const data64 = objectToBase64(JSON.parse(payload)); await qReq({ action: 'PUBLISH_QDN_RESOURCE', name: publisherName(), service: 'DOCUMENT', identifier: SITE_DOCUMENT_ID, data64, title: publisherName() + ' — site config', description: 'Portfolio config — About Me, links, pages, embeds (DOCUMENT)', }); setSiteStatus('Published to QDN'); toast('Site config published'); setQdnBaselineFromCurrent(); updateFloatingQdnBar(); } catch (e) { toast('Publish failed: ' + (e && e.message ? e.message : e), 'err'); } } async function loadSiteFromQDNButton() { if (!STATE.inQortal) { toast('Open inside Qortal to load from QDN', 'warn'); return; } if (!isDocumentQdnEnabledForPublisher()) { toast('DOCUMENT sync is not used for this name — use “Publish my website” for the public site; config is kept in this browser.', 'warn'); return; } try { const remote = await fetchSiteFromQDN(); if (!remote) { toast('No config published yet', 'warn'); return; } STATE.site = remote; setQdnBaselineFromCurrent(); saveLocalSite(); refreshAllUi(); setSiteStatus('Loaded from QDN'); toast('Loaded config from QDN'); } catch (e) { toast('Load failed: ' + (e && e.message ? e.message : e), 'err'); } } function setSiteStatus(t) { const n = $('#sn-site-status'); if (n) n.textContent = t || ''; } async function openQApp(appName, identifier, path) { const name = String(appName || '').trim(); if (!name) return false; const payload = { action: 'LINK_TO_QDN_RESOURCE', service: 'APP', name }; if (identifier) payload.identifier = identifier; if (path) payload.path = path; try { await qReq(payload); return true; } catch (e) { try { window.parent.postMessage( { action: 'SET_TAB', requestedHandler: 'UI', payload: { service: 'APP', name, identifier, path } }, '*' ); return true; } catch (e2) { toast('Open inside Qortal to launch apps', 'warn'); return false; } } } async function openQortalResource(service, name, identifier, path) { if (!name || !service) return false; const payload = { action: 'LINK_TO_QDN_RESOURCE', service: String(service).toUpperCase(), name, }; if (identifier) payload.identifier = identifier; if (path) payload.path = path; try { await qReq(payload); return true; } catch (e) { try { window.parent.postMessage( { action: 'SET_TAB', requestedHandler: 'UI', payload: { service: String(service).toUpperCase(), name, identifier, path, }, }, '*' ); return true; } catch (e2) { toast('Open inside Qortal to view this resource', 'warn'); return false; } } } function qdnUrl(service, name, identifier, filepath) { let u = '/arbitrary/' + encodeURIComponent(String(service).toUpperCase()) + '/' + encodeURIComponent(name); if (identifier) u += '/' + encodeURIComponent(identifier); if (filepath) u += (u.indexOf('?') === -1 ? '?' : '&') + 'filepath=' + encodeURIComponent(filepath); return u; } /** * Q-Mintership-style URL for embedding qortal:// links in iframes: * qortal://SERVICE/rest → /render/SERVICE/rest?theme=… * (see MinterBoard.js processLink / AdminBoard processQortalLinkForRendering) */ function qortalLinkToRenderUrl(link) { if (!link || typeof link !== 'string') return ''; if (!/^qortal:\/\//i.test(link)) return link; const match = link.match(/^qortal:\/\/([^/]+)(\/.*)?$/i); if (!match) return link; const firstSeg = match[1].toUpperCase(); const rest = match[2] || ''; const theme = typeof window._qdnTheme === 'string' && window._qdnTheme.trim() ? window._qdnTheme.trim() : 'default'; return '/render/' + firstSeg + rest + '?theme=' + encodeURIComponent(theme); } /** After wallet connect, force embeds to reload so child Q-Apps see the same session. */ function appendAuthBust(url) { if (!url || url === 'about:blank' || !STATE.sessionTag) return url; const sep = url.indexOf('?') >= 0 ? '&' : '?'; return url + sep + '_snConn=' + STATE.sessionTag; } /** * Prime the hub for each linked / embedded APP (GET_QDN_RESOURCE_URL) so permissions * and routing align with the wallet session — same targets as link previews + Live Channels. */ async function primeLinkedAppResourcesAfterConnect() { const seen = new Set(); const push = (href) => { if (!href || typeof href !== 'string') return; const h = href.trim(); if (!/^qortal:\/\//i.test(h)) return; if (seen.has(h)) return; seen.add(h); }; (STATE.site.links || []).forEach((L) => push(L.href)); (STATE.site.embeds || []).forEach((E) => push(E.qortalHref)); if (STATE.site.workshop && STATE.site.workshop.qortalHref) { push(String(STATE.site.workshop.qortalHref).trim()); } for (const href of seen) { const ap = parseQortalAppHref(href); if (!ap) continue; try { const req = { action: 'GET_QDN_RESOURCE_URL', service: 'APP', name: ap.name }; if (ap.path) req.path = ap.path; await qReq(req); } catch (_) { /* older cores / non-APP links */ } await new Promise((r) => setTimeout(r, 35)); } } /** Resolve a URL suitable for an