@occluze/tether 0.7.0 → 0.7.1

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 (132) hide show
  1. package/README.md +21 -21
  2. package/dist/esm/chunks/{account-UIPWJQ3Y.js → account-CXMA5RPZ.js} +1 -1
  3. package/dist/esm/chunks/{account-pages-6ROA6Y4V.js → account-pages-IZO7FEOZ.js} +1 -1
  4. package/dist/esm/chunks/{analytics-2V632IRV.js → analytics-PG5FPRDN.js} +1 -1
  5. package/dist/esm/chunks/{chunk-4GCXM764.js → chunk-4HD4J5JC.js} +1 -1
  6. package/dist/esm/chunks/{chunk-BFTZR4OB.js → chunk-6TFMLT4A.js} +2 -2
  7. package/dist/esm/chunks/{chunk-T44MUL4Y.js → chunk-BBVUVPMO.js} +1 -1
  8. package/dist/esm/chunks/chunk-EW5SYO6L.js +1 -0
  9. package/dist/esm/chunks/chunk-HJY2JSNB.js +1 -0
  10. package/dist/esm/chunks/chunk-MQYB6BH4.js +1 -0
  11. package/dist/esm/chunks/{chunk-5Z5O3767.js → chunk-N3V45JPS.js} +1 -1
  12. package/dist/esm/chunks/{chunk-YLUULATY.js → chunk-RPMBCAGS.js} +1 -1
  13. package/dist/esm/chunks/{chunk-MPDWM2NN.js → chunk-VFNHIGCS.js} +1 -1
  14. package/dist/esm/chunks/{chunk-6OZJCAGZ.js → chunk-WT7RGGJU.js} +1 -1
  15. package/dist/esm/chunks/chunk-YN6LEBEN.js +1 -0
  16. package/dist/esm/chunks/chunk-Z6HS3H7K.js +1 -0
  17. package/dist/esm/chunks/{diagnose-3FEKRIEK.js → diagnose-XCEBQKBO.js} +1 -1
  18. package/dist/esm/chunks/extras-4HU3VSBQ.js +1 -0
  19. package/dist/esm/chunks/{markets-ui-FD6NI2OG.js → markets-ui-M4IQ62CT.js} +1 -1
  20. package/dist/esm/chunks/search-A5DS2IOS.js +1 -0
  21. package/dist/esm/index.js +1 -1
  22. package/dist/esm/server.js +2 -2
  23. package/dist/tether.account-pages.js +13 -3
  24. package/dist/tether.account-pages.min.js +1 -1
  25. package/dist/tether.account.js +13 -3
  26. package/dist/tether.account.min.js +1 -1
  27. package/dist/tether.analytics.js +18 -3
  28. package/dist/tether.analytics.min.js +1 -1
  29. package/dist/tether.diagnose.js +18 -3
  30. package/dist/tether.diagnose.min.js +2 -2
  31. package/dist/tether.extras.js +19 -6
  32. package/dist/tether.extras.min.js +1 -1
  33. package/dist/tether.js +162 -112
  34. package/dist/tether.markets.js +18 -3
  35. package/dist/tether.markets.min.js +1 -1
  36. package/dist/tether.min.js +3 -3
  37. package/dist/tether.min.js.map +4 -4
  38. package/dist/tether.search.js +18 -3
  39. package/dist/tether.search.min.js +1 -1
  40. package/dist/types/server/src/shared/cart-id.d.ts +2 -0
  41. package/dist/types/src/cart/mutations.d.ts +6 -0
  42. package/dist/types/src/cart/store.d.ts +5 -2
  43. package/dist/types/src/cart.d.ts +2 -2
  44. package/dist/types/src/client.d.ts +53 -19
  45. package/dist/types/src/config.d.ts +6 -1
  46. package/dist/types/src/index.d.ts +2 -2
  47. package/dist/types/src/product-shape.d.ts +3 -1
  48. package/package.json +1 -2
  49. package/dist/esm/chunks/chunk-56FCLJJD.js +0 -1
  50. package/dist/esm/chunks/chunk-A5SYCVQM.js +0 -1
  51. package/dist/esm/chunks/chunk-BA4LQJJQ.js +0 -1
  52. package/dist/esm/chunks/chunk-C5JYQCWH.js +0 -1
  53. package/dist/esm/chunks/extras-6POFBNOP.js +0 -1
  54. package/dist/esm/chunks/search-UJ73YBQ5.js +0 -1
  55. package/src/account/addresses-view.ts +0 -58
  56. package/src/account/answers.ts +0 -79
  57. package/src/account/markup.ts +0 -36
  58. package/src/account/operations.ts +0 -145
  59. package/src/account/order-lines.ts +0 -75
  60. package/src/account/order-list.ts +0 -107
  61. package/src/account/order-page.ts +0 -137
  62. package/src/account/page-answers.ts +0 -317
  63. package/src/account/page-events.ts +0 -253
  64. package/src/account/server-client.ts +0 -183
  65. package/src/account/subscriptions-view.ts +0 -115
  66. package/src/account-api.ts +0 -98
  67. package/src/account-marks.ts +0 -35
  68. package/src/account-pages.ts +0 -81
  69. package/src/account.ts +0 -284
  70. package/src/amounts.ts +0 -64
  71. package/src/analytics-shopify.ts +0 -246
  72. package/src/analytics.ts +0 -315
  73. package/src/api-version.ts +0 -79
  74. package/src/astro.ts +0 -272
  75. package/src/cart/cart-shape.ts +0 -25
  76. package/src/cart/checkout.ts +0 -101
  77. package/src/cart/drawer.ts +0 -125
  78. package/src/cart/mutations.ts +0 -237
  79. package/src/cart/render.ts +0 -266
  80. package/src/cart/store.ts +0 -279
  81. package/src/cart.ts +0 -345
  82. package/src/client.ts +0 -391
  83. package/src/collection.ts +0 -175
  84. package/src/config-sections.ts +0 -284
  85. package/src/config.ts +0 -480
  86. package/src/controls/cart-controls.ts +0 -123
  87. package/src/controls/discount.ts +0 -75
  88. package/src/controls/extras.ts +0 -54
  89. package/src/controls/navigation.ts +0 -77
  90. package/src/diagnose-markup.ts +0 -298
  91. package/src/diagnose.ts +0 -294
  92. package/src/dom.ts +0 -268
  93. package/src/extras.ts +0 -136
  94. package/src/fragments.ts +0 -261
  95. package/src/globals.d.ts +0 -15
  96. package/src/images.ts +0 -91
  97. package/src/index.ts +0 -415
  98. package/src/initial.ts +0 -101
  99. package/src/lazy-iife.ts +0 -197
  100. package/src/lazy.ts +0 -42
  101. package/src/loader.ts +0 -363
  102. package/src/market-codes.ts +0 -6
  103. package/src/markets-ui.ts +0 -227
  104. package/src/markets.ts +0 -246
  105. package/src/markup.ts +0 -127
  106. package/src/mounting.ts +0 -264
  107. package/src/page-market.ts +0 -110
  108. package/src/part-integrity.ts +0 -7
  109. package/src/parts/account-pages.ts +0 -7
  110. package/src/parts/account.ts +0 -7
  111. package/src/parts/analytics.ts +0 -7
  112. package/src/parts/diagnose.ts +0 -7
  113. package/src/parts/extras.ts +0 -7
  114. package/src/parts/markets.ts +0 -7
  115. package/src/parts/search.ts +0 -7
  116. package/src/presets/barba.ts +0 -194
  117. package/src/product-events.ts +0 -115
  118. package/src/product-render.ts +0 -218
  119. package/src/product-shape.ts +0 -49
  120. package/src/product-state.ts +0 -318
  121. package/src/product.ts +0 -308
  122. package/src/repeaters.ts +0 -161
  123. package/src/search.ts +0 -305
  124. package/src/selling-plans.ts +0 -145
  125. package/src/server.ts +0 -211
  126. package/src/shopify-values.ts +0 -91
  127. package/src/status.ts +0 -106
  128. package/src/storage.ts +0 -34
  129. package/src/store-html.ts +0 -359
  130. package/src/storefront/catalog.ts +0 -196
  131. package/src/storefront/json-ld.ts +0 -144
  132. package/src/variant-render.ts +0 -139
