@code-collective/booking-widget 1.0.0
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 +266 -0
- package/dist/booking-widget.css +2 -0
- package/dist/booking-widget.js +4359 -0
- package/dist/booking-widget.umd.cjs +2 -0
- package/package.json +48 -0
- package/src/lib/AvailabilityCalendar.svelte +222 -0
- package/src/lib/CartBar.svelte +32 -0
- package/src/lib/CartBarView.svelte +60 -0
- package/src/lib/CartOverview.svelte +20 -0
- package/src/lib/CartOverviewButton.svelte +120 -0
- package/src/lib/Checkout.svelte +55 -0
- package/src/lib/CheckoutModal.svelte +510 -0
- package/src/lib/ConsentSection.svelte +45 -0
- package/src/lib/ContactForm.svelte +67 -0
- package/src/lib/CountdownTimer.svelte +30 -0
- package/src/lib/EditBookingView.svelte +73 -0
- package/src/lib/OptionCard.svelte +41 -0
- package/src/lib/PaymentPage.svelte +155 -0
- package/src/lib/PickupPointPicker.svelte +76 -0
- package/src/lib/ResultView.svelte +84 -0
- package/src/lib/SelectableCard.svelte +71 -0
- package/src/lib/TicketConfigurator.svelte +78 -0
- package/src/lib/TimeSlotPicker.svelte +45 -0
- package/src/lib/UnitCounter.svelte +91 -0
- package/src/lib/WizardPage.svelte +561 -0
- package/src/lib/api.ts +204 -0
- package/src/lib/app.css +246 -0
- package/src/lib/cart-manager.ts +76 -0
- package/src/lib/client-types.ts +52 -0
- package/src/lib/config.ts +92 -0
- package/src/lib/currency.ts +18 -0
- package/src/lib/elements/bw-cart.svelte +44 -0
- package/src/lib/elements/bw-checkout.svelte +99 -0
- package/src/lib/elements/bw-configurator.svelte +66 -0
- package/src/lib/elements/env.ts +11 -0
- package/src/lib/elements/register.ts +149 -0
- package/src/lib/elements/shared.ts +14 -0
- package/src/lib/elements/theme.css +193 -0
- package/src/lib/fake-api.ts +282 -0
- package/src/lib/generated-types.ts +828 -0
- package/src/lib/index.ts +189 -0
- package/src/lib/messages.ts +3 -0
- package/src/lib/mock-data.ts +241 -0
- package/src/lib/session-manager.ts +75 -0
- package/src/lib/utils.ts +25 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CheckoutAddCartItemDto,
|
|
3
|
+
CheckoutSessionDto,
|
|
4
|
+
CheckoutProductDto,
|
|
5
|
+
CheckoutAvailabilityCalendarDto,
|
|
6
|
+
CheckoutAvailabilityDto,
|
|
7
|
+
CheckoutCartResult,
|
|
8
|
+
CheckoutCartDetailDto,
|
|
9
|
+
CheckoutCartItemDetailDto,
|
|
10
|
+
CheckoutCartItemAddedDto,
|
|
11
|
+
CheckoutCartPaymentInitiationDto,
|
|
12
|
+
CheckoutCartConfirmResultDto,
|
|
13
|
+
CheckoutCartConfirmItemResultDto,
|
|
14
|
+
OctoContact,
|
|
15
|
+
} from './client-types';
|
|
16
|
+
import type { BookingApi } from './api';
|
|
17
|
+
import {
|
|
18
|
+
sampleProductData,
|
|
19
|
+
generateSampleAvailability,
|
|
20
|
+
generateSampleTimeSlots,
|
|
21
|
+
} from './mock-data';
|
|
22
|
+
|
|
23
|
+
function delay<T>(ms: number, fn: () => T): Promise<T> {
|
|
24
|
+
return new Promise((r) => setTimeout(() => r(fn()), ms));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function uuid(): string {
|
|
28
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
29
|
+
const r = (Math.random() * 16) | 0;
|
|
30
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
31
|
+
return v.toString(16);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FakeCartItem {
|
|
36
|
+
id: string;
|
|
37
|
+
bookingUuid: string;
|
|
38
|
+
request: CheckoutAddCartItemDto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CART_STORAGE_KEY = 'morii-checkout-cart';
|
|
42
|
+
const ITEMS_STORAGE_KEY = 'morii-checkout-cart-items';
|
|
43
|
+
|
|
44
|
+
export class FakeApiClient implements BookingApi {
|
|
45
|
+
sessionToken = '';
|
|
46
|
+
private cartToken = '';
|
|
47
|
+
private cartItems: FakeCartItem[] = [];
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
this.loadCartFromStorage() || this.seedCart();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private seedCart(): void {
|
|
54
|
+
this.cartToken = `fake-cart-seeded`;
|
|
55
|
+
|
|
56
|
+
this.cartItems = [
|
|
57
|
+
{
|
|
58
|
+
id: '00000000-0000-0000-0000-000000000001',
|
|
59
|
+
bookingUuid: '00000000-0000-0000-0000-aaaaaaaaaaaa',
|
|
60
|
+
request: {
|
|
61
|
+
productId: sampleProductData.id,
|
|
62
|
+
optionId: 'option_blue_tram',
|
|
63
|
+
unitItems: [{ unitId: 'unit_adult' }, { unitId: 'unit_adult' }, { unitId: 'unit_child' }],
|
|
64
|
+
availabilityId: 'avail_seeded',
|
|
65
|
+
amount: 202500,
|
|
66
|
+
currencyCode: 'ZAR',
|
|
67
|
+
currencyPrecision: 2,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
this.saveCartToStorage();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private saveCartToStorage(): void {
|
|
76
|
+
try {
|
|
77
|
+
const now = new Date();
|
|
78
|
+
const absolute = new Date(now.getTime() + 60 * 60_000);
|
|
79
|
+
localStorage.setItem(CART_STORAGE_KEY, JSON.stringify({
|
|
80
|
+
cartToken: this.cartToken,
|
|
81
|
+
absoluteExpiresAt: absolute.toISOString(),
|
|
82
|
+
}));
|
|
83
|
+
localStorage.setItem(ITEMS_STORAGE_KEY, JSON.stringify(this.cartItems));
|
|
84
|
+
} catch {
|
|
85
|
+
// localStorage unavailable
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private loadCartFromStorage(): boolean {
|
|
90
|
+
try {
|
|
91
|
+
const cartRaw = localStorage.getItem(CART_STORAGE_KEY);
|
|
92
|
+
const itemsRaw = localStorage.getItem(ITEMS_STORAGE_KEY);
|
|
93
|
+
if (!cartRaw || !itemsRaw) return false;
|
|
94
|
+
|
|
95
|
+
const cart = JSON.parse(cartRaw);
|
|
96
|
+
if (new Date(cart.absoluteExpiresAt).getTime() <= Date.now()) return false;
|
|
97
|
+
|
|
98
|
+
this.cartToken = cart.cartToken;
|
|
99
|
+
this.cartItems = JSON.parse(itemsRaw);
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Session ---
|
|
107
|
+
|
|
108
|
+
async startSession(_checkoutKey: string): Promise<CheckoutSessionDto> {
|
|
109
|
+
return delay(300, () => {
|
|
110
|
+
const now = new Date();
|
|
111
|
+
const expires = new Date(now.getTime() + 30 * 60_000);
|
|
112
|
+
const refresh = new Date(now.getTime() + 20 * 60_000);
|
|
113
|
+
this.sessionToken = `fake-session-${uuid().slice(0, 8)}`;
|
|
114
|
+
const session: CheckoutSessionDto = {
|
|
115
|
+
sessionToken: this.sessionToken,
|
|
116
|
+
issuedAt: now.toISOString(),
|
|
117
|
+
expiresAt: expires.toISOString(),
|
|
118
|
+
refreshAfter: refresh.toISOString(),
|
|
119
|
+
octo: {
|
|
120
|
+
basePath: '/v1/checkout/octo',
|
|
121
|
+
capabilities: 'pricing,content,pickups',
|
|
122
|
+
},
|
|
123
|
+
supplier: {
|
|
124
|
+
name: 'Franschhoek Wine Tram',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
return session;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async refreshSession(): Promise<CheckoutSessionDto> {
|
|
132
|
+
return this.startSession('');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --- Catalog ---
|
|
136
|
+
|
|
137
|
+
async getProduct(productId: string): Promise<CheckoutProductDto> {
|
|
138
|
+
return delay(500, () => ({ ...sampleProductData, id: productId }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async getAvailabilityCalendar(
|
|
142
|
+
_productId: string,
|
|
143
|
+
_optionId: string,
|
|
144
|
+
): Promise<Record<string, CheckoutAvailabilityCalendarDto>> {
|
|
145
|
+
return delay(600, () => generateSampleAvailability());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getAvailability(
|
|
149
|
+
_productId: string,
|
|
150
|
+
_optionId: string,
|
|
151
|
+
localDate: string,
|
|
152
|
+
): Promise<CheckoutAvailabilityDto[]> {
|
|
153
|
+
return delay(400, () => {
|
|
154
|
+
const parts = localDate.split('-').map(Number);
|
|
155
|
+
const date = new Date(parts[0], parts[1] - 1, parts[2]);
|
|
156
|
+
return generateSampleTimeSlots(date);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- Cart ---
|
|
161
|
+
|
|
162
|
+
async createCart(): Promise<CheckoutCartResult> {
|
|
163
|
+
return delay(300, () => {
|
|
164
|
+
const now = new Date();
|
|
165
|
+
const idle = new Date(now.getTime() + 15 * 60_000);
|
|
166
|
+
const absolute = new Date(now.getTime() + 60 * 60_000);
|
|
167
|
+
|
|
168
|
+
if (this.cartToken && this.cartItems.length > 0) {
|
|
169
|
+
return {
|
|
170
|
+
cartToken: this.cartToken,
|
|
171
|
+
issuedAt: now.toISOString(),
|
|
172
|
+
idleExpiresAt: idle.toISOString(),
|
|
173
|
+
absoluteExpiresAt: absolute.toISOString(),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.cartToken = `fake-cart-${uuid().slice(0, 8)}`;
|
|
178
|
+
this.cartItems = [];
|
|
179
|
+
this.saveCartToStorage();
|
|
180
|
+
return {
|
|
181
|
+
cartToken: this.cartToken,
|
|
182
|
+
issuedAt: now.toISOString(),
|
|
183
|
+
idleExpiresAt: idle.toISOString(),
|
|
184
|
+
absoluteExpiresAt: absolute.toISOString(),
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getCart(): Promise<CheckoutCartDetailDto> {
|
|
190
|
+
return delay(300, () => {
|
|
191
|
+
const now = new Date();
|
|
192
|
+
const idle = new Date(now.getTime() + 15 * 60_000);
|
|
193
|
+
const absolute = new Date(now.getTime() + 60 * 60_000);
|
|
194
|
+
const detail: CheckoutCartDetailDto = {
|
|
195
|
+
cartToken: this.cartToken,
|
|
196
|
+
issuedAt: now.toISOString(),
|
|
197
|
+
idleExpiresAt: idle.toISOString(),
|
|
198
|
+
absoluteExpiresAt: absolute.toISOString(),
|
|
199
|
+
items: this.cartItems.map((item): CheckoutCartItemDetailDto => ({
|
|
200
|
+
id: item.id,
|
|
201
|
+
bookingUuid: item.bookingUuid,
|
|
202
|
+
productId: item.request.productId,
|
|
203
|
+
optionId: item.request.optionId,
|
|
204
|
+
unitItems: item.request.unitItems,
|
|
205
|
+
availabilityId: item.request.availabilityId,
|
|
206
|
+
localDate: item.request.localDate,
|
|
207
|
+
pickupPointId: item.request.pickupPointId,
|
|
208
|
+
amount: item.request.amount,
|
|
209
|
+
currencyCode: item.request.currencyCode,
|
|
210
|
+
currencyPrecision: item.request.currencyPrecision,
|
|
211
|
+
})),
|
|
212
|
+
};
|
|
213
|
+
return detail;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async addCartItem(request: CheckoutAddCartItemDto): Promise<CheckoutCartItemAddedDto> {
|
|
218
|
+
return delay(400, () => {
|
|
219
|
+
const item: FakeCartItem = {
|
|
220
|
+
id: uuid(),
|
|
221
|
+
bookingUuid: uuid(),
|
|
222
|
+
request,
|
|
223
|
+
};
|
|
224
|
+
this.cartItems.push(item);
|
|
225
|
+
this.saveCartToStorage();
|
|
226
|
+
return { id: item.id, bookingUuid: item.bookingUuid };
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async updateCartItem(itemId: string, request: CheckoutAddCartItemDto): Promise<CheckoutCartItemAddedDto> {
|
|
231
|
+
return delay(400, () => {
|
|
232
|
+
const existing = this.cartItems.find((i) => i.id === itemId);
|
|
233
|
+
if (existing) {
|
|
234
|
+
existing.request = request;
|
|
235
|
+
this.saveCartToStorage();
|
|
236
|
+
return { id: existing.id, bookingUuid: existing.bookingUuid };
|
|
237
|
+
}
|
|
238
|
+
const item: FakeCartItem = { id: itemId, bookingUuid: uuid(), request };
|
|
239
|
+
this.cartItems.push(item);
|
|
240
|
+
this.saveCartToStorage();
|
|
241
|
+
return { id: item.id, bookingUuid: item.bookingUuid };
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async removeCartItem(itemId: string): Promise<void> {
|
|
246
|
+
return delay(200, () => {
|
|
247
|
+
this.cartItems = this.cartItems.filter((i) => i.id !== itemId);
|
|
248
|
+
this.saveCartToStorage();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async payCart(_contact: OctoContact): Promise<CheckoutCartPaymentInitiationDto> {
|
|
253
|
+
return delay(800, () => ({
|
|
254
|
+
checkoutId: `fake-checkout-${uuid().slice(0, 8)}`,
|
|
255
|
+
redirectUrl: '',
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async confirmCart(): Promise<CheckoutCartConfirmResultDto> {
|
|
260
|
+
return delay(700, () => {
|
|
261
|
+
const result: CheckoutCartConfirmResultDto = {
|
|
262
|
+
items: this.cartItems.map((item): CheckoutCartConfirmItemResultDto => ({
|
|
263
|
+
bookingUuid: item.bookingUuid,
|
|
264
|
+
statusCode: 200,
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
id: `booking-${item.bookingUuid.slice(0, 8)}`,
|
|
267
|
+
uuid: item.bookingUuid,
|
|
268
|
+
status: 'CONFIRMED',
|
|
269
|
+
productId: item.request.productId,
|
|
270
|
+
optionId: item.request.optionId,
|
|
271
|
+
unitItems: (item.request.unitItems ?? []).map((u) => ({
|
|
272
|
+
uuid: uuid(),
|
|
273
|
+
unitId: u.unitId,
|
|
274
|
+
status: 'CONFIRMED',
|
|
275
|
+
})),
|
|
276
|
+
}),
|
|
277
|
+
})),
|
|
278
|
+
};
|
|
279
|
+
return result;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|