metatroncubeswdev e56182e0e3 chore: initial Datewise app (rebranded from SlotSure prototype)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:06:49 -04:00

422 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Datewise storefront widget. Vanilla JS, no dependencies, safe to load twice.
* Talks to the app through the shop's app proxy (/apps/datewise/*), so it works on any theme.
*
* Lifecycle
* cart change ─▶ refresh(): read /cart.js ─▶ POST availability ─▶ revalidate selection ─▶ render
* slot click ─▶ POST hold (capacity reserved for N minutes) ─▶ write cart attributes + fee line
* checkout ─▶ (optional) block click until a time is chosen; the checkout function enforces it server-side
*/
(function () {
"use strict";
if (window.__datewiseLoaded) return;
window.__datewiseLoaded = true;
// ---------- config ----------
var cfgEl = document.querySelector("[data-datewise-config]");
var CFG = {};
try { CFG = cfgEl ? JSON.parse(cfgEl.textContent) : {}; } catch (e) { CFG = {}; }
var T = Object.assign({
choose: "Pick a highlighted date", needTime: "Please choose a delivery or pickup time before checking out.",
held: "Held for you for", expired: "Your hold expired. Tap the time again to check it's still free.", left: "left", full: "Full",
postal: "Postal / ZIP code", check: "Check", waitlist: "Join the waitlist", joined: "You're on the waitlist. We'll message you if a place opens.",
dispatch: "Ships this day (not an arrival date)", change: "Change", prep: "Earliest times include preparation for", free: "Free", fee: "fee",
nextAvailable: "Next available", offer: "A place opened up and we're holding it for you", error: "Something went wrong. Please try again."
}, CFG.text || {});
var ROOT = ((window.Shopify && Shopify.routes && Shopify.routes.root) || "/").replace(/\/?$/, "/");
var API = ROOT + "apps/datewise";
var LOCALE = CFG.locale || document.documentElement.lang || "en";
var CURRENCY = CFG.currency || (window.Shopify && Shopify.currency && Shopify.currency.active) || "CAD";
var ATTR = "_datewise";
// ---------- storage (best effort) ----------
function lsGet(k) { try { return localStorage.getItem("datewise_" + k); } catch (e) { return null; } }
function lsSet(k, v) { try { v == null ? localStorage.removeItem("datewise_" + k) : localStorage.setItem("datewise_" + k, v); } catch (e) {} }
// ---------- state ----------
var S = {
data: null, cart: null, lines: [], loading: false, message: null, messageTone: "info",
method: lsGet("method"), locationId: lsGet("location"), postal: lsGet("postal") || "",
date: null, sel: null, token: lsGet("token"), expiresAt: null, monthOffset: 0, waitlistDone: false, mutating: 0
};
// ---------- helpers ----------
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]; }); }
function money(c) { try { return new Intl.NumberFormat(LOCALE, { style: "currency", currency: CURRENCY }).format(c / 100); } catch (e) { return "$" + (c / 100).toFixed(2); } }
function dateLabel(d, long) { return new Intl.DateTimeFormat(LOCALE, { weekday: long ? "long" : "short", month: long ? "long" : "short", day: "numeric", timeZone: "UTC" }).format(new Date(d + "T12:00:00Z")); }
function timeLabel(hhmm) { var p = hhmm.split(":"); return new Intl.DateTimeFormat(LOCALE, { hour: "numeric", minute: p[1] === "00" ? undefined : "2-digit", timeZone: "UTC" }).format(new Date(Date.UTC(2000, 0, 1, +p[0], +p[1]))); }
function slotText(s) { return timeLabel(s.start) + " – " + timeLabel(s.end); }
function addMonths(ym, n) { var y = +ym.slice(0, 4), m = +ym.slice(5, 7) - 1 + n; y += Math.floor(m / 12); m = ((m % 12) + 12) % 12; return y + "-" + String(m + 1).padStart(2, "0"); }
function daysIn(ym) { return new Date(Date.UTC(+ym.slice(0, 4), +ym.slice(5, 7), 0)).getUTCDate(); }
function weekday(d) { return new Date(d + "T12:00:00Z").getUTCDay(); }
function api(path, body, method) {
var url = API + path;
var init = { method: method || "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, credentials: "same-origin" };
if (body && init.method !== "GET") init.body = JSON.stringify(body);
return realFetch(url, init).then(function (r) { return r.json().catch(function () { return { ok: false, message: T.error }; }); });
}
function cartReq(path, body) {
S.mutating++;
return realFetch(ROOT + path, { method: body ? "POST" : "GET", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: body ? JSON.stringify(body) : undefined, credentials: "same-origin" })
.then(function (r) { return r.json(); })
.finally(function () { setTimeout(function () { S.mutating--; }, 50); });
}
// ---------- cart ----------
function readCart() {
return cartReq("cart.js").then(function (cart) {
S.cart = cart;
S.lines = cart.items.filter(function (i) { return !(i.properties && i.properties._datewise_fee); })
.map(function (i) { return { variantId: i.variant_id, quantity: i.quantity, price: i.final_price, title: i.product_title }; });
var raw = cart.attributes && cart.attributes[ATTR];
if (raw) {
try {
var a = JSON.parse(raw);
S.sel = { method: a.m, locationId: a.l, date: a.d, slot: a.s, fee: a.f };
S.token = a.t; lsSet("token", a.t);
S.method = S.method || a.m; S.locationId = S.locationId || a.l; S.date = S.date || a.d;
} catch (e) { S.sel = null; }
} else S.sel = null;
return cart;
});
}
function feeLines() { return (S.cart ? S.cart.items : []).filter(function (i) { return i.properties && i.properties._datewise_fee; }); }
function syncFee(variantId) {
var current = feeLines();
var chain = Promise.resolve();
var keep = null;
current.forEach(function (line) {
if (!keep && variantId && String(line.variant_id) === String(variantId) && line.quantity === 1) keep = line;
else chain = chain.then(function () { return cartReq("cart/change.js", { id: line.key, quantity: 0 }); });
});
if (variantId && !keep) chain = chain.then(function () { return cartReq("cart/add.js", { items: [{ id: Number(variantId), quantity: 1, properties: { _datewise_fee: "1" } }] }); });
var changed = current.length !== (keep ? 1 : 0) || (variantId && !keep);
return chain.then(function () { return changed; });
}
function writeAttributes(attrs) {
var prev = [];
try { prev = JSON.parse(lsGet("keys") || "[]"); } catch (e) {}
var out = {};
prev.forEach(function (k) { out[k] = ""; });
Object.keys(attrs).forEach(function (k) { out[k] = attrs[k]; });
lsSet("keys", JSON.stringify(Object.keys(attrs)));
return cartReq("cart/update.js", { attributes: out });
}
function notifyTheme(feeChanged) {
var detail = { source: "datewise" };
["cart:refresh", "cart:updated", "cart:change", "theme:cart:change"].forEach(function (n) { document.dispatchEvent(new CustomEvent(n, { detail: detail, bubbles: true })); });
if (typeof window.publish === "function" && window.PUB_SUB_EVENTS) {
readCart().then(function (cart) { window.publish(window.PUB_SUB_EVENTS.cartUpdate, { source: "datewise", cartData: cart }); });
} else if (feeChanged && /\/cart\/?$/.test(location.pathname)) {
setTimeout(function () { location.reload(); }, 300); // totals must show the fee line
}
}
function clearSelection(reason) {
var token = S.token;
S.sel = null; S.expiresAt = null;
if (reason) setMessage(reason, "critical");
if (token) api("/release", { token: token }).catch(function () {});
return writeAttributes({ [ATTR]: "" }).then(function () { return syncFee(null); }).then(function (changed) { lsSet("keys", "[]"); if (changed) notifyTheme(true); });
}
// ---------- data ----------
var timer = null;
function refresh(soon) {
clearTimeout(timer);
timer = setTimeout(load, soon ? 0 : 300);
}
function load() {
if (!roots().length && !CFG.checkoutGuard) return Promise.resolve();
S.loading = true; renderAll();
return readCart().then(function () {
if (!S.lines.length) { S.data = null; S.loading = false; if (S.sel) clearSelection(null); renderAll(); return; }
return api("/availability", { lines: S.lines, method: S.method || undefined, locationId: S.locationId || undefined, postal: S.postal || undefined, token: S.token || undefined, locale: LOCALE })
.then(function (data) {
S.loading = false;
if (!data || data.ok === false) { setMessage((data && data.message) || T.error, "critical"); renderAll(); return; }
S.data = data;
S.method = data.method; S.locationId = data.locationId;
revalidate();
renderAll();
});
}).catch(function () { S.loading = false; setMessage(T.error, "critical"); renderAll(); });
}
function findSlot(date, slot) {
var d = S.data && S.data.dates.filter(function (x) { return x.date === date; })[0];
return { d: d, s: d && d.slots.filter(function (x) { return x.id === slot; })[0] };
}
/** The cart, method or location changed: drop a selection the rules no longer allow, with a reason. */
function revalidate() {
if (!S.sel) return;
var d = S.data;
if (!d || d.method !== S.sel.method || d.locationId !== S.sel.locationId) return clearSelection(null);
var f = findSlot(S.sel.date, S.sel.slot);
if (!f.s || !f.s.available) {
var why = (f.s && f.s.reason) || (f.d && f.d.reasons[0]) || "";
clearSelection(why ? why + ". " + T.choose + "." : T.choose);
}
}
function hold(date, slot) {
if (!S.data) return;
setMessage(null);
S.loading = true; renderAll();
return api("/hold", { lines: S.lines, method: S.data.method, locationId: S.data.locationId, date: date, slot: slot, token: S.token || undefined, postal: S.postal || undefined, locale: LOCALE })
.then(function (res) {
if (!res.ok) { S.loading = false; setMessage(res.message || T.error, "critical"); refresh(true); return; }
S.token = res.token; lsSet("token", res.token);
S.expiresAt = Date.parse(res.expiresAt);
S.sel = { method: S.data.method, locationId: S.data.locationId, date: date, slot: slot, fee: res.fee ? res.fee.fee : 0 };
return writeAttributes(res.attributes)
.then(function () { return syncFee(res.feeVariantId); })
.then(function (changed) { S.loading = false; renderAll(); notifyTheme(changed); });
})
.catch(function () { S.loading = false; setMessage(T.error, "critical"); renderAll(); });
}
function setMessage(text, tone) { S.message = text; S.messageTone = tone || "info"; }
// ---------- rendering ----------
function roots() { return Array.prototype.slice.call(document.querySelectorAll("[data-datewise-root]")); }
function renderAll() { roots().forEach(render); }
function render(root) {
if (!root.classList.contains("datewise")) root.classList.add("datewise"); // custom mounts get styles too
var d = S.data;
var heading = root.getAttribute("data-heading") || (d && d.text && d.text.heading) || "";
if (d && d.text && d.text.accent) root.style.setProperty("--ss-accent", d.text.accent);
var h = "";
if (heading) h += '<h2 class="ss-h">' + esc(heading) + "</h2>";
h += '<div class="ss-live" role="status" aria-live="polite">' + (S.message ? '<p class="ss-msg ss-' + S.messageTone + '">' + esc(S.message) + "</p>" : "") + "</div>";
if (!d) { root.innerHTML = h + (S.loading ? '<div class="ss-skeleton" aria-hidden="true"></div>' : ""); root.setAttribute("aria-busy", S.loading); return; }
// methods
h += '<div class="ss-methods" role="group" aria-label="Delivery method">';
d.methods.forEach(function (m) {
var on = m.method === d.method;
h += '<button type="button" class="ss-method" data-ss="method" data-v="' + m.method + '" aria-pressed="' + on + '"' + (m.available || on ? "" : ' aria-disabled="true"') + ">" +
"<b>" + esc(d.text.labels[m.method]) + "</b><small>" + (m.needsAddress ? esc(T.postal) : m.available ? (m.firstDate ? esc(T.nextAvailable + ": " + dateLabel(m.firstDate)) : "") : esc(m.reason || "")) + "</small></button>";
});
h += "</div>";
if (!d.method) { root.innerHTML = h; return; }
if (d.method === "delivery") {
h += '<form class="ss-postal" data-ss="postal"><label for="ss-postal-' + rid(root) + '">' + esc(T.postal) + '</label><div class="ss-row"><input id="ss-postal-' + rid(root) + '" name="postal" autocomplete="postal-code" value="' + esc(S.postal) + '"><button type="submit">' + esc(T.check) + "</button></div></form>";
}
if (d.split) h += '<p class="ss-msg ss-critical">' + esc(d.split) + "</p>";
if (d.method !== "shipping" && d.locations.length > 1) {
h += '<div class="ss-locs" role="group" aria-label="Location">';
d.locations.forEach(function (l) {
var on = l.id === d.locationId;
var right = l.fee ? (l.fee.waived ? T.free : money(l.fee.fee)) : l.distanceKm != null ? l.distanceKm + " km" : "";
h += '<button type="button" class="ss-loc" data-ss="loc" data-v="' + esc(l.id) + '" aria-pressed="' + on + '"' + (l.eligible ? "" : ' aria-disabled="true"') + "><span><b>" + esc(l.name) + "</b><small>" +
esc(l.eligible ? (l.first ? T.nextAvailable + ": " + dateLabel(l.first.date) : "") : l.reason || "") + "</small></span><span>" + esc(right) + "</span></button>";
});
h += "</div>";
} else if (d.locationId) {
var only = d.locations.filter(function (l) { return l.id === d.locationId; })[0];
if (only && d.method !== "shipping") h += '<p class="ss-note">' + esc(only.name) + (only.fee ? " · " + (only.fee.waived ? T.free : money(only.fee.fee) + " " + T.fee) : "") + "</p>";
}
if (!d.locationId) {
if (d.needsAddress) { root.innerHTML = h; return; }
var why = d.locations[0] && d.locations[0].reason;
root.innerHTML = h + (why ? '<p class="ss-msg ss-critical">' + esc(why) + "</p>" : "");
return;
}
if (d.prepNote && d.prepNote.minutes >= 60) h += '<p class="ss-note">' + esc(T.prep + " " + d.prepNote.item) + " (" + Math.round(d.prepNote.minutes / 6) / 10 + " h)</p>";
var peak = d.dates.filter(function (x) { return x.peak && x.peak.message; })[0];
if (peak) h += '<p class="ss-msg ss-peak">' + esc(peak.peak.message) + "</p>";
h += calendar(d);
h += slots(d, root);
root.innerHTML = h;
root.setAttribute("aria-busy", S.loading);
tickTimer();
}
var ridSeq = 0;
function rid(root) { if (!root.__ssid) root.__ssid = ++ridSeq; return root.__ssid; }
function calendar(d) {
var map = {};
d.dates.forEach(function (x) { map[x.date] = x; });
var firstAvail = (d.dates.filter(function (x) { return x.available; })[0] || d.dates[0] || {}).date;
if (!firstAvail) return "";
var ym = addMonths(firstAvail.slice(0, 7), S.monthOffset);
var start = d.text.weekStartsOn || 0;
var first = ym + "-01";
var lead = (weekday(first) - start + 7) % 7;
var h = '<div class="ss-cal"><div class="ss-calhead"><button type="button" data-ss="month" data-v="-1" aria-label="Previous month">‹</button><b>' +
esc(new Intl.DateTimeFormat(LOCALE, { month: "long", year: "numeric", timeZone: "UTC" }).format(new Date(first + "T12:00:00Z"))) +
'</b><button type="button" data-ss="month" data-v="1" aria-label="Next month">›</button></div><div class="ss-grid" role="grid">';
for (var w = 0; w < 7; w++) {
var wd = (start + w) % 7;
h += '<span class="ss-dow" aria-hidden="true">' + esc(new Intl.DateTimeFormat(LOCALE, { weekday: "narrow", timeZone: "UTC" }).format(new Date(Date.UTC(2023, 0, 1 + wd)))) + "</span>";
}
for (var i = 0; i < lead; i++) h += "<span></span>";
for (var day = 1; day <= daysIn(ym); day++) {
var date = ym + "-" + String(day).padStart(2, "0");
var x = map[date];
var cls = "ss-day", label = dateLabel(date, true), extra = "";
if (!x) cls += " ss-out";
else if (x.available) { cls += " ss-avail"; if (d.text.showRemaining && x.remaining != null && x.remaining <= 5) extra = '<i class="ss-left">' + x.remaining + "</i>"; }
else if (x.waitlist || /full|booked/i.test(x.reasons[0] || "")) { cls += " ss-full"; label += ", " + T.full; }
else { cls += " ss-closed"; label += ", " + (x.reasons[0] || ""); }
if (S.date === date) cls += " ss-sel";
if (x && x.peak) { cls += " ss-peakday"; label += ", " + x.peak.name; }
h += '<button type="button" class="' + cls + '" data-ss="date" data-v="' + date + '" aria-label="' + esc(label) + '"' + (x && (x.available || x.waitlist || x.reasons.length) ? "" : ' aria-disabled="true"') + ' tabindex="' + (S.date === date || (!S.date && date === firstAvail) ? "0" : "-1") + '">' + day + extra + "</button>";
}
return h + "</div></div>";
}
function slots(d, root) {
if (!S.date) return '<p class="ss-note">' + esc(T.choose) + "</p>";
var x = d.dates.filter(function (y) { return y.date === S.date; })[0];
if (!x) return "";
var h = '<div class="ss-day-detail"><h3>' + esc(dateLabel(x.date, true)) + (x.peak ? ' <span class="ss-badge">' + esc(x.peak.name) + "</span>" : "") + "</h3>";
if (d.dateOnly) h += '<p class="ss-note">' + esc(T.dispatch) + "</p>";
if (!x.available) {
h += '<p class="ss-msg ' + (x.waitlist ? "ss-peak" : "ss-critical") + '">' + esc(x.reasons.join(". ")) + "</p>";
if (x.waitlist && !S.waitlistDone)
h += '<form class="ss-row" data-ss="waitlist"><input type="email" name="email" required placeholder="Email" aria-label="Email"><button type="submit">' + esc(T.waitlist) + "</button></form>";
if (S.waitlistDone) h += '<p class="ss-msg ss-success">' + esc(T.joined) + "</p>";
return h + "</div>";
}
h += '<div class="ss-slots" role="group" aria-label="Times">';
x.slots.forEach(function (s) {
var on = S.sel && S.sel.date === x.date && S.sel.slot === s.id;
var sub = s.available ? (d.text.showRemaining && s.remaining != null ? s.remaining + " " + T.left : "") + (s.surcharge ? (s.remaining != null && d.text.showRemaining ? " · " : "") + "+" + money(s.surcharge) : "") : s.reason || "";
h += '<button type="button" class="ss-slot" data-ss="slot" data-v="' + s.id + '" aria-pressed="' + !!on + '"' + (s.available || on ? "" : ' aria-disabled="true"') + "><b>" + esc(d.dateOnly ? dateLabel(x.date) : slotText(s)) + "</b><small>" + esc(sub) + "</small></button>";
});
h += "</div>";
if (S.sel && S.sel.date === x.date) {
var live = S.expiresAt && S.expiresAt > Date.now();
h += '<p class="ss-hold">' + (live ? esc(T.held) + ' <b data-ss-timer></b>' : S.expiresAt ? esc(T.expired) : "✓") + "</p>";
var loc = d.locations.filter(function (l) { return l.id === d.locationId; })[0];
if (loc && loc.pickupInstructions) h += '<p class="ss-note">' + esc(loc.pickupInstructions) + "</p>";
}
return h + "</div>";
}
function tickTimer() {
var left = S.expiresAt ? S.expiresAt - Date.now() : 0;
var els = document.querySelectorAll("[data-ss-timer]");
if (left <= 0 && els.length) return renderAll();
var s = Math.max(0, Math.round(left / 1000));
for (var i = 0; i < els.length; i++) els[i].textContent = Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0");
}
setInterval(tickTimer, 1000);
// ---------- events ----------
document.addEventListener("click", function (e) {
var t = e.target.closest && e.target.closest("[data-datewise-root] [data-ss]");
if (!t || t.tagName === "FORM") return;
if (t.getAttribute("aria-disabled") === "true" && t.getAttribute("data-ss") !== "date") return;
var a = t.getAttribute("data-ss"), v = t.getAttribute("data-v");
if (a === "method" && v !== S.method) { S.method = v; lsSet("method", v); S.date = null; S.monthOffset = 0; refresh(true); }
else if (a === "loc" && v !== S.locationId) { S.locationId = v; lsSet("location", v); S.date = null; refresh(true); }
else if (a === "month") { S.monthOffset += +v; renderAll(); }
else if (a === "date") { S.date = v; S.waitlistDone = false; setMessage(null); renderAll(); focusFirst(".ss-slot:not([aria-disabled])"); }
else if (a === "slot") hold(S.date, v);
});
document.addEventListener("submit", function (e) {
var f = e.target;
if (!f.closest || !f.closest("[data-datewise-root]")) return;
var a = f.getAttribute("data-ss");
if (!a) return;
e.preventDefault();
if (a === "postal") { S.postal = f.postal.value.trim(); lsSet("postal", S.postal); S.date = null; refresh(true); }
if (a === "waitlist" && S.data) {
api("/waitlist", { method: S.data.method, locationId: S.data.locationId, date: S.date, slot: "*", email: f.email.value.trim() }).then(function (r) {
if (r.ok) S.waitlistDone = true; else setMessage(r.message || T.error, "critical");
renderAll();
});
}
});
// Arrow-key navigation in the calendar grid.
document.addEventListener("keydown", function (e) {
var t = e.target;
if (!t.classList || !t.classList.contains("ss-day")) return;
var step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 }[e.key];
if (!step) return;
var days = Array.prototype.slice.call(t.parentNode.querySelectorAll(".ss-day"));
var next = days[days.indexOf(t) + step];
if (next) { e.preventDefault(); t.tabIndex = -1; next.tabIndex = 0; next.focus(); }
});
function focusFirst(sel) { setTimeout(function () { var el = document.querySelector("[data-datewise-root] " + sel); if (el) el.focus(); }, 0); }
// Checkout guard (convenience; the checkout function is the real enforcement).
function needsTime() { return S.data && S.data.requireScheduling && S.lines.length > 0 && !S.sel; }
function guard(e) {
if (!CFG.checkoutGuard || !needsTime()) return;
var t = e.target.closest && e.target.closest('[name="checkout"], a[href$="/checkout"], form[action$="/checkout"] [type="submit"]');
if (!t) return;
e.preventDefault(); e.stopPropagation();
setMessage(T.needTime, "critical");
renderAll();
var r = roots()[0];
if (r) { r.scrollIntoView({ behavior: "smooth", block: "center" }); }
}
document.addEventListener("click", guard, true);
// React to cart changes made by the theme (add, quantity, remove) — revalidate the selection.
var realFetch = window.fetch.bind(window);
window.fetch = function (input, init) {
var url = typeof input === "string" ? input : input && input.url;
var p = realFetch(input, init);
if (url && /\/cart\/(add|change|update|clear)(\.js)?/.test(url) && !S.mutating) p.then(function () { refresh(); });
return p;
};
["cart:updated", "cart:change", "cart-drawer:open", "ajaxCart:updated"].forEach(function (n) {
document.addEventListener(n, function (e) { if (!(e.detail && e.detail.source === "datewise")) refresh(); });
});
if (typeof window.subscribe === "function" && window.PUB_SUB_EVENTS)
window.subscribe(window.PUB_SUB_EVENTS.cartUpdate, function (ev) { if (!ev || ev.source !== "datewise") refresh(); });
// ---------- cart drawer mounting ----------
var DRAWER_SELECTORS = [CFG.drawerSelector, "cart-drawer .drawer__footer", "#CartDrawer .drawer__footer", "#CartDrawer-Footer", ".cart-drawer__footer", "#cart-drawer .cart__footer", ".js-cart-drawer .cart-drawer__footer", "[data-cart-drawer] footer"].filter(Boolean);
function mountDrawer() {
for (var i = 0; i < DRAWER_SELECTORS.length; i++) {
var foot = document.querySelector(DRAWER_SELECTORS[i]);
if (foot && !foot.querySelector("[data-datewise-root]")) {
var el = document.createElement("div");
el.className = "datewise datewise--drawer";
el.setAttribute("data-datewise-root", "");
el.setAttribute("data-drawer", "");
foot.insertBefore(el, foot.firstChild);
if (S.data) render(el); else refresh(true);
return;
}
}
}
if (cfgEl) {
mountDrawer();
new MutationObserver(function () { mountDrawer(); }).observe(document.body, { childList: true, subtree: true });
}
// ---------- waitlist offer links (?datewise_offer=…) ----------
function adoptOffer() {
var m = location.search.match(/[?&]datewise_offer=([^&]+)/);
if (!m) return Promise.resolve();
return api("/offer?token=" + encodeURIComponent(m[1]), null, "GET").then(function (o) {
if (!o.ok) { setMessage(o.message, "critical"); return; }
S.token = o.token; lsSet("token", o.token);
S.method = o.method; S.locationId = o.locationId; S.date = o.date;
setMessage(T.offer + ": " + dateLabel(o.date, true) + ".", "success");
return load().then(function () { if (S.data && S.data.locationId === o.locationId) return hold(o.date, o.slot); });
});
}
adoptOffer().then(function () { if (!S.data) refresh(true); });
})();