@@ -1,36 +0,0 @@
1
- /**
2
- * The account markup's shared vocabulary: `tether-*-show` conditions, binds on a template row (the row itself may
3
- * carry one), and how Shopify's enum values and formatted addresses read on the page.
4
- */
5
- import { get, qsa, sel, show } from '../dom.ts';
6
-
7
- /**
8
- * Shows or hides each `name` element whose value is one of `conditions`' keys; other values are left alone.
9
- *
10
- * @param name `show`, `order-show`, `address-show`…
11
- */
12
- export function showIn(root: ParentNode, name: string, conditions: Readonly<Record<string, boolean>>): void {
13
- qsa(root, sel(name)).forEach((el) => {
14
- const key = get(el, name) ?? '';
15
- if (Object.hasOwn(conditions, key)) show(el, conditions[key] === true);
16
- });
17
- }
18
-
19
- /**
20
- * @param row A copy of a repeater's template.
21
- * @returns The `name` elements in `row`, and `row` itself when it carries `name`.
22
- */
23
- export const bindsIn = (row: HTMLElement, name: string): HTMLElement[] =>
24
- qsa(row, sel(name)).concat(row.matches(sel(name)) ? [row] : []);
25
-
26
- /** @returns A Shopify enum as text: `PARTIALLY_FULFILLED` → `partially fulfilled`. */
27
- export const enumText = (value: string | null | undefined): string =>
28
- value ? value.toLowerCase().replace(/_/g, ' ') : '';
29
-
30
- /** @returns Shopify's formatted address on one line. */
31
- export const addressText = (address: { formatted?: string[] | null } | null | undefined): string =>
32
- (address?.formatted ?? []).join(', ');
33
-
34
- /** A bind's value, looked up by the attribute's text: only the table's own keys, never `constructor` and the like. */
35
- export const ownValue = <T>(values: Readonly<Record<string, T>>, key: string): T | undefined =>
36
- Object.hasOwn(values, key) ? values[key] : undefined;
@@ -1,145 +0,0 @@
1
- /**
2
- * The account pages' operations: the Worker's fixed routes (shared/account-routes.ts), which check the input and run
3
- * the documents with the customer's token. Each result is read into its typed shape (page-answers.ts), and a failure
4
- * is a `TetherError`: `NOT_LOGGED_IN`, `INVALID` for input the Worker or Shopify refuses, `NETWORK`, and
5
- * `HTTP_<status>` for a request that failed.
6
- */
7
- import { TetherError } from '../client.ts';
8
- import { emit } from '../dom.ts';
9
- import { operationRequest } from '../../server/src/shared/account-routes.ts';
10
- import type {
11
- AddressFields,
12
- OperationInputs,
13
- OperationName,
14
- ReturnLine,
15
- SubscriptionAction,
16
- } from '../../server/src/shared/account-routes.ts';
17
-
18
- export type { AddressFields, ReturnLine };
19
- import {
20
- toAccountAddress,
21
- toCustomerAddresses,
22
- toDeletedId,
23
- toOrderDetail,
24
- toProfile,
25
- toReturn,
26
- toSubscriptionChange,
27
- toSubscriptionContracts,
28
- } from './page-answers.ts';
29
- import type {
30
- AccountAddress,
31
- CustomerAddress,
32
- OpenedReturn,
33
- OrderDetail,
34
- Profile,
35
- SubscriptionChange,
36
- SubscriptionContract,
37
- } from './page-answers.ts';
38
- import type { ServerAnswer, ServerRequest } from './server-client.ts';
39
- import type { AccountCustomer } from './answers.ts';
40
-
41
- /** The account part's session, shared so both parts act as one customer. */
42
- interface PagesContext {
43
- serverFetch: (path: string, request?: ServerRequest) => Promise<ServerAnswer>;
44
- customer: () => Promise<AccountCustomer | null>;
45
- bindCustomer: (root: ParentNode) => Promise<void>;
46
- /** Forgets the cached customer after a change of their data. */
47
- reset: () => void;
48
- }
49
-
50
- /** `tether:account-updated`'s `what`. */
51
- type ChangedData = 'profile' | 'addresses' | 'subscriptions' | 'orders';
52
-
53
- let context: PagesContext | null = null;
54
-
55
- /** Called by the account part once, when it loads this part. */
56
- export function setup(shared: PagesContext): void {
57
- context = shared;
58
- }
59
-
60
- /** @throws {Error} Before `setup`: this part is only ever loaded by the account part, which calls it first. */
61
- export function pagesContext(): PagesContext {
62
- if (!context) throw new Error('account-pages: loaded without the account part');
63
- return context;
64
- }
65
-
66
- /**
67
- * @returns The operation's result fields, read by the caller.
68
- * @throws {TetherError} With one of the codes in the file's comment.
69
- */
70
- async function run<Name extends OperationName>(
71
- name: Name,
72
- input: OperationInputs[Name],
73
- ): Promise<Record<string, unknown>> {
74
- const { serverFetch } = pagesContext();
75
- const { method, path } = operationRequest(name, input);
76
- const answer = await serverFetch(path, method === 'POST' ? { method, body: input } : {});
77
- if (answer.kind === 'logged-out') throw new TetherError('Not logged in', { code: 'NOT_LOGGED_IN', status: 401 });
78
- return answer.data;
79
- }
80
-
81
- function changed(what: ChangedData): void {
82
- pagesContext().reset();
83
- emit('account-updated', { what });
84
- }
85
-
86
- /**
87
- * One of the customer's orders, in full (lines, totals, addresses, fulfillments, returns).
88
- *
89
- * @param id A gid or the numeric order id.
90
- * @returns `null` when the customer has no such order.
91
- */
92
- export async function order(id: string | number): Promise<OrderDetail | null> {
93
- const result = await run('order', { id });
94
- return toOrderDetail(result['order']);
95
- }
96
-
97
- export async function updateProfile({
98
- firstName,
99
- lastName,
100
- }: { firstName?: string; lastName?: string } = {}): Promise<Profile> {
101
- const result = await run('profile', { firstName, lastName });
102
- changed('profile');
103
- return toProfile(result['customer']);
104
- }
105
-
106
- /** @returns The customer's addresses, the default one marked `isDefault`. */
107
- export async function addresses(): Promise<CustomerAddress[]> {
108
- const result = await run('addresses', {});
109
- return toCustomerAddresses(result['addresses']);
110
- }
111
-
112
- /** Creates an address, or updates the one with `id`; without fields, only makes it the default. */
113
- export async function saveAddress(
114
- address: AddressFields | null,
115
- { id, isDefault }: { id?: string | undefined; isDefault?: boolean } = {},
116
- ): Promise<AccountAddress> {
117
- const result = await run('saveAddress', { id, address, isDefault });
118
- changed('addresses');
119
- return toAccountAddress(result['address']);
120
- }
121
-
122
- /** @returns The deleted address's id. */
123
- export async function deleteAddress(id: string): Promise<string> {
124
- const result = await run('deleteAddress', { id });
125
- changed('addresses');
126
- return toDeletedId(result['deleted']);
127
- }
128
-
129
- export async function subscriptions(): Promise<SubscriptionContract[]> {
130
- const result = await run('subscriptions', {});
131
- return toSubscriptionContracts(result['subscriptions']);
132
- }
133
-
134
- /** @returns The changed contract or billing cycle. */
135
- export async function subscriptionAction(id: string | number, action: SubscriptionAction): Promise<SubscriptionChange> {
136
- const result = await run('subscription', { id, action });
137
- changed('subscriptions');
138
- return toSubscriptionChange(result);
139
- }
140
-
141
- export async function requestReturn(orderId: string | number, lines: ReturnLine[]): Promise<OpenedReturn> {
142
- const result = await run('requestReturn', { orderId, lines });
143
- changed('orders');
144
- return toReturn(result['return']);
145
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * The products of an order (`tether-order-lines`), for the order list and the order page alike. The order page's
3
- * lines also carry price, total, sku and what can be returned; the order list's document has none of those, so they
4
- * render empty there, and its rows get no return controls.
5
- */
6
- import { formatMoney, get, qsa, repeat, sel, setImage, setText } from '../dom.ts';
7
- import { bindsIn, ownValue, showIn } from './markup.ts';
8
- import type { AccountOrderLine } from '../../server/src/shared/account-orders.ts';
9
- import type { OrderDetailLine } from './page-answers.ts';
10
-
11
- /** A line of the order list, or of the order page, which has the fields after `image` too. */
12
- type OrderLine = AccountOrderLine &
13
- Partial<Pick<OrderDetailLine, 'id' | 'name' | 'sku' | 'price' | 'totalPrice' | 'suggestedReturnReasonDefinitions'>>;
14
- /** Line id → how many of it can still be returned. */
15
- type Returnable = ReadonlyMap<string, number>;
16
-
17
- /**
18
- * @param returnable `null` in the order list, which knows nothing of returns.
19
- * @returns The text of each `tether-order-line` value.
20
- */
21
- const lineTexts = (line: OrderLine, returnable: number | null): Record<string, string | undefined> => ({
22
- title: line.title || line.name || '',
23
- 'variant-title': line.variantTitle || '',
24
- quantity: String(line.quantity),
25
- price: formatMoney(line.price),
26
- total: formatMoney(line.totalPrice),
27
- sku: line.sku || '',
28
- returnable: returnable === null ? undefined : String(returnable),
29
- });
30
-
31
- const isInput = (el: HTMLElement): el is HTMLInputElement => el.tagName === 'INPUT';
32
-
33
- /** The return controls of one line: how many can be returned, and Shopify's suggested reasons. */
34
- function fillReturnControls(row: HTMLElement, line: OrderLine & { id: string }, returnable: number): void {
35
- showIn(row, 'order-line-show', { returnable: returnable > 0 });
36
- qsa(row, sel('return-quantity')).forEach((input) => {
37
- if (!isInput(input)) return;
38
- input.max = String(returnable);
39
- input.min = '0';
40
- if (!input.value) input.value = '0';
41
- input.disabled = returnable < 1;
42
- });
43
- qsa(row, `select${sel('return-reason')}`).forEach((select) => {
44
- // A select keeps its options across renders of the same line: they are added once.
45
- if (select.dataset['tetherFilled'] === line.id) return;
46
- select.dataset['tetherFilled'] = line.id;
47
- (line.suggestedReturnReasonDefinitions?.nodes ?? []).forEach((reason) => {
48
- select.appendChild(new Option(reason.name ?? '', reason.id));
49
- });
50
- });
51
- }
52
-
53
- /**
54
- * Renders `lines` into a `tether-order-lines` repeater.
55
- *
56
- * @param returns `null` for the order list.
57
- */
58
- export function fillLines(box: HTMLElement, lines: readonly OrderLine[], returns: Returnable | null): void {
59
- repeat(box, lines, (row, line) => {
60
- const id = line.id;
61
- const returnable = returns && id ? (returns.get(id) ?? 0) : null;
62
- if (id) row.dataset['tetherLine'] = id;
63
- const texts = lineTexts(line, returnable);
64
- bindsIn(row, 'order-line').forEach((el) => {
65
- const key = get(el, 'order-line') ?? '';
66
- if (key === 'image') {
67
- if (line.image) setImage(el, line.image.url, line.image.altText || line.title || '');
68
- return;
69
- }
70
- const text = ownValue(texts, key);
71
- if (text !== undefined) setText(el, text);
72
- });
73
- if (id && returnable !== null) fillReturnControls(row, { ...line, id }, returnable);
74
- });
75
- }
@@ -1,107 +0,0 @@
1
- /**
2
- * The customer's recent orders (`tether-orders`), with `tether-show="orders-loading"` while they load and
3
- * `orders-empty` when there are none yet.
4
- */
5
- import { cfg, reportError } from '../config.ts';
6
- import { isHttpsUrl } from '../shopify-values.ts';
7
- import { formatDate, formatMoney, get, qsa, repeat, sel, setImage, setText } from '../dom.ts';
8
- import { ownValue, showIn } from './markup.ts';
9
- import { fillLines } from './order-lines.ts';
10
- import type { AccountOrder } from '../../server/src/shared/account-orders.ts';
11
-
12
- /** How many orders `orders()` answers, and a `tether-orders` list shows, without being told. */
13
- export const DEFAULT_ORDER_COUNT = 10;
14
- /** The most orders one request asks for: the Worker answers no more. */
15
- export const MAX_ORDER_COUNT = 50;
16
-
17
- /** @returns True for a number of orders one request can ask for: a whole number from 1 to `MAX_ORDER_COUNT`. */
18
- export const isOrderCount = (value: unknown): value is number =>
19
- typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= MAX_ORDER_COUNT;
20
-
21
- /**
22
- * @param limit The list's `tether-limit`.
23
- * @returns How many orders the list shows: the limit, kept within what one request can ask for.
24
- */
25
- function listLimit(limit: string | null): number {
26
- const asked = Number.parseInt(limit || String(DEFAULT_ORDER_COUNT), 10) || DEFAULT_ORDER_COUNT;
27
- return Math.min(Math.max(asked, 1), MAX_ORDER_COUNT);
28
- }
29
-
30
- /**
31
- * @param gid `gid://shopify/Order/123`, possibly with a query.
32
- * @returns The numeric id at its end.
33
- */
34
- const numericId = (gid: string): string => gid.replace(/\?.*$/, '').replace(/^.*\//, '');
35
-
36
- /**
37
- * The order's page: the site's own (`account.orderUrl`, e.g. `/account/order?id={id}`, every `{id}` the numeric
38
- * order id), else Shopify's status page when it is https.
39
- */
40
- function orderUrl(order: AccountOrder): string | null {
41
- const template = cfg.account?.orderUrl;
42
- if (template) return template.replaceAll('{id}', encodeURIComponent(numericId(order.id)));
43
- const statusPage = order.statusPageUrl;
44
- return statusPage !== null && isHttpsUrl(statusPage) ? statusPage : null;
45
- }
46
-
47
- /** @returns The text of each `tether-order-bind` value. */
48
- const orderTexts = (order: AccountOrder): Record<string, string | undefined> => ({
49
- name: order.name ?? '',
50
- date: formatDate(order.processedAt),
51
- total: formatMoney(order.totalPrice),
52
- status: order.financialStatus?.toLowerCase(),
53
- fulfillment: order.fulfillmentStatus?.toLowerCase(),
54
- items: order.lineItems.nodes.map((line) => `${String(line.quantity)}× ${line.title ?? ''}`).join(', '),
55
- });
56
-
57
- /** @param node A copy of the list's template. */
58
- function fillOrder(node: HTMLElement, order: AccountOrder): void {
59
- const lines = order.lineItems.nodes;
60
- const url = orderUrl(order);
61
- const texts = orderTexts(order);
62
- qsa(node, sel('order-bind')).forEach((el) => {
63
- const key = get(el, 'order-bind') ?? '';
64
- if (key === 'url') {
65
- if (url) el.setAttribute('href', url);
66
- return;
67
- }
68
- if (key === 'image') {
69
- const image = lines.find((line) => line.image)?.image;
70
- if (image) setImage(el, image.url, image.altText || lines[0]?.title || '');
71
- return;
72
- }
73
- const text = ownValue(texts, key);
74
- if (text !== undefined) setText(el, text);
75
- });
76
- qsa(node, sel('order-lines')).forEach((box) => {
77
- fillLines(box, lines, null);
78
- });
79
- if (get(node, 'order-bind') === 'url' && url) node.setAttribute('href', url);
80
- }
81
-
82
- /**
83
- * Fills every `tether-orders` list in `root`. A failure is reported and leaves the lists empty, without claiming the
84
- * customer has no orders.
85
- */
86
- export async function renderOrderList(
87
- root: ParentNode,
88
- loadOrders: (first: number) => Promise<AccountOrder[]>,
89
- ): Promise<void> {
90
- const lists = qsa(root, sel('orders'));
91
- const first = lists[0];
92
- if (!first) return;
93
- showIn(root, 'show', { 'orders-loading': true, 'orders-empty': false });
94
- const limit = listLimit(get(first, 'limit'));
95
- /** `null` after a failure. */
96
- let orders: AccountOrder[] | null;
97
- try {
98
- orders = await loadOrders(limit);
99
- } catch (error) {
100
- reportError(error, 'orders');
101
- orders = null;
102
- }
103
- showIn(root, 'show', { 'orders-loading': false, 'orders-empty': orders?.length === 0 });
104
- lists.forEach((list) => {
105
- repeat(list, orders ?? [], fillOrder);
106
- });
107
- }
@@ -1,137 +0,0 @@
1
- /**
2
- * The order page (`tether-order`): one order in full, with its lines, totals, addresses, shipments and what can be
3
- * returned. `tether-order-state` and `tether-order-show` follow the load: `loading`, then `ready`, `not-found` or
4
- * `error`.
5
- */
6
- import { reportError } from '../config.ts';
7
- import { isHttpsUrl } from '../shopify-values.ts';
8
- import { attr, formatDate, formatMoney, get, qsa, repeat, sel, setText, show } from '../dom.ts';
9
- import { addressText, bindsIn, enumText, ownValue, showIn } from './markup.ts';
10
- import { fillLines } from './order-lines.ts';
11
- import { order } from './operations.ts';
12
- import type { Fulfillment, OrderDetail } from './page-answers.ts';
13
-
14
- type OrderState = 'loading' | 'ready' | 'not-found' | 'error';
15
-
16
- /** @returns The order `tether-order` names: an id, or `url:<param>` for one in the page's URL. */
17
- function orderKey(el: HTMLElement): string | null {
18
- const raw = get(el, 'order') || '';
19
- if (!raw.startsWith('url:')) return raw;
20
- return new URL(window.location.href).searchParams.get(raw.slice('url:'.length) || 'id');
21
- }
22
-
23
- /** Every state but `ready`, which shows with the order. */
24
- export function showOrderState(el: HTMLElement, state: Exclude<OrderState, 'ready'>): void {
25
- el.setAttribute(attr('order-state'), state);
26
- showIn(el, 'order-show', {
27
- loading: state === 'loading',
28
- ready: false,
29
- 'not-found': state === 'not-found',
30
- error: state === 'error',
31
- });
32
- }
33
-
34
- /** @returns The text of each `tether-order-bind` value. */
35
- const orderTexts = (detail: OrderDetail): Record<string, string> => ({
36
- name: detail.name ?? '',
37
- date: formatDate(detail.processedAt),
38
- total: formatMoney(detail.totalPrice),
39
- subtotal: formatMoney(detail.subtotal),
40
- shipping: formatMoney(detail.totalShipping),
41
- tax: formatMoney(detail.totalTax),
42
- status: enumText(detail.financialStatus),
43
- fulfillment: enumText(detail.fulfillmentStatus),
44
- 'shipping-address': addressText(detail.shippingAddress),
45
- 'billing-address': addressText(detail.billingAddress),
46
- items: detail.lineItems.nodes.map((line) => `${String(line.quantity)}× ${line.title || line.name || ''}`).join(', '),
47
- cancelled: formatDate(detail.cancelledAt),
48
- });
49
-
50
- /** One shipment row: its status and dates, and its tracking link, hidden without an https one. */
51
- function fillFulfillment(row: HTMLElement, fulfillment: Fulfillment): void {
52
- const tracking = fulfillment.trackingInformation[0];
53
- const texts: Record<string, string> = {
54
- status: enumText(fulfillment.latestShipmentStatus || fulfillment.status),
55
- date: formatDate(fulfillment.createdAt),
56
- eta: formatDate(fulfillment.estimatedDeliveryAt),
57
- company: tracking?.company || '',
58
- number: tracking?.number || '',
59
- };
60
- bindsIn(row, 'fulfillment-bind').forEach((el) => {
61
- const key = get(el, 'fulfillment-bind') ?? '';
62
- if (key === 'url') {
63
- const url = tracking?.url ?? null;
64
- const linked = url !== null && isHttpsUrl(url);
65
- if (linked) el.setAttribute('href', url);
66
- show(el, linked);
67
- return;
68
- }
69
- const text = ownValue(texts, key);
70
- if (text !== undefined) setText(el, text);
71
- });
72
- }
73
-
74
- function fillOrder(el: HTMLElement, detail: OrderDetail): void {
75
- const returnable = new Map(
76
- (detail.returnInformation?.returnableLineItems.nodes ?? []).map((entry) => [
77
- entry.lineItem?.id ?? '',
78
- entry.quantity,
79
- ]),
80
- );
81
- const shipments = detail.fulfillments.nodes;
82
- const texts = orderTexts(detail);
83
- qsa(el, sel('order-bind')).forEach((bind) => {
84
- const key = get(bind, 'order-bind') ?? '';
85
- if (key === 'url') {
86
- const statusPage = detail.statusPageUrl;
87
- if (statusPage !== null && isHttpsUrl(statusPage)) bind.setAttribute('href', statusPage);
88
- return;
89
- }
90
- const text = ownValue(texts, key);
91
- if (text !== undefined) setText(bind, text);
92
- });
93
- qsa(el, sel('order-lines')).forEach((box) => {
94
- fillLines(box, detail.lineItems.nodes, returnable);
95
- });
96
- qsa(el, sel('order-fulfillments')).forEach((box) => {
97
- repeat(box, shipments, fillFulfillment);
98
- });
99
- const returnRequested = el.dataset['tetherReturned'] === '1';
100
- showIn(el, 'order-show', {
101
- loading: false,
102
- ready: true,
103
- error: false,
104
- 'not-found': false,
105
- cancelled: Boolean(detail.cancelledAt),
106
- shipped: shipments.length > 0,
107
- returnable: [...returnable.values()].some((quantity) => quantity > 0),
108
- 'has-returns': !returnRequested && detail.returns.nodes.length > 0,
109
- 'return-requested': returnRequested,
110
- });
111
- el.setAttribute(attr('order-state'), 'ready');
112
- }
113
-
114
- /** Loads and renders the order `el` names. A failure is reported and shows the error state. */
115
- export async function renderOrder(el: HTMLElement): Promise<void> {
116
- const key = orderKey(el);
117
- if (!key) {
118
- el.setAttribute(attr('order-state'), 'not-found');
119
- showIn(el, 'order-show', { 'not-found': true });
120
- return;
121
- }
122
- showOrderState(el, 'loading');
123
- let detail: OrderDetail | null;
124
- try {
125
- detail = await order(key);
126
- } catch (error) {
127
- reportError(error, 'order');
128
- showOrderState(el, 'error');
129
- return;
130
- }
131
- if (!detail) {
132
- showOrderState(el, 'not-found');
133
- return;
134
- }
135
- el.dataset['tetherOrderId'] = detail.id;
136
- fillOrder(el, detail);
137
- }