create-magic-storefront 0.1.1 → 0.1.3

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 (109) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/skills/storefront-design/SKILL.md +19 -8
  3. package/template/.claude/skills/storefront-verify/SKILL.md +85 -0
  4. package/template/.claude/skills/vercel-react-best-practices/AGENTS.md +3810 -0
  5. package/template/.claude/skills/vercel-react-best-practices/README.md +123 -0
  6. package/template/.claude/skills/vercel-react-best-practices/SKILL.md +149 -0
  7. package/template/.claude/skills/vercel-react-best-practices/SOURCE.md +7 -0
  8. package/template/.claude/skills/vercel-react-best-practices/metadata.json +15 -0
  9. package/template/.claude/skills/vercel-react-best-practices/rules/_sections.md +46 -0
  10. package/template/.claude/skills/vercel-react-best-practices/rules/_template.md +28 -0
  11. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
  12. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
  13. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
  14. package/template/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
  15. package/template/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
  16. package/template/.claude/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
  17. package/template/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
  18. package/template/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
  19. package/template/.claude/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
  20. package/template/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
  21. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
  22. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
  23. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
  24. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
  25. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
  26. package/template/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
  27. package/template/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
  28. package/template/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
  29. package/template/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
  30. package/template/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
  31. package/template/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
  32. package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
  33. package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
  34. package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
  35. package/template/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
  36. package/template/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
  37. package/template/.claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
  38. package/template/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
  39. package/template/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
  40. package/template/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
  41. package/template/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
  42. package/template/.claude/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
  43. package/template/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
  44. package/template/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
  45. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
  46. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
  47. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
  48. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
  49. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
  50. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
  51. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
  52. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
  53. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
  54. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
  55. package/template/.claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
  56. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
  57. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
  58. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
  59. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
  60. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
  61. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
  62. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
  63. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
  64. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
  65. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
  66. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
  67. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
  68. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
  69. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
  70. package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
  71. package/template/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
  72. package/template/.claude/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
  73. package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
  74. package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
  75. package/template/.claude/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
  76. package/template/.claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
  77. package/template/.claude/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
  78. package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
  79. package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
  80. package/template/.claude/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
  81. package/template/.claude/skills/web-interface-guidelines/LICENSE +21 -0
  82. package/template/.claude/skills/web-interface-guidelines/SKILL.md +40 -0
  83. package/template/.claude/skills/web-interface-guidelines/SOURCE.md +9 -0
  84. package/template/.claude/skills/web-interface-guidelines/guidelines.md +155 -0
  85. package/template/AGENTS.md +19 -12
  86. package/template/PAGES.md +25 -2
  87. package/template/README.md +1 -1
  88. package/template/_gitignore +1 -0
  89. package/template/app/account/page.tsx +31 -11
  90. package/template/app/cart/page.tsx +50 -54
  91. package/template/app/checkout/page.tsx +86 -38
  92. package/template/app/collections/[handle]/page.tsx +3 -2
  93. package/template/app/error.tsx +6 -4
  94. package/template/app/globals.css +29 -0
  95. package/template/app/layout.tsx +4 -2
  96. package/template/app/not-found.tsx +5 -2
  97. package/template/app/pages/[handle]/page.tsx +3 -2
  98. package/template/app/search/page.tsx +11 -6
  99. package/template/components/buy-box.tsx +7 -4
  100. package/template/components/cart-link.tsx +8 -2
  101. package/template/components/pager.tsx +3 -1
  102. package/template/components/product-grid.tsx +9 -5
  103. package/template/components/sections/banner.tsx +7 -1
  104. package/template/components/sections/deal-of-day.tsx +16 -5
  105. package/template/components/sections/product-shelves.tsx +8 -7
  106. package/template/components/sections/store-reviews.tsx +7 -5
  107. package/template/lib/errors.ts +1 -4
  108. package/template/lib/i18n.ts +315 -0
  109. package/template/llms.txt +8 -6
@@ -28,6 +28,8 @@ each error code means — then `PAGES.md` for how each page type is built, and
28
28
  webhook). Anything personal — cart, customer, wishlist — lives in client components under
