@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,93 @@
|
|
|
1
|
+
import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
import {type Shop} from '@shopify/hydrogen/storefront-api-types';
|
|
4
|
+
|
|
5
|
+
type SelectedPolicies = keyof Pick<
|
|
6
|
+
Shop,
|
|
7
|
+
'privacyPolicy' | 'shippingPolicy' | 'termsOfService' | 'refundPolicy'
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
11
|
+
return [{title: `Hydrogen | ${data?.policy.title ?? ''}`}];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function loader({params, context}: LoaderFunctionArgs) {
|
|
15
|
+
if (!params.handle) {
|
|
16
|
+
throw new Response('No handle was passed in', {status: 404});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const policyName = params.handle.replace(
|
|
20
|
+
/-([a-z])/g,
|
|
21
|
+
(_: unknown, m1: string) => m1.toUpperCase(),
|
|
22
|
+
) as SelectedPolicies;
|
|
23
|
+
|
|
24
|
+
const data = await context.storefront.query(POLICY_CONTENT_QUERY, {
|
|
25
|
+
variables: {
|
|
26
|
+
privacyPolicy: false,
|
|
27
|
+
shippingPolicy: false,
|
|
28
|
+
termsOfService: false,
|
|
29
|
+
refundPolicy: false,
|
|
30
|
+
[policyName]: true,
|
|
31
|
+
language: context.storefront.i18n?.language,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const policy = data.shop?.[policyName];
|
|
36
|
+
|
|
37
|
+
if (!policy) {
|
|
38
|
+
throw new Response('Could not find the policy', {status: 404});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return json({policy});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function Policy() {
|
|
45
|
+
const {policy} = useLoaderData<typeof loader>();
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="policy">
|
|
49
|
+
<br />
|
|
50
|
+
<br />
|
|
51
|
+
<div>
|
|
52
|
+
<Link to="/policies">← Back to Policies</Link>
|
|
53
|
+
</div>
|
|
54
|
+
<br />
|
|
55
|
+
<h1>{policy.title}</h1>
|
|
56
|
+
<div dangerouslySetInnerHTML={{__html: policy.body}} />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// NOTE: https://shopify.dev/docs/api/storefront/latest/objects/Shop
|
|
62
|
+
const POLICY_CONTENT_QUERY = `#graphql
|
|
63
|
+
fragment Policy on ShopPolicy {
|
|
64
|
+
body
|
|
65
|
+
handle
|
|
66
|
+
id
|
|
67
|
+
title
|
|
68
|
+
url
|
|
69
|
+
}
|
|
70
|
+
query Policy(
|
|
71
|
+
$country: CountryCode
|
|
72
|
+
$language: LanguageCode
|
|
73
|
+
$privacyPolicy: Boolean!
|
|
74
|
+
$refundPolicy: Boolean!
|
|
75
|
+
$shippingPolicy: Boolean!
|
|
76
|
+
$termsOfService: Boolean!
|
|
77
|
+
) @inContext(language: $language, country: $country) {
|
|
78
|
+
shop {
|
|
79
|
+
privacyPolicy @include(if: $privacyPolicy) {
|
|
80
|
+
...Policy
|
|
81
|
+
}
|
|
82
|
+
shippingPolicy @include(if: $shippingPolicy) {
|
|
83
|
+
...Policy
|
|
84
|
+
}
|
|
85
|
+
termsOfService @include(if: $termsOfService) {
|
|
86
|
+
...Policy
|
|
87
|
+
}
|
|
88
|
+
refundPolicy @include(if: $refundPolicy) {
|
|
89
|
+
...Policy
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
` as const;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {json, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
import {useLoaderData, Link} from '@remix-run/react';
|
|
3
|
+
|
|
4
|
+
export async function loader({context}: LoaderFunctionArgs) {
|
|
5
|
+
const data = await context.storefront.query(POLICIES_QUERY);
|
|
6
|
+
const policies = Object.values(data.shop || {});
|
|
7
|
+
|
|
8
|
+
if (!policies.length) {
|
|
9
|
+
throw new Response('No policies found', {status: 404});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return json({policies});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function Policies() {
|
|
16
|
+
const {policies} = useLoaderData<typeof loader>();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="policies">
|
|
20
|
+
<h1>Policies</h1>
|
|
21
|
+
<div>
|
|
22
|
+
{policies.map((policy) => {
|
|
23
|
+
if (!policy) return null;
|
|
24
|
+
return (
|
|
25
|
+
<fieldset key={policy.id}>
|
|
26
|
+
<Link to={`/policies/${policy.handle}`}>{policy.title}</Link>
|
|
27
|
+
</fieldset>
|
|
28
|
+
);
|
|
29
|
+
})}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const POLICIES_QUERY = `#graphql
|
|
36
|
+
fragment PolicyItem on ShopPolicy {
|
|
37
|
+
id
|
|
38
|
+
title
|
|
39
|
+
handle
|
|
40
|
+
}
|
|
41
|
+
query Policies ($country: CountryCode, $language: LanguageCode)
|
|
42
|
+
@inContext(country: $country, language: $language) {
|
|
43
|
+
shop {
|
|
44
|
+
privacyPolicy {
|
|
45
|
+
...PolicyItem
|
|
46
|
+
}
|
|
47
|
+
shippingPolicy {
|
|
48
|
+
...PolicyItem
|
|
49
|
+
}
|
|
50
|
+
termsOfService {
|
|
51
|
+
...PolicyItem
|
|
52
|
+
}
|
|
53
|
+
refundPolicy {
|
|
54
|
+
...PolicyItem
|
|
55
|
+
}
|
|
56
|
+
subscriptionPolicy {
|
|
57
|
+
id
|
|
58
|
+
title
|
|
59
|
+
handle
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
` as const;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import {Suspense} from 'react';
|
|
2
|
+
import {defer, redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
3
|
+
import {Await, useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
4
|
+
import type {ProductFragment} from 'storefrontapi.generated';
|
|
5
|
+
import {
|
|
6
|
+
getSelectedProductOptions,
|
|
7
|
+
Analytics,
|
|
8
|
+
useOptimisticVariant,
|
|
9
|
+
} from '@shopify/hydrogen';
|
|
10
|
+
import type {SelectedOption} from '@shopify/hydrogen/storefront-api-types';
|
|
11
|
+
import {getVariantUrl} from '~/lib/variants';
|
|
12
|
+
import {ProductPrice} from '~/components/ProductPrice';
|
|
13
|
+
import {ProductImage} from '~/components/ProductImage';
|
|
14
|
+
import {ProductForm} from '~/components/ProductForm';
|
|
15
|
+
|
|
16
|
+
export const meta: MetaFunction<typeof loader> = ({data}) => {
|
|
17
|
+
return [{title: `Hydrogen | ${data?.product.title ?? ''}`}];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function loader(args: LoaderFunctionArgs) {
|
|
21
|
+
// Start fetching non-critical data without blocking time to first byte
|
|
22
|
+
const deferredData = loadDeferredData(args);
|
|
23
|
+
|
|
24
|
+
// Await the critical data required to render initial state of the page
|
|
25
|
+
const criticalData = await loadCriticalData(args);
|
|
26
|
+
|
|
27
|
+
return defer({...deferredData, ...criticalData});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load data necessary for rendering content above the fold. This is the critical data
|
|
32
|
+
* needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
|
|
33
|
+
*/
|
|
34
|
+
async function loadCriticalData({
|
|
35
|
+
context,
|
|
36
|
+
params,
|
|
37
|
+
request,
|
|
38
|
+
}: LoaderFunctionArgs) {
|
|
39
|
+
const {handle} = params;
|
|
40
|
+
const {storefront} = context;
|
|
41
|
+
|
|
42
|
+
if (!handle) {
|
|
43
|
+
throw new Error('Expected product handle to be defined');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const [{product}] = await Promise.all([
|
|
47
|
+
storefront.query(PRODUCT_QUERY, {
|
|
48
|
+
variables: {handle, selectedOptions: getSelectedProductOptions(request)},
|
|
49
|
+
}),
|
|
50
|
+
// Add other queries here, so that they are loaded in parallel
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
if (!product?.id) {
|
|
54
|
+
throw new Response(null, {status: 404});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const firstVariant = product.variants.nodes[0];
|
|
58
|
+
const firstVariantIsDefault = Boolean(
|
|
59
|
+
firstVariant.selectedOptions.find(
|
|
60
|
+
(option: SelectedOption) =>
|
|
61
|
+
option.name === 'Title' && option.value === 'Default Title',
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (firstVariantIsDefault) {
|
|
66
|
+
product.selectedVariant = firstVariant;
|
|
67
|
+
} else {
|
|
68
|
+
// if no selected variant was returned from the selected options,
|
|
69
|
+
// we redirect to the first variant's url with it's selected options applied
|
|
70
|
+
if (!product.selectedVariant) {
|
|
71
|
+
throw redirectToFirstVariant({product, request});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
product,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load data for rendering content below the fold. This data is deferred and will be
|
|
82
|
+
* fetched after the initial page load. If it's unavailable, the page should still 200.
|
|
83
|
+
* Make sure to not throw any errors here, as it will cause the page to 500.
|
|
84
|
+
*/
|
|
85
|
+
function loadDeferredData({context, params}: LoaderFunctionArgs) {
|
|
86
|
+
// In order to show which variants are available in the UI, we need to query
|
|
87
|
+
// all of them. But there might be a *lot*, so instead separate the variants
|
|
88
|
+
// into it's own separate query that is deferred. So there's a brief moment
|
|
89
|
+
// where variant options might show as available when they're not, but after
|
|
90
|
+
// this deffered query resolves, the UI will update.
|
|
91
|
+
const variants = context.storefront
|
|
92
|
+
.query(VARIANTS_QUERY, {
|
|
93
|
+
variables: {handle: params.handle!},
|
|
94
|
+
})
|
|
95
|
+
.catch((error) => {
|
|
96
|
+
// Log query errors, but don't throw them so the page can still render
|
|
97
|
+
console.error(error);
|
|
98
|
+
return null;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
variants,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function redirectToFirstVariant({
|
|
107
|
+
product,
|
|
108
|
+
request,
|
|
109
|
+
}: {
|
|
110
|
+
product: ProductFragment;
|
|
111
|
+
request: Request;
|
|
112
|
+
}) {
|
|
113
|
+
const url = new URL(request.url);
|
|
114
|
+
const firstVariant = product.variants.nodes[0];
|
|
115
|
+
|
|
116
|
+
return redirect(
|
|
117
|
+
getVariantUrl({
|
|
118
|
+
pathname: url.pathname,
|
|
119
|
+
handle: product.handle,
|
|
120
|
+
selectedOptions: firstVariant.selectedOptions,
|
|
121
|
+
searchParams: new URLSearchParams(url.search),
|
|
122
|
+
}),
|
|
123
|
+
{
|
|
124
|
+
status: 302,
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export default function Product() {
|
|
130
|
+
const {product, variants} = useLoaderData<typeof loader>();
|
|
131
|
+
const selectedVariant = useOptimisticVariant(
|
|
132
|
+
product.selectedVariant,
|
|
133
|
+
variants,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const {title, descriptionHtml} = product;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="product">
|
|
140
|
+
<ProductImage image={selectedVariant?.image} />
|
|
141
|
+
<div className="product-main">
|
|
142
|
+
<h1>{title}</h1>
|
|
143
|
+
<ProductPrice
|
|
144
|
+
price={selectedVariant?.price}
|
|
145
|
+
compareAtPrice={selectedVariant?.compareAtPrice}
|
|
146
|
+
/>
|
|
147
|
+
<br />
|
|
148
|
+
<Suspense
|
|
149
|
+
fallback={
|
|
150
|
+
<ProductForm
|
|
151
|
+
product={product}
|
|
152
|
+
selectedVariant={selectedVariant}
|
|
153
|
+
variants={[]}
|
|
154
|
+
/>
|
|
155
|
+
}
|
|
156
|
+
>
|
|
157
|
+
<Await
|
|
158
|
+
errorElement="There was a problem loading product variants"
|
|
159
|
+
resolve={variants}
|
|
160
|
+
>
|
|
161
|
+
{(data) => (
|
|
162
|
+
<ProductForm
|
|
163
|
+
product={product}
|
|
164
|
+
selectedVariant={selectedVariant}
|
|
165
|
+
variants={data?.product?.variants.nodes || []}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
</Await>
|
|
169
|
+
</Suspense>
|
|
170
|
+
<br />
|
|
171
|
+
<br />
|
|
172
|
+
<p>
|
|
173
|
+
<strong>Description</strong>
|
|
174
|
+
</p>
|
|
175
|
+
<br />
|
|
176
|
+
<div dangerouslySetInnerHTML={{__html: descriptionHtml}} />
|
|
177
|
+
<br />
|
|
178
|
+
</div>
|
|
179
|
+
<Analytics.ProductView
|
|
180
|
+
data={{
|
|
181
|
+
products: [
|
|
182
|
+
{
|
|
183
|
+
id: product.id,
|
|
184
|
+
title: product.title,
|
|
185
|
+
price: selectedVariant?.price.amount || '0',
|
|
186
|
+
vendor: product.vendor,
|
|
187
|
+
variantId: selectedVariant?.id || '',
|
|
188
|
+
variantTitle: selectedVariant?.title || '',
|
|
189
|
+
quantity: 1,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const PRODUCT_VARIANT_FRAGMENT = `#graphql
|
|
199
|
+
fragment ProductVariant on ProductVariant {
|
|
200
|
+
availableForSale
|
|
201
|
+
compareAtPrice {
|
|
202
|
+
amount
|
|
203
|
+
currencyCode
|
|
204
|
+
}
|
|
205
|
+
id
|
|
206
|
+
image {
|
|
207
|
+
__typename
|
|
208
|
+
id
|
|
209
|
+
url
|
|
210
|
+
altText
|
|
211
|
+
width
|
|
212
|
+
height
|
|
213
|
+
}
|
|
214
|
+
price {
|
|
215
|
+
amount
|
|
216
|
+
currencyCode
|
|
217
|
+
}
|
|
218
|
+
product {
|
|
219
|
+
title
|
|
220
|
+
handle
|
|
221
|
+
}
|
|
222
|
+
selectedOptions {
|
|
223
|
+
name
|
|
224
|
+
value
|
|
225
|
+
}
|
|
226
|
+
sku
|
|
227
|
+
title
|
|
228
|
+
unitPrice {
|
|
229
|
+
amount
|
|
230
|
+
currencyCode
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
` as const;
|
|
234
|
+
|
|
235
|
+
const PRODUCT_FRAGMENT = `#graphql
|
|
236
|
+
fragment Product on Product {
|
|
237
|
+
id
|
|
238
|
+
title
|
|
239
|
+
vendor
|
|
240
|
+
handle
|
|
241
|
+
descriptionHtml
|
|
242
|
+
description
|
|
243
|
+
options {
|
|
244
|
+
name
|
|
245
|
+
values
|
|
246
|
+
}
|
|
247
|
+
selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) {
|
|
248
|
+
...ProductVariant
|
|
249
|
+
}
|
|
250
|
+
variants(first: 1) {
|
|
251
|
+
nodes {
|
|
252
|
+
...ProductVariant
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
seo {
|
|
256
|
+
description
|
|
257
|
+
title
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
${PRODUCT_VARIANT_FRAGMENT}
|
|
261
|
+
` as const;
|
|
262
|
+
|
|
263
|
+
const PRODUCT_QUERY = `#graphql
|
|
264
|
+
query Product(
|
|
265
|
+
$country: CountryCode
|
|
266
|
+
$handle: String!
|
|
267
|
+
$language: LanguageCode
|
|
268
|
+
$selectedOptions: [SelectedOptionInput!]!
|
|
269
|
+
) @inContext(country: $country, language: $language) {
|
|
270
|
+
product(handle: $handle) {
|
|
271
|
+
...Product
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
${PRODUCT_FRAGMENT}
|
|
275
|
+
` as const;
|
|
276
|
+
|
|
277
|
+
const PRODUCT_VARIANTS_FRAGMENT = `#graphql
|
|
278
|
+
fragment ProductVariants on Product {
|
|
279
|
+
variants(first: 250) {
|
|
280
|
+
nodes {
|
|
281
|
+
...ProductVariant
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
${PRODUCT_VARIANT_FRAGMENT}
|
|
286
|
+
` as const;
|
|
287
|
+
|
|
288
|
+
const VARIANTS_QUERY = `#graphql
|
|
289
|
+
${PRODUCT_VARIANTS_FRAGMENT}
|
|
290
|
+
query ProductVariants(
|
|
291
|
+
$country: CountryCode
|
|
292
|
+
$language: LanguageCode
|
|
293
|
+
$handle: String!
|
|
294
|
+
) @inContext(country: $country, language: $language) {
|
|
295
|
+
product(handle: $handle) {
|
|
296
|
+
...ProductVariants
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
` as const;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {defer, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
|
|
2
|
+
import {useLoaderData, type MetaFunction} from '@remix-run/react';
|
|
3
|
+
import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
|
|
4
|
+
|
|
5
|
+
import {SearchForm, SearchResults, NoSearchResults} from '~/components/Search';
|
|
6
|
+
|
|
7
|
+
export const meta: MetaFunction = () => {
|
|
8
|
+
return [{title: `Hydrogen | Search`}];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function loader({request, context}: LoaderFunctionArgs) {
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const searchParams = new URLSearchParams(url.search);
|
|
14
|
+
const variables = getPaginationVariables(request, {pageBy: 8});
|
|
15
|
+
const searchTerm = String(searchParams.get('q') || '');
|
|
16
|
+
|
|
17
|
+
if (!searchTerm) {
|
|
18
|
+
return {
|
|
19
|
+
searchResults: {results: null, totalResults: 0},
|
|
20
|
+
searchTerm,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {errors, ...data} = await context.storefront.query(SEARCH_QUERY, {
|
|
25
|
+
variables: {
|
|
26
|
+
query: searchTerm,
|
|
27
|
+
...variables,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!data) {
|
|
32
|
+
throw new Error('No search data returned from Shopify API');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const totalResults = Object.values(data).reduce((total, value) => {
|
|
36
|
+
return total + value.nodes.length;
|
|
37
|
+
}, 0);
|
|
38
|
+
|
|
39
|
+
const searchResults = {
|
|
40
|
+
results: data,
|
|
41
|
+
totalResults,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return defer({
|
|
45
|
+
searchTerm,
|
|
46
|
+
searchResults,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function SearchPage() {
|
|
51
|
+
const {searchTerm, searchResults} = useLoaderData<typeof loader>();
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="search">
|
|
55
|
+
<h1>Search</h1>
|
|
56
|
+
<SearchForm searchTerm={searchTerm} />
|
|
57
|
+
{!searchTerm || !searchResults.totalResults ? (
|
|
58
|
+
<NoSearchResults />
|
|
59
|
+
) : (
|
|
60
|
+
<SearchResults
|
|
61
|
+
results={searchResults.results}
|
|
62
|
+
searchTerm={searchTerm}
|
|
63
|
+
/>
|
|
64
|
+
)}
|
|
65
|
+
<Analytics.SearchView
|
|
66
|
+
data={{searchTerm, searchResults}}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SEARCH_QUERY = `#graphql
|
|
73
|
+
fragment SearchProduct on Product {
|
|
74
|
+
__typename
|
|
75
|
+
handle
|
|
76
|
+
id
|
|
77
|
+
publishedAt
|
|
78
|
+
title
|
|
79
|
+
trackingParameters
|
|
80
|
+
vendor
|
|
81
|
+
variants(first: 1) {
|
|
82
|
+
nodes {
|
|
83
|
+
id
|
|
84
|
+
image {
|
|
85
|
+
url
|
|
86
|
+
altText
|
|
87
|
+
width
|
|
88
|
+
height
|
|
89
|
+
}
|
|
90
|
+
price {
|
|
91
|
+
amount
|
|
92
|
+
currencyCode
|
|
93
|
+
}
|
|
94
|
+
compareAtPrice {
|
|
95
|
+
amount
|
|
96
|
+
currencyCode
|
|
97
|
+
}
|
|
98
|
+
selectedOptions {
|
|
99
|
+
name
|
|
100
|
+
value
|
|
101
|
+
}
|
|
102
|
+
product {
|
|
103
|
+
handle
|
|
104
|
+
title
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
fragment SearchPage on Page {
|
|
110
|
+
__typename
|
|
111
|
+
handle
|
|
112
|
+
id
|
|
113
|
+
title
|
|
114
|
+
trackingParameters
|
|
115
|
+
}
|
|
116
|
+
fragment SearchArticle on Article {
|
|
117
|
+
__typename
|
|
118
|
+
handle
|
|
119
|
+
id
|
|
120
|
+
title
|
|
121
|
+
trackingParameters
|
|
122
|
+
}
|
|
123
|
+
query search(
|
|
124
|
+
$country: CountryCode
|
|
125
|
+
$endCursor: String
|
|
126
|
+
$first: Int
|
|
127
|
+
$language: LanguageCode
|
|
128
|
+
$last: Int
|
|
129
|
+
$query: String!
|
|
130
|
+
$startCursor: String
|
|
131
|
+
) @inContext(country: $country, language: $language) {
|
|
132
|
+
products: search(
|
|
133
|
+
query: $query,
|
|
134
|
+
unavailableProducts: HIDE,
|
|
135
|
+
types: [PRODUCT],
|
|
136
|
+
first: $first,
|
|
137
|
+
sortKey: RELEVANCE,
|
|
138
|
+
last: $last,
|
|
139
|
+
before: $startCursor,
|
|
140
|
+
after: $endCursor
|
|
141
|
+
) {
|
|
142
|
+
nodes {
|
|
143
|
+
...on Product {
|
|
144
|
+
...SearchProduct
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
pageInfo {
|
|
148
|
+
hasNextPage
|
|
149
|
+
hasPreviousPage
|
|
150
|
+
startCursor
|
|
151
|
+
endCursor
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
pages: search(
|
|
155
|
+
query: $query,
|
|
156
|
+
types: [PAGE],
|
|
157
|
+
first: 10
|
|
158
|
+
) {
|
|
159
|
+
nodes {
|
|
160
|
+
...on Page {
|
|
161
|
+
...SearchPage
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
articles: search(
|
|
166
|
+
query: $query,
|
|
167
|
+
types: [ARTICLE],
|
|
168
|
+
first: 10
|
|
169
|
+
) {
|
|
170
|
+
nodes {
|
|
171
|
+
...on Article {
|
|
172
|
+
...SearchArticle
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
` as const;
|