@code-collective/booking-widget 1.0.3 → 1.0.6

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.
@@ -9,8 +9,11 @@
9
9
  interface Props {
10
10
  api: BookingApi;
11
11
  wizardPages?: WizardPages;
12
+ editPages?: WizardPages;
13
+ autoSelectSingleTimeSlot?: boolean;
12
14
  }
13
- let { api, wizardPages = DEFAULT_WIZARD_PAGES }: Props = $props();
15
+ let { api, wizardPages = DEFAULT_WIZARD_PAGES, editPages: editPagesProp, autoSelectSingleTimeSlot = false }: Props = $props();
16
+ let editPages = $derived(editPagesProp ?? wizardPages);
14
17
 
15
18
  let cart = $state<CheckoutCartDetailDto | null>(null);
16
19
  let isLoading = $state(true);
@@ -27,29 +30,41 @@
27
30
  load();
28
31
  </script>
29
32
 
30
- {#if isLoading}
31
- <div class="loading-center" style="height:100vh">
32
- <div class="spinner"></div>
33
- </div>
34
- {:else if cart && cart.items.length > 0}
35
- <CheckoutModal
36
- {cart}
37
- {api}
38
- {wizardPages}
39
- onClose={() => postMessage({ type: 'modal:close' })}
40
- onOrderConfirmed={() => {
41
- const total = cart!.items.reduce((s, i) => s + i.amount, 0);
42
- const currency = cart!.items[0]?.currencyCode ?? 'ZAR';
43
- postMessage({
44
- type: 'order:complete',
45
- cartToken: cart!.cartToken,
46
- value: total,
47
- currency,
48
- });
49
- }}
50
- />
51
- {:else}
52
- <div class="loading-center" style="height:100vh;color:var(--bw-color-text-secondary)">
53
- No items in cart.
54
- </div>
55
- {/if}
33
+ <div class="bw-widget">
34
+ {#if isLoading}
35
+ <div class="loading-center" style="height:100vh">
36
+ <div class="spinner"></div>
37
+ </div>
38
+ {:else if cart && cart.items.length > 0}
39
+ <CheckoutModal
40
+ {cart}
41
+ {api}
42
+ {wizardPages}
43
+ {editPages}
44
+ {autoSelectSingleTimeSlot}
45
+ onClose={() => postMessage({ type: 'modal:close' })}
46
+ onOrderConfirmed={() => {
47
+ const total = cart!.items.reduce((s, i) => s + i.amount, 0);
48
+ const currency = cart!.items[0]?.currencyCode ?? 'ZAR';
49
+ postMessage({
50
+ type: 'order:complete',
51
+ cartToken: cart!.cartToken,
52
+ value: total,
53
+ currency,
54
+ });
55
+ }}
56
+ />
57
+ {:else}
58
+ <div class="loading-center" style="height:100vh;color:var(--bw-color-text-secondary)">
59
+ No items in cart.
60
+ </div>
61
+ {/if}
62
+ </div>
63
+
64
+ <style>
65
+ /* display: contents - a plain box here would break CheckoutModal's own .modal{height:100%}, which needs
66
+ to resolve against this component's real parent, not an unsized wrapper inserted in between. */
67
+ .bw-widget {
68
+ display: contents;
69
+ }
70
+ </style>
@@ -16,10 +16,12 @@
16
16
  cart: CheckoutCartDetailDto;
17
17
  api: BookingApi;
18
18
  wizardPages?: WizardPages;
19
+ editPages?: WizardPages;
20
+ autoSelectSingleTimeSlot?: boolean;
19
21
  onClose: () => void;
20
22
  onOrderConfirmed: () => void;
21
23
  }
22
- let { cart, api, wizardPages, onClose, onOrderConfirmed }: Props = $props();
24
+ let { cart, api, wizardPages, editPages, autoSelectSingleTimeSlot = false, onClose, onOrderConfirmed }: Props = $props();
23
25
 
24
26
  type View = 'cart' | 'contact' | 'edit' | 'payment' | 'result';
25
27
  let currentView = $state<View>('cart');
@@ -124,15 +126,25 @@
124
126
  const updated = await api.getCart();
125
127
  cart = updated;
126
128
  currentView = 'cart';
129
+ notifyCartUpdated();
127
130
  }
