@shopify/create-hydrogen 4.3.13 → 5.0.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/hydrogen/bundle/analyzer.html +2045 -0
- package/dist/assets/hydrogen/i18n/domains.ts +28 -0
- package/dist/assets/hydrogen/i18n/mock-i18n-types.ts +3 -0
- package/dist/assets/hydrogen/i18n/subdomains.ts +27 -0
- package/dist/assets/hydrogen/i18n/subfolders.ts +29 -0
- package/dist/assets/hydrogen/routes/locale-check.ts +16 -0
- package/dist/assets/hydrogen/starter/.eslintignore +5 -0
- package/dist/assets/hydrogen/starter/.eslintrc.cjs +19 -0
- package/dist/assets/hydrogen/starter/.graphqlrc.yml +12 -0
- package/dist/assets/hydrogen/starter/CHANGELOG.md +709 -0
- package/dist/assets/hydrogen/starter/README.md +45 -0
- package/dist/assets/hydrogen/starter/app/assets/favicon.svg +28 -0
- package/dist/assets/hydrogen/starter/app/components/AddToCartButton.tsx +37 -0
- package/dist/assets/hydrogen/starter/app/components/Aside.tsx +76 -0
- package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +150 -0
- package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +68 -0
- package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +101 -0
- package/dist/assets/hydrogen/starter/app/components/Footer.tsx +129 -0
- package/dist/assets/hydrogen/starter/app/components/Header.tsx +230 -0
- package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +126 -0
- package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +80 -0
- package/dist/assets/hydrogen/starter/app/components/ProductImage.tsx +23 -0
- package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +27 -0
- package/dist/assets/hydrogen/starter/app/components/Search.tsx +514 -0
- package/dist/assets/hydrogen/starter/app/entry.client.tsx +12 -0
- package/dist/assets/hydrogen/starter/app/entry.server.tsx +47 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerAddressMutations.ts +61 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerDetailsQuery.ts +40 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrderQuery.ts +87 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerOrdersQuery.ts +58 -0
- package/dist/assets/hydrogen/starter/app/graphql/customer-account/CustomerUpdateMutation.ts +24 -0
- package/dist/assets/hydrogen/starter/app/lib/fragments.ts +174 -0
- package/dist/assets/hydrogen/starter/app/lib/search.ts +29 -0
- package/dist/assets/hydrogen/starter/app/lib/session.ts +72 -0
- package/dist/assets/hydrogen/starter/app/lib/variants.ts +46 -0
- package/dist/assets/hydrogen/starter/app/root.tsx +191 -0
- package/dist/assets/hydrogen/starter/app/routes/$.tsx +11 -0
- package/dist/assets/hydrogen/starter/app/routes/[robots.txt].tsx +118 -0
- package/dist/assets/hydrogen/starter/app/routes/[sitemap.xml].tsx +177 -0
- package/dist/assets/hydrogen/starter/app/routes/_index.tsx +182 -0
- package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +8 -0
- package/dist/assets/hydrogen/starter/app/routes/account._index.tsx +5 -0
- package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +513 -0
- package/dist/assets/hydrogen/starter/app/routes/account.orders.$id.tsx +195 -0
- package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +107 -0
- package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +136 -0
- package/dist/assets/hydrogen/starter/app/routes/account.tsx +88 -0
- package/dist/assets/hydrogen/starter/app/routes/account_.authorize.tsx +5 -0
- package/dist/assets/hydrogen/starter/app/routes/account_.login.tsx +5 -0
- package/dist/assets/hydrogen/starter/app/routes/account_.logout.tsx +10 -0
- package/dist/assets/hydrogen/starter/app/routes/api.predictive-search.tsx +318 -0
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle.$articleHandle.tsx +113 -0
- package/dist/assets/hydrogen/starter/app/routes/blogs.$blogHandle._index.tsx +188 -0
- package/dist/assets/hydrogen/starter/app/routes/blogs._index.tsx +119 -0
- package/dist/assets/hydrogen/starter/app/routes/cart.$lines.tsx +69 -0
- package/dist/assets/hydrogen/starter/app/routes/cart.tsx +102 -0
- package/dist/assets/hydrogen/starter/app/routes/collections.$handle.tsx +225 -0
- package/dist/assets/hydrogen/starter/app/routes/collections._index.tsx +146 -0
- package/dist/assets/hydrogen/starter/app/routes/collections.all.tsx +185 -0
- package/dist/assets/hydrogen/starter/app/routes/discount.$code.tsx +47 -0
- package/dist/assets/hydrogen/starter/app/routes/pages.$handle.tsx +84 -0
- package/dist/assets/hydrogen/starter/app/routes/policies.$handle.tsx +93 -0
- package/dist/assets/hydrogen/starter/app/routes/policies._index.tsx +63 -0
- package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +299 -0
- package/dist/assets/hydrogen/starter/app/routes/search.tsx +177 -0
- package/dist/assets/hydrogen/starter/app/styles/app.css +486 -0
- package/dist/assets/hydrogen/starter/app/styles/reset.css +129 -0
- package/dist/assets/hydrogen/starter/customer-accountapi.generated.d.ts +509 -0
- package/dist/assets/hydrogen/starter/env.d.ts +54 -0
- package/dist/assets/hydrogen/starter/package.json +50 -0
- package/dist/assets/hydrogen/starter/public/.gitkeep +0 -0
- package/dist/assets/hydrogen/starter/server.ts +119 -0
- package/dist/assets/hydrogen/starter/storefrontapi.generated.d.ts +1211 -0
- package/dist/assets/hydrogen/starter/tsconfig.json +23 -0
- package/dist/assets/hydrogen/starter/vite.config.ts +41 -0
- package/dist/assets/hydrogen/tailwind/package.json +8 -0
- package/dist/assets/hydrogen/tailwind/tailwind.css +6 -0
- package/dist/assets/hydrogen/vanilla-extract/package.json +8 -0
- package/dist/assets/hydrogen/virtual-routes/assets/debug-network.css +592 -0
- package/dist/assets/hydrogen/virtual-routes/assets/favicon-dark.svg +20 -0
- package/dist/assets/hydrogen/virtual-routes/assets/favicon.svg +28 -0
- package/dist/assets/hydrogen/virtual-routes/assets/inter-variable-font.woff2 +0 -0
- package/dist/assets/hydrogen/virtual-routes/assets/jetbrainsmono-variable-font.woff2 +0 -0
- package/dist/assets/hydrogen/virtual-routes/assets/styles.css +238 -0
- package/dist/assets/hydrogen/virtual-routes/components/FlameChartWrapper.jsx +123 -0
- package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseBW.jsx +32 -0
- package/dist/assets/hydrogen/virtual-routes/components/HydrogenLogoBaseColor.jsx +47 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconBanner.jsx +292 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconClose.jsx +38 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconDiscard.jsx +44 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconError.jsx +61 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconGithub.jsx +23 -0
- package/dist/assets/hydrogen/virtual-routes/components/IconTwitter.jsx +21 -0
- package/dist/assets/hydrogen/virtual-routes/components/PageLayout.jsx +7 -0
- package/dist/assets/hydrogen/virtual-routes/components/RequestDetails.jsx +178 -0
- package/dist/assets/hydrogen/virtual-routes/components/RequestTable.jsx +91 -0
- package/dist/assets/hydrogen/virtual-routes/components/RequestWaterfall.jsx +151 -0
- package/dist/assets/hydrogen/virtual-routes/lib/useDebugNetworkServer.jsx +178 -0
- package/dist/assets/hydrogen/virtual-routes/routes/graphiql.jsx +5 -0
- package/dist/assets/hydrogen/virtual-routes/routes/index.jsx +265 -0
- package/dist/assets/hydrogen/virtual-routes/routes/subrequest-profiler.jsx +243 -0
- package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +64 -0
- package/dist/assets/hydrogen/vite/package.json +14 -0
- package/dist/assets/hydrogen/vite/vite.config.js +41 -0
- package/dist/chokidar-2CKIHN27.js +12 -0
- package/dist/chunk-EO6F7WJJ.js +2 -0
- package/dist/chunk-FB327AH7.js +5 -0
- package/dist/chunk-FJPX4XUR.js +2 -0
- package/dist/chunk-JKOXGRAA.js +10 -0
- package/dist/chunk-LNQWGFTB.js +45 -0
- package/dist/chunk-M6JXYI3V.js +23 -0
- package/dist/chunk-MNT4XW23.js +2 -0
- package/dist/chunk-N7HFZHSO.js +1145 -0
- package/dist/chunk-PMDMUCNY.js +2 -0
- package/dist/chunk-QGLB6FFL.js +3 -0
- package/dist/chunk-VMIOG46Y.js +2 -0
- package/dist/create-app.js +1867 -34
- package/dist/del-CZGKV5SQ.js +11 -0
- package/dist/devtools-ZCRGQE64.js +8 -0
- package/dist/error-handler-GEQXZJ25.js +2 -0
- package/dist/lib-NJYCLW6W.js +22 -0
- package/dist/morph-ZJCCGFNC.js +30499 -0
- package/dist/multipart-parser-6HGDQWV7.js +3 -0
- package/dist/open-OD6DRFEG.js +2 -0
- package/dist/out-7KAQXZLP.js +2 -0
- package/dist/yoga.wasm +0 -0
- package/package.json +7 -3
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
import {
|
|
4
|
+
Pagination,
|
|
5
|
+
getPaginationVariables,
|
|
6
|
+
Image,
|
|
7
|
+
Money,
|
|
8
|
+
Analytics,
|
|
9
|
+
} from '@shopify/hydrogen';
|
|
10
|
+
import type {ProductItemFragment} from 'storefrontapi.generated';
|
|
11
|
+
import {useVariantUrl} from '~/lib/variants';
|
|
12
|
+
|
|
13
|
+
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
14
|
+
return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function loader(args: LoaderFunctionArgs) {
|
|
18
|
+
// Start fetching non-critical data without blocking time to first byte
|
|
19
|
+
const deferredData = loadDeferredData(args);
|
|
20
|
+
|
|
21
|
+
// Await the critical data required to render initial state of the page
|
|
22
|
+
const criticalData = await loadCriticalData(args);
|
|
23
|
+
|
|
24
|
+
return defer({...deferredData, ...criticalData});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load data necessary for rendering content above the fold. This is the critical data
|
|
29
|
+
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
30
|
+
*/
|
|
31
|
+
async function loadCriticalData({
|
|
32
|
+
context,
|
|
33
|
+
params,
|
|
34
|
+
request,
|
|
35
|
+
}: LoaderFunctionArgs) {
|
|
36
|
+
const {handle} = params;
|
|
37
|
+
const {storefront} = context;
|
|
38
|
+
const paginationVariables = getPaginationVariables(request, {
|
|
39
|
+
pageBy: 8,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!handle) {
|
|
43
|
+
throw redirect('/collections');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const [{collection}] = await Promise.all([
|
|
47
|
+
storefront.query(COLLECTION_QUERY, {
|
|
48
|
+
variables: {handle, ...paginationVariables},
|
|
49
|
+
// Add other queries here, so that they are loaded in parallel
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
if (!collection) {
|
|
54
|
+
throw new Response(`Collection ${handle} not found`, {
|
|
55
|
+
status: 404,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
collection,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Load data for rendering content below the fold. This data is deferred and will be
|
|
66
|
+
* fetched after the initial page load. If it's unavailable, the page should still 200.
|
|
67
|
+
* Make sure to not throw any errors here, as it will cause the page to 500.
|
|
68
|
+
*/
|
|
69
|
+
function loadDeferredData({context}: LoaderFunctionArgs) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function Collection() {
|
|
74
|
+
const {collection} = useLoaderData<typeof loader>();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="collection">
|
|
78
|
+
<h1>{collection.title}</h1>
|
|
79
|
+
<p className="collection-description">{collection.description}</p>
|
|
80
|
+
<Pagination connection={collection.products}>
|
|
81
|
+
{({nodes, isLoading, PreviousLink, NextLink}) => (
|
|
82
|
+
<>
|
|
83
|
+
<PreviousLink>
|
|
84
|
+
{isLoading ? 'Loading...' : <span>↑ Load previous</span>}
|
|
85
|
+
</PreviousLink>
|
|
86
|
+
<ProductsGrid products={nodes} />
|
|
87
|
+
<br />
|
|
88
|
+
<NextLink>
|
|
89
|
+
{isLoading ? 'Loading...' : <span>Load more ↓</span>}
|
|
90
|
+
</NextLink>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
</Pagination>
|
|
94
|
+
<Analytics.CollectionView
|
|
95
|
+
data={{
|
|
96
|
+
collection: {
|
|
97
|
+
id: collection.id,
|
|
98
|
+
handle: collection.handle,
|
|
99
|
+
},
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ProductsGrid({products}: {products: ProductItemFragment[]}) {
|
|
107
|
+
return (
|
|
108
|
+
<div className="products-grid">
|
|
109
|
+
{products.map((product, index) => {
|
|
110
|
+
return (
|
|
111
|
+
<ProductItem
|
|
112
|
+
key={product.id}
|
|
113
|
+
product={product}
|
|
114
|
+
loading={index < 8 ? 'eager' : undefined}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ProductItem({
|
|
123
|
+
product,
|
|
124
|
+
loading,
|
|
125
|
+
}: {
|
|
126
|
+
product: ProductItemFragment;
|
|
127
|
+
loading?: 'eager' | 'lazy';
|
|
128
|
+
}) {
|
|
129
|
+
const variant = product.variants.nodes[0];
|
|
130
|
+
const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
|
|
131
|
+
return (
|
|
132
|
+
<Link
|
|
133
|
+
className="product-item"
|
|
134
|
+
key={product.id}
|
|
135
|
+
prefetch="intent"
|
|
136
|
+
to={variantUrl}
|
|
137
|
+
>
|
|
138
|
+
{product.featuredImage && (
|
|
139
|
+
<Image
|
|
140
|
+
alt={product.featuredImage.altText || product.title}
|
|
141
|
+
aspectRatio="1/1"
|
|
142
|
+
data={product.featuredImage}
|
|
143
|
+
loading={loading}
|
|
144
|
+
sizes="(min-width: 45em) 400px, 100vw"
|
|
145
|
+
/>
|
|
146
|
+
)}
|
|
147
|
+
<h4>{product.title}</h4>
|
|
148
|
+
<small>
|
|
149
|
+
<Money data={product.priceRange.minVariantPrice} />
|
|
150
|
+
</small>
|
|
151
|
+
</Link>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const PRODUCT_ITEM_FRAGMENT = `#graphql
|
|
156
|
+
fragment MoneyProductItem on MoneyV2 {
|
|
157
|
+
amount
|
|
158
|
+
currencyCode
|
|
159
|
+
}
|
|
160
|
+
fragment ProductItem on Product {
|
|
161
|
+
id
|
|
162
|
+
handle
|
|
163
|
+
title
|
|
164
|
+
featuredImage {
|
|
165
|
+
id
|
|
166
|
+
altText
|
|
167
|
+
url
|
|
168
|
+
width
|
|
169
|
+
height
|
|
170
|
+
}
|
|
171
|
+
priceRange {
|
|
172
|
+
minVariantPrice {
|
|
173
|
+
...MoneyProductItem
|
|
174
|
+
}
|
|
175
|
+
maxVariantPrice {
|
|
176
|
+
...MoneyProductItem
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
variants(first: 1) {
|
|
180
|
+
nodes {
|
|
181
|
+
selectedOptions {
|
|
182
|
+
name
|
|
183
|
+
value
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
` as const;
|
|
189
|
+
|
|
190
|
+
// NOTE: https://shopify.dev/docs/api/storefront/2022-04/objects/collection
|
|
191
|
+
const COLLECTION_QUERY = `#graphql
|
|
192
|
+
${PRODUCT_ITEM_FRAGMENT}
|
|
193
|
+
query Collection(
|
|
194
|
+
$handle: String!
|
|
195
|
+
$country: CountryCode
|
|
196
|
+
$language: LanguageCode
|
|
197
|
+
$first: Int
|
|
198
|
+
$last: Int
|
|
199
|
+
$startCursor: String
|
|
200
|
+
$endCursor: String
|
|
201
|
+
) @inContext(country: $country, language: $language) {
|
|
202
|
+
collection(handle: $handle) {
|
|
203
|
+
id
|
|
204
|
+
handle
|
|
205
|
+
title
|
|
206
|
+
description
|
|
207
|
+
products(
|
|
208
|
+
first: $first,
|
|
209
|
+
last: $last,
|
|
210
|
+
before: $startCursor,
|
|
211
|
+
after: $endCursor
|
|
212
|
+
) {
|
|
213
|
+
nodes {
|
|
214
|
+
...ProductItem
|
|
215
|
+
}
|
|
216
|
+
pageInfo {
|
|
217
|
+
hasPreviousPage
|
|
218
|
+
hasNextPage
|
|
219
|
+
endCursor
|
|
220
|
+
startCursor
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
` as const;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {useLoaderData, Link} from '@remix-run/react';
|
|
2
|
+
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
3
|
+
import {Pagination, getPaginationVariables, Image} from '@shopify/hydrogen';
|
|
4
|
+
import type {CollectionFragment} from 'storefrontapi.generated';
|
|
5
|
+
|
|
6
|
+
export async function loader(args: LoaderFunctionArgs) {
|
|
7
|
+
// Start fetching non-critical data without blocking time to first byte
|
|
8
|
+
const deferredData = loadDeferredData(args);
|
|
9
|
+
|
|
10
|
+
// Await the critical data required to render initial state of the page
|
|
11
|
+
const criticalData = await loadCriticalData(args);
|
|
12
|
+
|
|
13
|
+
return defer({...deferredData, ...criticalData});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load data necessary for rendering content above the fold. This is the critical data
|
|
18
|
+
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
19
|
+
*/
|
|
20
|
+
async function loadCriticalData({context, request}: LoaderFunctionArgs) {
|
|
21
|
+
const paginationVariables = getPaginationVariables(request, {
|
|
22
|
+
pageBy: 4,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const [{collections}] = await Promise.all([
|
|
26
|
+
context.storefront.query(COLLECTIONS_QUERY, {
|
|
27
|
+
variables: paginationVariables,
|
|
28
|
+
}),
|
|
29
|
+
// Add other queries here, so that they are loaded in parallel
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
return {collections};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load data for rendering content below the fold. This data is deferred and will be
|
|
37
|
+
* fetched after the initial page load. If it's unavailable, the page should still 200.
|
|
38
|
+
* Make sure to not throw any errors here, as it will cause the page to 500.
|
|
39
|
+
*/
|
|
40
|
+
function loadDeferredData({context}: LoaderFunctionArgs) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function Collections() {
|
|
45
|
+
const {collections} = useLoaderData<typeof loader>();
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="collections">
|
|
49
|
+
<h1>Collections</h1>
|
|
50
|
+
<Pagination connection={collections}>
|
|
51
|
+
{({nodes, isLoading, PreviousLink, NextLink}) => (
|
|
52
|
+
<div>
|
|
53
|
+
<PreviousLink>
|
|
54
|
+
{isLoading ? 'Loading...' : <span>↑ Load previous</span>}
|
|
55
|
+
</PreviousLink>
|
|
56
|
+
<CollectionsGrid collections={nodes} />
|
|
57
|
+
<NextLink>
|
|
58
|
+
{isLoading ? 'Loading...' : <span>Load more ↓</span>}
|
|
59
|
+
</NextLink>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</Pagination>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function CollectionsGrid({collections}: {collections: CollectionFragment[]}) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="collections-grid">
|
|
70
|
+
{collections.map((collection, index) => (
|
|
71
|
+
<CollectionItem
|
|
72
|
+
key={collection.id}
|
|
73
|
+
collection={collection}
|
|
74
|
+
index={index}
|
|
75
|
+
/>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function CollectionItem({
|
|
82
|
+
collection,
|
|
83
|
+
index,
|
|
84
|
+
}: {
|
|
85
|
+
collection: CollectionFragment;
|
|
86
|
+
index: number;
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<Link
|
|
90
|
+
className="collection-item"
|
|
91
|
+
key={collection.id}
|
|
92
|
+
to={`/collections/${collection.handle}`}
|
|
93
|
+
prefetch="intent"
|
|
94
|
+
>
|
|
95
|
+
{collection?.image && (
|
|
96
|
+
<Image
|
|
97
|
+
alt={collection.image.altText || collection.title}
|
|
98
|
+
aspectRatio="1/1"
|
|
99
|
+
data={collection.image}
|
|
100
|
+
loading={index < 3 ? 'eager' : undefined}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
<h5>{collection.title}</h5>
|
|
104
|
+
</Link>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const COLLECTIONS_QUERY = `#graphql
|
|
109
|
+
fragment Collection on Collection {
|
|
110
|
+
id
|
|
111
|
+
title
|
|
112
|
+
handle
|
|
113
|
+
image {
|
|
114
|
+
id
|
|
115
|
+
url
|
|
116
|
+
altText
|
|
117
|
+
width
|
|
118
|
+
height
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
query StoreCollections(
|
|
122
|
+
$country: CountryCode
|
|
123
|
+
$endCursor: String
|
|
124
|
+
$first: Int
|
|
125
|
+
$language: LanguageCode
|
|
126
|
+
$last: Int
|
|
127
|
+
$startCursor: String
|
|
128
|
+
) @inContext(country: $country, language: $language) {
|
|
129
|
+
collections(
|
|
130
|
+
first: $first,
|
|
131
|
+
last: $last,
|
|
132
|
+
before: $startCursor,
|
|
133
|
+
after: $endCursor
|
|
134
|
+
) {
|
|
135
|
+
nodes {
|
|
136
|
+
...Collection
|
|
137
|
+
}
|
|
138
|
+
pageInfo {
|
|
139
|
+
hasNextPage
|
|
140
|
+
hasPreviousPage
|
|
141
|
+
startCursor
|
|
142
|
+
endCursor
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
` as const;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
import {
|
|
4
|
+
Pagination,
|
|
5
|
+
getPaginationVariables,
|
|
6
|
+
Image,
|
|
7
|
+
Money,
|
|
8
|
+
} from '@shopify/hydrogen';
|
|
9
|
+
import type {ProductItemFragment} from 'storefrontapi.generated';
|
|
10
|
+
import {useVariantUrl} from '~/lib/variants';
|
|
11
|
+
|
|
12
|
+
export const meta: MetaFunction<typeof loader> = () => {
|
|
13
|
+
return [{title: `Hydrogen | Products`}];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function loader(args: LoaderFunctionArgs) {
|
|
17
|
+
// Start fetching non-critical data without blocking time to first byte
|
|
18
|
+
const deferredData = loadDeferredData(args);
|
|
19
|
+
|
|
20
|
+
// Await the critical data required to render initial state of the page
|
|
21
|
+
const criticalData = await loadCriticalData(args);
|
|
22
|
+
|
|
23
|
+
return defer({...deferredData, ...criticalData});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load data necessary for rendering content above the fold. This is the critical data
|
|
28
|
+
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
29
|
+
*/
|
|
30
|
+
async function loadCriticalData({context, request}: LoaderFunctionArgs) {
|
|
31
|
+
const {storefront} = context;
|
|
32
|
+
const paginationVariables = getPaginationVariables(request, {
|
|
33
|
+
pageBy: 8,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const [{products}] = await Promise.all([
|
|
37
|
+
storefront.query(CATALOG_QUERY, {
|
|
38
|
+
variables: {...paginationVariables},
|
|
39
|
+
}),
|
|
40
|
+
// Add other queries here, so that they are loaded in parallel
|
|
41
|
+
]);
|
|
42
|
+
return {products};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load data for rendering content below the fold. This data is deferred and will be
|
|
47
|
+
* fetched after the initial page load. If it's unavailable, the page should still 200.
|
|
48
|
+
* Make sure to not throw any errors here, as it will cause the page to 500.
|
|
49
|
+
*/
|
|
50
|
+
function loadDeferredData({context}: LoaderFunctionArgs) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function Collection() {
|
|
55
|
+
const {products} = useLoaderData<typeof loader>();
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="collection">
|
|
59
|
+
<h1>Products</h1>
|
|
60
|
+
<Pagination connection={products}>
|
|
61
|
+
{({nodes, isLoading, PreviousLink, NextLink}) => (
|
|
62
|
+
<>
|
|
63
|
+
<PreviousLink>
|
|
64
|
+
{isLoading ? 'Loading...' : <span>↑ Load previous</span>}
|
|
65
|
+
</PreviousLink>
|
|
66
|
+
<ProductsGrid products={nodes} />
|
|
67
|
+
<br />
|
|
68
|
+
<NextLink>
|
|
69
|
+
{isLoading ? 'Loading...' : <span>Load more ↓</span>}
|
|
70
|
+
</NextLink>
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
</Pagination>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ProductsGrid({products}: {products: ProductItemFragment[]}) {
|
|
79
|
+
return (
|
|
80
|
+
<div className="products-grid">
|
|
81
|
+
{products.map((product, index) => {
|
|
82
|
+
return (
|
|
83
|
+
<ProductItem
|
|
84
|
+
key={product.id}
|
|
85
|
+
product={product}
|
|
86
|
+
loading={index < 8 ? 'eager' : undefined}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ProductItem({
|
|
95
|
+
product,
|
|
96
|
+
loading,
|
|
97
|
+
}: {
|
|
98
|
+
product: ProductItemFragment;
|
|
99
|
+
loading?: 'eager' | 'lazy';
|
|
100
|
+
}) {
|
|
101
|
+
const variant = product.variants.nodes[0];
|
|
102
|
+
const variantUrl = useVariantUrl(product.handle, variant.selectedOptions);
|
|
103
|
+
return (
|
|
104
|
+
<Link
|
|
105
|
+
className="product-item"
|
|
106
|
+
key={product.id}
|
|
107
|
+
prefetch="intent"
|
|
108
|
+
to={variantUrl}
|
|
109
|
+
>
|
|
110
|
+
{product.featuredImage && (
|
|
111
|
+
<Image
|
|
112
|
+
alt={product.featuredImage.altText || product.title}
|
|
113
|
+
aspectRatio="1/1"
|
|
114
|
+
data={product.featuredImage}
|
|
115
|
+
loading={loading}
|
|
116
|
+
sizes="(min-width: 45em) 400px, 100vw"
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
<h4>{product.title}</h4>
|
|
120
|
+
<small>
|
|
121
|
+
<Money data={product.priceRange.minVariantPrice} />
|
|
122
|
+
</small>
|
|
123
|
+
</Link>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const PRODUCT_ITEM_FRAGMENT = `#graphql
|
|
128
|
+
fragment MoneyProductItem on MoneyV2 {
|
|
129
|
+
amount
|
|
130
|
+
currencyCode
|
|
131
|
+
}
|
|
132
|
+
fragment ProductItem on Product {
|
|
133
|
+
id
|
|
134
|
+
handle
|
|
135
|
+
title
|
|
136
|
+
featuredImage {
|
|
137
|
+
id
|
|
138
|
+
altText
|
|
139
|
+
url
|
|
140
|
+
width
|
|
141
|
+
height
|
|
142
|
+
}
|
|
143
|
+
priceRange {
|
|
144
|
+
minVariantPrice {
|
|
145
|
+
...MoneyProductItem
|
|
146
|
+
}
|
|
147
|
+
maxVariantPrice {
|
|
148
|
+
...MoneyProductItem
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
variants(first: 1) {
|
|
152
|
+
nodes {
|
|
153
|
+
selectedOptions {
|
|
154
|
+
name
|
|
155
|
+
value
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
` as const;
|
|
161
|
+
|
|
162
|
+
// NOTE: https://shopify.dev/docs/api/storefront/2024-01/objects/product
|
|
163
|
+
const CATALOG_QUERY = `#graphql
|
|
164
|
+
query Catalog(
|
|
165
|
+
$country: CountryCode
|
|
166
|
+
$language: LanguageCode
|
|
167
|
+
$first: Int
|
|
168
|
+
$last: Int
|
|
169
|
+
$startCursor: String
|
|
170
|
+
$endCursor: String
|
|
171
|
+
) @inContext(country: $country, language: $language) {
|
|
172
|
+
products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
|
|
173
|
+
nodes {
|
|
174
|
+
...ProductItem
|
|
175
|
+
}
|
|
176
|
+
pageInfo {
|
|
177
|
+
hasPreviousPage
|
|
178
|
+
hasNextPage
|
|
179
|
+
startCursor
|
|
180
|
+
endCursor
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
${PRODUCT_ITEM_FRAGMENT}
|
|
185
|
+
` as const;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Automatically applies a discount found on the url
|
|
5
|
+
* If a cart exists it's updated with the discount, otherwise a cart is created with the discount already applied
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* Example path applying a discount and optional redirecting (defaults to the home page)
|
|
9
|
+
* ```js
|
|
10
|
+
* /discount/FREESHIPPING?redirect=/products
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export async function loader({request, context, params}: LoaderFunctionArgs) {
|
|
15
|
+
const {cart} = context;
|
|
16
|
+
const {code} = params;
|
|
17
|
+
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
const searchParams = new URLSearchParams(url.search);
|
|
20
|
+
let redirectParam =
|
|
21
|
+
searchParams.get('redirect') || searchParams.get('return_to') || '/';
|
|
22
|
+
|
|
23
|
+
if (redirectParam.includes('//')) {
|
|
24
|
+
// Avoid redirecting to external URLs to prevent phishing attacks
|
|
25
|
+
redirectParam = '/';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
searchParams.delete('redirect');
|
|
29
|
+
searchParams.delete('return_to');
|
|
30
|
+
|
|
31
|
+
const redirectUrl = `${redirectParam}?${searchParams}`;
|
|
32
|
+
|
|
33
|
+
if (!code) {
|
|
34
|
+
return redirect(redirectUrl);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await cart.updateDiscountCodes([code]);
|
|
38
|
+
const headers = cart.setCartId(result.cart.id);
|
|
39
|
+
|
|
40
|
+
// Using set-cookie on a 303 redirect will not work if the domain origin have port number (:3000)
|
|
41
|
+
// If there is no cart id and a new cart id is created in the progress, it will not be set in the cookie
|
|
42
|
+
// on localhost:3000
|
|
43
|
+
return redirect(redirectUrl, {
|
|
44
|
+
status: 303,
|
|
45
|
+
headers,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
|
|
4
|
+
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
5
|
+
return [{title: `Hydrogen | ${data?.page.title ?? ''}`}];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export async function loader(args: LoaderFunctionArgs) {
|
|
9
|
+
// Start fetching non-critical data without blocking time to first byte
|
|
10
|
+
const deferredData = loadDeferredData(args);
|
|
11
|
+
|
|
12
|
+
// Await the critical data required to render initial state of the page
|
|
13
|
+
const criticalData = await loadCriticalData(args);
|
|
14
|
+
|
|
15
|
+
return defer({...deferredData, ...criticalData});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load data necessary for rendering content above the fold. This is the critical data
|
|
20
|
+
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
21
|
+
*/
|
|
22
|
+
async function loadCriticalData({context, params}: LoaderFunctionArgs) {
|
|
23
|
+
if (!params.handle) {
|
|
24
|
+
throw new Error('Missing page handle');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const [{page}] = await Promise.all([
|
|
28
|
+
context.storefront.query(PAGE_QUERY, {
|
|
29
|
+
variables: {
|
|
30
|
+
handle: params.handle,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
// Add other queries here, so that they are loaded in parallel
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
if (!page) {
|
|
37
|
+
throw new Response('Not Found', {status: 404});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
page,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load data for rendering content below the fold. This data is deferred and will be
|
|
47
|
+
* fetched after the initial page load. If it's unavailable, the page should still 200.
|
|
48
|
+
* Make sure to not throw any errors here, as it will cause the page to 500.
|
|
49
|
+
*/
|
|
50
|
+
function loadDeferredData({context}: LoaderFunctionArgs) {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function Page() {
|
|
55
|
+
const {page} = useLoaderData<typeof loader>();
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="page">
|
|
59
|
+
<header>
|
|
60
|
+
<h1>{page.title}</h1>
|
|
61
|
+
</header>
|
|
62
|
+
<main dangerouslySetInnerHTML={{__html: page.body}} />
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const PAGE_QUERY = `#graphql
|
|
68
|
+
query Page(
|
|
69
|
+
$language: LanguageCode,
|
|
70
|
+
$country: CountryCode,
|
|
71
|
+
$handle: String!
|
|
72
|
+
)
|
|
73
|
+
@inContext(language: $language, country: $country) {
|
|
74
|
+
page(handle: $handle) {
|
|
75
|
+
id
|
|
76
|
+
title
|
|
77
|
+
body
|
|
78
|
+
seo {
|
|
79
|
+
description
|
|
80
|
+
title
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
` as const;
|