Files
Simon James b54a3139c7 Initial commit: Qortal Web Builder monorepo.
Includes QWB, Qortal Web, and Q-Shops Q-Apps with shared packages and build scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-09 12:17:29 +00:00

461 lines
20 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
(function () {
/* Hub /render/APP|WEBSITE/{name} often has no trailing slash. Without it, ./assets/…
* resolves under /render/APP/ instead of /render/APP/{name}/ (assets 404). */
try {
var p = location.pathname;
if (/\.html?$/i.test(p)) p = p.replace(/\/[^/]+$/, '/');
else if (p !== '/' && !p.endsWith('/')) p += '/';
if (p.length < 1 || p.charAt(0) !== '/') return;
var b = document.createElement('base');
b.href = p;
document.head.insertBefore(b, document.head.firstChild);
} catch (e) {
/* */
}
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Qortal Web Builder</title>
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
</head>
<body>
<div id="root">
<p
class="qwb-fallback"
style="padding: 2rem; font: 14px system-ui, sans-serif; color: #7a8a7e; margin: 0"
>
Loading…
</p>
</div>
<script>
(function () {
var shown = false;
function show(html) {
if (shown) return;
var r = document.getElementById('root');
if (!r || !r.querySelector('.qwb-fallback')) return;
shown = true;
r.innerHTML =
'<div class="qwb-fallback" style="padding:1.5rem;font:14px/1.5 system-ui,sans-serif;color:#c8a090;max-width:36rem">' +
html +
'</div>';
}
window.addEventListener(
'error',
function (ev) {
var t = ev && ev.target;
if (!t || t.tagName !== 'SCRIPT') return;
var a = t.getAttribute && t.getAttribute('src');
var s = a || t.src || '';
if (s) {
show(
'Could not load: ' +
s +
'<p style="margin:12px 0 0;font-size:13px;line-height:1.45;color:inherit">The <code>assets</code> folder must live next to this HTML file. For the built Q-App use the flat zip from <code>scripts/zip-portfolio.sh</code> (do not upload only <code>index.html</code>). In dev, serve the repo and open this page via <code>npm run dev</code>.</p>',
);
}
},
true,
);
setTimeout(function () {
if (shown) return;
var r = document.getElementById('root');
if (!r || !r.querySelector('.qwb-fallback')) return;
show('If this persists, run from repo root: <code>npm run dev</code> and open <code>dev.html</code>.');
}, 15000);
})();
</script>
<script id="sn-embed-config" type="application/json"></script>
<script>
// Inline minimal q-apps bridge (used when Qortal core does NOT auto-inject q-apps.js,
// e.g. Hub preview mode at /render/hash/...). Two paths:
// - Read-only actions → direct fetch to Qortal core REST API (same origin).
// - User-interaction actions → postMessage to Hub parent via MessageChannel.
(function () {
/* Never return early: Hub may inject `qortalRequest` without long-timeout helpers. */
function qs(obj) {
var parts = [];
for (var k in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
var v = obj[k];
if (v == null) continue;
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(v)));
}
return parts.length ? '?' + parts.join('&') : '';
}
function getJson(url, timeoutMs) {
var ctl = typeof AbortController === 'function' ? new AbortController() : null;
var timer = setTimeout(function () { try { ctl && ctl.abort(); } catch (e) {} }, timeoutMs || 4000);
return fetch(url, { headers: { accept: 'application/json' }, signal: ctl ? ctl.signal : undefined })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + url);
return r.text().then(function (t) {
var s = (t || '').trim();
if (!(s.charAt(0) === '{' || s.charAt(0) === '[')) {
throw new Error('Non-JSON response from ' + url + ' (got ' + s.slice(0, 40) + '…)');
}
return JSON.parse(s);
});
})
.finally(function () { clearTimeout(timer); });
}
function getText(url) {
return fetch(url).then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + url);
return r.text();
});
}
/** Arbitrary path segments: encodeURIComponent leaves ' ( ) ! * literal — REST 404s (e.g. MA's). */
function encodeQdnSeg(seg) {
return encodeURIComponent(String(seg == null ? '' : seg)).replace(/[!'()*]/g, function (ch) {
var c = ch.charCodeAt(0);
var h = c.toString(16).toUpperCase();
return '%' + (h.length < 2 ? '0' + h : h);
});
}
// Read-only routes handled by Qortal core REST API directly.
function localHandler(req) {
var a = req && req.action;
if (a === 'GET_ACCOUNT_NAMES' && req.address) {
return null; // Hub-native qortalRequest is authoritative for wallet-owned names.
}
if (a === 'GET_NAME_DATA' && req.name) {
return null; // Avoid stale/empty local REST results inside Hub iframes.
}
if (a === 'GET_ACCOUNT_DATA' && req.address) {
return null;
}
if (a === 'SEARCH_NAMES' && req.query) {
return getJson('/names/search' + qs({ query: req.query, prefix: req.prefix, limit: req.limit, offset: req.offset, reverse: req.reverse }))
.catch(function () { return null; });
}
if (a === 'LIST_QDN_RESOURCES' || a === 'SEARCH_QDN_RESOURCES') {
var base = a === 'SEARCH_QDN_RESOURCES' ? '/arbitrary/resources/search' : '/arbitrary/resources';
return getJson(base + qs({
service: req.service,
name: req.name,
identifier: req.identifier,
query: req.query,
prefix: req.prefix,
exactmatchnames: req.exactmatchnames,
default: req.default,
mode: req.mode,
minlevel: req.minlevel,
namefilter: req.namefilter,
followedonly: req.followedonly != null ? req.followedonly : req.followedOnly,
excludeblocked: req.excludeblocked != null ? req.excludeblocked : req.excludeBlocked,
includemetadata: req.includemetadata != null ? req.includemetadata : req.includeMetadata,
includestatus: req.includestatus != null ? req.includestatus : req.includeStatus,
before: req.before,
after: req.after,
limit: req.limit,
offset: req.offset,
reverse: req.reverse,
}));
}
if (a === 'GET_QDN_RESOURCE_STATUS' && req.service && req.name) {
return getJson(
'/arbitrary/resource/status/' +
encodeQdnSeg(req.service) +
'/' +
encodeQdnSeg(req.name) +
(req.identifier ? '/' + encodeQdnSeg(req.identifier) : ''),
);
}
if (a === 'GET_QDN_RESOURCE_URL' && req.service && req.name) {
var svcUrl = String(req.service).toUpperCase();
var u = '/arbitrary/' + encodeQdnSeg(req.service) + '/' + encodeQdnSeg(req.name);
if (req.identifier) u += '/' + encodeQdnSeg(req.identifier);
var pRaw = req.path != null ? String(req.path) : '';
var webLike = svcUrl === 'WEBSITE' || svcUrl === 'APP';
if (pRaw && webLike) {
u += pRaw.charAt(0) === '/' ? pRaw : '/' + pRaw;
}
var fpRaw = req.filepath != null && req.filepath !== '' ? String(req.filepath) : '';
if (!webLike && pRaw && !fpRaw) {
fpRaw = pRaw;
}
if (fpRaw) {
u +=
u.indexOf('?') >= 0
? '&filepath=' + encodeURIComponent(fpRaw)
: '?filepath=' + encodeURIComponent(fpRaw);
}
return Promise.resolve(u);
}
if (a === 'FETCH_QDN_RESOURCE' && req.service && req.name) {
/**
* IMPORTANT: do **NOT** intercept FETCH_QDN_RESOURCE locally for IMAGE / VIDEO / AUDIO
* or whenever `encoding: 'base64'` is requested. Plain `fetch().text()` mangles binary
* bytes into a garbled UTF-8 string, which then can't be base64-decoded. World Map's
* QDN raster tiles require Hub/native qortalRequest to return real base64 image data.
*/
var svc = String(req.service).toUpperCase();
var binaryService = svc === 'IMAGE' || svc === 'VIDEO' || svc === 'AUDIO' || svc === 'FILE' || svc === 'THUMBNAIL';
var wantsBase64 = req.encoding && String(req.encoding).toLowerCase() === 'base64';
var restBase64Text =
wantsBase64 &&
(svc === 'DOCUMENT' || svc === 'DOCUMENT_PRIVATE' || svc === 'JSON');
if (binaryService || (wantsBase64 && !restBase64Text)) {
return null; // fall through to forwardToParent → Hub native qortalRequest
}
var url = '/arbitrary/' + encodeQdnSeg(req.service) + '/' + encodeQdnSeg(req.name);
if (req.identifier) url += '/' + encodeQdnSeg(req.identifier);
if (req.filepath) {
url += (url.indexOf('?') >= 0 ? '&' : '?') + 'filepath=' + encodeURIComponent(req.filepath);
} else if (wantsBase64) {
url += (url.indexOf('?') >= 0 ? '&' : '?') + 'encoding=base64';
}
return getText(url)
.then(function (text) {
var t = typeof text === 'string' ? text : String(text);
var s = t.trim();
if (s.length > 1 && (s.charAt(0) === '{' || s.charAt(0) === '[')) {
try { return JSON.parse(s); } catch (e) { /* not JSON */ }
}
return t;
})
.catch(function (e) {
try { console.log('[qwb FETCH rest fail → Hub]', req, e && e.message); } catch (ee) {}
return forwardToParent(req);
});
}
return null;
}
window.__qwbBridgeLog = window.__qwbBridgeLog || [];
/** Align with Qortal q-apps.js `getDefaultTimeout` — Q-Mail / QDN publish must not abort at 60s. */
function qwbStubForwardTimeoutMs(action) {
if (!action) return 60000;
switch (action) {
case 'GET_USER_ACCOUNT':
case 'SAVE_FILE':
case 'SIGN_TRANSACTION':
case 'DECRYPT_DATA':
return 60 * 60 * 1000;
case 'SEARCH_QDN_RESOURCES':
return 30 * 1000;
case 'FETCH_QDN_RESOURCE':
return 60 * 1000;
case 'PUBLISH_QDN_RESOURCE':
case 'PUBLISH_MULTIPLE_QDN_RESOURCES':
return 60 * 60 * 1000;
case 'SEND_CHAT_MESSAGE':
return 60 * 1000;
case 'CREATE_TRADE_BUY_ORDER':
case 'CREATE_TRADE_SELL_ORDER':
case 'CANCEL_TRADE_SELL_ORDER':
case 'VOTE_ON_POLL':
case 'CREATE_POLL':
case 'JOIN_GROUP':
case 'DEPLOY_AT':
case 'SEND_COIN':
case 'TRANSFER_ASSET':
case 'SEND_PAYMENT':
return 5 * 60 * 1000;
case 'GET_WALLET_BALANCE':
return 2 * 60 * 1000;
case 'GET_NAME_DATA':
case 'GET_ACCOUNT_DATA':
case 'GET_ACCOUNT_NAMES':
return 5 * 60 * 1000;
default:
return 5 * 60 * 1000;
}
}
function forwardToParent(request, timeoutMs) {
return new Promise(function (resolve, reject) {
var channel = new MessageChannel();
var done = false;
var t0 = Date.now();
var waitMs =
timeoutMs != null && timeoutMs > 0 ? timeoutMs : qwbStubForwardTimeoutMs(request && request.action);
channel.port1.onmessage = function (ev) {
if (done) return;
done = true;
try { channel.port1.close(); } catch (e) {}
var d = ev && ev.data;
try {
window.__qwbBridgeLog.push({ t: Date.now() - t0, action: request && request.action, raw: d });
console.log('[qwb bridge <-]', request && request.action, d);
} catch (e) { /* */ }
// Qortal Hub response shape (from useQortalMessageListener): { result, error }.
// Error branch: { result: null, error: { error, message } | string }.
// Success branch: { result: <payload>, error: null }.
if (d && typeof d === 'object' && !Array.isArray(d) && ('result' in d || 'error' in d)) {
var err = d.error;
if (err) {
var msg;
if (typeof err === 'string') msg = err;
else if (err && typeof err === 'object') msg = err.message || err.error || JSON.stringify(err);
else msg = String(err);
reject(new Error(msg));
return;
}
d = d.result;
} else if (d && typeof d === 'object' && !Array.isArray(d)) {
if ('data' in d && d.data !== undefined && !('address' in d && 'publicKey' in d)) d = d.data;
else if ('response' in d && d.response !== undefined) d = d.response;
}
if (d && d.error) {
reject(new Error(typeof d.error === 'string' ? d.error : JSON.stringify(d.error)));
} else {
resolve(d);
}
};
channel.port1.onmessageerror = function () {
if (done) return;
done = true;
reject(new Error('qortalRequest MessageChannel error'));
};
try {
var payload = Object.assign({}, request || {}, { requestedHandler: 'UI' });
try { console.log('[qwb bridge ->]', request && request.action, payload); } catch (e) { /* */ }
// Full public/qortal-bridge.js uses window.postMessage to the same document (it installs the listener).
// This minimal stub has no in-page handler: in a Hub iframe, forward to `parent` (allowed cross-origin).
if (window.parent && window.parent !== window) {
window.parent.postMessage(payload, '*', [channel.port2]);
} else {
window.postMessage(payload, '*', [channel.port2]);
}
} catch (e) {
if (done) return;
done = true;
reject(e);
return;
}
setTimeout(function () {
if (done) return;
done = true;
try { channel.port1.close(); } catch (e) {}
reject(new Error('qortalRequest timed out'));
}, waitMs);
});
}
try {
window.__QWB_QORTAL_BRIDGE_STUB__ = true;
} catch (e) {}
function qwbLocalOrForward(request, forwardTimeoutMs) {
var tm =
forwardTimeoutMs != null && forwardTimeoutMs > 0
? forwardTimeoutMs
: qwbStubForwardTimeoutMs(request && request.action);
var local = null;
try {
local = localHandler(request);
} catch (e) {
local = null;
}
if (!local) return forwardToParent(request, tm);
return Promise.resolve(local).then(function (result) {
if (result == null) return forwardToParent(request, tm);
return result;
}).catch(function () {
return forwardToParent(request, tm);
});
}
if (typeof window.qortalRequest !== 'function') {
window.qortalRequest = function (request) {
return qwbLocalOrForward(request, null);
};
}
if (typeof window.qortalRequestWithNoTimeout !== 'function') {
window.qortalRequestWithNoTimeout = function (request, effectiveTimeoutMs) {
var ms =
effectiveTimeoutMs != null && effectiveTimeoutMs > 0
? effectiveTimeoutMs
: qwbStubForwardTimeoutMs(request && request.action);
return qwbLocalOrForward(request, ms);
};
}
if (typeof window.qortalRequestWithTimeout !== 'function') {
window.qortalRequestWithTimeout = function (req, ms) {
var outer = ms != null && ms > 0 ? ms : qwbStubForwardTimeoutMs(req && req.action);
return qwbLocalOrForward(req, outer);
};
}
})();
</script>
<script>
(function () {
/**
* Embedded Q-App iframes use the same stub as this page: they postMessage the Hub via
* their *parent* — but for them the parent is this QWB document, not the Hub, so the port
* never reached a wallet. When this document has a Hub bridge, we fulfill child requests
* on the MessageChannel (see site.html, same block).
*/
function isQAppChildFrameSource(src) {
if (!src || src === window) return false;
try {
var ifr = document.getElementsByTagName('iframe');
for (var i = 0; i < ifr.length; i++) {
if (ifr[i].contentWindow === src) return true;
}
} catch (e) {
return false;
}
return false;
}
window.addEventListener('message', function (ev) {
if (!ev || !ev.data) return;
if (!ev.ports || !ev.ports[0]) return;
if (typeof ev.data.action !== 'string' || !ev.data.action) return;
if (!isQAppChildFrameSource(ev.source)) return;
var port = ev.ports[0];
function replyRes(res) {
try {
port.postMessage({ result: res, error: null });
} catch (e) {}
}
function replyErr(err) {
var msg;
if (err == null) msg = 'Unknown error';
else if (typeof err === 'string') msg = err;
else if (err && typeof err === 'object' && 'message' in err) msg = String((err).message);
else msg = String(err);
try {
port.postMessage({ result: null, error: { message: msg } });
} catch (e) {}
}
if (typeof window.qortalRequest === 'function') {
var clean = {};
for (var k in ev.data) {
if (Object.prototype.hasOwnProperty.call(ev.data, k) && k !== 'requestedHandler') clean[k] = ev.data[k];
}
var run =
typeof window.qortalRequestWithNoTimeout === 'function'
? window.qortalRequestWithNoTimeout(clean)
: window.qortalRequest(clean);
run.then(replyRes, function (e) { replyErr(e); });
return;
}
if (window.top && window.top !== window) {
try {
window.top.postMessage(ev.data, '*', [port]);
} catch (e) {
replyErr(e);
}
return;
}
replyErr('No Qortal bridge on this page for embedded Q-App');
});
})();
</script>
<script src="./zip-store.js"></script>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>