@code-collective/booking-widget 1.0.3 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +318 -282
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +1060 -830
- package/dist/booking-widget.min.js +6 -7
- package/dist/booking-widget.umd.cjs +2 -2
- package/package.json +1 -1
- package/src/lib/Checkout.svelte +6 -1
- package/src/lib/CheckoutModal.svelte +16 -43
- package/src/lib/EditBookingView.svelte +5 -1
- package/src/lib/WizardPage.svelte +371 -96
- package/src/lib/config.ts +72 -14
- package/src/lib/elements/bw-checkout.svelte +9 -4
- package/src/lib/elements/bw-configurator.svelte +2 -2
- package/src/lib/elements/register.ts +12 -1
- package/src/lib/index.ts +4 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { CartItem, CartUnitItem, CheckoutProductDto, CheckoutCartItemDetailDto, CheckoutOptionDto, CheckoutAvailabilityDto, CheckoutPickupLocationDto, CheckoutAvailabilityCalendarDto } from './client-types';
|
|
3
3
|
import type { BookingApi } from './api';
|
|
4
|
-
import type { WizardPages, WizardWidgetType } from './config';
|
|
4
|
+
import type { WizardPages, WizardPageConfig, WizardWidgetType } from './config';
|
|
5
5
|
import { formatCurrency } from './currency';
|
|
6
6
|
import { dateKey, formatTime, parseDateFromAvailabilityId } from './utils';
|
|
7
7
|
import OptionCard from './OptionCard.svelte';
|
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
product: CheckoutProductDto;
|
|
16
16
|
api: BookingApi;
|
|
17
17
|
wizardPages: WizardPages;
|
|
18
|
+
editPages?: WizardPages;
|
|
18
19
|
editItem?: CheckoutCartItemDetailDto;
|
|
19
20
|
autoSelectSingleTimeSlot?: boolean;
|
|
20
21
|
onComplete: (item: CartItem) => void;
|
|
21
22
|
onCancel?: () => void;
|
|
22
23
|
}
|
|
23
24
|
let {
|
|
24
|
-
product, api, wizardPages,
|
|
25
|
+
product, api, wizardPages, editPages = wizardPages,
|
|
25
26
|
editItem, autoSelectSingleTimeSlot = false, onComplete, onCancel,
|
|
26
27
|
}: Props = $props();
|
|
27
28
|
|
|
@@ -43,10 +44,27 @@
|
|
|
43
44
|
let loadingTimeSlots = $state(false);
|
|
44
45
|
let dirty = $state(false);
|
|
45
46
|
let showConfirmDialog = $state(false);
|
|
47
|
+
let expandedSection = $state(0);
|
|
48
|
+
|
|
49
|
+
// Widgets whose previously-made selection was invalidated by a later change (e.g. a
|
|
50
|
+
// capacity increase that no longer fits the selected slot) and now needs to be redone.
|
|
51
|
+
// Surfaced on the edit accordion as a red border + "needs new configuration" summary,
|
|
52
|
+
// even while collapsed, so the user knows exactly what to revisit instead of everything.
|
|
53
|
+
let invalidatedWidgets = $state<Set<WizardWidgetType>>(new Set());
|
|
54
|
+
|
|
55
|
+
function markInvalidated(...types: WizardWidgetType[]) {
|
|
56
|
+
invalidatedWidgets = new Set([...invalidatedWidgets, ...types]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clearInvalidated(...types: WizardWidgetType[]) {
|
|
60
|
+
if (types.every(t => !invalidatedWidgets.has(t))) return;
|
|
61
|
+
invalidatedWidgets = new Set([...invalidatedWidgets].filter(w => !types.includes(w)));
|
|
62
|
+
}
|
|
46
63
|
|
|
47
64
|
let hasQuantities = $derived(Object.values(unitQuantities).some(q => q > 0));
|
|
48
65
|
let hasAllDaySlot = $derived(timeSlots.length === 1 && timeSlots[0].allDay);
|
|
49
66
|
let hasSingleSlot = $derived(timeSlots.length === 1);
|
|
67
|
+
let hasNoTimeSlots = $derived(!loadingTimeSlots && timeSlots.length === 0);
|
|
50
68
|
let hideTimePicker = $derived(hasAllDaySlot || (autoSelectSingleTimeSlot && hasSingleSlot));
|
|
51
69
|
|
|
52
70
|
// -- Flow detection --
|
|
@@ -55,8 +73,8 @@
|
|
|
55
73
|
// same unit types, so we can show units and fetch availability before an
|
|
56
74
|
// option is selected by using the first option as a stand-in.
|
|
57
75
|
let isAgeFirstFlow = $derived(() => {
|
|
58
|
-
const agePage = wizardPages.findIndex(p => p.includes('age-category'));
|
|
59
|
-
const optPage = wizardPages.findIndex(p => p.includes('option'));
|
|
76
|
+
const agePage = wizardPages.findIndex(p => p.widgets.includes('age-category'));
|
|
77
|
+
const optPage = wizardPages.findIndex(p => p.widgets.includes('option'));
|
|
60
78
|
return agePage >= 0 && (optPage < 0 || agePage < optPage);
|
|
61
79
|
});
|
|
62
80
|
|
|
@@ -132,7 +150,7 @@
|
|
|
132
150
|
case 'option': return !lockedOptionId;
|
|
133
151
|
case 'age-category': return selectedOption != null;
|
|
134
152
|
case 'date': return hasQuantities;
|
|
135
|
-
case 'time': return selectedDate != null && !hideTimePicker;
|
|
153
|
+
case 'time': return selectedDate != null && !hideTimePicker && !hasNoTimeSlots;
|
|
136
154
|
case 'pickup': return selectedTimeSlot != null && selectedOption?.pickupAvailable === true;
|
|
137
155
|
case 'addon': return false;
|
|
138
156
|
}
|
|
@@ -143,7 +161,7 @@
|
|
|
143
161
|
switch (type) {
|
|
144
162
|
case 'age-category': return availableUnits().length > 0;
|
|
145
163
|
case 'date': return hasQuantities;
|
|
146
|
-
case 'time': return selectedDate != null && !hideTimePicker;
|
|
164
|
+
case 'time': return selectedDate != null && !hideTimePicker && !hasNoTimeSlots;
|
|
147
165
|
case 'option': return !lockedOptionId && (selectedTimeSlot != null || (selectedDate != null && hideTimePicker));
|
|
148
166
|
case 'pickup': return selectedOption != null && selectedOption.pickupAvailable === true && selectedTimeSlot != null;
|
|
149
167
|
case 'addon': return false;
|
|
@@ -151,18 +169,18 @@
|
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
let visiblePages = $derived(() => {
|
|
154
|
-
const result: { index: number; widgets: WizardWidgetType[] }[] = [];
|
|
172
|
+
const result: { index: number; title: string; widgets: WizardWidgetType[] }[] = [];
|
|
155
173
|
for (let i = 0; i < wizardPages.length; i++) {
|
|
156
|
-
const visible = wizardPages[i].filter(isWidgetVisible);
|
|
157
|
-
if (visible.length > 0) result.push({ index: i, widgets: visible });
|
|
174
|
+
const visible = wizardPages[i].widgets.filter(isWidgetVisible);
|
|
175
|
+
if (visible.length > 0) result.push({ index: i, title: wizardPages[i].title, widgets: visible });
|
|
158
176
|
}
|
|
159
177
|
return result;
|
|
160
178
|
});
|
|
161
179
|
|
|
162
180
|
let allVisibleWidgets = $derived(() => {
|
|
163
181
|
const result: WizardWidgetType[] = [];
|
|
164
|
-
for (const page of
|
|
165
|
-
for (const w of page) {
|
|
182
|
+
for (const page of editPages) {
|
|
183
|
+
for (const w of page.widgets) {
|
|
166
184
|
if (isWidgetVisible(w) && !result.includes(w)) result.push(w);
|
|
167
185
|
}
|
|
168
186
|
}
|
|
@@ -177,7 +195,53 @@
|
|
|
177
195
|
|
|
178
196
|
let isLastPage = $derived(() => pageIndex >= visiblePages().length - 1);
|
|
179
197
|
|
|
198
|
+
let pageTitle = $derived(() => currentPage()?.title ?? '');
|
|
199
|
+
|
|
200
|
+
function widgetSummary(type: WizardWidgetType): string {
|
|
201
|
+
switch (type) {
|
|
202
|
+
case 'option':
|
|
203
|
+
return selectedOption?.title ?? 'Not selected';
|
|
204
|
+
case 'age-category': {
|
|
205
|
+
const parts: string[] = [];
|
|
206
|
+
for (const unit of availableUnits()) {
|
|
207
|
+
const qty = unitQuantities[unit.id] ?? 0;
|
|
208
|
+
if (qty > 0) parts.push(`${qty} ${unit.title}`);
|
|
209
|
+
}
|
|
210
|
+
return parts.length > 0 ? parts.join(', ') : 'Not selected';
|
|
211
|
+
}
|
|
212
|
+
case 'date':
|
|
213
|
+
if (!selectedDate) return 'Not selected';
|
|
214
|
+
return selectedDate.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
|
|
215
|
+
case 'time':
|
|
216
|
+
return selectedTime ?? (hideTimePicker ? 'All day' : 'Not selected');
|
|
217
|
+
case 'pickup':
|
|
218
|
+
return selectedPickupPoint?.title ?? selectedPickupPoint?.id ?? 'Not selected';
|
|
219
|
+
default:
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sectionSummary(widgets: WizardWidgetType[]): string {
|
|
225
|
+
return widgets.map(widgetSummary).filter(s => s && s !== 'Not selected').join(' \u00B7 ') || 'Not selected';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let editSections = $derived(() => {
|
|
229
|
+
const result: { title: string; widgets: WizardWidgetType[] }[] = [];
|
|
230
|
+
for (const page of editPages) {
|
|
231
|
+
const visible = page.widgets.filter(isWidgetVisible);
|
|
232
|
+
if (visible.length > 0) {
|
|
233
|
+
result.push({ title: page.title, widgets: visible });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
});
|
|
238
|
+
|
|
180
239
|
let pageComplete = $derived(() => {
|
|
240
|
+
// A date with confirmed zero slots can never be completed - 'time' is hidden entirely in
|
|
241
|
+
// this case (see isWidgetVisible), so without this the widget-list check below would never
|
|
242
|
+
// even look at it and could wrongly treat the page as done.
|
|
243
|
+
if (selectedDate != null && hasNoTimeSlots) return false;
|
|
244
|
+
|
|
181
245
|
const widgets = editMode ? allVisibleWidgets() : (currentPage()?.widgets ?? []);
|
|
182
246
|
if (widgets.length === 0) return false;
|
|
183
247
|
for (const w of widgets) {
|
|
@@ -207,10 +271,14 @@
|
|
|
207
271
|
|
|
208
272
|
function selectOption(option: CheckoutOptionDto) {
|
|
209
273
|
// In age-first flow the user already picked quantities, date, and time
|
|
210
|
-
// before reaching the option step — keep those selections intact.
|
|
274
|
+
// before reaching the option step — keep those selections intact. Pickup only
|
|
275
|
+
// needs to be redone if the new option doesn't offer the point already chosen.
|
|
211
276
|
if (isAgeFirstFlow()) {
|
|
212
277
|
selectedOption = option;
|
|
213
|
-
selectedPickupPoint
|
|
278
|
+
if (selectedPickupPoint && !option.pickupLocations?.some(p => p.id === selectedPickupPoint!.id)) {
|
|
279
|
+
selectedPickupPoint = null;
|
|
280
|
+
markInvalidated('pickup');
|
|
281
|
+
}
|
|
214
282
|
dirty = true;
|
|
215
283
|
return;
|
|
216
284
|
}
|
|
@@ -232,6 +300,22 @@
|
|
|
232
300
|
if (hasQuantities && Object.keys(availabilityByDate).length === 0) {
|
|
233
301
|
fetchAvailability();
|
|
234
302
|
}
|
|
303
|
+
invalidateDateIfCapacityExceedsSlot();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// A capacity increase can outgrow the already-selected slot's vacancy - if so, that slot
|
|
307
|
+
// no longer covers the booking and needs to be reselected. If it still has room (or has no
|
|
308
|
+
// vacancy limit at all), the date/time picked earlier is still perfectly valid.
|
|
309
|
+
function invalidateDateIfCapacityExceedsSlot() {
|
|
310
|
+
if (!selectedTimeSlot || selectedTimeSlot.vacancies == null) return;
|
|
311
|
+
const totalQty = Object.values(unitQuantities).reduce((sum, q) => sum + q, 0);
|
|
312
|
+
if (totalQty <= selectedTimeSlot.vacancies) return;
|
|
313
|
+
|
|
314
|
+
selectedDate = null;
|
|
315
|
+
selectedTime = null;
|
|
316
|
+
selectedTimeSlot = null;
|
|
317
|
+
timeSlots = [];
|
|
318
|
+
markInvalidated('date', 'time');
|
|
235
319
|
}
|
|
236
320
|
|
|
237
321
|
async function fetchAvailability() {
|
|
@@ -245,13 +329,15 @@
|
|
|
245
329
|
async function onDateSelected(date: Date) {
|
|
246
330
|
const optionId = effectiveOptionId();
|
|
247
331
|
if (!optionId) return;
|
|
332
|
+
// Pickup points are a property of the option, not the date/time, so an already-chosen
|
|
333
|
+
// one stays valid across a date change and doesn't need to be picked again.
|
|
248
334
|
selectedDate = date;
|
|
249
335
|
selectedTime = null;
|
|
250
336
|
selectedTimeSlot = null;
|
|
251
|
-
selectedPickupPoint = null;
|
|
252
337
|
timeSlots = [];
|
|
253
338
|
dirty = true;
|
|
254
339
|
loadingTimeSlots = true;
|
|
340
|
+
clearInvalidated('date', 'time');
|
|
255
341
|
|
|
256
342
|
const slots = await api.getAvailability(product.id, optionId, dateKey(date));
|
|
257
343
|
timeSlots = slots;
|
|
@@ -270,13 +356,14 @@
|
|
|
270
356
|
if (!slot) return;
|
|
271
357
|
selectedTime = time;
|
|
272
358
|
selectedTimeSlot = slot;
|
|
273
|
-
selectedPickupPoint = null;
|
|
274
359
|
dirty = true;
|
|
360
|
+
clearInvalidated('time');
|
|
275
361
|
}
|
|
276
362
|
|
|
277
363
|
function onPickupSelected(point: CheckoutPickupLocationDto) {
|
|
278
364
|
selectedPickupPoint = point;
|
|
279
365
|
dirty = true;
|
|
366
|
+
clearInvalidated('pickup');
|
|
280
367
|
}
|
|
281
368
|
|
|
282
369
|
function goNext() {
|
|
@@ -359,100 +446,205 @@
|
|
|
359
446
|
</div>
|
|
360
447
|
{/if}
|
|
361
448
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
{
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
{
|
|
369
|
-
|
|
370
|
-
{option}
|
|
371
|
-
selected={selectedOption?.id === option.id}
|
|
372
|
-
onSelect={() => selectOption(option)}
|
|
373
|
-
/>
|
|
374
|
-
{/each}
|
|
375
|
-
</div>
|
|
449
|
+
{#if !editMode}
|
|
450
|
+
<div class="page-title-row">
|
|
451
|
+
<h3 class="page-title">{pageTitle()}</h3>
|
|
452
|
+
{#if visiblePages().length > 1}
|
|
453
|
+
<div class="page-dots">
|
|
454
|
+
{#each visiblePages() as _, i}
|
|
455
|
+
<span class="dot" class:active={i === pageIndex}></span>
|
|
456
|
+
{/each}
|
|
376
457
|
</div>
|
|
377
458
|
{/if}
|
|
459
|
+
</div>
|
|
460
|
+
{/if}
|
|
378
461
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
462
|
+
{#if editMode}
|
|
463
|
+
<div class="wizard-body">
|
|
464
|
+
{#each editSections() as section, sectionIndex}
|
|
465
|
+
{@const sectionInvalidated = section.widgets.some(w => invalidatedWidgets.has(w))}
|
|
466
|
+
<div class="accordion-section" class:expanded={expandedSection === sectionIndex} class:invalidated={sectionInvalidated}>
|
|
467
|
+
<button class="accordion-header" onclick={() => { expandedSection = expandedSection === sectionIndex ? -1 : sectionIndex; }}>
|
|
468
|
+
<div class="accordion-header-text">
|
|
469
|
+
<span class="accordion-title">{section.title.toUpperCase()}</span>
|
|
470
|
+
{#if expandedSection !== sectionIndex}
|
|
471
|
+
<span class="accordion-summary" class:invalidated={sectionInvalidated}>
|
|
472
|
+
{sectionInvalidated ? 'Changes made require new configuration' : sectionSummary(section.widgets)}
|
|
473
|
+
</span>
|
|
474
|
+
{/if}
|
|
475
|
+
</div>
|
|
476
|
+
<span class="accordion-chevron" class:open={expandedSection === sectionIndex}></span>
|
|
477
|
+
</button>
|
|
478
|
+
{#if expandedSection === sectionIndex}
|
|
479
|
+
<div class="accordion-body">
|
|
480
|
+
{#each section.widgets as widgetType}
|
|
481
|
+
{#if widgetType === 'option' && !lockedOptionId}
|
|
482
|
+
<div class="section">
|
|
483
|
+
<div class="option-list">
|
|
484
|
+
{#each product.options as option}
|
|
485
|
+
<OptionCard
|
|
486
|
+
{option}
|
|
487
|
+
selected={selectedOption?.id === option.id}
|
|
488
|
+
onSelect={() => selectOption(option)}
|
|
489
|
+
/>
|
|
490
|
+
{/each}
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
{/if}
|
|
494
|
+
|
|
495
|
+
{#if widgetType === 'age-category' && availableUnits().length > 0}
|
|
496
|
+
<div class="section">
|
|
497
|
+
<div class="unit-list">
|
|
498
|
+
{#each availableUnits() as unit}
|
|
499
|
+
<UnitCounter
|
|
500
|
+
{unit}
|
|
501
|
+
quantity={unitQuantities[unit.id] ?? 0}
|
|
502
|
+
onChange={(qty) => onUnitChanged(unit.id, qty)}
|
|
503
|
+
/>
|
|
504
|
+
{/each}
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
{/if}
|
|
508
|
+
|
|
509
|
+
{#if widgetType === 'date' && hasQuantities}
|
|
510
|
+
<div class="section">
|
|
511
|
+
<AvailabilityCalendar
|
|
512
|
+
{availabilityByDate}
|
|
513
|
+
{selectedDate}
|
|
514
|
+
onDateSelected={onDateSelected}
|
|
515
|
+
loading={loadingAvailability}
|
|
516
|
+
/>
|
|
517
|
+
</div>
|
|
518
|
+
{/if}
|
|
519
|
+
|
|
520
|
+
{#if widgetType === 'time' && selectedDate && !hideTimePicker}
|
|
521
|
+
<div class="section">
|
|
522
|
+
<TimeSlotPicker
|
|
523
|
+
{timeSlots}
|
|
524
|
+
{selectedTime}
|
|
525
|
+
loading={loadingTimeSlots}
|
|
526
|
+
onTimeSelected={onTimeSelected}
|
|
527
|
+
/>
|
|
528
|
+
</div>
|
|
529
|
+
{/if}
|
|
530
|
+
|
|
531
|
+
{#if widgetType === 'pickup' && selectedOption?.pickupAvailable && selectedOption.pickupLocations}
|
|
532
|
+
<div class="section">
|
|
533
|
+
<div class="pickup-list">
|
|
534
|
+
{#each selectedOption.pickupLocations as point}
|
|
535
|
+
<SelectableCard
|
|
536
|
+
selected={selectedPickupPoint?.id === point.id}
|
|
537
|
+
onSelect={() => onPickupSelected(point)}
|
|
538
|
+
heading={point.title ?? point.id}
|
|
539
|
+
body={point.address}
|
|
540
|
+
/>
|
|
541
|
+
{/each}
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
{/if}
|
|
545
|
+
{/each}
|
|
546
|
+
</div>
|
|
547
|
+
{/if}
|
|
391
548
|
</div>
|
|
392
|
-
{/
|
|
549
|
+
{/each}
|
|
550
|
+
</div>
|
|
393
551
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
/>
|
|
403
|
-
</div>
|
|
404
|
-
{/if}
|
|
552
|
+
<div class="action-bar">
|
|
553
|
+
<button class="btn btn-secondary" onclick={handleCancel}>
|
|
554
|
+
Return to Cart
|
|
555
|
+
</button>
|
|
556
|
+
<button class="btn btn-primary" onclick={completeWizard} disabled={!pageComplete()}>
|
|
557
|
+
Save & Update
|
|
558
|
+
</button>
|
|
559
|
+
</div>
|
|
405
560
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
561
|
+
{:else}
|
|
562
|
+
<div class="wizard-body">
|
|
563
|
+
{#each currentPage()?.widgets ?? [] as widgetType}
|
|
564
|
+
{#if widgetType === 'option' && !lockedOptionId}
|
|
565
|
+
<div class="section">
|
|
566
|
+
<h4 class="section-title">Options</h4>
|
|
567
|
+
<div class="option-list">
|
|
568
|
+
{#each product.options as option}
|
|
569
|
+
<OptionCard
|
|
570
|
+
{option}
|
|
571
|
+
selected={selectedOption?.id === option.id}
|
|
572
|
+
onSelect={() => selectOption(option)}
|
|
573
|
+
/>
|
|
574
|
+
{/each}
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
{/if}
|
|
417
578
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
579
|
+
{#if widgetType === 'age-category' && availableUnits().length > 0}
|
|
580
|
+
<div class="section">
|
|
581
|
+
<h4 class="section-title">How many tickets?</h4>
|
|
582
|
+
<div class="unit-list">
|
|
583
|
+
{#each availableUnits() as unit}
|
|
584
|
+
<UnitCounter
|
|
585
|
+
{unit}
|
|
586
|
+
quantity={unitQuantities[unit.id] ?? 0}
|
|
587
|
+
onChange={(qty) => onUnitChanged(unit.id, qty)}
|
|
588
|
+
/>
|
|
589
|
+
{/each}
|
|
590
|
+
</div>
|
|
430
591
|
</div>
|
|
431
|
-
|
|
432
|
-
{/if}
|
|
433
|
-
{/each}
|
|
592
|
+
{/if}
|
|
434
593
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
594
|
+
{#if widgetType === 'date' && hasQuantities}
|
|
595
|
+
<div class="section">
|
|
596
|
+
<h4 class="section-title">Choose a date</h4>
|
|
597
|
+
<AvailabilityCalendar
|
|
598
|
+
{availabilityByDate}
|
|
599
|
+
{selectedDate}
|
|
600
|
+
onDateSelected={onDateSelected}
|
|
601
|
+
loading={loadingAvailability}
|
|
602
|
+
/>
|
|
603
|
+
</div>
|
|
604
|
+
{/if}
|
|
605
|
+
|
|
606
|
+
{#if widgetType === 'time' && selectedDate && !hideTimePicker}
|
|
607
|
+
<div class="section">
|
|
608
|
+
<h4 class="section-title">Select a time</h4>
|
|
609
|
+
<TimeSlotPicker
|
|
610
|
+
{timeSlots}
|
|
611
|
+
{selectedTime}
|
|
612
|
+
loading={loadingTimeSlots}
|
|
613
|
+
onTimeSelected={onTimeSelected}
|
|
614
|
+
/>
|
|
615
|
+
</div>
|
|
448
616
|
{/if}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
<
|
|
617
|
+
|
|
618
|
+
{#if widgetType === 'pickup' && selectedOption?.pickupAvailable && selectedOption.pickupLocations}
|
|
619
|
+
<div class="section">
|
|
620
|
+
<h4 class="section-title">Select pickup point</h4>
|
|
621
|
+
<div class="pickup-list">
|
|
622
|
+
{#each selectedOption.pickupLocations as point}
|
|
623
|
+
<SelectableCard
|
|
624
|
+
selected={selectedPickupPoint?.id === point.id}
|
|
625
|
+
onSelect={() => onPickupSelected(point)}
|
|
626
|
+
heading={point.title ?? point.id}
|
|
627
|
+
body={point.address}
|
|
628
|
+
/>
|
|
629
|
+
{/each}
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
{/if}
|
|
633
|
+
{/each}
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<div class="action-bar">
|
|
637
|
+
{#if pageIndex > 0 || onCancel}
|
|
638
|
+
<button class="btn btn-secondary" onclick={goBack}>
|
|
639
|
+
{pageIndex > 0 ? 'Back' : 'Cancel'}
|
|
452
640
|
</button>
|
|
453
641
|
{/if}
|
|
642
|
+
<button class="btn btn-primary" onclick={goNext} disabled={!pageComplete()}>
|
|
643
|
+
{isLastPage() ? 'Add to Cart' : 'Next'}
|
|
644
|
+
<span class="arrow">→</span>
|
|
645
|
+
</button>
|
|
454
646
|
</div>
|
|
455
|
-
|
|
647
|
+
{/if}
|
|
456
648
|
</div>
|
|
457
649
|
|
|
458
650
|
{#if showConfirmDialog}
|
|
@@ -484,6 +676,89 @@
|
|
|
484
676
|
font-size: 20px;
|
|
485
677
|
font-weight: 800;
|
|
486
678
|
}
|
|
679
|
+
.page-title-row {
|
|
680
|
+
display: flex;
|
|
681
|
+
align-items: center;
|
|
682
|
+
justify-content: space-between;
|
|
683
|
+
padding: 12px 16px 0;
|
|
684
|
+
}
|
|
685
|
+
.page-title {
|
|
686
|
+
font-size: 16px;
|
|
687
|
+
font-weight: 700;
|
|
688
|
+
color: var(--bw-color-text);
|
|
689
|
+
}
|
|
690
|
+
.page-dots {
|
|
691
|
+
display: flex;
|
|
692
|
+
gap: 6px;
|
|
693
|
+
}
|
|
694
|
+
.dot {
|
|
695
|
+
width: 8px;
|
|
696
|
+
height: 8px;
|
|
697
|
+
border-radius: 50%;
|
|
698
|
+
background: var(--bw-color-border);
|
|
699
|
+
}
|
|
700
|
+
.dot.active {
|
|
701
|
+
background: var(--bw-color-text);
|
|
702
|
+
}
|
|
703
|
+
.accordion-section {
|
|
704
|
+
border: 1px solid var(--bw-color-border);
|
|
705
|
+
border-radius: var(--bw-radius-lg);
|
|
706
|
+
margin-bottom: 12px;
|
|
707
|
+
overflow: hidden;
|
|
708
|
+
background: var(--bw-color-bg);
|
|
709
|
+
}
|
|
710
|
+
.accordion-section.invalidated {
|
|
711
|
+
border-color: var(--bw-color-error);
|
|
712
|
+
}
|
|
713
|
+
.accordion-header {
|
|
714
|
+
display: flex;
|
|
715
|
+
align-items: center;
|
|
716
|
+
justify-content: space-between;
|
|
717
|
+
width: 100%;
|
|
718
|
+
padding: 16px 20px;
|
|
719
|
+
background: none;
|
|
720
|
+
border: none;
|
|
721
|
+
cursor: pointer;
|
|
722
|
+
text-align: left;
|
|
723
|
+
font-family: inherit;
|
|
724
|
+
}
|
|
725
|
+
.accordion-header-text {
|
|
726
|
+
display: flex;
|
|
727
|
+
flex-direction: column;
|
|
728
|
+
gap: 4px;
|
|
729
|
+
min-width: 0;
|
|
730
|
+
flex: 1;
|
|
731
|
+
}
|
|
732
|
+
.accordion-title {
|
|
733
|
+
font-size: 11px;
|
|
734
|
+
font-weight: 700;
|
|
735
|
+
letter-spacing: 0.08em;
|
|
736
|
+
color: var(--bw-color-text-secondary);
|
|
737
|
+
}
|
|
738
|
+
.accordion-summary {
|
|
739
|
+
font-size: 15px;
|
|
740
|
+
font-weight: 600;
|
|
741
|
+
color: var(--bw-color-text);
|
|
742
|
+
}
|
|
743
|
+
.accordion-summary.invalidated {
|
|
744
|
+
color: var(--bw-color-error);
|
|
745
|
+
}
|
|
746
|
+
.accordion-chevron {
|
|
747
|
+
flex-shrink: 0;
|
|
748
|
+
margin-left: 12px;
|
|
749
|
+
width: 12px;
|
|
750
|
+
height: 12px;
|
|
751
|
+
border-right: 2px solid var(--bw-color-text-secondary);
|
|
752
|
+
border-bottom: 2px solid var(--bw-color-text-secondary);
|
|
753
|
+
transform: rotate(45deg);
|
|
754
|
+
transition: transform 0.15s ease;
|
|
755
|
+
}
|
|
756
|
+
.accordion-chevron.open {
|
|
757
|
+
transform: rotate(-135deg);
|
|
758
|
+
}
|
|
759
|
+
.accordion-body {
|
|
760
|
+
padding: 0 20px 20px;
|
|
761
|
+
}
|
|
487
762
|
.wizard-body {
|
|
488
763
|
flex: 1;
|
|
489
764
|
overflow-y: auto;
|