@shopify/cli-hydrogen 8.1.1 → 8.2.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 (100) hide show
  1. package/dist/assets/hydrogen/starter/CHANGELOG.md +158 -9
  2. package/dist/assets/hydrogen/starter/app/components/AddToCartButton.tsx +37 -0
  3. package/dist/assets/hydrogen/starter/app/components/CartLineItem.tsx +150 -0
  4. package/dist/assets/hydrogen/starter/app/components/CartMain.tsx +68 -0
  5. package/dist/assets/hydrogen/starter/app/components/CartSummary.tsx +101 -0
  6. package/dist/assets/hydrogen/starter/app/components/Header.tsx +3 -3
  7. package/dist/assets/hydrogen/starter/app/components/PageLayout.tsx +2 -2
  8. package/dist/assets/hydrogen/starter/app/components/ProductForm.tsx +80 -0
  9. package/dist/assets/hydrogen/starter/app/components/ProductImage.tsx +23 -0
  10. package/dist/assets/hydrogen/starter/app/components/ProductPrice.tsx +27 -0
  11. package/dist/assets/hydrogen/starter/app/lib/session.ts +5 -0
  12. package/dist/assets/hydrogen/starter/app/root.tsx +23 -36
  13. package/dist/assets/hydrogen/starter/app/routes/account.$.tsx +1 -5
  14. package/dist/assets/hydrogen/starter/app/routes/account.addresses.tsx +12 -70
  15. package/dist/assets/hydrogen/starter/app/routes/account.orders.$id.tsx +7 -14
  16. package/dist/assets/hydrogen/starter/app/routes/account.orders._index.tsx +1 -8
  17. package/dist/assets/hydrogen/starter/app/routes/account.profile.tsx +5 -22
  18. package/dist/assets/hydrogen/starter/app/routes/account.tsx +0 -1
  19. package/dist/assets/hydrogen/starter/app/routes/cart.tsx +1 -3
  20. package/dist/assets/hydrogen/starter/app/routes/products.$handle.tsx +51 -232
  21. package/dist/assets/hydrogen/starter/package.json +10 -11
  22. package/dist/assets/hydrogen/starter/server.ts +4 -0
  23. package/dist/assets/hydrogen/tailwind/package.json +1 -6
  24. package/dist/assets/hydrogen/tailwind/tailwind.css +6 -3
  25. package/dist/assets/hydrogen/vanilla-extract/package.json +2 -3
  26. package/dist/assets/hydrogen/virtual-routes/components/{Layout.jsx → PageLayout.jsx} +2 -2
  27. package/dist/assets/hydrogen/virtual-routes/components/RequestDetails.jsx +1 -2
  28. package/dist/assets/hydrogen/virtual-routes/components/RequestTable.jsx +1 -2
  29. package/dist/assets/hydrogen/virtual-routes/routes/index.jsx +1 -2
  30. package/dist/assets/hydrogen/virtual-routes/virtual-root.jsx +8 -30
  31. package/dist/commands/hydrogen/build.js +33 -10
  32. package/dist/commands/hydrogen/customer-account/push.js +3 -6
  33. package/dist/commands/hydrogen/debug/cpu.js +3 -3
  34. package/dist/commands/hydrogen/deploy.js +14 -3
  35. package/dist/commands/hydrogen/dev.js +3 -6
  36. package/dist/commands/hydrogen/env/list.js +1 -2
  37. package/dist/commands/hydrogen/env/pull.js +2 -4
  38. package/dist/commands/hydrogen/env/push.js +6 -12
  39. package/dist/commands/hydrogen/init.d.ts +18 -15
  40. package/dist/commands/hydrogen/init.js +12 -24
  41. package/dist/commands/hydrogen/link.js +1 -2
  42. package/dist/commands/hydrogen/preview.js +4 -6
  43. package/dist/commands/hydrogen/setup/css.js +29 -12
  44. package/dist/commands/hydrogen/setup/vite.js +3 -6
  45. package/dist/commands/hydrogen/setup.js +8 -7
  46. package/dist/commands/hydrogen/upgrade.js +16 -32
  47. package/dist/hooks/init.js +50 -6
  48. package/dist/index.d.ts +46 -46
  49. package/dist/lib/auth.js +1 -2
  50. package/dist/lib/build.js +1 -2
  51. package/dist/lib/bundle/analyzer.js +39 -24
  52. package/dist/lib/bundle/vite-plugin.js +161 -0
  53. package/dist/lib/check-cli-version.js +61 -0
  54. package/dist/lib/check-lockfile.js +2 -2
  55. package/dist/lib/classic-compiler/build.js +3 -3
  56. package/dist/lib/classic-compiler/dev.js +5 -10
  57. package/dist/lib/codegen.js +8 -16
  58. package/dist/lib/defer.js +2 -4
  59. package/dist/lib/environment-variables.js +2 -4
  60. package/dist/lib/file.js +15 -7
  61. package/dist/lib/flags.js +10 -0
  62. package/dist/lib/get-oxygen-deployment-data.js +1 -2
  63. package/dist/lib/graphiql-url.js +1 -2
  64. package/dist/lib/log.js +11 -22
  65. package/dist/lib/mini-oxygen/common.js +1 -2
  66. package/dist/lib/mini-oxygen/node.js +1 -2
  67. package/dist/lib/missing-routes.js +1 -2
  68. package/dist/lib/onboarding/common.js +60 -15
  69. package/dist/lib/onboarding/local.js +14 -13
  70. package/dist/lib/onboarding/remote.js +16 -9
  71. package/dist/lib/onboarding/setup-template.mocks.js +6 -3
  72. package/dist/lib/remix-config.js +2 -4
  73. package/dist/lib/remix-version-check.js +1 -2
  74. package/dist/lib/request-events.js +3 -6
  75. package/dist/lib/setups/css/assets.js +1 -1
  76. package/dist/lib/setups/css/index.js +17 -10
  77. package/dist/lib/setups/css/replacers.js +74 -76
  78. package/dist/lib/setups/css/tailwind.js +16 -20
  79. package/dist/lib/setups/css/vanilla-extract.js +8 -5
  80. package/dist/lib/setups/i18n/replacers.js +1 -2
  81. package/dist/lib/setups/routes/generate.js +18 -19
  82. package/dist/lib/shell.js +5 -10
  83. package/dist/lib/template-diff.js +83 -104
  84. package/dist/lib/template-downloader.js +2 -2
  85. package/dist/lib/transpile/morph/functions.js +3 -6
  86. package/dist/lib/transpile/morph/index.js +2 -4
  87. package/dist/lib/transpile/morph/typedefs.js +3 -6
  88. package/dist/lib/transpile/morph/utils.js +2 -4
  89. package/dist/lib/transpile/project.js +4 -3
  90. package/oclif.manifest.json +51 -4
  91. package/package.json +8 -12
  92. package/dist/assets/hydrogen/css-modules/package.json +0 -6
  93. package/dist/assets/hydrogen/postcss/package.json +0 -10
  94. package/dist/assets/hydrogen/postcss/postcss.config.js +0 -8
  95. package/dist/assets/hydrogen/starter/app/components/Cart.tsx +0 -364
  96. package/dist/assets/hydrogen/tailwind/postcss.config.js +0 -10
  97. package/dist/assets/hydrogen/tailwind/tailwind.config.js +0 -8
  98. package/dist/lib/check-version.js +0 -75
  99. package/dist/lib/setups/css/css-modules.js +0 -23
  100. package/dist/lib/setups/css/postcss.js +0 -31