128
131
 
129
132
  async function removeItem(item: CheckoutCartItemDetailDto) {
130
133
  await api.removeCartItem(item.id);
131
134
  const updated = await api.getCart();
132
135
  cart = updated;
136
+ notifyCartUpdated();
133
137
  if (cart.items.length === 0) close();
134
138
  }
135
139
 
140
+ // A standalone postMessage - not the itemCount/cartItemId-shaped cart:change TicketConfigurator posts on
141
+ // add (which bw-configurator.svelte re-dispatches as the public bw:cart-change event and auto-opens
142
+ // checkout for). This is purely internal: CartBar/CartOverviewButton listen for it to refetch their own
143
+ // total after an edit or remove, which otherwise only ever fetch once on mount.
144
+ function notifyCartUpdated(): void {
145
+ postMessage({ type: 'cart:updated' });
146
+ }
147
+
136
148
  async function onPayNow() {
137
149
  hasAttemptedSubmit = true;
138
150
  if (!contactForm?.isValid() || !privacyAccepted) return;
@@ -210,6 +222,8 @@
210
222
  product={productsById.get(editingItem.productId)!}
211
223
  {api}
212
224
  {wizardPages}
225
+ {editPages}
226
+ {autoSelectSingleTimeSlot}
213
227
  onBack={() => { currentView = 'cart'; }}
214
228
  onClose={close}
215
229
  onUpdated={onItemUpdated}
@@ -250,15 +264,14 @@
250
264
 
251
265
  {:else}
252
266
  <!-- Cart view -->
253
- <div class="body">
254
- <div class="cart-header">
255
- <h2 class="cart-title">Your cart</h2>
256
- <div class="cart-header-right">
257
- <CountdownTimer expiresAt={expiresAt()} />
258
- <button class="keep-shopping-btn" onclick={continueShopping}>+ Keep shopping</button>
259
- </div>
260
- </div>
267
+ <div class="page-header">
268
+ <h2>Your cart</h2>
269
+ <span class="spacer"></span>
270
+ <CountdownTimer expiresAt={expiresAt()} />
271
+ <button class="keep-shopping-btn" onclick={continueShopping}>+ Keep shopping</button>
272
+ </div>
261
273
 
274
+ <div class="body">
262
275
  {#each cart.items as item}
263
276
  <div class="cart-card">
264
277
  <div class="cart-card-top">
@@ -296,9 +309,11 @@
296
309
  <span>Total payable today</span>
297
310
  <span class="cart-total-amount">{formattedTotal()}</span>
298
311
  </div>
312
+ </div>
299
313
 
300
- <button class="checkout-btn" onclick={() => { currentView = 'contact'; }}>
301
- Checkout &rarr;
314
+ <div class="action-bar">
315
+ <button class="btn btn-primary" onclick={() => { currentView = 'contact'; }}>
316
+ Checkout <span class="arrow">&rarr;</span>
302
317
  </button>
303
318
  </div>
304
319
  {/if}
@@ -320,21 +335,6 @@
320
335
  }
321
336
 
322
337
  /* -- Cart view -- */
323
- .cart-header {
324
- display: flex;
325
- justify-content: space-between;
326
- align-items: center;
327
- margin-bottom: 20px;
328
- }
329
- .cart-title {
330
- font-size: 22px;
331
- font-weight: 800;
332
- }
333
- .cart-header-right {
334
- display: flex;
335
- align-items: center;
336
- gap: 12px;
337
- }
338
338
  .keep-shopping-btn {
339
339
  padding: 8px 16px;
340
340
  background: none;
@@ -443,23 +443,6 @@
443
443
  font-size: 20px;
444
444
  font-weight: 800;
445
445
  }
446
- .checkout-btn {
447
- display: block;
448
- width: 100%;
449
- padding: 16px;
450
- background: var(--bw-color-primary);
451
- color: white;
452
- border: none;
453
- border-radius: var(--bw-radius-md);
454
- font-size: 16px;
455
- font-weight: 700;
456
- cursor: pointer;
457
- text-align: center;
458
- }
459
- .checkout-btn:hover {
460
- background: var(--bw-color-primary-dark);
461
- }
462
-
463
446
  /* -- Contact view -- */
464
447
  .contact-header {
465
448
  display: flex;
@@ -11,11 +11,13 @@
11
11
  product: CheckoutProductDto;
12
12
  api: BookingApi;
13
13
  wizardPages?: WizardPages;
14
+ editPages?: WizardPages;
15
+ autoSelectSingleTimeSlot?: boolean;
14
16
  onBack: () => void;
15
17
  onClose: () => void;
16
18
  onUpdated: () => void;
17
19
  }
18
- let { cartItem, product, api, wizardPages, onBack, onClose, onUpdated }: Props = $props();
20
+ let { cartItem, product, api, wizardPages, editPages, autoSelectSingleTimeSlot = false, onBack, onClose, onUpdated }: Props = $props();
19
21
 
20
22
  let isSaving = $state(false);
21
23
 
@@ -55,7 +57,9 @@
55
57
  {product}
56
58
  {api}
57
59
  wizardPages={wizardPages ?? DEFAULT_WIZARD_PAGES}
60
+ editPages={editPages ?? wizardPages ?? DEFAULT_WIZARD_PAGES}
58
61
  editItem={cartItem}
62
+ {autoSelectSingleTimeSlot}
59
63
  {onComplete}
60
64
  onCancel={onBack}
61
65
  />
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { BookingApi } from './api';
3
3
  import type { WizardPages } from './config';
4
+ import { DEFAULT_WIZARD_PAGES } from './config';
4
5
  import type { CartItem, CheckoutProductDto } from './client-types';
5
6
  import { CartManager } from './cart-manager';
6
7
  import { postMessage } from './messages';
@@ -12,11 +13,11 @@
12
13
  api: BookingApi;
13
14
  cartManager: CartManager;
14
15
  productId: string;
15
- wizardPages: WizardPages;
16
+ wizardPages?: WizardPages;
16
17
  autoSelectSingleTimeSlot?: boolean;
17
18
  onCancel?: () => void;
18
19
  }
