@shopify/cli 3.78.1 → 3.79.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/dist/assets/dev-console/extensions/dev-console/assets/index-BteeGmYf.css +1 -0
- package/dist/assets/dev-console/extensions/dev-console/assets/index-CqNhY1h-.js +64 -0
- package/dist/assets/dev-console/index.html +2 -2
- package/dist/assets/hydrogen/starter/.cursor/rules/cookbook-recipe-subscriptions.mdc +921 -0
- package/dist/assets/hydrogen/starter/CHANGELOG.md +20 -0
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +15 -0
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +1 -1
- package/dist/assets/hydrogen/starter/app/components/ProductItem.tsx +44 -0
- package/dist/assets/hydrogen/starter/app/lib/redirect.ts +23 -0
- package/dist/assets/hydrogen/starter/app/routes/_index.tsx +8 -23
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +22 -2
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +4 -0
- package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +7 -41
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +11 -44
- package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +9 -1
- package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +5 -1
- package/dist/assets/hydrogen/starter/app/styles/app.css +15 -3
- package/dist/assets/hydrogen/starter/package.json +5 -5
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +57 -36
- package/dist/{chunk-F3SJ7GH2.js → chunk-2B4E6VSB.js} +3 -3
- package/dist/{chunk-TJUGCRWY.js → chunk-4BO44W6A.js} +2 -2
- package/dist/{chunk-ZJ2IPVO4.js → chunk-54K4AM4U.js} +3 -3
- package/dist/{chunk-HE3BDMSQ.js → chunk-7QKG75SI.js} +73 -7
- package/dist/{chunk-6GQF27II.js → chunk-ALANDDAF.js} +3 -3
- package/dist/{chunk-GA5Q2G7N.js → chunk-ANW4ZUSD.js} +2 -2
- package/dist/{chunk-RH2XRA3A.js → chunk-AVR7SY23.js} +4 -4
- package/dist/{chunk-5DXEBIVF.js → chunk-CZ7EMDNP.js} +5 -5
- package/dist/{chunk-6IPFYNHW.js → chunk-DG3SRCQ7.js} +2 -2
- package/dist/{chunk-QYA3TD2H.js → chunk-DOKAITPJ.js} +4 -4
- package/dist/{chunk-SWU3QODP.js → chunk-EKAPTGJ2.js} +2 -2
- package/dist/{chunk-OAZABGFS.js → chunk-F55GHOR3.js} +83 -130
- package/dist/{chunk-HRT2J6AZ.js → chunk-G63ZK76J.js} +3 -3
- package/dist/{chunk-VE4ESNRH.js → chunk-HTK3YCUG.js} +3 -3
- package/dist/{chunk-F23MB6TB.js → chunk-IARZXI6R.js} +3 -3
- package/dist/{chunk-HGD26EDV.js → chunk-IYBFDT7N.js} +41 -16
- package/dist/{chunk-WWOE2EFZ.js → chunk-JCZQQS22.js} +4 -4
- package/dist/{chunk-TIMWAEJ5.js → chunk-KRTNJAVX.js} +3 -3
- package/dist/{chunk-AV64MUSE.js → chunk-KVPURBXO.js} +5 -5
- package/dist/{chunk-EBRJEUGC.js → chunk-LFQMCRXU.js} +13 -9
- package/dist/{chunk-H73PDGHM.js → chunk-MLGPNWXX.js} +87 -97
- package/dist/{chunk-3COQLGRL.js → chunk-MRU3X272.js} +4 -4
- package/dist/{chunk-BV6PURBM.js → chunk-NOSVFQH5.js} +3 -3
- package/dist/{chunk-GWN4I7WX.js → chunk-OOVDD552.js} +3 -3
- package/dist/{chunk-FHYKRLSA.js → chunk-P6WUJAQG.js} +3 -3
- package/dist/{chunk-FEYZLXHI.js → chunk-PRSCEDOW.js} +18 -5
- package/dist/{chunk-HO5XX2LU.js → chunk-QBB7AFBN.js} +7 -7
- package/dist/{chunk-LE26CN2H.js → chunk-QJTHRGGC.js} +7 -5
- package/dist/{chunk-TFCMG5O2.js → chunk-QW44EO42.js} +8 -8
- package/dist/{chunk-PT5BDLIN.js → chunk-SHIHLMYP.js} +8 -6
- package/dist/{chunk-UGX6DGR4.js → chunk-SUWZRQHU.js} +2 -2
- package/dist/{chunk-DNGCAQBF.js → chunk-U6TDVNHS.js} +16 -15
- package/dist/{chunk-QTRXKFCW.js → chunk-UQLLSBKG.js} +3 -3
- package/dist/{chunk-QAKYKTFV.js → chunk-UTDSSCPI.js} +5 -5
- package/dist/{chunk-O6JLOWUE.js → chunk-YILOICE5.js} +5 -5
- package/dist/{chunk-FZI742QM.js → chunk-ZCIGSW2P.js} +2 -2
- package/dist/cli/commands/auth/logout.js +13 -13
- package/dist/cli/commands/auth/logout.test.js +14 -14
- package/dist/cli/commands/cache/clear.js +12 -12
- package/dist/cli/commands/debug/command-flags.js +12 -12
- package/dist/cli/commands/docs/generate.js +12 -12
- package/dist/cli/commands/docs/generate.test.js +13 -13
- package/dist/cli/commands/help.js +12 -12
- package/dist/cli/commands/kitchen-sink/async.js +13 -13
- package/dist/cli/commands/kitchen-sink/async.test.js +13 -13
- package/dist/cli/commands/kitchen-sink/index.js +15 -15
- package/dist/cli/commands/kitchen-sink/index.test.js +15 -15
- package/dist/cli/commands/kitchen-sink/prompts.js +13 -13
- package/dist/cli/commands/kitchen-sink/prompts.test.js +13 -13
- package/dist/cli/commands/kitchen-sink/static.js +13 -13
- package/dist/cli/commands/kitchen-sink/static.test.js +13 -13
- package/dist/cli/commands/notifications/generate.js +13 -13
- package/dist/cli/commands/notifications/list.js +13 -13
- package/dist/cli/commands/search.js +13 -13
- package/dist/cli/commands/upgrade.js +13 -13
- package/dist/cli/commands/version.js +13 -13
- package/dist/cli/commands/version.test.js +13 -13
- package/dist/cli/services/commands/notifications.js +8 -8
- package/dist/cli/services/commands/search.js +5 -4
- package/dist/cli/services/commands/search.test.js +5 -4
- package/dist/cli/services/commands/version.js +6 -5
- package/dist/cli/services/commands/version.test.js +7 -6
- package/dist/cli/services/kitchen-sink/async.js +5 -4
- package/dist/cli/services/kitchen-sink/prompts.js +5 -4
- package/dist/cli/services/kitchen-sink/static.js +5 -4
- package/dist/cli/services/upgrade.js +5 -5
- package/dist/cli/services/upgrade.test.js +7 -7
- package/dist/configs/all.yml +19 -4
- package/dist/configs/recommended.yml +19 -4
- package/dist/{custom-oclif-loader-BGXT6N23.js → custom-oclif-loader-ZMH2MAPE.js} +5 -4
- package/dist/{error-handler-HVEM4RKZ.js → error-handler-7Z6N6VQJ.js} +10 -10
- package/dist/hooks/postrun.js +9 -9
- package/dist/hooks/prerun.js +10 -10
- package/dist/index.js +2780 -2431
- package/dist/is-wsl-YAJ3DFN7.js +64 -0
- package/dist/{lib-IZAG57CE.js → lib-MWWAATIC.js} +3 -3
- package/dist/{local-UX2FKODS.js → local-LWMJQBFZ.js} +5 -6
- package/dist/{morph-W4XLLZ47.js → morph-MDY3DSJL.js} +9 -9
- package/dist/{node-RJ5OAO2M.js → node-JJXJUMVQ.js} +17 -17
- package/dist/{node-package-manager-NS6MYGUI.js → node-package-manager-BZGMCXZM.js} +5 -5
- package/dist/toml-patch-wasm-J5VGG6WG.js +140 -0
- package/dist/toml_patch_bg.wasm +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/{ui-FQHY2OWD.js → ui-IJDVK4OG.js} +5 -4
- package/dist/{workerd-KUHMQGDX.js → workerd-XA4WKUCO.js} +16 -16
- package/oclif.manifest.json +100 -2
- package/package.json +7 -7
- package/dist/assets/dev-console/extensions/dev-console/assets/index-lyGm6l3x.js +0 -64
- package/dist/assets/dev-console/extensions/dev-console/assets/index-n8yr6cxq.css +0 -1
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# skeleton
|
|
2
2
|
|
|
3
|
+
## 2025.1.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix an issue with our starter template where duplicate content can exist on URLs that use internationalized handles. For example, if you have a product handle in english of `the-havoc` and translate it to `das-chaos` in German, duplicate content exists at both: ([#2821](https://github.com/Shopify/hydrogen/pull/2821)) by [@blittle](https://github.com/blittle)
|
|
8
|
+
|
|
9
|
+
1. https://hydrogen.shop/de-de/products/das-chaos
|
|
10
|
+
2. https://hydrogen.shop/de-de/products/the-havoc
|
|
11
|
+
|
|
12
|
+
We've changed the starter template to make the second redirect to the first.
|
|
13
|
+
|
|
14
|
+
- Added the Cursor rule for the subscriptions recipe. ([#2874](https://github.com/Shopify/hydrogen/pull/2874)) by [@ruggishop](https://github.com/ruggishop)
|
|
15
|
+
|
|
16
|
+
- Fix faulty truthiness check for cart quantity ([#2855](https://github.com/Shopify/hydrogen/pull/2855)) by [@frontsideair](https://github.com/frontsideair)
|
|
17
|
+
|
|
18
|
+
- Refactor ProductItem into a separate component ([#2872](https://github.com/Shopify/hydrogen/pull/2872)) by [@juanpprieto](https://github.com/juanpprieto)
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [[`f80f3bc7`](https://github.com/Shopify/hydrogen/commit/f80f3bc7239b3ee6641cb468a17e15c77bb7815b), [`61ddf924`](https://github.com/Shopify/hydrogen/commit/61ddf92487524b3c04632ae2cfdaa2869a3ae02c), [`642bde4f`](https://github.com/Shopify/hydrogen/commit/642bde4f3df11511e125b013abd977618da25692)]:
|
|
21
|
+
- @shopify/hydrogen@2025.1.4
|
|
22
|
+
|
|
3
23
|
## 2025.1.6
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
|
@@ -123,6 +123,7 @@ function CartLineRemoveButton({
|
|
|
123
123
|
}) {
|
|
124
124
|
return (
|
|
125
125
|
<CartForm
|
|
126
|
+
fetcherKey={getUpdateKey(lineIds)}
|
|
126
127
|
route="/cart"
|
|
127
128
|
action={CartForm.ACTIONS.LinesRemove}
|
|
128
129
|
inputs={{lineIds}}
|
|
@@ -141,8 +142,11 @@ function CartLineUpdateButton({
|
|
|
141
142
|
children: React.ReactNode;
|
|
142
143
|
lines: CartLineUpdateInput[];
|
|
143
144
|
}) {
|
|
145
|
+
const lineIds = lines.map((line) => line.id);
|
|
146
|
+
|
|
144
147
|
return (
|
|
145
148
|
<CartForm
|
|
149
|
+
fetcherKey={getUpdateKey(lineIds)}
|
|
146
150
|
route="/cart"
|
|
147
151
|
action={CartForm.ACTIONS.LinesUpdate}
|
|
148
152
|
inputs={{lines}}
|
|
@@ -151,3 +155,14 @@ function CartLineUpdateButton({
|
|
|
151
155
|
</CartForm>
|
|
152
156
|
);
|
|
153
157
|
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns a unique key for the update action. This is used to make sure actions modifying the same line
|
|
161
|
+
* items are not run concurrently, but cancel each other. For example, if the user clicks "Increase quantity"
|
|
162
|
+
* and "Decrease quantity" in rapid succession, the actions will cancel each other and only the last one will run.
|
|
163
|
+
* @param lineIds - line ids affected by the update
|
|
164
|
+
* @returns
|
|
165
|
+
*/
|
|
166
|
+
function getUpdateKey(lineIds: string[]) {
|
|
167
|
+
return [CartForm.ACTIONS.LinesUpdate, ...lineIds].join('-');
|
|
168
|
+
}
|
|
@@ -26,7 +26,7 @@ export function CartMain({layout, cart: originalCart}: CartMainProps) {
|
|
|
26
26
|
cart &&
|
|
27
27
|
Boolean(cart?.discountCodes?.filter((code) => code.applicable)?.length);
|
|
28
28
|
const className = `cart-main ${withDiscount ? 'with-discount' : ''}`;
|
|
29
|
-
const cartHasItems = cart?.totalQuantity
|
|
29
|
+
const cartHasItems = cart?.totalQuantity ? cart.totalQuantity > 0 : false;
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
32
|
<div className={className}>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {Link} from '@remix-run/react';
|
|
2
|
+
import {Image, Money} from '@shopify/hydrogen';
|
|
3
|
+
import type {
|
|
4
|
+
ProductItemFragment,
|
|
5
|
+
CollectionItemFragment,
|
|
6
|
+
RecommendedProductFragment,
|
|
7
|
+
} from 'storefrontapi.generated';
|
|
8
|
+
import {useVariantUrl} from '~/lib/variants';
|
|
9
|
+
|
|
10
|
+
export function ProductItem({
|
|
11
|
+
product,
|
|
12
|
+
loading,
|
|
13
|
+
}: {
|
|
14
|
+
product:
|
|
15
|
+
| CollectionItemFragment
|
|
16
|
+
| ProductItemFragment
|
|
17
|
+
| RecommendedProductFragment;
|
|
18
|
+
loading?: 'eager' | 'lazy';
|
|
19
|
+
}) {
|
|
20
|
+
const variantUrl = useVariantUrl(product.handle);
|
|
21
|
+
const image = product.featuredImage;
|
|
22
|
+
return (
|
|
23
|
+
<Link
|
|
24
|
+
className="product-item"
|
|
25
|
+
key={product.id}
|
|
26
|
+
prefetch="intent"
|
|
27
|
+
to={variantUrl}
|
|
28
|
+
>
|
|
29
|
+
{image && (
|
|
30
|
+
<Image
|
|
31
|
+
alt={image.altText || product.title}
|
|
32
|
+
aspectRatio="1/1"
|
|
33
|
+
data={image}
|
|
34
|
+
loading={loading}
|
|
35
|
+
sizes="(min-width: 45em) 400px, 100vw"
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
<h4>{product.title}</h4>
|
|
39
|
+
<small>
|
|
40
|
+
<Money data={product.priceRange.minVariantPrice} />
|
|
41
|
+
</small>
|
|
42
|
+
</Link>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {redirect} from '@shopify/remix-oxygen';
|
|
2
|
+
|
|
3
|
+
export function redirectIfHandleIsLocalized(
|
|
4
|
+
request: Request,
|
|
5
|
+
...localizedResources: Array<{
|
|
6
|
+
handle: string;
|
|
7
|
+
data: {handle: string} & unknown;
|
|
8
|
+
}>
|
|
9
|
+
) {
|
|
10
|
+
const url = new URL(request.url);
|
|
11
|
+
let shouldRedirect = false;
|
|
12
|
+
|
|
13
|
+
localizedResources.forEach(({handle, data}) => {
|
|
14
|
+
if (handle !== data.handle) {
|
|
15
|
+
url.pathname = url.pathname.replace(handle, data.handle);
|
|
16
|
+
shouldRedirect = true;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (shouldRedirect) {
|
|
21
|
+
throw redirect(url.toString());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
FeaturedCollectionFragment,
|
|
7
7
|
RecommendedProductsQuery,
|
|
8
8
|
} from 'storefrontapi.generated';
|
|
9
|
+
import {ProductItem} from '~/components/ProductItem';
|
|
9
10
|
|
|
10
11
|
export const meta: MetaFunction = () => {
|
|
11
12
|
return [{title: 'Hydrogen | Home'}];
|
|
@@ -101,21 +102,7 @@ function RecommendedProducts({
|
|
|
101
102
|
<div className="recommended-products-grid">
|
|
102
103
|
{response
|
|
103
104
|
? response.products.nodes.map((product) => (
|
|
104
|
-
<
|
|
105
|
-
key={product.id}
|
|
106
|
-
className="recommended-product"
|
|
107
|
-
to={`/products/${product.handle}`}
|
|
108
|
-
>
|
|
109
|
-
<Image
|
|
110
|
-
data={product.images.nodes[0]}
|
|
111
|
-
aspectRatio="1/1"
|
|
112
|
-
sizes="(min-width: 45em) 20vw, 50vw"
|
|
113
|
-
/>
|
|
114
|
-
<h4>{product.title}</h4>
|
|
115
|
-
<small>
|
|
116
|
-
<Money data={product.priceRange.minVariantPrice} />
|
|
117
|
-
</small>
|
|
118
|
-
</Link>
|
|
105
|
+
<ProductItem key={product.id} product={product} />
|
|
119
106
|
))
|
|
120
107
|
: null}
|
|
121
108
|
</div>
|
|
@@ -161,14 +148,12 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
|
|
|
161
148
|
currencyCode
|
|
162
149
|
}
|
|
163
150
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
height
|
|
171
|
-
}
|
|
151
|
+
featuredImage {
|
|
152
|
+
id
|
|
153
|
+
url
|
|
154
|
+
altText
|
|
155
|
+
width
|
|
156
|
+
height
|
|
172
157
|
}
|
|
173
158
|
}
|
|
174
159
|
query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
2
|
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
3
|
import {Image} from '@shopify/hydrogen';
|
|
4
|
+
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
|
|
4
5
|
|
|
5
6
|
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
6
7
|
return [{title: `Hydrogen | ${data?.article.title ?? ''} article`}];
|
|
@@ -20,7 +21,11 @@ export async function loader(args: LoaderFunctionArgs) {
|
|
|
20
21
|
* Load data necessary for rendering content above the fold. This is the critical data
|
|
21
22
|
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
22
23
|
*/
|
|
23
|
-
async function loadCriticalData({
|
|
24
|
+
async function loadCriticalData({
|
|
25
|
+
context,
|
|
26
|
+
request,
|
|
27
|
+
params,
|
|
28
|
+
}: LoaderFunctionArgs) {
|
|
24
29
|
const {blogHandle, articleHandle} = params;
|
|
25
30
|
|
|
26
31
|
if (!articleHandle || !blogHandle) {
|
|
@@ -38,6 +43,18 @@ async function loadCriticalData({context, params}: LoaderFunctionArgs) {
|
|
|
38
43
|
throw new Response(null, {status: 404});
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
redirectIfHandleIsLocalized(
|
|
47
|
+
request,
|
|
48
|
+
{
|
|
49
|
+
handle: articleHandle,
|
|
50
|
+
data: blog.articleByHandle,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
handle: blogHandle,
|
|
54
|
+
data: blog,
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
41
58
|
const article = blog.articleByHandle;
|
|
42
59
|
|
|
43
60
|
return {article};
|
|
@@ -67,7 +84,8 @@ export default function Article() {
|
|
|
67
84
|
<h1>
|
|
68
85
|
{title}
|
|
69
86
|
<div>
|
|
70
|
-
{publishedDate} ·
|
|
87
|
+
<time dateTime={article.publishedAt}>{publishedDate}</time> ·{' '}
|
|
88
|
+
<address>{author?.name}</address>
|
|
71
89
|
</div>
|
|
72
90
|
</h1>
|
|
73
91
|
|
|
@@ -89,7 +107,9 @@ const ARTICLE_QUERY = `#graphql
|
|
|
89
107
|
$language: LanguageCode
|
|
90
108
|
) @inContext(language: $language, country: $country) {
|
|
91
109
|
blog(handle: $blogHandle) {
|
|
110
|
+
handle
|
|
92
111
|
articleByHandle(handle: $articleHandle) {
|
|
112
|
+
handle
|
|
93
113
|
title
|
|
94
114
|
contentHtml
|
|
95
115
|
publishedAt
|
|
@@ -3,6 +3,7 @@ import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
|
3
3
|
import {Image, getPaginationVariables} from '@shopify/hydrogen';
|
|
4
4
|
import type {ArticleItemFragment} from 'storefrontapi.generated';
|
|
5
5
|
import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
|
|
6
|
+
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
|
|
6
7
|
|
|
7
8
|
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
8
9
|
return [{title: `Hydrogen | ${data?.blog.title ?? ''} blog`}];
|
|
@@ -49,6 +50,8 @@ async function loadCriticalData({
|
|
|
49
50
|
throw new Response('Not found', {status: 404});
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
redirectIfHandleIsLocalized(request, {handle: params.blogHandle, data: blog});
|
|
54
|
+
|
|
52
55
|
return {blog};
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -128,6 +131,7 @@ const BLOGS_QUERY = `#graphql
|
|
|
128
131
|
) @inContext(language: $language) {
|
|
129
132
|
blog(handle: $blogHandle) {
|
|
130
133
|
title
|
|
134
|
+
handle
|
|
131
135
|
seo {
|
|
132
136
|
title
|
|
133
137
|
description
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
-
import {useLoaderData,
|
|
3
|
-
import {
|
|
4
|
-
getPaginationVariables,
|
|
5
|
-
Image,
|
|
6
|
-
Money,
|
|
7
|
-
Analytics,
|
|
8
|
-
} from '@shopify/hydrogen';
|
|
9
|
-
import type {ProductItemFragment} from 'storefrontapi.generated';
|
|
10
|
-
import {useVariantUrl} from '~/lib/variants';
|
|
2
|
+
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
|
|
11
4
|
import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
|
|
5
|
+
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
|
|
6
|
+
import {ProductItem} from '~/components/ProductItem';
|
|
12
7
|
|
|
13
8
|
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
14
9
|
return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}];
|
|
@@ -56,6 +51,9 @@ async function loadCriticalData({
|
|
|
56
51
|
});
|
|
57
52
|
}
|
|
58
53
|
|
|
54
|
+
// The API handle might be localized, so redirect to the localized handle
|
|
55
|
+
redirectIfHandleIsLocalized(request, {handle, data: collection});
|
|
56
|
+
|
|
59
57
|
return {
|
|
60
58
|
collection,
|
|
61
59
|
};
|
|
@@ -101,38 +99,6 @@ export default function Collection() {
|
|
|
101
99
|
);
|
|
102
100
|
}
|
|
103
101
|
|
|
104
|
-
function ProductItem({
|
|
105
|
-
product,
|
|
106
|
-
loading,
|
|
107
|
-
}: {
|
|
108
|
-
product: ProductItemFragment;
|
|
109
|
-
loading?: 'eager' | 'lazy';
|
|
110
|
-
}) {
|
|
111
|
-
const variantUrl = useVariantUrl(product.handle);
|
|
112
|
-
return (
|
|
113
|
-
<Link
|
|
114
|
-
className="product-item"
|
|
115
|
-
key={product.id}
|
|
116
|
-
prefetch="intent"
|
|
117
|
-
to={variantUrl}
|
|
118
|
-
>
|
|
119
|
-
{product.featuredImage && (
|
|
120
|
-
<Image
|
|
121
|
-
alt={product.featuredImage.altText || product.title}
|
|
122
|
-
aspectRatio="1/1"
|
|
123
|
-
data={product.featuredImage}
|
|
124
|
-
loading={loading}
|
|
125
|
-
sizes="(min-width: 45em) 400px, 100vw"
|
|
126
|
-
/>
|
|
127
|
-
)}
|
|
128
|
-
<h4>{product.title}</h4>
|
|
129
|
-
<small>
|
|
130
|
-
<Money data={product.priceRange.minVariantPrice} />
|
|
131
|
-
</small>
|
|
132
|
-
</Link>
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
102
|
const PRODUCT_ITEM_FRAGMENT = `#graphql
|
|
137
103
|
fragment MoneyProductItem on MoneyV2 {
|
|
138
104
|
amount
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
-
import {useLoaderData,
|
|
3
|
-
import {getPaginationVariables
|
|
4
|
-
import type {ProductItemFragment} from 'storefrontapi.generated';
|
|
5
|
-
import {useVariantUrl} from '~/lib/variants';
|
|
2
|
+
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
import {getPaginationVariables} from '@shopify/hydrogen';
|
|
6
4
|
import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
|
|
5
|
+
import {ProductItem} from '~/components/ProductItem';
|
|
7
6
|
|
|
8
7
|
export const meta: MetaFunction<typeof loader> = () => {
|
|
9
8
|
return [{title: `Hydrogen | Products`}];
|
|
@@ -69,44 +68,12 @@ export default function Collection() {
|
|
|
69
68
|
);
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
loading,
|
|
75
|
-
}: {
|
|
76
|
-
product: ProductItemFragment;
|
|
77
|
-
loading?: 'eager' | 'lazy';
|
|
78
|
-
}) {
|
|
79
|
-
const variantUrl = useVariantUrl(product.handle);
|
|
80
|
-
return (
|
|
81
|
-
<Link
|
|
82
|
-
className="product-item"
|
|
83
|
-
key={product.id}
|
|
84
|
-
prefetch="intent"
|
|
85
|
-
to={variantUrl}
|
|
86
|
-
>
|
|
87
|
-
{product.featuredImage && (
|
|
88
|
-
<Image
|
|
89
|
-
alt={product.featuredImage.altText || product.title}
|
|
90
|
-
aspectRatio="1/1"
|
|
91
|
-
data={product.featuredImage}
|
|
92
|
-
loading={loading}
|
|
93
|
-
sizes="(min-width: 45em) 400px, 100vw"
|
|
94
|
-
/>
|
|
95
|
-
)}
|
|
96
|
-
<h4>{product.title}</h4>
|
|
97
|
-
<small>
|
|
98
|
-
<Money data={product.priceRange.minVariantPrice} />
|
|
99
|
-
</small>
|
|
100
|
-
</Link>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const PRODUCT_ITEM_FRAGMENT = `#graphql
|
|
105
|
-
fragment MoneyProductItem on MoneyV2 {
|
|
71
|
+
const COLLECTION_ITEM_FRAGMENT = `#graphql
|
|
72
|
+
fragment MoneyCollectionItem on MoneyV2 {
|
|
106
73
|
amount
|
|
107
74
|
currencyCode
|
|
108
75
|
}
|
|
109
|
-
fragment
|
|
76
|
+
fragment CollectionItem on Product {
|
|
110
77
|
id
|
|
111
78
|
handle
|
|
112
79
|
title
|
|
@@ -119,16 +86,16 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
|
|
|
119
86
|
}
|
|
120
87
|
priceRange {
|
|
121
88
|
minVariantPrice {
|
|
122
|
-
...
|
|
89
|
+
...MoneyCollectionItem
|
|
123
90
|
}
|
|
124
91
|
maxVariantPrice {
|
|
125
|
-
...
|
|
92
|
+
...MoneyCollectionItem
|
|
126
93
|
}
|
|
127
94
|
}
|
|
128
95
|
}
|
|
129
96
|
` as const;
|
|
130
97
|
|
|
131
|
-
// NOTE: https://shopify.dev/docs/api/storefront/
|
|
98
|
+
// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/product
|
|
132
99
|
const CATALOG_QUERY = `#graphql
|
|
133
100
|
query Catalog(
|
|
134
101
|
$country: CountryCode
|
|
@@ -140,7 +107,7 @@ const CATALOG_QUERY = `#graphql
|
|
|
140
107
|
) @inContext(country: $country, language: $language) {
|
|
141
108
|
products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
|
|
142
109
|
nodes {
|
|
143
|
-
...
|
|
110
|
+
...CollectionItem
|
|
144
111
|
}
|
|
145
112
|
pageInfo {
|
|
146
113
|
hasPreviousPage
|
|
@@ -150,5 +117,5 @@ const CATALOG_QUERY = `#graphql
|
|
|
150
117
|
}
|
|
151
118
|
}
|
|
152
119
|
}
|
|
153
|
-
${
|
|
120
|
+
${COLLECTION_ITEM_FRAGMENT}
|
|
154
121
|
` as const;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
2
|
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
|
|
3
4
|
|
|
4
5
|
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
5
6
|
return [{title: `Hydrogen | ${data?.page.title ?? ''}`}];
|
|
@@ -19,7 +20,11 @@ export async function loader(args: LoaderFunctionArgs) {
|
|
|
19
20
|
* Load data necessary for rendering content above the fold. This is the critical data
|
|
20
21
|
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
21
22
|
*/
|
|
22
|
-
async function loadCriticalData({
|
|
23
|
+
async function loadCriticalData({
|
|
24
|
+
context,
|
|
25
|
+
request,
|
|
26
|
+
params,
|
|
27
|
+
}: LoaderFunctionArgs) {
|
|
23
28
|
if (!params.handle) {
|
|
24
29
|
throw new Error('Missing page handle');
|
|
25
30
|
}
|
|
@@ -37,6 +42,8 @@ async function loadCriticalData({context, params}: LoaderFunctionArgs) {
|
|
|
37
42
|
throw new Response('Not Found', {status: 404});
|
|
38
43
|
}
|
|
39
44
|
|
|
45
|
+
redirectIfHandleIsLocalized(request, {handle: params.handle, data: page});
|
|
46
|
+
|
|
40
47
|
return {
|
|
41
48
|
page,
|
|
42
49
|
};
|
|
@@ -72,6 +79,7 @@ const PAGE_QUERY = `#graphql
|
|
|
72
79
|
)
|
|
73
80
|
@inContext(language: $language, country: $country) {
|
|
74
81
|
page(handle: $handle) {
|
|
82
|
+
handle
|
|
75
83
|
id
|
|
76
84
|
title
|
|
77
85
|
body
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
1
|
+
import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
2
|
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
3
|
import {
|
|
4
4
|
getSelectedProductOptions,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import {ProductPrice} from '~/components/ProductPrice';
|
|
12
12
|
import {ProductImage} from '~/components/ProductImage';
|
|
13
13
|
import {ProductForm} from '~/components/ProductForm';
|
|
14
|
+
import {redirectIfHandleIsLocalized} from '~/lib/redirect';
|
|
14
15
|
|
|
15
16
|
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
16
17
|
return [
|
|
@@ -59,6 +60,9 @@ async function loadCriticalData({
|
|
|
59
60
|
throw new Response(null, {status: 404});
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// The API handle might be localized, so redirect to the localized handle
|
|
64
|
+
redirectIfHandleIsLocalized(request, {handle, data: product});
|
|
65
|
+
|
|
62
66
|
return {
|
|
63
67
|
product,
|
|
64
68
|
};
|
|
@@ -27,12 +27,17 @@ img {
|
|
|
27
27
|
* components/Aside
|
|
28
28
|
* --------------------------------------------------
|
|
29
29
|
*/
|
|
30
|
+
@media (max-width: 45em) {
|
|
31
|
+
html:has(.overlay.expanded) {
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
aside {
|
|
31
37
|
background: var(--color-light);
|
|
32
38
|
box-shadow: 0 0 50px rgba(0, 0, 0, 0.3);
|
|
33
39
|
height: 100vh;
|
|
34
|
-
|
|
35
|
-
min-width: var(--aside-width);
|
|
40
|
+
width: min(var(--aside-width), 100vw);
|
|
36
41
|
position: fixed;
|
|
37
42
|
right: calc(-1 * var(--aside-width));
|
|
38
43
|
top: 0;
|
|
@@ -201,6 +206,11 @@ button.reset:hover:not(:has(> *)) {
|
|
|
201
206
|
margin-left: auto;
|
|
202
207
|
}
|
|
203
208
|
|
|
209
|
+
.header-ctas > * {
|
|
210
|
+
min-width: fit-content;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
204
214
|
/*
|
|
205
215
|
* --------------------------------------------------
|
|
206
216
|
* components/Footer
|
|
@@ -212,14 +222,16 @@ button.reset:hover:not(:has(> *)) {
|
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
.footer-menu {
|
|
215
|
-
|
|
225
|
+
justify-content: center;
|
|
216
226
|
display: flex;
|
|
227
|
+
flex-wrap: wrap;
|
|
217
228
|
grid-gap: 1rem;
|
|
218
229
|
padding: 1rem;
|
|
219
230
|
}
|
|
220
231
|
|
|
221
232
|
.footer-menu a {
|
|
222
233
|
color: var(--color-light);
|
|
234
|
+
min-width: fit-content;
|
|
223
235
|
}
|
|
224
236
|
|
|
225
237
|
/*
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "skeleton",
|
|
3
3
|
"private": true,
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "2025.1.
|
|
5
|
+
"version": "2025.1.7",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "shopify hydrogen build --codegen",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@remix-run/react": "^2.16.1",
|
|
18
18
|
"@remix-run/server-runtime": "^2.16.1",
|
|
19
19
|
"graphql": "^16.10.0",
|
|
20
|
-
"@shopify/hydrogen": "2025.1.
|
|
20
|
+
"@shopify/hydrogen": "2025.1.4",
|
|
21
21
|
"@shopify/remix-oxygen": "^2.0.12",
|
|
22
22
|
"graphql-tag": "^2.12.6",
|
|
23
23
|
"isbot": "^5.1.22",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"@remix-run/dev": "^2.16.1",
|
|
33
33
|
"@remix-run/fs-routes": "^2.16.1",
|
|
34
34
|
"@remix-run/route-config": "^2.16.1",
|
|
35
|
-
"@shopify/cli": "~3.
|
|
35
|
+
"@shopify/cli": "~3.78.1",
|
|
36
36
|
"@shopify/hydrogen-codegen": "^0.3.3",
|
|
37
|
-
"@shopify/mini-oxygen": "^3.2.
|
|
37
|
+
"@shopify/mini-oxygen": "^3.2.1",
|
|
38
38
|
"@shopify/oxygen-workers-types": "^4.1.6",
|
|
39
39
|
"@shopify/prettier-config": "^1.1.2",
|
|
40
40
|
"@total-typescript/ts-reset": "^0.6.1",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"eslint-plugin-react-hooks": "^5.1.0",
|
|
56
56
|
"globals": "^15.14.0",
|
|
57
57
|
"typescript": "^5.2.2",
|
|
58
|
-
"vite": "^6.2.
|
|
58
|
+
"vite": "^6.2.4",
|
|
59
59
|
"vite-tsconfig-paths": "^4.3.1"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|