@ticketlayer/elements-core 0.1.0 → 0.2.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 +141 -4
- package/dist/cjs/cores/buy-tickets-button.js +213 -0
- package/dist/cjs/cores/buy-tickets-wrapper.js +106 -0
- package/dist/cjs/cores/cart-badge.js +150 -0
- package/dist/cjs/cores/cart-drawer.js +122 -0
- package/dist/cjs/cores/cart.js +247 -0
- package/dist/cjs/cores/checkout-wrapper.js +75 -0
- package/dist/cjs/cores/event-card.js +209 -0
- package/dist/cjs/cores/event-list.js +109 -0
- package/dist/cjs/cores/index.js +64 -1
- package/dist/cjs/cores/login.js +309 -0
- package/dist/cjs/cores/my-orders.js +221 -0
- package/dist/cjs/cores/occurrence-selector.js +528 -0
- package/dist/cjs/cores/order-confirmation.js +301 -0
- package/dist/cjs/cores/order-tickets.js +172 -0
- package/dist/cjs/cores/promo-code.js +136 -0
- package/dist/cjs/cores/ticket-selector.js +30 -0
- package/dist/cjs/define.js +76 -0
- package/dist/cjs/index.js +17 -1
- package/dist/cjs/manifest.js +5 -0
- package/dist/cjs/orders.js +169 -0
- package/dist/esm/client.d.ts +106 -0
- package/dist/esm/cores/buy-tickets-button.d.ts +55 -0
- package/dist/esm/cores/buy-tickets-button.js +210 -0
- package/dist/esm/cores/buy-tickets-wrapper.d.ts +27 -0
- package/dist/esm/cores/buy-tickets-wrapper.js +103 -0
- package/dist/esm/cores/cart-badge.d.ts +48 -0
- package/dist/esm/cores/cart-badge.js +147 -0
- package/dist/esm/cores/cart-drawer.d.ts +44 -0
- package/dist/esm/cores/cart-drawer.js +119 -0
- package/dist/esm/cores/cart.d.ts +105 -0
- package/dist/esm/cores/cart.js +244 -0
- package/dist/esm/cores/checkout-wrapper.d.ts +22 -0
- package/dist/esm/cores/checkout-wrapper.js +72 -0
- package/dist/esm/cores/event-card.d.ts +80 -0
- package/dist/esm/cores/event-card.js +205 -0
- package/dist/esm/cores/event-list.d.ts +35 -0
- package/dist/esm/cores/event-list.js +106 -0
- package/dist/esm/cores/index.d.ts +28 -0
- package/dist/esm/cores/index.js +42 -0
- package/dist/esm/cores/login.d.ts +125 -0
- package/dist/esm/cores/login.js +306 -0
- package/dist/esm/cores/my-orders.d.ts +63 -0
- package/dist/esm/cores/my-orders.js +218 -0
- package/dist/esm/cores/occurrence-selector.d.ts +170 -0
- package/dist/esm/cores/occurrence-selector.js +525 -0
- package/dist/esm/cores/order-confirmation.d.ts +99 -0
- package/dist/esm/cores/order-confirmation.js +298 -0
- package/dist/esm/cores/order-tickets.d.ts +46 -0
- package/dist/esm/cores/order-tickets.js +169 -0
- package/dist/esm/cores/promo-code.d.ts +56 -0
- package/dist/esm/cores/promo-code.js +132 -0
- package/dist/esm/cores/ticket-selector.js +30 -0
- package/dist/esm/define.d.ts +146 -0
- package/dist/esm/define.js +75 -0
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.js +2 -1
- package/dist/esm/manifest.d.ts +12 -0
- package/dist/esm/manifest.js +5 -0
- package/dist/esm/orders.d.ts +110 -0
- package/dist/esm/orders.js +154 -0
- package/package.json +2 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cartDrawerCore = exports.CLOSE_INTENT = exports.OPEN_INTENT = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `tl-cart-drawer`: the off-canvas panel the cart slides into.
|
|
6
|
+
*
|
|
7
|
+
* It owns no cart of its own - the panel embeds `tl-cart` - so what is here is
|
|
8
|
+
* the open/closed rule and the two bus intents it listens for, bound inside its
|
|
9
|
+
* own `mount` and unbound by the teardown (TKT-197).
|
|
10
|
+
*
|
|
11
|
+
* Two ways in, and they are not the same, which is the subtlety this core has
|
|
12
|
+
* to keep: an intent, the backdrop, the close button and Escape all go through
|
|
13
|
+
* `setOpen`, which announces the change with `tlOpen` or `tlClose`. The host
|
|
14
|
+
* setting the `open` prop goes through `openChanged`, which does not: the
|
|
15
|
+
* component's `@Watch('open')` moved the panel without emitting, and a host
|
|
16
|
+
* that drove the drawer and also listened would otherwise start hearing its
|
|
17
|
+
* own writes back.
|
|
18
|
+
*
|
|
19
|
+
* The subscription is the client's bus and not the cart manager's: these are
|
|
20
|
+
* UI intents, which `@ticketlayer/live` relays untouched, and nothing here
|
|
21
|
+
* reads the cart.
|
|
22
|
+
*/
|
|
23
|
+
const define_js_1 = require("../define.js");
|
|
24
|
+
/** What opens the drawer, and what closes it. Relayed on the client's bus. */
|
|
25
|
+
exports.OPEN_INTENT = 'ui:cart-requested';
|
|
26
|
+
exports.CLOSE_INTENT = 'ui:modal-close-requested';
|
|
27
|
+
const STRINGS = {
|
|
28
|
+
close: 'Close cart',
|
|
29
|
+
privacy: 'Privacy Policy',
|
|
30
|
+
privacyHref: 'https://ticketlayer.com/privacy',
|
|
31
|
+
reportAbuse: 'Report Abuse',
|
|
32
|
+
reportAbuseHref: 'https://ticketlayer.com/report-abuse',
|
|
33
|
+
poweredBy: 'Powered by',
|
|
34
|
+
brand: 'ticketlayer',
|
|
35
|
+
brandHref: 'https://ticketlayer.com',
|
|
36
|
+
};
|
|
37
|
+
exports.cartDrawerCore = (0, define_js_1.defineElement)({
|
|
38
|
+
tag: 'tl-cart-drawer',
|
|
39
|
+
docs: 'A slide-in panel holding the cart, opened by the cart intent on the client bus.',
|
|
40
|
+
category: 'cart',
|
|
41
|
+
runtimes: ['web'],
|
|
42
|
+
tokens: ['background', 'border', 'font-family', 'foreground', 'muted', 'muted-foreground'],
|
|
43
|
+
sdkMethods: ['client.on'],
|
|
44
|
+
props: {
|
|
45
|
+
side: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
tsType: '"left" | "right"',
|
|
48
|
+
attr: 'side',
|
|
49
|
+
default: 'right',
|
|
50
|
+
docs: 'Edge the panel slides in from.',
|
|
51
|
+
},
|
|
52
|
+
heading: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
attr: 'heading',
|
|
55
|
+
default: 'Your cart',
|
|
56
|
+
docs: 'Heading shown at the top of the panel.',
|
|
57
|
+
},
|
|
58
|
+
open: {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
attr: 'open',
|
|
61
|
+
default: false,
|
|
62
|
+
docs: 'Controlled open state (also driven by the `ui:cart-requested` intent).',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
events: {
|
|
66
|
+
tlOpen: { detail: 'void', docs: '' },
|
|
67
|
+
tlClose: { detail: 'void', docs: '' },
|
|
68
|
+
},
|
|
69
|
+
strings: STRINGS,
|
|
70
|
+
statuses: ['open', 'closed'],
|
|
71
|
+
state: (props) => ({ visible: props.open }),
|
|
72
|
+
/**
|
|
73
|
+
* On screen: take the `open` prop as it stands, then listen for the intents.
|
|
74
|
+
*
|
|
75
|
+
* Taking the prop again is what the component's `connectedCallback` did, and
|
|
76
|
+
* it matters here for a second reason: an instance is built before a harness
|
|
77
|
+
* has read its attributes, so the state a constructor could see is the
|
|
78
|
+
* default rather than the host's.
|
|
79
|
+
*/
|
|
80
|
+
mount: (ctx) => {
|
|
81
|
+
ctx.actions.openChanged(ctx.props.open);
|
|
82
|
+
void (async () => {
|
|
83
|
+
let client;
|
|
84
|
+
try {
|
|
85
|
+
client = await ctx.whenClient();
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// No client, no intents. The drawer still opens from its `open` prop.
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!ctx.active())
|
|
92
|
+
return;
|
|
93
|
+
const offOpen = client.on?.(exports.OPEN_INTENT, () => ctx.actions.setOpen(true));
|
|
94
|
+
if (typeof offOpen === 'function')
|
|
95
|
+
ctx.onTeardown(offOpen);
|
|
96
|
+
const offClose = client.on?.(exports.CLOSE_INTENT, () => ctx.actions.setOpen(false));
|
|
97
|
+
if (typeof offClose === 'function')
|
|
98
|
+
ctx.onTeardown(offClose);
|
|
99
|
+
})();
|
|
100
|
+
},
|
|
101
|
+
status: ({ state }) => (state.visible ? 'open' : 'closed'),
|
|
102
|
+
derive: ({ state, props }) => ({
|
|
103
|
+
visible: state.visible,
|
|
104
|
+
sideClass: `panel--${props.side}`,
|
|
105
|
+
ariaHidden: state.visible ? 'false' : 'true',
|
|
106
|
+
}),
|
|
107
|
+
actions: (ctx) => ({
|
|
108
|
+
/** Open or close, and say so. Asking for the state it is already in is nothing. */
|
|
109
|
+
setOpen(open) {
|
|
110
|
+
if (ctx.state.visible === open)
|
|
111
|
+
return;
|
|
112
|
+
ctx.setState({ visible: open });
|
|
113
|
+
ctx.emit(open ? 'tlOpen' : 'tlClose', undefined);
|
|
114
|
+
},
|
|
115
|
+
/** The host moved the `open` prop. Follow it, and stay quiet about it. */
|
|
116
|
+
openChanged(open) {
|
|
117
|
+
if (ctx.state.visible === open)
|
|
118
|
+
return;
|
|
119
|
+
ctx.setState({ visible: open });
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cartCore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `tl-cart`: the cart as a list of lines, its money rows and a checkout button.
|
|
6
|
+
*
|
|
7
|
+
* The first core that lives on a subscription. It reads the cart once when a
|
|
8
|
+
* harness mounts it and then keeps itself current from the client's
|
|
9
|
+
* `cart:updated` bus, binding that subscription inside its own `mount` and
|
|
10
|
+
* handing the unsubscribe to `ctx.onTeardown` (TKT-197). No harness owns a
|
|
11
|
+
* subscription any more, on any runtime, and a remount cannot leave two
|
|
12
|
+
* behind: `createElement` keeps at most one mount live.
|
|
13
|
+
*
|
|
14
|
+
* Behaviour is what the Stencil component did before the split, to the letter:
|
|
15
|
+
* the same money rows through `cartTotals`, the same compact mode, the same
|
|
16
|
+
* silence when the host published no client, the same events in the same
|
|
17
|
+
* order. What did not come across is the `console.log` tracing, which a library
|
|
18
|
+
* should not do for its host.
|
|
19
|
+
*/
|
|
20
|
+
const define_js_1 = require("../define.js");
|
|
21
|
+
const money_js_1 = require("../money.js");
|
|
22
|
+
const STRINGS = {
|
|
23
|
+
emptyTitle: 'Your cart is empty',
|
|
24
|
+
emptyMessage: 'Add tickets to get started',
|
|
25
|
+
subtotalRow: 'Subtotal',
|
|
26
|
+
discountRow: 'Discount',
|
|
27
|
+
totalRow: 'Total',
|
|
28
|
+
removeItem: 'Remove item',
|
|
29
|
+
quantityPrefix: 'Qty: ',
|
|
30
|
+
loadFailed: 'Failed to load cart',
|
|
31
|
+
removeFailed: 'Failed to remove item',
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The occurrence date, as the component formatted it.
|
|
35
|
+
*
|
|
36
|
+
* `en-GB` is written here rather than taken from the instance's locale because
|
|
37
|
+
* that is what the component did, and this split is behaviour-identical. There
|
|
38
|
+
* is no locale on the view context today; `formatMoney` closes over one and
|
|
39
|
+
* nothing else does.
|
|
40
|
+
*/
|
|
41
|
+
function formatDate(dateString) {
|
|
42
|
+
return new Date(dateString).toLocaleDateString('en-GB', {
|
|
43
|
+
weekday: 'short',
|
|
44
|
+
day: 'numeric',
|
|
45
|
+
month: 'short',
|
|
46
|
+
hour: '2-digit',
|
|
47
|
+
minute: '2-digit',
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
exports.cartCore = (0, define_js_1.defineElement)({
|
|
51
|
+
tag: 'tl-cart',
|
|
52
|
+
docs: "The buyer's cart: its lines, the money rows the surface reported and a checkout button.",
|
|
53
|
+
category: 'cart',
|
|
54
|
+
runtimes: ['web', 'native'],
|
|
55
|
+
tokens: [
|
|
56
|
+
'background',
|
|
57
|
+
'border',
|
|
58
|
+
'error',
|
|
59
|
+
'error-bg',
|
|
60
|
+
'font-family',
|
|
61
|
+
'foreground',
|
|
62
|
+
'muted',
|
|
63
|
+
'muted-foreground',
|
|
64
|
+
'primary',
|
|
65
|
+
'primary-foreground',
|
|
66
|
+
'radius-md',
|
|
67
|
+
'radius-sm',
|
|
68
|
+
'spacing-1',
|
|
69
|
+
'spacing-2',
|
|
70
|
+
'spacing-3',
|
|
71
|
+
'spacing-4',
|
|
72
|
+
'spacing-8',
|
|
73
|
+
],
|
|
74
|
+
sdkMethods: ['cart.get', 'cart.on', 'cart.removeItem'],
|
|
75
|
+
props: {
|
|
76
|
+
showCheckoutButton: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
attr: 'show-checkout-button',
|
|
79
|
+
default: true,
|
|
80
|
+
docs: 'Show checkout button',
|
|
81
|
+
},
|
|
82
|
+
checkoutLabel: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
attr: 'checkout-label',
|
|
85
|
+
default: 'Checkout',
|
|
86
|
+
docs: 'Checkout button label',
|
|
87
|
+
},
|
|
88
|
+
showRemoveButtons: {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
attr: 'show-remove-buttons',
|
|
91
|
+
default: true,
|
|
92
|
+
docs: 'Show remove item buttons',
|
|
93
|
+
},
|
|
94
|
+
compact: { type: 'boolean', attr: 'compact', default: false, docs: 'Compact mode - hides some details' },
|
|
95
|
+
},
|
|
96
|
+
events: {
|
|
97
|
+
tlCheckout: { detail: '{ cart: TlCartData; }', docs: 'Emitted when checkout is clicked' },
|
|
98
|
+
tlItemRemoved: { detail: '{ itemId: string; }', docs: 'Emitted when item is removed' },
|
|
99
|
+
tlCartUpdated: { detail: '{ cart: TlCartData; }', docs: 'Emitted when cart is updated' },
|
|
100
|
+
tlError: { detail: '{ message: string; }', docs: 'Emitted on error' },
|
|
101
|
+
},
|
|
102
|
+
strings: STRINGS,
|
|
103
|
+
statuses: ['loading', 'empty', 'ready'],
|
|
104
|
+
state: () => ({ cart: null, loading: true, error: null, removingItemId: null }),
|
|
105
|
+
/**
|
|
106
|
+
* On screen: wait for the host's client, subscribe to the cart, then read it.
|
|
107
|
+
*
|
|
108
|
+
* Subscribing before the read is deliberate. The component started both at
|
|
109
|
+
* once (`componentWillLoad` read, `componentDidLoad` subscribed) and so could
|
|
110
|
+
* miss an update that landed mid-read; going first closes that window
|
|
111
|
+
* without changing anything a host can see.
|
|
112
|
+
*
|
|
113
|
+
* The subscription is the cart manager's and not the client's: the connector
|
|
114
|
+
* adapts the payload on the cart manager's bus and passes it through
|
|
115
|
+
* untouched on the client's, and this element renders the adapted figures.
|
|
116
|
+
*/
|
|
117
|
+
mount: (ctx) => {
|
|
118
|
+
void (async () => {
|
|
119
|
+
let client;
|
|
120
|
+
try {
|
|
121
|
+
client = await ctx.whenClient();
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (!ctx.active())
|
|
125
|
+
return;
|
|
126
|
+
ctx.actions.connectFailed(err instanceof Error ? err.message : ctx.strings.loadFailed);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!ctx.active())
|
|
130
|
+
return;
|
|
131
|
+
const manager = client.getCartManager?.();
|
|
132
|
+
const off = manager?.on?.('cart:updated', (data) => {
|
|
133
|
+
ctx.actions.cartUpdated(data);
|
|
134
|
+
});
|
|
135
|
+
if (typeof off === 'function')
|
|
136
|
+
ctx.onTeardown(off);
|
|
137
|
+
await ctx.actions.load();
|
|
138
|
+
})();
|
|
139
|
+
},
|
|
140
|
+
status: ({ state }) => state.loading ? 'loading' : !state.cart || state.cart.items.length === 0 ? 'empty' : 'ready',
|
|
141
|
+
derive: (ctx) => {
|
|
142
|
+
const { state, strings, props, formatMoney } = ctx;
|
|
143
|
+
// Money comes from the response through cartTotals: the fee row is shown
|
|
144
|
+
// when the surface expressed a fee, and the total is the server's figure,
|
|
145
|
+
// never a sum taken here.
|
|
146
|
+
const totals = (0, money_js_1.cartTotals)(state.cart);
|
|
147
|
+
const currency = totals.currency;
|
|
148
|
+
const items = state.cart?.items ?? [];
|
|
149
|
+
return {
|
|
150
|
+
rows: items.map((item) => ({
|
|
151
|
+
id: item.id,
|
|
152
|
+
name: item.ticketTypeName,
|
|
153
|
+
eventName: item.eventName,
|
|
154
|
+
dateLabel: item.occurrenceDate ? formatDate(item.occurrenceDate) : null,
|
|
155
|
+
quantityLabel: `${strings.quantityPrefix}${item.quantity}`,
|
|
156
|
+
priceLabel: formatMoney(item.totalPrice, item.currency),
|
|
157
|
+
removing: state.removingItemId === item.id,
|
|
158
|
+
showEvent: !props.compact,
|
|
159
|
+
showDate: !props.compact && !!item.occurrenceDate,
|
|
160
|
+
})),
|
|
161
|
+
totals,
|
|
162
|
+
subtotalLabel: formatMoney(totals.subtotal, currency),
|
|
163
|
+
feeAmountLabel: formatMoney(totals.fees, currency),
|
|
164
|
+
discountLabel: formatMoney(totals.discount, currency),
|
|
165
|
+
totalLabel: formatMoney(totals.total, currency),
|
|
166
|
+
showSubtotalRow: !props.compact,
|
|
167
|
+
showFeeRow: totals.showFee && !props.compact,
|
|
168
|
+
showDiscountRow: totals.discount > 0 && !props.compact,
|
|
169
|
+
checkoutDisabled: items.length === 0,
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
actions: (ctx) => ({
|
|
173
|
+
/** Read the cart as it stands. */
|
|
174
|
+
async load() {
|
|
175
|
+
try {
|
|
176
|
+
ctx.setState({ loading: true, error: null });
|
|
177
|
+
const manager = ctx.optionalClient()?.getCartManager?.();
|
|
178
|
+
if (manager) {
|
|
179
|
+
// Bound rather than called optionally: a connector that carries a
|
|
180
|
+
// cart manager without a `get` is broken, and the component said so
|
|
181
|
+
// through tlError rather than rendering an empty cart.
|
|
182
|
+
const read = manager.get?.bind(manager);
|
|
183
|
+
if (!read)
|
|
184
|
+
throw new TypeError('cartManager.get is not a function');
|
|
185
|
+
ctx.setState({ cart: ((await read()) ?? null) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
const message = err instanceof Error ? err.message : ctx.strings.loadFailed;
|
|
190
|
+
ctx.setState({ error: message });
|
|
191
|
+
ctx.emit('tlError', { message });
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
ctx.setState({ loading: false });
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
/** The cart changed under us. Show it, and tell the host. */
|
|
198
|
+
cartUpdated(cart) {
|
|
199
|
+
ctx.setState({ cart });
|
|
200
|
+
ctx.emit('tlCartUpdated', { cart });
|
|
201
|
+
},
|
|
202
|
+
/**
|
|
203
|
+
* Take one line out of the cart. With no client at all, nothing happens
|
|
204
|
+
* and nothing is announced: the buyer's click is simply inert, which is
|
|
205
|
+
* what it was before.
|
|
206
|
+
*/
|
|
207
|
+
async removeItem(itemId) {
|
|
208
|
+
if (!ctx.optionalClient())
|
|
209
|
+
return;
|
|
210
|
+
ctx.setState({ removingItemId: itemId });
|
|
211
|
+
try {
|
|
212
|
+
const manager = ctx.client().getCartManager?.();
|
|
213
|
+
if (manager) {
|
|
214
|
+
const remove = manager.removeItem?.bind(manager);
|
|
215
|
+
if (!remove)
|
|
216
|
+
throw new TypeError('cartManager.removeItem is not a function');
|
|
217
|
+
await remove(itemId);
|
|
218
|
+
ctx.emit('tlItemRemoved', { itemId });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const message = err instanceof Error ? err.message : ctx.strings.removeFailed;
|
|
223
|
+
ctx.setState({ error: message });
|
|
224
|
+
ctx.emit('tlError', { message });
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
ctx.setState({ removingItemId: null });
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
/** Hand the cart to whatever the host does next. An empty cart says nothing. */
|
|
231
|
+
checkout() {
|
|
232
|
+
const cart = ctx.state.cart;
|
|
233
|
+
if (cart && cart.items.length > 0)
|
|
234
|
+
ctx.emit('tlCheckout', { cart });
|
|
235
|
+
},
|
|
236
|
+
/**
|
|
237
|
+
* The host never produced a client. Announced, because the component
|
|
238
|
+
* announced it: the read and the wait were one try block, so a page that
|
|
239
|
+
* never started Live emitted tlError.
|
|
240
|
+
*/
|
|
241
|
+
connectFailed(message) {
|
|
242
|
+
const text = message || ctx.strings.loadFailed;
|
|
243
|
+
ctx.setState({ error: text, loading: false });
|
|
244
|
+
ctx.emit('tlError', { message: text });
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkoutWrapperCore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* `tl-checkout-wrapper`: the same idea as `tl-buy-tickets-wrapper`, pointed at
|
|
6
|
+
* the checkout rather than at one event.
|
|
7
|
+
*
|
|
8
|
+
* Whatever the host slots in becomes the thing that opens the checkout modal
|
|
9
|
+
* for the cart as it stands, so this core takes no subject at all: the cart is
|
|
10
|
+
* the client's, and the modal is the client's.
|
|
11
|
+
*
|
|
12
|
+
* It was blocked from being split by the same thing its sibling was - events
|
|
13
|
+
* named `tl-complete`, `tl-error` and `tl-close`, which `defineElement`
|
|
14
|
+
* refuses - and is unblocked by the same rename to `tlComplete`, `tlError` and
|
|
15
|
+
* `tlClose` (TKT-203).
|
|
16
|
+
*
|
|
17
|
+
* Behaviour is what the Stencil component did, to the letter, including the
|
|
18
|
+
* `tlError` a click announces when no client has been published; the
|
|
19
|
+
* `console.error` beside it did not come across, for the reason `tl-cart` and
|
|
20
|
+
* `tl-promo-code` dropped theirs.
|
|
21
|
+
*/
|
|
22
|
+
const define_js_1 = require("../define.js");
|
|
23
|
+
const STRINGS = {
|
|
24
|
+
noClient: 'SDK not initialized',
|
|
25
|
+
notAvailable: 'Checkout is not available',
|
|
26
|
+
};
|
|
27
|
+
exports.checkoutWrapperCore = (0, define_js_1.defineElement)({
|
|
28
|
+
tag: 'tl-checkout-wrapper',
|
|
29
|
+
docs: 'Wrap any content and open the checkout modal when it is activated.',
|
|
30
|
+
category: 'checkout',
|
|
31
|
+
runtimes: ['web'],
|
|
32
|
+
tokens: ['radius-sm', 'ring'],
|
|
33
|
+
sdkMethods: ['checkout.openModal'],
|
|
34
|
+
props: {
|
|
35
|
+
disabled: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
attr: 'disabled',
|
|
38
|
+
default: false,
|
|
39
|
+
docs: 'Whether the wrapper is disabled',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
events: {
|
|
43
|
+
// `OrderResult` because that is the name the web harness writes; see
|
|
44
|
+
// `buy-tickets-wrapper.ts`.
|
|
45
|
+
tlComplete: { detail: 'OrderResult', docs: 'Emitted when checkout completes successfully' },
|
|
46
|
+
tlError: { detail: '{ message: string; }', docs: 'Emitted when an error occurs' },
|
|
47
|
+
tlClose: { detail: 'void', docs: 'Emitted when the modal closes' },
|
|
48
|
+
},
|
|
49
|
+
strings: STRINGS,
|
|
50
|
+
statuses: ['ready', 'disabled'],
|
|
51
|
+
state: () => ({}),
|
|
52
|
+
status: ({ props }) => (props.disabled ? 'disabled' : 'ready'),
|
|
53
|
+
derive: ({ props }) => ({ interactive: !props.disabled }),
|
|
54
|
+
actions: (ctx) => ({
|
|
55
|
+
/** A click, a tap, Enter or Space: whatever the harness calls a press. */
|
|
56
|
+
activate() {
|
|
57
|
+
if (ctx.props.disabled)
|
|
58
|
+
return;
|
|
59
|
+
const client = ctx.optionalClient();
|
|
60
|
+
if (!client) {
|
|
61
|
+
ctx.emit('tlError', { message: ctx.strings.noClient });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!client.checkout) {
|
|
65
|
+
ctx.emit('tlError', { message: ctx.strings.notAvailable });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
client.checkout.openModal({
|
|
69
|
+
onComplete: (order) => ctx.emit('tlComplete', order),
|
|
70
|
+
onError: (err) => ctx.emit('tlError', { message: err.message }),
|
|
71
|
+
onClose: () => ctx.emit('tlClose', undefined),
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.eventCardCore = void 0;
|
|
4
|
+
exports.toEventCardEvent = toEventCardEvent;
|
|
5
|
+
/**
|
|
6
|
+
* `tl-event-card`: one event, as a card.
|
|
7
|
+
*
|
|
8
|
+
* Two ways to fill it, which is the whole of its behaviour: a host hands it an
|
|
9
|
+
* event through the `event` prop (that is what `tl-event-list` does), or it is
|
|
10
|
+
* given an `eventId` and reads the event itself. The prop wins, and a card that
|
|
11
|
+
* has one never touches the client.
|
|
12
|
+
*
|
|
13
|
+
* Behaviour is what the Stencil component did before the split, to the letter,
|
|
14
|
+
* including the status badge's three labels and the `en-GB` date. What did not
|
|
15
|
+
* come across is the `console.log` tracing, which a library should not do for
|
|
16
|
+
* its host.
|
|
17
|
+
*/
|
|
18
|
+
const define_js_1 = require("../define.js");
|
|
19
|
+
/**
|
|
20
|
+
* Adapt an event detail to the card shape. The list adapter lives in the SDK
|
|
21
|
+
* connector; this one is the card's own, and it is behaviour, so it is here.
|
|
22
|
+
*/
|
|
23
|
+
function toEventCardEvent(detail) {
|
|
24
|
+
const occurrences = detail.occurrences ?? [];
|
|
25
|
+
const starts = occurrences
|
|
26
|
+
.map((o) => o.startsAt)
|
|
27
|
+
.filter((s) => !!s)
|
|
28
|
+
.sort();
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const next = starts.find((s) => new Date(s).getTime() >= now) ?? starts[0];
|
|
31
|
+
const soldOut = detail.status === 'sold_out' ||
|
|
32
|
+
(occurrences.length > 0 && occurrences.every((o) => o.status === 'sold_out'));
|
|
33
|
+
return {
|
|
34
|
+
id: detail.id,
|
|
35
|
+
name: detail.name,
|
|
36
|
+
...(detail.shortDescription ? { shortDescription: detail.shortDescription } : {}),
|
|
37
|
+
...(detail.imageUrl ? { imageUrl: detail.imageUrl } : {}),
|
|
38
|
+
...(detail.venueName
|
|
39
|
+
? { venue: { id: detail.venueId ?? '', name: detail.venueName, city: detail.venueCity ?? '' } }
|
|
40
|
+
: {}),
|
|
41
|
+
...(next ? { nextOccurrence: { startsAt: next } } : {}),
|
|
42
|
+
availabilityStatus: soldOut ? 'sold_out' : 'available',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const STRINGS = {
|
|
46
|
+
available: 'Available',
|
|
47
|
+
limited: 'Limited',
|
|
48
|
+
soldOut: 'Sold Out',
|
|
49
|
+
notFound: 'Event not found',
|
|
50
|
+
loadFailed: 'Failed to load event',
|
|
51
|
+
};
|
|
52
|
+
/** As the component formatted it. See the note on the cart core's date. */
|
|
53
|
+
function formatDate(dateString) {
|
|
54
|
+
return new Date(dateString).toLocaleDateString('en-GB', {
|
|
55
|
+
weekday: 'short',
|
|
56
|
+
day: 'numeric',
|
|
57
|
+
month: 'short',
|
|
58
|
+
hour: '2-digit',
|
|
59
|
+
minute: '2-digit',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
exports.eventCardCore = (0, define_js_1.defineElement)({
|
|
63
|
+
tag: 'tl-event-card',
|
|
64
|
+
docs: 'One event as a card: its image, name, next date, venue and how it is selling.',
|
|
65
|
+
category: 'discovery',
|
|
66
|
+
runtimes: ['web', 'native'],
|
|
67
|
+
tokens: [
|
|
68
|
+
'error',
|
|
69
|
+
'error-foreground',
|
|
70
|
+
'font-family',
|
|
71
|
+
'foreground',
|
|
72
|
+
'muted',
|
|
73
|
+
'muted-foreground',
|
|
74
|
+
'primary',
|
|
75
|
+
'radius-lg',
|
|
76
|
+
'radius-sm',
|
|
77
|
+
'secondary',
|
|
78
|
+
'spacing-4',
|
|
79
|
+
'success',
|
|
80
|
+
'success-foreground',
|
|
81
|
+
'warning',
|
|
82
|
+
'warning-foreground',
|
|
83
|
+
],
|
|
84
|
+
sdkMethods: ['events.get'],
|
|
85
|
+
props: {
|
|
86
|
+
event: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
tsType: 'TlEventCardEvent | undefined',
|
|
89
|
+
attr: null,
|
|
90
|
+
docs: 'Event data to display (tl-event-list passes this)',
|
|
91
|
+
},
|
|
92
|
+
eventId: {
|
|
93
|
+
type: 'string',
|
|
94
|
+
tsType: 'string | undefined',
|
|
95
|
+
attr: 'event-id',
|
|
96
|
+
docs: 'Load the event from the SDK by id when no `event` is given, so a single\ncard works standalone (e.g. on a Webflow page).',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
events: {
|
|
100
|
+
tlClick: { detail: '{ eventId: string; }', docs: 'Emitted when the card is clicked' },
|
|
101
|
+
tlError: { detail: '{ message: string; }', docs: 'Emitted when the event could not be loaded by id' },
|
|
102
|
+
},
|
|
103
|
+
strings: STRINGS,
|
|
104
|
+
statuses: ['loading', 'error', 'empty', 'ready'],
|
|
105
|
+
state: () => ({ loadedEvent: null, loading: false, error: null }),
|
|
106
|
+
/**
|
|
107
|
+
* On screen: read the event, but only when there is one to read and nobody
|
|
108
|
+
* has handed us the event already.
|
|
109
|
+
*
|
|
110
|
+
* `loading` is set here, synchronously, rather than inside the read. The
|
|
111
|
+
* component awaited its load in `componentWillLoad`, so its first paint was
|
|
112
|
+
* already past it; a mount hook cannot hold the first paint, so the card
|
|
113
|
+
* shows its own skeleton instead of a blank. That is the same change
|
|
114
|
+
* `tl-ticket-selector` took when it moved onto the hook (TKT-197).
|
|
115
|
+
*/
|
|
116
|
+
mount: (ctx) => {
|
|
117
|
+
if (ctx.props.event || !ctx.props.eventId)
|
|
118
|
+
return;
|
|
119
|
+
ctx.setState({ loading: true, error: null });
|
|
120
|
+
void (async () => {
|
|
121
|
+
try {
|
|
122
|
+
await ctx.whenClient();
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (!ctx.active())
|
|
126
|
+
return;
|
|
127
|
+
ctx.actions.loadFailed(err instanceof Error && err.message ? err.message : ctx.strings.loadFailed);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!ctx.active())
|
|
131
|
+
return;
|
|
132
|
+
await ctx.actions.load();
|
|
133
|
+
})();
|
|
134
|
+
},
|
|
135
|
+
status: ({ state, props }) => state.loading ? 'loading' : state.error ? 'error' : props.event || state.loadedEvent ? 'ready' : 'empty',
|
|
136
|
+
derive: ({ state, props, strings }) => {
|
|
137
|
+
const event = props.event ?? state.loadedEvent;
|
|
138
|
+
const badge = (suffix) => `status-badge ${suffix}`;
|
|
139
|
+
const status = event?.availabilityStatus;
|
|
140
|
+
return {
|
|
141
|
+
event,
|
|
142
|
+
// The sales surface exposes a single `imageUrl` (card/listing
|
|
143
|
+
// thumbnail); `thumbnailUrl` is an accepted alias if a host ever
|
|
144
|
+
// supplies a distinct one.
|
|
145
|
+
image: event ? event.thumbnailUrl || event.imageUrl || null : null,
|
|
146
|
+
statusBadgeClass: status === 'available'
|
|
147
|
+
? badge('status-available')
|
|
148
|
+
: status === 'limited'
|
|
149
|
+
? badge('status-limited')
|
|
150
|
+
: status === 'sold_out'
|
|
151
|
+
? badge('status-sold-out')
|
|
152
|
+
: 'status-badge',
|
|
153
|
+
statusLabel: status === 'available'
|
|
154
|
+
? strings.available
|
|
155
|
+
: status === 'limited'
|
|
156
|
+
? strings.limited
|
|
157
|
+
: status === 'sold_out'
|
|
158
|
+
? strings.soldOut
|
|
159
|
+
: '',
|
|
160
|
+
dateLabel: event?.nextOccurrence ? formatDate(event.nextOccurrence.startsAt) : null,
|
|
161
|
+
venueLabel: event?.venue ? `${event.venue.name}, ${event.venue.city}` : null,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
actions: (ctx) => ({
|
|
165
|
+
/**
|
|
166
|
+
* Read the event by id. A card that was handed an event, or has no id, has
|
|
167
|
+
* nothing to read, and this is the guard the component's watcher relied on.
|
|
168
|
+
*/
|
|
169
|
+
async load() {
|
|
170
|
+
if (ctx.props.event || !ctx.props.eventId)
|
|
171
|
+
return;
|
|
172
|
+
try {
|
|
173
|
+
ctx.setState({ loading: true, error: null });
|
|
174
|
+
const events = ctx.client().getEventsManager();
|
|
175
|
+
const read = events.get?.bind(events);
|
|
176
|
+
if (!read)
|
|
177
|
+
throw new TypeError('eventsManager.get is not a function');
|
|
178
|
+
const detail = (await read(ctx.props.eventId));
|
|
179
|
+
if (!detail)
|
|
180
|
+
throw new Error(ctx.strings.notFound);
|
|
181
|
+
ctx.setState({ loadedEvent: toEventCardEvent(detail) });
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
const message = err instanceof Error && err.message ? err.message : ctx.strings.loadFailed;
|
|
185
|
+
ctx.setState({ error: message });
|
|
186
|
+
ctx.emit('tlError', { message });
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
ctx.setState({ loading: false });
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
/**
|
|
193
|
+
* The host never produced a client. Announced, because the component
|
|
194
|
+
* announced it: the wait and the read were one try block.
|
|
195
|
+
*/
|
|
196
|
+
loadFailed(message) {
|
|
197
|
+
const text = message || ctx.strings.loadFailed;
|
|
198
|
+
ctx.setState({ error: text, loading: false });
|
|
199
|
+
ctx.emit('tlError', { message: text });
|
|
200
|
+
},
|
|
201
|
+
/** The buyer picked this card. Where that goes is the host's business. */
|
|
202
|
+
click() {
|
|
203
|
+
const event = ctx.props.event ?? ctx.state.loadedEvent;
|
|
204
|
+
if (!event)
|
|
205
|
+
return;
|
|
206
|
+
ctx.emit('tlClick', { eventId: event.id });
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
});
|