@ticketlayer/elements 0.3.3 → 0.4.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 +3 -2
- package/dist/cjs/{index-BpfoKd9Q.js → index-I9CoYv3k.js} +1 -1
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/money-BtmM2gt-.js +118 -0
- package/dist/cjs/{order-summary-ZMkJ5bez.js → order-summary-X3TlZxc2.js} +34 -0
- package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
- package/dist/cjs/tl-buy-modal.cjs.entry.js +3 -3
- package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +10 -7
- package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +2 -2
- package/dist/cjs/tl-cart-badge.cjs.entry.js +2 -2
- package/dist/cjs/tl-cart-drawer.cjs.entry.js +3 -3
- package/dist/cjs/tl-cart.cjs.entry.js +10 -4
- package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +2 -2
- package/dist/cjs/tl-event-card.cjs.entry.js +4 -3
- package/dist/cjs/tl-event-list.cjs.entry.js +2 -2
- package/dist/cjs/tl-login.cjs.entry.js +4 -3
- package/dist/cjs/tl-my-orders.cjs.entry.js +4 -3
- package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +454 -122
- package/dist/cjs/tl-order-confirmation.cjs.entry.js +11 -8
- package/dist/cjs/tl-order-tickets_2.cjs.entry.js +3 -3
- package/dist/cjs/tl-promo-code.cjs.entry.js +4 -3
- package/dist/cjs/tl-seat-map.cjs.entry.js +2 -2
- package/dist/collection/components/tl-buy-modal/tl-buy-modal.css +12 -19
- package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +1 -1
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +4 -4
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +8 -5
- package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.css +2 -2
- package/dist/collection/components/tl-cart/tl-cart.css +52 -51
- package/dist/collection/components/tl-cart/tl-cart.js +8 -2
- package/dist/collection/components/tl-cart-badge/tl-cart-badge.css +11 -11
- package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.css +6 -6
- package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +1 -1
- package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.css +2 -2
- package/dist/collection/components/tl-event-card/tl-event-card.css +22 -22
- package/dist/collection/components/tl-event-list/tl-event-list.css +17 -17
- package/dist/collection/components/tl-login/tl-login.css +26 -26
- package/dist/collection/components/tl-my-orders/tl-my-orders.css +40 -40
- package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.css +54 -65
- package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.css +38 -38
- package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +8 -6
- package/dist/collection/components/tl-order-tickets/tl-order-tickets.css +9 -9
- package/dist/collection/components/tl-promo-code/tl-promo-code.css +25 -25
- package/dist/collection/components/tl-seat-map/tl-seat-map.css +10 -10
- package/dist/collection/components/tl-ticket/tl-ticket.css +13 -13
- package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.css +74 -49
- package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +72 -126
- package/dist/collection/utils/cart-totals.js +9 -0
- package/dist/collection/utils/order-summary.js +36 -0
- package/dist/components/money.js +1 -0
- package/dist/components/order-summary.js +1 -1
- package/dist/components/tl-buy-modal.js +1 -1
- package/dist/components/tl-buy-tickets-button.js +1 -1
- package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
- package/dist/components/tl-cart-badge.js +1 -1
- package/dist/components/tl-cart-drawer.js +1 -1
- package/dist/components/tl-cart2.js +1 -1
- package/dist/components/tl-checkout-wrapper2.js +1 -1
- package/dist/components/tl-event-card2.js +1 -1
- package/dist/components/tl-event-list.js +1 -1
- package/dist/components/tl-login2.js +1 -1
- package/dist/components/tl-my-orders.js +1 -1
- package/dist/components/tl-occurrence-selector2.js +1 -1
- package/dist/components/tl-order-confirmation.js +1 -1
- package/dist/components/tl-order-tickets2.js +1 -1
- package/dist/components/tl-promo-code.js +1 -1
- package/dist/components/tl-seat-map.js +1 -1
- package/dist/components/tl-ticket-selector2.js +1 -1
- package/dist/components/tl-ticket2.js +1 -1
- package/dist/elements.docs.json +4 -4
- package/dist/esm/{index-CNX8hZa8.js → index-OoYV1faY.js} +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/loader.js +3 -3
- package/dist/esm/money-4ig0-WB6.js +112 -0
- package/dist/esm/{order-summary-BGekdjN9.js → order-summary-DiDtvfZM.js} +34 -1
- package/dist/esm/ticketlayer-elements.js +3 -3
- package/dist/esm/tl-buy-modal.entry.js +3 -3
- package/dist/esm/tl-buy-tickets-button.entry.js +10 -7
- package/dist/esm/tl-buy-tickets-wrapper.entry.js +2 -2
- package/dist/esm/tl-cart-badge.entry.js +2 -2
- package/dist/esm/tl-cart-drawer.entry.js +3 -3
- package/dist/esm/tl-cart.entry.js +10 -4
- package/dist/esm/tl-checkout-wrapper.entry.js +2 -2
- package/dist/esm/tl-event-card.entry.js +4 -3
- package/dist/esm/tl-event-list.entry.js +2 -2
- package/dist/esm/tl-login.entry.js +4 -3
- package/dist/esm/tl-my-orders.entry.js +4 -3
- package/dist/esm/tl-occurrence-selector_2.entry.js +455 -123
- package/dist/esm/tl-order-confirmation.entry.js +11 -8
- package/dist/esm/tl-order-tickets_2.entry.js +3 -3
- package/dist/esm/tl-promo-code.entry.js +4 -3
- package/dist/esm/tl-seat-map.entry.js +2 -2
- package/dist/ticketlayer-elements/index.esm.js +1 -1
- package/dist/ticketlayer-elements/p-0814420f.entry.js +1 -0
- package/dist/ticketlayer-elements/p-0acabc26.entry.js +1 -0
- package/dist/ticketlayer-elements/{p-9629029a.entry.js → p-109a3feb.entry.js} +1 -1
- package/dist/ticketlayer-elements/{p-e02079fa.entry.js → p-1a4ba71c.entry.js} +1 -1
- package/dist/ticketlayer-elements/p-318965c1.entry.js +1 -0
- package/dist/ticketlayer-elements/p-347c7fd1.entry.js +1 -0
- package/dist/ticketlayer-elements/p-3649c26d.entry.js +1 -0
- package/dist/ticketlayer-elements/p-40260cc9.entry.js +1 -0
- package/dist/ticketlayer-elements/p-4ig0-WB6.js +1 -0
- package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +1 -0
- package/dist/ticketlayer-elements/p-62ee659b.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7f0abf95.entry.js +1 -0
- package/dist/ticketlayer-elements/p-BXpVGW51.js +1 -0
- package/dist/ticketlayer-elements/p-OoYV1faY.js +3 -0
- package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +1 -0
- package/dist/ticketlayer-elements/{p-1c51cbc1.entry.js → p-ac264adc.entry.js} +1 -1
- package/dist/ticketlayer-elements/{p-895ba1f3.entry.js → p-aeb4c6fb.entry.js} +1 -1
- package/dist/ticketlayer-elements/{p-fa639d88.entry.js → p-ba62541f.entry.js} +1 -1
- package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
- package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
- package/dist/types/components/tl-cart/tl-cart.d.ts +10 -0
- package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +40 -29
- package/dist/types/components.d.ts +32 -0
- package/dist/types/utils/cart-totals.d.ts +10 -0
- package/dist/types/utils/order-summary.d.ts +34 -0
- package/elements.catalogue.json +91 -22
- package/package.json +12 -6
- package/dist/ticketlayer-elements/p-0b1d16f8.entry.js +0 -1
- package/dist/ticketlayer-elements/p-1471f6d8.entry.js +0 -1
- package/dist/ticketlayer-elements/p-5169f68b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-8d5722e1.entry.js +0 -1
- package/dist/ticketlayer-elements/p-917d8e09.entry.js +0 -1
- package/dist/ticketlayer-elements/p-BGekdjN9.js +0 -1
- package/dist/ticketlayer-elements/p-CNX8hZa8.js +0 -3
- package/dist/ticketlayer-elements/p-a22714d0.entry.js +0 -1
- package/dist/ticketlayer-elements/p-bf73044b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-e04ace15.entry.js +0 -1
- package/dist/ticketlayer-elements/p-e6fab1d5.entry.js +0 -1
- package/dist/ticketlayer-elements/p-f826b753.entry.js +0 -1
- package/dist/ticketlayer-elements/p-fbaca948.entry.js +0 -1
|
@@ -1,7 +1,398 @@
|
|
|
1
|
-
import { r as registerInstance, c as createEvent, h } from './index-
|
|
2
|
-
import { w as whenSDK, g as getSDK
|
|
1
|
+
import { r as registerInstance, c as createEvent, h } from './index-OoYV1faY.js';
|
|
2
|
+
import { w as whenSDK, g as getSDK } from './sdk-connector-CqhvfyKV.js';
|
|
3
|
+
import { s as selectionTotals } from './money-4ig0-WB6.js';
|
|
3
4
|
|
|
4
|
-
const
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'discovery',
|
|
7
|
+
'purchase',
|
|
8
|
+
'cart',
|
|
9
|
+
'checkout',
|
|
10
|
+
'account',
|
|
11
|
+
'tickets',
|
|
12
|
+
];
|
|
13
|
+
const RUNTIMES = ['web', 'native'];
|
|
14
|
+
/**
|
|
15
|
+
* Declare a core. The checks here are the ones a type cannot make: a tag that
|
|
16
|
+
* is not a `tl-` custom element name, a category or runtime outside the set the
|
|
17
|
+
* catalogue knows, a duplicated token, a status list that does not include what
|
|
18
|
+
* `status` can return. They run at import time, so a malformed core fails the
|
|
19
|
+
* build rather than a browser.
|
|
20
|
+
*/
|
|
21
|
+
function defineElement(core) {
|
|
22
|
+
const fail = (message) => {
|
|
23
|
+
throw new Error(`[elements-core] ${core.tag || '<no tag>'}: ${message}`);
|
|
24
|
+
};
|
|
25
|
+
if (!/^tl-[a-z0-9]+(-[a-z0-9]+)*$/.test(core.tag))
|
|
26
|
+
fail('tag must look like tl-something');
|
|
27
|
+
if (!CATEGORIES.includes(core.category))
|
|
28
|
+
fail(`category must be one of ${CATEGORIES.join(', ')}`);
|
|
29
|
+
if (!core.runtimes.length || core.runtimes.some((r) => !RUNTIMES.includes(r))) {
|
|
30
|
+
fail(`runtimes must be a non-empty subset of ${RUNTIMES.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
if (new Set(core.tokens).size !== core.tokens.length)
|
|
33
|
+
fail('tokens must not repeat');
|
|
34
|
+
if (!core.statuses.length)
|
|
35
|
+
fail('statuses must not be empty');
|
|
36
|
+
for (const [name, spec] of Object.entries(core.events)) {
|
|
37
|
+
if (!/^tl[A-Z]/.test(name))
|
|
38
|
+
fail(`event ${name} must be named tlSomething`);
|
|
39
|
+
if (!spec.detail)
|
|
40
|
+
fail(`event ${name} must state its detail type`);
|
|
41
|
+
}
|
|
42
|
+
for (const [name, spec] of Object.entries(core.props)) {
|
|
43
|
+
if (spec.required && spec.default !== undefined)
|
|
44
|
+
fail(`prop ${name} is required and has a default`);
|
|
45
|
+
}
|
|
46
|
+
return Object.freeze({ ...core });
|
|
47
|
+
}
|
|
48
|
+
const DEFAULT_LOCALE = 'en-GB';
|
|
49
|
+
const DEFAULT_CURRENCY = 'GBP';
|
|
50
|
+
/**
|
|
51
|
+
* Instantiate a core. The instance owns the state and the timers; the harness
|
|
52
|
+
* owns the pixels. `subscribe` fires after every change with the whole view, so
|
|
53
|
+
* a harness only has to assign it to whatever makes its runtime re-render.
|
|
54
|
+
*/
|
|
55
|
+
function createElement(core, options = {}) {
|
|
56
|
+
const defaults = {};
|
|
57
|
+
for (const [name, spec] of Object.entries(core.props)) {
|
|
58
|
+
if (spec.default !== undefined)
|
|
59
|
+
defaults[name] = spec.default;
|
|
60
|
+
}
|
|
61
|
+
let props = { ...defaults, ...(options.props ?? {}) };
|
|
62
|
+
const strings = { ...core.strings, ...(options.strings ?? {}) };
|
|
63
|
+
const locale = options.locale ?? DEFAULT_LOCALE;
|
|
64
|
+
const fallbackCurrency = options.currency ?? DEFAULT_CURRENCY;
|
|
65
|
+
let resolveClient = toResolver(options.client);
|
|
66
|
+
let state = core.state(props);
|
|
67
|
+
let destroyed = false;
|
|
68
|
+
const timers = new Set();
|
|
69
|
+
const listeners = new Set();
|
|
70
|
+
const formatMoney = (amountInMinorUnits, currency) => new Intl.NumberFormat(locale, {
|
|
71
|
+
style: 'currency',
|
|
72
|
+
currency: currency || fallbackCurrency,
|
|
73
|
+
}).format(amountInMinorUnits / 100);
|
|
74
|
+
const viewContext = () => ({ props, state, strings, formatMoney });
|
|
75
|
+
const buildView = () => {
|
|
76
|
+
const ctx = viewContext();
|
|
77
|
+
return {
|
|
78
|
+
status: core.status(ctx),
|
|
79
|
+
props,
|
|
80
|
+
state,
|
|
81
|
+
strings,
|
|
82
|
+
derived: core.derive(ctx),
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const notify = () => {
|
|
86
|
+
if (destroyed)
|
|
87
|
+
return;
|
|
88
|
+
const view = buildView();
|
|
89
|
+
for (const listener of [...listeners])
|
|
90
|
+
listener(view);
|
|
91
|
+
};
|
|
92
|
+
const actionContext = {
|
|
93
|
+
get props() {
|
|
94
|
+
return props;
|
|
95
|
+
},
|
|
96
|
+
get state() {
|
|
97
|
+
return state;
|
|
98
|
+
},
|
|
99
|
+
strings,
|
|
100
|
+
formatMoney,
|
|
101
|
+
setState(patch) {
|
|
102
|
+
if (destroyed)
|
|
103
|
+
return;
|
|
104
|
+
const next = typeof patch === 'function' ? patch(state) : patch;
|
|
105
|
+
state = { ...state, ...next };
|
|
106
|
+
notify();
|
|
107
|
+
},
|
|
108
|
+
emit(event, detail) {
|
|
109
|
+
if (!core.events[event]) {
|
|
110
|
+
throw new Error(`[elements-core] ${core.tag}: ${event} is not a declared event`);
|
|
111
|
+
}
|
|
112
|
+
options.emit?.(event, detail);
|
|
113
|
+
},
|
|
114
|
+
client() {
|
|
115
|
+
const client = resolveClient();
|
|
116
|
+
if (!client) {
|
|
117
|
+
throw new Error(`[elements-core] ${core.tag}: no client. A harness hands the core the client the host published.`);
|
|
118
|
+
}
|
|
119
|
+
return client;
|
|
120
|
+
},
|
|
121
|
+
optionalClient() {
|
|
122
|
+
try {
|
|
123
|
+
return resolveClient();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
after(ms, run) {
|
|
130
|
+
if (destroyed)
|
|
131
|
+
return;
|
|
132
|
+
const handle = setTimeout(() => {
|
|
133
|
+
timers.delete(handle);
|
|
134
|
+
if (!destroyed)
|
|
135
|
+
run();
|
|
136
|
+
}, ms);
|
|
137
|
+
timers.add(handle);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
const actions = core.actions(actionContext);
|
|
141
|
+
return {
|
|
142
|
+
core,
|
|
143
|
+
get props() {
|
|
144
|
+
return props;
|
|
145
|
+
},
|
|
146
|
+
get state() {
|
|
147
|
+
return state;
|
|
148
|
+
},
|
|
149
|
+
get view() {
|
|
150
|
+
return buildView();
|
|
151
|
+
},
|
|
152
|
+
actions,
|
|
153
|
+
setProps(next) {
|
|
154
|
+
if (destroyed)
|
|
155
|
+
return;
|
|
156
|
+
props = { ...props, ...next };
|
|
157
|
+
notify();
|
|
158
|
+
},
|
|
159
|
+
setClient(client) {
|
|
160
|
+
resolveClient = toResolver(client);
|
|
161
|
+
},
|
|
162
|
+
subscribe(listener) {
|
|
163
|
+
listeners.add(listener);
|
|
164
|
+
return () => {
|
|
165
|
+
listeners.delete(listener);
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
destroy() {
|
|
169
|
+
destroyed = true;
|
|
170
|
+
for (const handle of timers)
|
|
171
|
+
clearTimeout(handle);
|
|
172
|
+
timers.clear();
|
|
173
|
+
listeners.clear();
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function toResolver(client) {
|
|
178
|
+
if (typeof client === 'function')
|
|
179
|
+
return client;
|
|
180
|
+
return () => client ?? null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `tl-ticket-selector`: the reference core.
|
|
185
|
+
*
|
|
186
|
+
* This is the whole of what the element does. The Stencil harness in
|
|
187
|
+
* `packages/elements/src/components/tl-ticket-selector/` renders `view` and
|
|
188
|
+
* forwards the three `tl*` events; it decides nothing, holds no copy and does
|
|
189
|
+
* no arithmetic. A React Native harness will render the same view.
|
|
190
|
+
*
|
|
191
|
+
* Behaviour is the behaviour the Stencil component had before the split, to
|
|
192
|
+
* the letter: the same quantity clamping, the same reset after a successful
|
|
193
|
+
* add, the same three second success message, the same button text, and the
|
|
194
|
+
* same rule that no fee row is shown until the surface quotes a fee. The one
|
|
195
|
+
* thing that did not come across is the `console.log` tracing, which a library
|
|
196
|
+
* should not do for its host.
|
|
197
|
+
*/
|
|
198
|
+
const STRINGS = {
|
|
199
|
+
selectTickets: 'Select Tickets',
|
|
200
|
+
adding: 'Adding...',
|
|
201
|
+
added: 'Added to cart!',
|
|
202
|
+
limited: 'Limited',
|
|
203
|
+
soldOut: 'Sold Out',
|
|
204
|
+
ticketsRow: 'Tickets',
|
|
205
|
+
totalRow: 'Total',
|
|
206
|
+
decrease: 'Decrease quantity',
|
|
207
|
+
increase: 'Increase quantity',
|
|
208
|
+
loadFailed: 'Failed to load tickets',
|
|
209
|
+
addFailed: 'Failed to add to cart',
|
|
210
|
+
noClient: 'SDK not initialized.',
|
|
211
|
+
};
|
|
212
|
+
/** How long the "Added to cart!" message stays up. */
|
|
213
|
+
const SUCCESS_MS = 3000;
|
|
214
|
+
const totalsOf = (ctx) => selectionTotals(ctx.state.ticketTypes.map((tt) => ({ price: tt.price, quantity: ctx.state.quantities[tt.id] || 0 })), ctx.state.feeQuote);
|
|
215
|
+
const totalItemsOf = (state) => Object.values(state.quantities).reduce((sum, qty) => sum + qty, 0);
|
|
216
|
+
const ticketSelectorCore = defineElement({
|
|
217
|
+
tag: 'tl-ticket-selector',
|
|
218
|
+
docs: 'Pick quantities of the ticket types on sale for one occurrence, and add them to the cart.',
|
|
219
|
+
category: 'purchase',
|
|
220
|
+
runtimes: ['web', 'native'],
|
|
221
|
+
tokens: [
|
|
222
|
+
'background',
|
|
223
|
+
'border',
|
|
224
|
+
'border-hover',
|
|
225
|
+
'error',
|
|
226
|
+
'error-bg',
|
|
227
|
+
'font-family',
|
|
228
|
+
'foreground',
|
|
229
|
+
'muted',
|
|
230
|
+
'muted-foreground',
|
|
231
|
+
'primary',
|
|
232
|
+
'primary-foreground',
|
|
233
|
+
'radius-md',
|
|
234
|
+
'radius-sm',
|
|
235
|
+
'spacing-1',
|
|
236
|
+
'spacing-2',
|
|
237
|
+
'spacing-3',
|
|
238
|
+
'spacing-4',
|
|
239
|
+
'success',
|
|
240
|
+
'warning',
|
|
241
|
+
'warning-bg',
|
|
242
|
+
],
|
|
243
|
+
sdkMethods: ['cart.addItem', 'events.getTicketTypes'],
|
|
244
|
+
props: {
|
|
245
|
+
eventId: { type: 'string', attr: 'event-id', required: true, docs: 'Event ID' },
|
|
246
|
+
occurrenceId: { type: 'string', attr: 'occurrence-id', required: true, docs: 'Occurrence ID' },
|
|
247
|
+
showButton: { type: 'boolean', attr: 'show-button', default: true, docs: 'Show add to cart button' },
|
|
248
|
+
buttonLabel: { type: 'string', attr: 'button-label', default: 'Add to Cart', docs: 'Button label' },
|
|
249
|
+
},
|
|
250
|
+
events: {
|
|
251
|
+
tlAddToCart: {
|
|
252
|
+
detail: '{ items: AddToCartItem[]; }',
|
|
253
|
+
docs: 'Emitted when items are added to cart',
|
|
254
|
+
},
|
|
255
|
+
tlQuantityChange: {
|
|
256
|
+
detail: '{ ticketTypeId: string; quantity: number; total: number; }',
|
|
257
|
+
docs: 'Emitted when quantity changes',
|
|
258
|
+
},
|
|
259
|
+
tlError: {
|
|
260
|
+
detail: '{ message: string; }',
|
|
261
|
+
docs: 'Emitted on error',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
strings: STRINGS,
|
|
265
|
+
statuses: ['loading', 'error', 'ready'],
|
|
266
|
+
state: () => ({
|
|
267
|
+
ticketTypes: [],
|
|
268
|
+
quantities: {},
|
|
269
|
+
loading: true,
|
|
270
|
+
error: null,
|
|
271
|
+
adding: false,
|
|
272
|
+
addSuccess: false,
|
|
273
|
+
feeQuote: null,
|
|
274
|
+
}),
|
|
275
|
+
status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
|
|
276
|
+
derive: (ctx) => {
|
|
277
|
+
const { state, strings, props, formatMoney } = ctx;
|
|
278
|
+
const totals = totalsOf(ctx);
|
|
279
|
+
const totalItems = totalItemsOf(state);
|
|
280
|
+
const currency = state.ticketTypes[0]?.currency;
|
|
281
|
+
const rows = state.ticketTypes.map((tt) => {
|
|
282
|
+
const quantity = state.quantities[tt.id] || 0;
|
|
283
|
+
const soldOut = tt.availabilityStatus === 'sold_out';
|
|
284
|
+
return {
|
|
285
|
+
id: tt.id,
|
|
286
|
+
name: tt.name,
|
|
287
|
+
description: tt.description ?? null,
|
|
288
|
+
priceLabel: formatMoney(tt.price, tt.currency),
|
|
289
|
+
quantity,
|
|
290
|
+
soldOut,
|
|
291
|
+
limited: tt.availabilityStatus === 'limited',
|
|
292
|
+
canDecrease: !soldOut && quantity > 0,
|
|
293
|
+
canIncrease: !soldOut && quantity < tt.maxPerOrder,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
rows,
|
|
298
|
+
totals,
|
|
299
|
+
totalItems,
|
|
300
|
+
currency,
|
|
301
|
+
subtotalLabel: formatMoney(totals.subtotal, currency),
|
|
302
|
+
feeLabel: formatMoney(totals.fees, currency),
|
|
303
|
+
totalLabel: formatMoney(totals.total, currency),
|
|
304
|
+
showTotals: totals.showFee && totalItems > 0,
|
|
305
|
+
actionLabel: state.adding
|
|
306
|
+
? strings.adding
|
|
307
|
+
: totalItems === 0
|
|
308
|
+
? strings.selectTickets
|
|
309
|
+
: `${props.buttonLabel} - ${formatMoney(totals.total)}`,
|
|
310
|
+
actionDisabled: totalItems === 0 || state.adding,
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
actions: (ctx) => ({
|
|
314
|
+
/** Read the ticket types for the current event and occurrence. */
|
|
315
|
+
async load() {
|
|
316
|
+
try {
|
|
317
|
+
ctx.setState({ loading: true, error: null });
|
|
318
|
+
const types = (await ctx.client().getEventsManager().getTicketTypes(ctx.props.eventId, ctx.props.occurrenceId));
|
|
319
|
+
const quantities = {};
|
|
320
|
+
for (const type of types)
|
|
321
|
+
quantities[type.id] = 0;
|
|
322
|
+
ctx.setState({ ticketTypes: types, quantities });
|
|
323
|
+
}
|
|
324
|
+
catch (err) {
|
|
325
|
+
const message = err instanceof Error ? err.message : ctx.strings.loadFailed;
|
|
326
|
+
ctx.setState({ error: message });
|
|
327
|
+
ctx.emit('tlError', { message });
|
|
328
|
+
}
|
|
329
|
+
finally {
|
|
330
|
+
ctx.setState({ loading: false });
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
/** Nudge one row's quantity, clamped to 0 and the type's maxPerOrder. */
|
|
334
|
+
changeQuantity(ticketTypeId, delta) {
|
|
335
|
+
const ticketType = ctx.state.ticketTypes.find((t) => t.id === ticketTypeId);
|
|
336
|
+
if (!ticketType)
|
|
337
|
+
return;
|
|
338
|
+
const current = ctx.state.quantities[ticketTypeId] || 0;
|
|
339
|
+
const quantity = Math.max(0, Math.min(ticketType.maxPerOrder, current + delta));
|
|
340
|
+
ctx.setState((state) => ({ quantities: { ...state.quantities, [ticketTypeId]: quantity } }));
|
|
341
|
+
// After setState, so the total is the one the buyer can now see.
|
|
342
|
+
ctx.emit('tlQuantityChange', { ticketTypeId, quantity, total: totalsOf(ctx).total });
|
|
343
|
+
},
|
|
344
|
+
/**
|
|
345
|
+
* Add every selected line to the cart, then clear the selection.
|
|
346
|
+
*
|
|
347
|
+
* With no client, or a client too old to carry a cart manager, the
|
|
348
|
+
* selection is still cleared and `tlAddToCart` still fires: a host that
|
|
349
|
+
* listens for the event and does its own cart keeps working.
|
|
350
|
+
*/
|
|
351
|
+
async addToCart() {
|
|
352
|
+
const items = Object.entries(ctx.state.quantities)
|
|
353
|
+
.filter(([, quantity]) => quantity > 0)
|
|
354
|
+
.map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
|
|
355
|
+
if (items.length === 0)
|
|
356
|
+
return;
|
|
357
|
+
ctx.setState({ adding: true, addSuccess: false });
|
|
358
|
+
try {
|
|
359
|
+
const cart = ctx.optionalClient()?.getCartManager?.();
|
|
360
|
+
if (cart) {
|
|
361
|
+
for (const item of items) {
|
|
362
|
+
await cart.addItem(item.ticketTypeId, item.quantity, ctx.props.occurrenceId);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const quantities = {};
|
|
366
|
+
for (const type of ctx.state.ticketTypes)
|
|
367
|
+
quantities[type.id] = 0;
|
|
368
|
+
ctx.setState({ quantities, addSuccess: true });
|
|
369
|
+
ctx.after(SUCCESS_MS, () => ctx.setState({ addSuccess: false }));
|
|
370
|
+
ctx.emit('tlAddToCart', { items });
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
const message = err instanceof Error ? err.message : ctx.strings.addFailed;
|
|
374
|
+
ctx.setState({ error: message });
|
|
375
|
+
ctx.emit('tlError', { message });
|
|
376
|
+
}
|
|
377
|
+
finally {
|
|
378
|
+
ctx.setState({ adding: false });
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
/** What the surface quoted for this selection, when it ever quotes one. */
|
|
382
|
+
setFeeQuote(quote) {
|
|
383
|
+
ctx.setState({ feeQuote: quote });
|
|
384
|
+
},
|
|
385
|
+
/**
|
|
386
|
+
* The host never produced a client. Shown, not emitted: the element has
|
|
387
|
+
* nothing to say to a page that has not started Live yet.
|
|
388
|
+
*/
|
|
389
|
+
connectFailed(message) {
|
|
390
|
+
ctx.setState({ error: message || ctx.strings.noClient, loading: false });
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const tlOccurrenceSelectorCss = () => `:host{display:block;font-family:var(--tl-font-family);color:var(--tl-foreground)}*{box-sizing:border-box;margin:0;padding:0}.occurrence-selector{width:100%}.no-occurrences{text-align:center;padding:3rem 1rem;color:var(--tl-muted-foreground)}.list-view{display:flex;flex-direction:column;gap:0.75rem}.occurrence-item{display:flex;justify-content:space-between;align-items:flex-start;width:100%;padding:1rem 1.25rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);text-align:left;cursor:pointer;transition:all 0.15s ease;font-family:inherit}.occurrence-item:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.occurrence-item:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.occurrence-item:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.occurrence-item.sold-out{opacity:0.6;background:var(--tl-muted)}.occurrence-info{flex:1}.occurrence-date{font-weight:600;font-size:0.9375rem;color:var(--tl-foreground);margin-bottom:0.25rem}.occurrence-time{font-size:0.875rem;color:var(--tl-muted-foreground)}.occurrence-status{flex-shrink:0;text-align:right}.status-sold-out{font-size:0.875rem;font-weight:500;color:var(--tl-muted-foreground)}.status-available{font-size:0.875rem;color:var(--tl-muted-foreground)}.status-arrow{font-size:1.25rem;color:var(--tl-primary)}.slots-view{display:flex;flex-direction:column;gap:1rem}.slots-date-header{font-size:1.125rem;font-weight:600;color:var(--tl-foreground);padding-bottom:0.5rem;border-bottom:1px solid var(--tl-border)}.time-slots-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));gap:0.5rem}.time-slot{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:0.75rem 0.5rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);cursor:pointer;transition:all 0.15s ease;font-family:inherit;min-height:3.5rem}.time-slot:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.time-slot:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.time-slot:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.time-slot.sold-out{opacity:0.6;background:var(--tl-muted)}.slot-time{font-size:0.9375rem;font-weight:600;color:var(--tl-foreground)}.slot-sold-out{font-size:0.6875rem;color:var(--tl-muted-foreground);margin-top:0.125rem}.calendar-view{display:flex;flex-direction:column;gap:1.5rem}.calendar-view.horizontal{flex-direction:row;gap:1.5rem;align-items:flex-start}.calendar-view.horizontal .calendar-panel{flex-shrink:0;width:340px}.calendar-view.horizontal .slots-panel{flex:1;min-width:0}.calendar-view.vertical .calendar-panel{width:100%}.calendar-view.vertical .slots-panel{width:100%}.calendar-panel{border:1px solid var(--tl-border);border-radius:var(--tl-radius);padding:1rem}.calendar-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem}.month-label{font-size:1rem;font-weight:600;color:var(--tl-foreground);flex:1}.calendar-nav-buttons{display:flex;gap:0.375rem}.nav-button{display:flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;padding:0;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);cursor:pointer;transition:all 0.15s ease;color:var(--tl-foreground)}.nav-button:hover:not(:disabled){background:var(--tl-muted)}.nav-button:disabled{cursor:not-allowed;opacity:0.4}.nav-button svg{width:0.875rem;height:0.875rem}.calendar-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0.1875rem}.weekday-header{text-align:center;font-size:0.6875rem;font-weight:600;color:var(--tl-muted-foreground);padding:0.375rem 0;text-transform:uppercase;letter-spacing:0.05em}.calendar-day{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;aspect-ratio:1;padding:0.1875rem;background:var(--tl-background);border:1px solid transparent;border-radius:var(--tl-radius);cursor:default;transition:all 0.15s ease;font-family:inherit;font-size:0.9375rem;font-weight:500;color:var(--tl-foreground)}.calendar-day.other-month{color:var(--tl-muted-foreground);opacity:0.5}.calendar-day.has-occurrences{cursor:pointer;background:color-mix(in srgb, var(--tl-primary) 10%, var(--tl-background));border-color:transparent;font-weight:600;color:var(--tl-primary)}.calendar-day.has-occurrences:hover{background:color-mix(in srgb, var(--tl-primary) 18%, var(--tl-background));border-color:transparent}.calendar-day.has-occurrences.sold-out{background:var(--tl-muted);border-color:transparent}.calendar-day.has-occurrences.sold-out:hover{background:color-mix(in srgb, var(--tl-muted) 80%, var(--tl-background));border-color:transparent}.calendar-day.selected{background:var(--tl-primary);border-color:var(--tl-primary);color:var(--tl-primary-foreground)}.calendar-day.selected:hover{background:color-mix(in srgb, var(--tl-primary) 90%, black)}.calendar-day.selected .occurrence-dot{background:var(--tl-primary-foreground)}.calendar-day.today .day-number{font-weight:700;text-decoration:underline;text-underline-offset:2px}.calendar-day.past:not(.has-occurrences){opacity:0.4}.day-number{line-height:1}.occurrence-dot{display:none}.slots-panel{min-height:200px}.selected-date-slots{height:100%}.selected-date-slots.empty{display:flex;align-items:center;justify-content:center;color:var(--tl-muted-foreground);background:var(--tl-muted);border-radius:var(--tl-radius);padding:2rem 1rem;text-align:center;min-height:200px;font-size:0.875rem}.selected-date-header{font-size:0.9375rem;font-weight:600;color:var(--tl-foreground);padding-bottom:0.625rem;margin-bottom:0.625rem}.time-slots-list{display:flex;flex-direction:column;gap:0.5rem}.time-slot-item{display:flex;align-items:center;justify-content:space-between;width:100%;padding:0.625rem 0.875rem;background:var(--tl-background);border:1px solid var(--tl-border);border-radius:var(--tl-radius);text-align:left;cursor:pointer;transition:all 0.15s ease;font-family:inherit}.time-slot-item:hover:not(:disabled){border-color:var(--tl-primary);background:color-mix(in srgb, var(--tl-primary) 5%, var(--tl-background))}.time-slot-item:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}.time-slot-item:disabled{cursor:not-allowed;opacity:0.6;background:var(--tl-muted)}.time-slot-item.sold-out{opacity:0.6;background:var(--tl-muted)}.time-slot-item .slot-time{font-size:0.9375rem;font-weight:600}.slot-status{flex-shrink:0}.slot-status .status-sold-out{font-size:0.75rem}.slot-status .status-available{font-size:0.75rem}.slot-status .status-arrow{font-size:1rem}@media (max-width: 640px){.event-name{font-size:1.25rem}.occurrence-date{font-size:0.875rem}.time-slots-grid{grid-template-columns:repeat(auto-fill, minmax(80px, 1fr))}.time-slot{padding:0.625rem 0.375rem;min-height:3rem}.slot-time{font-size:0.875rem}.calendar-day{font-size:0.75rem}.weekday-header{font-size:0.625rem}.calendar-view.horizontal .calendar-panel{width:100%}}@media (max-width: 767px){.calendar-view.horizontal{flex-direction:column}}.loading-container{padding:1rem 0;min-height:400px}@keyframes skeleton-pulse{0%,100%{opacity:1}50%{opacity:0.5}}.skeleton{background:var(--tl-muted);border-radius:var(--tl-radius);animation:skeleton-pulse 1.5s ease-in-out infinite}.skeleton-list{display:flex;flex-direction:column;gap:0.75rem}.skeleton-list-item{height:4.5rem;border-radius:var(--tl-radius)}.skeleton-calendar{border:1px solid var(--tl-border);border-radius:var(--tl-radius);padding:1rem;min-height:350px}.skeleton-calendar-header{height:1.75rem;width:50%;margin-bottom:0.75rem;border-radius:var(--tl-radius)}.skeleton-calendar-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0.1875rem;margin-top:0.75rem}.skeleton-calendar-day{aspect-ratio:1;border-radius:var(--tl-radius)}.skeleton-slots{display:flex;flex-direction:column;gap:1rem}.skeleton-slots-header{height:1.75rem;width:60%;border-radius:var(--tl-radius);margin-bottom:0.5rem}.skeleton-slots-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(100px, 1fr));gap:0.5rem}.skeleton-slot-item{height:3.5rem;border-radius:var(--tl-radius)}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem 1rem;gap:1rem;text-align:center}.error-icon{width:3rem;height:3rem;color:var(--tl-muted-foreground);stroke-width:1.5}.error-message{color:var(--tl-foreground);font-size:0.9375rem;max-width:400px}.retry-button{margin-top:0.5rem;padding:0.625rem 1.25rem;background:var(--tl-primary);color:var(--tl-primary-foreground);border:none;border-radius:var(--tl-radius);font-family:inherit;font-size:0.875rem;font-weight:500;cursor:pointer;transition:all 0.15s ease}.retry-button:hover{background:color-mix(in srgb, var(--tl-primary) 90%, black)}.retry-button:focus-visible{outline:2px solid var(--tl-primary);outline-offset:2px}`;
|
|
5
396
|
|
|
6
397
|
const TlOccurrenceSelector = class {
|
|
7
398
|
constructor(hostRef) {
|
|
@@ -569,7 +960,7 @@ const TlOccurrenceSelector = class {
|
|
|
569
960
|
};
|
|
570
961
|
TlOccurrenceSelector.style = tlOccurrenceSelectorCss();
|
|
571
962
|
|
|
572
|
-
const tlTicketSelectorCss = () => `:host{--tl-color-primary:var(--tl-primary
|
|
963
|
+
const tlTicketSelectorCss = () => `:host{--tl-color-primary:var(--tl-primary);--tl-color-primary-foreground:var(--tl-primary-foreground);--tl-color-background:var(--tl-background);--tl-color-foreground:var(--tl-foreground);--tl-color-muted:var(--tl-muted);--tl-color-muted-foreground:var(--tl-muted-foreground);--tl-color-border:var(--tl-border);--tl-color-destructive:var(--tl-error);display:block;font-family:var(--tl-font-family)}.ticket-selector{background:var(--tl-color-background)}.skeleton-container{padding:var(--tl-spacing-4)}.skeleton-row{height:60px;background:var(--tl-color-muted);border-radius:var(--tl-radius-md);margin-bottom:var(--tl-spacing-3);animation:pulse 1.5s ease-in-out infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}.error-state{padding:var(--tl-spacing-4);color:var(--tl-color-destructive)}.error-message{margin:0;font-size:14px}.ticket-list{margin-bottom:var(--tl-spacing-4)}.ticket-row{display:flex;align-items:center;justify-content:space-between;padding:var(--tl-spacing-3) 0;border-bottom:1px solid var(--tl-color-border)}.ticket-row.sold-out{opacity:0.5}.ticket-info{flex:1;min-width:0}.ticket-header{display:flex;align-items:center;gap:var(--tl-spacing-2);flex-wrap:wrap}.ticket-name{font-weight:600;color:var(--tl-color-foreground);font-size:15px}.status-badge{font-size:12px;font-weight:500;padding:2px 8px;border-radius:4px}.status-badge.limited{background-color:var(--tl-color-warning-bg);color:var(--tl-color-warning)}.status-badge.sold-out{background-color:var(--tl-color-destructive-bg);color:var(--tl-color-destructive)}.ticket-description{margin:var(--tl-spacing-1) 0 0;font-size:14px;color:var(--tl-color-muted-foreground)}.ticket-price{margin:var(--tl-spacing-1) 0 0;font-size:14px;font-weight:600;color:var(--tl-color-foreground)}.quantity-controls{display:flex;align-items:center;gap:var(--tl-spacing-2);margin-left:var(--tl-spacing-4)}.qty-btn{width:32px;height:32px;border-radius:var(--tl-radius-sm);border:1px solid var(--tl-color-border);background:var(--tl-color-background);color:var(--tl-color-foreground);font-size:18px;font-weight:500;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background-color 0.15s, border-color 0.15s}.qty-btn:hover:not(:disabled){background-color:var(--tl-color-muted);border-color:var(--tl-color-border-hover)}.qty-btn:disabled{cursor:not-allowed;opacity:0.5}.qty-value{width:32px;text-align:center;font-weight:600;font-size:16px;color:var(--tl-color-foreground)}.success-message{display:flex;align-items:center;justify-content:center;gap:var(--tl-spacing-2);padding:var(--tl-spacing-3);background-color:var(--tl-color-success);color:#ffffff;border-radius:var(--tl-radius-md);margin-bottom:var(--tl-spacing-3);font-weight:500;font-size:14px}.success-icon{width:18px;height:18px}.add-to-cart-btn{width:100%;padding:var(--tl-spacing-3) var(--tl-spacing-4);border-radius:var(--tl-radius-md);border:none;background-color:var(--tl-color-primary);color:var(--tl-color-primary-foreground);font-weight:600;font-size:16px;cursor:pointer;transition:opacity 0.15s, transform 0.1s}.add-to-cart-btn:hover:not(:disabled){opacity:0.9}.add-to-cart-btn:active:not(:disabled){transform:scale(0.98)}.add-to-cart-btn.disabled,.add-to-cart-btn:disabled{background-color:var(--tl-color-muted);color:var(--tl-color-muted-foreground);cursor:not-allowed}.selection-totals{padding:var(--tl-spacing-3) 0;border-top:1px solid var(--tl-color-border)}.selection-totals .total-row{display:flex;justify-content:space-between;align-items:center;padding:var(--tl-spacing-1) 0;font-size:14px;color:var(--tl-color-muted-foreground)}.selection-totals .total-row.total{font-size:16px;font-weight:700;color:var(--tl-color-foreground);padding-top:var(--tl-spacing-2);border-top:1px solid var(--tl-color-border);margin-top:var(--tl-spacing-2)}`;
|
|
573
964
|
|
|
574
965
|
const TlTicketSelector = class {
|
|
575
966
|
constructor(hostRef) {
|
|
@@ -585,19 +976,49 @@ const TlTicketSelector = class {
|
|
|
585
976
|
* Button label
|
|
586
977
|
*/
|
|
587
978
|
this.buttonLabel = 'Add to Cart';
|
|
588
|
-
this.
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
this.
|
|
979
|
+
this.element = createElement(ticketSelectorCore, {
|
|
980
|
+
client: () => getSDK(),
|
|
981
|
+
emit: (event, detail) => this.forward(event, detail),
|
|
982
|
+
});
|
|
983
|
+
/** The core's whole view. Assigning it is what makes Stencil re-render. */
|
|
984
|
+
this.view = this.element.view;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* The fee the surface has quoted for this selection. Core state, exposed here
|
|
988
|
+
* because it is set from outside the element and not from an attribute: there
|
|
989
|
+
* is no quote operation on the buyer-facing surface yet, so it stays null and
|
|
990
|
+
* no fee row is rendered.
|
|
991
|
+
*/
|
|
992
|
+
get feeQuote() {
|
|
993
|
+
return this.element.state.feeQuote;
|
|
994
|
+
}
|
|
995
|
+
set feeQuote(quote) {
|
|
996
|
+
this.element.actions.setFeeQuote(quote);
|
|
594
997
|
}
|
|
595
998
|
watchPropsChange() {
|
|
596
|
-
this.
|
|
999
|
+
this.syncProps();
|
|
1000
|
+
this.element.actions.load();
|
|
1001
|
+
}
|
|
1002
|
+
syncProps() {
|
|
1003
|
+
this.element.setProps({
|
|
1004
|
+
eventId: this.eventId,
|
|
1005
|
+
occurrenceId: this.occurrenceId,
|
|
1006
|
+
showButton: this.showButton,
|
|
1007
|
+
buttonLabel: this.buttonLabel,
|
|
1008
|
+
});
|
|
597
1009
|
}
|
|
598
1010
|
async componentWillLoad() {
|
|
1011
|
+
this.syncProps();
|
|
1012
|
+
this.unsubscribe = this.element.subscribe((view) => {
|
|
1013
|
+
this.view = view;
|
|
1014
|
+
});
|
|
1015
|
+
this.view = this.element.view;
|
|
599
1016
|
await this.waitForSDKAndLoad();
|
|
600
1017
|
}
|
|
1018
|
+
disconnectedCallback() {
|
|
1019
|
+
this.unsubscribe?.();
|
|
1020
|
+
this.element.destroy();
|
|
1021
|
+
}
|
|
601
1022
|
/**
|
|
602
1023
|
* Wait for SDK to be ready, then load ticket types
|
|
603
1024
|
*/
|
|
@@ -607,135 +1028,40 @@ const TlTicketSelector = class {
|
|
|
607
1028
|
}
|
|
608
1029
|
try {
|
|
609
1030
|
await whenSDK();
|
|
610
|
-
await this.
|
|
611
|
-
}
|
|
612
|
-
catch (err) {
|
|
613
|
-
this.error = err instanceof Error ? err.message : 'SDK not initialized.';
|
|
614
|
-
this.loading = false;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
async loadTicketTypes() {
|
|
618
|
-
try {
|
|
619
|
-
console.log('[tl-ticket-selector] Loading ticket types...', {
|
|
620
|
-
eventId: this.eventId,
|
|
621
|
-
occurrenceId: this.occurrenceId
|
|
622
|
-
});
|
|
623
|
-
this.loading = true;
|
|
624
|
-
this.error = null;
|
|
625
|
-
const sdk = getSDK();
|
|
626
|
-
const eventsManager = sdk.getEventsManager();
|
|
627
|
-
const types = await eventsManager.getTicketTypes(this.eventId, this.occurrenceId);
|
|
628
|
-
console.log('[tl-ticket-selector] Ticket types loaded:', types.length);
|
|
629
|
-
this.ticketTypes = types;
|
|
630
|
-
// Initialise quantities to 0
|
|
631
|
-
const initialQuantities = {};
|
|
632
|
-
types.forEach((t) => {
|
|
633
|
-
initialQuantities[t.id] = 0;
|
|
634
|
-
});
|
|
635
|
-
this.quantities = initialQuantities;
|
|
1031
|
+
await this.element.actions.load();
|
|
636
1032
|
}
|
|
637
1033
|
catch (err) {
|
|
638
|
-
|
|
639
|
-
this.error = err instanceof Error ? err.message : 'Failed to load tickets';
|
|
640
|
-
this.tlError.emit({ message: this.error });
|
|
641
|
-
}
|
|
642
|
-
finally {
|
|
643
|
-
this.loading = false;
|
|
1034
|
+
this.element.actions.connectFailed(err instanceof Error ? err.message : this.view.strings.noClient);
|
|
644
1035
|
}
|
|
645
1036
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const newQty = Math.max(0, Math.min(ticketType.maxPerOrder, currentQty + delta));
|
|
652
|
-
this.quantities = {
|
|
653
|
-
...this.quantities,
|
|
654
|
-
[ticketTypeId]: newQty,
|
|
655
|
-
};
|
|
656
|
-
const total = this.calculateTotal();
|
|
657
|
-
this.tlQuantityChange.emit({ ticketTypeId, quantity: newQty, total });
|
|
658
|
-
}
|
|
659
|
-
calculateTotal() {
|
|
660
|
-
return this.ticketTypes.reduce((sum, tt) => {
|
|
661
|
-
return sum + (this.quantities[tt.id] || 0) * tt.price;
|
|
662
|
-
}, 0);
|
|
663
|
-
}
|
|
664
|
-
getTotalItems() {
|
|
665
|
-
return Object.values(this.quantities).reduce((sum, qty) => sum + qty, 0);
|
|
666
|
-
}
|
|
667
|
-
async handleAddToCart() {
|
|
668
|
-
const itemsToAdd = Object.entries(this.quantities)
|
|
669
|
-
.filter(([_, qty]) => qty > 0)
|
|
670
|
-
.map(([ticketTypeId, quantity]) => ({ ticketTypeId, quantity }));
|
|
671
|
-
if (itemsToAdd.length === 0)
|
|
672
|
-
return;
|
|
673
|
-
this.adding = true;
|
|
674
|
-
this.addSuccess = false;
|
|
675
|
-
try {
|
|
676
|
-
if (hasSDK()) {
|
|
677
|
-
const sdk = getSDK();
|
|
678
|
-
const cartManager = sdk.getCartManager?.();
|
|
679
|
-
if (cartManager) {
|
|
680
|
-
for (const item of itemsToAdd) {
|
|
681
|
-
await cartManager.addItem(item.ticketTypeId, item.quantity, this.occurrenceId);
|
|
682
|
-
}
|
|
683
|
-
console.log('[tl-ticket-selector] Items added to cart');
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
// Reset quantities after adding
|
|
687
|
-
const resetQuantities = {};
|
|
688
|
-
this.ticketTypes.forEach(t => {
|
|
689
|
-
resetQuantities[t.id] = 0;
|
|
690
|
-
});
|
|
691
|
-
this.quantities = resetQuantities;
|
|
692
|
-
this.addSuccess = true;
|
|
693
|
-
// Clear success message after 3 seconds
|
|
694
|
-
setTimeout(() => {
|
|
695
|
-
this.addSuccess = false;
|
|
696
|
-
}, 3000);
|
|
697
|
-
this.tlAddToCart.emit({ items: itemsToAdd });
|
|
698
|
-
}
|
|
699
|
-
catch (err) {
|
|
700
|
-
console.error('[tl-ticket-selector] Failed to add to cart:', err);
|
|
701
|
-
this.error = err instanceof Error ? err.message : 'Failed to add to cart';
|
|
702
|
-
this.tlError.emit({ message: this.error });
|
|
703
|
-
}
|
|
704
|
-
finally {
|
|
705
|
-
this.adding = false;
|
|
1037
|
+
forward(event, detail) {
|
|
1038
|
+
if (event === 'tlAddToCart')
|
|
1039
|
+
this.tlAddToCart.emit(detail);
|
|
1040
|
+
else if (event === 'tlQuantityChange') {
|
|
1041
|
+
this.tlQuantityChange.emit(detail);
|
|
706
1042
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
return new Intl.NumberFormat('en-GB', {
|
|
710
|
-
style: 'currency',
|
|
711
|
-
currency,
|
|
712
|
-
}).format(priceInPence / 100);
|
|
1043
|
+
else if (event === 'tlError')
|
|
1044
|
+
this.tlError.emit(detail);
|
|
713
1045
|
}
|
|
714
1046
|
renderSkeleton() {
|
|
715
1047
|
return (h("div", { class: "skeleton-container" }, h("div", { class: "skeleton-row" }), h("div", { class: "skeleton-row" }), h("div", { class: "skeleton-row" })));
|
|
716
1048
|
}
|
|
717
1049
|
renderError() {
|
|
718
|
-
return (h("div", { class: "error-state" }, h("p", { class: "error-message" }, this.error)));
|
|
1050
|
+
return (h("div", { class: "error-state" }, h("p", { class: "error-message" }, this.view.state.error)));
|
|
719
1051
|
}
|
|
720
|
-
renderTicketRow(
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
return (h("div", { class: `ticket-row ${isSoldOut ? 'sold-out' : ''}`, key: tt.id }, h("div", { class: "ticket-info" }, h("div", { class: "ticket-header" }, h("span", { class: "ticket-name" }, tt.name), tt.availabilityStatus === 'limited' && (h("span", { class: "status-badge limited" }, "Limited")), isSoldOut && (h("span", { class: "status-badge sold-out" }, "Sold Out"))), tt.description && (h("p", { class: "ticket-description" }, tt.description)), h("p", { class: "ticket-price" }, this.formatPrice(tt.price, tt.currency))), h("div", { class: "quantity-controls" }, h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, -1), disabled: isSoldOut || qty === 0, "aria-label": "Decrease quantity" }, "\u2212"), h("span", { class: "qty-value" }, qty), h("button", { class: "qty-btn", onClick: () => this.handleQuantityChange(tt.id, 1), disabled: isSoldOut || qty >= tt.maxPerOrder, "aria-label": "Increase quantity" }, "+"))));
|
|
1052
|
+
renderTicketRow(row) {
|
|
1053
|
+
const strings = this.view.strings;
|
|
1054
|
+
return (h("div", { class: `ticket-row ${row.soldOut ? 'sold-out' : ''}`, key: row.id }, h("div", { class: "ticket-info" }, h("div", { class: "ticket-header" }, h("span", { class: "ticket-name" }, row.name), row.limited && h("span", { class: "status-badge limited" }, strings.limited), row.soldOut && h("span", { class: "status-badge sold-out" }, strings.soldOut)), row.description && h("p", { class: "ticket-description" }, row.description), h("p", { class: "ticket-price" }, row.priceLabel)), h("div", { class: "quantity-controls" }, h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, -1), disabled: !row.canDecrease, "aria-label": strings.decrease }, "\u2212"), h("span", { class: "qty-value" }, row.quantity), h("button", { class: "qty-btn", onClick: () => this.element.actions.changeQuantity(row.id, 1), disabled: !row.canIncrease, "aria-label": strings.increase }, "+"))));
|
|
724
1055
|
}
|
|
725
1056
|
render() {
|
|
726
|
-
|
|
1057
|
+
const { status, state, strings, derived } = this.view;
|
|
1058
|
+
if (status === 'loading') {
|
|
727
1059
|
return this.renderSkeleton();
|
|
728
1060
|
}
|
|
729
|
-
if (
|
|
1061
|
+
if (status === 'error') {
|
|
730
1062
|
return this.renderError();
|
|
731
1063
|
}
|
|
732
|
-
|
|
733
|
-
const totalPrice = this.calculateTotal();
|
|
734
|
-
return (h("div", { class: "ticket-selector" }, h("div", { class: "ticket-list" }, this.ticketTypes.map(tt => this.renderTicketRow(tt))), this.addSuccess && (h("div", { class: "success-message" }, h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), "Added to cart!")), this.showButton && (h("button", { class: `add-to-cart-btn ${totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.handleAddToCart(), disabled: totalItems === 0 || this.adding }, this.adding
|
|
735
|
-
? 'Adding...'
|
|
736
|
-
: totalItems === 0
|
|
737
|
-
? 'Select Tickets'
|
|
738
|
-
: `${this.buttonLabel} - ${this.formatPrice(totalPrice)}`))));
|
|
1064
|
+
return (h("div", { class: "ticket-selector" }, h("div", { class: "ticket-list" }, derived.rows.map((row) => this.renderTicketRow(row))), derived.showTotals && (h("div", { class: "selection-totals" }, h("div", { class: "total-row" }, h("span", null, strings.ticketsRow), h("span", null, derived.subtotalLabel)), h("div", { class: "total-row fee" }, h("span", null, derived.totals.feeLabel), h("span", null, derived.feeLabel)), h("div", { class: "total-row total" }, h("span", null, strings.totalRow), h("span", null, derived.totalLabel)))), state.addSuccess && (h("div", { class: "success-message" }, h("svg", { class: "success-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor" }, h("path", { "stroke-linecap": "round", "stroke-linejoin": "round", "stroke-width": "2", d: "M5 13l4 4L19 7" })), strings.added)), this.showButton && (h("button", { class: `add-to-cart-btn ${derived.totalItems === 0 ? 'disabled' : ''}`, onClick: () => this.element.actions.addToCart(), disabled: derived.actionDisabled }, derived.actionLabel))));
|
|
739
1065
|
}
|
|
740
1066
|
static get watchers() { return {
|
|
741
1067
|
"eventId": [{
|
|
@@ -743,6 +1069,12 @@ const TlTicketSelector = class {
|
|
|
743
1069
|
}],
|
|
744
1070
|
"occurrenceId": [{
|
|
745
1071
|
"watchPropsChange": 0
|
|
1072
|
+
}],
|
|
1073
|
+
"showButton": [{
|
|
1074
|
+
"syncProps": 0
|
|
1075
|
+
}],
|
|
1076
|
+
"buttonLabel": [{
|
|
1077
|
+
"syncProps": 0
|
|
746
1078
|
}]
|
|
747
1079
|
}; }
|
|
748
1080
|
};
|