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