@shopify/cli 3.65.1 → 3.65.3

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 (122) hide show
  1. package/dist/assets/hydrogen/i18n/domains.ts +4 -11
  2. package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +4 -2
  3. package/dist/assets/hydrogen/i18n/subdomains.ts +4 -11
  4. package/dist/assets/hydrogen/i18n/subfolders.ts +4 -11
  5. package/dist/assets/hydrogen/starter/CHANGELOG.md +165 -0
  6. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +5 -2
  7. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +2 -2
  8. package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +65 -19
  9. package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +42 -0
  10. package/dist/assets/hydrogen/starter/app/components/SearchForm.tsx +68 -0
  11. package/dist/assets/hydrogen/starter/app/components/SearchFormPredictive.tsx +76 -0
  12. package/dist/assets/hydrogen/starter/app/components/SearchResults.tsx +164 -0
  13. package/dist/assets/hydrogen/starter/app/components/SearchResultsPredictive.tsx +322 -0
  14. package/dist/assets/hydrogen/starter/app/entry.client.tsx +10 -8
  15. package/dist/assets/hydrogen/starter/app/entry.server.tsx +1 -1
  16. package/dist/assets/hydrogen/starter/app/lib/context.ts +43 -0
  17. package/dist/assets/hydrogen/starter/app/lib/fragments.ts +53 -0
  18. package/dist/assets/hydrogen/starter/app/lib/search.ts +74 -24
  19. package/dist/assets/hydrogen/starter/app/root.tsx +4 -7
  20. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +2 -3
  21. package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +5 -19
  22. package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +11 -24
  23. package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +14 -27
  24. package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +12 -30
  25. package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +13 -27
  26. package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +9 -31
  27. package/dist/assets/hydrogen/starter/app/routes/search.tsx +312 -73
  28. package/dist/assets/hydrogen/starter/app/styles/reset.css +12 -2
  29. package/dist/assets/hydrogen/starter/env.d.ts +11 -30
  30. package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
  31. package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +391 -0
  32. package/dist/assets/hydrogen/starter/guides/search/search.jpg +0 -0
  33. package/dist/assets/hydrogen/starter/guides/search/search.md +333 -0
  34. package/dist/assets/hydrogen/starter/package.json +4 -4
  35. package/dist/assets/hydrogen/starter/server.ts +18 -74
  36. package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +242 -172
  37. package/dist/assets/hydrogen/virtual-routes/components/{PageLayout.jsx → Layout.jsx} +2 -2
  38. package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +7 -6
  39. package/dist/{chunk-H42RFZDD.js → chunk-3ABSSTBQ.js} +4 -4
  40. package/dist/{chunk-M7WMYV4S.js → chunk-3D4VZQOH.js} +2 -2
  41. package/dist/{chunk-J7BYFGNJ.js → chunk-3GSKXZGY.js} +2 -2
  42. package/dist/{chunk-TDWX3KIR.js → chunk-3LDWVYMD.js} +2 -2
  43. package/dist/{chunk-N2BXKOJG.js → chunk-646BIVHE.js} +4 -4
  44. package/dist/{chunk-CZ3SHYYH.js → chunk-7WAEFADN.js} +4 -4
  45. package/dist/{chunk-EKT2GUGH.js → chunk-7WGBIPDW.js} +2 -2
  46. package/dist/{chunk-M6KGRVDD.js → chunk-AX77SAMU.js} +3 -3
  47. package/dist/{chunk-4HAEQQTQ.js → chunk-BQBBVYYU.js} +4 -4
  48. package/dist/{chunk-5YD4FDOS.js → chunk-BZLNTDGG.js} +3 -3
  49. package/dist/{chunk-VWALMO2Z.js → chunk-CSCEGIBZ.js} +3 -3
  50. package/dist/{chunk-F2Y7KYHZ.js → chunk-EIUQV76I.js} +5 -5
  51. package/dist/{chunk-MODBIZ4R.js → chunk-GN74L7IW.js} +2 -2
  52. package/dist/{chunk-5EAVIJTQ.js → chunk-HYCRESCR.js} +2 -2
  53. package/dist/{chunk-GDARYUPU.js → chunk-K7KD247K.js} +188 -243
  54. package/dist/{chunk-PZM45AUI.js → chunk-KIUXMPTX.js} +3 -3
  55. package/dist/{chunk-PYMSCBPA.js → chunk-LAJ4OEME.js} +2 -2
  56. package/dist/{chunk-YVHV3H5H.js → chunk-MIQBXNSN.js} +4 -4
  57. package/dist/{chunk-BLKDGMHM.js → chunk-MV6A3QHA.js} +4 -4
  58. package/dist/{chunk-CFFAWVDL.js → chunk-N3YORLAS.js} +2 -2
  59. package/dist/{chunk-EU5ZOEUT.js → chunk-NBTEOGQW.js} +2 -2
  60. package/dist/{chunk-ZXJU6UP4.js → chunk-O3JOUAA5.js} +4 -4
  61. package/dist/{chunk-EZ5DG73H.js → chunk-PEAIOYXD.js} +4 -4
  62. package/dist/{chunk-YDS7NZBQ.js → chunk-R5GT4GBL.js} +4 -4
  63. package/dist/{chunk-6M65VRAT.js → chunk-S7FJTFYR.js} +5 -5
  64. package/dist/{chunk-DX2RXOQ5.js → chunk-S7RH664J.js} +3 -3
  65. package/dist/{chunk-WMECC32P.js → chunk-SKF2SKWO.js} +3 -3
  66. package/dist/{chunk-27HGZPUX.js → chunk-SMKCVFDT.js} +3 -3
  67. package/dist/{chunk-EID6L4PR.js → chunk-T4Y7NDNJ.js} +2 -2
  68. package/dist/{chunk-PY33KMCK.js → chunk-TWWJNMTO.js} +2 -2
  69. package/dist/{chunk-YXPGPWR2.js → chunk-U2PN6QZ2.js} +5 -5
  70. package/dist/{chunk-3REVOIEW.js → chunk-UBCH575K.js} +5 -5
  71. package/dist/{chunk-A4NQWDPT.js → chunk-XLURAR5E.js} +3 -3
  72. package/dist/{chunk-ZZKUI3DP.js → chunk-YPG7LXPN.js} +3 -3
  73. package/dist/cli/commands/auth/logout.js +10 -10
  74. package/dist/cli/commands/auth/logout.test.js +11 -11
  75. package/dist/cli/commands/debug/command-flags.js +9 -9
  76. package/dist/cli/commands/demo/catalog.js +10 -10
  77. package/dist/cli/commands/demo/generate-file.js +10 -10
  78. package/dist/cli/commands/demo/index.js +10 -10
  79. package/dist/cli/commands/demo/print-ai-prompt.js +10 -10
  80. package/dist/cli/commands/docs/generate.js +9 -9
  81. package/dist/cli/commands/docs/generate.test.js +9 -9
  82. package/dist/cli/commands/help.js +9 -9
  83. package/dist/cli/commands/kitchen-sink/async.js +10 -10
  84. package/dist/cli/commands/kitchen-sink/async.test.js +10 -10
  85. package/dist/cli/commands/kitchen-sink/index.js +12 -12
  86. package/dist/cli/commands/kitchen-sink/index.test.js +12 -12
  87. package/dist/cli/commands/kitchen-sink/prompts.js +10 -10
  88. package/dist/cli/commands/kitchen-sink/prompts.test.js +10 -10
  89. package/dist/cli/commands/kitchen-sink/static.js +10 -10
  90. package/dist/cli/commands/kitchen-sink/static.test.js +10 -10
  91. package/dist/cli/commands/search.js +10 -10
  92. package/dist/cli/commands/upgrade.js +9 -9
  93. package/dist/cli/commands/version.js +10 -10
  94. package/dist/cli/commands/version.test.js +10 -10
  95. package/dist/cli/services/commands/search.js +2 -2
  96. package/dist/cli/services/commands/search.test.js +2 -2
  97. package/dist/cli/services/commands/version.js +4 -4
  98. package/dist/cli/services/commands/version.test.js +5 -5
  99. package/dist/cli/services/demo.js +2 -2
  100. package/dist/cli/services/demo.test.js +2 -2
  101. package/dist/cli/services/kitchen-sink/async.js +2 -2
  102. package/dist/cli/services/kitchen-sink/prompts.js +2 -2
  103. package/dist/cli/services/kitchen-sink/static.js +2 -2
  104. package/dist/cli/services/upgrade.js +3 -3
  105. package/dist/cli/services/upgrade.test.js +5 -5
  106. package/dist/{custom-oclif-loader-JHNX2EGV.js → custom-oclif-loader-BT7EH2NN.js} +3 -3
  107. package/dist/{error-handler-4UJ6363X.js → error-handler-OSEY6KVA.js} +8 -8
  108. package/dist/hooks/postrun.js +6 -6
  109. package/dist/hooks/prerun.js +4 -4
  110. package/dist/index.js +1333 -1279
  111. package/dist/{local-V7RONWNU.js → local-OQXN5NM2.js} +2 -2
  112. package/dist/{morph-DN4AZJZW.js → morph-IQTWRBBT.js} +16 -12
  113. package/dist/{node-3H4OKRLA.js → node-YQVH3Y7J.js} +13 -13
  114. package/dist/{node-package-manager-XM7EXHQA.js → node-package-manager-VW2DN7R4.js} +3 -3
  115. package/dist/{system-F63VIZ5U.js → system-347PZWVP.js} +2 -2
  116. package/dist/tsconfig.tsbuildinfo +1 -1
  117. package/dist/{ui-BXWWRIFS.js → ui-S7L55PBH.js} +2 -2
  118. package/dist/{workerd-A5NCF6UA.js → workerd-OLKE7G4X.js} +12 -12
  119. package/oclif.manifest.json +39 -2
  120. package/package.json +7 -7
  121. package/dist/assets/hydrogen/starter/app/components/Search.tsx +0 -514
  122. package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +0 -318
