@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.
- package/README.md +353 -282
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +1219 -885
- package/dist/booking-widget.min.css +1 -1
- package/dist/booking-widget.min.js +9 -6
- package/dist/booking-widget.umd.cjs +2 -2
- package/package.json +1 -1
- package/src/lib/CartBar.svelte +14 -0
- package/src/lib/CartBarView.svelte +1 -1
- package/src/lib/CartOverviewButton.svelte +15 -1
- package/src/lib/Checkout.svelte +42 -27
- package/src/lib/CheckoutModal.svelte +26 -43
- package/src/lib/EditBookingView.svelte +5 -1
- package/src/lib/TicketConfigurator.svelte +31 -16
- package/src/lib/WizardPage.svelte +371 -96
- package/src/lib/api.ts +2 -1
- package/src/lib/app.css +12 -4
- package/src/lib/cart-manager.ts +1 -0
- 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/fake-api.ts +1 -1
- package/src/lib/index.ts +6 -1
package/src/lib/Checkout.svelte
CHANGED
|
@@ -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
|
-
|
|
31
|
-
|
|
32
|
-
<div class="
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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="
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
301
|
-
|
|
314
|
+
<div class="action-bar">
|
|
315
|
+
<button class="btn btn-primary" onclick={() => { currentView = 'contact'; }}>
|
|
316
|
+
Checkout <span class="arrow">→</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
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
<div class="
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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>
|