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