create-magic-storefront 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/template/.claude/skills/storefront-design/SKILL.md +14 -10
- package/template/AGENTS.md +32 -16
- package/template/DESIGN.md +117 -0
- package/template/PAGES.md +107 -29
- package/template/README.md +15 -3
- package/template/app/account/addresses/page.tsx +16 -0
- package/template/app/account/layout.tsx +14 -0
- package/template/app/account/loading.tsx +15 -0
- package/template/app/account/orders/[id]/page.tsx +15 -0
- package/template/app/account/page.tsx +12 -244
- package/template/app/account/profile/page.tsx +16 -0
- package/template/app/blog/(index)/loading.tsx +22 -0
- package/template/app/blog/{page.tsx → (index)/page.tsx} +42 -25
- package/template/app/blog/[handle]/page.tsx +92 -56
- package/template/app/blog/author/[handle]/page.tsx +1 -1
- package/template/app/cart/layout.tsx +14 -0
- package/template/app/cart/page.tsx +49 -101
- package/template/app/checkout/layout.tsx +14 -0
- package/template/app/checkout/page.tsx +98 -281
- package/template/app/collections/[handle]/layout.tsx +20 -0
- package/template/app/collections/[handle]/loading.tsx +23 -0
- package/template/app/collections/[handle]/page.tsx +131 -15
- package/template/app/collections/page.tsx +49 -0
- package/template/app/contacts/page.tsx +196 -0
- package/template/app/error.tsx +19 -8
- package/template/app/fonts/OFL.txt +93 -0
- package/template/app/fonts/index.ts +14 -0
- package/template/app/fonts/onest-variable.woff2 +0 -0
- package/template/app/globals.css +14 -518
- package/template/app/layout.tsx +73 -50
- package/template/app/not-found.tsx +34 -4
- package/template/app/page.tsx +1 -1
- package/template/app/pages/[handle]/page.tsx +1 -1
- package/template/app/products/[handle]/layout.tsx +20 -0
- package/template/app/products/[handle]/loading.tsx +25 -0
- package/template/app/products/[handle]/page.tsx +112 -15
- package/template/app/providers.tsx +5 -1
- package/template/app/search/loading.tsx +12 -0
- package/template/app/search/page.tsx +138 -25
- package/template/app/sitemap.ts +5 -1
- package/template/app/styles/base.css +138 -0
- package/template/app/styles/components.css +1108 -0
- package/template/app/styles/layout.css +455 -0
- package/template/app/styles/pages.css +1839 -0
- package/template/app/styles/reset.css +55 -0
- package/template/app/styles/sections.css +415 -0
- package/template/app/styles/tokens.css +167 -0
- package/template/app/styles/utilities.css +37 -0
- package/template/app/wishlist/page.tsx +26 -0
- package/template/components/account/account-shell.tsx +100 -0
- package/template/components/account/addresses.tsx +247 -0
- package/template/components/account/order-detail.tsx +281 -0
- package/template/components/account/order-list.tsx +94 -0
- package/template/components/account/order-status.tsx +19 -0
- package/template/components/account/profile.tsx +129 -0
- package/template/components/account/sign-in.tsx +164 -0
- package/template/components/article-body.tsx +3 -2
- package/template/components/article-card.tsx +37 -12
- package/template/components/cart/cart-drawer.tsx +149 -0
- package/template/components/cart/cart-lines.tsx +89 -0
- package/template/components/cart/cart-summary.tsx +191 -0
- package/template/components/catalog/catalog-form.tsx +107 -0
- package/template/components/catalog/catalog-layout.tsx +82 -0
- package/template/components/catalog/facets-shell.tsx +60 -0
- package/template/components/catalog/facets.tsx +216 -0
- package/template/components/checkout/order-summary.tsx +84 -0
- package/template/components/checkout/radio-card.tsx +34 -0
- package/template/components/checkout/step-indicator.tsx +56 -0
- package/template/components/checkout/steps.tsx +264 -0
- package/template/components/checkout/thank-you.tsx +48 -0
- package/template/components/checkout/use-checkout.ts +90 -0
- package/template/components/footer/site-footer.tsx +132 -0
- package/template/components/header/cart-button.tsx +43 -0
- package/template/components/header/desktop-nav.tsx +133 -0
- package/template/components/header/mobile-menu.tsx +102 -0
- package/template/components/header/site-header.tsx +78 -0
- package/template/components/page-loading.tsx +20 -0
- package/template/components/pager.tsx +3 -1
- package/template/components/product/buy-box.tsx +148 -0
- package/template/components/product/gallery.tsx +179 -0
- package/template/components/product/pre-order-form.tsx +93 -0
- package/template/components/product/product-selection.tsx +31 -0
- package/template/components/product/recommendations.tsx +26 -0
- package/template/components/product/review-list.tsx +159 -0
- package/template/components/product/reviews.tsx +80 -0
- package/template/components/product/variant-picker.tsx +75 -0
- package/template/components/product-card.tsx +103 -0
- package/template/components/product-grid.tsx +27 -57
- package/template/components/quick-add.tsx +62 -0
- package/template/components/search/predictive-search.tsx +282 -0
- package/template/components/sections/announcement-bar.tsx +1 -1
- package/template/components/sections/banner.tsx +1 -1
- package/template/components/sections/collections.tsx +50 -13
- package/template/components/sections/deal-of-day.tsx +2 -2
- package/template/components/sections/product-shelves.tsx +13 -5
- package/template/components/sections/shoppable-stories.tsx +2 -2
- package/template/components/sections/store-reviews.tsx +10 -4
- package/template/components/share-button.tsx +42 -0
- package/template/components/ui/accordion.tsx +35 -0
- package/template/components/ui/badge.tsx +20 -0
- package/template/components/ui/breadcrumbs.tsx +31 -0
- package/template/components/ui/button.tsx +90 -0
- package/template/components/ui/drawer.tsx +84 -0
- package/template/components/ui/empty-state.tsx +27 -0
- package/template/components/ui/field.tsx +33 -0
- package/template/components/ui/icon.tsx +112 -0
- package/template/components/ui/price.tsx +35 -0
- package/template/components/ui/quantity-stepper.tsx +91 -0
- package/template/components/ui/rating.tsx +48 -0
- package/template/components/ui/skeleton.tsx +46 -0
- package/template/components/wishlist/wishlist-view.tsx +134 -0
- package/template/lib/article-outline.ts +35 -0
- package/template/lib/catalog-params.ts +170 -0
- package/template/lib/contacts.ts +34 -0
- package/template/lib/errors.ts +29 -0
- package/template/lib/i18n.ts +484 -0
- package/template/lib/menu.ts +70 -0
- package/template/lib/theme-settings.ts +36 -0
- package/template/theme/index.ts +7 -1
- package/template/theme/sections/collapsible-content.tsx +75 -0
- package/template/theme/sections/collection-list.tsx +32 -4
- package/template/theme/sections/featured-collection.tsx +22 -2
- package/template/theme/sections/hero.tsx +94 -7
- package/template/theme/sections/image-with-text.tsx +19 -3
- package/template/theme/sections/multicolumn.tsx +113 -0
- package/template/theme/sections/page-content.tsx +10 -4
- package/template/theme/sections/product-shelf.tsx +26 -4
- package/template/theme/sections/rich-text.tsx +13 -1
- package/template/theme/sections/testimonials.tsx +14 -9
- package/template/theme/settings.ts +85 -1
- package/template/theme/templates/index.json +4 -2
- package/template/.screenshots/home-1280.png +0 -0
- package/template/.screenshots/home-390.png +0 -0
- package/template/.screenshots/home-filled-1280.png +0 -0
- package/template/.screenshots/home-filled-390.png +0 -0
- package/template/.screenshots/page-1280.png +0 -0
- package/template/components/buy-box.tsx +0 -89
- package/template/components/cart-link.tsx +0 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-magic-storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Scaffold a Next.js storefront on the MagicStore Storefront API v2",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"esbuild": "^0.27.0"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@magicstoreai/hydrogen": "0.
|
|
22
|
-
"@magicstoreai/storefront-client": "0.
|
|
21
|
+
"@magicstoreai/hydrogen": "0.6.0",
|
|
22
|
+
"@magicstoreai/storefront-client": "0.6.0",
|
|
23
23
|
"@types/node": "^26.6.2",
|
|
24
24
|
"react": "^19.3.0",
|
|
25
25
|
"tsup": "^8.5.1",
|
|
@@ -61,10 +61,11 @@ The merchant changes colors in the admin, and `SHOP_UPDATED` revalidates the sho
|
|
|
61
61
|
|
|
62
62
|
- **Never hard-code the merchant's colors** into CSS. The starter does this already:
|
|
63
63
|
`app/layout.tsx` puts `brandStyle(shop.branding)` (`lib/brand.ts`) on `<html style>`, and
|
|
64
|
-
`app/
|
|
65
|
-
Keep that wiring; restyle through the tokens.
|
|
64
|
+
`app/styles/tokens.css` declares every token with the design's own fallback, used when a color is
|
|
65
|
+
`null`. Keep that wiring; restyle through the tokens.
|
|
66
66
|
- Tokens: `--accent` / `--on-accent` (`colors.main` / `buttonText`), `--nav-bg` / `--nav-fg`
|
|
67
|
-
(header), `--label-bg` / `--label-fg` (badges: "New", "Sale", "Pre-order")
|
|
67
|
+
(header), `--label-bg` / `--label-fg` (badges: "New", "Sale", "Pre-order"); the accent as text on
|
|
68
|
+
the page is `--accent-text`, never `--accent` (a pale accent fails on white).
|
|
68
69
|
- One accent, used the same way on every page (the taste skill's Color Consistency Lock).
|
|
69
70
|
- A text color the merchant didn't set is picked by contrast (`readableTextOn`), never assumed
|
|
70
71
|
white. Only hex colors are accepted: anything else is dropped, never written into the style.
|
|
@@ -72,8 +73,10 @@ The merchant changes colors in the admin, and `SHOP_UPDATED` revalidates the sho
|
|
|
72
73
|
rather than overriding it.
|
|
73
74
|
- Merchant colors are unknown at design time, so check contrast with a light, a dark and a
|
|
74
75
|
saturated accent, not only the one shop you are looking at.
|
|
75
|
-
- Everything else (neutrals, radius, spacing, type scale, shadows) belongs to the design
|
|
76
|
-
in `
|
|
76
|
+
- Everything else (neutrals, radius, spacing, type scale, shadows) belongs to the design. The
|
|
77
|
+
token table, theme settings, CSS layers and the UI primitives in `components/ui/` are in
|
|
78
|
+
`DESIGN.md`: read it and build from them instead of restating values or writing one-off
|
|
79
|
+
components.
|
|
77
80
|
|
|
78
81
|
## 3. Page patterns
|
|
79
82
|
|
|
@@ -94,7 +97,7 @@ The whole card is one link; a quick-add button, if any, is a separate control.
|
|
|
94
97
|
below the fold. Grid: 2 columns on mobile, 3 to 5 on desktop depending on density. Sorting and
|
|
95
98
|
filters sit above the grid; pagination via `<Pagination>`.
|
|
96
99
|
|
|
97
|
-
**Product page** (`app/products/[handle]`, `components/
|
|
100
|
+
**Product page** (`app/products/[handle]`, `components/product/`). Gallery left, buy box right
|
|
98
101
|
on desktop; on mobile gallery, then title, price, options, add to cart. Price and add to cart are
|
|
99
102
|
visible without scrolling on a 390×844 screen. Options: sold-out values stay visible but disabled
|
|
100
103
|
(`isOptionValueAvailable`), never hidden. Stock text comes from `quantityAvailable`; do not add
|
|
@@ -128,10 +131,11 @@ tap targets at least 44px, no hover-only controls, no information that only appe
|
|
|
128
131
|
|
|
129
132
|
## 5. Stack
|
|
130
133
|
|
|
131
|
-
Keep the starter's approach: plain CSS with custom properties
|
|
132
|
-
framework. Add Tailwind v4 or Motion only when the owner of this
|
|
133
|
-
Motion, use it only in `'use client'` leaf components on the home
|
|
134
|
-
`next/font`. Before importing any package, check `package.json`
|
|
134
|
+
Keep the starter's approach: plain CSS with custom properties, one file per layer in
|
|
135
|
+
`app/styles/` (`DESIGN.md`), no UI framework. Add Tailwind v4 or Motion only when the owner of this
|
|
136
|
+
storefront asks for it. If you add Motion, use it only in `'use client'` leaf components on the home
|
|
137
|
+
page and content pages. Fonts via `next/font`. Before importing any package, check `package.json`
|
|
138
|
+
(taste skill Section 3.F).
|
|
135
139
|
|
|
136
140
|
## Overrides
|
|
137
141
|
|
package/template/AGENTS.md
CHANGED
|
@@ -51,6 +51,13 @@ from a landing page: brand colors come from `shop.branding` at runtime, product
|
|
|
51
51
|
come only from the API, nothing is invented (prices, badges, reviews, urgency), and the cart and
|
|
52
52
|
checkout stay conventional.
|
|
53
53
|
|
|
54
|
+
`DESIGN.md` is the design system: tokens (`app/styles/tokens.css`), type, the global theme
|
|
55
|
+
settings (`theme/settings.ts` → `data-*` on `<html>`: corner radius, image ratio, card style,
|
|
56
|
+
sticky header, vendor, rating, cart type, announcement), the CSS layers in `app/styles/`, and the
|
|
57
|
+
contrast and layout-shift rules. Build pages from the primitives in `components/ui/` (`Button`,
|
|
58
|
+
`Drawer`, `Accordion`, `QuantityStepper`, `Badge`, `Skeleton`, `Breadcrumbs`, `EmptyState`,
|
|
59
|
+
`Field`, `Price`, `Rating`, `Icon`) — never one-off markup with its own styles.
|
|
60
|
+
|
|
54
61
|
Before calling visual work done, follow `.claude/skills/storefront-verify/SKILL.md`: build, run,
|
|
55
62
|
screenshot every page type at 390 and 1280px, fix what you see. Interactive UI follows
|
|
56
63
|
`.claude/skills/web-interface-guidelines/SKILL.md`; React and Next.js code follows
|
|
@@ -58,22 +65,31 @@ screenshot every page type at 390 and 1280px, fix what you see. Interactive UI f
|
|
|
58
65
|
|
|
59
66
|
## Where things go
|
|
60
67
|
|
|
61
|
-
| Path | What
|
|
62
|
-
| ------------------------------- |
|
|
63
|
-
| `app/` | Pages (server components) and route handlers
|
|
64
|
-
| `app/error.tsx` | Error boundary: the API's `detail`, a retry
|
|
65
|
-
| `app/
|
|
66
|
-
| `app/
|
|
67
|
-
| `app/
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `app/
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
68
|
+
| Path | What |
|
|
69
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
70
|
+
| `app/` | Pages (server components) and route handlers |
|
|
71
|
+
| `app/error.tsx` | Error boundary: the API's `detail`, a retry |
|
|
72
|
+
| `app/not-found.tsx` | 404 with search; a route with a `loading.tsx` checks its resource in `layout.tsx` (a real 404 status) |
|
|
73
|
+
| `app/collections/` | All collections; a collection with facets and sort from the URL (`lib/catalog-params.ts`, `components/catalog/`) |
|
|
74
|
+
| `app/products/[handle]` | Product: `components/product/` (gallery, variant picker, buy box, reviews, recommendations) |
|
|
75
|
+
| `app/cart`, `app/checkout` | Cart page (`components/cart/`), checkout steps (`components/checkout/`); titles in their `layout.tsx` |
|
|
76
|
+
| `app/account/` | Overview, order, addresses, profile (`components/account/`) |
|
|
77
|
+
| `app/wishlist`, `app/contacts` | Wishlist (`features.wishlist`), contacts from `api.contacts()` (`lib/contacts.ts`) |
|
|
78
|
+
| `app/blog/` | Blog: index, category, author, tag and article pages (`PAGES.md`, Blog) |
|
|
79
|
+
| `app/sitemap.ts` | Sitemap from `GET /sitemap` |
|
|
80
|
+
| `app/page.tsx` | Home: the theme's `index` template (`loadTemplate` + `ThemeSections`, `PAGES.md`) |
|
|
81
|
+
| `theme/` | The theme: `sections/` (schema + component), `templates/*.json`, `settings.ts`, registered in `index.ts` |
|
|
82
|
+
| `components/sections/` | One renderer per `GET /home` section type (the `platform-home` theme section); an unknown type renders nothing |
|
|
83
|
+
| `components/` | Shared UI; client components start with `'use client'` |
|
|
84
|
+
| `components/ui/` | Primitives every page is built from (`DESIGN.md`, Components) |
|
|
85
|
+
| `components/header/`, `footer/` | Desktop nav and phone drawer from `lib/menu.ts`; footer columns and contacts |
|
|
86
|
+
| `app/styles/` | Layered CSS (`tokens`, `base`, `components`, `layout`, `sections`, `pages`, `utilities`), imported by `globals.css` |
|
|
87
|
+
| `components/telegram-shell.tsx` | Telegram Mini App: theme marks, back arrow, the one automatic Telegram sign-in (`PAGES.md`) |
|
|
88
|
+
| `lib/api.ts` | The server-side client (`nextCacheFetch`), `orNotFound` |
|
|
89
|
+
| `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist, analytics in the browser |
|
|
90
|
+
| `app/storefront-api/[...path]` | Same-origin proxy (`createStorefrontProxy`); holds tokens and cart / checkout ids in httpOnly cookies |
|
|
91
|
+
| `app/api/magicstore/webhook` | Signed webhook → `revalidateTag` |
|
|
92
|
+
| `app/api/magicstore/preview` | Theme editor preview: signed token → draft mode; pages pass `themePreview()` (`lib/preview.ts`) as `preview` |
|
|
77
93
|
|
|
78
94
|
## Creating a new section
|
|
79
95
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Design
|
|
2
|
+
|
|
3
|
+
The contract for anyone (person or agent) who restyles this storefront. It says what the design is
|
|
4
|
+
made of and where each part lives; the reasoning and the rules behind it are in the skills, not here:
|
|
5
|
+
|
|
6
|
+
- `.claude/skills/storefront-design/SKILL.md`: what a store needs, what is off limits, pre-flight.
|
|
7
|
+
It wins over everything below.
|
|
8
|
+
- `.claude/skills/design-taste-frontend/SKILL.md`: taste (design read, dials, anti-slop rules).
|
|
9
|
+
- `.claude/skills/web-interface-guidelines/SKILL.md`: keyboard, focus, forms, touch targets.
|
|
10
|
+
- `.claude/skills/storefront-verify/SKILL.md`: look at every page type before calling it done.
|
|
11
|
+
|
|
12
|
+
## Design read
|
|
13
|
+
|
|
14
|
+
> Reading this as: a general-purpose commerce theme for mainstream online shoppers in Uzbekistan
|
|
15
|
+
> and Russia (mobile-first, often inside Telegram), with a calm Dawn-style language, accent from
|
|
16
|
+
> `branding.colors.main`, leaning toward one neutral grotesk (Onest) with Cyrillic and Uzbek Latin,
|
|
17
|
+
> portrait 3:4 product media, and low density.
|
|
18
|
+
|
|
19
|
+
What the shops showed (2026-10-03, `humo.magicstore.dev`, `market.magicbot.test`):
|
|
20
|
+
|
|
21
|
+
- Catalogs of electronics accessories, cosmetics, clothing, toys: anything. Prices from 27 000 to
|
|
22
|
+
2 650 000 UZS, often with a `compareAtPrice`.
|
|
23
|
+
- Product photos are marketplace cards: 3:4 portrait (600×800, 800×1066), busy, saturated, with
|
|
24
|
+
text printed on them. The interface around them stays quiet: neutral surfaces, one accent, no
|
|
25
|
+
decoration competing with the photo.
|
|
26
|
+
- Branding: a saturated accent (`#16A34A`, `#2563eb`), a dark navbar (`#052E16`, `#0F172A`) with
|
|
27
|
+
white text, a logo. Image sizes come without `width`/`height`, so every media box has a fixed
|
|
28
|
+
ratio.
|
|
29
|
+
|
|
30
|
+
Dials (taste skill §1):
|
|
31
|
+
|
|
32
|
+
| Pages | `DESIGN_VARIANCE` | `MOTION_INTENSITY` | `VISUAL_DENSITY` |
|
|
33
|
+
| ----------------------------------------- | ----------------- | ------------------ | ---------------- |
|
|
34
|
+
| Catalog, product, cart, checkout, account | 4 | 3 | 4 |
|
|
35
|
+
| Home and content pages (blog, pages) | 5 | 4 | 3 |
|
|
36
|
+
|
|
37
|
+
## Tokens
|
|
38
|
+
|
|
39
|
+
All in `app/styles/tokens.css`. Components never use a raw color, size or radius; they use these.
|
|
40
|
+
|
|
41
|
+
| Group | Tokens | Notes |
|
|
42
|
+
| -------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
43
|
+
| Brand | `--accent --on-accent --accent-text --nav-bg --nav-fg --label-bg --label-fg` | From `shop.branding` at runtime (`lib/brand.ts`); fallbacks in `tokens.css` |
|
|
44
|
+
| Neutrals | `--bg --fg --muted --line --surface --placeholder --subtle --danger --success --warning` | Cool neutral grey; Telegram's theme replaces them in a Mini App |
|
|
45
|
+
| Spacing | `--space-1` … `--space-10` (4, 8, 12, 16, 24, 32, 48, 64, 96, 128 px) | Gaps and padding only from this scale |
|
|
46
|
+
| Type | `--text-xs` … `--text-4xl`, `--leading-tight/snug/normal`, `--font-body`, `--font-heading` | Sizes from `xl` up are fluid (`clamp`) |
|
|
47
|
+
| Shape | `--radius-sm` (controls) `--radius-md` (cards, media) `--radius-lg` (panels) `--radius-pill` | One scale, set by the `cornerRadius` theme setting |
|
|
48
|
+
| Depth | `--shadow-1` (raised) `--shadow-2` (drawers, menus) | Tinted with the foreground, never pure black |
|
|
49
|
+
| Layout | `--container` (1320px) `--gutter` (16px phone, up to 48px) `--header-h` | |
|
|
50
|
+
| Layers | `--z-header --z-drawer --z-toast` | Nothing else sets `z-index` above 1 |
|
|
51
|
+
| Motion | `--duration-fast --duration --ease` | `0ms` under `prefers-reduced-motion` |
|
|
52
|
+
| Media | `--media-ratio` | Product image ratio, from the `productImageRatio` theme setting |
|
|
53
|
+
|
|
54
|
+
Breakpoints, mobile first: **750px** (tablet) and **990px** (desktop).
|
|
55
|
+
|
|
56
|
+
Shape rule (taste skill §4.4): buttons, inputs and chips use `--radius-sm` (chips and badges may use
|
|
57
|
+
`--radius-pill`); cards and images `--radius-md`; drawers and menus `--radius-lg`. Nothing else.
|
|
58
|
+
|
|
59
|
+
## Type
|
|
60
|
+
|
|
61
|
+
Onest (variable, 100 to 900, OFL, `app/fonts/`), self-hosted through `next/font/local` so builds
|
|
62
|
+
need no network. It covers Cyrillic (with `Ғ Қ Ҳ Ў`) and Uzbek Latin (`oʻ gʻ`, U+02BB). One family:
|
|
63
|
+
headings take weight 600, body 400, prices 500. No second display face, no serif.
|
|
64
|
+
|
|
65
|
+
## Theme settings
|
|
66
|
+
|
|
67
|
+
Merchant-editable in the admin (`theme/settings.ts`), mapped by `app/layout.tsx` to `data-*` on
|
|
68
|
+
`<html>` and read by `tokens.css`:
|
|
69
|
+
|
|
70
|
+
| Setting | `data-*` | Values |
|
|
71
|
+
| ------------------------------------------------------ | ---------------------- | -------------------------------------------- |
|
|
72
|
+
| `cornerRadius` | `data-radius` | `none`, `small` (default), `medium`, `large` |
|
|
73
|
+
| `productImageRatio` | `data-image-ratio` | `square`, `portrait` (default), `natural` |
|
|
74
|
+
| `cardStyle` | `data-card-style` | `minimal` (default), `card` |
|
|
75
|
+
| `stickyHeader` | `data-sticky-header` | on by default |
|
|
76
|
+
| `showVendor`, `showRating`, `cartType`, `announcement` | — (read by components) | |
|
|
77
|
+
|
|
78
|
+
Colors are not theme settings: they stay in `shop.branding`.
|
|
79
|
+
|
|
80
|
+
## CSS architecture
|
|
81
|
+
|
|
82
|
+
`app/globals.css` declares the layer order and imports one file per layer from `app/styles/`:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
@layer reset, tokens, base, components, layout, sections, pages, utilities;
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Class names are BEM-lite without a prefix: a block (`.product-card`), its elements
|
|
89
|
+
(`.product-card__media`), modifiers as a second class (`.button--primary`) or a `data-*` attribute
|
|
90
|
+
for state (`[data-state='open']`, `[aria-expanded='true']`). Telegram overrides stay on
|
|
91
|
+
`html[data-telegram]`.
|
|
92
|
+
|
|
93
|
+
## Components
|
|
94
|
+
|
|
95
|
+
UI primitives in `components/ui/` (reuse them, never restyle a page with one-off markup): `Icon`,
|
|
96
|
+
`Button`, `IconButton`, `Drawer`, `Accordion`, `QuantityStepper`, `Badge`, `Skeleton`,
|
|
97
|
+
`Breadcrumbs`, `EmptyState`, `Field`, `Price`, `Rating`.
|
|
98
|
+
|
|
99
|
+
## Accessibility and stability
|
|
100
|
+
|
|
101
|
+
- Contrast AA (4.5:1) for what the design owns: a text color the merchant left empty is black or
|
|
102
|
+
white by contrast (`lib/brand.ts`); a pair the merchant set is theirs (point a low one out to the
|
|
103
|
+
owner); the accent used as text on the page is `--accent-text` (half way to `--fg`, AA for pale
|
|
104
|
+
and dark accents alike). In Telegram's dark scheme the status colors switch to lighter values.
|
|
105
|
+
Text on a status or muted fill uses `var(--bg)`, not a fixed white.
|
|
106
|
+
- Tap targets at least 44px (24px with spacing for text links in lists); a visible focus ring on
|
|
107
|
+
everything focusable; drawers trap focus and return it; nothing only on hover.
|
|
108
|
+
- No layout shift: images keep their box (`aspect-ratio`), the LCP image is eager with
|
|
109
|
+
`fetchpriority="high"`, `loading.tsx` skeletons take the final sizes, and `.site-main` is at least
|
|
110
|
+
a screen tall so the footer never moves on screen.
|
|
111
|
+
|
|
112
|
+
## Merchant data vs design
|
|
113
|
+
|
|
114
|
+
The merchant owns: colors, logo, favicon, every product, collection, page and article, banner
|
|
115
|
+
images, section content and order, the announcement. The design owns: neutrals, type, spacing,
|
|
116
|
+
shape, layout, interface words (`lib/i18n.ts`). A missing value hides its block; it is never
|
|
117
|
+
replaced with invented copy, images, ratings or urgency.
|
package/template/PAGES.md
CHANGED
|
@@ -42,6 +42,12 @@ storefront token sends the browser straight to the API; then the tokens and the
|
|
|
42
42
|
## Layout — `app/layout.tsx`
|
|
43
43
|
|
|
44
44
|
- Reads `api.shop()` and the navigation (`api.collectionsIndex` or `api.menusShow({ path: { handle: 'main' } })`).
|
|
45
|
+
`lib/menu.ts` turns them into links: `mainNav` (the `main` menu, else the root collections) for
|
|
46
|
+
`components/header/desktop-nav.tsx` and the phone drawer `components/header/mobile-menu.tsx`;
|
|
47
|
+
`footerNav` (the `footer` menu) for `components/footer/site-footer.tsx`, whose contact column
|
|
48
|
+
comes from `api.contacts()` through `lib/contacts.ts`.
|
|
49
|
+
- `<main>` is at least a screen tall, so the footer starts below the fold: a streamed page or a
|
|
50
|
+
client list replacing its placeholder moves nothing on screen.
|
|
45
51
|
- `generateMetadata`: `shop.seo.title || shop.name`, `shop.seo.description ?? shop.description`,
|
|
46
52
|
the favicon from `shop.branding.favicon`.
|
|
47
53
|
- The merchant's colors: `brandStyle(shop.branding)` (`lib/brand.ts`) on `<html style>`, as the
|
|
@@ -121,40 +127,73 @@ a contract change. The steps, the setting types and a worked example (`size-guid
|
|
|
121
127
|
`AGENTS.md` and `.claude/skills/theme-section/SKILL.md`; `theme/sections/testimonials.tsx` is a
|
|
122
128
|
section with blocks to copy from.
|
|
123
129
|
|
|
130
|
+
## Collections — `app/collections/page.tsx`
|
|
131
|
+
|
|
132
|
+
- Every root collection: `api.collectionsIndex({ query: { 'filter[root]': 'true', perPage: 100 } })`
|
|
133
|
+
as tiles (`CollectionTiles` in `components/sections/collections.tsx`; text cards when no
|
|
134
|
+
collection has an image), with its product count. The footer and the phone menu link it.
|
|
135
|
+
- States: no collections yet (an empty state with search).
|
|
136
|
+
|
|
124
137
|
## Collection — `app/collections/[handle]/page.tsx`
|
|
125
138
|
|
|
126
|
-
- `
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
- `app/collections/[handle]/layout.tsx` checks `api.collectionsShow` through `orNotFound` before
|
|
140
|
+
anything streams, so a missing handle is a real 404 (the route's `loading.tsx` would have sent
|
|
141
|
+
200). The page reads the same cached request.
|
|
142
|
+
- `api.collectionsShow`, `api.collectionsFilters` and the shop, then `api.collectionsProducts` with
|
|
143
|
+
the URL's filters, and the child collections (`hasChildren`) as chips; the FAQ below.
|
|
144
|
+
- Filters and sort live in the URL, so every view is a link and works without JavaScript:
|
|
145
|
+
`lib/catalog-params.ts` reads them (`parseCatalog`) against the facets the API offered and
|
|
146
|
+
drops the rest — an unknown filter or sort is a `VALIDATION_FAILED`, not ignored — then builds
|
|
147
|
+
the API query (`apiQuery`) and every link (`catalogQuery`, `without`, `cleared`, `onPage`).
|
|
148
|
+
`components/catalog/catalog-layout.tsx` renders the facets (`facets.tsx`: lists, price range,
|
|
149
|
+
in stock; a sidebar on desktop, a drawer on phones through `facets-shell.tsx`), the count, the
|
|
150
|
+
sort, the active filters as removable chips, the grid and the pager. Search uses the same layout.
|
|
151
|
+
- SEO: `pageMeta({ seo: collection.seo, title, description })`, `breadcrumbJsonLd`; filtered and
|
|
152
|
+
sorted views are `noindex`.
|
|
153
|
+
- States: an empty collection ("Nothing here yet"), a filter with no match (clear the filters), a
|
|
154
|
+
`page` past the last one (empty list), 404.
|
|
155
|
+
|
|
156
|
+
## Product — `app/products/[handle]/page.tsx`, `components/product/`
|
|
157
|
+
|
|
158
|
+
- `app/products/[handle]/layout.tsx` checks the product exists before the page streams (a real 404,
|
|
159
|
+
as for a collection).
|
|
160
|
+
- `api.productsShow({ path: { handle } })` through `orNotFound`, then in parallel: the shop,
|
|
161
|
+
`productsReviews` (first page; `meta.rating` has the average and the per-rating `breakdown`) and
|
|
162
|
+
`productsRecommendations` (`intent=COMPLEMENTARY` "pairs well with", `RELATED` "you may also
|
|
163
|
+
like"). All are catalog reads, cached like the product.
|
|
164
|
+
- `ProductSelection` (`product-selection.tsx`) holds `useVariantSelection(product)` for the parts
|
|
165
|
+
that depend on the variant: `Gallery` shows its `image`, `VariantPicker` its options (pills, a
|
|
166
|
+
`<select>` past eight values), `BuyBox` its price, stock and add to cart.
|
|
167
|
+
- The buy box: `<Price>` of the selected variant (else the product), the stock the API reports
|
|
168
|
+
(`quantityAvailable`; nothing when `null`), `QuantityStepper` bounded by it,
|
|
169
|
+
`useCart().addLine({ productId, variantId, quantity })` then the cart drawer, `useWishlist()`
|
|
170
|
+
(only with `features.wishlist`), `shop.checkout.deliveryText`, and
|
|
171
|
+
`useAnalytics().productView(...)`. A product with `preOrder` takes a pre-order instead
|
|
172
|
+
(`pre-order-form.tsx`, `preOrdersStore`: name and phone, none for a signed-in customer).
|
|
173
|
+
- Below: description and `faq` as accordions, share (Web Share API, else copy the link); reviews
|
|
174
|
+
(`reviews.tsx` server-renders the summary and first page, `review-list.tsx` loads more and
|
|
175
|
+
filters by `filter[rating]`); the two recommendation shelves.
|
|
142
176
|
- SEO: `pageMeta({ seo, title, description, images })`, and
|
|
143
|
-
`jsonLdScript(productJsonLd(product, { url }))`
|
|
177
|
+
`jsonLdScript(productJsonLd(product, { url }))` plus `breadcrumbJsonLd` in
|
|
178
|
+
`<script type="application/ld+json">`.
|
|
144
179
|
- States: 404; out of stock (`availableForSale` false on the product or the selected variant —
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
show `detail`); no images (
|
|
180
|
+
"Sold out", no stepper); an option value no variant for sale has (`isAvailable(name, value)`
|
|
181
|
+
false — struck through, still selectable: it moves to the closest variant); a failed add
|
|
182
|
+
(`CART_LINES_UNAVAILABLE` and others: show `detail`); no images (a placeholder); no reviews (the
|
|
183
|
+
section is hidden); no recommendations (the shelf is hidden).
|
|
148
184
|
|
|
149
185
|
## Search — `app/search/page.tsx`
|
|
150
186
|
|
|
151
187
|
- `api.search({ query: { q, page, perPage } })` — not cached (no tag). Suggestions while typing:
|
|
152
|
-
`searchSuggestions({ query: { q } })` from the browser; popular queries:
|
|
153
|
-
|
|
154
|
-
-
|
|
188
|
+
`searchSuggestions({ query: { q } })` from the browser (`components/search/`); popular queries:
|
|
189
|
+
`searchTrending`.
|
|
190
|
+
- Facets: `searchFilters({ query: { q } })`, rendered by the collection's `CatalogLayout` with `q`
|
|
191
|
+
kept in every link.
|
|
192
|
+
- States: no query yet (the form and popular searches), no results (an empty state and popular
|
|
193
|
+
searches), no match for the filters (clear them), pagination.
|
|
155
194
|
- Search pages are usually `noindex`; report `useAnalytics().search(q, total)`.
|
|
156
195
|
|
|
157
|
-
## Cart — `app/cart/page.tsx`
|
|
196
|
+
## Cart — `app/cart/page.tsx`, `components/cart/`
|
|
158
197
|
|
|
159
198
|
- Client only: `useCart()` — `cart`, `status` (`loading` / `updating` / `idle`), `error`, and the
|
|
160
199
|
changes (`updateLine`, `removeLine`, `setDiscountCodes`, `setNote`, `setGift`, `setPoints`).
|
|
@@ -162,9 +201,13 @@ section with blocks to copy from.
|
|
|
162
201
|
Discount codes answer `applicable` and a `reasonCode`.
|
|
163
202
|
- States: loading (the stored cart is being fetched), empty or no cart, a line no longer available,
|
|
164
203
|
a rejected code, the last failed change (`error`, the cart is already back to the server's).
|
|
204
|
+
- The page lists the lines; `components/cart/cart-summary.tsx` holds the discount codes (a
|
|
205
|
+
rejected one says why, from `reasonCode`), the note (saved on blur through `setNote`), the
|
|
206
|
+
discounts and the total from `cart.cost`, and the checkout button. The cart drawer is the same
|
|
207
|
+
cart, opened after every add.
|
|
165
208
|
- Never put the cart id in a URL.
|
|
166
209
|
|
|
167
|
-
## Checkout — `app/checkout/page.tsx`
|
|
210
|
+
## Checkout — `app/checkout/page.tsx`, `components/checkout/`
|
|
168
211
|
|
|
169
212
|
- Client only. `checkoutsStore({ body: { cartId } })` (safe to repeat), then fill what
|
|
170
213
|
`checkout.missing` lists, in order: `CONTACT` → `DELIVERY_ADDRESS` (or a pickup point from
|
|
@@ -176,10 +219,25 @@ section with blocks to copy from.
|
|
|
176
219
|
- States: empty cart, preparing, each step's validation errors (`VALIDATION_FAILED` → per field),
|
|
177
220
|
`DELIVERY_QUOTE_EXPIRED` (list the options again), `CHECKOUT_NOT_READY`, `CHECKOUT_CLOSED`,
|
|
178
221
|
`STORE_UNAVAILABLE`, a payment that is still pending.
|
|
222
|
+
- `components/checkout/use-checkout.ts` runs the steps and maps `VALIDATION_FAILED` to
|
|
223
|
+
`fieldErrors`; `steps.tsx` renders contact, delivery and payment (`radio-card.tsx` for the
|
|
224
|
+
choices) behind `step-indicator.tsx`; `order-summary.tsx` shows the lines and `checkout.cost`
|
|
225
|
+
(collapsed on phones); `thank-you.tsx` the order number after completion.
|
|
179
226
|
- Never put the checkout id in a URL or a log.
|
|
180
227
|
|
|
181
|
-
## Account — `app/account/
|
|
182
|
-
|
|
228
|
+
## Account — `app/account/`, `components/account/`
|
|
229
|
+
|
|
230
|
+
- Pages: overview with the orders (`app/account/page.tsx`), an order
|
|
231
|
+
(`app/account/orders/[id]/page.tsx`), addresses (`app/account/addresses/page.tsx`) and the
|
|
232
|
+
profile (`app/account/profile/page.tsx`), each inside `components/account/account-shell.tsx`
|
|
233
|
+
(sign-in gate, header, navigation). Client pages take their titles from
|
|
234
|
+
`app/account/layout.tsx`.
|
|
235
|
+
- An order (`order-detail.tsx`): its lines, amounts and status; "Order again" calls
|
|
236
|
+
`useCart().reorder(orderId)` (the server copies what is still for sale into a new cart, which
|
|
237
|
+
becomes the buyer's) and opens the cart; "Cancel" asks to confirm, then
|
|
238
|
+
`customerOrdersCancellation`.
|
|
239
|
+
- Addresses: `customerAddresses*` (add, edit, delete, make default). Profile: name, gender,
|
|
240
|
+
language through `customerUpdate`, and sign out.
|
|
183
241
|
- Client only. Signed out:
|
|
184
242
|
- Phone: `useCustomer().requestOtp(phone)` → `verifyOtp(phone, code)` (`OTP_INVALID`: re-type;
|
|
185
243
|
`OTP_EXPIRED`: ask again). On a shop whose sign-in code is off
|
|
@@ -246,7 +304,7 @@ same API.
|
|
|
246
304
|
|
|
247
305
|
All blog reads are `magicstore:pages`. Rules: the backend's `docs/api/v2/blog.md`.
|
|
248
306
|
|
|
249
|
-
- **Index** `app/blog/page.tsx
|
|
307
|
+
- **Index** `app/blog/(index)/page.tsx` (a route group: its `loading.tsx` covers the index only): heading and intro from `shop.blog` (else `t.blog`), categories
|
|
250
308
|
(`api.blogCategoriesIndex()`, `CategoryNav`), editor's picks (`filter[featured]=true`, first page
|
|
251
309
|
only), a search box (`q`, at most 200 characters) and newest / popular (`sort=-recentViews`),
|
|
252
310
|
then `api.blogArticlesIndex({ query: { page, perPage: 12, … } })` with `Pager`. Only a `sort` the
|
|
@@ -278,6 +336,21 @@ All blog reads are `magicstore:pages`. Rules: the backend's `docs/api/v2/blog.md
|
|
|
278
336
|
`X-Anonymous-Id`; the proxy forwards it). `ArticleView` reports `ARTICLE_VIEW`.
|
|
279
337
|
- Dates render in `shop.timezone` (`ArticleDate`).
|
|
280
338
|
|
|
339
|
+
## Wishlist — `app/wishlist/page.tsx`
|
|
340
|
+
|
|
341
|
+
- Only with `shop.features.wishlist` (else 404); `noindex`. `useWishlist()` holds the ids (the
|
|
342
|
+
browser for a guest, the account when signed in); `components/wishlist/wishlist-view.tsx` reads
|
|
343
|
+
them with `productsIndex({ query: { 'filter[ids]': … } })` in chunks of 100 and keeps the
|
|
344
|
+
wishlist's order. A guest sees that signing in keeps the list.
|
|
345
|
+
- States: empty, signed out (the hint), a product no longer for sale (dropped by the API).
|
|
346
|
+
|
|
347
|
+
## Contacts — `app/contacts/page.tsx`
|
|
348
|
+
|
|
349
|
+
- `api.contacts()`: phones and emails (`tel:` and `mailto:` links via `lib/contacts.ts`), the head
|
|
350
|
+
office with its hours (weekday names from `Intl`, closed days), branches with a map link,
|
|
351
|
+
requisites, socials. Nothing the shop did not fill in is shown; no contacts at all → an empty
|
|
352
|
+
state.
|
|
353
|
+
|
|
281
354
|
## Content pages, sitemap — `app/pages/[handle]/page.tsx`, `app/sitemap.ts`
|
|
282
355
|
|
|
283
356
|
- `api.pagesShow({ path: { handle } })` (`magicstore:pages`) through `orNotFound`,
|
|
@@ -291,7 +364,12 @@ updatedAt }` rows — on `SITE_URL` or the shop's `primaryDomain` (`lib/site.ts`
|
|
|
291
364
|
## Every page
|
|
292
365
|
|
|
293
366
|
- Import only the SDK's entry points; `npx create-magic-storefront check` passes.
|
|
294
|
-
- Not found → `notFound()
|
|
367
|
+
- Not found → `notFound()` before anything streams: a route with a `loading.tsx` checks the
|
|
368
|
+
resource in its `layout.tsx` (see Collection), else the answer is a 200 marked `noindex`. The blog
|
|
369
|
+
index keeps its skeleton in the route group `app/blog/(index)/` so articles are not covered by it.
|
|
370
|
+
- Loading: `loading.tsx` skeletons (`components/page-loading.tsx`, `components/ui/skeleton.tsx`)
|
|
371
|
+
take the final layout's sizes.
|
|
372
|
+
- Any other failure shows `describe(error, t.tryAgainLater)`
|
|
295
373
|
(`lib/errors.ts`) or lets `app/error.tsx` catch it (it shows `detail`, and "temporarily closed"
|
|
296
374
|
on `STORE_UNAVAILABLE`).
|
|
297
375
|
- The interface speaks the storefront's language: every word the storefront itself shows comes from
|
package/template/README.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
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 (or your own composition: `GET /home` is optional),
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
sections (or your own composition: `GET /home` is optional), all collections, a collection and
|
|
6
|
+
search with facets and sort, product with gallery, variants, reviews and recommendations, wishlist,
|
|
7
|
+
cart drawer and cart page, a stepped checkout (pickup or delivery, payment), an account (OTP
|
|
8
|
+
sign-in, orders with reorder and cancel, addresses, profile), blog, contacts and content pages,
|
|
9
|
+
SEO and JSON-LD, a webhook that revalidates cached pages — and it runs as a Telegram Mini App
|
|
10
|
+
(sign-in from the bot, back button, Telegram's theme, phone sharing). The design is a calm,
|
|
11
|
+
Dawn-like theme in plain CSS with tokens; the merchant's colors and theme settings restyle it.
|
|
8
12
|
|
|
9
13
|
```bash
|
|
10
14
|
cp .env.example .env.local # set MAGICSTORE_SHOP_DOMAIN
|
|
@@ -15,6 +19,7 @@ npm run dev
|
|
|
15
19
|
| Where | What |
|
|
16
20
|
| ------------------------------ | ----------------------------------------------------------------------------- |
|
|
17
21
|
| `PAGES.md` | How each page type is built: data calls, cache tags, SEO, required states. |
|
|
22
|
+
| `DESIGN.md` | The design system: tokens, type, theme settings, CSS layers, UI primitives. |
|
|
18
23
|
| `lib/api.ts` | The server-side client. Public reads are cached under `magicstore:*` tags. |
|
|
19
24
|
| `components/sections/` | One renderer per `GET /home` section type; an unknown type renders nothing. |
|
|
20
25
|
| `app/providers.tsx` | `MagicStoreProvider`: customer, cart, wishlist and analytics in the browser. |
|
|
@@ -41,3 +46,10 @@ The same secret signs the theme editor's preview links: the admin opens
|
|
|
41
46
|
and serves the draft theme to that browser only (`lib/preview.ts`); `?exit=1` ends it. Previews also need
|
|
42
47
|
`MAGICSTORE_STOREFRONT_TOKEN`, the storefront token that webhook belongs to: the API previews only a
|
|
43
48
|
read that names its token.
|
|
49
|
+
|
|
50
|
+
**Look before you ship.** After a visual change, build, run and screenshot every page type at 390
|
|
51
|
+
and 1280px (`.claude/skills/storefront-verify/SKILL.md`); the screenshots go to `.screenshots/`,
|
|
52
|
+
which git ignores.
|
|
53
|
+
|
|
54
|
+
**Font.** Onest, self-hosted from `app/fonts/` (SIL Open Font License 1.1, `app/fonts/OFL.txt`), so
|
|
55
|
+
builds need no network.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMagicStore } from '@magicstoreai/hydrogen';
|
|
4
|
+
import { AccountShell } from '@/components/account/account-shell';
|
|
5
|
+
import { Addresses } from '@/components/account/addresses';
|
|
6
|
+
import { messages } from '@/lib/i18n';
|
|
7
|
+
|
|
8
|
+
export default function AddressesPage() {
|
|
9
|
+
const t = messages(useMagicStore().locale);
|
|
10
|
+
return (
|
|
11
|
+
<AccountShell>
|
|
12
|
+
<h2 className="account__section-title">{t.addresses}</h2>
|
|
13
|
+
<Addresses />
|
|
14
|
+
</AccountShell>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { locale } from '@/lib/api';
|
|
4
|
+
import { messages } from '@/lib/i18n';
|
|
5
|
+
|
|
6
|
+
// Account pages are client components; their title and robots come from here. Personal: not indexed.
|
|
7
|
+
export const metadata: Metadata = {
|
|
8
|
+
title: messages(locale).account,
|
|
9
|
+
robots: { index: false, follow: false },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default function AccountLayout({ children }: { children: ReactNode }) {
|
|
13
|
+
return children;
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PageLoading } from '@/components/page-loading';
|
|
2
|
+
import { Skeleton, TitleSkeleton } from '@/components/ui/skeleton';
|
|
3
|
+
|
|
4
|
+
export default function AccountLoading() {
|
|
5
|
+
return (
|
|
6
|
+
<PageLoading>
|
|
7
|
+
<TitleSkeleton width="30%" />
|
|
8
|
+
<div className="stack">
|
|
9
|
+
<Skeleton height="5.5rem" />
|
|
10
|
+
<Skeleton height="5.5rem" />
|
|
11
|
+
<Skeleton height="5.5rem" />
|
|
12
|
+
</div>
|
|
13
|
+
</PageLoading>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useParams } from 'next/navigation';
|
|
4
|
+
import { AccountShell } from '@/components/account/account-shell';
|
|
5
|
+
import { OrderDetail } from '@/components/account/order-detail';
|
|
6
|
+
|
|
7
|
+
/** One order. Order ids are the customer's own (another's answers `NOT_FOUND`), so the path is safe. */
|
|
8
|
+
export default function OrderPage() {
|
|
9
|
+
const { id } = useParams<{ id: string }>();
|
|
10
|
+
return (
|
|
11
|
+
<AccountShell>
|
|
12
|
+
<OrderDetail id={id} />
|
|
13
|
+
</AccountShell>
|
|
14
|
+
);
|
|
15
|
+
}
|