create-magic-storefront 0.1.0 → 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.
- package/package.json +1 -1
- package/template/.claude/skills/design-taste-frontend/LICENSE +21 -0
- package/template/.claude/skills/design-taste-frontend/SKILL.md +1206 -0
- package/template/.claude/skills/design-taste-frontend/SOURCE.md +9 -0
- package/template/.claude/skills/storefront-design/SKILL.md +170 -0
- package/template/.claude/skills/storefront-verify/SKILL.md +85 -0
- package/template/.claude/skills/vercel-react-best-practices/AGENTS.md +3810 -0
- package/template/.claude/skills/vercel-react-best-practices/README.md +123 -0
- package/template/.claude/skills/vercel-react-best-practices/SKILL.md +149 -0
- package/template/.claude/skills/vercel-react-best-practices/SOURCE.md +7 -0
- package/template/.claude/skills/vercel-react-best-practices/metadata.json +15 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/_sections.md +46 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/_template.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
- package/template/.claude/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/template/.claude/skills/web-interface-guidelines/LICENSE +21 -0
- package/template/.claude/skills/web-interface-guidelines/SKILL.md +40 -0
- package/template/.claude/skills/web-interface-guidelines/SOURCE.md +9 -0
- package/template/.claude/skills/web-interface-guidelines/guidelines.md +155 -0
- package/template/AGENTS.md +16 -0
- package/template/PAGES.md +11 -3
- package/template/_gitignore +1 -0
- package/template/app/account/page.tsx +31 -11
- package/template/app/cart/page.tsx +50 -54
- package/template/app/checkout/page.tsx +86 -38
- package/template/app/collections/[handle]/page.tsx +3 -2
- package/template/app/error.tsx +6 -4
- package/template/app/globals.css +46 -4
- package/template/app/layout.tsx +14 -4
- package/template/app/not-found.tsx +5 -2
- package/template/app/pages/[handle]/page.tsx +3 -2
- package/template/app/search/page.tsx +11 -6
- package/template/components/buy-box.tsx +7 -4
- package/template/components/cart-link.tsx +8 -2
- package/template/components/pager.tsx +3 -1
- package/template/components/product-grid.tsx +30 -2
- package/template/components/sections/banner.tsx +7 -1
- package/template/components/sections/deal-of-day.tsx +16 -5
- package/template/components/sections/product-shelves.tsx +8 -7
- package/template/components/sections/store-reviews.tsx +7 -5
- package/template/lib/brand.ts +60 -0
- package/template/lib/errors.ts +1 -4
- package/template/lib/i18n.ts +315 -0
package/template/PAGES.md
CHANGED
|
@@ -31,7 +31,10 @@ API's, not the build's. The fetch cache still spares the API.
|
|
|
31
31
|
## Layout — `app/layout.tsx`
|
|
32
32
|
|
|
33
33
|
- Reads `api.shop()` and the navigation (`api.collectionsIndex` or `api.menusShow({ path: { handle: 'main' } })`).
|
|
34
|
-
- `generateMetadata`: `shop.seo.title || shop.name`, `shop.seo.description ?? shop.description
|
|
34
|
+
- `generateMetadata`: `shop.seo.title || shop.name`, `shop.seo.description ?? shop.description`,
|
|
35
|
+
the favicon from `shop.branding.favicon`.
|
|
36
|
+
- The merchant's colors: `brandStyle(shop.branding)` (`lib/brand.ts`) on `<html style>`, as the
|
|
37
|
+
tokens `app/globals.css` falls back from; the header shows `shop.branding.logo` when there is one.
|
|
35
38
|
- Wraps the page in `Providers` (`MagicStoreProvider` with `shop` passed in, so the browser does not
|
|
36
39
|
load it again).
|
|
37
40
|
|
|
@@ -147,8 +150,13 @@ updatedAt }` rows — on `SITE_URL` or the shop's `primaryDomain`, at most 50 pa
|
|
|
147
150
|
## Every page
|
|
148
151
|
|
|
149
152
|
- Import only the SDK's entry points; `npx create-magic-storefront check` passes.
|
|
150
|
-
- Not found → `notFound()`; any other failure shows `describe(error
|
|
151
|
-
`app/error.tsx` catch it (it shows `detail`, and "temporarily closed"
|
|
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.
|
|
152
160
|
- Images through `<Image>` (intrinsic size, lazy), prices through `<Money>`.
|
|
153
161
|
- Analytics: `PageViews` in `app/providers.tsx` reports each route; product, collection and search
|
|
154
162
|
views are reported where they render.
|
package/template/_gitignore
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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()}>
|
|
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>
|
|
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=
|
|
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 ?
|
|
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">
|
|
106
|
+
return <p className="muted">{t.loadingOrders}</p>;
|
|
92
107
|
}
|
|
93
108
|
return (
|
|
94
109
|
<section className="stack">
|
|
95
|
-
<h2>
|
|
96
|
-
{orders.length === 0 && <p className="muted">
|
|
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
|
-
|
|
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">
|
|
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>
|
|
19
|
-
<Link href="/">
|
|
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>
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
{cart
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
</
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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=
|
|
74
|
+
placeholder={t.discountCode}
|
|
75
|
+
aria-label={t.discountCode}
|
|
81
76
|
/>
|
|
82
|
-
<button>
|
|
77
|
+
<button>{t.apply}</button>
|
|
83
78
|
{applied && (
|
|
84
79
|
<span className={applied.applicable ? 'muted' : 'error'}>
|
|
85
|
-
{applied.code}:
|
|
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>
|
|
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>
|
|
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>
|
|
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
|
-
|
|
114
|
+
{t.checkout}
|
|
119
115
|
</Link>
|
|
120
|
-
{status === 'updating' && <span className="muted">
|
|
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 {
|
|
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(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
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>
|
|
76
|
+
<h1>{t.thankYou}</h1>
|
|
66
77
|
<p>
|
|
67
|
-
|
|
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
|
-
|
|
83
|
+
{t.payNow}
|
|
72
84
|
</a>
|
|
73
85
|
)}
|
|
74
|
-
<Link href="/">
|
|
86
|
+
<Link href="/">{t.continueShopping}</Link>
|
|
75
87
|
</div>
|
|
76
88
|
);
|
|
77
89
|
}
|
|
78
90
|
if (cartStatus === 'loading') {
|
|
79
|
-
return <p className="muted">
|
|
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
|
-
|
|
96
|
+
{t.cartEmpty}. <Link href="/">{t.continueShopping}</Link>
|
|
85
97
|
</p>
|
|
86
98
|
);
|
|
87
99
|
}
|
|
88
100
|
if (checkout === null) {
|
|
89
|
-
return error ?
|
|
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>
|
|
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>
|
|
129
|
+
<legend>
|
|
130
|
+
{t.contact} {missing.has('CONTACT') ? '' : '✓'}
|
|
131
|
+
</legend>
|
|
114
132
|
<input
|
|
115
133
|
name="name"
|
|
116
134
|
required
|
|
117
|
-
placeholder=
|
|
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}>
|
|
149
|
+
<button disabled={busy}>{t.saveContact}</button>
|
|
127
150
|
</fieldset>
|
|
128
151
|
</form>
|
|
129
152
|
|
|
130
153
|
<fieldset className="stack">
|
|
131
154
|
<legend>
|
|
132
|
-
|
|
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>
|
|
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>
|
|
180
|
-
<input
|
|
181
|
-
|
|
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
|
|
184
|
-
|
|
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
|
-
|
|
232
|
+
{t.useMyLocation}
|
|
187
233
|
</button>
|
|
188
234
|
</div>
|
|
189
|
-
<button disabled={busy}>
|
|
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=
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
37
|
+
{ name: messages(locale).home, url: '/' },
|
|
37
38
|
{ name: collection.title, url: `/collections/${collection.handle}` },
|
|
38
39
|
]),
|
|
39
40
|
),
|
package/template/app/error.tsx
CHANGED
|
@@ -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 ?
|
|
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}>
|
|
14
|
-
<a href="/">
|
|
15
|
+
<button onClick={reset}>{t.tryAgain}</button>
|
|
16
|
+
<a href="/">{t.backToShop}</a>
|
|
15
17
|
</div>
|
|
16
18
|
</div>
|
|
17
19
|
);
|