feat(widget): restyle storefront scheduler; booking stub with hold countdown
- segmented method control with line icons - round calendar days with availability dots and scarcity badges - pill time chips; confirmation stub shows method, place, date, time, fee - colours derive from theme text colour + one merchant accent (light/dark safe) - rename leftover ss- internals to dw- Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e56182e0e3
commit
123ca71d42
@ -1,65 +1,170 @@
|
||||
/* Datewise widget — inherits the theme's fonts and text colour; one accent variable. */
|
||||
/* Datewise widget.
|
||||
* Inherits the theme's font and text colour; the merchant sets one accent (--dw-accent).
|
||||
* Every other colour is mixed from currentColor or the accent, so it works on light and dark themes.
|
||||
* Radius hierarchy: panel 14px · controls 10px · days round · time chips pill. */
|
||||
.datewise {
|
||||
--ss-accent: #0d6b5d;
|
||||
--ss-line: rgba(127, 127, 127, 0.32);
|
||||
--ss-soft: color-mix(in srgb, var(--ss-accent) 12%, transparent);
|
||||
--ss-warn: #9a5b00;
|
||||
--ss-warn-soft: rgba(240, 180, 74, 0.18);
|
||||
--ss-bad: #b42318;
|
||||
--ss-good: #1e7a3a;
|
||||
--dw-accent: #0d6b5d;
|
||||
--dw-on-accent: #fff;
|
||||
--dw-line: color-mix(in srgb, currentColor 16%, transparent);
|
||||
--dw-line-strong: color-mix(in srgb, currentColor 34%, transparent);
|
||||
--dw-wash: color-mix(in srgb, currentColor 4%, transparent);
|
||||
--dw-tint: color-mix(in srgb, var(--dw-accent) 10%, transparent);
|
||||
--dw-tint-strong: color-mix(in srgb, var(--dw-accent) 18%, transparent);
|
||||
--dw-warn: #a35a00;
|
||||
--dw-warn-tint: color-mix(in srgb, #e8a33d 18%, transparent);
|
||||
--dw-bad: #b42318;
|
||||
--dw-good: #1e7a3a;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
gap: 14px;
|
||||
margin: 20px 0;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--dw-line);
|
||||
border-radius: 14px;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.4;
|
||||
line-height: 1.45;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.datewise--drawer { margin: 0 0 12px; font-size: 0.9em; }
|
||||
.datewise--drawer { margin: 0 0 12px; padding: 14px; font-size: 0.9em; gap: 12px; }
|
||||
.datewise * { box-sizing: border-box; }
|
||||
.datewise button { font: inherit; color: inherit; cursor: pointer; }
|
||||
.datewise [aria-disabled="true"] { cursor: not-allowed; opacity: 0.5; }
|
||||
.datewise :focus-visible { outline: 2px solid var(--ss-accent); outline-offset: 2px; }
|
||||
.ss-h { margin: 0; font-size: 1.15em; }
|
||||
.ss-live:empty { display: none; }
|
||||
.ss-msg { margin: 0; padding: 8px 12px; border-radius: 8px; background: var(--ss-soft); }
|
||||
.ss-critical { background: rgba(180, 35, 24, 0.1); color: var(--ss-bad); }
|
||||
.ss-success { background: rgba(30, 122, 58, 0.1); color: var(--ss-good); }
|
||||
.ss-peak { background: var(--ss-warn-soft); color: var(--ss-warn); }
|
||||
.ss-note { margin: 0; opacity: 0.8; font-size: 0.9em; }
|
||||
.ss-methods { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 8px; }
|
||||
.ss-method, .ss-loc, .ss-slot {
|
||||
display: grid; gap: 2px; text-align: left; padding: 10px; border-radius: 10px;
|
||||
border: 1.5px solid var(--ss-line); background: transparent;
|
||||
.datewise button { font: inherit; color: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
||||
.datewise [aria-disabled="true"] { cursor: not-allowed; opacity: 0.45; }
|
||||
.datewise :focus-visible { outline: 2px solid var(--dw-accent); outline-offset: 2px; }
|
||||
.dw-h { margin: 0; font-size: 1.12em; font-weight: 600; letter-spacing: -0.005em; }
|
||||
|
||||
/* messages */
|
||||
.dw-live:empty { display: none; }
|
||||
.dw-msg { margin: 0; padding: 10px 12px; border-radius: 10px; background: var(--dw-tint); }
|
||||
.dw-critical { background: color-mix(in srgb, var(--dw-bad) 9%, transparent); color: var(--dw-bad); }
|
||||
.dw-success { background: color-mix(in srgb, var(--dw-good) 10%, transparent); color: var(--dw-good); }
|
||||
.dw-peak { background: var(--dw-warn-tint); color: var(--dw-warn); }
|
||||
.dw-note { margin: 0; font-size: 0.88em; opacity: 0.75; }
|
||||
|
||||
/* method switcher: one segmented control */
|
||||
.dw-methods {
|
||||
display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 4px;
|
||||
padding: 4px; border-radius: 12px; background: var(--dw-wash); border: 1px solid var(--dw-line);
|
||||
}
|
||||
.dw-method {
|
||||
display: grid; justify-items: center; gap: 2px; padding: 10px 6px 9px; text-align: center;
|
||||
border: 0; border-radius: 9px; background: transparent; transition: background-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.dw-method b { font-weight: 600; font-size: 0.95em; }
|
||||
.dw-method small { font-size: 0.76em; opacity: 0.7; line-height: 1.25; }
|
||||
.dw-method[aria-pressed="true"] { background: var(--dw-accent); color: var(--dw-on-accent); box-shadow: 0 1px 2px color-mix(in srgb, var(--dw-accent) 40%, transparent); }
|
||||
.dw-method[aria-pressed="true"] small { opacity: 0.85; }
|
||||
.dw-method:not([aria-pressed="true"]):not([aria-disabled="true"]):hover { background: var(--dw-tint); }
|
||||
.dw-ico { width: 22px; height: 22px; flex: none; }
|
||||
@media (max-width: 380px) { .dw-method:not([aria-disabled="true"]) small { display: none; } }
|
||||
|
||||
/* locations */
|
||||
.dw-locs { display: grid; gap: 6px; }
|
||||
.dw-loc {
|
||||
display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px;
|
||||
padding: 11px 12px; text-align: left; border-radius: 10px; border: 1px solid var(--dw-line); background: transparent;
|
||||
}
|
||||
.dw-loc span:first-child { display: grid; gap: 1px; }
|
||||
.dw-loc small { font-size: 0.82em; opacity: 0.7; }
|
||||
.dw-loc span:last-child { font-variant-numeric: tabular-nums; font-size: 0.9em; }
|
||||
.dw-loc[aria-pressed="true"] { border-color: var(--dw-accent); box-shadow: inset 0 0 0 1px var(--dw-accent); background: var(--dw-tint); }
|
||||
|
||||
/* postal check */
|
||||
.dw-postal { display: grid; gap: 6px; }
|
||||
.dw-postal label { font-size: 0.85em; opacity: 0.75; }
|
||||
.dw-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.dw-row input {
|
||||
flex: 1; min-width: 0; padding: 10px 12px; border: 1px solid var(--dw-line-strong); border-radius: 10px;
|
||||
font: inherit; background: transparent; color: inherit;
|
||||
}
|
||||
.dw-row input:focus { outline: none; border-color: var(--dw-accent); box-shadow: 0 0 0 3px var(--dw-tint-strong); }
|
||||
.dw-row button { padding: 10px 16px; border-radius: 10px; border: 0; background: var(--dw-accent); color: var(--dw-on-accent); font-weight: 600; }
|
||||
|
||||
/* calendar */
|
||||
.dw-cal { display: grid; gap: 8px; }
|
||||
.dw-calhead { display: flex; justify-content: space-between; align-items: center; }
|
||||
.dw-calhead b { font-weight: 600; }
|
||||
.dw-calhead button {
|
||||
width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid var(--dw-line);
|
||||
border-radius: 50%; background: transparent; font-size: 1.15em; line-height: 1;
|
||||
}
|
||||
.dw-calhead button:hover { border-color: var(--dw-line-strong); }
|
||||
.dw-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px 2px; }
|
||||
.dw-dow { text-align: center; font-size: 0.74em; opacity: 0.55; padding-bottom: 2px; }
|
||||
.dw-day {
|
||||
position: relative; justify-self: center; width: 100%; max-width: 44px; aspect-ratio: 1;
|
||||
border: 1px solid transparent; border-radius: 50%; background: transparent; padding: 0;
|
||||
font-variant-numeric: tabular-nums; transition: background-color 0.12s, border-color 0.12s;
|
||||
}
|
||||
.dw-avail { font-weight: 600; }
|
||||
.dw-avail:hover { border-color: var(--dw-accent); }
|
||||
.dw-avail::before { /* availability dot */
|
||||
content: ""; position: absolute; left: 50%; bottom: 14%; width: 4px; height: 4px; margin-left: -2px;
|
||||
border-radius: 50%; background: var(--dw-accent);
|
||||
}
|
||||
.dw-full { color: var(--dw-warn); background: var(--dw-warn-tint); }
|
||||
.dw-closed { opacity: 0.32; text-decoration: line-through; text-decoration-thickness: 1px; }
|
||||
.dw-out { opacity: 0.18; }
|
||||
.dw-sel, .dw-sel:hover { background: var(--dw-accent); color: var(--dw-on-accent); border-color: var(--dw-accent); }
|
||||
.dw-sel::before { background: var(--dw-on-accent); }
|
||||
.dw-peakday::after {
|
||||
content: ""; position: absolute; top: 8%; right: 8%; width: 7px; height: 7px; border-radius: 50%;
|
||||
background: #d98a0b;
|
||||
}
|
||||
.dw-left { /* "3 left" — only shown when scarce */
|
||||
position: absolute; left: 50%; bottom: -3px; transform: translateX(-50%); padding: 0 5px;
|
||||
font-size: 0.58em; font-style: normal; font-weight: 600; line-height: 1.5; border-radius: 99px;
|
||||
background: var(--dw-warn-tint); color: var(--dw-warn); white-space: nowrap;
|
||||
}
|
||||
.dw-avail:has(.dw-left)::before { display: none; }
|
||||
|
||||
/* day detail + time chips */
|
||||
.dw-day-detail { display: grid; gap: 10px; padding-top: 12px; border-top: 1px solid var(--dw-line); }
|
||||
.dw-day-detail h3 { margin: 0; font-size: 1em; font-weight: 600; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||
.dw-badge { font-size: 0.74em; font-weight: 600; padding: 2px 9px; border-radius: 99px; background: var(--dw-warn-tint); color: var(--dw-warn); }
|
||||
.dw-slots { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.dw-slot {
|
||||
display: inline-grid; justify-items: center; gap: 0; padding: 8px 16px; border-radius: 999px;
|
||||
border: 1px solid var(--dw-line-strong); background: transparent; text-align: center; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.dw-slot b { font-weight: 600; white-space: nowrap; }
|
||||
.dw-slot small { font-size: 0.74em; opacity: 0.7; }
|
||||
.dw-slot small:empty { display: none; }
|
||||
.dw-slot:not([aria-disabled="true"]):hover { border-color: var(--dw-accent); }
|
||||
.dw-slot[aria-pressed="true"] { background: var(--dw-accent); border-color: var(--dw-accent); color: var(--dw-on-accent); }
|
||||
|
||||
/* booking stub: the confirmation ticket */
|
||||
.dw-stub {
|
||||
position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 14px;
|
||||
padding: 14px 16px 14px 18px; border-radius: 12px; background: var(--dw-tint);
|
||||
border-left: 3px dashed var(--dw-accent);
|
||||
animation: dw-stub-in 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
.dw-stub__main { display: grid; gap: 1px; min-width: 0; }
|
||||
.dw-stub__method { display: flex; align-items: center; gap: 6px; font-size: 0.82em; opacity: 0.8; }
|
||||
.dw-stub__method .dw-ico { width: 16px; height: 16px; }
|
||||
.dw-stub__date { font-size: 1.12em; font-weight: 650; letter-spacing: -0.01em; }
|
||||
.dw-stub__time { font-variant-numeric: tabular-nums; }
|
||||
.dw-stub__fee { font-size: 0.82em; opacity: 0.75; font-variant-numeric: tabular-nums; }
|
||||
.dw-stub__side { display: grid; justify-items: center; gap: 4px; text-align: center; max-width: 9em; }
|
||||
.dw-stub__hold { font-size: 0.74em; opacity: 0.8; line-height: 1.3; }
|
||||
.dw-stub__hold b { display: block; font-size: 1.25em; font-variant-numeric: tabular-nums; opacity: 1; }
|
||||
.dw-stub--expired { background: var(--dw-warn-tint); border-left-color: var(--dw-warn); }
|
||||
.dw-ring { width: 34px; height: 34px; transform: rotate(-90deg); }
|
||||
.dw-ring circle { fill: none; stroke: var(--dw-tint-strong); stroke-width: 3; }
|
||||
.dw-ring .dw-ring__arc { stroke: var(--dw-accent); stroke-linecap: round; stroke-dasharray: 100; transition: stroke-dashoffset 1s linear; }
|
||||
.dw-check { width: 30px; height: 30px; padding: 6px; border-radius: 50%; background: var(--dw-accent); fill: none; stroke: var(--dw-on-accent); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
|
||||
@keyframes dw-stub-in { from { opacity: 0; transform: translateY(4px); } }
|
||||
|
||||
/* loading */
|
||||
.dw-skeleton {
|
||||
height: 132px; border-radius: 12px;
|
||||
background: linear-gradient(90deg, var(--dw-wash, rgba(127,127,127,.08)) 0%, rgba(127, 127, 127, 0.16) 50%, var(--dw-wash, rgba(127,127,127,.08)) 100%);
|
||||
background-size: 200% 100%; animation: dw-shimmer 1.3s infinite linear;
|
||||
}
|
||||
@keyframes dw-shimmer { to { background-position: -200% 0; } }
|
||||
.datewise[aria-busy="true"] .dw-slots, .datewise[aria-busy="true"] .dw-grid { opacity: 0.55; pointer-events: none; transition: opacity 0.2s; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dw-skeleton, .dw-stub { animation: none; }
|
||||
.dw-ring .dw-ring__arc, .dw-day, .dw-method { transition: none; }
|
||||
}
|
||||
.ss-method small, .ss-loc small, .ss-slot small { opacity: 0.75; font-size: 0.82em; }
|
||||
.ss-method[aria-pressed="true"], .ss-loc[aria-pressed="true"], .ss-slot[aria-pressed="true"] { border-color: var(--ss-accent); background: var(--ss-soft); }
|
||||
.ss-locs { display: grid; gap: 6px; }
|
||||
.ss-loc { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
|
||||
.ss-loc span:first-child { display: grid; }
|
||||
.ss-postal { display: grid; gap: 4px; }
|
||||
.ss-postal label { font-size: 0.85em; opacity: 0.8; }
|
||||
.ss-row { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.ss-row input { flex: 1; min-width: 0; padding: 8px 10px; border: 1px solid var(--ss-line); border-radius: 8px; font: inherit; background: transparent; color: inherit; }
|
||||
.ss-row button { padding: 8px 14px; border-radius: 8px; border: 1px solid var(--ss-accent); background: var(--ss-accent); color: #fff; }
|
||||
.ss-calhead { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||
.ss-calhead button { border: 0; background: transparent; font-size: 1.3em; padding: 2px 10px; }
|
||||
.ss-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 4px; }
|
||||
.ss-dow { text-align: center; font-size: 0.75em; opacity: 0.7; }
|
||||
.ss-day { position: relative; aspect-ratio: 1 / 0.9; border: 1px solid transparent; border-radius: 8px; background: transparent; padding: 0; font-variant-numeric: tabular-nums; }
|
||||
.ss-avail { background: var(--ss-soft); }
|
||||
.ss-avail:hover { border-color: var(--ss-accent); }
|
||||
.ss-full { background: var(--ss-warn-soft); color: var(--ss-warn); }
|
||||
.ss-closed { text-decoration: line-through; opacity: 0.45; }
|
||||
.ss-out { opacity: 0.25; }
|
||||
.ss-sel { background: var(--ss-accent); color: #fff; }
|
||||
.ss-peakday::after { content: ""; position: absolute; top: 4px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: #c77c02; }
|
||||
.ss-left { position: absolute; bottom: 1px; left: 0; right: 0; font-size: 0.6em; font-style: normal; opacity: 0.8; }
|
||||
.ss-day-detail { display: grid; gap: 8px; }
|
||||
.ss-day-detail h3 { margin: 4px 0 0; font-size: 1em; }
|
||||
.ss-badge { font-size: 0.75em; padding: 1px 8px; border-radius: 99px; background: var(--ss-warn-soft); color: var(--ss-warn); }
|
||||
.ss-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 8px; }
|
||||
.ss-hold { margin: 0; padding: 8px 12px; border-radius: 8px; background: var(--ss-soft); }
|
||||
.ss-skeleton { height: 120px; border-radius: 10px; background: linear-gradient(90deg, var(--ss-line), transparent, var(--ss-line)); background-size: 200% 100%; opacity: 0.35; animation: ss-shimmer 1.2s infinite linear; }
|
||||
@keyframes ss-shimmer { to { background-position: -200% 0; } }
|
||||
@media (prefers-reduced-motion: reduce) { .ss-skeleton { animation: none; } }
|
||||
.datewise[aria-busy="true"] .ss-slots, .datewise[aria-busy="true"] .ss-grid { opacity: 0.6; pointer-events: none; }
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
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
|
||||
date: null, sel: null, token: lsGet("token"), expiresAt: null, heldAt: null, monthOffset: 0, waitlistDone: false, mutating: 0
|
||||
};
|
||||
|
||||
// ---------- helpers ----------
|
||||
@ -47,6 +47,12 @@
|
||||
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: '<path d="M4 10v9h16v-9M3 10l1.5-5h15L21 10M3 10h18M3 10c0 1.4 1.3 2.5 3 2.5S9 11.4 9 10c0 1.4 1.3 2.5 3 2.5s3-1.1 3-2.5c0 1.4 1.3 2.5 3 2.5s3-1.1 3-2.5M10 19v-4h4v4"/>',
|
||||
delivery: '<path d="M2 6h11v10H2zM13 9h4.5l3.5 3.5V16h-8M6.5 18.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM17 18.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/>',
|
||||
shipping: '<path d="M3.5 7.5 12 3l8.5 4.5v9L12 21l-8.5-4.5zM3.5 7.5 12 12l8.5-4.5M12 12v9M7.8 5.3l8.5 4.5"/>'
|
||||
};
|
||||
function icon(m) { return '<svg class="dw-ico" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" stroke-linecap="round">' + (ICON[m] || "") + "</svg>"; }
|
||||
function weekday(d) { return new Date(d + "T12:00:00Z").getUTCDay(); }
|
||||
|
||||
function api(path, body, method) {
|
||||
@ -173,6 +179,7 @@
|
||||
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.heldAt = Date.now();
|
||||
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); })
|
||||
@ -191,49 +198,49 @@
|
||||
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);
|
||||
if (d && d.text && d.text.accent) root.style.setProperty("--dw-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; }
|
||||
if (heading) h += '<h2 class="dw-h">' + esc(heading) + "</h2>";
|
||||
h += '<div class="dw-live" role="status" aria-live="polite">' + (S.message ? '<p class="dw-msg dw-' + S.messageTone + '">' + esc(S.message) + "</p>" : "") + "</div>";
|
||||
if (!d) { root.innerHTML = h + (S.loading ? '<div class="dw-skeleton" aria-hidden="true"></div>' : ""); root.setAttribute("aria-busy", S.loading); return; }
|
||||
|
||||
// methods
|
||||
h += '<div class="ss-methods" role="group" aria-label="Delivery method">';
|
||||
h += '<div class="dw-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"') + ">" +
|
||||
h += '<button type="button" class="dw-method" data-dw="method" data-v="' + m.method + '" aria-pressed="' + on + '"' + (m.available || on ? "" : ' aria-disabled="true"') + ">" + icon(m.method) +
|
||||
"<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>";
|
||||
h += '<form class="dw-postal" data-dw="postal"><label for="dw-postal-' + rid(root) + '">' + esc(T.postal) + '</label><div class="dw-row"><input id="dw-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.split) h += '<p class="dw-msg dw-critical">' + esc(d.split) + "</p>";
|
||||
|
||||
if (d.method !== "shipping" && d.locations.length > 1) {
|
||||
h += '<div class="ss-locs" role="group" aria-label="Location">';
|
||||
h += '<div class="dw-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>" +
|
||||
h += '<button type="button" class="dw-loc" data-dw="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 (only && d.method !== "shipping") h += '<p class="dw-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>" : "");
|
||||
root.innerHTML = h + (why ? '<p class="dw-msg dw-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>";
|
||||
if (d.prepNote && d.prepNote.minutes >= 60) h += '<p class="dw-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>";
|
||||
if (peak) h += '<p class="dw-msg dw-peak">' + esc(peak.peak.message) + "</p>";
|
||||
|
||||
h += calendar(d);
|
||||
h += slots(d, root);
|
||||
@ -243,7 +250,7 @@
|
||||
}
|
||||
|
||||
var ridSeq = 0;
|
||||
function rid(root) { if (!root.__ssid) root.__ssid = ++ridSeq; return root.__ssid; }
|
||||
function rid(root) { if (!root.__dwid) root.__dwid = ++ridSeq; return root.__dwid; }
|
||||
|
||||
function calendar(d) {
|
||||
var map = {};
|
||||
@ -254,84 +261,102 @@
|
||||
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>' +
|
||||
var h = '<div class="dw-cal"><div class="dw-calhead"><button type="button" data-dw="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">';
|
||||
'</b><button type="button" data-dw="month" data-v="1" aria-label="Next month">›</button></div><div class="dw-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>";
|
||||
h += '<span class="dw-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>";
|
||||
var cls = "dw-day", label = dateLabel(date, true), extra = "";
|
||||
if (!x) cls += " dw-out";
|
||||
else if (x.available) { cls += " dw-avail"; if (d.text.showRemaining && x.remaining != null && x.remaining <= 5) extra = '<i class="dw-left">' + x.remaining + "</i>"; }
|
||||
else if (x.waitlist || /full|booked/i.test(x.reasons[0] || "")) { cls += " dw-full"; label += ", " + T.full; }
|
||||
else { cls += " dw-closed"; label += ", " + (x.reasons[0] || ""); }
|
||||
if (S.date === date) cls += " dw-sel";
|
||||
if (x && x.peak) { cls += " dw-peakday"; label += ", " + x.peak.name; }
|
||||
h += '<button type="button" class="' + cls + '" data-dw="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>";
|
||||
if (!S.date) return '<p class="dw-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>";
|
||||
var h = '<div class="dw-day-detail"><h3>' + esc(dateLabel(x.date, true)) + (x.peak ? ' <span class="dw-badge">' + esc(x.peak.name) + "</span>" : "") + "</h3>";
|
||||
if (d.dateOnly) h += '<p class="dw-note">' + esc(T.dispatch) + "</p>";
|
||||
if (!x.available) {
|
||||
h += '<p class="ss-msg ' + (x.waitlist ? "ss-peak" : "ss-critical") + '">' + esc(x.reasons.join(". ")) + "</p>";
|
||||
h += '<p class="dw-msg ' + (x.waitlist ? "dw-peak" : "dw-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>";
|
||||
h += '<form class="dw-row" data-dw="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="dw-msg dw-success">' + esc(T.joined) + "</p>";
|
||||
return h + "</div>";
|
||||
}
|
||||
h += '<div class="ss-slots" role="group" aria-label="Times">';
|
||||
h += '<div class="dw-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 += '<button type="button" class="dw-slot" data-dw="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>";
|
||||
}
|
||||
if (S.sel && S.sel.date === x.date) h += stub(d, x);
|
||||
return h + "</div>";
|
||||
}
|
||||
|
||||
/** The "ticket" confirming what the customer chose, with the hold countdown. */
|
||||
function stub(d, x) {
|
||||
var loc = d.locations.filter(function (l) { return l.id === d.locationId; })[0];
|
||||
var s = x.slots.filter(function (y) { return y.id === S.sel.slot; })[0];
|
||||
var live = S.expiresAt && S.expiresAt > Date.now();
|
||||
var expired = S.expiresAt && !live;
|
||||
var h = '<div class="dw-stub' + (expired ? " dw-stub--expired" : "") + '"><div class="dw-stub__main">' +
|
||||
'<span class="dw-stub__method">' + icon(d.method) + esc(d.text.labels[d.method]) + (loc && d.method !== "shipping" ? ", " + esc(loc.name) : "") + "</span>" +
|
||||
'<b class="dw-stub__date">' + esc(dateLabel(x.date, true)) + "</b>" +
|
||||
(s && !d.dateOnly ? '<span class="dw-stub__time">' + esc(slotText(s)) + "</span>" : "") +
|
||||
(S.sel.fee ? '<span class="dw-stub__fee">+' + esc(money(S.sel.fee)) + " " + esc(T.fee) + "</span>" : "") +
|
||||
'</div><div class="dw-stub__side">';
|
||||
if (live) h += '<svg class="dw-ring" viewBox="0 0 36 36" aria-hidden="true"><circle cx="18" cy="18" r="15.5"/><circle class="dw-ring__arc" data-dw-ring cx="18" cy="18" r="15.5" pathLength="100"/></svg><span class="dw-stub__hold">' + esc(T.held) + ' <b data-dw-timer></b></span>';
|
||||
else if (expired) h += '<span class="dw-stub__hold">' + esc(T.expired) + "</span>";
|
||||
else h += '<svg class="dw-check" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5 10 17l9-10"/></svg>';
|
||||
h += "</div></div>";
|
||||
if (loc && loc.pickupInstructions && d.method === "pickup") h += '<p class="dw-note">' + esc(loc.pickupInstructions) + "</p>";
|
||||
return h;
|
||||
}
|
||||
|
||||
function tickTimer() {
|
||||
var left = S.expiresAt ? S.expiresAt - Date.now() : 0;
|
||||
var els = document.querySelectorAll("[data-ss-timer]");
|
||||
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-ss]");
|
||||
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-ss") !== "date") return;
|
||||
var a = t.getAttribute("data-ss"), v = t.getAttribute("data-v");
|
||||
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(".ss-slot:not([aria-disabled])"); }
|
||||
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-ss");
|
||||
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); }
|
||||
@ -346,10 +371,10 @@
|
||||
// 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;
|
||||
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(".ss-day"));
|
||||
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(); }
|
||||
});
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
data-heading="{{ block.settings.heading | escape }}"
|
||||
{{ block.shopify_attributes }}
|
||||
>
|
||||
<div class="ss-skeleton" aria-hidden="true"></div>
|
||||
<div class="dw-skeleton" aria-hidden="true"></div>
|
||||
<noscript>{{ 'datewise.noscript' | t }}</noscript>
|
||||
</div>
|
||||
|
||||
|
||||
@ -16,27 +16,27 @@ const step = (s) => console.log("•", s);
|
||||
|
||||
await fetch(`${BASE}/__reset`);
|
||||
await page.goto(`${BASE}/cart`);
|
||||
await page.waitForSelector(".ss-methods");
|
||||
await page.waitForSelector(".dw-methods");
|
||||
|
||||
step("shipping is blocked by the fresh-flower rule, with the reason shown");
|
||||
const ship = page.locator('.ss-method[data-v="shipping"]');
|
||||
const ship = page.locator('.dw-method[data-v="shipping"]');
|
||||
assert.equal(await ship.getAttribute("aria-disabled"), "true");
|
||||
assert.match(await ship.innerText(), /Fresh flowers are pickup or local delivery only/);
|
||||
|
||||
step("checkout is stopped until a time is chosen");
|
||||
await page.click('button[name="checkout"]');
|
||||
assert.match(await page.locator(".ss-live").innerText(), /choose a delivery or pickup time/i);
|
||||
assert.match(await page.locator(".dw-live").innerText(), /choose a delivery or pickup time/i);
|
||||
assert.equal(await page.locator("#checkedout").innerText(), "");
|
||||
|
||||
step("delivery: postal code → zone fee → pick first date and slot → hold");
|
||||
await page.click('.ss-method[data-v="delivery"]');
|
||||
await page.waitForSelector(".ss-postal");
|
||||
await page.fill(".ss-postal input", "N2H 5Z6");
|
||||
await page.click(".ss-postal button");
|
||||
await page.waitForSelector(".ss-day.ss-avail");
|
||||
await page.locator(".ss-day.ss-avail").first().click();
|
||||
await page.locator('.ss-slot:not([aria-disabled="true"])').first().click();
|
||||
await page.waitForSelector("[data-ss-timer]");
|
||||
await page.click('.dw-method[data-v="delivery"]');
|
||||
await page.waitForSelector(".dw-postal");
|
||||
await page.fill(".dw-postal input", "N2H 5Z6");
|
||||
await page.click(".dw-postal button");
|
||||
await page.waitForSelector(".dw-day.dw-avail");
|
||||
await page.locator(".dw-day.dw-avail").first().click();
|
||||
await page.locator('.dw-slot:not([aria-disabled="true"])').first().click();
|
||||
await page.waitForSelector("[data-dw-timer]");
|
||||
let c = await cart();
|
||||
const sel = JSON.parse(c.attributes._datewise);
|
||||
assert.equal(sel.m, "delivery");
|
||||
@ -59,25 +59,25 @@ c = await cart();
|
||||
assert.ok(c.attributes._datewise, "selection kept");
|
||||
|
||||
step("switching to pickup releases the delivery time and removes the fee");
|
||||
await page.click('.ss-method[data-v="pickup"]');
|
||||
await page.click('.dw-method[data-v="pickup"]');
|
||||
await page.waitForTimeout(1500);
|
||||
c = await cart();
|
||||
assert.equal(c.attributes._datewise, undefined);
|
||||
assert.equal(c.items.filter((i) => i.properties._datewise_fee).length, 0);
|
||||
|
||||
step("pickup hold → checkout goes through with the selection attached");
|
||||
await page.locator(".ss-day.ss-avail").first().click();
|
||||
await page.locator('.ss-slot:not([aria-disabled="true"])').first().click();
|
||||
await page.waitForSelector("[data-ss-timer]");
|
||||
await page.locator(".dw-day.dw-avail").first().click();
|
||||
await page.locator('.dw-slot:not([aria-disabled="true"])').first().click();
|
||||
await page.waitForSelector("[data-dw-timer]");
|
||||
await page.click('button[name="checkout"]');
|
||||
await page.waitForFunction(() => document.getElementById("checkedout").textContent.startsWith("CHECKOUT"));
|
||||
assert.match(await page.locator("#checkedout").innerText(), /"m\\?":\\?"pickup/);
|
||||
await page.screenshot({ path: `${SHOTS}/e2e-pickup.png`, fullPage: true });
|
||||
|
||||
step("keyboard: arrow keys move between calendar days");
|
||||
await page.locator(".ss-day[tabindex='0']").focus();
|
||||
await page.locator(".dw-day[tabindex='0']").focus();
|
||||
await page.keyboard.press("ArrowRight");
|
||||
assert.equal(await page.evaluate(() => document.activeElement.classList.contains("ss-day")), true);
|
||||
assert.equal(await page.evaluate(() => document.activeElement.classList.contains("dw-day")), true);
|
||||
|
||||
assert.deepEqual(errors, [], "no page errors");
|
||||
console.log("\nAll widget checks passed");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user