@shopify/cli-hydrogen 3.26.0 → 4.0.0-alpha.1

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 (249) hide show
  1. package/dist/commands/hydrogen/build.js +89 -0
  2. package/dist/commands/hydrogen/dev.js +116 -0
  3. package/dist/commands/hydrogen/init.js +42 -0
  4. package/dist/commands/hydrogen/preview.js +34 -0
  5. package/dist/hooks/init.js +21 -0
  6. package/dist/templates/demo-store/.editorconfig +8 -0
  7. package/dist/templates/demo-store/.eslintignore +4 -0
  8. package/dist/templates/demo-store/.eslintrc.js +16 -0
  9. package/dist/templates/demo-store/.graphqlrc.yml +1 -0
  10. package/dist/templates/demo-store/.prettierignore +2 -0
  11. package/dist/templates/demo-store/.turbo/turbo-build.log +13 -0
  12. package/dist/templates/demo-store/app/components/AccountAddressBook.tsx +97 -0
  13. package/dist/templates/demo-store/app/components/AccountDetails.tsx +41 -0
  14. package/dist/templates/demo-store/app/components/AddToCartButton.tsx +42 -0
  15. package/dist/templates/demo-store/app/components/Breadcrumbs.tsx +36 -0
  16. package/dist/templates/demo-store/app/components/Button.tsx +56 -0
  17. package/dist/templates/demo-store/app/components/Cart.tsx +431 -0
  18. package/dist/templates/demo-store/app/components/CartLoading.tsx +50 -0
  19. package/dist/templates/demo-store/app/components/CountrySelector.tsx +180 -0
  20. package/dist/templates/demo-store/app/components/Drawer.tsx +115 -0
  21. package/dist/templates/demo-store/app/components/FeaturedCollections.tsx +54 -0
  22. package/dist/templates/demo-store/app/components/FeaturedProducts.tsx +116 -0
  23. package/dist/templates/demo-store/app/components/FeaturedSection.tsx +39 -0
  24. package/dist/templates/demo-store/app/components/GenericError.tsx +58 -0
  25. package/dist/templates/demo-store/app/components/Grid.tsx +44 -0
  26. package/dist/templates/demo-store/app/components/Hero.tsx +136 -0
  27. package/dist/templates/demo-store/app/components/Icon.tsx +253 -0
  28. package/dist/templates/demo-store/app/components/Input.tsx +24 -0
  29. package/dist/templates/demo-store/app/components/Layout.tsx +492 -0
  30. package/dist/templates/demo-store/app/components/Link.tsx +46 -0
  31. package/dist/templates/demo-store/app/components/Modal.tsx +46 -0
  32. package/dist/templates/demo-store/app/components/NotFound.tsx +22 -0
  33. package/dist/templates/demo-store/app/components/OrderCard.tsx +85 -0
  34. package/dist/templates/demo-store/app/components/Pagination.tsx +277 -0
  35. package/dist/templates/demo-store/app/components/ProductCard.tsx +146 -0
  36. package/dist/templates/demo-store/app/components/ProductGallery.tsx +114 -0
  37. package/dist/templates/demo-store/app/components/ProductGrid.tsx +93 -0
  38. package/dist/templates/demo-store/app/components/ProductSwimlane.tsx +30 -0
  39. package/dist/templates/demo-store/app/components/Skeleton.tsx +24 -0
  40. package/dist/templates/demo-store/app/components/SortFilter.tsx +411 -0
  41. package/dist/templates/demo-store/app/components/Text.tsx +192 -0
  42. package/dist/templates/demo-store/app/components/index.ts +28 -0
  43. package/dist/templates/demo-store/app/data/countries.ts +194 -0
  44. package/dist/templates/demo-store/app/data/index.ts +1037 -0
  45. package/dist/templates/demo-store/app/entry.client.tsx +4 -0
  46. package/dist/templates/demo-store/app/entry.server.tsx +26 -0
  47. package/dist/templates/demo-store/app/hooks/useCartFetchers.tsx +14 -0
  48. package/dist/templates/demo-store/app/hooks/useIsHydrated.tsx +12 -0
  49. package/dist/templates/demo-store/app/lib/const.ts +10 -0
  50. package/dist/templates/demo-store/app/lib/placeholders.ts +242 -0
  51. package/dist/templates/demo-store/app/lib/seo/common.tsx +324 -0
  52. package/dist/templates/demo-store/app/lib/seo/debugger.tsx +175 -0
  53. package/dist/templates/demo-store/app/lib/seo/image.tsx +32 -0
  54. package/dist/templates/demo-store/app/lib/seo/index.ts +4 -0
  55. package/dist/templates/demo-store/app/lib/seo/seo.tsx +24 -0
  56. package/dist/templates/demo-store/app/lib/seo/types.ts +70 -0
  57. package/dist/templates/demo-store/app/lib/session.server.ts +57 -0
  58. package/dist/templates/demo-store/app/lib/type.ts +21 -0
  59. package/dist/templates/demo-store/app/lib/utils.ts +310 -0
  60. package/dist/templates/demo-store/app/root.tsx +282 -0
  61. package/dist/templates/demo-store/app/routes/$.tsx +7 -0
  62. package/dist/templates/demo-store/app/routes/$lang/$.tsx +1 -0
  63. package/dist/templates/demo-store/app/routes/$lang/[robots.txt].tsx +1 -0
  64. package/dist/templates/demo-store/app/routes/$lang/[sitemap.xml].tsx +1 -0
  65. package/dist/templates/demo-store/app/routes/$lang/account/__private/address/$id.tsx +1 -0
  66. package/dist/templates/demo-store/app/routes/$lang/account/__private/edit.tsx +1 -0
  67. package/dist/templates/demo-store/app/routes/$lang/account/__private/logout.ts +1 -0
  68. package/dist/templates/demo-store/app/routes/$lang/account/__private/orders.$id.tsx +1 -0
  69. package/dist/templates/demo-store/app/routes/$lang/account/__public/activate.$id.$activationToken.tsx +6 -0
  70. package/dist/templates/demo-store/app/routes/$lang/account/__public/login.tsx +7 -0
  71. package/dist/templates/demo-store/app/routes/$lang/account/__public/recover.tsx +1 -0
  72. package/dist/templates/demo-store/app/routes/$lang/account/__public/register.tsx +6 -0
  73. package/dist/templates/demo-store/app/routes/$lang/account/__public/reset.$id.$resetToken.tsx +5 -0
  74. package/dist/templates/demo-store/app/routes/$lang/account.tsx +1 -0
  75. package/dist/templates/demo-store/app/routes/$lang/api/countries.tsx +1 -0
  76. package/dist/templates/demo-store/app/routes/$lang/api/products.tsx +1 -0
  77. package/dist/templates/demo-store/app/routes/$lang/cart.tsx +1 -0
  78. package/dist/templates/demo-store/app/routes/$lang/collections/$collectionHandle.tsx +6 -0
  79. package/dist/templates/demo-store/app/routes/$lang/collections/all.tsx +1 -0
  80. package/dist/templates/demo-store/app/routes/$lang/collections/index.tsx +1 -0
  81. package/dist/templates/demo-store/app/routes/$lang/featured-products.tsx +1 -0
  82. package/dist/templates/demo-store/app/routes/$lang/index.tsx +7 -0
  83. package/dist/templates/demo-store/app/routes/$lang/journal/$journalHandle.tsx +7 -0
  84. package/dist/templates/demo-store/app/routes/$lang/journal/index.tsx +1 -0
  85. package/dist/templates/demo-store/app/routes/$lang/og-image.tsx +1 -0
  86. package/dist/templates/demo-store/app/routes/$lang/pages/$pageHandle.tsx +1 -0
  87. package/dist/templates/demo-store/app/routes/$lang/policies/$policyHandle.tsx +1 -0
  88. package/dist/templates/demo-store/app/routes/$lang/policies/index.tsx +1 -0
  89. package/dist/templates/demo-store/app/routes/$lang/products/$productHandle.tsx +6 -0
  90. package/dist/templates/demo-store/app/routes/$lang/products/index.tsx +1 -0
  91. package/dist/templates/demo-store/app/routes/$lang/search.tsx +6 -0
  92. package/dist/templates/demo-store/app/routes/[robots.txt].tsx +40 -0
  93. package/dist/templates/demo-store/app/routes/[sitemap.xml].tsx +198 -0
  94. package/dist/templates/demo-store/app/routes/account/__private/address/$id.tsx +320 -0
  95. package/dist/templates/demo-store/app/routes/account/__private/edit.tsx +273 -0
  96. package/dist/templates/demo-store/app/routes/account/__private/logout.ts +29 -0
  97. package/dist/templates/demo-store/app/routes/account/__private/orders.$id.tsx +324 -0
  98. package/dist/templates/demo-store/app/routes/account/__public/activate.$id.$activationToken.tsx +218 -0
  99. package/dist/templates/demo-store/app/routes/account/__public/login.tsx +197 -0
  100. package/dist/templates/demo-store/app/routes/account/__public/recover.tsx +144 -0
  101. package/dist/templates/demo-store/app/routes/account/__public/register.tsx +184 -0
  102. package/dist/templates/demo-store/app/routes/account/__public/reset.$id.$resetToken.tsx +214 -0
  103. package/dist/templates/demo-store/app/routes/account.tsx +191 -0
  104. package/dist/templates/demo-store/app/routes/api/countries.tsx +22 -0
  105. package/dist/templates/demo-store/app/routes/api/products.tsx +116 -0
  106. package/dist/templates/demo-store/app/routes/cart.tsx +498 -0
  107. package/dist/templates/demo-store/app/routes/collections/$collectionHandle.tsx +308 -0
  108. package/dist/templates/demo-store/app/routes/collections/all.tsx +5 -0
  109. package/dist/templates/demo-store/app/routes/collections/index.tsx +195 -0
  110. package/dist/templates/demo-store/app/routes/discounts.$code.tsx +60 -0
  111. package/dist/templates/demo-store/app/routes/featured-products.tsx +58 -0
  112. package/dist/templates/demo-store/app/routes/index.tsx +254 -0
  113. package/dist/templates/demo-store/app/routes/journal/$journalHandle.tsx +147 -0
  114. package/dist/templates/demo-store/app/routes/journal/index.tsx +150 -0
  115. package/dist/templates/demo-store/app/routes/og-image.tsx +19 -0
  116. package/dist/templates/demo-store/app/routes/pages/$pageHandle.tsx +82 -0
  117. package/dist/templates/demo-store/app/routes/policies/$policyHandle.tsx +117 -0
  118. package/dist/templates/demo-store/app/routes/policies/index.tsx +104 -0
  119. package/dist/templates/demo-store/app/routes/products/$productHandle.tsx +561 -0
  120. package/dist/templates/demo-store/app/routes/products/index.tsx +155 -0
  121. package/dist/templates/demo-store/app/routes/search.tsx +205 -0
  122. package/dist/templates/demo-store/app/styles/custom-font.css +13 -0
  123. package/dist/templates/demo-store/package-lock.json +25515 -0
  124. package/dist/templates/demo-store/package.json +67 -0
  125. package/dist/templates/demo-store/playwright.config.ts +109 -0
  126. package/dist/templates/demo-store/postcss.config.js +10 -0
  127. package/dist/templates/demo-store/public/favicon.svg +28 -0
  128. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-Text.woff2 +0 -0
  129. package/dist/templates/demo-store/public/fonts/IBMPlexSerif-TextItalic.woff2 +0 -0
  130. package/dist/templates/demo-store/remix.config.js +12 -0
  131. package/dist/templates/demo-store/remix.env.d.ts +34 -0
  132. package/dist/templates/demo-store/remix.init/index.ts +15 -0
  133. package/dist/templates/demo-store/remix.init/package.json +7 -0
  134. package/dist/templates/demo-store/server.ts +87 -0
  135. package/dist/templates/demo-store/styles/app.css +182 -0
  136. package/dist/templates/demo-store/tailwind.config.js +70 -0
  137. package/dist/templates/demo-store/tests/cart.test.ts +70 -0
  138. package/dist/templates/demo-store/tests/seo.test.ts +36 -0
  139. package/dist/templates/demo-store/tests/utils.ts +100 -0
  140. package/dist/templates/demo-store/tsconfig.json +26 -0
  141. package/dist/templates/hello-world/.eslintignore +4 -0
  142. package/dist/templates/hello-world/.eslintrc.js +6 -0
  143. package/dist/templates/hello-world/.graphqlrc.yml +1 -0
  144. package/dist/templates/hello-world/.turbo/turbo-build.log +9 -0
  145. package/dist/templates/hello-world/README.md +20 -0
  146. package/dist/templates/hello-world/app/components/Layout.tsx +15 -0
  147. package/dist/templates/hello-world/app/components/index.ts +1 -0
  148. package/dist/templates/hello-world/app/entry.client.tsx +4 -0
  149. package/dist/templates/hello-world/app/entry.server.tsx +21 -0
  150. package/dist/templates/hello-world/app/root.tsx +212 -0
  151. package/dist/templates/hello-world/app/routes/index.tsx +7 -0
  152. package/dist/templates/hello-world/app/styles/app.css +38 -0
  153. package/dist/templates/hello-world/package-lock.json +27641 -0
  154. package/dist/templates/hello-world/package.json +41 -0
  155. package/dist/templates/hello-world/public/favicon.svg +28 -0
  156. package/dist/templates/hello-world/remix.env.d.ts +29 -0
  157. package/dist/templates/hello-world/server.ts +127 -0
  158. package/dist/templates/hello-world/tsconfig.json +25 -0
  159. package/dist/utils/config.js +81 -0
  160. package/dist/utils/flags.js +15 -0
  161. package/dist/utils/log.js +20 -0
  162. package/dist/utils/mini-oxygen.js +70 -0
  163. package/package.json +27 -64
  164. package/tmp-create-app.mjs +29 -0
  165. package/LICENSE +0 -8
  166. package/README.md +0 -61
  167. package/dist/cli/commands/hydrogen/add/eslint.d.ts +0 -11
  168. package/dist/cli/commands/hydrogen/add/eslint.js +0 -26
  169. package/dist/cli/commands/hydrogen/add/eslint.js.map +0 -1
  170. package/dist/cli/commands/hydrogen/add/tailwind.d.ts +0 -11
  171. package/dist/cli/commands/hydrogen/add/tailwind.js +0 -26
  172. package/dist/cli/commands/hydrogen/add/tailwind.js.map +0 -1
  173. package/dist/cli/commands/hydrogen/build.d.ts +0 -14
  174. package/dist/cli/commands/hydrogen/build.js +0 -49
  175. package/dist/cli/commands/hydrogen/build.js.map +0 -1
  176. package/dist/cli/commands/hydrogen/deploy.d.ts +0 -19
  177. package/dist/cli/commands/hydrogen/deploy.js +0 -58
  178. package/dist/cli/commands/hydrogen/deploy.js.map +0 -1
  179. package/dist/cli/commands/hydrogen/dev.d.ts +0 -13
  180. package/dist/cli/commands/hydrogen/dev.js +0 -31
  181. package/dist/cli/commands/hydrogen/dev.js.map +0 -1
  182. package/dist/cli/commands/hydrogen/info.d.ts +0 -12
  183. package/dist/cli/commands/hydrogen/info.js +0 -28
  184. package/dist/cli/commands/hydrogen/info.js.map +0 -1
  185. package/dist/cli/commands/hydrogen/preview.d.ts +0 -13
  186. package/dist/cli/commands/hydrogen/preview.js +0 -46
  187. package/dist/cli/commands/hydrogen/preview.js.map +0 -1
  188. package/dist/cli/constants.d.ts +0 -15
  189. package/dist/cli/constants.js +0 -16
  190. package/dist/cli/constants.js.map +0 -1
  191. package/dist/cli/flags.d.ts +0 -4
  192. package/dist/cli/flags.js +0 -16
  193. package/dist/cli/flags.js.map +0 -1
  194. package/dist/cli/models/hydrogen.d.ts +0 -22
  195. package/dist/cli/models/hydrogen.js +0 -82
  196. package/dist/cli/models/hydrogen.js.map +0 -1
  197. package/dist/cli/prompts/git-init.d.ts +0 -1
  198. package/dist/cli/prompts/git-init.js +0 -16
  199. package/dist/cli/prompts/git-init.js.map +0 -1
  200. package/dist/cli/services/build/check-lockfile.d.ts +0 -3
  201. package/dist/cli/services/build/check-lockfile.js +0 -80
  202. package/dist/cli/services/build/check-lockfile.js.map +0 -1
  203. package/dist/cli/services/build.d.ts +0 -14
  204. package/dist/cli/services/build.js +0 -44
  205. package/dist/cli/services/build.js.map +0 -1
  206. package/dist/cli/services/deploy/config.d.ts +0 -4
  207. package/dist/cli/services/deploy/config.js +0 -49
  208. package/dist/cli/services/deploy/config.js.map +0 -1
  209. package/dist/cli/services/deploy/error.d.ts +0 -4
  210. package/dist/cli/services/deploy/error.js +0 -11
  211. package/dist/cli/services/deploy/error.js.map +0 -1
  212. package/dist/cli/services/deploy/graphql/create_deployment.d.ts +0 -10
  213. package/dist/cli/services/deploy/graphql/create_deployment.js +0 -15
  214. package/dist/cli/services/deploy/graphql/create_deployment.js.map +0 -1
  215. package/dist/cli/services/deploy/graphql/upload_deployment.d.ts +0 -1
  216. package/dist/cli/services/deploy/graphql/upload_deployment.js +0 -16
  217. package/dist/cli/services/deploy/graphql/upload_deployment.js.map +0 -1
  218. package/dist/cli/services/deploy/types.d.ts +0 -37
  219. package/dist/cli/services/deploy/types.js +0 -2
  220. package/dist/cli/services/deploy/types.js.map +0 -1
  221. package/dist/cli/services/deploy/upload.d.ts +0 -5
  222. package/dist/cli/services/deploy/upload.js +0 -81
  223. package/dist/cli/services/deploy/upload.js.map +0 -1
  224. package/dist/cli/services/deploy.d.ts +0 -2
  225. package/dist/cli/services/deploy.js +0 -103
  226. package/dist/cli/services/deploy.js.map +0 -1
  227. package/dist/cli/services/dev/check-version.d.ts +0 -1
  228. package/dist/cli/services/dev/check-version.js +0 -30
  229. package/dist/cli/services/dev/check-version.js.map +0 -1
  230. package/dist/cli/services/dev.d.ts +0 -10
  231. package/dist/cli/services/dev.js +0 -36
  232. package/dist/cli/services/dev.js.map +0 -1
  233. package/dist/cli/services/eslint.d.ts +0 -8
  234. package/dist/cli/services/eslint.js +0 -74
  235. package/dist/cli/services/eslint.js.map +0 -1
  236. package/dist/cli/services/info.d.ts +0 -7
  237. package/dist/cli/services/info.js +0 -131
  238. package/dist/cli/services/info.js.map +0 -1
  239. package/dist/cli/services/preview.d.ts +0 -12
  240. package/dist/cli/services/preview.js +0 -63
  241. package/dist/cli/services/preview.js.map +0 -1
  242. package/dist/cli/services/tailwind.d.ts +0 -9
  243. package/dist/cli/services/tailwind.js +0 -103
  244. package/dist/cli/services/tailwind.js.map +0 -1
  245. package/dist/cli/utilities/load-config.d.ts +0 -5
  246. package/dist/cli/utilities/load-config.js +0 -6
  247. package/dist/cli/utilities/load-config.js.map +0 -1
  248. package/dist/tsconfig.tsbuildinfo +0 -1
  249. package/oclif.manifest.json +0 -1