@@ -1,21 +1,170 @@
1
1
  # skeleton
2
2
 
3
- ## 2024.4.7
3
+ ## 2024.7.1
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
+ - Update `@shopify/oxygen-workers-types` to fix issues on Windows. ([#2252](https://github.com/Shopify/hydrogen/pull/2252)) by [@michenly](https://github.com/michenly)
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
+ - [**Breaking change**] ([#2113](https://github.com/Shopify/hydrogen/pull/2113)) by [@blittle](https://github.com/blittle)
12
10
 
13
- ## 2024.4.6
11
+ 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:
14
12
 
15
- ### Patch Changes
13
+ ```diff
14
+ <VariantSelector
15
+ handle={product.handle}
16
+ + options={product.options.filter((option) => option.values.length > 1)}
17
+ - options={product.options}
18
+ variants={variants}>
19
+ </VariantSelector>
20
+ ```
21
+
22
+ Fixes [#1198](https://github.com/Shopify/hydrogen/discussions/1198)
23
+
24
+ - Update remix to v2.10.1 ([#2290](https://github.com/Shopify/hydrogen/pull/2290)) by [@michenly](https://github.com/michenly)
25
+
26
+ - 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)
27
+
28
+ The diff below showcase how you can make this refactor in existing application.
29
+
30
+ ```diff
31
+ import {
32
+ Outlet,
33
+ - useLoaderData,
34
+ + useRouteLoaderData,
35
+ } from '@remix-run/react';
36
+ -import {Layout} from '~/components/Layout';
37
+ +import {PageLayout} from '~/components/PageLayout';
38
+
39
+ -export default function App() {
40
+ +export function Layout({children}: {children?: React.ReactNode}) {
41
+ const nonce = useNonce();
42
+ - const data = useLoaderData<typeof loader>();
43
+ + const data = useRouteLoaderData<typeof loader>('root');
44
+
45
+ return (
46
+ <html>
47
+ ...
48
+ <body>
49
+ - <Layout {...data}>
50
+ - <Outlet />
51
+ - </Layout>
52
+ + {data? (
53
+ + <PageLayout {...data}>{children}</PageLayout>
54
+ + ) : (
55
+ + children
56
+ + )}
57
+ </body>
58
+ </html>
59
+ );
60
+ }
61
+
62
+ +export default function App() {
63
+ + return <Outlet />;
64
+ +}
65
+
66
+ export function ErrorBoundary() {
67
+ - const rootData = useLoaderData<typeof loader>();
68
+
69
+ return (
70
+ - <html>
71
+ - ...
72
+ - <body>
73
+ - <Layout {...rootData}>
74
+ - <div className="route-error">
75
+ - <h1>Error</h1>
76
+ - ...
77
+ - </div>
78
+ - </Layout>
79
+ - </body>
80
+ - </html>
81
+ + <div className="route-error">
82
+ + <h1>Error</h1>
83
+ + ...
84
+ + </div>
85
+ );
86
+ }
87
+
88
+ ```
89
+
90
+ - Refactor the cart and product form components ([#2132](https://github.com/Shopify/hydrogen/pull/2132)) by [@blittle](https://github.com/blittle)
91
+
92
+ - 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)
93
+
94
+ Step 1: Add `isPending` implementation in session
95
+
96
+ ```diff
97
+ // in app/lib/session.ts
98
+ export class AppSession implements HydrogenSession {
99
+ + public isPending = false;
100
+
101
+ get unset() {
102
+ + this.isPending = true;
103
+ return this.#session.unset;
104
+ }
105
+
106
+ get set() {
107
+ + this.isPending = true;
108
+ return this.#session.set;
109
+ }
110
+
111
+ commit() {
112
+ + this.isPending = false;
113
+ return this.#sessionStorage.commitSession(this.#session);
114
+ }
115
+ }
116
+ ```
117
+
118
+ Step 2: update response header if `session.isPending` is true
119
+
120
+ ```diff
121
+ // in server.ts
122
+ export default {
123
+ async fetch(request: Request): Promise<Response> {
124
+ try {
125
+ const response = await handleRequest(request);
126
+
127
+ + if (session.isPending) {
128
+ + response.headers.set('Set-Cookie', await session.commit());
129
+ + }
130
+
131
+ return response;
132
+ } catch (error) {
133
+ ...
134
+ }
135
+ },
136
+ };
137
+ ```
138
+
139
+ Step 3: remove setting cookie with session.commit() in routes
140
+
141
+ ```diff
142
+ // in route files
143
+ export async function loader({context}: LoaderFunctionArgs) {
144
+ return json({},
145
+ - {
146
+ - headers: {
147
+ - 'Set-Cookie': await context.session.commit(),
148
+ - },
149
+ },
150
+ );
151
+ }
152
+ ```
153
+
154
+ - Moved `@shopify/cli` from `dependencies` to `devDependencies`. ([#2312](https://github.com/Shopify/hydrogen/pull/2312)) by [@frandiox](https://github.com/frandiox)
155
+
156
+ - 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)
157
+
158
+ ```diff
159
+ "@shopify/cli": "3.64.0",
160
+ - "@shopify/cli-hydrogen": "^8.1.1",
161
+ "@shopify/hydrogen": "2024.7.0",
162
+ ```
16
163
 
17
- - Updated dependencies [[`707afb96`](https://github.com/Shopify/hydrogen/commit/707afb96fd1ef64a59a14182f60ca61718b372d1)]:
18
- - @shopify/hydrogen@2024.4.4
164
+ - 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)]:
165
+ - @shopify/remix-oxygen@2.0.5
166
+ - @shopify/cli-hydrogen@8.2.0
167
+ - @shopify/hydrogen@2024.7.1
19
168
 
20
169
  ## 2024.4.5
21
170
 
@@ -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>
@@ -0,0 +1,80 @@
1
+ import {Link} from '@remix-run/react';
2
+ import {type VariantOption, VariantSelector} from '@shopify/hydrogen';
3
+ import type {
4
+ ProductFragment,
5
+ ProductVariantFragment,
6
+ } from 'storefrontapi.generated';
7
+ import {AddToCartButton} from '~/components/AddToCartButton';
8
+ import {useAside} from '~/components/Aside';
9
+
10
+ export function ProductForm({
11
+ product,
12
+ selectedVariant,
13
+ variants,
14
+ }: {
15
+ product: ProductFragment;
16
+ selectedVariant: ProductFragment['selectedVariant'];
17
+ variants: Array<ProductVariantFragment>;
18
+ }) {
19
+ const {open} = useAside();
20
+ return (
21
+ <div className="product-form">
22
+ <VariantSelector
23
+ handle={product.handle}
24
+ options={product.options.filter((option) => option.values.length > 1)}
25
+ variants={variants}
26
+ >
27
+ {({option}) => <ProductOptions key={option.name} option={option} />}
28
+ </VariantSelector>
29
+ <br />
30
+ <AddToCartButton
31
+ disabled={!selectedVariant || !selectedVariant.availableForSale}
32
+ onClick={() => {
33
+ open('cart');
34
+ }}
35
+ lines={
36
+ selectedVariant
37
+ ? [
38
+ {
39
+ merchandiseId: selectedVariant.id,
40
+ quantity: 1,
41
+ selectedVariant,
42
+ },
43
+ ]
44
+ : []
45
+ }
46
+ >
47
+ {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
48
+ </AddToCartButton>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function ProductOptions({option}: {option: VariantOption}) {
54
+ return (
55
+ <div className="product-options" key={option.name}>
56
+ <h5>{option.name}</h5>
57
+ <div className="product-options-grid">
58
+ {option.values.map(({value, isAvailable, isActive, to}) => {
59
+ return (
60
+ <Link
61
+ className="product-options-item"
62
+ key={option.name + value}
63
+ prefetch="intent"
64
+ preventScrollReset
65
+ replace
66
+ to={to}
67
+ style={{
68
+ border: isActive ? '1px solid black' : '1px solid transparent',
69
+ opacity: isAvailable ? 1 : 0.3,
70
+ }}
71
+ >
72
+ {value}
73
+ </Link>
74
+ );
75
+ })}
76
+ </div>
77
+ <br />
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,23 @@
1
+ import type {ProductVariantFragment} from 'storefrontapi.generated';
2
+ import {Image} from '@shopify/hydrogen';
3
+
4
+ export function ProductImage({
5
+ image,
6
+ }: {
7
+ image: ProductVariantFragment['image'];
8
+ }) {
9
+ if (!image) {
10
+ return <div className="product-image" />;
11
+ }
12
+ return (
13
+ <div className="product-image">
14
+ <Image
15
+ alt={image.altText || 'Product Image'}
16
+ aspectRatio="1/1"
17
+ data={image}
18
+ key={image.id}
19
+ sizes="(min-width: 45em) 50vw, 100vw"
20
+ />
21
+ </div>
22
+ );
23
+ }