/* Datewise storefront widget SOURCE. Edit this file, then `npm run build:widget` (minified copy → extensions/datewise-widget/assets/datewise.js). */
/* 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, heldAt: null, monthOffset: 0, waitlistDone: false, mutating: 0
};
// ---------- helpers ----------
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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(); }
var ICON = {
pickup: '
' + esc(d.split) + "
"; if (d.method !== "shipping" && d.locations.length > 1) { h += '' + esc(only.name) + (only.fee ? " · " + (only.fee.waived ? T.free : money(only.fee.fee) + " " + T.fee) : "") + "
"; } if (!d.locationId) { if (d.needsAddress) { root.innerHTML = h; return; } var why = d.locations[0] && d.locations[0].reason; root.innerHTML = h + (why ? '' + esc(why) + "
" : ""); return; } if (d.prepNote && d.prepNote.minutes >= 60) h += '' + esc(T.prep + " " + d.prepNote.item) + " (" + Math.round(d.prepNote.minutes / 6) / 10 + " h)
"; var peak = d.dates.filter(function (x) { return x.peak && x.peak.message; })[0]; if (peak) h += '' + esc(peak.peak.message) + "
"; 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.__dwid) root.__dwid = ++ridSeq; return root.__dwid; } 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 = '' + esc(T.choose) + "
"; var x = d.dates.filter(function (y) { return y.date === S.date; })[0]; if (!x) return ""; var h = '' + esc(T.dispatch) + "
"; if (!x.available) { h += '' + esc(x.reasons.join(". ")) + "
"; if (x.waitlist && !S.waitlistDone) h += '"; if (S.waitlistDone) h += '' + esc(T.joined) + "
"; return h + "' + esc(loc.pickupInstructions) + "
"; return h; } function tickTimer() { var left = S.expiresAt ? S.expiresAt - Date.now() : 0; var els = document.querySelectorAll("[data-dw-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"); var total = S.expiresAt && S.heldAt ? S.expiresAt - S.heldAt : 0; var rings = document.querySelectorAll("[data-dw-ring]"); for (var j = 0; j < rings.length; j++) rings[j].style.strokeDashoffset = total > 0 ? String(100 - Math.max(0, Math.min(100, (left / total) * 100))) : "0"; } setInterval(tickTimer, 1000); // ---------- events ---------- document.addEventListener("click", function (e) { var t = e.target.closest && e.target.closest("[data-datewise-root] [data-dw]"); if (!t || t.tagName === "FORM") return; if (t.getAttribute("aria-disabled") === "true" && t.getAttribute("data-dw") !== "date") return; var a = t.getAttribute("data-dw"), 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(".dw-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-dw"); 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("dw-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(".dw-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); }); })();