@shopify/cli-hydrogen 8.1.1 → 8.3.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.
Files changed (105) hide show
  1. package/dist/assets/hydrogen/starter/.graphqlrc.ts +27 -0
  2. package/dist/assets/hydrogen/starter/CHANGELOG.md +166 -8
  3. package/dist/assets/hydrogen/starter/app/components/AddToCartButton.tsx +37 -0
  4. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +150 -0
  5. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +68 -0
  6. package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +101 -0
  7. package/dist/assets/hydrogen/starter/app/components/Header.tsx +3 -3
  8. package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +2 -2
  9. package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +80 -0
  10. package/dist/assets/hydrogen/starter/app/components/ProductImage.tsx +23 -0
  11. package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +27 -0
  12. package/dist/assets/hydrogen/starter/app/lib/session.ts +5 -0
  13. package/dist/assets/hydrogen/starter/app/root.tsx +23 -36
  14. package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +1 -5
  15. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +12 -70
  16. package/dist/assets/hydrogen/starter/app/routes/account.orders.$id.tsx +7 -14
  17. package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +1 -8
  18. package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +5 -22
  19. package/dist/assets/hydrogen/starter/app/routes/account.tsx +0 -1
  20. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +1 -3
  21. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +51 -232
  22. package/dist/assets/hydrogen/starter/package.json +10 -11
  23. package/dist/assets/hydrogen/starter/server.ts +4 -0
  24. package/dist/assets/hydrogen/tailwind/package.json +1 -6
  25. package/dist/assets/hydrogen/tailwind/tailwind.css +6 -3
  26. package/dist/assets/hydrogen/vanilla-extract/package.json +2 -3
  27. package/dist/assets/hydrogen/virtual-routes/components/{Layout.jsx → PageLayout.jsx} +2 -2
  28. package/dist/assets/hydrogen/virtual-routes/components/RequestDetails.jsx +1 -2
  29. package/dist/assets/hydrogen/virtual-routes/components/RequestTable.jsx +1 -2
  30. package/dist/assets/hydrogen/virtual-routes/routes/index.jsx +1 -2
  31. package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +8 -30
  32. package/dist/commands/hydrogen/build.js +33 -10
  33. package/dist/commands/hydrogen/customer-account/push.js +3 -6
  34. package/dist/commands/hydrogen/debug/cpu.js +3 -3
  35. package/dist/commands/hydrogen/deploy.js +14 -3
  36. package/dist/commands/hydrogen/dev.js +3 -6
  37. package/dist/commands/hydrogen/env/list.js +1 -2
  38. package/dist/commands/hydrogen/env/pull.js +2 -4
  39. package/dist/commands/hydrogen/env/push.js +6 -12
  40. package/dist/commands/hydrogen/init.d.ts +18 -15
  41. package/dist/commands/hydrogen/init.js +12 -24
  42. package/dist/commands/hydrogen/link.js +1 -2
  43. package/dist/commands/hydrogen/login.js +4 -0
  44. package/dist/commands/hydrogen/logout.js +2 -0
  45. package/dist/commands/hydrogen/preview.js +4 -6
  46. package/dist/commands/hydrogen/setup/css.js +29 -12
  47. package/dist/commands/hydrogen/setup/vite.js +3 -6
  48. package/dist/commands/hydrogen/setup.js +8 -7
  49. package/dist/commands/hydrogen/upgrade.js +16 -32
  50. package/dist/hooks/init.js +53 -6
  51. package/dist/index.d.ts +46 -46
  52. package/dist/lib/auth.js +4 -65
  53. package/dist/lib/build.js +1 -2
  54. package/dist/lib/bundle/analyzer.js +39 -24
  55. package/dist/lib/bundle/vite-plugin.js +161 -0
  56. package/dist/lib/check-cli-version.js +61 -0
  57. package/dist/lib/check-lockfile.js +2 -2
  58. package/dist/lib/classic-compiler/build.js +3 -3
  59. package/dist/lib/classic-compiler/dev.js +5 -10
  60. package/dist/lib/codegen.js +75 -49
  61. package/dist/lib/defer.js +2 -4
  62. package/dist/lib/environment-variables.js +2 -4
  63. package/dist/lib/file.js +15 -7
  64. package/dist/lib/flags.js +10 -0
  65. package/dist/lib/get-oxygen-deployment-data.js +1 -2
  66. package/dist/lib/graphiql-url.js +1 -2
  67. package/dist/lib/import-utils.js +8 -2
  68. package/dist/lib/log.js +17 -53
  69. package/dist/lib/mini-oxygen/common.js +1 -2
  70. package/dist/lib/mini-oxygen/node.js +1 -2
  71. package/dist/lib/missing-routes.js +1 -2
  72. package/dist/lib/onboarding/common.js +62 -15
  73. package/dist/lib/onboarding/local.js +14 -13
  74. package/dist/lib/onboarding/remote.js +16 -9
  75. package/dist/lib/onboarding/setup-template.mocks.js +6 -3
  76. package/dist/lib/remix-config.js +2 -4
  77. package/dist/lib/remix-version-check.js +1 -2
  78. package/dist/lib/request-events.js +3 -6
  79. package/dist/lib/setups/css/assets.js +1 -1
  80. package/dist/lib/setups/css/index.js +17 -10
  81. package/dist/lib/setups/css/replacers.js +74 -76
  82. package/dist/lib/setups/css/tailwind.js +16 -20
  83. package/dist/lib/setups/css/vanilla-extract.js +8 -5
  84. package/dist/lib/setups/i18n/replacers.js +1 -2
  85. package/dist/lib/setups/routes/generate.js +18 -19
  86. package/dist/lib/shell.js +5 -10
  87. package/dist/lib/template-diff.js +83 -104
  88. package/dist/lib/template-downloader.js +2 -2
  89. package/dist/lib/transpile/morph/functions.js +3 -6
  90. package/dist/lib/transpile/morph/index.js +2 -4
  91. package/dist/lib/transpile/morph/typedefs.js +3 -6
  92. package/dist/lib/transpile/morph/utils.js +2 -4
  93. package/dist/lib/transpile/project.js +4 -3
  94. package/oclif.manifest.json +51 -4
  95. package/package.json +8 -12
  96. package/dist/assets/hydrogen/css-modules/package.json +0 -6
  97. package/dist/assets/hydrogen/postcss/package.json +0 -10
  98. package/dist/assets/hydrogen/postcss/postcss.config.js +0 -8
  99. package/dist/assets/hydrogen/starter/.graphqlrc.yml +0 -12
  100. package/dist/assets/hydrogen/starter/app/components/Cart.tsx +0 -364
  101. package/dist/assets/hydrogen/tailwind/postcss.config.js +0 -10
  102. package/dist/assets/hydrogen/tailwind/tailwind.config.js +0 -8
  103. package/dist/lib/check-version.js +0 -75
  104. package/dist/lib/setups/css/css-modules.js +0 -23
  105. package/dist/lib/setups/css/postcss.js +0 -31
