@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,45 @@
|
|
|
1
|
+
# Hydrogen template: Skeleton
|
|
2
|
+
|
|
3
|
+
Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen.
|
|
4
|
+
|
|
5
|
+
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen)
|
|
6
|
+
[Get familiar with Remix](https://remix.run/docs/en/v1)
|
|
7
|
+
|
|
8
|
+
## What's included
|
|
9
|
+
|
|
10
|
+
- Remix
|
|
11
|
+
- Hydrogen
|
|
12
|
+
- Oxygen
|
|
13
|
+
- Vite
|
|
14
|
+
- Shopify CLI
|
|
15
|
+
- ESLint
|
|
16
|
+
- Prettier
|
|
17
|
+
- GraphQL generator
|
|
18
|
+
- TypeScript and JavaScript flavors
|
|
19
|
+
- Minimal setup of components and routes
|
|
20
|
+
|
|
21
|
+
## Getting started
|
|
22
|
+
|
|
23
|
+
**Requirements:**
|
|
24
|
+
|
|
25
|
+
- Node.js version 18.0.0 or higher
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm create @shopify/hydrogen@latest
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Building for production
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Local development
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm run dev
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Setup for using Customer Account API (`/account` section)
|
|
44
|
+
|
|
45
|
+
Follow step 1 and 2 of <https://shopify.dev/docs/custom-storefronts/building-with-the-customer-account-api/hydrogen#step-1-set-up-a-public-domain-for-local-development>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
|
|
2
|
+
<style>
|
|
3
|
+
.stroke {
|
|
4
|
+
stroke: #000;
|
|
5
|
+
}
|
|
6
|
+
.fill {
|
|
7
|
+
fill: #000;
|
|
8
|
+
}
|
|
9
|
+
@media (prefers-color-scheme: dark) {
|
|
10
|
+
.stroke {
|
|
11
|
+
stroke: #fff;
|
|
12
|
+
}
|
|
13
|
+
.fill {
|
|
14
|
+
fill: #fff;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
<path
|
|
19
|
+
class="stroke"
|
|
20
|
+
fill-rule="evenodd"
|
|
21
|
+
d="M16.1 16.04 1 8.02 6.16 5.3l5.82 3.09 4.88-2.57-5.82-3.1L16.21 0l15.1 8.02-5.17 2.72-5.5-2.91-4.88 2.57 5.5 2.92-5.16 2.72Z"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
class="fill"
|
|
25
|
+
fill-rule="evenodd"
|
|
26
|
+
d="M16.1 32 1 23.98l5.16-2.72 5.82 3.08 4.88-2.57-5.82-3.08 5.17-2.73 15.1 8.02-5.17 2.72-5.5-2.92-4.88 2.58 5.5 2.92L16.1 32Z"
|
|
27
|
+
/>
|
|
28
|
+
</svg>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {type FetcherWithComponents} from '@remix-run/react';
|
|
2
|
+
import {CartForm, type OptimisticCartLineInput} from '@shopify/hydrogen';
|
|
3
|
+
|
|
4
|
+
export function AddToCartButton({
|
|
5
|
+
analytics,
|
|
6
|
+
children,
|
|
7
|
+
disabled,
|
|
8
|
+
lines,
|
|
9
|
+
onClick,
|
|
10
|
+
}: {
|
|
11
|
+
analytics?: unknown;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
lines: Array<OptimisticCartLineInput>;
|
|
15
|
+
onClick?: () => void;
|
|
16
|
+
}) {
|
|
17
|
+
return (
|
|
18
|
+
<CartForm route="/cart" inputs={{lines}} action={CartForm.ACTIONS.LinesAdd}>
|
|
19
|
+
{(fetcher: FetcherWithComponents<any>) => (
|
|
20
|
+
<>
|
|
21
|
+
<input
|
|
22
|
+
name="analytics"
|
|
23
|
+
type="hidden"
|
|
24
|
+
value={JSON.stringify(analytics)}
|
|
25
|
+
/>
|
|
26
|
+
<button
|
|
27
|
+
type="submit"
|
|
28
|
+
onClick={onClick}
|
|
29
|
+
disabled={disabled ?? fetcher.state !== 'idle'}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
</button>
|
|
33
|
+
</>
|
|
34
|
+
)}
|
|
35
|
+
</CartForm>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {createContext, type ReactNode, useContext, useState} from 'react';
|
|
2
|
+
|
|
3
|
+
type AsideType = 'search' | 'cart' | 'mobile' | 'closed';
|
|
4
|
+
type AsideContextValue = {
|
|
5
|
+
type: AsideType;
|
|
6
|
+
open: (mode: AsideType) => void;
|
|
7
|
+
close: () => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A side bar component with Overlay
|
|
12
|
+
* @example
|
|
13
|
+
* ```jsx
|
|
14
|
+
* <Aside type="search" heading="SEARCH">
|
|
15
|
+
* <input type="search" />
|
|
16
|
+
* ...
|
|
17
|
+
* </Aside>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function Aside({
|
|
21
|
+
children,
|
|
22
|
+
heading,
|
|
23
|
+
type,
|
|
24
|
+
}: {
|
|
25
|
+
children?: React.ReactNode;
|
|
26
|
+
type: AsideType;
|
|
27
|
+
heading: React.ReactNode;
|
|
28
|
+
}) {
|
|
29
|
+
const {type: activeType, close} = useAside();
|
|
30
|
+
const expanded = type === activeType;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
aria-modal
|
|
35
|
+
className={`overlay ${expanded ? 'expanded' : ''}`}
|
|
36
|
+
role="dialog"
|
|
37
|
+
>
|
|
38
|
+
<button className="close-outside" onClick={close} />
|
|
39
|
+
<aside>
|
|
40
|
+
<header>
|
|
41
|
+
<h3>{heading}</h3>
|
|
42
|
+
<button className="close reset" onClick={close}>
|
|
43
|
+
×
|
|
44
|
+
</button>
|
|
45
|
+
</header>
|
|
46
|
+
<main>{children}</main>
|
|
47
|
+
</aside>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const AsideContext = createContext<AsideContextValue | null>(null);
|
|
53
|
+
|
|
54
|
+
Aside.Provider = function AsideProvider({children}: {children: ReactNode}) {
|
|
55
|
+
const [type, setType] = useState<AsideType>('closed');
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<AsideContext.Provider
|
|
59
|
+
value={{
|
|
60
|
+
type,
|
|
61
|
+
open: setType,
|
|
62
|
+
close: () => setType('closed'),
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</AsideContext.Provider>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function useAside() {
|
|
71
|
+
const aside = useContext(AsideContext);
|
|
72
|
+
if (!aside) {
|
|
73
|
+
throw new Error('useAside must be used within an AsideProvider');
|
|
74
|
+
}
|
|
75
|
+
return aside;
|
|
76
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {CartLineUpdateInput} from '@shopify/hydrogen/storefront-api-types';
|
|
2
|
+
import type {CartLayout} from '~/components/CartMain';
|
|
3
|
+
import {CartForm, Image, type OptimisticCartLine} from '@shopify/hydrogen';
|
|
4
|
+
import {useVariantUrl} from '~/lib/variants';
|
|
5
|
+
import {Link} from '@remix-run/react';
|
|
6
|
+
import {ProductPrice} from './ProductPrice';
|
|
7
|
+
import {useAside} from './Aside';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A single line item in the cart. It displays the product image, title, price.
|
|
11
|
+
* It also provides controls to update the quantity or remove the line item.
|
|
12
|
+
*/
|
|
13
|
+
export function CartLineItem({
|
|
14
|
+
layout,
|
|
15
|
+
line,
|
|
16
|
+
}: {
|
|
17
|
+
layout: CartLayout;
|
|
18
|
+
line: OptimisticCartLine;
|
|
19
|
+
}) {
|
|
20
|
+
const {id, merchandise} = line;
|
|
21
|
+
const {product, title, image, selectedOptions} = merchandise;
|
|
22
|
+
const lineItemUrl = useVariantUrl(product.handle, selectedOptions);
|
|
23
|
+
const {close} = useAside();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<li key={id} className="cart-line">
|
|
27
|
+
{image && (
|
|
28
|
+
<Image
|
|
29
|
+
alt={title}
|
|
30
|
+
aspectRatio="1/1"
|
|
31
|
+
data={image}
|
|
32
|
+
height={100}
|
|
33
|
+
loading="lazy"
|
|
34
|
+
width={100}
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
<div>
|
|
39
|
+
<Link
|
|
40
|
+
prefetch="intent"
|
|
41
|
+
to={lineItemUrl}
|
|
42
|
+
onClick={() => {
|
|
43
|
+
if (layout === 'aside') {
|
|
44
|
+
close();
|
|
45
|
+
}
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<p>
|
|
49
|
+
<strong>{product.title}</strong>
|
|
50
|
+
</p>
|
|
51
|
+
</Link>
|
|
52
|
+
<ProductPrice price={line?.cost?.totalAmount} />
|
|
53
|
+
<ul>
|
|
54
|
+
{selectedOptions.map((option) => (
|
|
55
|
+
<li key={option.name}>
|
|
56
|
+
<small>
|
|
57
|
+
{option.name}: {option.value}
|
|
58
|
+
</small>
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
<CartLineQuantity line={line} />
|
|
63
|
+
</div>
|
|
64
|
+
</li>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Provides the controls to update the quantity of a line item in the cart.
|
|
70
|
+
* These controls are disabled when the line item is new, and the server
|
|
71
|
+
* hasn't yet responded that it was successfully added to the cart.
|
|
72
|
+
*/
|
|
73
|
+
function CartLineQuantity({line}: {line: OptimisticCartLine}) {
|
|
74
|
+
if (!line || typeof line?.quantity === 'undefined') return null;
|
|
75
|
+
const {id: lineId, quantity, isOptimistic} = line;
|
|
76
|
+
const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
|
|
77
|
+
const nextQuantity = Number((quantity + 1).toFixed(0));
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="cart-line-quantity">
|
|
81
|
+
<small>Quantity: {quantity} </small>
|
|
82
|
+
<CartLineUpdateButton lines={[{id: lineId, quantity: prevQuantity}]}>
|
|
83
|
+
<button
|
|
84
|
+
aria-label="Decrease quantity"
|
|
85
|
+
disabled={quantity <= 1 || !!isOptimistic}
|
|
86
|
+
name="decrease-quantity"
|
|
87
|
+
value={prevQuantity}
|
|
88
|
+
>
|
|
89
|
+
<span>− </span>
|
|
90
|
+
</button>
|
|
91
|
+
</CartLineUpdateButton>
|
|
92
|
+
|
|
93
|
+
<CartLineUpdateButton lines={[{id: lineId, quantity: nextQuantity}]}>
|
|
94
|
+
<button
|
|
95
|
+
aria-label="Increase quantity"
|
|
96
|
+
name="increase-quantity"
|
|
97
|
+
value={nextQuantity}
|
|
98
|
+
disabled={!!isOptimistic}
|
|
99
|
+
>
|
|
100
|
+
<span>+</span>
|
|
101
|
+
</button>
|
|
102
|
+
</CartLineUpdateButton>
|
|
103
|
+
|
|
104
|
+
<CartLineRemoveButton lineIds={[lineId]} disabled={!!isOptimistic} />
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A button that removes a line item from the cart. It is disabled
|
|
111
|
+
* when the line item is new, and the server hasn't yet responded
|
|
112
|
+
* that it was successfully added to the cart.
|
|
113
|
+
*/
|
|
114
|
+
function CartLineRemoveButton({
|
|
115
|
+
lineIds,
|
|
116
|
+
disabled,
|
|
117
|
+
}: {
|
|
118
|
+
lineIds: string[];
|
|
119
|
+
disabled: boolean;
|
|
120
|
+
}) {
|
|
121
|
+
return (
|
|
122
|
+
<CartForm
|
|
123
|
+
route="/cart"
|
|
124
|
+
action={CartForm.ACTIONS.LinesRemove}
|
|
125
|
+
inputs={{lineIds}}
|
|
126
|
+
>
|
|
127
|
+
<button disabled={disabled} type="submit">
|
|
128
|
+
Remove
|
|
129
|
+
</button>
|
|
130
|
+
</CartForm>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function CartLineUpdateButton({
|
|
135
|
+
children,
|
|
136
|
+
lines,
|
|
137
|
+
}: {
|
|
138
|
+
children: React.ReactNode;
|
|
139
|
+
lines: CartLineUpdateInput[];
|
|
140
|
+
}) {
|
|
141
|
+
return (
|
|
142
|
+
<CartForm
|
|
143
|
+
route="/cart"
|
|
144
|
+
action={CartForm.ACTIONS.LinesUpdate}
|
|
145
|
+
inputs={{lines}}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</CartForm>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {type OptimisticCartLine, useOptimisticCart} from '@shopify/hydrogen';
|
|
2
|
+
import {Link} from '@remix-run/react';
|
|
3
|
+
import type {CartApiQueryFragment} from 'storefrontapi.generated';
|
|
4
|
+
import {useAside} from '~/components/Aside';
|
|
5
|
+
import {CartLineItem} from '~/components/CartLineItem';
|
|
6
|
+
import {CartSummary} from './CartSummary';
|
|
7
|
+
|
|
8
|
+
export type CartLayout = 'page' | 'aside';
|
|
9
|
+
|
|
10
|
+
export type CartMainProps = {
|
|
11
|
+
cart: CartApiQueryFragment | null;
|
|
12
|
+
layout: CartLayout;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The main cart component that displays the cart items and summary.
|
|
17
|
+
* It is used by both the /cart route and the cart aside dialog.
|
|
18
|
+
*/
|
|
19
|
+
export function CartMain({layout, cart: originalCart}: CartMainProps) {
|
|
20
|
+
// The useOptimisticCart hook applies pending actions to the cart
|
|
21
|
+
// so the user immediately sees feedback when they modify the cart.
|
|
22
|
+
const cart = useOptimisticCart(originalCart);
|
|
23
|
+
|
|
24
|
+
const linesCount = Boolean(cart?.lines?.nodes?.length || 0);
|
|
25
|
+
const withDiscount =
|
|
26
|
+
cart &&
|
|
27
|
+
Boolean(cart?.discountCodes?.filter((code) => code.applicable)?.length);
|
|
28
|
+
const className = `cart-main ${withDiscount ? 'with-discount' : ''}`;
|
|
29
|
+
const cartHasItems = cart?.totalQuantity! > 0;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={className}>
|
|
33
|
+
<CartEmpty hidden={linesCount} layout={layout} />
|
|
34
|
+
<div className="cart-details">
|
|
35
|
+
<div aria-labelledby="cart-lines">
|
|
36
|
+
<ul>
|
|
37
|
+
{(cart?.lines?.nodes ?? []).map((line: OptimisticCartLine) => (
|
|
38
|
+
<CartLineItem key={line.id} line={line} layout={layout} />
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
</div>
|
|
42
|
+
{cartHasItems && <CartSummary cart={cart} layout={layout} />}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function CartEmpty({
|
|
49
|
+
hidden = false,
|
|
50
|
+
}: {
|
|
51
|
+
hidden: boolean;
|
|
52
|
+
layout?: CartMainProps['layout'];
|
|
53
|
+
}) {
|
|
54
|
+
const {close} = useAside();
|
|
55
|
+
return (
|
|
56
|
+
<div hidden={hidden}>
|
|
57
|
+
<br />
|
|
58
|
+
<p>
|
|
59
|
+
Looks like you haven’t added anything yet, let’s get you
|
|
60
|
+
started!
|
|
61
|
+
</p>
|
|
62
|
+
<br />
|
|
63
|
+
<Link to="/collections" onClick={close} prefetch="viewport">
|
|
64
|
+
Continue shopping →
|
|
65
|
+
</Link>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {CartApiQueryFragment} from 'storefrontapi.generated';
|
|
2
|
+
import type {CartLayout} from '~/components/CartMain';
|
|
3
|
+
import {CartForm, Money, type OptimisticCart} from '@shopify/hydrogen';
|
|
4
|
+
|
|
5
|
+
type CartSummaryProps = {
|
|
6
|
+
cart: OptimisticCart<CartApiQueryFragment | null>;
|
|
7
|
+
layout: CartLayout;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function CartSummary({cart, layout}: CartSummaryProps) {
|
|
11
|
+
const className =
|
|
12
|
+
layout === 'page' ? 'cart-summary-page' : 'cart-summary-aside';
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div aria-labelledby="cart-summary" className={className}>
|
|
16
|
+
<h4>Totals</h4>
|
|
17
|
+
<dl className="cart-subtotal">
|
|
18
|
+
<dt>Subtotal</dt>
|
|
19
|
+
<dd>
|
|
20
|
+
{cart.cost?.subtotalAmount?.amount ? (
|
|
21
|
+
<Money data={cart.cost?.subtotalAmount} />
|
|
22
|
+
) : (
|
|
23
|
+
'-'
|
|
24
|
+
)}
|
|
25
|
+
</dd>
|
|
26
|
+
</dl>
|
|
27
|
+
<CartDiscounts discountCodes={cart.discountCodes} />
|
|
28
|
+
<CartCheckoutActions checkoutUrl={cart.checkoutUrl} />
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
function CartCheckoutActions({checkoutUrl}: {checkoutUrl?: string}) {
|
|
33
|
+
if (!checkoutUrl) return null;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div>
|
|
37
|
+
<a href={checkoutUrl} target="_self">
|
|
38
|
+
<p>Continue to Checkout →</p>
|
|
39
|
+
</a>
|
|
40
|
+
<br />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CartDiscounts({
|
|
46
|
+
discountCodes,
|
|
47
|
+
}: {
|
|
48
|
+
discountCodes?: CartApiQueryFragment['discountCodes'];
|
|
49
|
+
}) {
|
|
50
|
+
const codes: string[] =
|
|
51
|
+
discountCodes
|
|
52
|
+
?.filter((discount) => discount.applicable)
|
|
53
|
+
?.map(({code}) => code) || [];
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div>
|
|
57
|
+
{/* Have existing discount, display it with a remove option */}
|
|
58
|
+
<dl hidden={!codes.length}>
|
|
59
|
+
<div>
|
|
60
|
+
<dt>Discount(s)</dt>
|
|
61
|
+
<UpdateDiscountForm>
|
|
62
|
+
<div className="cart-discount">
|
|
63
|
+
<code>{codes?.join(', ')}</code>
|
|
64
|
+
|
|
65
|
+
<button>Remove</button>
|
|
66
|
+
</div>
|
|
67
|
+
</UpdateDiscountForm>
|
|
68
|
+
</div>
|
|
69
|
+
</dl>
|
|
70
|
+
|
|
71
|
+
{/* Show an input to apply a discount */}
|
|
72
|
+
<UpdateDiscountForm discountCodes={codes}>
|
|
73
|
+
<div>
|
|
74
|
+
<input type="text" name="discountCode" placeholder="Discount code" />
|
|
75
|
+
|
|
76
|
+
<button type="submit">Apply</button>
|
|
77
|
+
</div>
|
|
78
|
+
</UpdateDiscountForm>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function UpdateDiscountForm({
|
|
84
|
+
discountCodes,
|
|
85
|
+
children,
|
|
86
|
+
}: {
|
|
87
|
+
discountCodes?: string[];
|
|
88
|
+
children: React.ReactNode;
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<CartForm
|
|
92
|
+
route="/cart"
|
|
93
|
+
action={CartForm.ACTIONS.DiscountCodesUpdate}
|
|
94
|
+
inputs={{
|
|
95
|
+
discountCodes: discountCodes || [],
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</CartForm>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import {Suspense} from 'react';
|
|
2
|
+
import {Await, NavLink} from '@remix-run/react';
|
|
3
|
+
import type {FooterQuery, HeaderQuery} from 'storefrontapi.generated';
|
|
4
|
+
|
|
5
|
+
interface FooterProps {
|
|
6
|
+
footer: Promise<FooterQuery | null>;
|
|
7
|
+
header: HeaderQuery;
|
|
8
|
+
publicStoreDomain: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Footer({
|
|
12
|
+
footer: footerPromise,
|
|
13
|
+
header,
|
|
14
|
+
publicStoreDomain,
|
|
15
|
+
}: FooterProps) {
|
|
16
|
+
return (
|
|
17
|
+
<Suspense>
|
|
18
|
+
<Await resolve={footerPromise}>
|
|
19
|
+
{(footer) => (
|
|
20
|
+
<footer className="footer">
|
|
21
|
+
{footer?.menu && header.shop.primaryDomain?.url && (
|
|
22
|
+
<FooterMenu
|
|
23
|
+
menu={footer.menu}
|
|
24
|
+
primaryDomainUrl={header.shop.primaryDomain.url}
|
|
25
|
+
publicStoreDomain={publicStoreDomain}
|
|
26
|
+
/>
|
|
27
|
+
)}
|
|
28
|
+
</footer>
|
|
29
|
+
)}
|
|
30
|
+
</Await>
|
|
31
|
+
</Suspense>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function FooterMenu({
|
|
36
|
+
menu,
|
|
37
|
+
primaryDomainUrl,
|
|
38
|
+
publicStoreDomain,
|
|
39
|
+
}: {
|
|
40
|
+
menu: FooterQuery['menu'];
|
|
41
|
+
primaryDomainUrl: FooterProps['header']['shop']['primaryDomain']['url'];
|
|
42
|
+
publicStoreDomain: string;
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<nav className="footer-menu" role="navigation">
|
|
46
|
+
{(menu || FALLBACK_FOOTER_MENU).items.map((item) => {
|
|
47
|
+
if (!item.url) return null;
|
|
48
|
+
// if the url is internal, we strip the domain
|
|
49
|
+
const url =
|
|
50
|
+
item.url.includes('myshopify.com') ||
|
|
51
|
+
item.url.includes(publicStoreDomain) ||
|
|
52
|
+
item.url.includes(primaryDomainUrl)
|
|
53
|
+
? new URL(item.url).pathname
|
|
54
|
+
: item.url;
|
|
55
|
+
const isExternal = !url.startsWith('/');
|
|
56
|
+
return isExternal ? (
|
|
57
|
+
<a href={url} key={item.id} rel="noopener noreferrer" target="_blank">
|
|
58
|
+
{item.title}
|
|
59
|
+
</a>
|
|
60
|
+
) : (
|
|
61
|
+
<NavLink
|
|
62
|
+
end
|
|
63
|
+
key={item.id}
|
|
64
|
+
prefetch="intent"
|
|
65
|
+
style={activeLinkStyle}
|
|
66
|
+
to={url}
|
|
67
|
+
>
|
|
68
|
+
{item.title}
|
|
69
|
+
</NavLink>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</nav>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const FALLBACK_FOOTER_MENU = {
|
|
77
|
+
id: 'gid://shopify/Menu/199655620664',
|
|
78
|
+
items: [
|
|
79
|
+
{
|
|
80
|
+
id: 'gid://shopify/MenuItem/461633060920',
|
|
81
|
+
resourceId: 'gid://shopify/ShopPolicy/23358046264',
|
|
82
|
+
tags: [],
|
|
83
|
+
title: 'Privacy Policy',
|
|
84
|
+
type: 'SHOP_POLICY',
|
|
85
|
+
url: '/policies/privacy-policy',
|
|
86
|
+
items: [],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'gid://shopify/MenuItem/461633093688',
|
|
90
|
+
resourceId: 'gid://shopify/ShopPolicy/23358013496',
|
|
91
|
+
tags: [],
|
|
92
|
+
title: 'Refund Policy',
|
|
93
|
+
type: 'SHOP_POLICY',
|
|
94
|
+
url: '/policies/refund-policy',
|
|
95
|
+
items: [],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'gid://shopify/MenuItem/461633126456',
|
|
99
|
+
resourceId: 'gid://shopify/ShopPolicy/23358111800',
|
|
100
|
+
tags: [],
|
|
101
|
+
title: 'Shipping Policy',
|
|
102
|
+
type: 'SHOP_POLICY',
|
|
103
|
+
url: '/policies/shipping-policy',
|
|
104
|
+
items: [],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: 'gid://shopify/MenuItem/461633159224',
|
|
108
|
+
resourceId: 'gid://shopify/ShopPolicy/23358079032',
|
|
109
|
+
tags: [],
|
|
110
|
+
title: 'Terms of Service',
|
|
111
|
+
type: 'SHOP_POLICY',
|
|
112
|
+
url: '/policies/terms-of-service',
|
|
113
|
+
items: [],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function activeLinkStyle({
|
|
119
|
+
isActive,
|
|
120
|
+
isPending,
|
|
121
|
+
}: {
|
|
122
|
+
isActive: boolean;
|
|
123
|
+
isPending: boolean;
|
|
124
|
+
}) {
|
|
125
|
+
return {
|
|
126
|
+
fontWeight: isActive ? 'bold' : undefined,
|
|
127
|
+
color: isPending ? 'grey' : 'white',
|
|
128
|
+
};
|
|
129
|
+
}
|