sproutkit-code 1.10.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +869 -29
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31819,6 +31819,8 @@ export default async function handler(req, res) {
|
|
|
31819
31819
|
`;
|
|
31820
31820
|
exports2.TRACK_SNIPPET = `<script>
|
|
31821
31821
|
(function(){
|
|
31822
|
+
// The owner editing their own page in /edit isn't a visitor.
|
|
31823
|
+
if (location.search.indexOf("sk_edit=") > -1) return;
|
|
31822
31824
|
function send(ev){
|
|
31823
31825
|
try {
|
|
31824
31826
|
var body = JSON.stringify({ event: ev });
|
|
@@ -33674,7 +33676,7 @@ var require_cms_runtime = __commonJS({
|
|
|
33674
33676
|
};
|
|
33675
33677
|
})();
|
|
33676
33678
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
33677
|
-
exports2.ADMIN_APP_JS = exports2.ADMIN_INDEX_HTML = exports2.API_ADMIN_UPLOAD_JS = exports2.API_ADMIN_LOGOUT_JS = exports2.API_ADMIN_PASSWORD_JS = exports2.API_ADMIN_WHOAMI_JS = exports2.API_ADMIN_LOGIN_JS = exports2.API_CONTENT_JS = void 0;
|
|
33679
|
+
exports2.ADMIN_APP_JS = exports2.ADMIN_INDEX_HTML = exports2.EDIT_INDEX_HTML = exports2.API_ADMIN_UPLOAD_JS = exports2.API_ADMIN_LOGOUT_JS = exports2.API_ADMIN_PASSWORD_JS = exports2.API_ADMIN_WHOAMI_JS = exports2.API_ADMIN_LOGIN_JS = exports2.API_CONTENT_JS = void 0;
|
|
33678
33680
|
exports2.generateFriendlyPassword = generateFriendlyPassword;
|
|
33679
33681
|
exports2.hashPassword = hashPassword;
|
|
33680
33682
|
exports2.generateSessionSecret = generateSessionSecret;
|
|
@@ -34381,6 +34383,548 @@ export default async function handler(req, res) {
|
|
|
34381
34383
|
res.status(500).json({ error: "upload_failed", message: String(e) });
|
|
34382
34384
|
}
|
|
34383
34385
|
}
|
|
34386
|
+
`;
|
|
34387
|
+
exports2.EDIT_INDEX_HTML = String.raw`<!DOCTYPE html>
|
|
34388
|
+
<html lang="en">
|
|
34389
|
+
<head>
|
|
34390
|
+
<meta charset="UTF-8" />
|
|
34391
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
|
34392
|
+
<meta name="robots" content="noindex" />
|
|
34393
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
34394
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
34395
|
+
<meta name="theme-color" content="#0a0a0a" />
|
|
34396
|
+
<title>__EDIT_TITLE__</title>
|
|
34397
|
+
<style>
|
|
34398
|
+
:root { --accent: #22c55e; --ink: #0a0a0a; --muted: #6b7280; --line: #e5e5e0; }
|
|
34399
|
+
* { box-sizing: border-box; }
|
|
34400
|
+
html, body { margin: 0; height: 100%; overflow: hidden; background: #f4f4f1; color: var(--ink);
|
|
34401
|
+
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif; -webkit-text-size-adjust: 100%; }
|
|
34402
|
+
#site { position: fixed; top: 0; left: 0; width: 100%; border: 0; display: block; background: #fff;
|
|
34403
|
+
height: calc(100% - 64px - env(safe-area-inset-bottom)); }
|
|
34404
|
+
#bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 5; display: flex; align-items: center; gap: 8px;
|
|
34405
|
+
min-height: calc(64px + env(safe-area-inset-bottom)); padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
|
34406
|
+
background: #0a0a0a; color: #fff; }
|
|
34407
|
+
#status { flex: 1; min-width: 0; font-size: 13px; line-height: 1.3; }
|
|
34408
|
+
#status b { display: block; font-size: 14px; }
|
|
34409
|
+
#status span { display: block; color: #a1a1aa; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
34410
|
+
button { font: inherit; font-size: 14px; font-weight: 600; border: 0; border-radius: 10px; padding: 11px 16px;
|
|
34411
|
+
cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
34412
|
+
.primary { background: var(--accent); color: #04130a; }
|
|
34413
|
+
.primary:disabled { opacity: 0.4; cursor: default; }
|
|
34414
|
+
.ghost { background: #27272a; color: #fff; }
|
|
34415
|
+
.icon { background: transparent; color: #fff; padding: 8px 10px; font-size: 20px; line-height: 1; }
|
|
34416
|
+
#menu { position: fixed; right: 10px; bottom: calc(72px + env(safe-area-inset-bottom)); z-index: 6; display: none;
|
|
34417
|
+
min-width: 230px; padding: 6px; background: #fff; border: 1px solid var(--line); border-radius: 12px;
|
|
34418
|
+
box-shadow: 0 12px 32px rgba(0,0,0,0.18); }
|
|
34419
|
+
#menu a, #menu button { display: block; width: 100%; padding: 12px 14px; border-radius: 8px; background: transparent;
|
|
34420
|
+
color: var(--ink); text-align: left; text-decoration: none; font-size: 14px; font-weight: 500; }
|
|
34421
|
+
#menu a:hover, #menu button:hover { background: #f4f4f1; }
|
|
34422
|
+
#sheet { position: fixed; inset: 0; z-index: 10; display: none; align-items: center; justify-content: center;
|
|
34423
|
+
padding: 16px; background: rgba(10,10,10,0.55); }
|
|
34424
|
+
#sheet form { width: 100%; max-width: 360px; padding: 24px; background: #fff; border-radius: 16px; }
|
|
34425
|
+
#sheet h1 { margin: 0 0 4px; font-size: 20px; letter-spacing: -0.01em; }
|
|
34426
|
+
#sheet p { margin: 0 0 16px; color: var(--muted); font-size: 14px; }
|
|
34427
|
+
#sheet input { width: 100%; margin-bottom: 12px; padding: 12px; border: 1px solid var(--line); border-radius: 10px;
|
|
34428
|
+
font: inherit; font-size: 16px; }
|
|
34429
|
+
#sheet button { width: 100%; }
|
|
34430
|
+
#err { min-height: 1em; margin-top: 10px; color: #b91c1c; font-size: 13px; }
|
|
34431
|
+
#toast { position: fixed; left: 50%; bottom: calc(76px + env(safe-area-inset-bottom)); z-index: 7;
|
|
34432
|
+
max-width: calc(100% - 32px); transform: translateX(-50%); padding: 10px 16px; border-radius: 999px;
|
|
34433
|
+
background: #fff; color: var(--ink); border: 1px solid var(--line); box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
|
34434
|
+
font-size: 13px; font-weight: 500; text-align: center; opacity: 0; transition: opacity 0.2s; pointer-events: none; }
|
|
34435
|
+
#toast.on { opacity: 1; }
|
|
34436
|
+
</style>
|
|
34437
|
+
</head>
|
|
34438
|
+
<body>
|
|
34439
|
+
<iframe id="site" title="Your website"></iframe>
|
|
34440
|
+
<div id="bar">
|
|
34441
|
+
<div id="status"><b>Edit mode</b><span>Loading your site…</span></div>
|
|
34442
|
+
<button id="discard" class="ghost" hidden>Undo</button>
|
|
34443
|
+
<button id="save" class="primary" disabled>Save</button>
|
|
34444
|
+
<button id="more" class="icon" aria-label="More options">⋯</button>
|
|
34445
|
+
</div>
|
|
34446
|
+
<div id="menu">
|
|
34447
|
+
<a href="/admin">All settings — services, hours, photos</a>
|
|
34448
|
+
<a href="/" target="_blank" rel="noopener">View live site</a>
|
|
34449
|
+
<button id="signout" type="button">Sign out</button>
|
|
34450
|
+
</div>
|
|
34451
|
+
<div id="sheet">
|
|
34452
|
+
<form id="login">
|
|
34453
|
+
<h1>Edit your website</h1>
|
|
34454
|
+
<p>Enter your password to start editing.</p>
|
|
34455
|
+
<input id="pw" type="password" autocomplete="current-password" placeholder="Password" />
|
|
34456
|
+
<button class="primary" type="submit" id="go">Sign in</button>
|
|
34457
|
+
<div id="err"></div>
|
|
34458
|
+
</form>
|
|
34459
|
+
</div>
|
|
34460
|
+
<input id="file" type="file" accept="image/*" hidden />
|
|
34461
|
+
<div id="toast"></div>
|
|
34462
|
+
<script src="https://unpkg.com/browser-image-compression@2.0.2/dist/browser-image-compression.js" crossorigin></script>
|
|
34463
|
+
<script>
|
|
34464
|
+
(function () {
|
|
34465
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
34466
|
+
var frame = $("site"), saveBtn = $("save"), discardBtn = $("discard"), statusEl = $("status");
|
|
34467
|
+
var menu = $("menu"), sheet = $("sheet"), pw = $("pw"), err = $("err"), fileInput = $("file");
|
|
34468
|
+
|
|
34469
|
+
// Fields the owner can change in place. Reviews come live from Google and
|
|
34470
|
+
// the theme is a design choice, so neither is offered here.
|
|
34471
|
+
var TEXT_PATHS = /^(business\.(name|tagline)|hero\.(headline|subheadline|ctaText)|about|services\.\d+\.(title|description|price)|contact\.(address|phone|email)|contact\.hours\.\d+)$/;
|
|
34472
|
+
var MULTILINE = /^(about|hero\.subheadline|services\.\d+\.description)$/;
|
|
34473
|
+
var HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
34474
|
+
|
|
34475
|
+
var base = null; // content as last saved
|
|
34476
|
+
var draft = null; // content including unsaved edits
|
|
34477
|
+
var dirty = {}; // path -> true while it differs from base
|
|
34478
|
+
var editing = null, saving = false, loginResolve = null, photoIdx = null;
|
|
34479
|
+
var chipFor = new Map(), observer = null, scanTimer = null;
|
|
34480
|
+
|
|
34481
|
+
function norm(s) { return String(s == null ? "" : s).replace(/\s+/g, " ").trim(); }
|
|
34482
|
+
function clone(o) { return JSON.parse(JSON.stringify(o)); }
|
|
34483
|
+
function get(obj, path) {
|
|
34484
|
+
return path.split(".").reduce(function (o, k) { return o == null ? undefined : o[k]; }, obj);
|
|
34485
|
+
}
|
|
34486
|
+
function put(obj, path, v) {
|
|
34487
|
+
var ks = path.split("."), o = obj;
|
|
34488
|
+
for (var i = 0; i < ks.length - 1; i++) { o = o[ks[i]]; if (o == null) return; }
|
|
34489
|
+
o[ks[ks.length - 1]] = v;
|
|
34490
|
+
}
|
|
34491
|
+
function leaves(obj, prefix, out) {
|
|
34492
|
+
if (typeof obj === "string") { if (TEXT_PATHS.test(prefix)) out.push(prefix); }
|
|
34493
|
+
else if (obj && typeof obj === "object") {
|
|
34494
|
+
Object.keys(obj).forEach(function (k) { leaves(obj[k], prefix ? prefix + "." + k : k, out); });
|
|
34495
|
+
}
|
|
34496
|
+
return out;
|
|
34497
|
+
}
|
|
34498
|
+
|
|
34499
|
+
var toastTimer = null;
|
|
34500
|
+
function toast(msg, ms) {
|
|
34501
|
+
var t = $("toast");
|
|
34502
|
+
t.textContent = msg; t.classList.add("on");
|
|
34503
|
+
clearTimeout(toastTimer);
|
|
34504
|
+
toastTimer = setTimeout(function () { t.classList.remove("on"); }, ms || 2600);
|
|
34505
|
+
}
|
|
34506
|
+
|
|
34507
|
+
function updateBar() {
|
|
34508
|
+
var n = Object.keys(dirty).length;
|
|
34509
|
+
saveBtn.disabled = n === 0 || saving;
|
|
34510
|
+
saveBtn.textContent = saving ? "Saving…" : "Save";
|
|
34511
|
+
discardBtn.hidden = n === 0 || saving;
|
|
34512
|
+
statusEl.innerHTML = n
|
|
34513
|
+
? "<b>" + n + " unsaved change" + (n > 1 ? "s" : "") + "</b><span>Press Save to put them live</span>"
|
|
34514
|
+
: "<b>Edit mode</b><span>Tap any outlined text or photo to change it</span>";
|
|
34515
|
+
}
|
|
34516
|
+
|
|
34517
|
+
function refreshDirty(paths) {
|
|
34518
|
+
paths.forEach(function (p) {
|
|
34519
|
+
var a = get(draft, p), b = get(base, p);
|
|
34520
|
+
if (p.indexOf("photos.") === 0 ? a !== b : norm(a) !== norm(b)) dirty[p] = true;
|
|
34521
|
+
else delete dirty[p];
|
|
34522
|
+
});
|
|
34523
|
+
updateBar();
|
|
34524
|
+
}
|
|
34525
|
+
|
|
34526
|
+
// ---- Login ----------------------------------------------------------------
|
|
34527
|
+
function requireLogin() {
|
|
34528
|
+
return new Promise(function (resolve) {
|
|
34529
|
+
loginResolve = resolve;
|
|
34530
|
+
err.textContent = "";
|
|
34531
|
+
sheet.style.display = "flex";
|
|
34532
|
+
setTimeout(function () { pw.focus(); }, 50);
|
|
34533
|
+
});
|
|
34534
|
+
}
|
|
34535
|
+
$("login").addEventListener("submit", async function (e) {
|
|
34536
|
+
e.preventDefault();
|
|
34537
|
+
if (!pw.value) return;
|
|
34538
|
+
$("go").disabled = true; err.textContent = "";
|
|
34539
|
+
try {
|
|
34540
|
+
var r = await fetch("/api/admin/login", {
|
|
34541
|
+
method: "POST", credentials: "same-origin",
|
|
34542
|
+
headers: { "Content-Type": "application/json" },
|
|
34543
|
+
body: JSON.stringify({ password: pw.value })
|
|
34544
|
+
});
|
|
34545
|
+
if (r.status === 429) {
|
|
34546
|
+
var lock = await r.json().catch(function () { return {}; });
|
|
34547
|
+
err.textContent = "Too many tries. Wait " + Math.ceil((lock.retryAfter || 900) / 60) + " minutes and try again.";
|
|
34548
|
+
} else if (!r.ok) {
|
|
34549
|
+
err.textContent = r.status === 401 ? "That password didn't work." : "Couldn't sign in (" + r.status + ").";
|
|
34550
|
+
} else {
|
|
34551
|
+
var who = await fetch("/api/admin/whoami", { credentials: "same-origin" });
|
|
34552
|
+
if (!who.ok) {
|
|
34553
|
+
err.textContent = "Signed in, but your browser blocked the login cookie. Turn off strict tracking protection for this site.";
|
|
34554
|
+
} else {
|
|
34555
|
+
pw.value = ""; sheet.style.display = "none";
|
|
34556
|
+
if (loginResolve) { loginResolve(); loginResolve = null; }
|
|
34557
|
+
}
|
|
34558
|
+
}
|
|
34559
|
+
} catch (x) {
|
|
34560
|
+
err.textContent = "No connection. Check your signal and try again.";
|
|
34561
|
+
}
|
|
34562
|
+
$("go").disabled = false;
|
|
34563
|
+
});
|
|
34564
|
+
|
|
34565
|
+
// ---- Matching the page to content.json ------------------------------------
|
|
34566
|
+
// A field may render as one element or, when it has blank-line breaks, one
|
|
34567
|
+
// element per paragraph. "about#1" means the second paragraph of about.
|
|
34568
|
+
function paras(v) { return String(v == null ? "" : v).split(/\n\s*\n/); }
|
|
34569
|
+
function readField(obj, ref) {
|
|
34570
|
+
var at = ref.indexOf("#");
|
|
34571
|
+
if (at < 0) return get(obj, ref);
|
|
34572
|
+
return paras(get(obj, ref.slice(0, at)))[Number(ref.slice(at + 1))];
|
|
34573
|
+
}
|
|
34574
|
+
function writeField(obj, ref, v) {
|
|
34575
|
+
var at = ref.indexOf("#");
|
|
34576
|
+
if (at < 0) return put(obj, ref, v);
|
|
34577
|
+
var path = ref.slice(0, at), list = paras(get(obj, path));
|
|
34578
|
+
list[Number(ref.slice(at + 1))] = v;
|
|
34579
|
+
put(obj, path, list.join("\n\n"));
|
|
34580
|
+
}
|
|
34581
|
+
function fieldOf(ref) { var at = ref.indexOf("#"); return at < 0 ? ref : ref.slice(0, at); }
|
|
34582
|
+
|
|
34583
|
+
function textIndex() {
|
|
34584
|
+
// Saved values too, so a page that still shows old text (a second page of
|
|
34585
|
+
// a multipage site, say) is still recognised — then shown with the draft.
|
|
34586
|
+
var map = {};
|
|
34587
|
+
function add(t, ref) {
|
|
34588
|
+
t = norm(t);
|
|
34589
|
+
if (t.length < 2) return;
|
|
34590
|
+
map[t] = map[t] || [];
|
|
34591
|
+
if (map[t].indexOf(ref) < 0) map[t].push(ref);
|
|
34592
|
+
}
|
|
34593
|
+
leaves(draft, "", []).forEach(function (p) {
|
|
34594
|
+
[draft, base].forEach(function (src) {
|
|
34595
|
+
var v = get(src, p);
|
|
34596
|
+
add(v, p);
|
|
34597
|
+
var list = paras(v);
|
|
34598
|
+
if (list.length > 1) list.forEach(function (para, i) { add(para, p + "#" + i); });
|
|
34599
|
+
});
|
|
34600
|
+
});
|
|
34601
|
+
return map;
|
|
34602
|
+
}
|
|
34603
|
+
|
|
34604
|
+
// Text, line breaks and inline styling (a coloured word in a headline) are
|
|
34605
|
+
// fine to edit in place; anything structural is not.
|
|
34606
|
+
var INLINE = /^(BR|EM|STRONG|B|I|U|SPAN|SMALL|MARK|SUP|SUB)$/;
|
|
34607
|
+
function editableShape(el, text) {
|
|
34608
|
+
for (var i = 0; i < el.children.length; i++) {
|
|
34609
|
+
var c = el.children[i];
|
|
34610
|
+
if (!INLINE.test(c.tagName) || c.children.length > 0) return false;
|
|
34611
|
+
if (norm(c.textContent) === text) return false; // the child is the better match
|
|
34612
|
+
}
|
|
34613
|
+
return true;
|
|
34614
|
+
}
|
|
34615
|
+
|
|
34616
|
+
function injectStyle(doc) {
|
|
34617
|
+
if (doc.getElementById("sk-edit-style")) return;
|
|
34618
|
+
var s = doc.createElement("style");
|
|
34619
|
+
s.id = "sk-edit-style";
|
|
34620
|
+
s.textContent =
|
|
34621
|
+
"[data-sk-path]{outline:1.5px dashed rgba(34,197,94,.9)!important;outline-offset:3px;cursor:text!important;border-radius:2px;-webkit-user-select:text;user-select:text}" +
|
|
34622
|
+
"[data-sk-path].sk-changed{outline-color:#f59e0b!important}" +
|
|
34623
|
+
"[data-sk-path][contenteditable]{outline:2px solid #22c55e!important;background:rgba(34,197,94,.12)!important}" +
|
|
34624
|
+
".sk-chip{position:absolute;z-index:2147483646;padding:9px 12px;border-radius:999px;border:1px solid rgba(255,255,255,.3);background:rgba(10,10,10,.85);color:#fff;font:600 12px/1 Inter,system-ui,sans-serif;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.3)}";
|
|
34625
|
+
doc.head.appendChild(s);
|
|
34626
|
+
}
|
|
34627
|
+
|
|
34628
|
+
function scan() {
|
|
34629
|
+
var doc = frame.contentDocument;
|
|
34630
|
+
if (!doc || !doc.body || !draft || editing) return;
|
|
34631
|
+
injectStyle(doc);
|
|
34632
|
+
var root = doc.getElementById("root") || doc.body;
|
|
34633
|
+
var map = textIndex();
|
|
34634
|
+
var els = root.querySelectorAll("*");
|
|
34635
|
+
for (var i = 0; i < els.length; i++) {
|
|
34636
|
+
var el = els[i];
|
|
34637
|
+
if (el.namespaceURI !== HTML_NS || /^(SCRIPT|STYLE|NOSCRIPT|IFRAME|INPUT|TEXTAREA|SELECT|OPTION)$/.test(el.tagName)) continue;
|
|
34638
|
+
var text = norm(el.textContent);
|
|
34639
|
+
var paths = map[text] && editableShape(el, text) ? map[text] : null;
|
|
34640
|
+
if (paths) {
|
|
34641
|
+
el.setAttribute("data-sk-path", paths.join("|"));
|
|
34642
|
+
var want = readField(draft, paths[0]);
|
|
34643
|
+
if (norm(want) !== text) el.textContent = want;
|
|
34644
|
+
el.classList.toggle("sk-changed", paths.some(function (p) { return dirty[fieldOf(p)]; }));
|
|
34645
|
+
} else if (el.hasAttribute("data-sk-path")) {
|
|
34646
|
+
el.removeAttribute("data-sk-path");
|
|
34647
|
+
el.classList.remove("sk-changed");
|
|
34648
|
+
}
|
|
34649
|
+
}
|
|
34650
|
+
placeChips(doc);
|
|
34651
|
+
}
|
|
34652
|
+
|
|
34653
|
+
function scheduleScan() {
|
|
34654
|
+
clearTimeout(scanTimer);
|
|
34655
|
+
scanTimer = setTimeout(scan, 250);
|
|
34656
|
+
}
|
|
34657
|
+
|
|
34658
|
+
// ---- Photos ---------------------------------------------------------------
|
|
34659
|
+
function abs(u, doc) { try { return new URL(u, doc.baseURI).href; } catch (x) { return u; } }
|
|
34660
|
+
function shownUrl(el, win) {
|
|
34661
|
+
if (el.tagName === "IMG") return el.currentSrc || el.src;
|
|
34662
|
+
var m = (win.getComputedStyle(el).backgroundImage || "").match(/url\(["']?(.*?)["']?\)/);
|
|
34663
|
+
return m ? m[1] : null;
|
|
34664
|
+
}
|
|
34665
|
+
function showPhoto(el, url) {
|
|
34666
|
+
if (el.tagName === "IMG") { el.removeAttribute("srcset"); el.src = url; return; }
|
|
34667
|
+
var cur = el.style.backgroundImage;
|
|
34668
|
+
el.style.backgroundImage = /url\(/.test(cur)
|
|
34669
|
+
? cur.replace(/url\((["']?)(.*?)\1\)/, 'url("' + url + '")')
|
|
34670
|
+
: 'url("' + url + '")';
|
|
34671
|
+
}
|
|
34672
|
+
|
|
34673
|
+
function placeChips(doc) {
|
|
34674
|
+
var win = frame.contentWindow;
|
|
34675
|
+
var known = {};
|
|
34676
|
+
(base.photos || []).forEach(function (p, i) { if (p && p.url) known[abs(p.url, doc)] = i; });
|
|
34677
|
+
(draft.photos || []).forEach(function (p, i) { if (p && p.url) known[abs(p.url, doc)] = i; });
|
|
34678
|
+
var seen = new Set();
|
|
34679
|
+
var els = doc.querySelectorAll('#root img, #root [style*="url("]');
|
|
34680
|
+
for (var i = 0; i < els.length; i++) {
|
|
34681
|
+
var el = els[i], u = shownUrl(el, win);
|
|
34682
|
+
var idx = u ? known[abs(u, doc)] : undefined;
|
|
34683
|
+
if (idx === undefined) continue;
|
|
34684
|
+
var want = draft.photos[idx] && draft.photos[idx].url;
|
|
34685
|
+
if (want && abs(want, doc) !== abs(u, doc)) showPhoto(el, want);
|
|
34686
|
+
var r = el.getBoundingClientRect(), cs = win.getComputedStyle(el);
|
|
34687
|
+
var chip = chipFor.get(el);
|
|
34688
|
+
if (r.width < 80 || r.height < 60 || cs.visibility === "hidden" || Number(cs.opacity) < 0.1) {
|
|
34689
|
+
if (chip) chip.style.display = "none";
|
|
34690
|
+
seen.add(el);
|
|
34691
|
+
continue;
|
|
34692
|
+
}
|
|
34693
|
+
if (!chip) {
|
|
34694
|
+
chip = doc.createElement("button");
|
|
34695
|
+
chip.type = "button";
|
|
34696
|
+
chip.className = "sk-chip";
|
|
34697
|
+
chip.setAttribute("data-sk-ui", "1");
|
|
34698
|
+
chip.textContent = "Change photo";
|
|
34699
|
+
chip.addEventListener("click", function (e) {
|
|
34700
|
+
e.preventDefault(); e.stopPropagation();
|
|
34701
|
+
photoIdx = Number(e.currentTarget.getAttribute("data-idx"));
|
|
34702
|
+
fileInput.value = "";
|
|
34703
|
+
fileInput.click();
|
|
34704
|
+
});
|
|
34705
|
+
doc.body.appendChild(chip);
|
|
34706
|
+
chipFor.set(el, chip);
|
|
34707
|
+
}
|
|
34708
|
+
chip.setAttribute("data-idx", String(idx));
|
|
34709
|
+
chip.style.display = "";
|
|
34710
|
+
chip.style.top = (r.top + win.scrollY + 10) + "px";
|
|
34711
|
+
chip.style.left = (r.left + win.scrollX + 10) + "px";
|
|
34712
|
+
seen.add(el);
|
|
34713
|
+
}
|
|
34714
|
+
chipFor.forEach(function (chip, el) {
|
|
34715
|
+
if (!seen.has(el)) { chip.remove(); chipFor.delete(el); }
|
|
34716
|
+
});
|
|
34717
|
+
}
|
|
34718
|
+
|
|
34719
|
+
fileInput.addEventListener("change", async function () {
|
|
34720
|
+
var f = fileInput.files && fileInput.files[0];
|
|
34721
|
+
if (!f || photoIdx === null) return;
|
|
34722
|
+
var idx = photoIdx;
|
|
34723
|
+
toast("Uploading photo…", 60000);
|
|
34724
|
+
try {
|
|
34725
|
+
var blob = f;
|
|
34726
|
+
if (window.imageCompression) {
|
|
34727
|
+
blob = await imageCompression(f, { maxSizeMB: 1.5, maxWidthOrHeight: 1600, fileType: "image/webp", useWebWorker: true });
|
|
34728
|
+
}
|
|
34729
|
+
var send = function () {
|
|
34730
|
+
return fetch("/api/admin/upload", {
|
|
34731
|
+
method: "POST", credentials: "same-origin",
|
|
34732
|
+
headers: { "Content-Type": blob.type || "image/webp" }, body: blob
|
|
34733
|
+
});
|
|
34734
|
+
};
|
|
34735
|
+
var r = await send();
|
|
34736
|
+
if (r.status === 401) { await requireLogin(); r = await send(); }
|
|
34737
|
+
if (r.status === 413) throw new Error("that photo is too big");
|
|
34738
|
+
if (!r.ok) throw new Error("upload failed (" + r.status + ")");
|
|
34739
|
+
var data = await r.json();
|
|
34740
|
+
draft.photos[idx] = { url: data.url, alt: (draft.photos[idx] && draft.photos[idx].alt) || "" };
|
|
34741
|
+
refreshDirty(["photos." + idx]);
|
|
34742
|
+
scan();
|
|
34743
|
+
toast("Photo changed. Press Save to put it live.");
|
|
34744
|
+
} catch (x) {
|
|
34745
|
+
toast("Couldn't change the photo: " + (x && x.message ? x.message : x), 4000);
|
|
34746
|
+
}
|
|
34747
|
+
});
|
|
34748
|
+
|
|
34749
|
+
// ---- Editing text ---------------------------------------------------------
|
|
34750
|
+
function pathsOf(el) { return (el.getAttribute("data-sk-path") || "").split("|").filter(Boolean); }
|
|
34751
|
+
|
|
34752
|
+
function startEdit(el) {
|
|
34753
|
+
if (editing === el) return;
|
|
34754
|
+
if (editing) editing.blur();
|
|
34755
|
+
editing = el;
|
|
34756
|
+
try { el.contentEditable = "plaintext-only"; } catch (x) { /* older browsers */ }
|
|
34757
|
+
if (el.contentEditable !== "plaintext-only") el.contentEditable = "true";
|
|
34758
|
+
el.spellcheck = true;
|
|
34759
|
+
el.addEventListener("input", onInput);
|
|
34760
|
+
el.addEventListener("keydown", onKey);
|
|
34761
|
+
el.addEventListener("paste", onPaste);
|
|
34762
|
+
el.addEventListener("blur", onBlur);
|
|
34763
|
+
el.focus();
|
|
34764
|
+
var doc = el.ownerDocument, sel = doc.getSelection(), range = doc.createRange();
|
|
34765
|
+
range.selectNodeContents(el); range.collapse(false);
|
|
34766
|
+
sel.removeAllRanges(); sel.addRange(range);
|
|
34767
|
+
}
|
|
34768
|
+
|
|
34769
|
+
function onInput(e) {
|
|
34770
|
+
var el = e.currentTarget, paths = pathsOf(el);
|
|
34771
|
+
var v = el.innerText.replace(/\u00a0/g, " ");
|
|
34772
|
+
// A single paragraph can't hold a paragraph break — it would split on reload.
|
|
34773
|
+
if (!MULTILINE.test(paths[0]) || paths[0].indexOf("#") > -1) v = v.replace(/\s*\n\s*/g, " ");
|
|
34774
|
+
v = v.trim();
|
|
34775
|
+
paths.forEach(function (p) { writeField(draft, p, v); });
|
|
34776
|
+
// The same text shown elsewhere on the page (name in the nav and footer).
|
|
34777
|
+
var others = el.ownerDocument.querySelectorAll("[data-sk-path]");
|
|
34778
|
+
for (var i = 0; i < others.length; i++) {
|
|
34779
|
+
if (others[i] !== el && others[i].getAttribute("data-sk-path") === el.getAttribute("data-sk-path")) others[i].textContent = v;
|
|
34780
|
+
}
|
|
34781
|
+
refreshDirty(paths.map(fieldOf));
|
|
34782
|
+
}
|
|
34783
|
+
|
|
34784
|
+
function onKey(e) {
|
|
34785
|
+
var el = e.currentTarget;
|
|
34786
|
+
var ref = pathsOf(el)[0];
|
|
34787
|
+
if (e.key === "Escape" || (e.key === "Enter" && !e.shiftKey && (!MULTILINE.test(ref) || ref.indexOf("#") > -1))) {
|
|
34788
|
+
e.preventDefault();
|
|
34789
|
+
el.blur();
|
|
34790
|
+
}
|
|
34791
|
+
}
|
|
34792
|
+
|
|
34793
|
+
function onPaste(e) {
|
|
34794
|
+
if (e.currentTarget.contentEditable === "plaintext-only") return;
|
|
34795
|
+
e.preventDefault();
|
|
34796
|
+
var text = (e.clipboardData || window.clipboardData).getData("text/plain");
|
|
34797
|
+
e.currentTarget.ownerDocument.execCommand("insertText", false, text);
|
|
34798
|
+
}
|
|
34799
|
+
|
|
34800
|
+
function onBlur(e) {
|
|
34801
|
+
var el = e.currentTarget;
|
|
34802
|
+
el.removeAttribute("contenteditable");
|
|
34803
|
+
el.removeEventListener("input", onInput);
|
|
34804
|
+
el.removeEventListener("keydown", onKey);
|
|
34805
|
+
el.removeEventListener("paste", onPaste);
|
|
34806
|
+
el.removeEventListener("blur", onBlur);
|
|
34807
|
+
el.classList.toggle("sk-changed", pathsOf(el).some(function (p) { return dirty[fieldOf(p)]; }));
|
|
34808
|
+
editing = null;
|
|
34809
|
+
scheduleScan();
|
|
34810
|
+
}
|
|
34811
|
+
|
|
34812
|
+
// ---- The site frame -------------------------------------------------------
|
|
34813
|
+
function onFrameClick(e) {
|
|
34814
|
+
var t = e.target;
|
|
34815
|
+
if (t && t.nodeType !== 1) t = t.parentElement;
|
|
34816
|
+
if (!t || !t.closest || t.closest("[data-sk-ui]")) return;
|
|
34817
|
+
var field = t.closest("[data-sk-path]");
|
|
34818
|
+
if (field) {
|
|
34819
|
+
// Also stops the site's own handler, so tapping a button's label edits
|
|
34820
|
+
// the label instead of pressing the button.
|
|
34821
|
+
e.preventDefault(); e.stopPropagation();
|
|
34822
|
+
startEdit(field);
|
|
34823
|
+
return;
|
|
34824
|
+
}
|
|
34825
|
+
var a = t.closest("a[href]");
|
|
34826
|
+
if (!a) return;
|
|
34827
|
+
var url = null;
|
|
34828
|
+
try { url = new URL(a.getAttribute("href"), a.ownerDocument.baseURI); } catch (x) {}
|
|
34829
|
+
var samePage = url && url.origin === location.origin && !a.target && !/^\/(admin|edit|api|book)(\/|$)/.test(url.pathname);
|
|
34830
|
+
if (samePage) return; // moving between the site's own pages is fine
|
|
34831
|
+
e.preventDefault(); e.stopPropagation();
|
|
34832
|
+
toast("Links don't open while you're editing");
|
|
34833
|
+
}
|
|
34834
|
+
|
|
34835
|
+
function onFrameLoad() {
|
|
34836
|
+
var doc = frame.contentDocument;
|
|
34837
|
+
if (!doc || !doc.body) return;
|
|
34838
|
+
chipFor = new Map();
|
|
34839
|
+
editing = null;
|
|
34840
|
+
doc.addEventListener("click", onFrameClick, true);
|
|
34841
|
+
doc.addEventListener("submit", function (e) {
|
|
34842
|
+
e.preventDefault(); e.stopPropagation();
|
|
34843
|
+
toast("Forms don't send while you're editing");
|
|
34844
|
+
}, true);
|
|
34845
|
+
if (observer) observer.disconnect();
|
|
34846
|
+
observer = new MutationObserver(scheduleScan);
|
|
34847
|
+
observer.observe(doc.getElementById("root") || doc.body, {
|
|
34848
|
+
childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["src", "style"]
|
|
34849
|
+
});
|
|
34850
|
+
frame.contentWindow.addEventListener("resize", scheduleScan);
|
|
34851
|
+
scan();
|
|
34852
|
+
}
|
|
34853
|
+
|
|
34854
|
+
// ---- Save / undo ----------------------------------------------------------
|
|
34855
|
+
async function save() {
|
|
34856
|
+
if (editing) editing.blur();
|
|
34857
|
+
if (!Object.keys(dirty).length) return;
|
|
34858
|
+
saving = true; updateBar();
|
|
34859
|
+
try {
|
|
34860
|
+
var send = function () {
|
|
34861
|
+
return fetch("/api/content", {
|
|
34862
|
+
method: "POST", credentials: "same-origin",
|
|
34863
|
+
headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft)
|
|
34864
|
+
});
|
|
34865
|
+
};
|
|
34866
|
+
var r = await send();
|
|
34867
|
+
if (r.status === 401) { await requireLogin(); r = await send(); }
|
|
34868
|
+
if (!r.ok) throw new Error("the server said " + r.status);
|
|
34869
|
+
var d = await r.json();
|
|
34870
|
+
base = d.content || clone(draft);
|
|
34871
|
+
draft = clone(base);
|
|
34872
|
+
dirty = {};
|
|
34873
|
+
toast("Saved. Visitors will see it within a minute.", 3500);
|
|
34874
|
+
} catch (x) {
|
|
34875
|
+
toast("Couldn't save: " + (x && x.message ? x.message : x) + ". Your changes are still here.", 5000);
|
|
34876
|
+
}
|
|
34877
|
+
saving = false;
|
|
34878
|
+
updateBar();
|
|
34879
|
+
scan();
|
|
34880
|
+
}
|
|
34881
|
+
|
|
34882
|
+
saveBtn.addEventListener("click", save);
|
|
34883
|
+
discardBtn.addEventListener("click", function () {
|
|
34884
|
+
if (!confirm("Undo all unsaved changes?")) return;
|
|
34885
|
+
draft = clone(base); dirty = {};
|
|
34886
|
+
updateBar();
|
|
34887
|
+
frame.contentWindow.location.reload();
|
|
34888
|
+
});
|
|
34889
|
+
$("more").addEventListener("click", function (e) {
|
|
34890
|
+
e.stopPropagation();
|
|
34891
|
+
menu.style.display = menu.style.display === "block" ? "none" : "block";
|
|
34892
|
+
});
|
|
34893
|
+
document.addEventListener("click", function () { menu.style.display = "none"; });
|
|
34894
|
+
$("signout").addEventListener("click", async function () {
|
|
34895
|
+
if (Object.keys(dirty).length && !confirm("You have unsaved changes. Sign out anyway?")) return;
|
|
34896
|
+
dirty = {};
|
|
34897
|
+
await fetch("/api/admin/logout", { method: "POST", credentials: "same-origin" }).catch(function () {});
|
|
34898
|
+
location.href = "/";
|
|
34899
|
+
});
|
|
34900
|
+
window.addEventListener("beforeunload", function (e) {
|
|
34901
|
+
if (Object.keys(dirty).length) { e.preventDefault(); e.returnValue = ""; }
|
|
34902
|
+
});
|
|
34903
|
+
|
|
34904
|
+
async function init() {
|
|
34905
|
+
frame.addEventListener("load", onFrameLoad);
|
|
34906
|
+
frame.src = "/?sk_edit=1";
|
|
34907
|
+
var who = await fetch("/api/admin/whoami", { credentials: "same-origin" }).catch(function () { return null; });
|
|
34908
|
+
if (!who || !who.ok) await requireLogin();
|
|
34909
|
+
var r = await fetch("/api/content?t=" + Date.now(), { cache: "no-store" }).catch(function () { return null; });
|
|
34910
|
+
if (!r || !r.ok) {
|
|
34911
|
+
statusEl.innerHTML = "<b>Editing isn't switched on</b><span>Ask whoever built your site to turn on the editor.</span>";
|
|
34912
|
+
return;
|
|
34913
|
+
}
|
|
34914
|
+
base = await r.json();
|
|
34915
|
+
if (!Array.isArray(base.photos)) base.photos = [];
|
|
34916
|
+
draft = clone(base);
|
|
34917
|
+
updateBar();
|
|
34918
|
+
scan();
|
|
34919
|
+
// Layout settles as fonts and images load; keep the photo chips in place.
|
|
34920
|
+
setInterval(function () { if (!editing) scan(); }, 1500);
|
|
34921
|
+
toast("Tap anything with a dashed outline to change it", 4000);
|
|
34922
|
+
}
|
|
34923
|
+
init();
|
|
34924
|
+
})();
|
|
34925
|
+
</script>
|
|
34926
|
+
</body>
|
|
34927
|
+
</html>
|
|
34384
34928
|
`;
|
|
34385
34929
|
exports2.ADMIN_INDEX_HTML = `<!DOCTYPE html>
|
|
34386
34930
|
<html lang="en">
|
|
@@ -34620,7 +35164,10 @@ export default async function handler(req, res) {
|
|
|
34620
35164
|
h("div", { className: "bar" },
|
|
34621
35165
|
h("div", null,
|
|
34622
35166
|
h("div", { style: { fontSize: 15, fontWeight: 600 } }, "Editing " + (content.business?.name || "your site")),
|
|
34623
|
-
h("div", { style: { fontSize: 12, color: "var(--muted)" } },
|
|
35167
|
+
h("div", { style: { fontSize: 12, color: "var(--muted)" } },
|
|
35168
|
+
h("a", { href: "/" }, "View site \u2192"), " \xB7 ",
|
|
35169
|
+
h("a", { href: "/edit" }, "Edit on the page \u2192")
|
|
35170
|
+
)
|
|
34624
35171
|
),
|
|
34625
35172
|
h("button", { className: "primary", onClick: save, disabled: saving[0] }, saving[0] ? "Saving\u2026" : "Save changes")
|
|
34626
35173
|
),
|
|
@@ -81053,9 +81600,11 @@ var require_builder = __commonJS({
|
|
|
81053
81600
|
exports2.deployBookingOnly = deployBookingOnly2;
|
|
81054
81601
|
exports2.deployStandaloneBooking = deployStandaloneBooking2;
|
|
81055
81602
|
exports2.handoverSite = handoverSite2;
|
|
81603
|
+
exports2.addEditorToSite = addEditorToSite2;
|
|
81056
81604
|
var sdk_1 = __importDefault(require_sdk());
|
|
81057
81605
|
var axios_1 = __importDefault(require_axios());
|
|
81058
81606
|
var crypto_1 = require("crypto");
|
|
81607
|
+
var vm = __importStar(require("vm"));
|
|
81059
81608
|
var runtime_1 = require_runtime();
|
|
81060
81609
|
var db_1 = require_db();
|
|
81061
81610
|
var llm_1 = require_llm();
|
|
@@ -81069,6 +81618,58 @@ var require_builder = __commonJS({
|
|
|
81069
81618
|
var analytics_runtime_1 = require_analytics_runtime();
|
|
81070
81619
|
var cms_runtime_1 = require_cms_runtime();
|
|
81071
81620
|
var booking_runtime_1 = require_booking_runtime();
|
|
81621
|
+
function storedSetting(key) {
|
|
81622
|
+
try {
|
|
81623
|
+
const row = (0, db_1.getDb)().get("SELECT value FROM settings WHERE key = ?", [key]);
|
|
81624
|
+
return row ? (0, runtime_1.decryptSettingValue)(key, row.value) : null;
|
|
81625
|
+
} catch {
|
|
81626
|
+
return null;
|
|
81627
|
+
}
|
|
81628
|
+
}
|
|
81629
|
+
function extractFallback(code) {
|
|
81630
|
+
const m = /const\s+FALLBACK\s*=\s*\{/.exec(code);
|
|
81631
|
+
if (!m)
|
|
81632
|
+
return null;
|
|
81633
|
+
const start = m.index + m[0].length - 1;
|
|
81634
|
+
let depth = 0;
|
|
81635
|
+
let quote = null;
|
|
81636
|
+
for (let i = start; i < code.length; i++) {
|
|
81637
|
+
const ch = code[i];
|
|
81638
|
+
if (quote) {
|
|
81639
|
+
if (ch === "\\")
|
|
81640
|
+
i++;
|
|
81641
|
+
else if (ch === quote)
|
|
81642
|
+
quote = null;
|
|
81643
|
+
continue;
|
|
81644
|
+
}
|
|
81645
|
+
if (ch === "/" && code[i + 1] === "/") {
|
|
81646
|
+
i = code.indexOf("\n", i);
|
|
81647
|
+
if (i < 0)
|
|
81648
|
+
return null;
|
|
81649
|
+
continue;
|
|
81650
|
+
}
|
|
81651
|
+
if (ch === "/" && code[i + 1] === "*") {
|
|
81652
|
+
i = code.indexOf("*/", i) + 1;
|
|
81653
|
+
if (i <= 0)
|
|
81654
|
+
return null;
|
|
81655
|
+
continue;
|
|
81656
|
+
}
|
|
81657
|
+
if (ch === '"' || ch === "'" || ch === "`")
|
|
81658
|
+
quote = ch;
|
|
81659
|
+
else if (ch === "{")
|
|
81660
|
+
depth++;
|
|
81661
|
+
else if (ch === "}" && --depth === 0) {
|
|
81662
|
+
try {
|
|
81663
|
+
const v = vm.runInNewContext("(" + code.slice(start, i + 1) + ")", /* @__PURE__ */ Object.create(null), { timeout: 100 });
|
|
81664
|
+
const json = JSON.parse(JSON.stringify(v));
|
|
81665
|
+
return json && typeof json === "object" && json.business ? json : null;
|
|
81666
|
+
} catch {
|
|
81667
|
+
return null;
|
|
81668
|
+
}
|
|
81669
|
+
}
|
|
81670
|
+
}
|
|
81671
|
+
return null;
|
|
81672
|
+
}
|
|
81072
81673
|
function siteUidFor(leadId) {
|
|
81073
81674
|
const db2 = (0, db_1.getDb)();
|
|
81074
81675
|
const row = db2.get("SELECT site_uid FROM leads WHERE id = ?", [leadId]);
|
|
@@ -81164,6 +81765,11 @@ var require_builder = __commonJS({
|
|
|
81164
81765
|
const title = p.name ? `${p.name} \u2014 site manager` : "Site editor";
|
|
81165
81766
|
return cms_runtime_1.ADMIN_INDEX_HTML.replace("__ADMIN_TITLE__", title.replace(/[<>&]/g, ""));
|
|
81166
81767
|
}
|
|
81768
|
+
function editHtml() {
|
|
81769
|
+
const p = (0, agency_1.getAgencyProfile)();
|
|
81770
|
+
const title = p.name ? `Edit your site \u2014 ${p.name}` : "Edit your site";
|
|
81771
|
+
return cms_runtime_1.EDIT_INDEX_HTML.replace("__EDIT_TITLE__", title.replace(/[<>&]/g, ""));
|
|
81772
|
+
}
|
|
81167
81773
|
function bannerFor(lead) {
|
|
81168
81774
|
let handedOver = false;
|
|
81169
81775
|
try {
|
|
@@ -81348,7 +81954,9 @@ var require_builder = __commonJS({
|
|
|
81348
81954
|
}
|
|
81349
81955
|
}
|
|
81350
81956
|
async function buildAndDeploySite(args) {
|
|
81351
|
-
const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress,
|
|
81957
|
+
const { lead, anthropicKey, vercelToken, placesKey, customInstructions, onProgress, reuseAdminPassword, skipContentExtraction, multipage, pages, skipQa } = args;
|
|
81958
|
+
const blobToken = args.blobToken ?? storedSetting("blob_token");
|
|
81959
|
+
const includeCms = args.includeCms ?? (!!blobToken && storedSetting("build_with_editor") !== "0");
|
|
81352
81960
|
const pageList = multipage && (!pages || pages.length === 0) ? ["Home", "Services", "About", "Contact"] : pages ?? [];
|
|
81353
81961
|
const credit = (0, agency_1.siteCredit)();
|
|
81354
81962
|
const footerCredit = credit.url ? `Built by ${credit.label} \xB7 ${credit.url}` : `Built by ${credit.label}`;
|
|
@@ -81816,7 +82424,15 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81816
82424
|
theme: { variant: "premium-dark", accent: "#22c55e" },
|
|
81817
82425
|
photos: photosArr
|
|
81818
82426
|
};
|
|
81819
|
-
const
|
|
82427
|
+
const fromCode = cmsSeedContent ? null : extractFallback(appCode);
|
|
82428
|
+
const initialContent = fromCode ? {
|
|
82429
|
+
...defaultContent,
|
|
82430
|
+
...fromCode,
|
|
82431
|
+
business: { ...defaultContent.business, ...fromCode.business },
|
|
82432
|
+
hero: { ...defaultContent.hero, ...fromCode.hero },
|
|
82433
|
+
contact: { ...defaultContent.contact, ...fromCode.contact },
|
|
82434
|
+
photos: photosArr
|
|
82435
|
+
} : cmsSeedContent ? {
|
|
81820
82436
|
...defaultContent,
|
|
81821
82437
|
...cmsSeedContent,
|
|
81822
82438
|
business: {
|
|
@@ -81861,7 +82477,9 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81861
82477
|
const vercelJson = JSON.stringify({
|
|
81862
82478
|
rewrites: [
|
|
81863
82479
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
81864
|
-
{ source: "/admin/", destination: "/admin/index.html" }
|
|
82480
|
+
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82481
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82482
|
+
{ source: "/edit/", destination: "/edit/index.html" }
|
|
81865
82483
|
]
|
|
81866
82484
|
});
|
|
81867
82485
|
cmsExtraFiles = [
|
|
@@ -81873,7 +82491,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81873
82491
|
{ file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" },
|
|
81874
82492
|
{ file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" },
|
|
81875
82493
|
{ file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" },
|
|
81876
|
-
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" }
|
|
82494
|
+
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
82495
|
+
{ file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" }
|
|
81877
82496
|
];
|
|
81878
82497
|
cmsAdmin = { url: "", password: adminPassword };
|
|
81879
82498
|
}
|
|
@@ -81973,6 +82592,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81973
82592
|
rewrites: [
|
|
81974
82593
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
81975
82594
|
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82595
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82596
|
+
{ source: "/edit/", destination: "/edit/index.html" },
|
|
81976
82597
|
{ source: "/book", destination: "/book/index.html" },
|
|
81977
82598
|
{ source: "/book/", destination: "/book/index.html" }
|
|
81978
82599
|
]
|
|
@@ -81985,7 +82606,7 @@ createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMo
|
|
|
81985
82606
|
const viteConfigCms = `import { defineConfig } from 'vite';
|
|
81986
82607
|
import react from '@vitejs/plugin-react';
|
|
81987
82608
|
export default defineConfig({ plugins: [react()] });`;
|
|
81988
|
-
filesToRedeploy.push({ file: "index.html", data: indexHtmlCms, encoding: "utf-8" }, { file: "src/main.jsx", data: mainJsxCms, encoding: "utf-8" }, { file: "vite.config.js", data: viteConfigCms, encoding: "utf-8" }, { file: "package.json", data: packageJsonCms, encoding: "utf-8" }, { file: "vercel.json", data: vercelJson, encoding: "utf-8" }, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
82609
|
+
filesToRedeploy.push({ file: "index.html", data: indexHtmlCms, encoding: "utf-8" }, { file: "src/main.jsx", data: mainJsxCms, encoding: "utf-8" }, { file: "vite.config.js", data: viteConfigCms, encoding: "utf-8" }, { file: "package.json", data: packageJsonCms, encoding: "utf-8" }, { file: "vercel.json", data: vercelJson, encoding: "utf-8" }, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" }, { file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" });
|
|
81989
82610
|
if (!filesToRedeploy.some((f) => f.file === "src/App.jsx")) {
|
|
81990
82611
|
throw new Error("Couldn't fetch src/App.jsx from the existing deployment. Try Regenerate website first, then Redeploy CMS.");
|
|
81991
82612
|
}
|
|
@@ -82121,6 +82742,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82121
82742
|
rewrites: [
|
|
82122
82743
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
82123
82744
|
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82745
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82746
|
+
{ source: "/edit/", destination: "/edit/index.html" },
|
|
82124
82747
|
{ source: "/book", destination: "/book/index.html" },
|
|
82125
82748
|
{ source: "/book/", destination: "/book/index.html" }
|
|
82126
82749
|
]
|
|
@@ -82141,6 +82764,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82141
82764
|
// Refresh the CMS admin app (contains the Bookings tab now)
|
|
82142
82765
|
{ file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" },
|
|
82143
82766
|
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
82767
|
+
{ file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" },
|
|
82144
82768
|
{ file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" },
|
|
82145
82769
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
82146
82770
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
@@ -82206,6 +82830,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82206
82830
|
// admin the CMS sites use — it already has a Bookings tab.
|
|
82207
82831
|
{ file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" },
|
|
82208
82832
|
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
82833
|
+
{ file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" },
|
|
82209
82834
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
82210
82835
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
82211
82836
|
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
@@ -82218,7 +82843,9 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82218
82843
|
data: JSON.stringify({
|
|
82219
82844
|
rewrites: [
|
|
82220
82845
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
82221
|
-
{ source: "/admin/", destination: "/admin/index.html" }
|
|
82846
|
+
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82847
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82848
|
+
{ source: "/edit/", destination: "/edit/index.html" }
|
|
82222
82849
|
]
|
|
82223
82850
|
}),
|
|
82224
82851
|
encoding: "utf-8"
|
|
@@ -82388,12 +83015,14 @@ export default defineConfig({ plugins: [react()] });`,
|
|
|
82388
83015
|
rewrites: [
|
|
82389
83016
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
82390
83017
|
{ source: "/admin/", destination: "/admin/index.html" },
|
|
83018
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
83019
|
+
{ source: "/edit/", destination: "/edit/index.html" },
|
|
82391
83020
|
{ source: "/book", destination: "/book/index.html" },
|
|
82392
83021
|
{ source: "/book/", destination: "/book/index.html" }
|
|
82393
83022
|
]
|
|
82394
83023
|
}),
|
|
82395
83024
|
encoding: "utf-8"
|
|
82396
|
-
}, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" });
|
|
83025
|
+
}, { file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" }, { file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" }, { file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" }, { file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" }, { file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" }, { file: "api/track.js", data: analytics_runtime_1.API_TRACK_JS, encoding: "utf-8" }, { file: "api/stats.js", data: analytics_runtime_1.API_STATS_JS, encoding: "utf-8" }, { file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" }, { file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" }, { file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" }, { file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" });
|
|
82397
83026
|
const env = {
|
|
82398
83027
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
82399
83028
|
ADMIN_PASSWORD_HASH: await (0, cms_runtime_1.hashPassword)(adminPassword),
|
|
@@ -82416,6 +83045,132 @@ export default defineConfig({ plugins: [react()] });`,
|
|
|
82416
83045
|
onProgress(`Live at ${url}`);
|
|
82417
83046
|
return { url };
|
|
82418
83047
|
}
|
|
83048
|
+
async function addEditorToSite2(args) {
|
|
83049
|
+
const { lead, anthropicKey, vercelToken, blobToken, reuseAdminPassword, onProgress } = args;
|
|
83050
|
+
onProgress("Loading site source\u2026");
|
|
83051
|
+
const snapshot = loadSnapshot(lead.id);
|
|
83052
|
+
const appIdx = snapshot ? snapshot.findIndex((f) => f.file === "src/App.jsx") : -1;
|
|
83053
|
+
if (!snapshot || appIdx < 0) {
|
|
83054
|
+
throw new Error("No local copy of this site's code. Regenerate the website once (that saves a copy), then add the editor.");
|
|
83055
|
+
}
|
|
83056
|
+
const photoPaths = snapshot.map((f) => f.file.match(/^public\/(photo(\d+)\.jpg)$/)).filter((m) => !!m).sort((a, b) => Number(a[2]) - Number(b[2])).map((m) => `/${m[1]}`);
|
|
83057
|
+
let seed = null;
|
|
83058
|
+
if (!/["'`]\/api\/content["'`]/.test(snapshot[appIdx].data)) {
|
|
83059
|
+
onProgress("Connecting the page's text and photos to the editor\u2026");
|
|
83060
|
+
const wired = await wireAppToCms({
|
|
83061
|
+
anthropicKey,
|
|
83062
|
+
appJsx: snapshot[appIdx].data,
|
|
83063
|
+
lead
|
|
83064
|
+
});
|
|
83065
|
+
snapshot[appIdx] = { ...snapshot[appIdx], data: wired.app };
|
|
83066
|
+
saveSnapshot(lead.id, snapshot);
|
|
83067
|
+
seed = {
|
|
83068
|
+
reviews: [],
|
|
83069
|
+
theme: { variant: "premium-dark", accent: "#22c55e" },
|
|
83070
|
+
...wired.content,
|
|
83071
|
+
photos: photoPaths.map((url2) => ({ url: url2, alt: "" }))
|
|
83072
|
+
};
|
|
83073
|
+
}
|
|
83074
|
+
const siteUid = siteUidFor(lead.id);
|
|
83075
|
+
if (seed) {
|
|
83076
|
+
onProgress("Saving the page's current text\u2026");
|
|
83077
|
+
const { put } = await Promise.resolve().then(() => __importStar(require_dist5()));
|
|
83078
|
+
await put(`sites/${siteUid}/content.json`, JSON.stringify(seed), {
|
|
83079
|
+
access: "public",
|
|
83080
|
+
addRandomSuffix: false,
|
|
83081
|
+
allowOverwrite: true,
|
|
83082
|
+
contentType: "application/json",
|
|
83083
|
+
token: blobToken
|
|
83084
|
+
});
|
|
83085
|
+
}
|
|
83086
|
+
const password = reuseAdminPassword ?? (0, cms_runtime_1.generateFriendlyPassword)();
|
|
83087
|
+
const { url } = await redeployWithoutBanner({
|
|
83088
|
+
lead,
|
|
83089
|
+
vercelToken,
|
|
83090
|
+
blobToken,
|
|
83091
|
+
hasCms: true,
|
|
83092
|
+
adminPassword: password,
|
|
83093
|
+
siteUid,
|
|
83094
|
+
onProgress
|
|
83095
|
+
});
|
|
83096
|
+
return { url, admin: { url: `${url}/admin`, password } };
|
|
83097
|
+
}
|
|
83098
|
+
async function wireAppToCms(args) {
|
|
83099
|
+
const { anthropicKey, appJsx, lead } = args;
|
|
83100
|
+
const prompt = `You are editing an existing, approved React website (App.jsx below) so its owner can edit the copy through a CMS. The design is final: the page must look IDENTICAL after your change.
|
|
83101
|
+
|
|
83102
|
+
Change ONLY where the text comes from:
|
|
83103
|
+
|
|
83104
|
+
1. Add near the top:
|
|
83105
|
+
const FALLBACK = { ...exactly the content JSON you output below... };
|
|
83106
|
+
2. Inside App:
|
|
83107
|
+
const [content, setContent] = useState(FALLBACK);
|
|
83108
|
+
useEffect(() => {
|
|
83109
|
+
fetch("/api/content").then(r => r.ok ? r.json() : null).then(d => {
|
|
83110
|
+
if (d && d.business) setContent(prev => ({ ...prev, ...d }));
|
|
83111
|
+
}).catch(() => {});
|
|
83112
|
+
}, []);
|
|
83113
|
+
If the file has several components (pages, sections), pass content down as a prop \u2014 keep the fetch in App only.
|
|
83114
|
+
3. Replace each hardcoded string that matches a field below with that field, e.g. <h1>{content.hero.headline}</h1>. The rendered text must stay character-for-character the same, so put the exact current wording into the JSON.
|
|
83115
|
+
4. Photos: const photos = (content.photos && content.photos.length > 0) ? content.photos.map(p => p.url) : PHOTOS; and use photos wherever PHOTOS / photo paths were used. If the file has no PHOTOS constant, define const PHOTOS = [the photo paths it uses, in order].
|
|
83116
|
+
5. Opening hours: render content.contact.hours if the page shows hours; if that array is empty the page must omit the hours block.
|
|
83117
|
+
|
|
83118
|
+
Content fields (use only what the page actually shows; leave others as "" or []):
|
|
83119
|
+
{
|
|
83120
|
+
"business": { "name": string, "tagline": string },
|
|
83121
|
+
"hero": { "headline": string, "subheadline": string, "ctaText": string },
|
|
83122
|
+
"about": string,
|
|
83123
|
+
"services": [{ "title": string, "description": string, "price": string }],
|
|
83124
|
+
"contact": { "address": string, "phone": string, "email": string, "hours": [string] }
|
|
83125
|
+
}
|
|
83126
|
+
Business name: ${JSON.stringify(lead.name)}. Phone: ${JSON.stringify(lead.phone ?? "")}. Address: ${JSON.stringify(lead.address ?? "")}.
|
|
83127
|
+
|
|
83128
|
+
Do NOT change: layout, styles, colours, fonts, class names, component structure, section order, animations, links, imports (other than adding useState/useEffect), or any text that has no field above \u2014 leave that hardcoded. Keep \`export default App;\` as the final line.
|
|
83129
|
+
|
|
83130
|
+
Reply in exactly this format and nothing else:
|
|
83131
|
+
<content>
|
|
83132
|
+
{ the content JSON }
|
|
83133
|
+
</content>
|
|
83134
|
+
<app>
|
|
83135
|
+
the complete new App.jsx
|
|
83136
|
+
</app>
|
|
83137
|
+
|
|
83138
|
+
App.jsx:
|
|
83139
|
+
${appJsx}`;
|
|
83140
|
+
let lastErr = "";
|
|
83141
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
83142
|
+
const res = await (0, llm_1.generateText)({
|
|
83143
|
+
apiKey: anthropicKey,
|
|
83144
|
+
prompt: attempt === 0 ? prompt : `${prompt}
|
|
83145
|
+
|
|
83146
|
+
Your previous reply was unusable (${lastErr}). Follow the reply format exactly.`,
|
|
83147
|
+
model: (0, models_1.getPreferredModel)(),
|
|
83148
|
+
maxTokens: 64e3,
|
|
83149
|
+
timeoutMs: 3e5
|
|
83150
|
+
});
|
|
83151
|
+
const text = res.text ?? "";
|
|
83152
|
+
const contentRaw = text.match(/<content>\s*([\s\S]*?)\s*<\/content>/)?.[1];
|
|
83153
|
+
const appRaw = text.match(/<app>\s*([\s\S]*?)\s*<\/app>/)?.[1];
|
|
83154
|
+
if (!contentRaw || !appRaw) {
|
|
83155
|
+
lastErr = res.stopReason === "max_tokens" ? "it was cut off" : "missing <content> or <app>";
|
|
83156
|
+
continue;
|
|
83157
|
+
}
|
|
83158
|
+
let content;
|
|
83159
|
+
try {
|
|
83160
|
+
content = JSON.parse(contentRaw.replace(/^```[a-z]*\s*/i, "").replace(/\s*```$/, ""));
|
|
83161
|
+
} catch {
|
|
83162
|
+
lastErr = "the content wasn't valid JSON";
|
|
83163
|
+
continue;
|
|
83164
|
+
}
|
|
83165
|
+
const app = appRaw.replace(/^```[a-z]*\s*/i, "").replace(/\s*```$/, "").trim();
|
|
83166
|
+
if (!app.includes("export default App") || !/\/api\/content/.test(app)) {
|
|
83167
|
+
lastErr = "the code didn't fetch /api/content or was incomplete";
|
|
83168
|
+
continue;
|
|
83169
|
+
}
|
|
83170
|
+
return { app, content };
|
|
83171
|
+
}
|
|
83172
|
+
throw new Error(`Couldn't connect this site to the editor (${lastErr}). Try again \u2014 nothing was changed.`);
|
|
83173
|
+
}
|
|
82419
83174
|
function hasBookLink(code) {
|
|
82420
83175
|
return /(href\s*=\s*["'`{]*\s*["'`]?\/book\/?["'`}]?)|(location(?:\.href)?\s*=\s*["'`]\/book["'`])/i.test(code);
|
|
82421
83176
|
}
|
|
@@ -82670,6 +83425,7 @@ var require_places = __commonJS({
|
|
|
82670
83425
|
async function searchOneType(args) {
|
|
82671
83426
|
const all = [];
|
|
82672
83427
|
let pageToken;
|
|
83428
|
+
let error;
|
|
82673
83429
|
for (let page = 0; page < 3; page++) {
|
|
82674
83430
|
const body = {
|
|
82675
83431
|
textQuery: `${args.type} in ${args.city}`,
|
|
@@ -82696,11 +83452,13 @@ var require_places = __commonJS({
|
|
|
82696
83452
|
pageToken = r.data.nextPageToken;
|
|
82697
83453
|
if (!pageToken)
|
|
82698
83454
|
break;
|
|
82699
|
-
} catch {
|
|
83455
|
+
} catch (e) {
|
|
83456
|
+
const ax = e;
|
|
83457
|
+
error = ax.response?.data?.error?.message ?? ax.message ?? "Places request failed";
|
|
82700
83458
|
break;
|
|
82701
83459
|
}
|
|
82702
83460
|
}
|
|
82703
|
-
return { type: args.type, places: all };
|
|
83461
|
+
return { type: args.type, places: all, error };
|
|
82704
83462
|
}
|
|
82705
83463
|
function processRawPlaces(type, places, center, radiusMeters, includeWithWebsite) {
|
|
82706
83464
|
const out = [];
|
|
@@ -82738,16 +83496,28 @@ var require_places = __commonJS({
|
|
|
82738
83496
|
return out;
|
|
82739
83497
|
}
|
|
82740
83498
|
async function searchPlacesStream(args, onBatch) {
|
|
83499
|
+
const stats = { scanned: 0, withWebsite: 0, outsideArea: 0, errors: [] };
|
|
82741
83500
|
await Promise.all(args.types.map(async (type) => {
|
|
82742
|
-
const { places } = await searchOneType({
|
|
83501
|
+
const { places, error } = await searchOneType({
|
|
82743
83502
|
apiKey: args.apiKey,
|
|
82744
83503
|
city: args.city,
|
|
82745
83504
|
type,
|
|
82746
83505
|
center: args.center,
|
|
82747
83506
|
radiusMeters: args.radiusMeters
|
|
82748
83507
|
});
|
|
83508
|
+
if (error)
|
|
83509
|
+
stats.errors.push(error);
|
|
83510
|
+
stats.scanned += places.length;
|
|
83511
|
+
for (const p of places) {
|
|
83512
|
+
if (p.websiteUri)
|
|
83513
|
+
stats.withWebsite++;
|
|
83514
|
+
else if (args.center && args.radiusMeters && p.location && haversineMeters(args.center.lat, args.center.lng, p.location.latitude, p.location.longitude) > args.radiusMeters) {
|
|
83515
|
+
stats.outsideArea++;
|
|
83516
|
+
}
|
|
83517
|
+
}
|
|
82749
83518
|
await onBatch(processRawPlaces(type, places, args.center, args.radiusMeters, args.includeWithWebsite));
|
|
82750
83519
|
}));
|
|
83520
|
+
return stats;
|
|
82751
83521
|
}
|
|
82752
83522
|
async function searchPlaces(args) {
|
|
82753
83523
|
const batches = await Promise.all(args.types.map((type) => searchOneType({
|
|
@@ -82835,15 +83605,24 @@ var require_places = __commonJS({
|
|
|
82835
83605
|
if (!query.trim())
|
|
82836
83606
|
return [];
|
|
82837
83607
|
const r = await axios_1.default.get("https://nominatim.openstreetmap.org/search", {
|
|
82838
|
-
params: { q: query, format: "json", limit, addressdetails: 1 },
|
|
83608
|
+
params: { q: query, format: "json", limit, addressdetails: 1, "accept-language": "en" },
|
|
82839
83609
|
headers: { "User-Agent": "Sproutio/0.1 (desktop app)" }
|
|
82840
83610
|
});
|
|
82841
83611
|
return (r.data ?? []).map((d) => ({
|
|
82842
83612
|
lat: parseFloat(d.lat),
|
|
82843
83613
|
lng: parseFloat(d.lon),
|
|
82844
|
-
label: d.display_name
|
|
83614
|
+
label: d.display_name,
|
|
83615
|
+
short: shortPlaceName(d.address, d.display_name)
|
|
82845
83616
|
}));
|
|
82846
83617
|
}
|
|
83618
|
+
function shortPlaceName(a, fallback) {
|
|
83619
|
+
if (!a)
|
|
83620
|
+
return fallback;
|
|
83621
|
+
const place = a.suburb && (a.city || a.town) ? `${a.suburb}, ${a.city || a.town}` : a.city || a.town || a.village || a.hamlet || a.suburb || a.municipality || a.county;
|
|
83622
|
+
const region = a.state_district || a.county || a.state;
|
|
83623
|
+
const parts = [place, region !== place ? region : null, a.country].filter(Boolean);
|
|
83624
|
+
return parts.length ? [...new Set(parts)].join(", ") : fallback;
|
|
83625
|
+
}
|
|
82847
83626
|
async function geocodeReverse(lat, lng) {
|
|
82848
83627
|
const r = await axios_1.default.get("https://nominatim.openstreetmap.org/reverse", {
|
|
82849
83628
|
params: { lat, lon: lng, format: "json", addressdetails: 1, zoom: 16 },
|
|
@@ -83351,7 +84130,8 @@ var require_lead_search = __commonJS({
|
|
|
83351
84130
|
const wantsSiteHavers = bookingMode || auditMode;
|
|
83352
84131
|
let saved = 0;
|
|
83353
84132
|
let found = 0;
|
|
83354
|
-
|
|
84133
|
+
let alreadyInPipeline = 0;
|
|
84134
|
+
const stats = await (0, places_1.searchPlacesStream)({
|
|
83355
84135
|
apiKey: args.placesKey,
|
|
83356
84136
|
city: args.city,
|
|
83357
84137
|
types: args.types,
|
|
@@ -83368,8 +84148,10 @@ var require_lead_search = __commonJS({
|
|
|
83368
84148
|
continue;
|
|
83369
84149
|
seen.add(r.place_id);
|
|
83370
84150
|
const existing = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
83371
|
-
if (existing.length > 0)
|
|
84151
|
+
if (existing.length > 0) {
|
|
84152
|
+
alreadyInPipeline++;
|
|
83372
84153
|
continue;
|
|
84154
|
+
}
|
|
83373
84155
|
let leadKind = "no_website";
|
|
83374
84156
|
let bookingStatus = null;
|
|
83375
84157
|
let bookingPlatform = null;
|
|
@@ -83436,7 +84218,15 @@ var require_lead_search = __commonJS({
|
|
|
83436
84218
|
});
|
|
83437
84219
|
}
|
|
83438
84220
|
});
|
|
83439
|
-
return {
|
|
84221
|
+
return {
|
|
84222
|
+
found,
|
|
84223
|
+
saved,
|
|
84224
|
+
scanned: stats.scanned,
|
|
84225
|
+
withWebsite: stats.withWebsite,
|
|
84226
|
+
outsideArea: stats.outsideArea,
|
|
84227
|
+
alreadyInPipeline,
|
|
84228
|
+
errors: [...new Set(stats.errors)]
|
|
84229
|
+
};
|
|
83440
84230
|
}
|
|
83441
84231
|
async function searchAndSaveLeads(args) {
|
|
83442
84232
|
const db2 = (0, db_1.getDb)();
|
|
@@ -95688,8 +96478,8 @@ var require_email = __commonJS({
|
|
|
95688
96478
|
var nodemailer_1 = __importDefault(require_nodemailer());
|
|
95689
96479
|
function smtpFromSettings(get) {
|
|
95690
96480
|
const host = get("smtp_host");
|
|
95691
|
-
const user = get("smtp_user");
|
|
95692
|
-
const pass = get("smtp_pass");
|
|
96481
|
+
const user = get("smtp_user") || get("imap_user");
|
|
96482
|
+
const pass = get("smtp_pass") || get("imap_pass");
|
|
95693
96483
|
if (!host || !user || !pass)
|
|
95694
96484
|
return null;
|
|
95695
96485
|
const port = Number(get("smtp_port") || "587");
|
|
@@ -96239,6 +97029,7 @@ Respond with JSON only:
|
|
|
96239
97029
|
AND COALESCE(followed_up_count, 0) <= 1
|
|
96240
97030
|
AND email IS NOT NULL AND email != ''
|
|
96241
97031
|
AND julianday('now') - julianday(last_contacted_at) >= ?
|
|
97032
|
+
${args.onlyLeadIds ? `AND id IN (${args.onlyLeadIds.map(Number).join(",") || "NULL"})` : ""}
|
|
96242
97033
|
ORDER BY last_contacted_at ASC
|
|
96243
97034
|
LIMIT ?`, [args.afterDays, args.limit]);
|
|
96244
97035
|
const agency2 = (0, agency_1.getAgencyProfile)().name ?? "";
|
|
@@ -96677,7 +97468,7 @@ var require_chat_tools = __commonJS({
|
|
|
96677
97468
|
},
|
|
96678
97469
|
{
|
|
96679
97470
|
name: "provision_cms",
|
|
96680
|
-
description: "
|
|
97471
|
+
description: "Give a built site its editor, keeping the design exactly as it is: the owner taps text and photos on their own page at <site>/edit (works on a phone) and has /admin for services, hours and photos. Returns the edit link and password to hand to the customer.",
|
|
96681
97472
|
input_schema: {
|
|
96682
97473
|
type: "object",
|
|
96683
97474
|
properties: { id: { type: "number" } },
|
|
@@ -97473,8 +98264,15 @@ If you'd rather not hear from me again, just reply and let me know.`;
|
|
|
97473
98264
|
}
|
|
97474
98265
|
case "provision_cms": {
|
|
97475
98266
|
const id = Number(input.id);
|
|
97476
|
-
const result = await ctx.invokeIpc("
|
|
97477
|
-
return
|
|
98267
|
+
const result = await ctx.invokeIpc("sites:addEditor", id);
|
|
98268
|
+
return {
|
|
98269
|
+
ok: true,
|
|
98270
|
+
id,
|
|
98271
|
+
publicUrl: result.url,
|
|
98272
|
+
editUrl: `${result.url}/edit`,
|
|
98273
|
+
adminUrl: result.admin?.url,
|
|
98274
|
+
adminPassword: result.admin?.password
|
|
98275
|
+
};
|
|
97478
98276
|
}
|
|
97479
98277
|
case "delete_lead": {
|
|
97480
98278
|
const id = Number(input.id);
|
|
@@ -97731,6 +98529,7 @@ var require_auto_mode = __commonJS({
|
|
|
97731
98529
|
exports2.resumeAutoJob = resumeAutoJob;
|
|
97732
98530
|
exports2.cancelAutoJob = cancelAutoJob2;
|
|
97733
98531
|
exports2.getAutoJobStatus = getAutoJobStatus;
|
|
98532
|
+
exports2.waitForAutoJob = waitForAutoJob;
|
|
97734
98533
|
var db_1 = require_db();
|
|
97735
98534
|
var builder_1 = require_builder();
|
|
97736
98535
|
var AutoJob = class {
|
|
@@ -97881,7 +98680,7 @@ var require_auto_mode = __commonJS({
|
|
|
97881
98680
|
}
|
|
97882
98681
|
}
|
|
97883
98682
|
});
|
|
97884
|
-
db2.run("UPDATE leads SET site_url = ?, status = 'site_built', site_built_at = datetime('now') WHERE id = ?", [result.url, leadId]);
|
|
98683
|
+
db2.run("UPDATE leads SET site_url = ?, status = 'site_built', site_built_at = datetime('now'), admin_url = ?, admin_password = ? WHERE id = ?", [result.url, result.admin?.url ?? null, result.admin?.password ?? null, leadId]);
|
|
97885
98684
|
state.status = "built";
|
|
97886
98685
|
state.siteUrl = result.url;
|
|
97887
98686
|
state.message = "Built";
|
|
@@ -97942,6 +98741,20 @@ var require_auto_mode = __commonJS({
|
|
|
97942
98741
|
function getAutoJobStatus() {
|
|
97943
98742
|
return activeJob ? activeJob.snapshot() : null;
|
|
97944
98743
|
}
|
|
98744
|
+
async function waitForAutoJob(jobId, onProgress, timeoutMs = 90 * 6e4) {
|
|
98745
|
+
const started = Date.now();
|
|
98746
|
+
for (; ; ) {
|
|
98747
|
+
const snap = activeJob && activeJob.id === jobId ? activeJob.snapshot() : null;
|
|
98748
|
+
if (!snap)
|
|
98749
|
+
return null;
|
|
98750
|
+
onProgress?.(snap);
|
|
98751
|
+
if (snap.state === "completed" || snap.state === "cancelled")
|
|
98752
|
+
return snap;
|
|
98753
|
+
if (Date.now() - started > timeoutMs)
|
|
98754
|
+
return snap;
|
|
98755
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
98756
|
+
}
|
|
98757
|
+
}
|
|
97945
98758
|
}
|
|
97946
98759
|
});
|
|
97947
98760
|
|
|
@@ -98481,7 +99294,7 @@ var require_vercel_store = __commonJS({
|
|
|
98481
99294
|
});
|
|
98482
99295
|
|
|
98483
99296
|
// src/engine.ts
|
|
98484
|
-
var db, chat, chatTools, autoMode, cms, bookingPreset, builder, vercelStore, agency, followup, invoicing, models, llm, initDatabase, getDb, chatWithTools, runTool, TOOLS, startAutoJob, cancelAutoJob, provisionCmsForLead, generatePreset, deployBookingOnly, deployStandaloneBooking, handoverSite, createBlobStore, refreshModels, refreshIfStale, listCachedModels, getPreferredModel, getAgencyProfile, setAgencyProfile, agencyConfigured, formatMoney, dueFollowUps, outreachStats, contactHistory, renderLetterPdf, listInvoicesRaw, checkClaudeCodeAuth, llmBackend, claudeBin, CLAUDE_CODE_MODELS, getClaudeCodeModel;
|
|
99297
|
+
var db, chat, chatTools, autoMode, cms, bookingPreset, builder, vercelStore, agency, followup, invoicing, models, llm, initDatabase, getDb, chatWithTools, runTool, TOOLS, startAutoJob, cancelAutoJob, provisionCmsForLead, generatePreset, deployBookingOnly, deployStandaloneBooking, handoverSite, addEditorToSite, createBlobStore, refreshModels, refreshIfStale, listCachedModels, getPreferredModel, getAgencyProfile, setAgencyProfile, agencyConfigured, formatMoney, dueFollowUps, outreachStats, contactHistory, renderLetterPdf, listInvoicesRaw, checkClaudeCodeAuth, llmBackend, claudeBin, CLAUDE_CODE_MODELS, getClaudeCodeModel;
|
|
98485
99298
|
var init_engine = __esm({
|
|
98486
99299
|
"src/engine.ts"() {
|
|
98487
99300
|
"use strict";
|
|
@@ -98510,6 +99323,7 @@ var init_engine = __esm({
|
|
|
98510
99323
|
deployBookingOnly = builder.deployBookingOnly;
|
|
98511
99324
|
deployStandaloneBooking = builder.deployStandaloneBooking;
|
|
98512
99325
|
handoverSite = builder.handoverSite;
|
|
99326
|
+
addEditorToSite = builder.addEditorToSite;
|
|
98513
99327
|
createBlobStore = vercelStore.createBlobStore;
|
|
98514
99328
|
refreshModels = models.refreshModels;
|
|
98515
99329
|
refreshIfStale = models.refreshIfStale;
|
|
@@ -98977,7 +99791,7 @@ var require_package2 = __commonJS({
|
|
|
98977
99791
|
"package.json"(exports2, module2) {
|
|
98978
99792
|
module2.exports = {
|
|
98979
99793
|
name: "sproutkit-code",
|
|
98980
|
-
version: "1.
|
|
99794
|
+
version: "1.11.1",
|
|
98981
99795
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
98982
99796
|
bin: {
|
|
98983
99797
|
sproutkit: "./dist/index.js"
|
|
@@ -99055,6 +99869,31 @@ function makeInvokeIpc(emit2) {
|
|
|
99055
99869
|
emit: emit2
|
|
99056
99870
|
});
|
|
99057
99871
|
}
|
|
99872
|
+
if (channel === "sites:addEditor") {
|
|
99873
|
+
const leadId = Number(payload);
|
|
99874
|
+
const db2 = getDb();
|
|
99875
|
+
const lead = db2.get("SELECT * FROM leads WHERE id = ?", [leadId]);
|
|
99876
|
+
if (!lead) throw new Error("Lead not found");
|
|
99877
|
+
if (!lead.site_url) throw new Error("Build the website first, then add the editor.");
|
|
99878
|
+
const vercelToken = getSetting("vercel_token");
|
|
99879
|
+
if (!vercelToken) throw new Error("Vercel token not set");
|
|
99880
|
+
const blobToken = await requireBlobToken(vercelToken, emit2, leadId);
|
|
99881
|
+
const result = await addEditorToSite({
|
|
99882
|
+
lead,
|
|
99883
|
+
anthropicKey: getSetting("anthropic_key"),
|
|
99884
|
+
vercelToken,
|
|
99885
|
+
blobToken,
|
|
99886
|
+
reuseAdminPassword: lead.admin_password ?? null,
|
|
99887
|
+
onProgress: (msg) => emit2("cms:progress", { leadId, msg })
|
|
99888
|
+
});
|
|
99889
|
+
db2.run("UPDATE leads SET site_url = ?, admin_url = ?, admin_password = ? WHERE id = ?", [
|
|
99890
|
+
result.url,
|
|
99891
|
+
result.admin?.url ?? null,
|
|
99892
|
+
result.admin?.password ?? null,
|
|
99893
|
+
leadId
|
|
99894
|
+
]);
|
|
99895
|
+
return result;
|
|
99896
|
+
}
|
|
99058
99897
|
if (channel === "cms:provision") {
|
|
99059
99898
|
const leadId = Number(payload);
|
|
99060
99899
|
const db2 = getDb();
|
|
@@ -100924,23 +101763,24 @@ async function cmdCms(argv) {
|
|
|
100924
101763
|
return;
|
|
100925
101764
|
}
|
|
100926
101765
|
const status = json ? void 0 : new Status();
|
|
100927
|
-
if (status) status.start(`
|
|
101766
|
+
if (status) status.start(`Adding the editor to lead #${id}'s site`);
|
|
100928
101767
|
try {
|
|
100929
101768
|
const result = await runTool("provision_cms", { id }, makeCtx({ json, status }));
|
|
100930
|
-
if (status) status.done(true, result.publicUrl ? `
|
|
101769
|
+
if (status) status.done(true, result.publicUrl ? `Editor live at ${result.publicUrl}/edit` : `Editor added for #${id}`);
|
|
100931
101770
|
autoSync();
|
|
100932
101771
|
if (json) emitJson(result);
|
|
100933
|
-
else sayAdminHandover(result.adminUrl, result.adminPassword);
|
|
101772
|
+
else sayAdminHandover(result.adminUrl, result.adminPassword, result.editUrl);
|
|
100934
101773
|
} catch (e) {
|
|
100935
101774
|
if (status) status.done(false, "CMS setup failed");
|
|
100936
101775
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
100937
101776
|
process.exitCode = 1;
|
|
100938
101777
|
}
|
|
100939
101778
|
}
|
|
100940
|
-
function sayAdminHandover(adminUrl, adminPassword) {
|
|
101779
|
+
function sayAdminHandover(adminUrl, adminPassword, editUrl) {
|
|
100941
101780
|
if (!adminUrl || !adminPassword) return;
|
|
100942
101781
|
say("");
|
|
100943
101782
|
say(c.bold(" Client handover"));
|
|
101783
|
+
if (editUrl) say(` Edit page: ${editUrl} (tap text/photos to change them \u2014 works on a phone)`);
|
|
100944
101784
|
say(` Admin panel: ${adminUrl}`);
|
|
100945
101785
|
say(` Password: ${adminPassword}`);
|
|
100946
101786
|
say(c.dim(" Send these to the client \u2014 they manage bookings and content there."));
|