29
29
  `MagicStoreProvider` (`app/providers.tsx`).
30
30
  - Branch on `MagicStoreError.code`, show `error.detail`.
31
+ - No hard-coded interface text: every word the storefront shows comes from `lib/i18n.ts`, in
32
+ every language it has (`ru`, `uz`, `en`).
31
33
  - Every page handles its empty, error and not-found states (`orNotFound` turns `NOT_FOUND` into
32
34
  the 404 page).
33
35
 
@@ -40,20 +42,25 @@ from a landing page: brand colors come from `shop.branding` at runtime, product
40
42
  come only from the API, nothing is invented (prices, badges, reviews, urgency), and the cart and
41
43
  checkout stay conventional.
42
44
 
45
+ Before calling visual work done, follow `.claude/skills/storefront-verify/SKILL.md`: build, run,
46
+ screenshot every page type at 390 and 1280px, fix what you see. Interactive UI follows
47
+ `.claude/skills/web-interface-guidelines/SKILL.md`; React and Next.js code follows
48
+ `.claude/skills/vercel-react-best-practices/SKILL.md`.
49
+
43
50
  ## Where things go
44
51
 
45
- | Path | What |
46
- | ------------------------------ | ------------------------------------------------------------------------ |
47
- | `app/` | Pages (server components) and route handlers |
48
- | `app/error.tsx` | Error boundary: the API's `detail`, a retry |
49
- | `app/sitemap.ts` | Sitemap from `GET /sitemap` |
50
- | `app/page.tsx` | Home: renders `GET /home` sections in the merchant's order |
51
- | `components/sections/` | One renderer per home section type; an unknown type renders nothing |
52
- | `components/` | Shared UI; client components start with `'use client'` |
53
- | `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
54
- | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
55
- | `app/storefront-api/[...path]` | Same-origin proxy for browser calls without a public storefront token |
56
- | `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
52
+ | Path | What |
53
+ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
54
+ | `app/` | Pages (server components) and route handlers |
55
+ | `app/error.tsx` | Error boundary: the API's `detail`, a retry |
56
+ | `app/sitemap.ts` | Sitemap from `GET /sitemap` |
57
+ | `app/page.tsx` | Home: renders `GET /home` sections in the merchant's order. Optional: a custom home composes its own (`PAGES.md`) |
58
+ | `components/sections/` | One renderer per home section type; an unknown type renders nothing |
59
+ | `components/` | Shared UI; client components start with `'use client'` |
60
+ | `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
61
+ | `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
62
+ | `app/storefront-api/[...path]` | Same-origin proxy for browser calls without a public storefront token |
63
+ | `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
57
64
 
58
65
  ## Commands
59
66
 
package/template/PAGES.md CHANGED
@@ -40,6 +40,24 @@ API's, not the build's. The fetch cache still spares the API.
40
40
 
41
41
  ## Home — `app/page.tsx`, `components/sections/`
42
42
 
43
+ The home page belongs to the storefront. `GET /home` is one way to fill it, not a requirement.
44
+ Pick one per project:
45
+
46
+ - **Merchant-managed** (this starter's default): render `GET /home`, described below. The merchant
47
+ arranges the page in the admin and the storefront follows.
48
+ - **Custom**: compose the page yourself from any API data — `productsIndex` with its sorts and
49
+ filters, `collectionsIndex`, `collectionsProducts`, `reviewsSite`, `pagesShow`, `shop.branding` —
50
+ in whatever layout the design calls for. Not calling `api.home()` at all is fine, and so is
51
+ taking only some sections from it (the merchant's `BANNER` slides, the `ANNOUNCEMENT_BAR` text)
52
+ and placing them where the design wants. `components/sections/` can then be deleted.
53
+
54
+ A custom home is a trade-off: the merchant's home editor in the admin no longer controls the page.
55
+ Tell whoever owns the store. Everything else still holds: data only from the API, nothing invented,
56
+ and the page is revalidated by the tags of what it reads (`magicstore:catalog`, `magicstore:shop`),
57
+ not by `HOME_UPDATED` unless it calls `api.home()`.
58
+
59
+ ### Rendering `GET /home`
60
+
43
61
  `api.home()` answers `{ sections: HomeSection[] }` in the merchant's order. Render each by `type`
44
62
  with one component per type; a type the storefront does not know renders **nothing** (the API can
45
63
  be newer than the storefront). `GET /theme/section-schema` (`api.themeSectionSchema()`) has the
@@ -150,8 +168,13 @@ updatedAt }` rows — on `SITE_URL` or the shop's `primaryDomain`, at most 50 pa
150
168
  ## Every page