@@ -0,0 +1,27 @@
1
+ import type {IGraphQLConfig} from 'graphql-config';
2
+ import {getSchema} from '@shopify/hydrogen-codegen';
3
+
4
+ /**
5
+ * GraphQL Config
6
+ * @see https://the-guild.dev/graphql/config/docs/user/usage
7
+ * @type {IGraphQLConfig}
8
+ */
9
+ export default {
10
+ projects: {
11
+ default: {
12
+ schema: getSchema('storefront'),
13
+ documents: [
14
+ './*.{ts,tsx,js,jsx}',
15
+ './app/**/*.{ts,tsx,js,jsx}',
16
+ '!./app/graphql/**/*.{ts,tsx,js,jsx}',
17
+ ],
18
+ },
19
+
20
+ customer: {
21
+ schema: getSchema('customer-account'),
22
+ documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'],
23
+ },
24
+
25
+ // Add your own GraphQL projects here for CMS, Shopify Admin API, etc.
26
+ },
27
+ } as IGraphQLConfig;
@@ -1,21 +1,179 @@
1
1
  # skeleton
2
2
 
3
- ## 2024.4.7
3
+ ## 2024.7.2
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Fix paths on Windows. ([#2243](https://github.com/Shopify/hydrogen/pull/2243)) by [@michenly](https://github.com/michenly)
7
+ - Changed the GraphQL config file format to be TS/JS instead of YAML. ([#2311](https://github.com/Shopify/hydrogen/pull/2311)) by [@frandiox](https://github.com/frandiox)
8
8
 
9
- - Updated dependencies [[`31452380`](https://github.com/Shopify/hydrogen/commit/31452380340e079cd4ec1f8c10cdab5e5313e921)]:
10
- - @shopify/hydrogen@2024.4.5
11
- - @shopify/cli-hydrogen@8.1.1
9
+ - Updated dependencies [[`18ea233c`](https://github.com/Shopify/hydrogen/commit/18ea233cd327bf3001ec9b107ad66b05c9c78584), [`8b2322d7`](https://github.com/Shopify/hydrogen/commit/8b2322d783078298cd5d20ec5f3b1faf99b7895b)]:
10
+ - @shopify/cli-hydrogen@8.3.0
12
11
 
13
- ## 2024.4.6
12
+ ## 2024.7.1
14
13
 
15
14
  ### Patch Changes
16
15
 
17
- - Updated dependencies [[`707afb96`](https://github.com/Shopify/hydrogen/commit/707afb96fd1ef64a59a14182f60ca61718b372d1)]:
18
- - @shopify/hydrogen@2024.4.4
16
+ - Update `@shopify/oxygen-workers-types` to fix issues on Windows. ([#2252](https://github.com/Shopify/hydrogen/pull/2252)) by [@michenly](https://github.com/michenly)
17
+
18
+ - [**Breaking change**] ([#2113](https://github.com/Shopify/hydrogen/pull/2113)) by [@blittle](https://github.com/blittle)
19
+
20
+ Previously the `VariantSelector` component would filter out options that only had one value. This is undesireable for some apps. We've removed that filter, if you'd like to retain the existing functionality, simply filter the options prop before it is passed to the `VariantSelector` component:
21
+
22
+ ```diff
23
+ <VariantSelector
24
+ handle={product.handle}
25
+ + options={product.options.filter((option) => option.values.length > 1)}
26
+ - options={product.options}
27
+ variants={variants}>
28
+ </VariantSelector>
29
+ ```
30
+
31
+ Fixes [#1198](https://github.com/Shopify/hydrogen/discussions/1198)
32
+
33
+ - Update remix to v2.10.1 ([#2290](https://github.com/Shopify/hydrogen/pull/2290)) by [@michenly](https://github.com/michenly)
34
+
35
+ - Update root to use [Remix's Layout Export pattern](https://remix.run/docs/en/main/file-conventions/root#layout-export) and eliminate the use of `useLoaderData` in root. ([#2292](https://github.com/Shopify/hydrogen/pull/2292)) by [@michenly](https://github.com/michenly)
36
+
37
+ The diff below showcase how you can make this refactor in existing application.
38
+
39
+ ```diff
40
+ import {
41
+ Outlet,
42
+ - useLoaderData,
43
+ + useRouteLoaderData,
44
+ } from '@remix-run/react';
45
+ -import {Layout} from '~/components/Layout';
46
+ +import {PageLayout} from '~/components/PageLayout';
47
+
48
+ -export default function App() {
49
+ +export function Layout({children}: {children?: React.ReactNode}) {
50
+ const nonce = useNonce();
51
+ - const data = useLoaderData<typeof loader>();
52
+ + const data = useRouteLoaderData<typeof loader>('root');
53
+
54
+ return (
55
+ <html>
56
+ ...
57
+ <body>
58
+ - <Layout {...data}>
59
+ - <Outlet />
60
+ - </Layout>
61
+ + {data? (
62
+ + <PageLayout {...data}>{children}</PageLayout>
63
+ + ) : (
64
+ + children
65
+ + )}
66
+ </body>
67
+ </html>
68
+ );
69
+ }
70
+
71
+ +export default function App() {
72
+ + return <Outlet />;
73
+ +}
74
+
75
+ export function ErrorBoundary() {
76
+ - const rootData = useLoaderData<typeof loader>();
77
+
78
+ return (
79
+ - <html>
80
+ - ...
81
+ - <body>
82
+ - <Layout {...rootData}>
83
+ - <div className="route-error">
84
+ - <h1>Error</h1>
85
+ - ...
86
+ - </div>
87
+ - </Layout>
88
+ - </body>
89
+ - </html>
90
+ + <div className="route-error">
91
+ + <h1>Error</h1>
92
+ + ...
93
+ + </div>
94
+ );
95
+ }
96
+
97
+ ```
98
+
99
+ - Refactor the cart and product form components ([#2132](https://github.com/Shopify/hydrogen/pull/2132)) by [@blittle](https://github.com/blittle)
100
+
101
+ - Remove manual setting of session in headers and recommend setting it in server after response is created. ([#2137](https://github.com/Shopify/hydrogen/pull/2137)) by [@michenly](https://github.com/michenly)
102
+
103
+ Step 1: Add `isPending` implementation in session
104
+
105
+ ```diff
106
+ // in app/lib/session.ts
107
+ export class AppSession implements HydrogenSession {
108
+ + public isPending = false;
109
+
110
+ get unset() {
111
+ + this.isPending = true;
112
+ return this.#session.unset;
113
+ }
114
+
115
+ get set() {
116
+ + this.isPending = true;
117
+ return this.#session.set;
118
+ }
119
+
120
+ commit() {
121
+ + this.isPending = false;
122
+ return this.#sessionStorage.commitSession(this.#session);
123
+ }
124
+ }
125
+ ```
126
+
127
+ Step 2: update response header if `session.isPending` is true
128
+
129
+ ```diff
130
+ // in server.ts
131
+ export default {
132
+ async fetch(request: Request): Promise<Response> {
133
+ try {
134
+ const response = await handleRequest(request);
135
+
136
+ + if (session.isPending) {
137
+ + response.headers.set('Set-Cookie', await session.commit());
138
+ + }
139
+
140
+ return response;
141
+ } catch (error) {
142
+ ...
143
+ }
144
+ },
145
+ };
146
+ ```
147
+
148
+ Step 3: remove setting cookie with session.commit() in routes
149
+
150
+ ```diff
151
+ // in route files
152
+ export async function loader({context}: LoaderFunctionArgs) {
153
+ return json({},
154
+ - {
155
+ - headers: {
156
+ - 'Set-Cookie': await context.session.commit(),
157
+ - },
158
+ },
159
+ );
160
+ }
161
+ ```
162
+
163
+ - Moved `@shopify/cli` from `dependencies` to `devDependencies`. ([#2312](https://github.com/Shopify/hydrogen/pull/2312)) by [@frandiox](https://github.com/frandiox)
164
+
165
+ - The `@shopify/cli` package now bundles the `@shopify/cli-hydrogen` plugin. Therefore, you can now remove the latter from your local dependencies: ([#2306](https://github.com/Shopify/hydrogen/pull/2306)) by [@frandiox](https://github.com/frandiox)
166
+
167
+ ```diff
168
+ "@shopify/cli": "3.64.0",
169
+ - "@shopify/cli-hydrogen": "^8.1.1",
170
+ "@shopify/hydrogen": "2024.7.0",
171
+ ```
172
+
173
+ - Updated dependencies [[`a0e84d76`](https://github.com/Shopify/hydrogen/commit/a0e84d76b67d4c57c4defee06185949c41782eab), [`426bb390`](https://github.com/Shopify/hydrogen/commit/426bb390b25f51e57499ff6673aef70ded935e87), [`4337200c`](https://github.com/Shopify/hydrogen/commit/4337200c7908d56c039171c283a4d92c31a8b7b6), [`710625c7`](https://github.com/Shopify/hydrogen/commit/710625c740a6656488d4b419e2d2451bef9d076f), [`8b9c726d`](https://github.com/Shopify/hydrogen/commit/8b9c726d34f3482b5b5a0da4c7c0c2f20e2c9caa), [`10a419bf`](https://github.com/Shopify/hydrogen/commit/10a419bf1db79cdfd8c41c0223ce695959f60da9), [`6a6278bb`](https://github.com/Shopify/hydrogen/commit/6a6278bb9187b3b5a98cd98ec9dd278882d03c0d), [`66236ca6`](https://github.com/Shopify/hydrogen/commit/66236ca65ddefac99eaa553c7877c85863d84cc2), [`dcbd0bbf`](https://github.com/Shopify/hydrogen/commit/dcbd0bbf4073a3e35e96f3cce257f7b19b2b2aea), [`a5e03e2a`](https://github.com/Shopify/hydrogen/commit/a5e03e2a1e99fcd83ee5a2be7bf6f5f6b47984b3), [`c2690653`](https://github.com/Shopify/hydrogen/commit/c2690653b6b24f7318e9088551a37195255a2247), [`54c2f7ad`](https://github.com/Shopify/hydrogen/commit/54c2f7ad3d0d52e6be10b2a54a1a4fd0cc107a35), [`4337200c`](https://github.com/Shopify/hydrogen/commit/4337200c7908d56c039171c283a4d92c31a8b7b6), [`e96b332b`](https://github.com/Shopify/hydrogen/commit/e96b332ba1aba79aa3d5c2ce18001292070faf49), [`f3065371`](https://github.com/Shopify/hydrogen/commit/f3065371c1dda222c6e40bd8c20528dc9fdea9a5), [`6cd5554b`](https://github.com/Shopify/hydrogen/commit/6cd5554b160d314d35964a5ee8976ed60972bf17), [`9eb60d73`](https://github.com/Shopify/hydrogen/commit/9eb60d73e552c3d22b9325ecbcd5878810893ad3), [`e432533e`](https://github.com/Shopify/hydrogen/commit/e432533e7391ec3fe16a4a24f2b3363206842580), [`de3f70be`](https://github.com/Shopify/hydrogen/commit/de3f70be1a838eda746903cbb38cc25cf0e09fa3), [`83cb96f4`](https://github.com/Shopify/hydrogen/commit/83cb96f42078bf79b20a153d8a8461f75d573ab1)]:
174
+ - @shopify/remix-oxygen@2.0.5
175
+ - @shopify/cli-hydrogen@8.2.0
176
+ - @shopify/hydrogen@2024.7.1
19
177
 
20
178
  ## 2024.4.5
21
179
 
@@ -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,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} &nbsp;&nbsp;</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>&#8722; </span>
90
+ </button>
91
+ </CartLineUpdateButton>
92
+ &nbsp;
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>&#43;</span>
101
+ </button>
102
+ </CartLineUpdateButton>
103
+ &nbsp;
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&rsquo;t added anything yet, let&rsquo;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 &rarr;</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
+ &nbsp;
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
+ &nbsp;
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
+ }
@@ -138,7 +138,7 @@ function SearchToggle() {
138
138
  );
139
139
  }
140
140
 
141
- function CartBadge({count}: {count: number}) {
141
+ function CartBadge({count}: {count: number | null}) {
142
142
  const {open} = useAside();
143
143
  const {publish, shop, cart, prevCart} = useAnalytics();
144
144
 
@@ -156,14 +156,14 @@ function CartBadge({count}: {count: number}) {
156
156
  } as CartViewPayload);
157
157
  }}
158
158
  >
159
- Cart {count}
159
+ Cart {count === null ? <span>&nbsp;</span> : count}
160
160
  </a>
161
161
  );
162
162
  }
163
163
 
164
164
  function CartToggle({cart}: Pick<HeaderProps, 'cart'>) {
165
165
  return (
166
- <Suspense fallback={<CartBadge count={0} />}>
166
+ <Suspense fallback={<CartBadge count={null} />}>
167
167
  <Await resolve={cart}>
168
168
  {(cart) => {
169
169
  if (!cart) return <CartBadge count={0} />;
@@ -8,7 +8,7 @@ import type {
8
8
  import {Aside} from '~/components/Aside';
9
9
  import {Footer} from '~/components/Footer';
10
10
  import {Header, HeaderMenu} from '~/components/Header';
11
- import {CartMain} from '~/components/Cart';
11
+ import {CartMain} from '~/components/CartMain';
12
12
  import {
13
13
  PredictiveSearchForm,
14
14
  PredictiveSearchResults,
@@ -60,7 +60,7 @@ function CartAside({cart}: {cart: PageLayoutProps['cart']}) {
60
60
  <Suspense fallback={<p>Loading cart ...</p>}>
61
61
  <Await resolve={cart}>
62
62
  {(cart) => {
63
- return <CartMain cart={cart!} layout="aside" />;
63
+ return <CartMain cart={cart} layout="aside" />;
64
64
  }}
65
65
  </Await>
66
66
  </Suspense>