@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.
Files changed (161) hide show
  1. package/dist/cjs/{index-I9CoYv3k.js → index-DanPUX0s.js} +1 -1
  2. package/dist/cjs/index.cjs.js +2 -1
  3. package/dist/cjs/loader.cjs.js +2 -2
  4. package/dist/cjs/ticket-selector-BYVL13k_.js +3623 -0
  5. package/dist/cjs/ticketlayer-elements.cjs.js +2 -2
  6. package/dist/cjs/tl-buy-modal.cjs.entry.js +3 -3
  7. package/dist/cjs/tl-buy-tickets-button.cjs.entry.js +89 -74
  8. package/dist/cjs/tl-buy-tickets-wrapper.cjs.entry.js +61 -30
  9. package/dist/cjs/tl-cart-badge.cjs.entry.js +74 -61
  10. package/dist/cjs/tl-cart-drawer.cjs.entry.js +51 -23
  11. package/dist/cjs/tl-cart.cjs.entry.js +71 -103
  12. package/dist/cjs/tl-checkout-wrapper.cjs.entry.js +51 -29
  13. package/dist/cjs/tl-event-card.cjs.entry.js +43 -105
  14. package/dist/cjs/tl-event-list.cjs.entry.js +72 -35
  15. package/dist/cjs/tl-login.cjs.entry.js +74 -141
  16. package/dist/cjs/tl-my-orders.cjs.entry.js +52 -92
  17. package/dist/cjs/tl-occurrence-selector_2.cjs.entry.js +153 -921
  18. package/dist/cjs/tl-order-confirmation.cjs.entry.js +71 -107
  19. package/dist/cjs/tl-order-tickets_2.cjs.entry.js +48 -74
  20. package/dist/cjs/tl-promo-code.cjs.entry.js +50 -47
  21. package/dist/cjs/tl-seat-map.cjs.entry.js +3 -3
  22. package/dist/cjs/web-query-Ce73jB4k.js +16 -0
  23. package/dist/collection/components/tl-buy-modal/tl-buy-modal.js +2 -2
  24. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.css +37 -0
  25. package/dist/collection/components/tl-buy-tickets-button/tl-buy-tickets-button.js +105 -79
  26. package/dist/collection/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.js +73 -30
  27. package/dist/collection/components/tl-cart/tl-cart.js +82 -107
  28. package/dist/collection/components/tl-cart-badge/tl-cart-badge.js +62 -62
  29. package/dist/collection/components/tl-cart-drawer/tl-cart-drawer.js +56 -24
  30. package/dist/collection/components/tl-checkout-wrapper/tl-checkout-wrapper.js +61 -29
  31. package/dist/collection/components/tl-event-card/event-card-adapter.js +11 -23
  32. package/dist/collection/components/tl-event-card/tl-event-card.js +51 -84
  33. package/dist/collection/components/tl-event-list/tl-event-list.js +82 -38
  34. package/dist/collection/components/tl-login/tl-login.js +83 -147
  35. package/dist/collection/components/tl-my-orders/tl-my-orders.js +58 -96
  36. package/dist/collection/components/tl-occurrence-selector/tl-occurrence-selector.js +137 -516
  37. package/dist/collection/components/tl-order-confirmation/tl-order-confirmation.js +81 -115
  38. package/dist/collection/components/tl-order-tickets/tl-order-tickets.js +57 -92
  39. package/dist/collection/components/tl-promo-code/tl-promo-code.js +55 -50
  40. package/dist/collection/components/tl-seat-map/tl-seat-map.js +2 -2
  41. package/dist/collection/components/tl-ticket-selector/tl-ticket-selector.js +34 -20
  42. package/dist/collection/utils/order-summary.js +15 -135
  43. package/dist/collection/utils/testing/deep-text.js +29 -0
  44. package/dist/collection/utils/testing/fixtures.js +290 -0
  45. package/dist/collection/utils/testing/mock-sdk.js +13 -2
  46. package/dist/collection/utils/web-query.js +12 -0
  47. package/dist/collection/utils/web-storage.js +22 -0
  48. package/dist/components/ticket-selector.js +1 -0
  49. package/dist/components/tl-buy-modal.js +1 -1
  50. package/dist/components/tl-buy-tickets-button.js +1 -1
  51. package/dist/components/tl-buy-tickets-wrapper2.js +1 -1
  52. package/dist/components/tl-cart-badge.js +1 -1
  53. package/dist/components/tl-cart-drawer.js +1 -1
  54. package/dist/components/tl-cart2.js +1 -1
  55. package/dist/components/tl-checkout-wrapper2.js +1 -1
  56. package/dist/components/tl-event-card2.js +1 -1
  57. package/dist/components/tl-event-list.js +1 -1
  58. package/dist/components/tl-login2.js +1 -1
  59. package/dist/components/tl-my-orders.js +1 -1
  60. package/dist/components/tl-occurrence-selector2.js +1 -1
  61. package/dist/components/tl-order-confirmation.js +1 -1
  62. package/dist/components/tl-order-tickets2.js +1 -1
  63. package/dist/components/tl-promo-code.js +1 -1
  64. package/dist/components/tl-seat-map.js +1 -1
  65. package/dist/components/tl-ticket-selector2.js +1 -1
  66. package/dist/components/web-query.js +1 -0
  67. package/dist/elements.docs.json +34 -34
  68. package/dist/esm/{index-OoYV1faY.js → index-imfxp765.js} +1 -1
  69. package/dist/esm/index.js +2 -1
  70. package/dist/esm/loader.js +3 -3
  71. package/dist/esm/ticket-selector-BwChnHVC.js +3606 -0
  72. package/dist/esm/ticketlayer-elements.js +3 -3
  73. package/dist/esm/tl-buy-modal.entry.js +3 -3
  74. package/dist/esm/tl-buy-tickets-button.entry.js +90 -75
  75. package/dist/esm/tl-buy-tickets-wrapper.entry.js +62 -31
  76. package/dist/esm/tl-cart-badge.entry.js +75 -62
  77. package/dist/esm/tl-cart-drawer.entry.js +52 -24
  78. package/dist/esm/tl-cart.entry.js +72 -104
  79. package/dist/esm/tl-checkout-wrapper.entry.js +52 -30
  80. package/dist/esm/tl-event-card.entry.js +44 -106
  81. package/dist/esm/tl-event-list.entry.js +73 -36
  82. package/dist/esm/tl-login.entry.js +75 -142
  83. package/dist/esm/tl-my-orders.entry.js +53 -93
  84. package/dist/esm/tl-occurrence-selector_2.entry.js +152 -920
  85. package/dist/esm/tl-order-confirmation.entry.js +72 -108
  86. package/dist/esm/tl-order-tickets_2.entry.js +49 -75
  87. package/dist/esm/tl-promo-code.entry.js +51 -48
  88. package/dist/esm/tl-seat-map.entry.js +3 -3
  89. package/dist/esm/web-query-Bdbb2lk5.js +14 -0
  90. package/dist/ticketlayer-elements/index.esm.js +1 -1
  91. package/dist/ticketlayer-elements/p-45923d2c.entry.js +1 -0
  92. package/dist/ticketlayer-elements/p-492adc77.entry.js +1 -0
  93. package/dist/ticketlayer-elements/p-4cfc9e53.entry.js +1 -0
  94. package/dist/ticketlayer-elements/{p-ac264adc.entry.js → p-5135630e.entry.js} +1 -1
  95. package/dist/ticketlayer-elements/p-5bbce3b5.entry.js +1 -0
  96. package/dist/ticketlayer-elements/p-7d547721.entry.js +1 -0
  97. package/dist/ticketlayer-elements/p-7fe7b6ba.entry.js +1 -0
  98. package/dist/ticketlayer-elements/p-8122f0c1.entry.js +1 -0
  99. package/dist/ticketlayer-elements/p-98d40869.entry.js +1 -0
  100. package/dist/ticketlayer-elements/p-Bdbb2lk5.js +1 -0
  101. package/dist/ticketlayer-elements/p-BwChnHVC.js +1 -0
  102. package/dist/ticketlayer-elements/p-abec290a.entry.js +1 -0
  103. package/dist/ticketlayer-elements/p-ac79a3f6.entry.js +1 -0
  104. package/dist/ticketlayer-elements/p-d01f4429.entry.js +1 -0
  105. package/dist/ticketlayer-elements/p-d7ffaf46.entry.js +1 -0
  106. package/dist/ticketlayer-elements/p-e7170f3c.entry.js +1 -0
  107. package/dist/ticketlayer-elements/p-e787e862.entry.js +1 -0
  108. package/dist/ticketlayer-elements/p-f1ff931f.entry.js +1 -0
  109. package/dist/ticketlayer-elements/p-imfxp765.js +3 -0
  110. package/dist/ticketlayer-elements/ticketlayer-elements.css +1 -1
  111. package/dist/ticketlayer-elements/ticketlayer-elements.esm.js +1 -1
  112. package/dist/types/components/tl-buy-tickets-button/tl-buy-tickets-button.d.ts +40 -8
  113. package/dist/types/components/tl-buy-tickets-wrapper/tl-buy-tickets-wrapper.d.ts +20 -1
  114. package/dist/types/components/tl-cart/tl-cart.d.ts +40 -42
  115. package/dist/types/components/tl-cart-badge/tl-cart-badge.d.ts +22 -7
  116. package/dist/types/components/tl-cart-drawer/tl-cart-drawer.d.ts +28 -5
  117. package/dist/types/components/tl-checkout-wrapper/tl-checkout-wrapper.d.ts +18 -1
  118. package/dist/types/components/tl-event-card/event-card-adapter.d.ts +9 -20
  119. package/dist/types/components/tl-event-card/tl-event-card.d.ts +25 -28
  120. package/dist/types/components/tl-event-list/tl-event-list.d.ts +46 -6
  121. package/dist/types/components/tl-login/tl-login.d.ts +68 -22
  122. package/dist/types/components/tl-my-orders/tl-my-orders.d.ts +30 -12
  123. package/dist/types/components/tl-occurrence-selector/tl-occurrence-selector.d.ts +73 -161
  124. package/dist/types/components/tl-order-confirmation/tl-order-confirmation.d.ts +48 -15
  125. package/dist/types/components/tl-order-tickets/tl-order-tickets.d.ts +29 -31
  126. package/dist/types/components/tl-promo-code/tl-promo-code.d.ts +28 -10
  127. package/dist/types/components/tl-ticket/tl-ticket.d.ts +2 -11
  128. package/dist/types/components/tl-ticket-selector/tl-ticket-selector.d.ts +22 -4
  129. package/dist/types/components.d.ts +270 -62
  130. package/dist/types/utils/order-summary.d.ts +14 -97
  131. package/dist/types/utils/testing/deep-text.d.ts +13 -0
  132. package/dist/types/utils/testing/fixtures.d.ts +225 -0
  133. package/dist/types/utils/testing/mock-sdk.d.ts +14 -1
  134. package/dist/types/utils/web-query.d.ts +18 -0
  135. package/dist/types/utils/web-storage.d.ts +16 -0
  136. package/elements.catalogue.json +602 -50
  137. package/package.json +8 -6
  138. package/dist/cjs/money-BtmM2gt-.js +0 -118
  139. package/dist/cjs/order-summary-X3TlZxc2.js +0 -150
  140. package/dist/components/money.js +0 -1
  141. package/dist/components/order-summary.js +0 -1
  142. package/dist/esm/money-4ig0-WB6.js +0 -112
  143. package/dist/esm/order-summary-DiDtvfZM.js +0 -136
  144. package/dist/ticketlayer-elements/p-0814420f.entry.js +0 -1
  145. package/dist/ticketlayer-elements/p-0acabc26.entry.js +0 -1
  146. package/dist/ticketlayer-elements/p-109a3feb.entry.js +0 -1
  147. package/dist/ticketlayer-elements/p-1a4ba71c.entry.js +0 -1
  148. package/dist/ticketlayer-elements/p-318965c1.entry.js +0 -1
  149. package/dist/ticketlayer-elements/p-347c7fd1.entry.js +0 -1
  150. package/dist/ticketlayer-elements/p-3649c26d.entry.js +0 -1
  151. package/dist/ticketlayer-elements/p-40260cc9.entry.js +0 -1
  152. package/dist/ticketlayer-elements/p-4ig0-WB6.js +0 -1
  153. package/dist/ticketlayer-elements/p-55fd2f9f.entry.js +0 -1
  154. package/dist/ticketlayer-elements/p-62ee659b.entry.js +0 -1
  155. package/dist/ticketlayer-elements/p-7d4ecc2b.entry.js +0 -1
  156. package/dist/ticketlayer-elements/p-7f0abf95.entry.js +0 -1
  157. package/dist/ticketlayer-elements/p-BXpVGW51.js +0 -1
  158. package/dist/ticketlayer-elements/p-OoYV1faY.js +0 -3
  159. package/dist/ticketlayer-elements/p-a3c1b26c.entry.js +0 -1
  160. package/dist/ticketlayer-elements/p-aeb4c6fb.entry.js +0 -1
  161. package/dist/ticketlayer-elements/p-ba62541f.entry.js +0 -1
