create-magic-storefront 0.1.1 → 0.1.2

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 (107) hide show
  1. package/package.json +1 -1
  2. package/template/.claude/skills/storefront-design/SKILL.md +10 -3
  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 +7 -0
  86. package/template/PAGES.md +7 -2
  87. package/template/_gitignore +1 -0
  88. package/template/app/account/page.tsx +31 -11
  89. package/template/app/cart/page.tsx +50 -54
  90. package/template/app/checkout/page.tsx +86 -38
  91. package/template/app/collections/[handle]/page.tsx +3 -2
  92. package/template/app/error.tsx +6 -4
  93. package/template/app/globals.css +29 -0
  94. package/template/app/layout.tsx +4 -2
  95. package/template/app/not-found.tsx +5 -2
  96. package/template/app/pages/[handle]/page.tsx +3 -2
  97. package/template/app/search/page.tsx +11 -6
  98. package/template/components/buy-box.tsx +7 -4
  99. package/template/components/cart-link.tsx +8 -2
  100. package/template/components/pager.tsx +3 -1
  101. package/template/components/product-grid.tsx +9 -5
  102. package/template/components/sections/banner.tsx +7 -1
  103. package/template/components/sections/deal-of-day.tsx +16 -5
  104. package/template/components/sections/product-shelves.tsx +8 -7
  105. package/template/components/sections/store-reviews.tsx +7 -5
  106. package/template/lib/errors.ts +1 -4
  107. package/template/lib/i18n.ts +315 -0
package/template/PAGES.md CHANGED
@@ -150,8 +150,13 @@ updatedAt }` rows — on `SITE_URL` or the shop's `primaryDomain`, at most 50 pa
150
150
  ## Every page
151
151
 
152
152
  - 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`).
153
+ - Not found → `notFound()`; any other failure shows `describe(error, t.tryAgainLater)`
154
+ (`lib/errors.ts`) or lets `app/error.tsx` catch it (it shows `detail`, and "temporarily closed"
155
+ on `STORE_UNAVAILABLE`).
156
+ - The interface speaks the storefront's language: every word the storefront itself shows comes from
157
+ `lib/i18n.ts` (`messages(locale)` on the server, `messages(useMagicStore().locale)` in a client
158
+ component), with an entry in each dictionary (`ru`, `uz`, `en`). What the merchant wrote comes
159
+ translated from the API.
155
160
  - Images through `<Image>` (intrinsic size, lazy), prices through `<Money>`.
156
161
  - Analytics: `PageViews` in `app/providers.tsx` reports each route; product, collection and search
157
162
  views are reported where they render.
@@ -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
  ),
@@ -1,17 +1,19 @@
1
1
  'use client';
2
2
 
3
- import { MagicStoreError } from '@magicstoreai/hydrogen';
3
+ import { MagicStoreError, useMagicStore } from '@magicstoreai/hydrogen';
4
+ import { messages } from '@/lib/i18n';
4
5
 
5
6
  /** Any failure a page did not handle itself: the API's detail when there is one, and a retry. */
6
7
  export default function ErrorPage({ error, reset }: { error: Error; reset: () => void }) {
8
+ const t = messages(useMagicStore().locale);
7
9
  const unavailable = error instanceof MagicStoreError && error.code === 'STORE_UNAVAILABLE';
8
10
  return (
9
11
  <div className="stack">
10
- <h1>{unavailable ? 'The shop is temporarily closed' : 'Something went wrong'}</h1>
12
+ <h1>{unavailable ? t.shopClosed : t.somethingWentWrong}</h1>
11
13
  {error instanceof MagicStoreError && error.detail && <p>{error.detail}</p>}
12
14
  <div className="row">
13
- <button onClick={reset}>Try again</button>
14
- <a href="/">Back to the shop</a>
15
+ <button onClick={reset}>{t.tryAgain}</button>
16
+ <a href="/">{t.backToShop}</a>
15
17
  </div>
16
18
  </div>
17
19
  );
@@ -150,6 +150,35 @@ th {
150
150
  text-align: left;
151
151
  vertical-align: middle;
152
152
  }
153
+ .cart-lines {
154
+ list-style: none;
155
+ margin: 0;
156
+ padding: 0;
157
+ }
158
+ /* Phone: image and title on one row, the controls under the title. Wider: all on one row. */
159
+ .cart-line {
160
+ display: grid;
161
+ grid-template-columns: 64px minmax(0, 1fr);
162
+ gap: 8px 12px;
163
+ padding: 12px 0;
164
+ border-bottom: 1px solid var(--line);
165
+ }
166
+ .cart-line-controls {
167
+ grid-column: 2;
168
+ display: flex;
169
+ gap: 12px;
170
+ align-items: center;
171
+ flex-wrap: wrap;
172
+ }
173
+ @media (min-width: 720px) {
174
+ .cart-line {
175
+ grid-template-columns: 64px minmax(0, 1fr) auto;
176
+ align-items: center;
177
+ }
178
+ .cart-line-controls {
179
+ grid-column: auto;
180
+ }
181
+ }
153
182
  .pager {
154
183
  display: flex;
155
184
  gap: 8px;
@@ -5,6 +5,7 @@ import { Image } from '@magicstoreai/hydrogen';
5
5
  import { CartLink } from '@/components/cart-link';
6
6
  import { api, locale } from '@/lib/api';
7
7
  import { brandStyle } from '@/lib/brand';
8
+ import { messages } from '@/lib/i18n';
8
9
  import { Providers } from './providers';
9
10
  import './globals.css';
10
11
 
@@ -27,6 +28,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
27
28
  api.collectionsIndex({ query: { perPage: 6 } }),
28
29
  api.pagesIndex(),
29
30
  ]);
31
+ const t = messages(locale);
30
32
 
31
33
  return (
32
34
  // The merchant's colors, read per request: a change in the admin shows after SHOP_UPDATED.
@@ -47,8 +49,8 @@ export default async function RootLayout({ children }: { children: ReactNode })
47
49
  {collection.title}
48
50
  </Link>
49
51
  ))}
50
- <Link href="/search">Search</Link>
51
- <Link href="/account">Account</Link>
52
+ <Link href="/search">{t.search}</Link>
53
+ <Link href="/account">{t.account}</Link>
52
54
  <CartLink />
53
55
  </nav>
54
56
  </header>