diff --git a/extensions/datewise-widget/assets/datewise.css b/extensions/datewise-widget/assets/datewise.css index 20a230a..84f4735 100644 --- a/extensions/datewise-widget/assets/datewise.css +++ b/extensions/datewise-widget/assets/datewise.css @@ -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; } diff --git a/extensions/datewise-widget/assets/datewise.js b/extensions/datewise-widget/assets/datewise.js index 2a5a4f3..d21d095 100644 --- a/extensions/datewise-widget/assets/datewise.js +++ b/extensions/datewise-widget/assets/datewise.js @@ -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: '', + delivery: '', + shipping: '' + }; + function icon(m) { return '"; } 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 += '

' + esc(heading) + "

"; - h += '
' + (S.message ? '

' + esc(S.message) + "

" : "") + "
"; - if (!d) { root.innerHTML = h + (S.loading ? '' : ""); root.setAttribute("aria-busy", S.loading); return; } + if (heading) h += '

' + esc(heading) + "

"; + h += '
' + (S.message ? '

' + esc(S.message) + "

" : "") + "
"; + if (!d) { root.innerHTML = h + (S.loading ? '' : ""); root.setAttribute("aria-busy", S.loading); return; } // methods - h += '
'; + h += '
'; d.methods.forEach(function (m) { var on = m.method === d.method; - h += '"; }); h += "
"; if (!d.method) { root.innerHTML = h; return; } if (d.method === "delivery") { - h += '
"; + h += '
"; } - if (d.split) h += '

' + esc(d.split) + "

"; + if (d.split) h += '

' + esc(d.split) + "

"; if (d.method !== "shipping" && d.locations.length > 1) { - h += '
'; + h += '
'; 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 += '"; }); h += "
"; } else if (d.locationId) { var only = d.locations.filter(function (l) { return l.id === d.locationId; })[0]; - if (only && d.method !== "shipping") h += '

' + esc(only.name) + (only.fee ? " · " + (only.fee.waived ? T.free : money(only.fee.fee) + " " + T.fee) : "") + "

"; + if (only && d.method !== "shipping") 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) + "

" : ""); + 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)

"; + 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) + "

"; + if (peak) h += '

' + esc(peak.peak.message) + "

"; 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 = '
' + + var h = '
' + esc(new Intl.DateTimeFormat(LOCALE, { month: "long", year: "numeric", timeZone: "UTC" }).format(new Date(first + "T12:00:00Z"))) + - '
'; + '
'; for (var w = 0; w < 7; w++) { var wd = (start + w) % 7; - h += '"; + h += '"; } for (var i = 0; i < lead; i++) h += ""; 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 = '' + x.remaining + ""; } - 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 += '"; + 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 = '' + x.remaining + ""; } + 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 += '"; } return h + "
"; } function slots(d, root) { - if (!S.date) return '

' + esc(T.choose) + "

"; + if (!S.date) return '

' + esc(T.choose) + "

"; var x = d.dates.filter(function (y) { return y.date === S.date; })[0]; if (!x) return ""; - var h = '

' + esc(dateLabel(x.date, true)) + (x.peak ? ' ' + esc(x.peak.name) + "" : "") + "

"; - if (d.dateOnly) h += '

' + esc(T.dispatch) + "

"; + var h = '

' + esc(dateLabel(x.date, true)) + (x.peak ? ' ' + esc(x.peak.name) + "" : "") + "

"; + if (d.dateOnly) h += '

' + esc(T.dispatch) + "

"; if (!x.available) { - h += '

' + esc(x.reasons.join(". ")) + "

"; + h += '

' + esc(x.reasons.join(". ")) + "

"; if (x.waitlist && !S.waitlistDone) - h += '
"; - if (S.waitlistDone) h += '

' + esc(T.joined) + "

"; + h += '
"; + if (S.waitlistDone) h += '

' + esc(T.joined) + "

"; return h + "
"; } - h += '
'; + h += '
'; 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 += '"; + h += '"; }); h += "
"; - if (S.sel && S.sel.date === x.date) { - var live = S.expiresAt && S.expiresAt > Date.now(); - h += '

' + (live ? esc(T.held) + ' ' : S.expiresAt ? esc(T.expired) : "✓") + "

"; - var loc = d.locations.filter(function (l) { return l.id === d.locationId; })[0]; - if (loc && loc.pickupInstructions) h += '

' + esc(loc.pickupInstructions) + "

"; - } + if (S.sel && S.sel.date === x.date) h += stub(d, x); return h + "
"; } + /** 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 = '
' + + '' + icon(d.method) + esc(d.text.labels[d.method]) + (loc && d.method !== "shipping" ? ", " + esc(loc.name) : "") + "" + + '' + esc(dateLabel(x.date, true)) + "" + + (s && !d.dateOnly ? '' + esc(slotText(s)) + "" : "") + + (S.sel.fee ? '+' + esc(money(S.sel.fee)) + " " + esc(T.fee) + "" : "") + + '
'; + if (live) h += '' + esc(T.held) + ' '; + else if (expired) h += '' + esc(T.expired) + ""; + else h += ''; + h += "
"; + if (loc && loc.pickupInstructions && d.method === "pickup") h += '

' + esc(loc.pickupInstructions) + "

"; + 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(); } }); diff --git a/extensions/datewise-widget/blocks/scheduler.liquid b/extensions/datewise-widget/blocks/scheduler.liquid index 3dc4d53..f366c6b 100644 --- a/extensions/datewise-widget/blocks/scheduler.liquid +++ b/extensions/datewise-widget/blocks/scheduler.liquid @@ -8,7 +8,7 @@ data-heading="{{ block.settings.heading | escape }}" {{ block.shopify_attributes }} > - +
diff --git a/tests/e2e/widget.e2e.mjs b/tests/e2e/widget.e2e.mjs index 5ee67f3..53dc868 100644 --- a/tests/e2e/widget.e2e.mjs +++ b/tests/e2e/widget.e2e.mjs @@ -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");