@@ -22,6 +22,9 @@ export class TlTicketSelector {
22
22
  this.buttonLabel = 'Add to Cart';
23
23
  this.element = createElement(ticketSelectorCore, {
24
24
  client: () => getSDK(),
25
+ // What "the client is up" means on the web. The core waits on it in its
26
+ // own mount; the harness no longer decides when to load (TKT-197).
27
+ clientReady: () => whenSDK(),
25
28
  emit: (event, detail) => this.forward(event, detail),
26
29
  });
27
30
  /** The core's whole view. Assigning it is what makes Stencil re-render. */
@@ -39,9 +42,14 @@ export class TlTicketSelector {
39
42
  set feeQuote(quote) {
40
43
  this.element.actions.setFeeQuote(quote);
41
44
  }
45
+ /**
46
+ * A new event or occurrence is a new subject: the element is torn down and
47
+ * mounted again, so whatever the core set up for the old one is undone
48
+ * before anything is set up for the new one.
49
+ */
42
50
  watchPropsChange() {
43
51
  this.syncProps();
44
- this.element.actions.load();
52
+ this.remount();
45
53
  }
46
54
  syncProps() {
47
55
  this.element.setProps({
@@ -51,32 +59,38 @@ export class TlTicketSelector {
51
59
  buttonLabel: this.buttonLabel,
52
60
  });
53
61
  }
54
- async componentWillLoad() {
62
+ /**
63
+ * On screen: render what the core says and put the core to work. What "to
64
+ * work" means is the core's own business - for this element, waiting for the
65
+ * client and reading the ticket types.
66
+ *
67
+ * `connectedCallback` and not `componentWillLoad` because this runs again
68
+ * when the element is moved in the DOM, and that is exactly when a mount and
69
+ * its teardown have to pair up.
70
+ */
71
+ connectedCallback() {
55
72
  this.syncProps();
56
- this.unsubscribe = this.element.subscribe((view) => {
73
+ this.unsubscribe ?? (this.unsubscribe = this.element.subscribe((view) => {
57
74
  this.view = view;
58
- });
75
+ }));
59
76
  this.view = this.element.view;
60
- await this.waitForSDKAndLoad();
77
+ this.teardown = this.element.mount();
61
78
  }
79
+ /**
80
+ * Off screen: run the mount's teardown, and nothing more. `destroy()` is
81
+ * terminal and Stencil calls this for a move as well as a removal, so
82
+ * destroying here would leave a re-inserted element frozen. The instance
83
+ * holds no listener and no subscription once these two have run.
84
+ */
62
85
  disconnectedCallback() {
86
+ this.teardown?.();
87
+ this.teardown = undefined;
63
88
  this.unsubscribe?.();
64
- this.element.destroy();
89
+ this.unsubscribe = undefined;
65
90
  }
66
- /**
67
- * Wait for SDK to be ready, then load ticket types
68
- */
69
- async waitForSDKAndLoad() {
70
- if (!this.eventId || !this.occurrenceId) {
71
- return;
72
- }
73
- try {
74
- await whenSDK();
75
- await this.element.actions.load();
76
- }
77
- catch (err) {
78
- this.element.actions.connectFailed(err instanceof Error ? err.message : this.view.strings.noClient);
79
- }
91
+ remount() {
92
+ this.teardown?.();
93
+ this.teardown = this.element.mount();
80
94
  }
81
95
  forward(event, detail) {
82
96
  if (event === 'tlAddToCart')
@@ -1,137 +1,17 @@
1
- import { expressesFee, minorUnits, FEE_LABEL_FALLBACK } from "./cart-totals";
2
- /** Money arrives as minor-unit integers or decimal strings ("50.00"). */
3
- export function formatPrice(value, currency = 'GBP') {
4
- if (value === null || value === undefined || value === '')
5
- return '';
6
- const fmt = new Intl.NumberFormat('en-GB', { style: 'currency', currency });
7
- if (typeof value === 'number')
8
- return fmt.format(value / 100);
9
- const n = Number(value);
10
- return Number.isNaN(n) ? String(value) : fmt.format(n);
11
- }
12
- export function formatDateTime(iso) {
13
- return new Date(iso).toLocaleDateString('en-GB', {
14
- weekday: 'short',
15
- day: 'numeric',
16
- month: 'short',
17
- year: 'numeric',
18
- hour: '2-digit',
19
- minute: '2-digit',
20
- });
21
- }
22
- export function formatDate(iso) {
23
- return new Date(iso).toLocaleDateString('en-GB', {
24
- day: 'numeric',
25
- month: 'short',
26
- year: 'numeric',
27
- });
28
- }
29
- /** Line items that are tickets (fees, taxes and discounts have their own type). */
30
- export function ticketItems(order) {
31
- return (order.items ?? []).filter((i) => !i.type || i.type === 'ticket');
32
- }
33
- export function ticketCount(order) {
34
- const fromItems = ticketItems(order).reduce((sum, i) => sum + (Number(i.quantity) || 0), 0);
35
- return fromItems || order.tickets?.length || 0;
36
- }
37
- /** Event name as the order view exposes it: the first ticket line's name. */
38
- export function orderEventName(order) {
39
- return order.eventName || ticketItems(order)[0]?.name || order.items?.[0]?.name || null;
40
- }
41
- export function orderCustomerEmail(order, customer) {
42
- const fromOrder = order?.customerEmail || order?.customer?.email;
43
- if (fromOrder)
44
- return fromOrder;
45
- const c = customer;
46
- return c?.email || null;
47
- }
48
- export function statusLabel(status) {
49
- if (!status)
50
- return '';
51
- return status.replace(/_/g, ' ').replace(/^\w/, (m) => m.toUpperCase());
52
- }
53
- export function statusTone(status) {
54
- switch (status) {
55
- case 'confirmed':
56
- return 'success';
57
- case 'completed':
58
- case 'fulfilled':
59
- return 'info';
60
- case 'pending':
61
- case 'processing':
62
- return 'warning';
63
- case 'cancelled':
64
- case 'expired':
65
- case 'failed':
66
- return 'error';
67
- default:
68
- return 'muted';
69
- }
70
- }
71
- export function errorMessage(err, fallback) {
72
- return err instanceof Error && err.message ? err.message : fallback;
73
- }
74
- /** True for the "not signed in" family of API errors (401/403, NO_SESSION, NOT_AUTHENTICATED). */
75
- export function isAuthError(err) {
76
- const e = err;
77
- if (!e || typeof e !== 'object')
78
- return false;
79
- return e.status === 401 || e.status === 403 || e.code === 'NO_SESSION' || e.code === 'NOT_AUTHENTICATED';
80
- }
81
1
  /**
82
- * Find the event and start time for an occurrence id. The order view only
83
- * carries the occurrence id, so this walks the channel's listings (cached by
84
- * live-api) until one owns the occurrence. Best effort: null when not found.
2
+ * The order helpers, as the components have always imported them.
3
+ *
4
+ * Everything here now lives in `@ticketlayer/elements-core`
5
+ * (`src/orders.ts`): the order shape, the money rows, the status tone, the
6
+ * date and price formatting and the occurrence lookup all moved across with
7
+ * the split of `tl-my-orders` and `tl-order-confirmation` (TKT-69), so a
8
+ * native harness answers the same questions the same way. This module stays as
9
+ * the import path, unchanged for callers, because `tl-order-tickets` and
10
+ * `tl-ticket` are not split yet and read these helpers from here.
11
+ *
12
+ * `resolveOccurrence` is the one signature that moved: it takes the core's
13
+ * `ElementClient` rather than this package's `TicketlayerSDK`. A
14
+ * `TicketlayerSDK` satisfies that shape, so every existing call still
15
+ * compiles, which is why it is re-exported rather than wrapped.
85
16
  */
86
- export async function resolveOccurrence(sdk, occurrenceId, maxEvents = 12) {
87
- const events = sdk.getEventsManager();
88
- const listing = await events.list({ limit: maxEvents });
89
- const candidates = listing.items.slice(0, maxEvents);
90
- for (const candidate of candidates) {
91
- const detail = (await events.get(candidate.id).catch(() => null));
92
- const occ = detail?.occurrences?.find((o) => o.id === occurrenceId);
93
- if (occ) {
94
- return {
95
- eventId: detail?.id ?? candidate.id,
96
- eventName: detail?.name ?? candidate.name ?? '',
97
- startsAt: occ.startsAt ?? null,
98
- };
99
- }
100
- }
101
- return null;
102
- }
103
- export function orderTotals(order) {
104
- const o = order || {};
105
- const items = Array.isArray(o.items) ? o.items : [];
106
- const feeLines = items.filter((i) => i.type === 'fee');
107
- const discountLines = items.filter((i) => i.type === 'discount');
108
- const tickets = ticketItems(o);
109
- const feeFromLines = feeLines.length
110
- ? feeLines.reduce((sum, i) => sum + (minorUnits(i.subtotal) ?? 0), 0)
111
- : null;
112
- const fees = feeFromLines ?? minorUnits(o.totalFees) ?? 0;
113
- const declaredSubtotal = minorUnits(o.subtotal);
114
- const subtotal = tickets.length
115
- ? tickets.reduce((sum, i) => sum + (minorUnits(i.subtotal) ?? 0), 0)
116
- : (declaredSubtotal ?? 0) - fees;
117
- const discountFromLines = discountLines.length
118
- ? discountLines.reduce((sum, i) => sum + Math.abs(minorUnits(i.subtotal) ?? 0), 0)
119
- : null;
120
- const discount = minorUnits(o.totalDiscounts) ?? discountFromLines ?? 0;
121
- const tax = minorUnits(o.totalTax) ?? 0;
122
- const named = feeLines.some((i) => typeof i.name === 'string' && !!i.name);
123
- return {
124
- currency: o.currency || 'GBP',
125
- subtotal,
126
- fees,
127
- feeLabel: feeLines.map((i) => i.name).find((n) => !!n) || FEE_LABEL_FALLBACK,
128
- showFee: expressesFee(feeFromLines ?? minorUnits(o.totalFees), named),
129
- tax,
130
- discount,
131
- total: minorUnits(o.total) ?? subtotal + fees + tax - discount,
132
- };
133
- }
134
- /** The rows add up to the total the order reports. Asserted in the specs. */
135
- export function totalsAgree(totals) {
136
- return totals.subtotal + totals.fees + totals.tax - totals.discount === totals.total;
137
- }
17
+ export { errorMessage, formatDate, formatDateTime, formatPrice, isAuthError, orderCustomerEmail, orderEventName, orderTotals, resolveOccurrence, statusLabel, statusTone, ticketCount, ticketItems, totalsAgree, } from '@ticketlayer/elements-core';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The text an element actually shows, shadow roots included.
3
+ *
4
+ * `textContent` stops at a shadow boundary, and every element here renders
5
+ * into one (and several compose others: `tl-cart-drawer` holds a `tl-cart`,
6
+ * `tl-order-tickets` a grid of `tl-ticket`). A check that an element rendered
7
+ * something has to see through that, or it reads an empty string from an
8
+ * element that is perfectly full.
9
+ *
10
+ * Used by the stories' `expectRendered` in the browser and by
11
+ * story-coverage.spec.tsx in jsdom, so both are asking the same question.
12
+ */
13
+ export function deepText(node) {
14
+ if (!node)
15
+ return '';
16
+ const parts = [];
17
+ const walk = (current) => {
18
+ if (current.nodeType === 3) {
19
+ parts.push(current.nodeValue ?? '');
20
+ return;
21
+ }
22
+ const shadow = current.shadowRoot;
23
+ if (shadow)
24
+ walk(shadow);
25
+ current.childNodes.forEach(walk);
26
+ };
27
+ walk(node);
28
+ return parts.join(' ').replace(/\s+/g, ' ').trim();
29
+ }
@@ -0,0 +1,290 @@
1
+ export const FIXTURE_CUSTOMER = {
2
+ id: 'cus_ada',
3
+ email: 'ada@example.com',
4
+ firstName: 'Ada',
5
+ lastName: 'Lovelace',
6
+ };
7
+ /** `events.list` items, i.e. what a card is handed by `tl-event-list`. */
8
+ export const FIXTURE_EVENT_CARDS = [
9
+ {
10
+ id: 'evt_midnight',
11
+ name: 'Midnight Quartet',
12
+ shortDescription: 'Late night strings in the Old Hall',
13
+ availabilityStatus: 'available',
14
+ venue: { id: 'ven_old_hall', name: 'The Old Hall', city: 'Bristol' },
15
+ nextOccurrence: { startsAt: '2030-06-14T19:30:00Z' },
16
+ },
17
+ {
18
+ id: 'evt_gala',
19
+ name: 'Summer Gala',
20
+ shortDescription: 'The season closes with the full orchestra',
21
+ availabilityStatus: 'limited',
22
+ venue: { id: 'ven_pavilion', name: 'Riverside Pavilion', city: 'Bath' },
23
+ nextOccurrence: { startsAt: '2030-07-05T18:00:00Z' },
24
+ },
25
+ {
26
+ id: 'evt_lates',
27
+ name: 'Gallery Lates',
28
+ shortDescription: 'Music after hours among the paintings',
29
+ availabilityStatus: 'sold_out',
30
+ venue: { id: 'ven_gallery', name: 'City Gallery', city: 'Bristol' },
31
+ nextOccurrence: { startsAt: '2030-08-02T20:00:00Z' },
32
+ },
33
+ ];
34
+ /** The occurrences of `evt_midnight`: enough dates for the calendar view. */
35
+ const MIDNIGHT_OCCURRENCES = [
36
+ {
37
+ id: 'evoc_1',
38
+ startsAt: '2030-06-14T19:30:00Z',
39
+ endsAt: '2030-06-14T21:30:00Z',
40
+ availabilityStatus: 'available',
41
+ availableCapacity: 120,
42
+ },
43
+ {
44
+ id: 'evoc_2',
45
+ startsAt: '2030-06-15T19:30:00Z',
46
+ endsAt: '2030-06-15T21:30:00Z',
47
+ availabilityStatus: 'available',
48
+ availableCapacity: 40,
49
+ },
50
+ {
51
+ id: 'evoc_3',
52
+ startsAt: '2030-06-21T19:30:00Z',
53
+ endsAt: '2030-06-21T21:30:00Z',
54
+ availabilityStatus: 'sold_out',
55
+ availableCapacity: 0,
56
+ },
57
+ ];
58
+ /** `events.get` details, keyed by event id. */
59
+ export const FIXTURE_EVENT_DETAILS = {
60
+ evt_midnight: {
61
+ id: 'evt_midnight',
62
+ name: 'Midnight Quartet',
63
+ status: 'on_sale',
64
+ shortDescription: 'Late night strings in the Old Hall',
65
+ venueId: 'ven_old_hall',
66
+ venueName: 'The Old Hall',
67
+ venueCity: 'Bristol',
68
+ occurrences: MIDNIGHT_OCCURRENCES,
69
+ },
70
+ evt_gala: {
71
+ id: 'evt_gala',
72
+ name: 'Summer Gala',
73
+ status: 'on_sale',
74
+ shortDescription: 'The season closes with the full orchestra',
75
+ venueId: 'ven_pavilion',
76
+ venueName: 'Riverside Pavilion',
77
+ venueCity: 'Bath',
78
+ occurrences: [
79
+ {
80
+ id: 'evoc_gala',
81
+ startsAt: '2030-07-05T18:00:00Z',
82
+ endsAt: '2030-07-05T22:00:00Z',
83
+ availabilityStatus: 'available',
84
+ availableCapacity: 300,
85
+ },
86
+ ],
87
+ },
88
+ };
89
+ /** `events.getTicketTypes` for `evoc_1`. */
90
+ export const FIXTURE_TICKET_TYPES = [
91
+ {
92
+ id: 'tkt_adult',
93
+ occurrenceId: 'evoc_1',
94
+ name: 'Adult',
95
+ description: 'Standing, unreserved',
96
+ price: 2500,
97
+ currency: 'GBP',
98
+ availableQuantity: 120,
99
+ maxPerOrder: 6,
100
+ minPerOrder: 0,
101
+ availabilityStatus: 'available',
102
+ sortOrder: 1,
103
+ },
104
+ {
105
+ id: 'tkt_concession',
106
+ occurrenceId: 'evoc_1',
107
+ name: 'Concession',
108
+ description: 'Students and over 65s',
109
+ price: 1500,
110
+ currency: 'GBP',
111
+ availableQuantity: 20,
112
+ maxPerOrder: 4,
113
+ minPerOrder: 0,
114
+ availabilityStatus: 'limited',
115
+ sortOrder: 2,
116
+ },
117
+ {
118
+ id: 'tkt_gallery',
119
+ occurrenceId: 'evoc_1',
120
+ name: 'Gallery',
121
+ description: 'Seated, upper level',
122
+ price: 4000,
123
+ currency: 'GBP',
124
+ availableQuantity: 0,
125
+ maxPerOrder: 4,
126
+ minPerOrder: 0,
127
+ availabilityStatus: 'sold_out',
128
+ sortOrder: 3,
129
+ },
130
+ ];
131
+ /** A cart with lines, a fee the surface quoted and a discount. */
132
+ export const FIXTURE_CART = {
133
+ items: [
134
+ {
135
+ id: 'ci_adult',
136
+ ticketTypeId: 'tkt_adult',
137
+ ticketTypeName: 'Adult',
138
+ eventName: 'Midnight Quartet',
139
+ occurrenceDate: '2030-06-14T19:30:00Z',
140
+ quantity: 2,
141
+ unitPrice: 2500,
142
+ totalPrice: 5000,
143
+ subtotal: 5000,
144
+ currency: 'GBP',
145
+ },
146
+ {
147
+ id: 'ci_concession',
148
+ ticketTypeId: 'tkt_concession',
149
+ ticketTypeName: 'Concession',
150
+ eventName: 'Midnight Quartet',
151
+ occurrenceDate: '2030-06-14T19:30:00Z',
152
+ quantity: 1,
153
+ unitPrice: 1500,
154
+ totalPrice: 1500,
155
+ subtotal: 1500,
156
+ currency: 'GBP',
157
+ },
158
+ ],
159
+ itemCount: 3,
160
+ subtotal: 6500,
161
+ fees: 325,
162
+ discount: 500,
163
+ total: 6325,
164
+ hasFees: true,
165
+ feeLabel: 'Booking fee',
166
+ currency: 'GBP',
167
+ };
168
+ /** An empty cart, for the states an element only reaches with nothing in it. */
169
+ export const FIXTURE_EMPTY_CART = {
170
+ items: [],
171
+ itemCount: 0,
172
+ subtotal: 0,
173
+ fees: 0,
174
+ discount: 0,
175
+ total: 0,
176
+ hasFees: false,
177
+ feeLabel: null,
178
+ currency: 'GBP',
179
+ };
180
+ /** Passes on the order: what `tl-ticket` renders and `tl-order-tickets` grids. */
181
+ export const FIXTURE_TICKETS = [
182
+ {
183
+ id: 'pas_adult_1',
184
+ barcode: 'TLQ-3H7K-9F2D-0001',
185
+ type: 'ticket',
186
+ status: 'active',
187
+ holderName: 'Ada Lovelace',
188
+ name: 'Adult - Midnight Quartet',
189
+ admissionStatus: 'not_redeemed',
190
+ eventOccurrenceId: 'evoc_1',
191
+ seatLabel: 'Stalls C12',
192
+ },
193
+ {
194
+ id: 'pas_adult_2',
195
+ barcode: 'TLQ-3H7K-9F2D-0002',
196
+ type: 'ticket',
197
+ status: 'active',
198
+ holderName: 'Ada Lovelace',
199
+ name: 'Adult - Midnight Quartet',
200
+ admissionStatus: 'fully_redeemed',
201
+ eventOccurrenceId: 'evoc_1',
202
+ seatLabel: 'Stalls C13',
203
+ },
204
+ ];
205
+ export const FIXTURE_ORDER = {
206
+ id: 'ord_midnight',
207
+ orderNumber: 'ORD-MQ7841',
208
+ status: 'confirmed',
209
+ paymentStatus: 'paid',
210
+ currency: 'GBP',
211
+ items: [
212
+ {
213
+ name: 'Midnight Quartet - Adult',
214
+ description: null,
215
+ type: 'ticket',
216
+ quantity: 2,
217
+ unitPrice: 2500,
218
+ subtotal: 5000,
219
+ },
220
+ { name: 'Booking fee', description: null, type: 'fee', quantity: 1, unitPrice: 250, subtotal: 250 },
221
+ ],
222
+ subtotal: 5000,
223
+ totalFees: 250,
224
+ totalTax: 0,
225
+ total: 5250,
226
+ tickets: FIXTURE_TICKETS,
227
+ expiresAt: null,
228
+ createdAt: '2030-05-02T10:15:00Z',
229
+ };
230
+ const OLDER_ORDER = {
231
+ id: 'ord_gala',
232
+ orderNumber: 'ORD-SG1290',
233
+ status: 'confirmed',
234
+ paymentStatus: 'paid',
235
+ currency: 'GBP',
236
+ items: [
237
+ {
238
+ name: 'Summer Gala - Concession',
239
+ description: null,
240
+ type: 'ticket',
241
+ quantity: 1,
242
+ unitPrice: 1500,
243
+ subtotal: 1500,
244
+ },
245
+ ],
246
+ subtotal: 1500,
247
+ totalFees: 0,
248
+ totalTax: 0,
249
+ total: 1500,
250
+ tickets: [],
251
+ expiresAt: null,
252
+ createdAt: '2030-04-11T09:00:00Z',
253
+ };
254
+ export const FIXTURE_ORDERS = [FIXTURE_ORDER, OLDER_ORDER];
255
+ /** The managers every dataset shares: the channel's catalogue and its orders. */
256
+ const catalogue = {
257
+ events: {
258
+ list: async () => ({ items: FIXTURE_EVENT_CARDS }),
259
+ get: async (id) => FIXTURE_EVENT_DETAILS[id] ?? FIXTURE_EVENT_DETAILS.evt_midnight,
260
+ getTicketTypes: async () => FIXTURE_TICKET_TYPES,
261
+ },
262
+ orders: {
263
+ mine: async () => FIXTURE_ORDERS,
264
+ get: async (orderId) => FIXTURE_ORDERS.find((o) => o.id === orderId) ?? FIXTURE_ORDER,
265
+ ticketPdf: async () => new Blob(['%PDF-1.4 fixture'], { type: 'application/pdf' }),
266
+ },
267
+ presale: {
268
+ redeem: async () => ({ benefit: 'Presale unlocked', alreadyHeld: false }),
269
+ },
270
+ };
271
+ /**
272
+ * The datasets a story may ask for by name, through
273
+ * `parameters.ticketlayer.dataset`. Keep the set small: a dataset is a state
274
+ * of the whole channel, not a per-element fixture.
275
+ */
276
+ export const FIXTURE_DATASETS = {
277
+ /** Signed in, three events on sale, a cart with lines and two orders. */
278
+ default: {
279
+ ...catalogue,
280
+ cart: { get: async () => FIXTURE_CART },
281
+ authState: { customer: FIXTURE_CUSTOMER, authenticated: true },
282
+ },
283
+ /** Nobody signed in and nothing in the cart. */
284
+ 'signed-out': {
285
+ ...catalogue,
286
+ cart: { get: async () => FIXTURE_EMPTY_CART },
287
+ authState: { customer: null, authenticated: false },
288
+ },
289
+ };
290
+ export const FIXTURE_DATASET_NAMES = Object.keys(FIXTURE_DATASETS);
@@ -58,17 +58,28 @@ export function createMockSDK(opts = {}) {
58
58
  redeem: async () => ({ benefit: 'presale.test', alreadyHeld: false }),
59
59
  ...opts.presale,
60
60
  };
61
+ const modalCalls = [];
61
62
  const sdk = {
62
63
  getEventsManager: () => eventsManager,
63
64
  getCartManager: () => cartManager,
64
65
  getOrdersManager: () => ordersManager,
65
66
  on,
66
67
  off,
67
- event: () => ({ openModal: () => undefined }),
68
- checkout: { openModal: () => undefined, closeModal: () => undefined },
68
+ event: (eventId) => ({
69
+ openModal: (options = {}) => {
70
+ modalCalls.push({ kind: 'event', eventId, options });
71
+ },
72
+ }),
73
+ checkout: {
74
+ openModal: (options = {}) => {
75
+ modalCalls.push({ kind: 'checkout', options });
76
+ },
77
+ closeModal: () => undefined,
78
+ },
69
79
  closeModal: () => undefined,
70
80
  emit,
71
81
  authState,
82
+ modalCalls,
72
83
  };
73
84
  if (!opts.withoutAuth)
74
85
  sdk.getAuthManager = () => authManager;
@@ -0,0 +1,12 @@
1
+ export const webQuery = {
2
+ get(name) {
3
+ if (typeof window === 'undefined' || !window.location?.search)
4
+ return null;
5
+ try {
6
+ return new URLSearchParams(window.location.search).get(name);
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ },
12
+ };
@@ -0,0 +1,22 @@
1
+ export const webStorage = {
2
+ get(key) {
3
+ if (typeof window === 'undefined')
4
+ return null;
5
+ try {
6
+ return window.localStorage.getItem(key);
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ },
12
+ set(key, value) {
13
+ if (typeof window === 'undefined')
14
+ return;
15
+ try {
16
+ window.localStorage.setItem(key, value);
17
+ }
18
+ catch {
19
+ // localStorage may be unavailable.
20
+ }
21
+ },
22
+ };