@@ -0,0 +1,431 @@
1
+ import clsx from 'clsx';
2
+ import {useRef, useState} from 'react';
3
+ import {useScroll} from 'react-use';
4
+ import {flattenConnection, Image, Money} from '@shopify/hydrogen-react';
5
+ import {
6
+ Button,
7
+ Heading,
8
+ IconRemove,
9
+ Text,
10
+ Link,
11
+ FeaturedProducts,
12
+ } from '~/components';
13
+ import {getInputStyleClasses} from '~/lib/utils';
14
+ import type {
15
+ Cart as CartType,
16
+ CartCost,
17
+ CartLine,
18
+ CartLineUpdateInput,
19
+ } from '@shopify/hydrogen-react/storefront-api-types';
20
+ import {useFetcher} from '@remix-run/react';
21
+ import {CartAction} from '~/lib/type';
22
+
23
+ type Layouts = 'page' | 'drawer';
24
+
25
+ export function Cart({
26
+ layout,
27
+ onClose,
28
+ cart,
29
+ }: {
30
+ layout: Layouts;
31
+ onClose?: () => void;
32
+ cart: CartType | null;
33
+ }) {
34
+ const linesCount = Boolean(cart?.lines?.edges?.length || 0);
35
+
36
+ return (
37
+ <>
38
+ <CartEmpty hidden={linesCount} onClose={onClose} layout={layout} />
39
+ <CartDetails cart={cart} layout={layout} />
40
+ </>
41
+ );
42
+ }
43
+
44
+ export function CartDetails({
45
+ layout,
46
+ cart,
47
+ }: {
48
+ layout: Layouts;
49
+ cart: CartType | null;
50
+ }) {
51
+ // @todo: get optimistic cart cost
52
+ const isZeroCost = !cart || cart?.cost?.subtotalAmount?.amount === '0.0';
53
+
54
+ const container = {
55
+ drawer: 'grid grid-cols-1 h-screen-no-nav grid-rows-[1fr_auto]',
56
+ page: 'w-full pb-12 grid md:grid-cols-2 md:items-start gap-8 md:gap-8 lg:gap-12',
57
+ };
58
+
59
+ return (
60
+ <div className={container[layout]}>
61
+ <CartLines lines={cart?.lines} layout={layout} />
62
+ {!isZeroCost && (
63
+ <CartSummary cost={cart.cost} layout={layout}>
64
+ <CartDiscounts discountCodes={cart.discountCodes} />
65
+ <CartCheckoutActions checkoutUrl={cart.checkoutUrl} />
66
+ </CartSummary>
67
+ )}
68
+ </div>
69
+ );
70
+ }
71
+
72
+ /**
73
+ * Temporary discount UI
74
+ * @param discountCodes the current discount codes applied to the cart
75
+ * @todo rework when a design is ready
76
+ */
77
+ function CartDiscounts({
78
+ discountCodes,
79
+ }: {
80
+ discountCodes: CartType['discountCodes'];
81
+ }) {
82
+ const codes = discountCodes?.map(({code}) => code).join(', ') || null;
83
+
84
+ return (
85
+ <>
86
+ {/* Have existing discount, display it with a remove option */}
87
+ <dl className={clsx(codes ? 'grid' : 'hidden')}>
88
+ <div className="flex items-center justify-between font-medium">
89
+ <Text as="dt">Discount(s)</Text>
90
+ <div className="flex items-center justify-between">
91
+ <UpdateDiscountForm>
92
+ <button>
93
+ <IconRemove
94
+ aria-hidden="true"
95
+ style={{height: 18, marginRight: 4}}
96
+ />
97
+ </button>
98
+ </UpdateDiscountForm>
99
+ <Text as="dd">{codes}</Text>
100
+ </div>
101
+ </div>
102
+ </dl>
103
+
104
+ {/* No discounts, show an input to apply a discount */}
105
+ <UpdateDiscountForm>
106
+ <div
107
+ className={clsx(
108
+ codes ? 'hidden' : 'flex',
109
+ 'items-center justify-between',
110
+ )}
111
+ >
112
+ <input
113
+ className={getInputStyleClasses()}
114
+ type="text"
115
+ name="discountCode"
116
+ />
117
+ <button className="w-[150px] flex justify-end">
118
+ <Text size="fine">Apply Discount</Text>
119
+ </button>
120
+ </div>
121
+ </UpdateDiscountForm>
122
+ </>
123
+ );
124
+ }
125
+
126
+ function UpdateDiscountForm({children}: {children: React.ReactNode}) {
127
+ const fetcher = useFetcher();
128
+ return (
129
+ <fetcher.Form action="/cart" method="post">
130
+ <input
131
+ type="hidden"
132
+ name="cartAction"
133
+ value={CartAction.UPDATE_DISCOUNT}
134
+ />
135
+ {children}
136
+ </fetcher.Form>
137
+ );
138
+ }
139
+
140
+ function CartLines({
141
+ layout = 'drawer',
142
+ lines: cartLines,
143
+ }: {
144
+ layout: Layouts;
145
+ lines: CartType['lines'] | undefined;
146
+ }) {
147
+ const currentLines = cartLines ? flattenConnection(cartLines) : [];
148
+ const scrollRef = useRef(null);
149
+ const {y} = useScroll(scrollRef);
150
+
151
+ const className = clsx([
152
+ y > 0 ? 'border-t' : '',
153
+ layout === 'page'
154
+ ? 'flex-grow md:translate-y-4'
155
+ : 'px-6 pb-6 sm-max:pt-2 overflow-auto transition md:px-12',
156
+ ]);
157
+
158
+ return (
159
+ <section
160
+ ref={scrollRef}
161
+ aria-labelledby="cart-contents"
162
+ className={className}
163
+ >
164
+ <ul className="grid gap-6 md:gap-10">
165
+ {currentLines.map((line) => (
166
+ <CartLineItem key={line.id} line={line as CartLine} />
167
+ ))}
168
+ </ul>
169
+ </section>
170
+ );
171
+ }
172
+
173
+ function CartCheckoutActions({checkoutUrl}: {checkoutUrl: string}) {
174
+ if (!checkoutUrl) return null;
175
+
176
+ return (
177
+ <div className="flex flex-col mt-2">
178
+ <a href={checkoutUrl} target="_self">
179
+ <Button as="span" width="full">
180
+ Continue to Checkout
181
+ </Button>
182
+ </a>
183
+ {/* @todo: <CartShopPayButton cart={cart} /> */}
184
+ </div>
185
+ );
186
+ }
187
+
188
+ function CartSummary({
189
+ cost,
190
+ layout,
191
+ children = null,
192
+ }: {
193
+ children?: React.ReactNode;
194
+ cost: CartCost;
195
+ layout: Layouts;
196
+ }) {
197
+ const summary = {
198
+ drawer: 'grid gap-4 p-6 border-t md:px-12',
199
+ page: 'sticky top-nav grid gap-6 p-4 md:px-6 md:translate-y-4 bg-primary/5 rounded w-full',
200
+ };
201
+
202
+ return (
203
+ <section aria-labelledby="summary-heading" className={summary[layout]}>
204
+ <h2 id="summary-heading" className="sr-only">
205
+ Order summary
206
+ </h2>
207
+ <dl className="grid">
208
+ <div className="flex items-center justify-between font-medium">
209
+ <Text as="dt">Subtotal</Text>
210
+ <Text as="dd" data-test="subtotal">
211
+ {cost?.subtotalAmount?.amount ? (
212
+ <Money data={cost?.subtotalAmount} />
213
+ ) : (
214
+ '-'
215
+ )}
216
+ </Text>
217
+ </div>
218
+ </dl>
219
+ {children}
220
+ </section>
221
+ );
222
+ }
223
+
224
+ function CartLineItem({line}: {line: CartLine}) {
225
+ if (!line?.id) return null;
226
+
227
+ const {id, quantity, merchandise} = line;
228
+
229
+ if (typeof quantity === 'undefined' || !merchandise?.product) return null;
230
+
231
+ return (
232
+ <li key={id} className="flex gap-4">
233
+ <div className="flex-shrink">
234
+ {merchandise.image && (
235
+ <Image
236
+ width={220}
237
+ height={220}
238
+ data={merchandise.image}
239
+ className="object-cover object-center w-24 h-24 border rounded md:w-28 md:h-28"
240
+ alt={merchandise.title}
241
+ />
242
+ )}
243
+ </div>
244
+
245
+ <div className="flex justify-between flex-grow">
246
+ <div className="grid gap-2">
247
+ <Heading as="h3" size="copy">
248
+ {merchandise?.product?.handle ? (
249
+ <Link to={`/products/${merchandise.product.handle}`}>
250
+ {merchandise?.product?.title || ''}
251
+ </Link>
252
+ ) : (
253
+ <Text>{merchandise?.product?.title || ''}</Text>
254
+ )}
255
+ </Heading>
256
+
257
+ <div className="grid pb-2">
258
+ {(merchandise?.selectedOptions || []).map((option) => (
259
+ <Text color="subtle" key={option.name}>
260
+ {option.name}: {option.value}
261
+ </Text>
262
+ ))}
263
+ </div>
264
+
265
+ <div className="flex items-center gap-2">
266
+ <div className="flex justify-start text-copy">
267
+ <CartLineQuantityAdjust line={line} />
268
+ </div>
269
+ <ItemRemoveButton lineIds={[id]} />
270
+ </div>
271
+ </div>
272
+ <Text>
273
+ <CartLinePrice line={line} as="span" />
274
+ </Text>
275
+ </div>
276
+ </li>
277
+ );
278
+ }
279
+
280
+ function ItemRemoveButton({lineIds}: {lineIds: CartLine['id'][]}) {
281
+ const fetcher = useFetcher();
282
+
283
+ return (
284
+ <fetcher.Form action="/cart" method="post">
285
+ <input
286
+ type="hidden"
287
+ name="cartAction"
288
+ value={CartAction.REMOVE_FROM_CART}
289
+ />
290
+ <input type="hidden" name="linesIds" value={JSON.stringify(lineIds)} />
291
+ <button
292
+ className="flex items-center justify-center w-10 h-10 border rounded"
293
+ type="submit"
294
+ >
295
+ <span className="sr-only">Remove</span>
296
+ <IconRemove aria-hidden="true" />
297
+ </button>
298
+ </fetcher.Form>
299
+ );
300
+ }
301
+
302
+ function CartLineQuantityAdjust({line}: {line: CartLine}) {
303
+ if (!line || typeof line?.quantity === 'undefined') return null;
304
+ const {id: lineId, quantity} = line;
305
+ const prevQuantity = Number(Math.max(0, quantity - 1).toFixed(0));
306
+ const nextQuantity = Number((quantity + 1).toFixed(0));
307
+
308
+ return (
309
+ <>
310
+ <label htmlFor={`quantity-${lineId}`} className="sr-only">
311
+ Quantity, {quantity}
312
+ </label>
313
+ <div className="flex items-center border rounded">
314
+ <UpdateCartButton lines={[{id: lineId, quantity: prevQuantity}]}>
315
+ <button
316
+ name="decrease-quantity"
317
+ aria-label="Decrease quantity"
318
+ className="w-10 h-10 transition text-primary/50 hover:text-primary disabled:text-primary/10"
319
+ value={prevQuantity}
320
+ disabled={quantity <= 1}
321
+ >
322
+ <span>&#8722;</span>
323
+ </button>
324
+ </UpdateCartButton>
325
+
326
+ <div className="px-2 text-center" data-test="item-quantity">
327
+ {quantity}
328
+ </div>
329
+
330
+ <UpdateCartButton lines={[{id: lineId, quantity: nextQuantity}]}>
331
+ <button
332
+ className="w-10 h-10 transition text-primary/50 hover:text-primary"
333
+ name="increase-quantity"
334
+ value={nextQuantity}
335
+ aria-label="Increase quantity"
336
+ >
337
+ <span>&#43;</span>
338
+ </button>
339
+ </UpdateCartButton>
340
+ </div>
341
+ </>
342
+ );
343
+ }
344
+
345
+ function UpdateCartButton({
346
+ children,
347
+ lines,
348
+ }: {
349
+ children: React.ReactNode;
350
+ lines: CartLineUpdateInput[];
351
+ }) {
352
+ const fetcher = useFetcher();
353
+
354
+ return (
355
+ <fetcher.Form action="/cart" method="post">
356
+ <input type="hidden" name="cartAction" value={CartAction.UPDATE_CART} />
357
+ <input type="hidden" name="lines" value={JSON.stringify(lines)} />
358
+ {children}
359
+ </fetcher.Form>
360
+ );
361
+ }
362
+
363
+ function CartLinePrice({
364
+ line,
365
+ priceType = 'regular',
366
+ ...passthroughProps
367
+ }: {
368
+ line: CartLine;
369
+ priceType?: 'regular' | 'compareAt';
370
+ [key: string]: any;
371
+ }) {
372
+ if (!line?.cost?.amountPerQuantity || !line?.cost?.totalAmount) return null;
373
+
374
+ const moneyV2 =
375
+ priceType === 'regular'
376
+ ? line.cost.totalAmount
377
+ : line.cost.compareAtAmountPerQuantity;
378
+
379
+ if (moneyV2 == null) {
380
+ return null;
381
+ }
382
+
383
+ return <Money withoutTrailingZeros {...passthroughProps} data={moneyV2} />;
384
+ }
385
+
386
+ export function CartEmpty({
387
+ hidden = false,
388
+ layout = 'drawer',
389
+ onClose,
390
+ }: {
391
+ hidden: boolean;
392
+ layout?: Layouts;
393
+ onClose?: () => void;
394
+ }) {
395
+ const scrollRef = useRef(null);
396
+ const {y} = useScroll(scrollRef);
397
+
398
+ const container = {
399
+ drawer: clsx([
400
+ 'content-start gap-4 px-6 pb-8 transition overflow-y-scroll md:gap-12 md:px-12 h-screen-no-nav md:pb-12',
401
+ y > 0 ? 'border-t' : '',
402
+ ]),
403
+ page: clsx([
404
+ hidden ? '' : 'grid',
405
+ `pb-12 w-full md:items-start gap-4 md:gap-8 lg:gap-12`,
406
+ ]),
407
+ };
408
+
409
+ return (
410
+ <div ref={scrollRef} className={container[layout]} hidden={hidden}>
411
+ <section className="grid gap-6">
412
+ <Text format>
413
+ Looks like you haven&rsquo;t added anything yet, let&rsquo;s get you
414
+ started!
415
+ </Text>
416
+ <div>
417
+ <Button onClick={onClose}>Continue shopping</Button>
418
+ </div>
419
+ </section>
420
+ <section className="grid gap-8 pt-4">
421
+ <FeaturedProducts
422
+ count={4}
423
+ heading="Shop Best Sellers"
424
+ layout={layout}
425
+ onClose={onClose}
426
+ sortKey="BEST_SELLING"
427
+ />
428
+ </section>
429
+ </div>
430
+ );
431
+ }
@@ -0,0 +1,50 @@
1
+ export function CartLoading() {
2
+ return (
3
+ <div className="flex w-full h-screen-no-nav justify-center items-center">
4
+ {/* @todo better spinner? */}
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width={38}
8
+ height={38}
9
+ viewBox="0 0 38 38"
10
+ >
11
+ <defs>
12
+ <linearGradient x1="8.042%" y1="0%" x2="65.682%" y2="23.865%" id="a">
13
+ <stop stopColor="#fff" stopOpacity={0} offset="0%" />
14
+ <stop stopColor="#fff" stopOpacity=".631" offset="63.146%" />
15
+ <stop stopColor="#fff" offset="100%" />
16
+ </linearGradient>
17
+ </defs>
18
+ <g fill="none" fillRule="evenodd">
19
+ <g transform="translate(1 1)">
20
+ <path
21
+ d="M36 18c0-9.94-8.06-18-18-18"
22
+ id="Oval-2"
23
+ stroke="url(#a)"
24
+ strokeWidth={2}
25
+ >
26
+ <animateTransform
27
+ attributeName="transform"
28
+ type="rotate"
29
+ from="0 18 18"
30
+ to="360 18 18"
31
+ dur="0.9s"
32
+ repeatCount="indefinite"
33
+ />
34
+ </path>
35
+ <circle fill="#fff" cx={36} cy={18} r={1}>
36
+ <animateTransform
37
+ attributeName="transform"
38
+ type="rotate"
39
+ from="0 18 18"
40
+ to="360 18 18"
41
+ dur="0.9s"
42
+ repeatCount="indefinite"
43
+ />
44
+ </circle>
45
+ </g>
46
+ </g>
47
+ </svg>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,180 @@
1
+ import {useFetcher, useLocation, useMatches} from '@remix-run/react';
2
+ import {Heading, Button, IconCheck} from '~/components';
3
+ import {useCallback, useEffect, useRef} from 'react';
4
+ import {useInView} from 'react-intersection-observer';
5
+ import {Localizations, Locale, CartAction} from '~/lib/type';
6
+ import {DEFAULT_LOCALE} from '~/lib/utils';
7
+ import clsx from 'clsx';
8
+ import {CartBuyerIdentityInput} from '@shopify/hydrogen-react/storefront-api-types';
9
+
10
+ export function CountrySelector() {
11
+ const [root] = useMatches();
12
+ const fetcher = useFetcher();
13
+ const closeRef = useRef<HTMLDetailsElement>(null);
14
+ const selectedLocale = root.data?.selectedLocale ?? DEFAULT_LOCALE;
15
+ const {pathname, search} = useLocation();
16
+ const pathWithoutLocale = `${pathname.replace(
17
+ selectedLocale.pathPrefix,
18
+ '',
19
+ )}${search}`;
20
+
21
+ const countries = (fetcher.data ?? {}) as Localizations;
22
+ const defaultLocale = countries?.['default'];
23
+ const defaultLocalePrefix = defaultLocale
24
+ ? `${defaultLocale?.language}-${defaultLocale?.country}`
25
+ : '';
26
+
27
+ const {ref, inView} = useInView({
28
+ threshold: 0,
29
+ triggerOnce: true,
30
+ });
31
+
32
+ const observerRef = useRef(null);
33
+ useEffect(() => {
34
+ ref(observerRef.current);
35
+ }, [ref, observerRef]);
36
+
37
+ // Get available countries list when in view
38
+ useEffect(() => {
39
+ if (!inView || fetcher.data || fetcher.state === 'loading') return;
40
+ fetcher.load('/api/countries');
41
+ }, [inView, fetcher]);
42
+
43
+ const closeDropdown = useCallback(() => {
44
+ closeRef.current?.removeAttribute('open');
45
+ }, []);
46
+
47
+ return (
48
+ <section
49
+ ref={observerRef}
50
+ className="grid gap-4 w-full md:max-w-[335px] md:ml-auto"
51
+ onMouseLeave={closeDropdown}
52
+ >
53
+ <Heading size="lead" className="cursor-default" as="h3">
54
+ Country
55
+ </Heading>
56
+ <div className="relative">
57
+ <details
58
+ className="border rounded border-contrast/30 dark:border-white open:round-b-none"
59
+ ref={closeRef}
60
+ >
61
+ <summary className="flex items-center justify-between w-full py-3 px-4 cursor-pointer">
62
+ {selectedLocale.label}
63
+ </summary>
64
+ <div className="overflow-auto border-t py-2 bg-contrast w-full max-h-36">
65
+ {countries &&
66
+ Object.keys(countries).map((countryPath) => {
67
+ const countryLocale = countries[countryPath];
68
+ const isSelected =
69
+ countryLocale.language === selectedLocale.language &&
70
+ countryLocale.country === selectedLocale.country;
71
+
72
+ const countryUrlPath = getCountryUrlPath({
73
+ countryLocale,
74
+ defaultLocalePrefix,
75
+ pathWithoutLocale,
76
+ });
77
+
78
+ return (
79
+ <Country
80
+ key={countryPath}
81
+ closeDropdown={closeDropdown}
82
+ countryUrlPath={countryUrlPath}
83
+ isSelected={isSelected}
84
+ countryLocale={countryLocale}
85
+ />
86
+ );
87
+ })}
88
+ </div>
89
+ </details>
90
+ </div>
91
+ </section>
92
+ );
93
+ }
94
+
95
+ function Country({
96
+ closeDropdown,
97
+ countryLocale,
98
+ countryUrlPath,
99
+ isSelected,
100
+ }: {
101
+ closeDropdown: () => void;
102
+ countryLocale: Locale;
103
+ countryUrlPath: string;
104
+ isSelected: boolean;
105
+ }) {
106
+ return (
107
+ <ChangeLocaleForm
108
+ key={countryLocale.country}
109
+ redirectTo={countryUrlPath}
110
+ buyerIdentity={{
111
+ countryCode: countryLocale.country,
112
+ }}
113
+ >
114
+ <Button
115
+ className={clsx([
116
+ 'text-contrast dark:text-primary',
117
+ 'bg-primary dark:bg-contrast w-full p-2 transition rounded flex justify-start',
118
+ 'items-center text-left cursor-pointer py-2 px-4',
119
+ ])}
120
+ type="submit"
121
+ variant="primary"
122
+ onClick={closeDropdown}
123
+ >
124
+ {countryLocale.label}
125
+ {isSelected ? (
126
+ <span className="ml-2">
127
+ <IconCheck />
128
+ </span>
129
+ ) : null}
130
+ </Button>
131
+ </ChangeLocaleForm>
132
+ );
133
+ }
134
+
135
+ function ChangeLocaleForm({
136
+ children,
137
+ buyerIdentity,
138
+ redirectTo,
139
+ }: {
140
+ children: React.ReactNode;
141
+ buyerIdentity: CartBuyerIdentityInput;
142
+ redirectTo: string;
143
+ }) {
144
+ const fetcher = useFetcher();
145
+
146
+ return (
147
+ <fetcher.Form action="/cart" method="post">
148
+ <input
149
+ type="hidden"
150
+ name="cartAction"
151
+ value={CartAction.UPDATE_BUYER_IDENTITY}
152
+ />
153
+ <input
154
+ type="hidden"
155
+ name="buyerIdentity"
156
+ value={JSON.stringify(buyerIdentity)}
157
+ />
158
+ <input type="hidden" name="redirectTo" value={redirectTo} />
159
+ {children}
160
+ </fetcher.Form>
161
+ );
162
+ }
163
+
164
+ function getCountryUrlPath({
165
+ countryLocale,
166
+ defaultLocalePrefix,
167
+ pathWithoutLocale,
168
+ }: {
169
+ countryLocale: Locale;
170
+ pathWithoutLocale: string;
171
+ defaultLocalePrefix: string;
172
+ }) {
173
+ let countryPrefixPath = '';
174
+ const countryLocalePrefix = `${countryLocale.language}-${countryLocale.country}`;
175
+
176
+ if (countryLocalePrefix !== defaultLocalePrefix) {
177
+ countryPrefixPath = `/${countryLocalePrefix.toLowerCase()}`;
178
+ }
179
+ return `${countryPrefixPath}${pathWithoutLocale}`;
180
+ }