151
169
 
152
170
  - Import only the SDK's entry points; `npx create-magic-storefront check` passes.
153
- - Not found → `notFound()`; any other failure shows `describe(error)` (`lib/errors.ts`) or lets
154
- `app/error.tsx` catch it (it shows `detail`, and "temporarily closed" on `STORE_UNAVAILABLE`).
171
+ - Not found → `notFound()`; any other failure shows `describe(error, t.tryAgainLater)`
172
+ (`lib/errors.ts`) or lets `app/error.tsx` catch it (it shows `detail`, and "temporarily closed"
173
+ on `STORE_UNAVAILABLE`).
174
+ - The interface speaks the storefront's language: every word the storefront itself shows comes from
175
+ `lib/i18n.ts` (`messages(locale)` on the server, `messages(useMagicStore().locale)` in a client
176
+ component), with an entry in each dictionary (`ru`, `uz`, `en`). What the merchant wrote comes
177
+ translated from the API.
155
178
  - Images through `<Image>` (intrinsic size, lazy), prices through `<Money>`.
156
179
  - Analytics: `PageViews` in `app/providers.tsx` reports each route; product, collection and search
157
180
  views are reported where they render.
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Next.js (App Router) storefront on the MagicStore Storefront API v2, built only on the public SDK
4
4
  (`@magicstoreai/storefront-client`, `@magicstoreai/hydrogen`). The home page from the merchant's