@@ -1,18 +1,13 @@
1
- import type {LanguageCode, CountryCode} from './mock-i18n-types.js';
1
+ import type {I18nBase} from './mock-i18n-types.js';
2
2
 
3
- export type I18nLocale = {language: LanguageCode; country: CountryCode};
4
-
5
- /**
6
- * @returns {I18nLocale}
7
- */
8
- function getLocaleFromRequest(request: Request): I18nLocale {
9
- const defaultLocale: I18nLocale = {language: 'EN', country: 'US'};
3
+ export function getLocaleFromRequest(request: Request): I18nBase {
4
+ const defaultLocale: I18nBase = {language: 'EN', country: 'US'};
10
5
  const supportedLocales = {
11
6
  ES: 'ES',
12
7
  FR: 'FR',
13
8
  DE: 'DE',
14
9
  JP: 'JA',
15
- } as Record<I18nLocale['country'], I18nLocale['language']>;
10
+ } as Record<I18nBase['country'], I18nBase['language']>;
16
11
 
17
12
  const url = new URL(request.url);
18
13
  const domain = url.hostname
@@ -24,5 +19,3 @@ function getLocaleFromRequest(request: Request): I18nLocale {
24
19
  ? {language: supportedLocales[domain], country: domain}
25
20
  : defaultLocale;
26
21
  }
27
-
28
- export {getLocaleFromRequest};
@@ -1,3 +1,5 @@
1
1
  // Mock types so we don't need to depend on Hydrogen React
2
- export type CountryCode = 'US' | 'ES' | 'FR' | 'DE' | 'JP';
3
- export type LanguageCode = 'EN' | 'ES' | 'FR' | 'DE' | 'JA';
2
+ export type I18nBase = {
3
+ language: 'EN' | 'ES' | 'FR' | 'DE' | 'JA';
4
+ country: 'US' | 'ES' | 'FR' | 'DE' | 'JP';
5
+ };
@@ -1,18 +1,13 @@
1
- import type {LanguageCode, CountryCode} from './mock-i18n-types.js';
1
+ import type {I18nBase} from './mock-i18n-types.js';
2
2
 
3
- export type I18nLocale = {language: LanguageCode; country: CountryCode};
4
-
5
- /**
6
- * @returns {I18nLocale}
7
- */
8
- function getLocaleFromRequest(request: Request): I18nLocale {
9
- const defaultLocale: I18nLocale = {language: 'EN', country: 'US'};
3
+ export function getLocaleFromRequest(request: Request): I18nBase {
4
+ const defaultLocale: I18nBase = {language: 'EN', country: 'US'};
10
5
  const supportedLocales = {
11
6
  ES: 'ES',
12
7
  FR: 'FR',
13
8
  DE: 'DE',
14
9
  JP: 'JA',
15
- } as Record<I18nLocale['country'], I18nLocale['language']>;
10
+ } as Record<I18nBase['country'], I18nBase['language']>;
16
11
 
17
12
  const url = new URL(request.url);
18
13
  const firstSubdomain = url.hostname
@@ -23,5 +18,3 @@ function getLocaleFromRequest(request: Request): I18nLocale {
23
18
  ? {language: supportedLocales[firstSubdomain], country: firstSubdomain}
24
19
  : defaultLocale;
25
20
  }
26
-
27
- export {getLocaleFromRequest};
@@ -1,15 +1,10 @@
1
- import type {LanguageCode, CountryCode} from './mock-i18n-types.js';
1
+ import type {I18nBase} from './mock-i18n-types.js';
2
2
 
3
- export type I18nLocale = {
4
- language: LanguageCode;
5
- country: CountryCode;
3
+ export interface I18nLocale extends I18nBase {
6
4
  pathPrefix: string;
7
- };
5
+ }
8
6
 
9
- /**
10
- * @returns {I18nLocale}
11
- */
12
- function getLocaleFromRequest(request: Request): I18nLocale {
7
+ export function getLocaleFromRequest(request: Request): I18nLocale {
13
8
  const url = new URL(request.url);
14
9
  const firstPathPart = url.pathname.split('/')[1]?.toUpperCase() ?? '';
15
10
 
@@ -25,5 +20,3 @@ function getLocaleFromRequest(request: Request): I18nLocale {
25
20
 
26
21
  return {language, country, pathPrefix};
27
22
  }
28
-
29
- export {getLocaleFromRequest};
@@ -1,5 +1,170 @@
1
1
  # skeleton
2
2
 
3
+ ## 2024.7.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Search & Predictive Search improvements ([#2363](https://github.com/Shopify/hydrogen/pull/2363)) by [@juanpprieto](https://github.com/juanpprieto)
8
+
9
+ - 1. Create a app/lib/context file and use `createHydrogenContext` in it. ([#2333](https://github.com/Shopify/hydrogen/pull/2333)) by [@michenly](https://github.com/michenly)
10
+
11
+ ```.ts
12
+ // in app/lib/context
13
+
14
+ import {createHydrogenContext} from '@shopify/hydrogen';
15
+
16
+ export async function createAppLoadContext(
17
+ request: Request,
18
+ env: Env,
19
+ executionContext: ExecutionContext,
20
+ ) {
21
+ const hydrogenContext = createHydrogenContext({
22
+ env,
23
+ request,
24
+ cache,
25
+ waitUntil,
26
+ session,
27
+ i18n: {language: 'EN', country: 'US'},
28
+ cart: {
29
+ queryFragment: CART_QUERY_FRAGMENT,
30
+ },
31
+ // ensure to overwrite any options that is not using the default values from your server.ts
32
+ });
33
+
34
+ return {
35
+ ...hydrogenContext,
36
+ // declare additional Remix loader context
37
+ };
38
+ }
39
+
40
+ ```
41
+
42
+ 2. Use `createAppLoadContext` method in server.ts Ensure to overwrite any options that is not using the default values in `createHydrogenContext`.
43
+
44
+ ```diff
45
+ // in server.ts
46
+
47
+ - import {
48
+ - createCartHandler,
49
+ - createStorefrontClient,
50
+ - createCustomerAccountClient,
51
+ - } from '@shopify/hydrogen';
52
+ + import {createAppLoadContext} from '~/lib/context';
53
+
54
+ export default {
55
+ async fetch(
56
+ request: Request,
57
+ env: Env,
58
+ executionContext: ExecutionContext,
59
+ ): Promise<Response> {
60
+
61
+ - const {storefront} = createStorefrontClient(
62
+ - ...
63
+ - );
64
+
65
+ - const customerAccount = createCustomerAccountClient(
66
+ - ...
67
+ - );
68
+
69
+ - const cart = createCartHandler(
70
+ - ...
71
+ - );
72
+
73
+ + const appLoadContext = await createAppLoadContext(
74
+ + request,
75
+ + env,
76
+ + executionContext,
77
+ + );
78
+
79
+ /**
80
+ * Create a Remix request handler and pass
81
+ * Hydrogen's Storefront client to the loader context.
82
+ */
83
+ const handleRequest = createRequestHandler({
84
+ build: remixBuild,
85
+ mode: process.env.NODE_ENV,
86
+ - getLoadContext: (): AppLoadContext => ({
87
+ - session,
88
+ - storefront,
89
+ - customerAccount,
90
+ - cart,
91
+ - env,
92
+ - waitUntil,
93
+ - }),
94
+ + getLoadContext: () => appLoadContext,
95
+ });
96
+ }
97
+ ```
98
+
99
+ 3. Use infer type for AppLoadContext in env.d.ts
100
+
101
+ ```diff
102
+ // in env.d.ts
103
+
104
+ + import type {createAppLoadContext} from '~/lib/context';
105
+
106
+ + interface AppLoadContext extends Awaited<ReturnType<typeof createAppLoadContext>> {
107
+ - interface AppLoadContext {
108
+ - env: Env;
109
+ - cart: HydrogenCart;
110
+ - storefront: Storefront;
111
+ - customerAccount: CustomerAccount;
112
+ - session: AppSession;
113
+ - waitUntil: ExecutionContext['waitUntil'];
114
+ }
115
+
116
+ ```
117
+
118
+ - Use type `HydrogenEnv` for all the env.d.ts ([#2333](https://github.com/Shopify/hydrogen/pull/2333)) by [@michenly](https://github.com/michenly)
119
+
120
+ ```diff
121
+ // in env.d.ts
122
+
123
+ + import type {HydrogenEnv} from '@shopify/hydrogen';
124
+
125
+ + interface Env extends HydrogenEnv {}
126
+ - interface Env {
127
+ - SESSION_SECRET: string;
128
+ - PUBLIC_STOREFRONT_API_TOKEN: string;
129
+ - PRIVATE_STOREFRONT_API_TOKEN: string;
130
+ - PUBLIC_STORE_DOMAIN: string;
131
+ - PUBLIC_STOREFRONT_ID: string;
132
+ - PUBLIC_CUSTOMER_ACCOUNT_API_CLIENT_ID: string;
133
+ - PUBLIC_CUSTOMER_ACCOUNT_API_URL: string;
134
+ - PUBLIC_CHECKOUT_DOMAIN: string;
135
+ - }
136
+
137
+ ```
138
+
139
+ - Add a hydration check for google web cache. This prevents an infinite redirect when viewing the cached version of a hydrogen site on Google. ([#2334](https://github.com/Shopify/hydrogen/pull/2334)) by [@blittle](https://github.com/blittle)
140
+
141
+ Update your entry.server.jsx file to include this check:
142
+
143
+ ```diff
144
+ + if (!window.location.origin.includes("webcache.googleusercontent.com")) {
145
+ startTransition(() => {
146
+ hydrateRoot(
147
+ document,
148
+ <StrictMode>
149
+ <RemixBrowser />
150
+ </StrictMode>
151
+ );
152
+ });
153
+ + }
154
+ ```
155
+
156
+ - Updated dependencies [[`a2d9acf9`](https://github.com/Shopify/hydrogen/commit/a2d9acf95e019c39df0b10f4841a1d809b810c80), [`c0d7d917`](https://github.com/Shopify/hydrogen/commit/c0d7d9176c80b996064d8e897876f954807c7640), [`b09e9a4c`](https://github.com/Shopify/hydrogen/commit/b09e9a4ca7b931e48462c2d174ca9f67c37f1da2), [`c204eacf`](https://github.com/Shopify/hydrogen/commit/c204eacf0273f625109523ee81053cdc0c4de7e1), [`bf4e3d3c`](https://github.com/Shopify/hydrogen/commit/bf4e3d3c00744a066b50250a12e4f3c675691811), [`20a8e63b`](https://github.com/Shopify/hydrogen/commit/20a8e63b5fd1c8acadda7612c5d4cc411e0c5932), [`6e5d8ea7`](https://github.com/Shopify/hydrogen/commit/6e5d8ea71a2639925d5817b662af26a6b2ba3c6d), [`7c4f67a6`](https://github.com/Shopify/hydrogen/commit/7c4f67a684ad31edea10d1407d00201bbaaa9822), [`dfb9be77`](https://github.com/Shopify/hydrogen/commit/dfb9be7721c7d10cf4354fda60db4e666625518e), [`31ea19e8`](https://github.com/Shopify/hydrogen/commit/31ea19e8957dbc4487314b014a14920444d37f78)]:
157
+ - @shopify/cli-hydrogen@8.4.0
158
+ - @shopify/hydrogen@2024.7.3
159
+ - @shopify/remix-oxygen@2.0.6
160
+
161
+ ## 2024.7.3
162
+
163
+ ### Patch Changes
164
+
165
+ - Updated dependencies [[`150854ed`](https://github.com/Shopify/hydrogen/commit/150854ed1352245eef180cc6b2bceb41dd8cc898)]:
166
+ - @shopify/hydrogen@2024.7.2
167
+
3
168
  ## 2024.7.2
4
169
 
5
170
  ### Patch Changes
@@ -5,6 +5,9 @@ import {useVariantUrl} from '~/lib/variants';
5
5
  import {Link} from '@remix-run/react';
6
6
  import {ProductPrice} from './ProductPrice';
7
7
  import {useAside} from './Aside';
8
+ import type {CartApiQueryFragment} from 'storefrontapi.generated';
9
+
10
+ type CartLine = OptimisticCartLine<CartApiQueryFragment>;
8
11
 
9
12
  /**
10
13
  * A single line item in the cart. It displays the product image, title, price.
@@ -15,7 +18,7 @@ export function CartLineItem({
15
18
  line,
16
19
  }: {
17
20
  layout: CartLayout;
18
- line: OptimisticCartLine;
21
+ line: CartLine;
19
22
  }) {
20
23
  const {id, merchandise} = line;
21
24
  const {product, title, image, selectedOptions} = merchandise;
@@ -70,7 +73,7 @@ export function CartLineItem({
70
73
  * These controls are disabled when the line item is new, and the server
71
74
  * hasn't yet responded that it was successfully added to the cart.
72
75
  */
73
- function CartLineQuantity({line}: {line: OptimisticCartLine}) {
76
+ function CartLineQuantity({line}: {line: CartLine}) {
74
77
  if (!line || typeof line?.quantity === 'undefined') return null;
75
78
  const {id: lineId, quantity, isOptimistic} = line;
76
79
  const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
@@ -1,4 +1,4 @@
1
- import {type OptimisticCartLine, useOptimisticCart} from '@shopify/hydrogen';
1
+ import {useOptimisticCart} from '@shopify/hydrogen';
2
2
  import {Link} from '@remix-run/react';
3
3
  import type {CartApiQueryFragment} from 'storefrontapi.generated';
4
4
  import {useAside} from '~/components/Aside';
@@ -34,7 +34,7 @@ export function CartMain({layout, cart: originalCart}: CartMainProps) {
34
34
  <div className="cart-details">
35
35
  <div aria-labelledby="cart-lines">
36
36
  <ul>
37
- {(cart?.lines?.nodes ?? []).map((line: OptimisticCartLine) => (
37
+ {(cart?.lines?.nodes ?? []).map((line) => (
38
38
  <CartLineItem key={line.id} line={line} layout={layout} />
39
39
  ))}
40
40
  </ul>
@@ -1,4 +1,4 @@
1
- import {Await} from '@remix-run/react';
1
+ import {Await, Link} from '@remix-run/react';
2
2
  import {Suspense} from 'react';
3
3
  import type {
4
4
  CartApiQueryFragment,
@@ -10,9 +10,10 @@ import {Footer} from '~/components/Footer';
10
10
  import {Header, HeaderMenu} from '~/components/Header';
11
11
  import {CartMain} from '~/components/CartMain';
12
12
  import {
13
- PredictiveSearchForm,
14
- PredictiveSearchResults,
15
- } from '~/components/Search';
13
+ SEARCH_ENDPOINT,
14
+ SearchFormPredictive,
15
+ } from '~/components/SearchFormPredictive';
16
+ import {SearchResultsPredictive} from '~/components/SearchResultsPredictive';
16
17
 
17
18
  interface PageLayoutProps {
18
19
  cart: Promise<CartApiQueryFragment | null>;
@@ -73,9 +74,9 @@ function SearchAside() {
73
74
  <Aside type="search" heading="SEARCH">
74
75
  <div className="predictive-search">
75
76
  <br />
76
- <PredictiveSearchForm>
77
- {({fetchResults, inputRef}) => (
78
- <div>
77
+ <SearchFormPredictive>
78
+ {({fetchResults, goToSearch, inputRef}) => (
79
+ <>
79
80
  <input
80
81
  name="q"
81
82
  onChange={fetchResults}
@@ -85,19 +86,64 @@ function SearchAside() {
85
86
  type="search"
86
87
  />
87
88
  &nbsp;
88
- <button
89
- onClick={() => {
90
- window.location.href = inputRef?.current?.value
91
- ? `/search?q=${inputRef.current.value}`
92
- : `/search`;
93
- }}
94
- >
95
- Search
96
- </button>
97
- </div>
89
+ <button onClick={goToSearch}>Search</button>
90
+ </>
98
91
  )}
99
- </PredictiveSearchForm>
100
- <PredictiveSearchResults />
92
+ </SearchFormPredictive>
93
+
94
+ <SearchResultsPredictive>
95
+ {({items, total, term, state, inputRef, closeSearch}) => {
96
+ const {articles, collections, pages, products, queries} = items;
97
+
98
+ if (state === 'loading' && term.current) {
99
+ return <div>Loading...</div>;
100
+ }
101
+
102
+ if (!total) {
103
+ return <SearchResultsPredictive.Empty term={term} />;
104
+ }
105
+
106
+ return (
107
+ <>
108
+ <SearchResultsPredictive.Queries
109
+ queries={queries}
110
+ inputRef={inputRef}
111
+ />
112
+ <SearchResultsPredictive.Products
113
+ products={products}
114
+ closeSearch={closeSearch}
115
+ term={term}
116
+ />
117
+ <SearchResultsPredictive.Collections
118
+ collections={collections}
119
+ closeSearch={closeSearch}
120
+ term={term}
121
+ />
122
+ <SearchResultsPredictive.Pages
123
+ pages={pages}
124
+ closeSearch={closeSearch}
125
+ term={term}
126
+ />
127
+ <SearchResultsPredictive.Articles
128
+ articles={articles}
129
+ closeSearch={closeSearch}
130
+ term={term}
131
+ />
132
+ {term.current && total ? (
133
+ <Link
134
+ onClick={closeSearch}
135
+ to={`${SEARCH_ENDPOINT}?q=${term.current}`}
136
+ >
137
+ <p>
138
+ View all results for <q>{term.current}</q>
139
+ &nbsp; →
140
+ </p>
141
+ </Link>
142
+ ) : null}
143
+ </>
144
+ );
145
+ }}
146
+ </SearchResultsPredictive>
101
147
  </div>
102
148
  </Aside>
103
149
  );
@@ -0,0 +1,42 @@
1
+ import * as React from 'react';
2
+ import {Pagination} from '@shopify/hydrogen';
3
+
4
+ /**
5
+ * <PaginatedResourceSection > is a component that encapsulate how the previous and next behaviors throughout your application.
6
+ */
7
+
8
+ export function PaginatedResourceSection<NodesType>({
9
+ connection,
10
+ children,
11
+ resourcesClassName,
12
+ }: {
13
+ connection: React.ComponentProps<typeof Pagination<NodesType>>['connection'];
14
+ children: React.FunctionComponent<{node: NodesType; index: number}>;
15
+ resourcesClassName?: string;
16
+ }) {
17
+ return (
18
+ <Pagination connection={connection}>
19
+ {({nodes, isLoading, PreviousLink, NextLink}) => {
20
+ const resoucesMarkup = nodes.map((node, index) =>
21
+ children({node, index}),
22
+ );
23
+
24
+ return (
25
+ <div>
26
+ <PreviousLink>
27
+ {isLoading ? 'Loading...' : <span>↑ Load previous</span>}
28
+ </PreviousLink>
29
+ {resourcesClassName ? (
30
+ <div className={resourcesClassName}>{resoucesMarkup}</div>
31
+ ) : (
32
+ resoucesMarkup
33
+ )}
34
+ <NextLink>
35
+ {isLoading ? 'Loading...' : <span>Load more ↓</span>}
36
+ </NextLink>
37
+ </div>
38
+ );
39
+ }}
40
+ </Pagination>
41
+ );
42
+ }
@@ -0,0 +1,68 @@
1
+ import {useRef, useEffect} from 'react';
2
+ import {Form, type FormProps} from '@remix-run/react';
3
+
4
+ type SearchFormProps = Omit<FormProps, 'children'> & {
5
+ children: (args: {
6
+ inputRef: React.RefObject<HTMLInputElement>;
7
+ }) => React.ReactNode;
8
+ };
9
+
10
+ /**
11
+ * Search form component that sends search requests to the `/search` route.
12
+ * @example
13
+ * ```tsx
14
+ * <SearchForm>
15
+ * {({inputRef}) => (
16
+ * <>
17
+ * <input
18
+ * ref={inputRef}
19
+ * type="search"
20
+ * defaultValue={term}
21
+ * name="q"
22
+ * placeholder="Search…"
23
+ * />
24
+ * <button type="submit">Search</button>
25
+ * </>
26
+ * )}
27
+ * </SearchForm>
28
+ */
29
+ export function SearchForm({children, ...props}: SearchFormProps) {
30
+ const inputRef = useRef<HTMLInputElement | null>(null);
31
+
32
+ useFocusOnCmdK(inputRef);
33
+
34
+ if (typeof children !== 'function') {
35
+ return null;
36
+ }
37
+
38
+ return (
39
+ <Form method="get" {...props}>
40
+ {children({inputRef})}
41
+ </Form>
42
+ );
43
+ }
44
+
45
+ /**
46
+ * Focuses the input when cmd+k is pressed
47
+ */
48
+ function useFocusOnCmdK(inputRef: React.RefObject<HTMLInputElement>) {
49
+ // focus the input when cmd+k is pressed
50
+ useEffect(() => {
51
+ function handleKeyDown(event: KeyboardEvent) {
52
+ if (event.key === 'k' && event.metaKey) {
53
+ event.preventDefault();
54
+ inputRef.current?.focus();
55
+ }
56
+
57
+ if (event.key === 'Escape') {
58
+ inputRef.current?.blur();
59
+ }
60
+ }
61
+
62
+ document.addEventListener('keydown', handleKeyDown);
63
+
64
+ return () => {
65
+ document.removeEventListener('keydown', handleKeyDown);
66
+ };
67
+ }, [inputRef]);
68
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ useFetcher,
3
+ useNavigate,
4
+ type FormProps,
5
+ type Fetcher,
6
+ } from '@remix-run/react';
7
+ import React, {useRef, useEffect} from 'react';
8
+ import type {PredictiveSearchReturn} from '~/lib/search';
9
+ import {useAside} from './Aside';
10
+
11
+ type SearchFormPredictiveChildren = (args: {
12
+ fetchResults: (event: React.ChangeEvent<HTMLInputElement>) => void;
13
+ goToSearch: () => void;
14
+ inputRef: React.MutableRefObject<HTMLInputElement | null>;
15
+ fetcher: Fetcher<PredictiveSearchReturn>;
16
+ }) => React.ReactNode;
17
+
18
+ type SearchFormPredictiveProps = Omit<FormProps, 'children'> & {
19
+ children: SearchFormPredictiveChildren | null;
20
+ };
21
+
22
+ export const SEARCH_ENDPOINT = '/search';
23
+
24
+ /**
25
+ * Search form component that sends search requests to the `/search` route
26
+ **/
27
+ export function SearchFormPredictive({
28
+ children,
29
+ className = 'predictive-search-form',
30
+ ...props
31
+ }: SearchFormPredictiveProps) {
32
+ const fetcher = useFetcher<PredictiveSearchReturn>({key: 'search'});
33
+ const inputRef = useRef<HTMLInputElement | null>(null);
34
+ const navigate = useNavigate();
35
+ const aside = useAside();
36
+
37
+ /** Reset the input value and blur the input */
38
+ function resetInput(event: React.FormEvent<HTMLFormElement>) {
39
+ event.preventDefault();
40
+ event.stopPropagation();
41
+ if (inputRef?.current?.value) {
42
+ inputRef.current.blur();
43
+ }
44
+ }
45
+
46
+ /** Navigate to the search page with the current input value */
47
+ function goToSearch() {
48
+ const term = inputRef?.current?.value;
49
+ navigate(SEARCH_ENDPOINT + (term ? `?q=${term}` : ''));
50
+ aside.close();
51
+ }
52
+
53
+ /** Fetch search results based on the input value */
54
+ function fetchResults(event: React.ChangeEvent<HTMLInputElement>) {
55
+ fetcher.submit(
56
+ {q: event.target.value || '', limit: 5, predictive: true},
57
+ {method: 'GET', action: SEARCH_ENDPOINT},
58
+ );
59
+ }
60
+
61
+ // ensure the passed input has a type of search, because SearchResults
62
+ // will select the element based on the input
63
+ useEffect(() => {
64
+ inputRef?.current?.setAttribute('type', 'search');
65
+ }, []);
66
+
67
+ if (typeof children !== 'function') {
68
+ return null;
69
+ }
70
+
71
+ return (
72
+ <fetcher.Form {...props} className={className} onSubmit={resetInput}>
73
+ {children({inputRef, fetcher, fetchResults, goToSearch})}
74
+ </fetcher.Form>
75
+ );
76
+ }