@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.
- package/dist/assets/hydrogen/i18n/domains.ts +4 -11
- package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +4 -2
- package/dist/assets/hydrogen/i18n/subdomains.ts +4 -11
- package/dist/assets/hydrogen/i18n/subfolders.ts +4 -11
- package/dist/assets/hydrogen/starter/CHANGELOG.md +165 -0
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +5 -2
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +2 -2
- package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +65 -19
- package/dist/assets/hydrogen/starter/app/components/PaginatedResourceSection.tsx +42 -0
- package/dist/assets/hydrogen/starter/app/components/SearchForm.tsx +68 -0
- package/dist/assets/hydrogen/starter/app/components/SearchFormPredictive.tsx +76 -0
- package/dist/assets/hydrogen/starter/app/components/SearchResults.tsx +164 -0
- package/dist/assets/hydrogen/starter/app/components/SearchResultsPredictive.tsx +322 -0
- package/dist/assets/hydrogen/starter/app/entry.client.tsx +10 -8
- package/dist/assets/hydrogen/starter/app/entry.server.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/lib/context.ts +43 -0
- package/dist/assets/hydrogen/starter/app/lib/fragments.ts +53 -0
- package/dist/assets/hydrogen/starter/app/lib/search.ts +74 -24
- package/dist/assets/hydrogen/starter/app/root.tsx +4 -7
- package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +2 -3
- package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +5 -19
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +11 -24
- package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +14 -27
- package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +12 -30
- package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +13 -27
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +9 -31
- package/dist/assets/hydrogen/starter/app/routes/search.tsx +312 -73
- package/dist/assets/hydrogen/starter/app/styles/reset.css +12 -2
- package/dist/assets/hydrogen/starter/env.d.ts +11 -30
- package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.jpg +0 -0
- package/dist/assets/hydrogen/starter/guides/predictiveSearch/predictiveSearch.md +391 -0
- package/dist/assets/hydrogen/starter/guides/search/search.jpg +0 -0
- package/dist/assets/hydrogen/starter/guides/search/search.md +333 -0
- package/dist/assets/hydrogen/starter/package.json +4 -4
- package/dist/assets/hydrogen/starter/server.ts +18 -74
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +242 -172
- package/dist/assets/hydrogen/virtual-routes/components/{PageLayout.jsx → Layout.jsx} +2 -2
- package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +7 -6
- package/dist/{chunk-H42RFZDD.js → chunk-3ABSSTBQ.js} +4 -4
- package/dist/{chunk-M7WMYV4S.js → chunk-3D4VZQOH.js} +2 -2
- package/dist/{chunk-J7BYFGNJ.js → chunk-3GSKXZGY.js} +2 -2
- package/dist/{chunk-TDWX3KIR.js → chunk-3LDWVYMD.js} +2 -2
- package/dist/{chunk-N2BXKOJG.js → chunk-646BIVHE.js} +4 -4
- package/dist/{chunk-CZ3SHYYH.js → chunk-7WAEFADN.js} +4 -4
- package/dist/{chunk-EKT2GUGH.js → chunk-7WGBIPDW.js} +2 -2
- package/dist/{chunk-M6KGRVDD.js → chunk-AX77SAMU.js} +3 -3
- package/dist/{chunk-4HAEQQTQ.js → chunk-BQBBVYYU.js} +4 -4
- package/dist/{chunk-5YD4FDOS.js → chunk-BZLNTDGG.js} +3 -3
- package/dist/{chunk-VWALMO2Z.js → chunk-CSCEGIBZ.js} +3 -3
- package/dist/{chunk-F2Y7KYHZ.js → chunk-EIUQV76I.js} +5 -5
- package/dist/{chunk-MODBIZ4R.js → chunk-GN74L7IW.js} +2 -2
- package/dist/{chunk-5EAVIJTQ.js → chunk-HYCRESCR.js} +2 -2
- package/dist/{chunk-GDARYUPU.js → chunk-K7KD247K.js} +188 -243
- package/dist/{chunk-PZM45AUI.js → chunk-KIUXMPTX.js} +3 -3
- package/dist/{chunk-PYMSCBPA.js → chunk-LAJ4OEME.js} +2 -2
- package/dist/{chunk-YVHV3H5H.js → chunk-MIQBXNSN.js} +4 -4
- package/dist/{chunk-BLKDGMHM.js → chunk-MV6A3QHA.js} +4 -4
- package/dist/{chunk-CFFAWVDL.js → chunk-N3YORLAS.js} +2 -2
- package/dist/{chunk-EU5ZOEUT.js → chunk-NBTEOGQW.js} +2 -2
- package/dist/{chunk-ZXJU6UP4.js → chunk-O3JOUAA5.js} +4 -4
- package/dist/{chunk-EZ5DG73H.js → chunk-PEAIOYXD.js} +4 -4
- package/dist/{chunk-YDS7NZBQ.js → chunk-R5GT4GBL.js} +4 -4
- package/dist/{chunk-6M65VRAT.js → chunk-S7FJTFYR.js} +5 -5
- package/dist/{chunk-DX2RXOQ5.js → chunk-S7RH664J.js} +3 -3
- package/dist/{chunk-WMECC32P.js → chunk-SKF2SKWO.js} +3 -3
- package/dist/{chunk-27HGZPUX.js → chunk-SMKCVFDT.js} +3 -3
- package/dist/{chunk-EID6L4PR.js → chunk-T4Y7NDNJ.js} +2 -2
- package/dist/{chunk-PY33KMCK.js → chunk-TWWJNMTO.js} +2 -2
- package/dist/{chunk-YXPGPWR2.js → chunk-U2PN6QZ2.js} +5 -5
- package/dist/{chunk-3REVOIEW.js → chunk-UBCH575K.js} +5 -5
- package/dist/{chunk-A4NQWDPT.js → chunk-XLURAR5E.js} +3 -3
- package/dist/{chunk-ZZKUI3DP.js → chunk-YPG7LXPN.js} +3 -3
- package/dist/cli/commands/auth/logout.js +10 -10
- package/dist/cli/commands/auth/logout.test.js +11 -11
- package/dist/cli/commands/debug/command-flags.js +9 -9
- package/dist/cli/commands/demo/catalog.js +10 -10
- package/dist/cli/commands/demo/generate-file.js +10 -10
- package/dist/cli/commands/demo/index.js +10 -10
- package/dist/cli/commands/demo/print-ai-prompt.js +10 -10
- package/dist/cli/commands/docs/generate.js +9 -9
- package/dist/cli/commands/docs/generate.test.js +9 -9
- package/dist/cli/commands/help.js +9 -9
- package/dist/cli/commands/kitchen-sink/async.js +10 -10
- package/dist/cli/commands/kitchen-sink/async.test.js +10 -10
- package/dist/cli/commands/kitchen-sink/index.js +12 -12
- package/dist/cli/commands/kitchen-sink/index.test.js +12 -12
- package/dist/cli/commands/kitchen-sink/prompts.js +10 -10
- package/dist/cli/commands/kitchen-sink/prompts.test.js +10 -10
- package/dist/cli/commands/kitchen-sink/static.js +10 -10
- package/dist/cli/commands/kitchen-sink/static.test.js +10 -10
- package/dist/cli/commands/search.js +10 -10
- package/dist/cli/commands/upgrade.js +9 -9
- package/dist/cli/commands/version.js +10 -10
- package/dist/cli/commands/version.test.js +10 -10
- package/dist/cli/services/commands/search.js +2 -2
- package/dist/cli/services/commands/search.test.js +2 -2
- package/dist/cli/services/commands/version.js +4 -4
- package/dist/cli/services/commands/version.test.js +5 -5
- package/dist/cli/services/demo.js +2 -2
- package/dist/cli/services/demo.test.js +2 -2
- package/dist/cli/services/kitchen-sink/async.js +2 -2
- package/dist/cli/services/kitchen-sink/prompts.js +2 -2
- package/dist/cli/services/kitchen-sink/static.js +2 -2
- package/dist/cli/services/upgrade.js +3 -3
- package/dist/cli/services/upgrade.test.js +5 -5
- package/dist/{custom-oclif-loader-JHNX2EGV.js → custom-oclif-loader-BT7EH2NN.js} +3 -3
- package/dist/{error-handler-4UJ6363X.js → error-handler-OSEY6KVA.js} +8 -8
- package/dist/hooks/postrun.js +6 -6
- package/dist/hooks/prerun.js +4 -4
- package/dist/index.js +1333 -1279
- package/dist/{local-V7RONWNU.js → local-OQXN5NM2.js} +2 -2
- package/dist/{morph-DN4AZJZW.js → morph-IQTWRBBT.js} +16 -12
- package/dist/{node-3H4OKRLA.js → node-YQVH3Y7J.js} +13 -13
- package/dist/{node-package-manager-XM7EXHQA.js → node-package-manager-VW2DN7R4.js} +3 -3
- package/dist/{system-F63VIZ5U.js → system-347PZWVP.js} +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/{ui-BXWWRIFS.js → ui-S7L55PBH.js} +2 -2
- package/dist/{workerd-A5NCF6UA.js → workerd-OLKE7G4X.js} +12 -12
- package/oclif.manifest.json +39 -2
- package/package.json +7 -7
- package/dist/assets/hydrogen/starter/app/components/Search.tsx +0 -514
- package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +0 -318
|
@@ -1,18 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {I18nBase} from './mock-i18n-types.js';
|
|
2
2
|
|
|
3
|
-
export
|
|
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<
|
|
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
|
|
3
|
-
|
|
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 {
|
|
1
|
+
import type {I18nBase} from './mock-i18n-types.js';
|
|
2
2
|
|
|
3
|
-
export
|
|
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<
|
|
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 {
|
|
1
|
+
import type {I18nBase} from './mock-i18n-types.js';
|
|
2
2
|
|
|
3
|
-
export
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
} from '~/components/
|
|
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
|
-
<
|
|
77
|
-
{({fetchResults, inputRef}) => (
|
|
78
|
-
|
|
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
|
|
|
88
|
-
<button
|
|
89
|
-
|
|
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
|
-
</
|
|
100
|
-
|
|
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
|
+
→
|
|
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
|
+
}
|