5
- sections, catalog, collections, search, product with variants, cart, checkout (pickup or delivery,
5
+ sections (or your own composition: `GET /home` is optional), catalog, collections, search, product with variants, cart, checkout (pickup or delivery,
6
6
  payment), OTP sign-in and orders, SEO and JSON-LD, and a webhook that revalidates cached pages.
7
7
 
8
8
  ```bash
@@ -3,3 +3,4 @@ node_modules/
3
3
  .env*.local
4
4
  next-env.d.ts
5
5
  *.tsbuildinfo
6
+ .screenshots/
@@ -1,19 +1,27 @@
1
1
  'use client';
2
2
 
3
- import { Money, useCustomer, useStorefrontClient, useWishlist } from '@magicstoreai/hydrogen';
3
+ import {
4
+ Money,
5
+ useCustomer,
6
+ useMagicStore,
7
+ useStorefrontClient,
8
+ useWishlist,
9
+ } from '@magicstoreai/hydrogen';
4
10
  import type { Schema } from '@magicstoreai/storefront-client';
5
11
  import { useEffect, useState } from 'react';
6
12
  import { describe } from '@/lib/errors';
13
+ import { messages } from '@/lib/i18n';
7
14
 
8
15
  export default function AccountPage() {
9
16
  const { customer, isSignedIn, signOut } = useCustomer();
17
+ const t = messages(useMagicStore().locale);
10
18
  return isSignedIn && customer ? (
11
19
  <div className="stack">
12
20
  <h1>{customer.name || customer.phone}</h1>
13
21
  <Orders />
14
22
  <SavedCount />
15
23
  <div>
16
- <button onClick={() => void signOut()}>Sign out</button>
24
+ <button onClick={() => void signOut()}>{t.signOut}</button>
17
25
  </div>
18
26
  </div>
19
27
  ) : (
@@ -23,6 +31,7 @@ export default function AccountPage() {
23
31
 
24
32
  function SignIn() {
25
33
  const { requestOtp, verifyOtp } = useCustomer();
34
+ const t = messages(useMagicStore().locale);
26
35
  const [phone, setPhone] = useState('');
27
36
  const [sent, setSent] = useState(false);
28
37
  const [code, setCode] = useState('');
@@ -33,7 +42,7 @@ function SignIn() {
33
42
  try {
34
43
  await step();
35
44
  } catch (e) {
36
- setError(describe(e));
45
+ setError(describe(e, t.tryAgainLater));
37
46
  }
38
47
  }
39
48
 
@@ -53,11 +62,14 @@ function SignIn() {
53
62
  });
54
63
  }}
55
64
  >
56
- <h1>Sign in</h1>
65
+ <h1>{t.signIn}</h1>
57
66
  <input
58
67
  value={phone}
59
68
  onChange={(e) => setPhone(e.target.value)}
60
69
  placeholder="+998 90 123 45 67"
70
+ aria-label={t.phone}
71
+ type="tel"
72
+ autoComplete="tel"
61
73
  required
62
74
  disabled={sent}
63
75
  />
@@ -65,12 +77,14 @@ function SignIn() {
65
77
  <input
66
78
  value={code}
67
79
  onChange={(e) => setCode(e.target.value)}
68
- placeholder="Code from the SMS"
80
+ placeholder={t.smsCode}
81
+ aria-label={t.smsCode}
69
82
  inputMode="numeric"
83
+ autoComplete="one-time-code"
70
84
  required
71
85
  />
72
86
  )}
73
- <button className="primary">{sent ? 'Sign in' : 'Send code'}</button>
87
+ <button className="primary">{sent ? t.signIn : t.sendCode}</button>
74
88
  {error && <p className="error">{error}</p>}
75
89
  </form>
76
90
  );
@@ -79,6 +93,7 @@ function SignIn() {
79
93
  function Orders() {
80
94
  const client = useStorefrontClient();
81
95
  const [orders, setOrders] = useState<Schema<'Order'>[] | null>(null);
96
+ const t = messages(useMagicStore().locale);
82
97
 
83
98
  useEffect(() => {
84
99
  void client.customerOrdersIndex({ query: { perPage: 20 } }).then(
@@ -88,18 +103,18 @@ function Orders() {
88
103
  }, [client]);
89
104
 
90
105
  if (orders === null) {
91
- return <p className="muted">Loading orders…</p>;
106
+ return <p className="muted">{t.loadingOrders}</p>;
92
107
  }
93
108
  return (
94
109
  <section className="stack">
95
- <h2>Orders</h2>
96
- {orders.length === 0 && <p className="muted">No orders yet.</p>}
110
+ <h2>{t.orders}</h2>
111
+ {orders.length === 0 && <p className="muted">{t.noOrders}</p>}
97
112
  <table>
98
113
  <tbody>
99
114
  {orders.map((order) => (
100
115
  <tr key={order.id}>
101
116
  <td>#{order.number}</td>
102
- <td>{order.status}</td>
117
+ <td>{t.orderStatus[order.status]}</td>
103
118
  <td>
104
119
  <Money data={(order.cost as { total?: Schema<'Money'> | null }).total ?? null} />
105
120
  </td>
@@ -113,5 +128,10 @@ function Orders() {
113
128
 
114
129
  function SavedCount() {
115
130
  const { productIds } = useWishlist();
116
- return <p className="muted">Saved products: {productIds.length}</p>;
131
+ const t = messages(useMagicStore().locale);
132
+ return (
133
+ <p className="muted">
134
+ {t.savedProducts}: {productIds.length}
135
+ </p>
136
+ );
117
137
  }
@@ -1,22 +1,24 @@
1
1
  'use client';
2
2
 
3
- import { Image, Money, moneyAmount, useCart } from '@magicstoreai/hydrogen';
3
+ import { Image, Money, moneyAmount, useCart, useMagicStore } from '@magicstoreai/hydrogen';
4
4
  import Link from 'next/link';
5
5
  import { useState } from 'react';
6
6
  import { describe } from '@/lib/errors';
7
+ import { messages } from '@/lib/i18n';
7
8
 
8
9
  export default function CartPage() {
9
10
  const { cart, status, error, updateLine, removeLine, setDiscountCodes } = useCart();
10
11
  const [code, setCode] = useState('');
12
+ const t = messages(useMagicStore().locale);
11
13
 
12
14
  if (status === 'loading') {
13
- return <p className="muted">Loading your cart…</p>;
15
+ return <p className="muted">{t.loadingCart}</p>;
14
16
  }
15
17
  if (cart === null || cart.lines.length === 0) {
16
18
  return (
17
19
  <div className="stack">
18
- <h1>Your cart is empty</h1>
19
- <Link href="/">Continue shopping</Link>
20
+ <h1>{t.cartEmpty}</h1>
21
+ <Link href="/">{t.continueShopping}</Link>
20
22
  </div>
21
23
  );
22
24
  }
@@ -25,47 +27,39 @@ export default function CartPage() {
25
27
 
26
28
  return (
27
29
  <div className="stack">
28
- <h1>Cart</h1>
29
- <table>
30
- <tbody>
31
- {cart.lines.map((line) => (
32
- <tr key={line.id}>
33
- <td style={{ width: 72 }}>
34
- <Image data={line.merchandise.image} width={64} height={64} alt="" />
35
- </td>
36
- <td>
37
- <div>{line.merchandise.product?.title}</div>
38
- {line.merchandise.variant &&
39
- !/^Default( Title)?$/.test(line.merchandise.variant.title) && (
40
- <div className="muted">{line.merchandise.variant.title}</div>
41
- )}
42
- {!line.available && <div className="error">Not available any more</div>}
43
- </td>
44
- <td>
45
- <input
46
- type="number"
47
- min={0}
48
- max={999}
49
- value={line.quantity}
50
- aria-label="Quantity"
51
- onChange={(event) =>
52
- void updateLine(line.id, Number(event.target.value)).catch(() => undefined)
53
- }
54
- style={{ width: 72 }}
55
- />
56
- </td>
57
- <td>
58
- <Money data={line.cost.total} />
59
- </td>
60
- <td>
61
- <button onClick={() => void removeLine(line.id).catch(() => undefined)}>
62
- Remove
63
- </button>
64
- </td>
65
- </tr>
66
- ))}
67
- </tbody>
68
- </table>
30
+ <h1>{t.cart}</h1>
31
+ <ul className="cart-lines">
32
+ {cart.lines.map((line) => (
33
+ <li key={line.id} className="cart-line">
34
+ <Image data={line.merchandise.image} width={64} height={64} alt="" />
35
+ <div>
36
+ <div>{line.merchandise.product?.title}</div>
37
+ {line.merchandise.variant &&
38
+ !/^Default( Title)?$/.test(line.merchandise.variant.title) && (
39
+ <div className="muted">{line.merchandise.variant.title}</div>
40
+ )}
41
+ {!line.available && <div className="error">{t.notAvailable}</div>}
42
+ </div>
43
+ <div className="cart-line-controls">
44
+ <input
45
+ type="number"
46
+ min={0}
47
+ max={999}
48
+ value={line.quantity}
49
+ aria-label={t.quantity}
50
+ onChange={(event) =>
51
+ void updateLine(line.id, Number(event.target.value)).catch(() => undefined)
52
+ }
53
+ style={{ width: 72 }}
54
+ />
55
+ <Money data={line.cost.total} />
56
+ <button onClick={() => void removeLine(line.id).catch(() => undefined)}>
57
+ {t.remove}
58
+ </button>
59
+ </div>
60
+ </li>
61
+ ))}
62
+ </ul>
69
63
 
70
64
  <form
71
65
  className="row"
@@ -77,12 +71,14 @@ export default function CartPage() {
77
71
  <input
78
72
  value={code}
79
73
  onChange={(event) => setCode(event.target.value)}
80
- placeholder="Discount code"
74
+ placeholder={t.discountCode}
75
+ aria-label={t.discountCode}
81
76
  />
82
- <button>Apply</button>
77
+ <button>{t.apply}</button>
83
78
  {applied && (
84
79
  <span className={applied.applicable ? 'muted' : 'error'}>
85
- {applied.code}: {applied.applicable ? 'applied' : `not applied (${applied.reasonCode})`}
80
+ {applied.code}:{' '}
81
+ {applied.applicable ? t.codeApplied : `${t.codeNotApplied} (${applied.reasonCode})`}
86
82
  </span>
87
83
  )}
88
84
  </form>
@@ -90,21 +86,21 @@ export default function CartPage() {
90
86
  <table>
91
87
  <tbody>
92
88
  <tr>
93
- <td>Subtotal</td>
89
+ <td>{t.subtotal}</td>
94
90
  <td>
95
91
  <Money data={cart.cost.subtotal} />
96
92
  </td>
97
93
  </tr>
98
94
  {cart.cost.discountTotal && moneyAmount(cart.cost.discountTotal) > 0 && (
99
95
  <tr>
100
- <td>Discount</td>
96
+ <td>{t.discount}</td>
101
97
  <td>
102
98
  −<Money data={cart.cost.discountTotal} />
103
99
  </td>
104
100
  </tr>
105
101
  )}
106
102
  <tr>
107
- <th>Total</th>
103
+ <th>{t.total}</th>
108
104
  <th>
109
105
  <Money data={cart.cost.total} />
110
106
  </th>
@@ -112,12 +108,12 @@ export default function CartPage() {
112
108
  </tbody>
113
109
  </table>
114
110
 
115
- {error && <p className="error">{describe(error)}</p>}
111
+ {error && <p className="error">{describe(error, t.tryAgainLater)}</p>}
116
112
  <div className="row">
117
113
  <Link href="/checkout" className="button primary">
118
- Checkout
114
+ {t.checkout}
119
115
  </Link>
120
- {status === 'updating' && <span className="muted">Updating…</span>}
116
+ {status === 'updating' && <span className="muted">{t.updating}</span>}
121
117
  </div>
122
118
  </div>
123
119
  );
@@ -1,10 +1,17 @@
1
1
  'use client';
2
2
 
3
- import { Money, useCart, useCustomer, useStorefrontClient } from '@magicstoreai/hydrogen';
3
+ import {
4
+ Money,
5
+ useCart,
6
+ useCustomer,
7
+ useMagicStore,
8
+ useStorefrontClient,
9
+ } from '@magicstoreai/hydrogen';
4
10
  import type { Schema } from '@magicstoreai/storefront-client';
5
11
  import Link from 'next/link';
6
12
  import { useCallback, useEffect, useState } from 'react';
7
13
  import { describe } from '@/lib/errors';
14
+ import { messages } from '@/lib/i18n';
8
15
 
9
16
  type Checkout = Schema<'Checkout'>;
10
17
  type DeliveryOption = Schema<'DeliveryOption'>;
@@ -20,6 +27,7 @@ export default function CheckoutPage() {
20
27
  const client = useStorefrontClient();
21
28
  const { cart, status: cartStatus } = useCart();
22
29
  const { customer } = useCustomer();
30
+ const t = messages(useMagicStore().locale);
23
31
  const [checkout, setCheckout] = useState<Checkout | null>(null);
24
32
  const [locations, setLocations] = useState<Location[]>([]);
25
33
  const [options, setOptions] = useState<DeliveryOption[]>([]);
@@ -27,20 +35,23 @@ export default function CheckoutPage() {
27
35
  const [error, setError] = useState<string | null>(null);
28
36
  const [busy, setBusy] = useState(false);
29
37
 
30
- const run = useCallback(async (step: () => Promise<{ data: Checkout } | void>) => {
31
- setBusy(true);
32
- setError(null);
33
- try {
34
- const result = await step();
35
- if (result) {
36
- setCheckout(result.data);
38
+ const run = useCallback(
39
+ async (step: () => Promise<{ data: Checkout } | void>) => {
40
+ setBusy(true);
41
+ setError(null);
42
+ try {
43
+ const result = await step();
44
+ if (result) {
45
+ setCheckout(result.data);
46
+ }
47
+ } catch (e) {
48
+ setError(describe(e, t.tryAgainLater));
49
+ } finally {
50
+ setBusy(false);
37
51
  }
38
- } catch (e) {
39
- setError(describe(e));
40
- } finally {
41
- setBusy(false);
42
- }
43
- }, []);
52
+ },
53
+ [t],
54
+ );
44
55
 
45
56
  useEffect(() => {
46
57
  if (cart === null || checkout !== null) {
@@ -62,31 +73,36 @@ export default function CheckoutPage() {
62
73
  if (completion) {
63
74
  return (
64
75
  <div className="stack">
65
- <h1>Thank you!</h1>
76
+ <h1>{t.thankYou}</h1>
66
77
  <p>
67
- Order <strong>{completion.order.number}</strong> — <Money data={completion.order.total} />
78
+ {t.order} <strong>{completion.order.number}</strong> —{' '}
79
+ <Money data={completion.order.total} />
68
80
  </p>
69
81
  {completion.payment.status === 'PENDING' && completion.payment.redirectUrl && (
70
82
  <a className="button primary" href={completion.payment.redirectUrl}>
71
- Pay now
83
+ {t.payNow}
72
84
  </a>
73
85
  )}
74
- <Link href="/">Continue shopping</Link>
86
+ <Link href="/">{t.continueShopping}</Link>
75
87
  </div>
76
88
  );
77
89
  }
78
90
  if (cartStatus === 'loading') {
79
- return <p className="muted">Loading your cart…</p>;
91
+ return <p className="muted">{t.loadingCart}</p>;
80
92
  }
81
93
  if (cart === null || cart.lines.length === 0) {
82
94
  return (
83
95
  <p>
84
- Your cart is empty. <Link href="/">Continue shopping</Link>
96
+ {t.cartEmpty}. <Link href="/">{t.continueShopping}</Link>
85
97
  </p>
86
98
  );
87
99
  }
88
100
  if (checkout === null) {
89
- return error ? <p className="error">{error}</p> : <p className="muted">Preparing checkout…</p>;
101
+ return error ? (
102
+ <p className="error">{error}</p>
103
+ ) : (
104
+ <p className="muted">{t.preparingCheckout}</p>
105
+ );
90
106
  }
91
107
 
92
108
  const id = checkout.id;
@@ -94,7 +110,7 @@ export default function CheckoutPage() {
94
110
 
95
111
  return (
96
112
  <div className="stack">
97
- <h1>Checkout</h1>
113
+ <h1>{t.checkoutTitle}</h1>
98
114
 
99
115
  <form
100
116
  className="stack"
@@ -110,30 +126,38 @@ export default function CheckoutPage() {
110
126
  }}
111
127
  >
112
128
  <fieldset className="stack">
113
- <legend>Contact {missing.has('CONTACT') ? '' : '✓'}</legend>
129
+ <legend>
130
+ {t.contact} {missing.has('CONTACT') ? '' : '✓'}
131
+ </legend>
114
132
  <input
115
133
  name="name"
116
134
  required
117
- placeholder="Name"
135
+ placeholder={t.name}
136
+ aria-label={t.name}
137
+ autoComplete="name"
118
138
  defaultValue={checkout.contact?.name ?? customer?.name ?? ''}
119
139
  />
120
140
  <input
121
141
  name="phone"
122
142
  required
123
143
  placeholder="+998 90 123 45 67"
144
+ aria-label={t.phone}
145
+ type="tel"
146
+ autoComplete="tel"
124
147
  defaultValue={checkout.contact?.phone ?? customer?.phone ?? ''}
125
148
  />
126
- <button disabled={busy}>Save contact</button>
149
+ <button disabled={busy}>{t.saveContact}</button>
127
150
  </fieldset>
128
151
  </form>
129
152
 
130
153
  <fieldset className="stack">
131
154
  <legend>
132
- Delivery {missing.has('DELIVERY_ADDRESS') || missing.has('DELIVERY_OPTION') ? '' : '✓'}
155
+ {t.delivery}{' '}
156
+ {missing.has('DELIVERY_ADDRESS') || missing.has('DELIVERY_OPTION') ? '' : '✓'}
133
157
  </legend>
134
158
  {locations.length > 0 && (
135
159
  <div className="stack">
136
- <strong>Pick up</strong>
160
+ <strong>{t.pickUp}</strong>
137
161
  {locations.map((location) => (
138
162
  <label key={location.id} className="row">
139
163
  <input
@@ -176,17 +200,39 @@ export default function CheckoutPage() {
176
200
  });
177
201
  }}
178
202
  >
179
- <strong>Or deliver to</strong>
180
- <input name="address" placeholder="Street, house" />
181
- <input name="city" placeholder="City" />
203
+ <strong>{locations.length > 0 ? t.orDeliverTo : t.deliverTo}</strong>
204
+ <input
205
+ name="address"
206
+ placeholder={t.street}
207
+ aria-label={t.street}
208
+ autoComplete="street-address"
209
+ />
210
+ <input
211
+ name="city"
212
+ placeholder={t.city}
213
+ aria-label={t.city}
214
+ autoComplete="address-level2"
215
+ />
182
216
  <div className="row">
183
- <input name="latitude" required inputMode="decimal" placeholder="Latitude" />
184
- <input name="longitude" required inputMode="decimal" placeholder="Longitude" />
217
+ <input
218
+ name="latitude"
219
+ required
220
+ inputMode="decimal"
221
+ placeholder={t.latitude}
222
+ aria-label={t.latitude}
223
+ />
224
+ <input
225
+ name="longitude"
226
+ required
227
+ inputMode="decimal"
228
+ placeholder={t.longitude}
229
+ aria-label={t.longitude}
230
+ />
185
231
  <button type="button" onClick={(event) => fillMyLocation(event.currentTarget.form)}>
186
- Use my location
232
+ {t.useMyLocation}
187
233
  </button>
188
234
  </div>
189
- <button disabled={busy}>Show delivery options</button>
235
+ <button disabled={busy}>{t.showDeliveryOptions}</button>
190
236
  </form>
191
237
  {options.map((option) => (
192
238
  <label key={option.id} className="row">
@@ -204,13 +250,15 @@ export default function CheckoutPage() {
204
250
  )
205
251
  }
206
252
  />
207
- {option.title} — <Money data={option.price} fallback="free" />
253
+ {option.title} — <Money data={option.price} fallback={t.free} />
208
254
  </label>
209
255
  ))}
210
256
  </fieldset>
211
257
 
212
258
  <fieldset className="stack">
213
- <legend>Payment {missing.has('PAYMENT_METHOD') ? '' : '✓'}</legend>
259
+ <legend>
260
+ {t.payment} {missing.has('PAYMENT_METHOD') ? '' : '✓'}
261
+ </legend>
214
262
  {checkout.paymentMethods.map((method) => (
215
263
  <label key={method.id} className="row">
216
264
  <input
@@ -232,7 +280,7 @@ export default function CheckoutPage() {
232
280
  </fieldset>
233
281
 
234
282
  <p>
235
- Total:{' '}
283
+ {t.total}:{' '}
236
284
  <strong>
237
285
  <Money data={checkout.cost.total} />
238
286
  </strong>
@@ -248,7 +296,7 @@ export default function CheckoutPage() {
248
296
  })
249
297
  }
250
298
  >
251
- Place order
299
+ {t.placeOrder}
252
300
  </button>
253
301
  </div>
254
302
  );
@@ -3,7 +3,8 @@ import { breadcrumbJsonLd, jsonLdScript, pageMeta } from '@magicstoreai/hydrogen
3
3
  import { CollectionView } from '@/components/analytics-views';
4
4
  import { Pager } from '@/components/pager';
5
5
  import { ProductGrid } from '@/components/product-grid';
6
- import { api, orNotFound } from '@/lib/api';
6
+ import { api, locale, orNotFound } from '@/lib/api';
7
+ import { messages } from '@/lib/i18n';
7
8
 
8
9
  type Props = { params: Promise<{ handle: string }>; searchParams: Promise<{ page?: string }> };
9
10
 
@@ -33,7 +34,7 @@ export default async function CollectionPage({ params, searchParams }: Props) {
33
34
  dangerouslySetInnerHTML={{
34
35
  __html: jsonLdScript(
35
36
  breadcrumbJsonLd([
36
- { name: 'Home', url: '/' },
37
+ { name: messages(locale).home, url: '/' },
37
38
  { name: collection.title, url: `/collections/${collection.handle}` },
38
39
  ]),
39
40
  ),