@ticketlayer/elements 0.4.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/{index-I9CoYv3k.js → index-DanPUX0s.js} +1 -1
- package/dist/cjs/index.cjs.js +2 -1
- package/dist/cjs/loader.cjs.js +2 -2
- package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -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 +89 -74
- package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +61 -30
- package/dist/cjs/tl-cart-badge.cjs.entry.js +74 -61
- package/dist/cjs/tl-cart-drawer.cjs.entry.js +51 -23
- package/dist/cjs/tl-cart.cjs.entry.js +71 -103
- package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +51 -29
- package/dist/cjs/tl-event-card.cjs.entry.js +43 -105
- package/dist/cjs/tl-event-list.cjs.entry.js +72 -35
- package/dist/cjs/tl-login.cjs.entry.js +74 -141
- package/dist/cjs/tl-my-orders.cjs.entry.js +52 -92
- package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +153 -921
- package/dist/cjs/tl-order-confirmation.cjs.entry.js +71 -107
- package/dist/cjs/tl-order-tickets_2.cjs.entry.js +48 -74
- package/dist/cjs/tl-promo-code.cjs.entry.js +50 -47
- package/dist/cjs/tl-seat-map.cjs.entry.js +3 -3
- package/dist/cjs/web-query-Ce73jB4k.js +16 -0
- package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +37 -0
- package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -79
- package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
- package/dist/collection/components/tl-cart/tl-cart.js +82 -107
- package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
- package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
- package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
- package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
- package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
- package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
- package/dist/collection/components/tl-login/tl-login.js +83 -147
- package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
- package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
- package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -115
- package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
- package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
- package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
- package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +34 -20
- package/dist/collection/utils/order-summary.js +15 -135
- package/dist/collection/utils/testing/deep-text.js +29 -0
- package/dist/collection/utils/testing/fixtures.js +290 -0
- package/dist/collection/utils/testing/mock-sdk.js +13 -2
- package/dist/collection/utils/web-query.js +12 -0
- package/dist/collection/utils/web-storage.js +22 -0
- package/dist/components/ticket-selector.js +1 -0
- 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/web-query.js +1 -0
- package/dist/elements.docs.json +34 -34
- package/dist/esm/{index-OoYV1faY.js → index-imfxp765.js} +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/loader.js +3 -3
- package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
- 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 +90 -75
- package/dist/esm/tl-buy-tickets-wrapper.entry.js +62 -31
- package/dist/esm/tl-cart-badge.entry.js +75 -62
- package/dist/esm/tl-cart-drawer.entry.js +52 -24
- package/dist/esm/tl-cart.entry.js +72 -104
- package/dist/esm/tl-checkout-wrapper.entry.js +52 -30
- package/dist/esm/tl-event-card.entry.js +44 -106
- package/dist/esm/tl-event-list.entry.js +73 -36
- package/dist/esm/tl-login.entry.js +75 -142
- package/dist/esm/tl-my-orders.entry.js +53 -93
- package/dist/esm/tl-occurrence-selector_2.entry.js +152 -920
- package/dist/esm/tl-order-confirmation.entry.js +72 -108
- package/dist/esm/tl-order-tickets_2.entry.js +49 -75
- package/dist/esm/tl-promo-code.entry.js +51 -48
- package/dist/esm/tl-seat-map.entry.js +3 -3
- package/dist/esm/web-query-Bdbb2lk5.js +14 -0
- package/dist/ticketlayer-elements/index.esm.js +1 -1
- package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
- package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
- package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
- package/dist/ticketlayer-elements/{p-ac264adc.entry.js → p-5135630e.entry.js} +1 -1
- package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
- package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
- package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
- package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
- package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
- package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
- package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
- package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
- package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
- package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
- package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
- package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
- package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
- package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
- package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
- package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
- package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
- package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
- package/dist/types/components/tl-cart/tl-cart.d.ts +40 -42
- package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
- package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
- package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
- package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
- package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
- package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
- package/dist/types/components/tl-login/tl-login.d.ts +68 -22
- package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
- package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
- package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
- package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
- package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
- package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
- package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +22 -4
- package/dist/types/components.d.ts +270 -62
- package/dist/types/utils/order-summary.d.ts +14 -97
- package/dist/types/utils/testing/deep-text.d.ts +13 -0
- package/dist/types/utils/testing/fixtures.d.ts +225 -0
- package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
- package/dist/types/utils/web-query.d.ts +18 -0
- package/dist/types/utils/web-storage.d.ts +16 -0
- package/elements.catalogue.json +602 -50
- package/package.json +8 -6
- package/dist/cjs/money-BtmM2gt-.js +0 -118
- package/dist/cjs/order-summary-X3TlZxc2.js +0 -150
- package/dist/components/money.js +0 -1
- package/dist/components/order-summary.js +0 -1
- package/dist/esm/money-4ig0-WB6.js +0 -112
- package/dist/esm/order-summary-DiDtvfZM.js +0 -136
- package/dist/ticketlayer-elements/p-0814420f.entry.js +0 -1
- package/dist/ticketlayer-elements/p-0acabc26.entry.js +0 -1
- package/dist/ticketlayer-elements/p-109a3feb.entry.js +0 -1
- package/dist/ticketlayer-elements/p-1a4ba71c.entry.js +0 -1
- package/dist/ticketlayer-elements/p-318965c1.entry.js +0 -1
- package/dist/ticketlayer-elements/p-347c7fd1.entry.js +0 -1
- package/dist/ticketlayer-elements/p-3649c26d.entry.js +0 -1
- package/dist/ticketlayer-elements/p-40260cc9.entry.js +0 -1
- package/dist/ticketlayer-elements/p-4ig0-WB6.js +0 -1
- package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +0 -1
- package/dist/ticketlayer-elements/p-62ee659b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +0 -1
- package/dist/ticketlayer-elements/p-7f0abf95.entry.js +0 -1
- package/dist/ticketlayer-elements/p-BXpVGW51.js +0 -1
- package/dist/ticketlayer-elements/p-OoYV1faY.js +0 -3
- package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +0 -1
- package/dist/ticketlayer-elements/p-aeb4c6fb.entry.js +0 -1
- package/dist/ticketlayer-elements/p-ba62541f.entry.js +0 -1
|
@@ -1,399 +1,21 @@
|
|
|
1
|
-
import { r as registerInstance, c as createEvent, h } from './index-
|
|
1
|
+
import { r as registerInstance, c as createEvent, h } from './index-imfxp765.js';
|
|
2
2
|
import { w as whenSDK, g as getSDK } from './sdk-connector-CqhvfyKV.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
});
|
|
3
|
+
import { c as createElement, h as occurrenceSelectorCore, t as ticketSelectorCore } from './ticket-selector-BwChnHVC.js';
|
|
394
4
|
|
|
395
5
|
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}`;
|
|
396
6
|
|
|
7
|
+
/** Theme field -> the custom property the stylesheet reads it through. */
|
|
8
|
+
const THEME_PROPERTIES = [
|
|
9
|
+
['primaryColor', '--tl-primary'],
|
|
10
|
+
['primaryForeground', '--tl-primary-foreground'],
|
|
11
|
+
['backgroundColor', '--tl-background'],
|
|
12
|
+
['foregroundColor', '--tl-foreground'],
|
|
13
|
+
['mutedColor', '--tl-muted'],
|
|
14
|
+
['mutedForeground', '--tl-muted-foreground'],
|
|
15
|
+
['borderColor', '--tl-border'],
|
|
16
|
+
['borderRadius', '--tl-radius'],
|
|
17
|
+
['fontFamily', '--tl-font-family'],
|
|
18
|
+
];
|
|
397
19
|
const TlOccurrenceSelector = class {
|
|
398
20
|
constructor(hostRef) {
|
|
399
21
|
registerInstance(this, hostRef);
|
|
@@ -402,559 +24,155 @@ const TlOccurrenceSelector = class {
|
|
|
402
24
|
* Layout orientation (auto = horizontal on desktop, vertical on mobile)
|
|
403
25
|
*/
|
|
404
26
|
this.orientation = 'auto';
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
this.selectedDate = null;
|
|
425
|
-
/**
|
|
426
|
-
* Computed display mode based on occurrences
|
|
427
|
-
*/
|
|
428
|
-
this.computedDisplayMode = 'list';
|
|
429
|
-
/**
|
|
430
|
-
* Container width for responsive layout
|
|
431
|
-
*/
|
|
432
|
-
this.containerWidth = 0;
|
|
27
|
+
this.element = createElement(occurrenceSelectorCore, {
|
|
28
|
+
client: () => getSDK(),
|
|
29
|
+
// What "the client is up" means on the web. The core waits on it inside its
|
|
30
|
+
// own mount and reads there; the harness decides none of it (TKT-197).
|
|
31
|
+
clientReady: () => whenSDK(),
|
|
32
|
+
emit: (event, detail) => this.forward(event, detail),
|
|
33
|
+
});
|
|
34
|
+
/** The core's whole view. Assigning it is what makes Stencil re-render. */
|
|
35
|
+
this.view = this.element.view;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A new event is a new subject: the element is torn down and mounted again,
|
|
39
|
+
* so whatever the core set up for the old one is undone before anything is
|
|
40
|
+
* set up for the new one.
|
|
41
|
+
*/
|
|
42
|
+
watchSubject() {
|
|
43
|
+
this.syncProps();
|
|
44
|
+
this.teardown?.();
|
|
45
|
+
this.teardown = this.element.mount();
|
|
433
46
|
}
|
|
434
|
-
|
|
435
|
-
|
|
47
|
+
syncProps() {
|
|
48
|
+
this.element.setProps({
|
|
49
|
+
eventId: this.eventId,
|
|
50
|
+
displayMode: this.displayMode,
|
|
51
|
+
orientation: this.orientation,
|
|
52
|
+
theme: this.theme,
|
|
53
|
+
});
|
|
436
54
|
}
|
|
437
55
|
/**
|
|
438
|
-
*
|
|
56
|
+
* On screen: subscribe to the view, then put the core to work.
|
|
57
|
+
*
|
|
58
|
+
* `connectedCallback` and not `componentWillLoad` because this runs again
|
|
59
|
+
* when the element is moved in the DOM, and that is exactly when a mount and
|
|
60
|
+
* its teardown have to pair up.
|
|
439
61
|
*/
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
this.
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
await whenSDK();
|
|
448
|
-
await this.fetchOccurrences();
|
|
449
|
-
}
|
|
450
|
-
catch (err) {
|
|
451
|
-
this.error = err instanceof Error ? err.message : 'SDK not initialized.';
|
|
452
|
-
this.loading = false;
|
|
453
|
-
}
|
|
62
|
+
connectedCallback() {
|
|
63
|
+
this.syncProps();
|
|
64
|
+
this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
|
|
65
|
+
this.view = view;
|
|
66
|
+
}));
|
|
67
|
+
this.view = this.element.view;
|
|
68
|
+
this.teardown = this.element.mount();
|
|
454
69
|
}
|
|
70
|
+
/** Measuring the element is the runtime's job; what a width means is not. */
|
|
455
71
|
componentDidLoad() {
|
|
456
|
-
// Set up resize observer for responsive layout
|
|
457
72
|
if (this.containerRef && typeof ResizeObserver !== 'undefined') {
|
|
458
73
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
459
|
-
for (const entry of entries)
|
|
460
|
-
this.
|
|
461
|
-
}
|
|
74
|
+
for (const entry of entries)
|
|
75
|
+
this.element.actions.resized(entry.contentRect.width);
|
|
462
76
|
});
|
|
463
77
|
this.resizeObserver.observe(this.containerRef);
|
|
464
78
|
}
|
|
465
79
|
}
|
|
466
|
-
disconnectedCallback() {
|
|
467
|
-
if (this.resizeObserver) {
|
|
468
|
-
this.resizeObserver.disconnect();
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
async onEventIdChange() {
|
|
472
|
-
await this.fetchOccurrences();
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Fetch occurrences from the SDK
|
|
476
|
-
*/
|
|
477
|
-
async fetchOccurrences() {
|
|
478
|
-
this.loading = true;
|
|
479
|
-
this.error = null;
|
|
480
|
-
try {
|
|
481
|
-
const sdk = getSDK();
|
|
482
|
-
const eventsManager = sdk.getEventsManager();
|
|
483
|
-
const event = await eventsManager.get(this.eventId);
|
|
484
|
-
if (!event) {
|
|
485
|
-
throw new Error('Event not found');
|
|
486
|
-
}
|
|
487
|
-
// Map EventOccurrence to TlOccurrence
|
|
488
|
-
this.occurrences = (event.occurrences || []).map((occ) => ({
|
|
489
|
-
id: occ.id,
|
|
490
|
-
startsAt: occ.startsAt,
|
|
491
|
-
endsAt: occ.endsAt,
|
|
492
|
-
status: occ.availabilityStatus,
|
|
493
|
-
availableCapacity: occ.availableCapacity,
|
|
494
|
-
soldOut: occ.availabilityStatus === 'sold_out',
|
|
495
|
-
}));
|
|
496
|
-
this.computeDisplayMode();
|
|
497
|
-
this.computeInitialSelection();
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
console.error('[tl-occurrence-selector] Failed to fetch occurrences:', err);
|
|
501
|
-
this.error = err instanceof Error ? err.message : 'Failed to load occurrences';
|
|
502
|
-
this.occurrences = [];
|
|
503
|
-
}
|
|
504
|
-
finally {
|
|
505
|
-
this.loading = false;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Compute the display mode based on occurrence count and date distribution
|
|
510
|
-
*/
|
|
511
|
-
computeDisplayMode() {
|
|
512
|
-
if (this.displayMode) {
|
|
513
|
-
this.computedDisplayMode = this.displayMode;
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const count = this.occurrences.length;
|
|
517
|
-
if (count <= 5) {
|
|
518
|
-
this.computedDisplayMode = 'list';
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
// Check if all occurrences are on the same calendar date
|
|
522
|
-
const uniqueDates = new Set();
|
|
523
|
-
for (const occ of this.occurrences) {
|
|
524
|
-
const date = new Date(occ.startsAt);
|
|
525
|
-
uniqueDates.add(date.toDateString());
|
|
526
|
-
}
|
|
527
|
-
if (uniqueDates.size === 1) {
|
|
528
|
-
this.computedDisplayMode = 'slots';
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
this.computedDisplayMode = 'calendar';
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
/**
|
|
535
|
-
* Compute the initial date selection for calendar/slots views
|
|
536
|
-
*/
|
|
537
|
-
computeInitialSelection() {
|
|
538
|
-
if (this.occurrences.length === 0)
|
|
539
|
-
return;
|
|
540
|
-
const today = new Date();
|
|
541
|
-
today.setHours(0, 0, 0, 0);
|
|
542
|
-
// Get all dates with occurrences
|
|
543
|
-
const datesWithOccurrences = this.getDatesWithOccurrences();
|
|
544
|
-
// Find the first date that is today or in the future
|
|
545
|
-
let selectedDate = null;
|
|
546
|
-
for (const dateStr of Array.from(datesWithOccurrences.keys()).sort()) {
|
|
547
|
-
const date = new Date(dateStr);
|
|
548
|
-
if (date >= today) {
|
|
549
|
-
selectedDate = date;
|
|
550
|
-
break;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
// If no future dates, select the last available date
|
|
554
|
-
if (!selectedDate && datesWithOccurrences.size > 0) {
|
|
555
|
-
const sortedDates = Array.from(datesWithOccurrences.keys()).sort();
|
|
556
|
-
selectedDate = new Date(sortedDates[sortedDates.length - 1]);
|
|
557
|
-
}
|
|
558
|
-
if (selectedDate) {
|
|
559
|
-
this.selectedDate = selectedDate;
|
|
560
|
-
this.currentMonth = new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Get a map of dates to occurrences
|
|
565
|
-
*/
|
|
566
|
-
getDatesWithOccurrences() {
|
|
567
|
-
const map = new Map();
|
|
568
|
-
for (const occ of this.occurrences) {
|
|
569
|
-
const date = new Date(occ.startsAt);
|
|
570
|
-
date.setHours(0, 0, 0, 0);
|
|
571
|
-
const dateStr = date.toDateString();
|
|
572
|
-
if (!map.has(dateStr)) {
|
|
573
|
-
map.set(dateStr, []);
|
|
574
|
-
}
|
|
575
|
-
map.get(dateStr).push(occ);
|
|
576
|
-
}
|
|
577
|
-
return map;
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Get the first and last occurrence dates
|
|
581
|
-
*/
|
|
582
|
-
getDateRange() {
|
|
583
|
-
if (this.occurrences.length === 0)
|
|
584
|
-
return null;
|
|
585
|
-
let first = new Date(this.occurrences[0].startsAt);
|
|
586
|
-
let last = new Date(this.occurrences[0].startsAt);
|
|
587
|
-
for (const occ of this.occurrences) {
|
|
588
|
-
const date = new Date(occ.startsAt);
|
|
589
|
-
if (date < first)
|
|
590
|
-
first = date;
|
|
591
|
-
if (date > last)
|
|
592
|
-
last = date;
|
|
593
|
-
}
|
|
594
|
-
return { first, last };
|
|
595
|
-
}
|
|
596
|
-
/**
|
|
597
|
-
* Check if all occurrences fit in one week (Monday-Sunday)
|
|
598
|
-
*/
|
|
599
|
-
isSingleWeek() {
|
|
600
|
-
const dateRange = this.getDateRange();
|
|
601
|
-
if (!dateRange)
|
|
602
|
-
return false;
|
|
603
|
-
// Get Monday of the week containing the first date
|
|
604
|
-
const firstMonday = new Date(dateRange.first);
|
|
605
|
-
const dayOfWeek = firstMonday.getDay();
|
|
606
|
-
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Monday = 0 days
|
|
607
|
-
firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
|
|
608
|
-
firstMonday.setHours(0, 0, 0, 0);
|
|
609
|
-
// Get Sunday of that week
|
|
610
|
-
const sunday = new Date(firstMonday);
|
|
611
|
-
sunday.setDate(sunday.getDate() + 6);
|
|
612
|
-
sunday.setHours(23, 59, 59, 999);
|
|
613
|
-
// Check if last date is within this week
|
|
614
|
-
return dateRange.last <= sunday;
|
|
615
|
-
}
|
|
616
80
|
/**
|
|
617
|
-
*
|
|
81
|
+
* Off screen: run the mount's teardown, and nothing more. Not `destroy()`:
|
|
82
|
+
* Stencil calls this for a DOM move as well as a removal, and a destroyed
|
|
83
|
+
* core never mounts again.
|
|
618
84
|
*/
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Check if we should use horizontal layout
|
|
628
|
-
*/
|
|
629
|
-
isHorizontalLayout() {
|
|
630
|
-
if (this.orientation === 'horizontal')
|
|
631
|
-
return true;
|
|
632
|
-
if (this.orientation === 'vertical')
|
|
633
|
-
return false;
|
|
634
|
-
// Auto: use horizontal if container is wide enough (768px)
|
|
635
|
-
return this.containerWidth >= 768;
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Format a date for display
|
|
639
|
-
*/
|
|
640
|
-
formatDate(date) {
|
|
641
|
-
return date.toLocaleDateString('en-GB', {
|
|
642
|
-
weekday: 'long',
|
|
643
|
-
day: 'numeric',
|
|
644
|
-
month: 'long',
|
|
645
|
-
year: 'numeric',
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
/**
|
|
649
|
-
* Format a time for display
|
|
650
|
-
*/
|
|
651
|
-
formatTime(dateString) {
|
|
652
|
-
const date = new Date(dateString);
|
|
653
|
-
return date.toLocaleTimeString('en-GB', {
|
|
654
|
-
hour: '2-digit',
|
|
655
|
-
minute: '2-digit',
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
/**
|
|
659
|
-
* Format a full date and time for list view
|
|
660
|
-
*/
|
|
661
|
-
formatDateTime(dateString) {
|
|
662
|
-
const date = new Date(dateString);
|
|
663
|
-
return {
|
|
664
|
-
date: date.toLocaleDateString('en-GB', {
|
|
665
|
-
weekday: 'long',
|
|
666
|
-
day: 'numeric',
|
|
667
|
-
month: 'long',
|
|
668
|
-
year: 'numeric',
|
|
669
|
-
}),
|
|
670
|
-
time: date.toLocaleTimeString('en-GB', {
|
|
671
|
-
hour: '2-digit',
|
|
672
|
-
minute: '2-digit',
|
|
673
|
-
}),
|
|
674
|
-
};
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Handle occurrence selection
|
|
678
|
-
*/
|
|
679
|
-
handleSelect(occurrence) {
|
|
680
|
-
if (occurrence.soldOut || occurrence.availableCapacity === 0)
|
|
681
|
-
return;
|
|
682
|
-
console.log('[tl-occurrence-selector] Selected:', occurrence.id);
|
|
683
|
-
this.tlSelect.emit({ occurrence });
|
|
684
|
-
}
|
|
685
|
-
/**
|
|
686
|
-
* Handle date selection in calendar view
|
|
687
|
-
*/
|
|
688
|
-
handleDateSelect(date) {
|
|
689
|
-
this.selectedDate = date;
|
|
690
|
-
// If date is in a different month, navigate to that month
|
|
691
|
-
if (date.getMonth() !== this.currentMonth.getMonth() ||
|
|
692
|
-
date.getFullYear() !== this.currentMonth.getFullYear()) {
|
|
693
|
-
this.currentMonth = new Date(date.getFullYear(), date.getMonth(), 1);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* Navigate to previous month
|
|
698
|
-
*/
|
|
699
|
-
handlePrevMonth() {
|
|
700
|
-
const dateRange = this.getDateRange();
|
|
701
|
-
if (!dateRange)
|
|
702
|
-
return;
|
|
703
|
-
const newMonth = new Date(this.currentMonth);
|
|
704
|
-
newMonth.setMonth(newMonth.getMonth() - 1);
|
|
705
|
-
// Don't go before the first occurrence month
|
|
706
|
-
const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
|
|
707
|
-
if (newMonth >= firstMonth) {
|
|
708
|
-
this.currentMonth = newMonth;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Navigate to next month
|
|
713
|
-
*/
|
|
714
|
-
handleNextMonth() {
|
|
715
|
-
const dateRange = this.getDateRange();
|
|
716
|
-
if (!dateRange)
|
|
717
|
-
return;
|
|
718
|
-
const newMonth = new Date(this.currentMonth);
|
|
719
|
-
newMonth.setMonth(newMonth.getMonth() + 1);
|
|
720
|
-
// Don't go past the last occurrence month
|
|
721
|
-
const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
|
|
722
|
-
if (newMonth <= lastMonth) {
|
|
723
|
-
this.currentMonth = newMonth;
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Check if prev month button should be disabled
|
|
728
|
-
*/
|
|
729
|
-
isPrevMonthDisabled() {
|
|
730
|
-
const dateRange = this.getDateRange();
|
|
731
|
-
if (!dateRange)
|
|
732
|
-
return true;
|
|
733
|
-
const firstMonth = new Date(dateRange.first.getFullYear(), dateRange.first.getMonth(), 1);
|
|
734
|
-
return this.currentMonth <= firstMonth;
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Check if next month button should be disabled
|
|
738
|
-
*/
|
|
739
|
-
isNextMonthDisabled() {
|
|
740
|
-
const dateRange = this.getDateRange();
|
|
741
|
-
if (!dateRange)
|
|
742
|
-
return true;
|
|
743
|
-
const lastMonth = new Date(dateRange.last.getFullYear(), dateRange.last.getMonth(), 1);
|
|
744
|
-
return this.currentMonth >= lastMonth;
|
|
745
|
-
}
|
|
746
|
-
/**
|
|
747
|
-
* Get calendar grid days for the current month
|
|
748
|
-
*/
|
|
749
|
-
getCalendarDays() {
|
|
750
|
-
// If all occurrences fit in one week, only show that week
|
|
751
|
-
if (this.isSingleWeek()) {
|
|
752
|
-
const dateRange = this.getDateRange();
|
|
753
|
-
if (!dateRange)
|
|
754
|
-
return [];
|
|
755
|
-
// Get Monday of the week containing the first date
|
|
756
|
-
const firstMonday = new Date(dateRange.first);
|
|
757
|
-
const dayOfWeek = firstMonday.getDay();
|
|
758
|
-
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
|
759
|
-
firstMonday.setDate(firstMonday.getDate() - daysToSubtract);
|
|
760
|
-
firstMonday.setHours(0, 0, 0, 0);
|
|
761
|
-
// Generate 7 days (Monday to Sunday)
|
|
762
|
-
const days = [];
|
|
763
|
-
const current = new Date(firstMonday);
|
|
764
|
-
for (let i = 0; i < 7; i++) {
|
|
765
|
-
days.push(new Date(current));
|
|
766
|
-
current.setDate(current.getDate() + 1);
|
|
767
|
-
}
|
|
768
|
-
return days;
|
|
769
|
-
}
|
|
770
|
-
// Otherwise show full month grid
|
|
771
|
-
const year = this.currentMonth.getFullYear();
|
|
772
|
-
const month = this.currentMonth.getMonth();
|
|
773
|
-
// Start of month
|
|
774
|
-
const monthStart = new Date(year, month, 1);
|
|
775
|
-
// End of month
|
|
776
|
-
const monthEnd = new Date(year, month + 1, 0);
|
|
777
|
-
// Get the start of the week containing the first day (Monday = 1)
|
|
778
|
-
const startDay = monthStart.getDay();
|
|
779
|
-
const daysToSubtract = startDay === 0 ? 6 : startDay - 1; // Monday = 0 days to subtract
|
|
780
|
-
const calendarStart = new Date(monthStart);
|
|
781
|
-
calendarStart.setDate(calendarStart.getDate() - daysToSubtract);
|
|
782
|
-
// Get the end of the week containing the last day
|
|
783
|
-
const endDay = monthEnd.getDay();
|
|
784
|
-
const daysToAdd = endDay === 0 ? 0 : 7 - endDay;
|
|
785
|
-
const calendarEnd = new Date(monthEnd);
|
|
786
|
-
calendarEnd.setDate(calendarEnd.getDate() + daysToAdd);
|
|
787
|
-
// Generate all days
|
|
788
|
-
const days = [];
|
|
789
|
-
const current = new Date(calendarStart);
|
|
790
|
-
while (current <= calendarEnd) {
|
|
791
|
-
days.push(new Date(current));
|
|
792
|
-
current.setDate(current.getDate() + 1);
|
|
793
|
-
}
|
|
794
|
-
return days;
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Check if a date has occurrences
|
|
798
|
-
*/
|
|
799
|
-
hasOccurrencesOnDate(date) {
|
|
800
|
-
const dateStr = date.toDateString();
|
|
801
|
-
return this.getDatesWithOccurrences().has(dateStr);
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Check if all occurrences on a date are sold out
|
|
805
|
-
*/
|
|
806
|
-
isDateSoldOut(date) {
|
|
807
|
-
const dateStr = date.toDateString();
|
|
808
|
-
const occurrences = this.getDatesWithOccurrences().get(dateStr);
|
|
809
|
-
if (!occurrences)
|
|
810
|
-
return false;
|
|
811
|
-
return occurrences.every(occ => occ.soldOut || occ.availableCapacity === 0);
|
|
85
|
+
disconnectedCallback() {
|
|
86
|
+
this.resizeObserver?.disconnect();
|
|
87
|
+
this.resizeObserver = undefined;
|
|
88
|
+
this.teardown?.();
|
|
89
|
+
this.teardown = undefined;
|
|
90
|
+
this.unsubscribe?.();
|
|
91
|
+
this.unsubscribe = undefined;
|
|
812
92
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
getSelectedDateOccurrences() {
|
|
817
|
-
if (!this.selectedDate)
|
|
818
|
-
return [];
|
|
819
|
-
const dateStr = this.selectedDate.toDateString();
|
|
820
|
-
const occurrences = this.getDatesWithOccurrences().get(dateStr) || [];
|
|
821
|
-
// Sort by start time
|
|
822
|
-
return occurrences.sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime());
|
|
93
|
+
forward(event, detail) {
|
|
94
|
+
if (event === 'tlSelect')
|
|
95
|
+
this.tlSelect.emit(detail);
|
|
823
96
|
}
|
|
824
97
|
/**
|
|
825
|
-
*
|
|
98
|
+
* The host's theme, as custom properties over the ones the theme package
|
|
99
|
+
* ships. Web only: a custom property is not a fact every runtime has, so the
|
|
100
|
+
* core declares the prop and the harness is what acts on it.
|
|
826
101
|
*/
|
|
827
|
-
|
|
828
|
-
if (!this.theme)
|
|
829
|
-
return {};
|
|
102
|
+
themeStyles() {
|
|
830
103
|
const styles = {};
|
|
831
|
-
if (this.theme
|
|
832
|
-
styles
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
if (this.theme.backgroundColor) {
|
|
838
|
-
styles['--tl-background'] = this.theme.backgroundColor;
|
|
839
|
-
}
|
|
840
|
-
if (this.theme.foregroundColor) {
|
|
841
|
-
styles['--tl-foreground'] = this.theme.foregroundColor;
|
|
842
|
-
}
|
|
843
|
-
if (this.theme.mutedColor) {
|
|
844
|
-
styles['--tl-muted'] = this.theme.mutedColor;
|
|
845
|
-
}
|
|
846
|
-
if (this.theme.mutedForeground) {
|
|
847
|
-
styles['--tl-muted-foreground'] = this.theme.mutedForeground;
|
|
848
|
-
}
|
|
849
|
-
if (this.theme.borderColor) {
|
|
850
|
-
styles['--tl-border'] = this.theme.borderColor;
|
|
851
|
-
}
|
|
852
|
-
if (this.theme.borderRadius) {
|
|
853
|
-
styles['--tl-radius'] = this.theme.borderRadius;
|
|
854
|
-
}
|
|
855
|
-
if (this.theme.fontFamily) {
|
|
856
|
-
styles['--tl-font-family'] = this.theme.fontFamily;
|
|
104
|
+
if (!this.theme)
|
|
105
|
+
return styles;
|
|
106
|
+
for (const [field, property] of THEME_PROPERTIES) {
|
|
107
|
+
const value = this.theme[field];
|
|
108
|
+
if (value)
|
|
109
|
+
styles[property] = value;
|
|
857
110
|
}
|
|
858
111
|
return styles;
|
|
859
112
|
}
|
|
860
113
|
// =========================================================================
|
|
861
114
|
// RENDER METHODS
|
|
862
115
|
// =========================================================================
|
|
863
|
-
/**
|
|
864
|
-
|
|
865
|
-
|
|
116
|
+
/** The availability mark a row carries, in the list and the slots panel. */
|
|
117
|
+
renderRowStatus(row) {
|
|
118
|
+
const { strings } = this.view;
|
|
119
|
+
if (row.statusKind === 'soldOut')
|
|
120
|
+
return h("span", { class: "status-sold-out" }, strings.soldOut);
|
|
121
|
+
if (row.statusKind === 'available')
|
|
122
|
+
return h("span", { class: "status-available" }, row.capacityLabel);
|
|
123
|
+
return h("span", { class: "status-arrow" }, "\u2192");
|
|
124
|
+
}
|
|
125
|
+
/** The list view (five occurrences or fewer). */
|
|
866
126
|
renderListView() {
|
|
867
|
-
return (h("div", { class: "list-view" }, this.
|
|
868
|
-
const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
|
|
869
|
-
const { date, time } = this.formatDateTime(occurrence.startsAt);
|
|
870
|
-
return (h("button", { key: occurrence.id, class: `occurrence-item ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("div", { class: "occurrence-info" }, h("div", { class: "occurrence-date" }, date), h("div", { class: "occurrence-time" }, time, occurrence.endsAt && ` - ${this.formatTime(occurrence.endsAt)}`)), h("div", { class: "occurrence-status" }, isSoldOut ? (h("span", { class: "status-sold-out" }, "Sold Out")) : occurrence.availableCapacity ? (h("span", { class: "status-available" }, occurrence.availableCapacity, " available")) : (h("span", { class: "status-arrow" }, "\u2192")))));
|
|
871
|
-
})));
|
|
127
|
+
return (h("div", { class: "list-view" }, this.view.derived.rows.map((row) => (h("button", { key: row.id, class: `occurrence-item ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("div", { class: "occurrence-info" }, h("div", { class: "occurrence-date" }, row.dateLabel), h("div", { class: "occurrence-time" }, row.timeLabel)), h("div", { class: "occurrence-status" }, this.renderRowStatus(row)))))));
|
|
872
128
|
}
|
|
873
|
-
/**
|
|
874
|
-
* Render the time slots view (>5 occurrences, same day)
|
|
875
|
-
*/
|
|
129
|
+
/** The time slots view (more than five, all on one date). */
|
|
876
130
|
renderSlotsView() {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
// Get the date from the first occurrence
|
|
880
|
-
const dateLabel = sortedOccurrences.length > 0
|
|
881
|
-
? this.formatDate(new Date(sortedOccurrences[0].startsAt))
|
|
882
|
-
: '';
|
|
883
|
-
return (h("div", { class: "slots-view" }, dateLabel && h("div", { class: "slots-date-header" }, dateLabel), h("div", { class: "time-slots-grid" }, sortedOccurrences.map((occurrence) => {
|
|
884
|
-
const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
|
|
885
|
-
return (h("button", { key: occurrence.id, class: `time-slot ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("span", { class: "slot-time" }, this.formatTime(occurrence.startsAt)), isSoldOut && h("span", { class: "slot-sold-out" }, "Sold Out")));
|
|
886
|
-
}))));
|
|
131
|
+
const { slots, slotsDateLabel } = this.view.derived;
|
|
132
|
+
return (h("div", { class: "slots-view" }, slotsDateLabel && h("div", { class: "slots-date-header" }, slotsDateLabel), h("div", { class: "time-slots-grid" }, slots.map((row) => (h("button", { key: row.id, class: `time-slot ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("span", { class: "slot-time" }, row.startLabel), row.soldOut && h("span", { class: "slot-sold-out" }, this.view.strings.soldOut)))))));
|
|
887
133
|
}
|
|
888
|
-
/**
|
|
889
|
-
* Render the calendar header
|
|
890
|
-
*/
|
|
891
134
|
renderCalendarHeader() {
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
return (h("div", { class: "calendar-header" }, h("span", { class: "month-label" }, monthLabel), showNavigation && (h("div", { class: "calendar-nav-buttons" }, h("button", { class: "nav-button", onClick: () => this.handlePrevMonth(), disabled: this.isPrevMonthDisabled(), "aria-label": "Previous month" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M15 18l-6-6 6-6" }))), h("button", { class: "nav-button", onClick: () => this.handleNextMonth(), disabled: this.isNextMonthDisabled(), "aria-label": "Next month" }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M9 18l6-6-6-6" })))))));
|
|
135
|
+
const { strings, derived } = this.view;
|
|
136
|
+
return (h("div", { class: "calendar-header" }, h("span", { class: "month-label" }, derived.monthLabel), derived.showMonthNav && (h("div", { class: "calendar-nav-buttons" }, h("button", { class: "nav-button", onClick: () => this.element.actions.previousMonth(), disabled: !derived.canGoBack, "aria-label": strings.previousMonth }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M15 18l-6-6 6-6" }))), h("button", { class: "nav-button", onClick: () => this.element.actions.nextMonth(), disabled: !derived.canGoForward, "aria-label": strings.nextMonth }, h("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("path", { d: "M9 18l6-6-6-6" })))))));
|
|
137
|
+
}
|
|
138
|
+
renderCalendarDay(day) {
|
|
139
|
+
return (h("button", { key: day.key, class: day.className, onClick: () => this.element.actions.selectDate(day.key), disabled: !day.selectable }, h("span", { class: "day-number" }, day.dayNumber), day.hasOccurrences && h("span", { class: "occurrence-dot" })));
|
|
898
140
|
}
|
|
899
|
-
/**
|
|
900
|
-
* Render the calendar grid
|
|
901
|
-
*/
|
|
902
141
|
renderCalendarGrid() {
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
const today = new Date();
|
|
906
|
-
today.setHours(0, 0, 0, 0);
|
|
907
|
-
return (h("div", { class: "calendar-grid" }, weekDays.map((day) => (h("div", { class: "weekday-header", key: day }, day))), days.map((date) => {
|
|
908
|
-
const isCurrentMonth = date.getMonth() === this.currentMonth.getMonth();
|
|
909
|
-
const hasOccurrences = this.hasOccurrencesOnDate(date);
|
|
910
|
-
const isSoldOut = this.isDateSoldOut(date);
|
|
911
|
-
const isSelected = this.selectedDate && date.toDateString() === this.selectedDate.toDateString();
|
|
912
|
-
const isToday = date.toDateString() === today.toDateString();
|
|
913
|
-
const isPast = date < today;
|
|
914
|
-
return (h("button", { key: date.toISOString(), class: `calendar-day
|
|
915
|
-
${!isCurrentMonth ? 'other-month' : ''}
|
|
916
|
-
${hasOccurrences ? 'has-occurrences' : ''}
|
|
917
|
-
${isSoldOut ? 'sold-out' : ''}
|
|
918
|
-
${isSelected ? 'selected' : ''}
|
|
919
|
-
${isToday ? 'today' : ''}
|
|
920
|
-
${isPast ? 'past' : ''}`, onClick: () => hasOccurrences && this.handleDateSelect(date), disabled: !hasOccurrences }, h("span", { class: "day-number" }, date.getDate()), hasOccurrences && h("span", { class: "occurrence-dot" })));
|
|
921
|
-
})));
|
|
142
|
+
const { derived } = this.view;
|
|
143
|
+
return (h("div", { class: "calendar-grid" }, derived.weekdayLabels.map((day) => (h("div", { class: "weekday-header", key: day }, day))), derived.days.map((day) => this.renderCalendarDay(day))));
|
|
922
144
|
}
|
|
923
|
-
/**
|
|
924
|
-
* Render the selected date's time slots
|
|
925
|
-
*/
|
|
145
|
+
/** The picked date's times, beside or below the calendar. */
|
|
926
146
|
renderSelectedDateSlots() {
|
|
927
|
-
const
|
|
928
|
-
if (!
|
|
929
|
-
return (h("div", { class: "selected-date-slots empty" }, h("p", null,
|
|
147
|
+
const { strings, derived } = this.view;
|
|
148
|
+
if (!derived.selectedDateLabel) {
|
|
149
|
+
return (h("div", { class: "selected-date-slots empty" }, h("p", null, strings.selectADate)));
|
|
930
150
|
}
|
|
931
|
-
|
|
932
|
-
return (h("div", { class: "selected-date-slots" }, h("div", { class: "selected-date-header" }, dateLabel), h("div", { class: "time-slots-list" }, occurrences.map((occurrence) => {
|
|
933
|
-
const isSoldOut = occurrence.soldOut || occurrence.availableCapacity === 0;
|
|
934
|
-
return (h("button", { key: occurrence.id, class: `time-slot-item ${isSoldOut ? 'sold-out' : ''}`, onClick: () => this.handleSelect(occurrence), disabled: isSoldOut }, h("span", { class: "slot-time" }, this.formatTime(occurrence.startsAt)), h("div", { class: "slot-status" }, isSoldOut ? (h("span", { class: "status-sold-out" }, "Sold Out")) : occurrence.availableCapacity ? (h("span", { class: "status-available" }, occurrence.availableCapacity, " available")) : (h("span", { class: "status-arrow" }, "\u2192")))));
|
|
935
|
-
}))));
|
|
151
|
+
return (h("div", { class: "selected-date-slots" }, h("div", { class: "selected-date-header" }, derived.selectedDateLabel), h("div", { class: "time-slots-list" }, derived.selectedSlots.map((row) => (h("button", { key: row.id, class: `time-slot-item ${row.soldOut ? 'sold-out' : ''}`, onClick: () => this.element.actions.select(row.id), disabled: row.soldOut }, h("span", { class: "slot-time" }, row.startLabel), h("div", { class: "slot-status" }, this.renderRowStatus(row))))))));
|
|
936
152
|
}
|
|
937
|
-
/**
|
|
938
|
-
* Render the calendar view with slots panel
|
|
939
|
-
*/
|
|
940
153
|
renderCalendarView() {
|
|
941
|
-
|
|
942
|
-
return (h("div", { class: `calendar-view ${isHorizontal ? 'horizontal' : 'vertical'}` }, h("div", { class: "calendar-panel" }, this.renderCalendarHeader(), this.renderCalendarGrid()), h("div", { class: "slots-panel" }, this.renderSelectedDateSlots())));
|
|
154
|
+
return (h("div", { class: this.view.derived.viewClass }, h("div", { class: "calendar-panel" }, this.renderCalendarHeader(), this.renderCalendarGrid()), h("div", { class: "slots-panel" }, this.renderSelectedDateSlots())));
|
|
943
155
|
}
|
|
944
|
-
/**
|
|
945
|
-
* Render skeleton loading state
|
|
946
|
-
*/
|
|
156
|
+
/** The skeleton, which is a calendar because that is the common shape. */
|
|
947
157
|
renderSkeleton() {
|
|
948
|
-
// Default to calendar skeleton as it's the most common
|
|
949
158
|
return (h("div", { class: "loading-container" }, h("div", { class: "skeleton-calendar" }, h("div", { class: "skeleton skeleton-calendar-header" }), h("div", { class: "skeleton-calendar-grid" }, Array.from({ length: 35 }, (_, i) => (h("div", { key: i, class: "skeleton skeleton-calendar-day" })))))));
|
|
950
159
|
}
|
|
951
160
|
render() {
|
|
952
|
-
const
|
|
953
|
-
return (h("div", { key: '
|
|
161
|
+
const { status, strings, state, derived } = this.view;
|
|
162
|
+
return (h("div", { key: '84951fb5171ed6c79604861a171f49f43c8b809c', class: "occurrence-selector", ref: (el) => (this.containerRef = el), style: this.themeStyles() }, status === 'loading' && this.renderSkeleton(), status === 'error' && (h("div", { key: '2850712eae277f32f3538426c8a5d36b73ca918e', class: "error-container" }, h("svg", { key: '89192cd7cce29781b87c83c8dee71d59f6a685ed', class: "error-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2" }, h("circle", { key: '27a52ecb1228c541202752be84323abe687c1655', cx: "12", cy: "12", r: "10" }), h("path", { key: 'f51c2b49de89b9c66cde01e11a2de4c56d4dbad9', d: "M12 8v4m0 4h.01" })), h("p", { key: '3c85d762de430f477d2c9ad4341b23765c93e2e3', class: "error-message" }, state.error), h("button", { key: '22fa271a8f0a15e18141cf6d66a25a2bf9299c72', class: "retry-button", onClick: () => void this.element.actions.load() }, strings.retry))), status === 'empty' && (h("div", { key: '285403135ccff6cd6c8de6a9c62c9186c67c102c', class: "no-occurrences" }, h("p", { key: '98373f0c5793cb1aba1328558fac3210e7c6aeeb' }, strings.empty))), status === 'ready' && (h("div", { key: 'a61d2c02538f3339e54770e53a4f807e72182357', class: "selector-content" }, derived.mode === 'list' && this.renderListView(), derived.mode === 'slots' && this.renderSlotsView(), derived.mode === 'calendar' && this.renderCalendarView()))));
|
|
954
163
|
}
|
|
955
164
|
static get watchers() { return {
|
|
956
165
|
"eventId": [{
|
|
957
|
-
"
|
|
166
|
+
"watchSubject": 0
|
|
167
|
+
}],
|
|
168
|
+
"displayMode": [{
|
|
169
|
+
"syncProps": 0
|
|
170
|
+
}],
|
|
171
|
+
"orientation": [{
|
|
172
|
+
"syncProps": 0
|
|
173
|
+
}],
|
|
174
|
+
"theme": [{
|
|
175
|
+
"syncProps": 0
|
|
958
176
|
}]
|
|
959
177
|
}; }
|
|
960
178
|
};
|
|
@@ -978,6 +196,9 @@ const TlTicketSelector = class {
|
|
|
978
196
|
this.buttonLabel = 'Add to Cart';
|
|
979
197
|
this.element = createElement(ticketSelectorCore, {
|
|
980
198
|
client: () => getSDK(),
|
|
199
|
+
// What "the client is up" means on the web. The core waits on it in its
|
|
200
|
+
// own mount; the harness no longer decides when to load (TKT-197).
|
|
201
|
+
clientReady: () => whenSDK(),
|
|
981
202
|
emit: (event, detail) => this.forward(event, detail),
|
|
982
203
|
});
|
|
983
204
|
/** The core's whole view. Assigning it is what makes Stencil re-render. */
|
|
@@ -995,9 +216,14 @@ const TlTicketSelector = class {
|
|
|
995
216
|
set feeQuote(quote) {
|
|
996
217
|
this.element.actions.setFeeQuote(quote);
|
|
997
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* A new event or occurrence is a new subject: the element is torn down and
|
|
221
|
+
* mounted again, so whatever the core set up for the old one is undone
|
|
222
|
+
* before anything is set up for the new one.
|
|
223
|
+
*/
|
|
998
224
|
watchPropsChange() {
|
|
999
225
|
this.syncProps();
|
|
1000
|
-
this.
|
|
226
|
+
this.remount();
|
|
1001
227
|
}
|
|
1002
228
|
syncProps() {
|
|
1003
229
|
this.element.setProps({
|
|
@@ -1007,32 +233,38 @@ const TlTicketSelector = class {
|
|
|
1007
233
|
buttonLabel: this.buttonLabel,
|
|
1008
234
|
});
|
|
1009
235
|
}
|
|
1010
|
-
|
|
236
|
+
/**
|
|
237
|
+
* On screen: render what the core says and put the core to work. What "to
|
|
238
|
+
* work" means is the core's own business - for this element, waiting for the
|
|
239
|
+
* client and reading the ticket types.
|
|
240
|
+
*
|
|
241
|
+
* `connectedCallback` and not `componentWillLoad` because this runs again
|
|
242
|
+
* when the element is moved in the DOM, and that is exactly when a mount and
|
|
243
|
+
* its teardown have to pair up.
|
|
244
|
+
*/
|
|
245
|
+
connectedCallback() {
|
|
1011
246
|
this.syncProps();
|
|
1012
|
-
this.unsubscribe = this.element.subscribe((view) => {
|
|
247
|
+
this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
|
|
1013
248
|
this.view = view;
|
|
1014
|
-
});
|
|
249
|
+
}));
|
|
1015
250
|
this.view = this.element.view;
|
|
1016
|
-
|
|
251
|
+
this.teardown = this.element.mount();
|
|
1017
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Off screen: run the mount's teardown, and nothing more. `destroy()` is
|
|
255
|
+
* terminal and Stencil calls this for a move as well as a removal, so
|
|
256
|
+
* destroying here would leave a re-inserted element frozen. The instance
|
|
257
|
+
* holds no listener and no subscription once these two have run.
|
|
258
|
+
*/
|
|
1018
259
|
disconnectedCallback() {
|
|
260
|
+
this.teardown?.();
|
|
261
|
+
this.teardown = undefined;
|
|
1019
262
|
this.unsubscribe?.();
|
|
1020
|
-
this.
|
|
263
|
+
this.unsubscribe = undefined;
|
|
1021
264
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
async waitForSDKAndLoad() {
|
|
1026
|
-
if (!this.eventId || !this.occurrenceId) {
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
try {
|
|
1030
|
-
await whenSDK();
|
|
1031
|
-
await this.element.actions.load();
|
|
1032
|
-
}
|
|
1033
|
-
catch (err) {
|
|
1034
|
-
this.element.actions.connectFailed(err instanceof Error ? err.message : this.view.strings.noClient);
|
|
1035
|
-
}
|
|
265
|
+
remount() {
|
|
266
|
+
this.teardown?.();
|
|
267
|
+
this.teardown = this.element.mount();
|
|
1036
268
|
}
|
|
1037
269
|
forward(event, detail) {
|
|
1038
270
|
if (event === 'tlAddToCart')
|