sproutkit-code 1.10.0 → 1.11.0
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 +803 -26
- 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,6 +81600,7 @@ 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");
|
|
@@ -81164,6 +81712,11 @@ var require_builder = __commonJS({
|
|
|
81164
81712
|
const title = p.name ? `${p.name} \u2014 site manager` : "Site editor";
|
|
81165
81713
|
return cms_runtime_1.ADMIN_INDEX_HTML.replace("__ADMIN_TITLE__", title.replace(/[<>&]/g, ""));
|
|
81166
81714
|
}
|
|
81715
|
+
function editHtml() {
|
|
81716
|
+
const p = (0, agency_1.getAgencyProfile)();
|
|
81717
|
+
const title = p.name ? `Edit your site \u2014 ${p.name}` : "Edit your site";
|
|
81718
|
+
return cms_runtime_1.EDIT_INDEX_HTML.replace("__EDIT_TITLE__", title.replace(/[<>&]/g, ""));
|
|
81719
|
+
}
|
|
81167
81720
|
function bannerFor(lead) {
|
|
81168
81721
|
let handedOver = false;
|
|
81169
81722
|
try {
|
|
@@ -81861,7 +82414,9 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81861
82414
|
const vercelJson = JSON.stringify({
|
|
81862
82415
|
rewrites: [
|
|
81863
82416
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
81864
|
-
{ source: "/admin/", destination: "/admin/index.html" }
|
|
82417
|
+
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82418
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82419
|
+
{ source: "/edit/", destination: "/edit/index.html" }
|
|
81865
82420
|
]
|
|
81866
82421
|
});
|
|
81867
82422
|
cmsExtraFiles = [
|
|
@@ -81873,7 +82428,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81873
82428
|
{ file: "api/admin/logout.js", data: cms_runtime_1.API_ADMIN_LOGOUT_JS, encoding: "utf-8" },
|
|
81874
82429
|
{ file: "api/admin/upload.js", data: cms_runtime_1.API_ADMIN_UPLOAD_JS, encoding: "utf-8" },
|
|
81875
82430
|
{ 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" }
|
|
82431
|
+
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
82432
|
+
{ file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" }
|
|
81877
82433
|
];
|
|
81878
82434
|
cmsAdmin = { url: "", password: adminPassword };
|
|
81879
82435
|
}
|
|
@@ -81973,6 +82529,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
81973
82529
|
rewrites: [
|
|
81974
82530
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
81975
82531
|
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82532
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82533
|
+
{ source: "/edit/", destination: "/edit/index.html" },
|
|
81976
82534
|
{ source: "/book", destination: "/book/index.html" },
|
|
81977
82535
|
{ source: "/book/", destination: "/book/index.html" }
|
|
81978
82536
|
]
|
|
@@ -81985,7 +82543,7 @@ createRoot(document.getElementById('root')).render(<StrictMode><App /></StrictMo
|
|
|
81985
82543
|
const viteConfigCms = `import { defineConfig } from 'vite';
|
|
81986
82544
|
import react from '@vitejs/plugin-react';
|
|
81987
82545
|
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" });
|
|
82546
|
+
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
82547
|
if (!filesToRedeploy.some((f) => f.file === "src/App.jsx")) {
|
|
81990
82548
|
throw new Error("Couldn't fetch src/App.jsx from the existing deployment. Try Regenerate website first, then Redeploy CMS.");
|
|
81991
82549
|
}
|
|
@@ -82121,6 +82679,8 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82121
82679
|
rewrites: [
|
|
82122
82680
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
82123
82681
|
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82682
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82683
|
+
{ source: "/edit/", destination: "/edit/index.html" },
|
|
82124
82684
|
{ source: "/book", destination: "/book/index.html" },
|
|
82125
82685
|
{ source: "/book/", destination: "/book/index.html" }
|
|
82126
82686
|
]
|
|
@@ -82141,6 +82701,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82141
82701
|
// Refresh the CMS admin app (contains the Bookings tab now)
|
|
82142
82702
|
{ file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" },
|
|
82143
82703
|
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
82704
|
+
{ file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" },
|
|
82144
82705
|
{ file: "api/content.js", data: cms_runtime_1.API_CONTENT_JS, encoding: "utf-8" },
|
|
82145
82706
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
82146
82707
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
@@ -82206,6 +82767,7 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82206
82767
|
// admin the CMS sites use — it already has a Bookings tab.
|
|
82207
82768
|
{ file: "public/admin/index.html", data: adminHtml(), encoding: "utf-8" },
|
|
82208
82769
|
{ file: "public/admin/app.js", data: cms_runtime_1.ADMIN_APP_JS, encoding: "utf-8" },
|
|
82770
|
+
{ file: "public/edit/index.html", data: editHtml(), encoding: "utf-8" },
|
|
82209
82771
|
{ file: "api/admin/login.js", data: cms_runtime_1.API_ADMIN_LOGIN_JS, encoding: "utf-8" },
|
|
82210
82772
|
{ file: "api/admin/whoami.js", data: cms_runtime_1.API_ADMIN_WHOAMI_JS, encoding: "utf-8" },
|
|
82211
82773
|
{ file: "api/admin/password.js", data: cms_runtime_1.API_ADMIN_PASSWORD_JS, encoding: "utf-8" },
|
|
@@ -82218,7 +82780,9 @@ export default defineConfig({ plugins: [react()] });`;
|
|
|
82218
82780
|
data: JSON.stringify({
|
|
82219
82781
|
rewrites: [
|
|
82220
82782
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
82221
|
-
{ source: "/admin/", destination: "/admin/index.html" }
|
|
82783
|
+
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82784
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82785
|
+
{ source: "/edit/", destination: "/edit/index.html" }
|
|
82222
82786
|
]
|
|
82223
82787
|
}),
|
|
82224
82788
|
encoding: "utf-8"
|
|
@@ -82388,12 +82952,14 @@ export default defineConfig({ plugins: [react()] });`,
|
|
|
82388
82952
|
rewrites: [
|
|
82389
82953
|
{ source: "/admin", destination: "/admin/index.html" },
|
|
82390
82954
|
{ source: "/admin/", destination: "/admin/index.html" },
|
|
82955
|
+
{ source: "/edit", destination: "/edit/index.html" },
|
|
82956
|
+
{ source: "/edit/", destination: "/edit/index.html" },
|
|
82391
82957
|
{ source: "/book", destination: "/book/index.html" },
|
|
82392
82958
|
{ source: "/book/", destination: "/book/index.html" }
|
|
82393
82959
|
]
|
|
82394
82960
|
}),
|
|
82395
82961
|
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" });
|
|
82962
|
+
}, { 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
82963
|
const env = {
|
|
82398
82964
|
BLOB_READ_WRITE_TOKEN: blobToken,
|
|
82399
82965
|
ADMIN_PASSWORD_HASH: await (0, cms_runtime_1.hashPassword)(adminPassword),
|
|
@@ -82416,6 +82982,132 @@ export default defineConfig({ plugins: [react()] });`,
|
|
|
82416
82982
|
onProgress(`Live at ${url}`);
|
|
82417
82983
|
return { url };
|
|
82418
82984
|
}
|
|
82985
|
+
async function addEditorToSite2(args) {
|
|
82986
|
+
const { lead, anthropicKey, vercelToken, blobToken, reuseAdminPassword, onProgress } = args;
|
|
82987
|
+
onProgress("Loading site source\u2026");
|
|
82988
|
+
const snapshot = loadSnapshot(lead.id);
|
|
82989
|
+
const appIdx = snapshot ? snapshot.findIndex((f) => f.file === "src/App.jsx") : -1;
|
|
82990
|
+
if (!snapshot || appIdx < 0) {
|
|
82991
|
+
throw new Error("No local copy of this site's code. Regenerate the website once (that saves a copy), then add the editor.");
|
|
82992
|
+
}
|
|
82993
|
+
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]}`);
|
|
82994
|
+
let seed = null;
|
|
82995
|
+
if (!/["'`]\/api\/content["'`]/.test(snapshot[appIdx].data)) {
|
|
82996
|
+
onProgress("Connecting the page's text and photos to the editor\u2026");
|
|
82997
|
+
const wired = await wireAppToCms({
|
|
82998
|
+
anthropicKey,
|
|
82999
|
+
appJsx: snapshot[appIdx].data,
|
|
83000
|
+
lead
|
|
83001
|
+
});
|
|
83002
|
+
snapshot[appIdx] = { ...snapshot[appIdx], data: wired.app };
|
|
83003
|
+
saveSnapshot(lead.id, snapshot);
|
|
83004
|
+
seed = {
|
|
83005
|
+
reviews: [],
|
|
83006
|
+
theme: { variant: "premium-dark", accent: "#22c55e" },
|
|
83007
|
+
...wired.content,
|
|
83008
|
+
photos: photoPaths.map((url2) => ({ url: url2, alt: "" }))
|
|
83009
|
+
};
|
|
83010
|
+
}
|
|
83011
|
+
const siteUid = siteUidFor(lead.id);
|
|
83012
|
+
if (seed) {
|
|
83013
|
+
onProgress("Saving the page's current text\u2026");
|
|
83014
|
+
const { put } = await Promise.resolve().then(() => __importStar(require_dist5()));
|
|
83015
|
+
await put(`sites/${siteUid}/content.json`, JSON.stringify(seed), {
|
|
83016
|
+
access: "public",
|
|
83017
|
+
addRandomSuffix: false,
|
|
83018
|
+
allowOverwrite: true,
|
|
83019
|
+
contentType: "application/json",
|
|
83020
|
+
token: blobToken
|
|
83021
|
+
});
|
|
83022
|
+
}
|
|
83023
|
+
const password = reuseAdminPassword ?? (0, cms_runtime_1.generateFriendlyPassword)();
|
|
83024
|
+
const { url } = await redeployWithoutBanner({
|
|
83025
|
+
lead,
|
|
83026
|
+
vercelToken,
|
|
83027
|
+
blobToken,
|
|
83028
|
+
hasCms: true,
|
|
83029
|
+
adminPassword: password,
|
|
83030
|
+
siteUid,
|
|
83031
|
+
onProgress
|
|
83032
|
+
});
|
|
83033
|
+
return { url, admin: { url: `${url}/admin`, password } };
|
|
83034
|
+
}
|
|
83035
|
+
async function wireAppToCms(args) {
|
|
83036
|
+
const { anthropicKey, appJsx, lead } = args;
|
|
83037
|
+
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.
|
|
83038
|
+
|
|
83039
|
+
Change ONLY where the text comes from:
|
|
83040
|
+
|
|
83041
|
+
1. Add near the top:
|
|
83042
|
+
const FALLBACK = { ...exactly the content JSON you output below... };
|
|
83043
|
+
2. Inside App:
|
|
83044
|
+
const [content, setContent] = useState(FALLBACK);
|
|
83045
|
+
useEffect(() => {
|
|
83046
|
+
fetch("/api/content").then(r => r.ok ? r.json() : null).then(d => {
|
|
83047
|
+
if (d && d.business) setContent(prev => ({ ...prev, ...d }));
|
|
83048
|
+
}).catch(() => {});
|
|
83049
|
+
}, []);
|
|
83050
|
+
If the file has several components (pages, sections), pass content down as a prop \u2014 keep the fetch in App only.
|
|
83051
|
+
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.
|
|
83052
|
+
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].
|
|
83053
|
+
5. Opening hours: render content.contact.hours if the page shows hours; if that array is empty the page must omit the hours block.
|
|
83054
|
+
|
|
83055
|
+
Content fields (use only what the page actually shows; leave others as "" or []):
|
|
83056
|
+
{
|
|
83057
|
+
"business": { "name": string, "tagline": string },
|
|
83058
|
+
"hero": { "headline": string, "subheadline": string, "ctaText": string },
|
|
83059
|
+
"about": string,
|
|
83060
|
+
"services": [{ "title": string, "description": string, "price": string }],
|
|
83061
|
+
"contact": { "address": string, "phone": string, "email": string, "hours": [string] }
|
|
83062
|
+
}
|
|
83063
|
+
Business name: ${JSON.stringify(lead.name)}. Phone: ${JSON.stringify(lead.phone ?? "")}. Address: ${JSON.stringify(lead.address ?? "")}.
|
|
83064
|
+
|
|
83065
|
+
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.
|
|
83066
|
+
|
|
83067
|
+
Reply in exactly this format and nothing else:
|
|
83068
|
+
<content>
|
|
83069
|
+
{ the content JSON }
|
|
83070
|
+
</content>
|
|
83071
|
+
<app>
|
|
83072
|
+
the complete new App.jsx
|
|
83073
|
+
</app>
|
|
83074
|
+
|
|
83075
|
+
App.jsx:
|
|
83076
|
+
${appJsx}`;
|
|
83077
|
+
let lastErr = "";
|
|
83078
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
83079
|
+
const res = await (0, llm_1.generateText)({
|
|
83080
|
+
apiKey: anthropicKey,
|
|
83081
|
+
prompt: attempt === 0 ? prompt : `${prompt}
|
|
83082
|
+
|
|
83083
|
+
Your previous reply was unusable (${lastErr}). Follow the reply format exactly.`,
|
|
83084
|
+
model: (0, models_1.getPreferredModel)(),
|
|
83085
|
+
maxTokens: 64e3,
|
|
83086
|
+
timeoutMs: 3e5
|
|
83087
|
+
});
|
|
83088
|
+
const text = res.text ?? "";
|
|
83089
|
+
const contentRaw = text.match(/<content>\s*([\s\S]*?)\s*<\/content>/)?.[1];
|
|
83090
|
+
const appRaw = text.match(/<app>\s*([\s\S]*?)\s*<\/app>/)?.[1];
|
|
83091
|
+
if (!contentRaw || !appRaw) {
|
|
83092
|
+
lastErr = res.stopReason === "max_tokens" ? "it was cut off" : "missing <content> or <app>";
|
|
83093
|
+
continue;
|
|
83094
|
+
}
|
|
83095
|
+
let content;
|
|
83096
|
+
try {
|
|
83097
|
+
content = JSON.parse(contentRaw.replace(/^```[a-z]*\s*/i, "").replace(/\s*```$/, ""));
|
|
83098
|
+
} catch {
|
|
83099
|
+
lastErr = "the content wasn't valid JSON";
|
|
83100
|
+
continue;
|
|
83101
|
+
}
|
|
83102
|
+
const app = appRaw.replace(/^```[a-z]*\s*/i, "").replace(/\s*```$/, "").trim();
|
|
83103
|
+
if (!app.includes("export default App") || !/\/api\/content/.test(app)) {
|
|
83104
|
+
lastErr = "the code didn't fetch /api/content or was incomplete";
|
|
83105
|
+
continue;
|
|
83106
|
+
}
|
|
83107
|
+
return { app, content };
|
|
83108
|
+
}
|
|
83109
|
+
throw new Error(`Couldn't connect this site to the editor (${lastErr}). Try again \u2014 nothing was changed.`);
|
|
83110
|
+
}
|
|
82419
83111
|
function hasBookLink(code) {
|
|
82420
83112
|
return /(href\s*=\s*["'`{]*\s*["'`]?\/book\/?["'`}]?)|(location(?:\.href)?\s*=\s*["'`]\/book["'`])/i.test(code);
|
|
82421
83113
|
}
|
|
@@ -82670,6 +83362,7 @@ var require_places = __commonJS({
|
|
|
82670
83362
|
async function searchOneType(args) {
|
|
82671
83363
|
const all = [];
|
|
82672
83364
|
let pageToken;
|
|
83365
|
+
let error;
|
|
82673
83366
|
for (let page = 0; page < 3; page++) {
|
|
82674
83367
|
const body = {
|
|
82675
83368
|
textQuery: `${args.type} in ${args.city}`,
|
|
@@ -82696,11 +83389,13 @@ var require_places = __commonJS({
|
|
|
82696
83389
|
pageToken = r.data.nextPageToken;
|
|
82697
83390
|
if (!pageToken)
|
|
82698
83391
|
break;
|
|
82699
|
-
} catch {
|
|
83392
|
+
} catch (e) {
|
|
83393
|
+
const ax = e;
|
|
83394
|
+
error = ax.response?.data?.error?.message ?? ax.message ?? "Places request failed";
|
|
82700
83395
|
break;
|
|
82701
83396
|
}
|
|
82702
83397
|
}
|
|
82703
|
-
return { type: args.type, places: all };
|
|
83398
|
+
return { type: args.type, places: all, error };
|
|
82704
83399
|
}
|
|
82705
83400
|
function processRawPlaces(type, places, center, radiusMeters, includeWithWebsite) {
|
|
82706
83401
|
const out = [];
|
|
@@ -82738,16 +83433,28 @@ var require_places = __commonJS({
|
|
|
82738
83433
|
return out;
|
|
82739
83434
|
}
|
|
82740
83435
|
async function searchPlacesStream(args, onBatch) {
|
|
83436
|
+
const stats = { scanned: 0, withWebsite: 0, outsideArea: 0, errors: [] };
|
|
82741
83437
|
await Promise.all(args.types.map(async (type) => {
|
|
82742
|
-
const { places } = await searchOneType({
|
|
83438
|
+
const { places, error } = await searchOneType({
|
|
82743
83439
|
apiKey: args.apiKey,
|
|
82744
83440
|
city: args.city,
|
|
82745
83441
|
type,
|
|
82746
83442
|
center: args.center,
|
|
82747
83443
|
radiusMeters: args.radiusMeters
|
|
82748
83444
|
});
|
|
83445
|
+
if (error)
|
|
83446
|
+
stats.errors.push(error);
|
|
83447
|
+
stats.scanned += places.length;
|
|
83448
|
+
for (const p of places) {
|
|
83449
|
+
if (p.websiteUri)
|
|
83450
|
+
stats.withWebsite++;
|
|
83451
|
+
else if (args.center && args.radiusMeters && p.location && haversineMeters(args.center.lat, args.center.lng, p.location.latitude, p.location.longitude) > args.radiusMeters) {
|
|
83452
|
+
stats.outsideArea++;
|
|
83453
|
+
}
|
|
83454
|
+
}
|
|
82749
83455
|
await onBatch(processRawPlaces(type, places, args.center, args.radiusMeters, args.includeWithWebsite));
|
|
82750
83456
|
}));
|
|
83457
|
+
return stats;
|
|
82751
83458
|
}
|
|
82752
83459
|
async function searchPlaces(args) {
|
|
82753
83460
|
const batches = await Promise.all(args.types.map((type) => searchOneType({
|
|
@@ -82835,15 +83542,24 @@ var require_places = __commonJS({
|
|
|
82835
83542
|
if (!query.trim())
|
|
82836
83543
|
return [];
|
|
82837
83544
|
const r = await axios_1.default.get("https://nominatim.openstreetmap.org/search", {
|
|
82838
|
-
params: { q: query, format: "json", limit, addressdetails: 1 },
|
|
83545
|
+
params: { q: query, format: "json", limit, addressdetails: 1, "accept-language": "en" },
|
|
82839
83546
|
headers: { "User-Agent": "Sproutio/0.1 (desktop app)" }
|
|
82840
83547
|
});
|
|
82841
83548
|
return (r.data ?? []).map((d) => ({
|
|
82842
83549
|
lat: parseFloat(d.lat),
|
|
82843
83550
|
lng: parseFloat(d.lon),
|
|
82844
|
-
label: d.display_name
|
|
83551
|
+
label: d.display_name,
|
|
83552
|
+
short: shortPlaceName(d.address, d.display_name)
|
|
82845
83553
|
}));
|
|
82846
83554
|
}
|
|
83555
|
+
function shortPlaceName(a, fallback) {
|
|
83556
|
+
if (!a)
|
|
83557
|
+
return fallback;
|
|
83558
|
+
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;
|
|
83559
|
+
const region = a.state_district || a.county || a.state;
|
|
83560
|
+
const parts = [place, region !== place ? region : null, a.country].filter(Boolean);
|
|
83561
|
+
return parts.length ? [...new Set(parts)].join(", ") : fallback;
|
|
83562
|
+
}
|
|
82847
83563
|
async function geocodeReverse(lat, lng) {
|
|
82848
83564
|
const r = await axios_1.default.get("https://nominatim.openstreetmap.org/reverse", {
|
|
82849
83565
|
params: { lat, lon: lng, format: "json", addressdetails: 1, zoom: 16 },
|
|
@@ -83351,7 +84067,8 @@ var require_lead_search = __commonJS({
|
|
|
83351
84067
|
const wantsSiteHavers = bookingMode || auditMode;
|
|
83352
84068
|
let saved = 0;
|
|
83353
84069
|
let found = 0;
|
|
83354
|
-
|
|
84070
|
+
let alreadyInPipeline = 0;
|
|
84071
|
+
const stats = await (0, places_1.searchPlacesStream)({
|
|
83355
84072
|
apiKey: args.placesKey,
|
|
83356
84073
|
city: args.city,
|
|
83357
84074
|
types: args.types,
|
|
@@ -83368,8 +84085,10 @@ var require_lead_search = __commonJS({
|
|
|
83368
84085
|
continue;
|
|
83369
84086
|
seen.add(r.place_id);
|
|
83370
84087
|
const existing = db2.all("SELECT id FROM leads WHERE place_id = ?", [r.place_id]);
|
|
83371
|
-
if (existing.length > 0)
|
|
84088
|
+
if (existing.length > 0) {
|
|
84089
|
+
alreadyInPipeline++;
|
|
83372
84090
|
continue;
|
|
84091
|
+
}
|
|
83373
84092
|
let leadKind = "no_website";
|
|
83374
84093
|
let bookingStatus = null;
|
|
83375
84094
|
let bookingPlatform = null;
|
|
@@ -83436,7 +84155,15 @@ var require_lead_search = __commonJS({
|
|
|
83436
84155
|
});
|
|
83437
84156
|
}
|
|
83438
84157
|
});
|
|
83439
|
-
return {
|
|
84158
|
+
return {
|
|
84159
|
+
found,
|
|
84160
|
+
saved,
|
|
84161
|
+
scanned: stats.scanned,
|
|
84162
|
+
withWebsite: stats.withWebsite,
|
|
84163
|
+
outsideArea: stats.outsideArea,
|
|
84164
|
+
alreadyInPipeline,
|
|
84165
|
+
errors: [...new Set(stats.errors)]
|
|
84166
|
+
};
|
|
83440
84167
|
}
|
|
83441
84168
|
async function searchAndSaveLeads(args) {
|
|
83442
84169
|
const db2 = (0, db_1.getDb)();
|
|
@@ -95688,8 +96415,8 @@ var require_email = __commonJS({
|
|
|
95688
96415
|
var nodemailer_1 = __importDefault(require_nodemailer());
|
|
95689
96416
|
function smtpFromSettings(get) {
|
|
95690
96417
|
const host = get("smtp_host");
|
|
95691
|
-
const user = get("smtp_user");
|
|
95692
|
-
const pass = get("smtp_pass");
|
|
96418
|
+
const user = get("smtp_user") || get("imap_user");
|
|
96419
|
+
const pass = get("smtp_pass") || get("imap_pass");
|
|
95693
96420
|
if (!host || !user || !pass)
|
|
95694
96421
|
return null;
|
|
95695
96422
|
const port = Number(get("smtp_port") || "587");
|
|
@@ -96239,6 +96966,7 @@ Respond with JSON only:
|
|
|
96239
96966
|
AND COALESCE(followed_up_count, 0) <= 1
|
|
96240
96967
|
AND email IS NOT NULL AND email != ''
|
|
96241
96968
|
AND julianday('now') - julianday(last_contacted_at) >= ?
|
|
96969
|
+
${args.onlyLeadIds ? `AND id IN (${args.onlyLeadIds.map(Number).join(",") || "NULL"})` : ""}
|
|
96242
96970
|
ORDER BY last_contacted_at ASC
|
|
96243
96971
|
LIMIT ?`, [args.afterDays, args.limit]);
|
|
96244
96972
|
const agency2 = (0, agency_1.getAgencyProfile)().name ?? "";
|
|
@@ -96677,7 +97405,7 @@ var require_chat_tools = __commonJS({
|
|
|
96677
97405
|
},
|
|
96678
97406
|
{
|
|
96679
97407
|
name: "provision_cms",
|
|
96680
|
-
description: "
|
|
97408
|
+
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
97409
|
input_schema: {
|
|
96682
97410
|
type: "object",
|
|
96683
97411
|
properties: { id: { type: "number" } },
|
|
@@ -97473,8 +98201,15 @@ If you'd rather not hear from me again, just reply and let me know.`;
|
|
|
97473
98201
|
}
|
|
97474
98202
|
case "provision_cms": {
|
|
97475
98203
|
const id = Number(input.id);
|
|
97476
|
-
const result = await ctx.invokeIpc("
|
|
97477
|
-
return
|
|
98204
|
+
const result = await ctx.invokeIpc("sites:addEditor", id);
|
|
98205
|
+
return {
|
|
98206
|
+
ok: true,
|
|
98207
|
+
id,
|
|
98208
|
+
publicUrl: result.url,
|
|
98209
|
+
editUrl: `${result.url}/edit`,
|
|
98210
|
+
adminUrl: result.admin?.url,
|
|
98211
|
+
adminPassword: result.admin?.password
|
|
98212
|
+
};
|
|
97478
98213
|
}
|
|
97479
98214
|
case "delete_lead": {
|
|
97480
98215
|
const id = Number(input.id);
|
|
@@ -97731,6 +98466,7 @@ var require_auto_mode = __commonJS({
|
|
|
97731
98466
|
exports2.resumeAutoJob = resumeAutoJob;
|
|
97732
98467
|
exports2.cancelAutoJob = cancelAutoJob2;
|
|
97733
98468
|
exports2.getAutoJobStatus = getAutoJobStatus;
|
|
98469
|
+
exports2.waitForAutoJob = waitForAutoJob;
|
|
97734
98470
|
var db_1 = require_db();
|
|
97735
98471
|
var builder_1 = require_builder();
|
|
97736
98472
|
var AutoJob = class {
|
|
@@ -97942,6 +98678,20 @@ var require_auto_mode = __commonJS({
|
|
|
97942
98678
|
function getAutoJobStatus() {
|
|
97943
98679
|
return activeJob ? activeJob.snapshot() : null;
|
|
97944
98680
|
}
|
|
98681
|
+
async function waitForAutoJob(jobId, onProgress, timeoutMs = 90 * 6e4) {
|
|
98682
|
+
const started = Date.now();
|
|
98683
|
+
for (; ; ) {
|
|
98684
|
+
const snap = activeJob && activeJob.id === jobId ? activeJob.snapshot() : null;
|
|
98685
|
+
if (!snap)
|
|
98686
|
+
return null;
|
|
98687
|
+
onProgress?.(snap);
|
|
98688
|
+
if (snap.state === "completed" || snap.state === "cancelled")
|
|
98689
|
+
return snap;
|
|
98690
|
+
if (Date.now() - started > timeoutMs)
|
|
98691
|
+
return snap;
|
|
98692
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
98693
|
+
}
|
|
98694
|
+
}
|
|
97945
98695
|
}
|
|
97946
98696
|
});
|
|
97947
98697
|
|
|
@@ -98481,7 +99231,7 @@ var require_vercel_store = __commonJS({
|
|
|
98481
99231
|
});
|
|
98482
99232
|
|
|
98483
99233
|
// 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;
|
|
99234
|
+
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
99235
|
var init_engine = __esm({
|
|
98486
99236
|
"src/engine.ts"() {
|
|
98487
99237
|
"use strict";
|
|
@@ -98510,6 +99260,7 @@ var init_engine = __esm({
|
|
|
98510
99260
|
deployBookingOnly = builder.deployBookingOnly;
|
|
98511
99261
|
deployStandaloneBooking = builder.deployStandaloneBooking;
|
|
98512
99262
|
handoverSite = builder.handoverSite;
|
|
99263
|
+
addEditorToSite = builder.addEditorToSite;
|
|
98513
99264
|
createBlobStore = vercelStore.createBlobStore;
|
|
98514
99265
|
refreshModels = models.refreshModels;
|
|
98515
99266
|
refreshIfStale = models.refreshIfStale;
|
|
@@ -98977,7 +99728,7 @@ var require_package2 = __commonJS({
|
|
|
98977
99728
|
"package.json"(exports2, module2) {
|
|
98978
99729
|
module2.exports = {
|
|
98979
99730
|
name: "sproutkit-code",
|
|
98980
|
-
version: "1.
|
|
99731
|
+
version: "1.11.0",
|
|
98981
99732
|
description: "Sproutkit in your terminal \u2014 find leads, build sites, run outreach, all via chat.",
|
|
98982
99733
|
bin: {
|
|
98983
99734
|
sproutkit: "./dist/index.js"
|
|
@@ -99055,6 +99806,31 @@ function makeInvokeIpc(emit2) {
|
|
|
99055
99806
|
emit: emit2
|
|
99056
99807
|
});
|
|
99057
99808
|
}
|
|
99809
|
+
if (channel === "sites:addEditor") {
|
|
99810
|
+
const leadId = Number(payload);
|
|
99811
|
+
const db2 = getDb();
|
|
99812
|
+
const lead = db2.get("SELECT * FROM leads WHERE id = ?", [leadId]);
|
|
99813
|
+
if (!lead) throw new Error("Lead not found");
|
|
99814
|
+
if (!lead.site_url) throw new Error("Build the website first, then add the editor.");
|
|
99815
|
+
const vercelToken = getSetting("vercel_token");
|
|
99816
|
+
if (!vercelToken) throw new Error("Vercel token not set");
|
|
99817
|
+
const blobToken = await requireBlobToken(vercelToken, emit2, leadId);
|
|
99818
|
+
const result = await addEditorToSite({
|
|
99819
|
+
lead,
|
|
99820
|
+
anthropicKey: getSetting("anthropic_key"),
|
|
99821
|
+
vercelToken,
|
|
99822
|
+
blobToken,
|
|
99823
|
+
reuseAdminPassword: lead.admin_password ?? null,
|
|
99824
|
+
onProgress: (msg) => emit2("cms:progress", { leadId, msg })
|
|
99825
|
+
});
|
|
99826
|
+
db2.run("UPDATE leads SET site_url = ?, admin_url = ?, admin_password = ? WHERE id = ?", [
|
|
99827
|
+
result.url,
|
|
99828
|
+
result.admin?.url ?? null,
|
|
99829
|
+
result.admin?.password ?? null,
|
|
99830
|
+
leadId
|
|
99831
|
+
]);
|
|
99832
|
+
return result;
|
|
99833
|
+
}
|
|
99058
99834
|
if (channel === "cms:provision") {
|
|
99059
99835
|
const leadId = Number(payload);
|
|
99060
99836
|
const db2 = getDb();
|
|
@@ -100924,23 +101700,24 @@ async function cmdCms(argv) {
|
|
|
100924
101700
|
return;
|
|
100925
101701
|
}
|
|
100926
101702
|
const status = json ? void 0 : new Status();
|
|
100927
|
-
if (status) status.start(`
|
|
101703
|
+
if (status) status.start(`Adding the editor to lead #${id}'s site`);
|
|
100928
101704
|
try {
|
|
100929
101705
|
const result = await runTool("provision_cms", { id }, makeCtx({ json, status }));
|
|
100930
|
-
if (status) status.done(true, result.publicUrl ? `
|
|
101706
|
+
if (status) status.done(true, result.publicUrl ? `Editor live at ${result.publicUrl}/edit` : `Editor added for #${id}`);
|
|
100931
101707
|
autoSync();
|
|
100932
101708
|
if (json) emitJson(result);
|
|
100933
|
-
else sayAdminHandover(result.adminUrl, result.adminPassword);
|
|
101709
|
+
else sayAdminHandover(result.adminUrl, result.adminPassword, result.editUrl);
|
|
100934
101710
|
} catch (e) {
|
|
100935
101711
|
if (status) status.done(false, "CMS setup failed");
|
|
100936
101712
|
json ? emitJson({ ok: false, error: e.message }) : say(c.red(" " + e.message));
|
|
100937
101713
|
process.exitCode = 1;
|
|
100938
101714
|
}
|
|
100939
101715
|
}
|
|
100940
|
-
function sayAdminHandover(adminUrl, adminPassword) {
|
|
101716
|
+
function sayAdminHandover(adminUrl, adminPassword, editUrl) {
|
|
100941
101717
|
if (!adminUrl || !adminPassword) return;
|
|
100942
101718
|
say("");
|
|
100943
101719
|
say(c.bold(" Client handover"));
|
|
101720
|
+
if (editUrl) say(` Edit page: ${editUrl} (tap text/photos to change them \u2014 works on a phone)`);
|
|
100944
101721
|
say(` Admin panel: ${adminUrl}`);
|
|
100945
101722
|
say(` Password: ${adminPassword}`);
|
|
100946
101723
|
say(c.dim(" Send these to the client \u2014 they manage bookings and content there."));
|