19
- let { api, cartManager, productId, wizardPages, autoSelectSingleTimeSlot = false, onCancel }: Props = $props();
20
+ let { api, cartManager, productId, wizardPages = DEFAULT_WIZARD_PAGES, autoSelectSingleTimeSlot = false, onCancel }: Props = $props();
20
21
 
21
22
  let product = $state<CheckoutProductDto | null>(null);
22
23
  let isLoading = $state(true);
@@ -56,23 +57,37 @@
56
57
  totalFormatted: total,
57
58
  openCheckout: true,
58
59
  });
60
+ // Separate from cart:change above - that one is per-item-added detail forwarded to consumers as the
61
+ // public bw:cart-change event. This one just tells CartBar/CartOverviewButton to refetch their own
62
+ // total, same signal update/remove in CheckoutModal use - see this event's own doc comment there.
63
+ postMessage({ type: 'cart:updated' });
59
64
  } finally {
60
65
  isAddingToCart = false;
61
66
  }
62
67
  }
63
68
  </script>
64
69
 
65
- {#if isLoading || isAddingToCart}
66
- <div class="loading-center" style="height:100vh">
67
- <div class="spinner"></div>
68
- </div>
69
- {:else if product}
70
- <WizardPage
71
- {product}
72
- {api}
73
- {wizardPages}
74
- {autoSelectSingleTimeSlot}
75
- {onCancel}
76
- onComplete={onAddToCart}
77
- />
78
- {/if}
70
+ <div class="bw-widget">
71
+ {#if isLoading || isAddingToCart}
72
+ <div class="loading-center" style="height:100vh">
73
+ <div class="spinner"></div>
74
+ </div>
75
+ {:else if product}
76
+ <WizardPage
77
+ {product}
78
+ {api}
79
+ {wizardPages}
80
+ {autoSelectSingleTimeSlot}
81
+ {onCancel}
82
+ onComplete={onAddToCart}
83
+ />
84
+ {/if}
85
+ </div>
86
+
87
+ <style>
88
+ /* display: contents - a plain box here would break WizardPage's own .wizard{height:100%}, which needs
89
+ to resolve against this component's real parent, not an unsized wrapper inserted in between. */
90
+ .bw-widget {
91
+ display: contents;
92
+ }
93
+ </style>