@thorprovider/storefront 0.4.0-mvp.0 → 0.4.0-mvp.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/README.md +173 -153
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +42 -1
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +1 -5
- package/assets/templates/app/app/robots.ts +9 -3
- package/assets/templates/app/app/sitemap.ts +64 -5
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +70 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +15 -5
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +36 -4
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +82 -50
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -0
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +29 -14
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +31 -7
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +4 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +56 -26
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +165 -0
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +26 -7
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +2 -1
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/features/checkout/handlers.ts +6 -0
- package/assets/templates/features/customer-auth/state.ts +10 -1
- package/assets/templates/views/account/index.tsx +1 -1
- package/assets/templates/views/account/registry.tsx +80 -19
- package/assets/templates/views/account/schema.ts +1 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +1 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +64 -6
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +2 -1
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/index.tsx +76 -7
- package/assets/templates/views/category/props.ts +7 -0
- package/assets/templates/views/category/registry.tsx +19 -0
- package/assets/templates/views/category/schema.ts +13 -0
- package/assets/templates/views/home/assembly/assembly.json +9 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +22 -4
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +1 -1
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +2 -2
- package/assets/templates/views/home/data.ts +9 -0
- package/assets/templates/views/home/index.tsx +23 -5
- package/assets/templates/views/home/registry.tsx +2 -2
- package/assets/templates/views/product/data.ts +4 -2
- package/assets/templates/views/product/index.tsx +49 -5
- package/assets/templates/views/product/props.ts +7 -0
- package/assets/templates/views/product/registry.tsx +25 -0
- package/assets/templates/views/product/schema.ts +13 -0
- package/assets/templates/views/static/content.ts +4 -4
- package/assets/templates/views/static/index.tsx +1 -1
- package/bin/storefront.js +1231 -154
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +11 -7
- package/recipes/archetypes.json +9 -22
- package/recipes/catalog.json +40 -1
- package/recipes/features.json +25 -2
- package/recipes/recipe.schema.json +1 -1
- package/recipes/recipes.json +32 -30
- package/recipes/sections.json +10 -0
- package/recipes/validate.mjs +54 -0
- package/scripts/candidate.mjs +4 -4
- package/scripts/composers.mjs +25 -5
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +5 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/pinning.mjs +74 -5
- package/scripts/plan-apply.mjs +149 -5
- package/scripts/reconcile.mjs +49 -11
- package/scripts/render-templates.mjs +97 -44
- package/scripts/section-config.mjs +253 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +47 -1
- package/skills/sf-feature/SKILL.md +32 -19
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +99 -36
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +63 -40
- package/skills/sf-view-gen/assets/recipes/archetypes.json +9 -22
- package/skills/sf-view-gen/assets/recipes/catalog.json +40 -1
- package/skills/sf-view-gen/assets/recipes/features.json +25 -2
- package/skills/sf-view-gen/assets/recipes/recipes.json +32 -30
- package/skills/sf-view-gen/assets/recipes/sections.json +10 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +28 -37
- package/skills/sf-view-gen/scripts/validate.mjs +54 -0
- package/skills/storefront-workflow/SKILL.md +94 -31
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -84
- package/assets/templates/views/user-panel/index.tsx +0 -106
- package/assets/templates/views/user-panel/registry.tsx +0 -54
- package/assets/templates/views/user-panel/schema.ts +0 -79
- package/commands/sf-add-view.md +0 -57
- package/commands/sf-feature.md +0 -28
- package/commands/sf-theme.md +0 -26
- package/commands/sf-view.md +0 -67
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# AGENTS.md — __SITE_NAME__ storefront
|
|
2
|
+
|
|
3
|
+
This storefront was generated by `@thorprovider/storefront`. It is governed by a
|
|
4
|
+
manifest (`storefront.config.json` at the project root) and produced by the
|
|
5
|
+
`storefront` CLI. This file is managed: `storefront sync` regenerates it from the
|
|
6
|
+
package template, and `storefront doctor` reports it as drift when edited by
|
|
7
|
+
hand. Read it before changing anything.
|
|
8
|
+
|
|
9
|
+
## RULE 0 — Non-negotiable
|
|
10
|
+
|
|
11
|
+
1. **The `storefront` CLI is the authority.** Every structural change — adding or
|
|
12
|
+
removing a view or feature, composing sections, changing the theme mode,
|
|
13
|
+
connecting the backend — goes through `npx @thorprovider/storefront …`. Never
|
|
14
|
+
copy templates or scaffold structure by hand.
|
|
15
|
+
2. **Derived files are CLI-only; never hand-edit them.** The CLI is the only
|
|
16
|
+
writer of derived files. If a change seems to need a capability the CLI does
|
|
17
|
+
not expose, first check the CLI subcommands and the `plan` → `apply`
|
|
18
|
+
operations envelope; if it is still missing, **STOP and report the gap** —
|
|
19
|
+
do not hand-edit a derived file. A hand edit is silently overwritten by the
|
|
20
|
+
next `sync`.
|
|
21
|
+
3. **Hand-edit only user-owned files and the bounded external surface.** Manual
|
|
22
|
+
edits are limited to the declared user-owned files:
|
|
23
|
+
`lib/<name>/views/<view>/{schema,content,props,data}.ts`, `lib/<name>/mappers.ts`,
|
|
24
|
+
`lib/<name>/icon-map.ts`, and `lib/<name>/shell/**` — plus the bounded external
|
|
25
|
+
customization surface of `app/globals.css` and `app/layout.tsx`: theme design
|
|
26
|
+
tokens and their foreground pairs, font wiring, and global identity/metadata.
|
|
27
|
+
Prefer the deterministic `storefront theme` controls (colors, radius, fonts,
|
|
28
|
+
presets, imports) over hand edits; the bounded surface remains editable when a
|
|
29
|
+
value has no CLI control.
|
|
30
|
+
Editing the external files must preserve the CLI-managed wiring: the library
|
|
31
|
+
stylesheet import, the provider import and marker, the theme-mode marker, and
|
|
32
|
+
the block token contract. The CLI only appends that wiring idempotently, so a
|
|
33
|
+
restructure that removes it is drift, not a customization. Everything else is
|
|
34
|
+
derived (CLI-only).
|
|
35
|
+
4. **Respect the architecture invariants.** UI components come only from
|
|
36
|
+
`@thorprovider/blocks`; data comes only from `@thorprovider/adapters` and
|
|
37
|
+
`@thorprovider/runtime/commerce` (never HTTP directly); routing stays the
|
|
38
|
+
catch-all `app/[[...slug]]/page.tsx`; `@thorprovider/medusa-extended` is not
|
|
39
|
+
used.
|
|
40
|
+
5. **Verify after every change.** Run `npx @thorprovider/storefront sync`, then
|
|
41
|
+
`npx @thorprovider/storefront doctor`. `doctor` must report no drift; a build
|
|
42
|
+
(`yarn build`) is the final acceptance.
|
|
43
|
+
6. **Drive non-trivial customization through the spec-driven loop.** A change
|
|
44
|
+
that needs judgment — content authoring, prop/data mapping, composition
|
|
45
|
+
reshaping, or a cross-file redesign whose acceptance is not a CLI assertion —
|
|
46
|
+
is **non-trivial**: drive it through the spec-driven loop and surface the five
|
|
47
|
+
stages to the user, **explore → plan → build → verify → inform**. A change the
|
|
48
|
+
CLI performs deterministically (theme mode, `view add|remove`,
|
|
49
|
+
`feature add|remove`, `setup`) is recorded in `storefront.config.json` and does
|
|
50
|
+
**not** require an OpenSpec change — the manifest is its record. Never ask the
|
|
51
|
+
user to run `openspec` or to approve individual artifacts.
|
|
52
|
+
|
|
53
|
+
## Workflow model
|
|
54
|
+
|
|
55
|
+
**bootstrap → compose → customize → verify.** The manifest
|
|
56
|
+
(`storefront.config.json`) is the structural source of truth; the CLI is the only
|
|
57
|
+
writer of derived files; the developer (or agent) edits only user-owned files and
|
|
58
|
+
the bounded external surface of `app/globals.css` and `app/layout.tsx`.
|
|
59
|
+
Customization that needs judgment runs through the spec-driven loop surfaced as
|
|
60
|
+
explore → plan → build → verify → inform.
|
|
61
|
+
See the `storefront-workflow` skill for the full model and the ownership classes.
|
|
62
|
+
|
|
63
|
+
## Router — intent → skill → CLI
|
|
64
|
+
|
|
65
|
+
The user states the intent in natural language; load the skill and delegate the
|
|
66
|
+
write to the CLI. Never edit a derived file to achieve the intent.
|
|
67
|
+
|
|
68
|
+
| Intent | Load skill | Delegate to CLI |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| Add or remove a view; compose a view's sections | `sf-view-gen` | `storefront views --json`, then `storefront view add\|remove …`; sections via `plan` → `apply` |
|
|
71
|
+
| Customize the theme: colors, radius, fonts, presets; identity/SEO | `sf-theme-gen` | `storefront theme` — `--brand orange`, `--primary <color>`, `--accent <color>`, `--radius <length>`, `--font-sans google:Inter`, `--set light.primary=#f97316`, `--preset <ref>`, `--import <src>`, `--export <file>`, `--mode light\|dark\|system`. Visual values live in `app/globals.css`; the mode in `storefront.config.json`. Inspect with `storefront theme --inspect`. The bounded external surface stays editable by hand |
|
|
72
|
+
| Add or remove a feature (e.g. `checkout`) | `sf-feature` | `storefront features --json`, then `storefront feature add\|remove …` |
|
|
73
|
+
| Connect or re-point the Medusa backend; change region, sales channel, or publishable key | `sf-setup` | `storefront setup --backend-url … --publishable-key … --sales-channel-id … --region-id …` |
|
|
74
|
+
| Edit a view's content/props/data; map props or data; reshape composition; any change whose acceptance is not a CLI assertion (non-trivial) | `storefront-workflow` | spec-driven loop surfaced as **explore → plan → build → verify → inform** — the agent drives it and never asks the user to confirm OpenSpec artifacts; structural edits still go through `storefront …`, verified with `storefront sync` then `storefront doctor` |
|
|
75
|
+
|
|
76
|
+
The step-by-step procedures live in the skills, not in this file.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by Thor Commerce storefront (template: assets/templates/app/app/[[...slug]]/page.tsx)
|
|
2
2
|
|
|
3
3
|
import { notFound } from 'next/navigation'
|
|
4
|
+
import { buildBreadcrumbListJsonLd, serializeJsonLd } from '@thorprovider/runtime/contracts'
|
|
4
5
|
import { __STOREFRONT_CAMEL__App } from '@/lib/__STOREFRONT__/runtime'
|
|
5
6
|
import { readCartStateForRender } from '@/lib/__STOREFRONT__/cart-state'
|
|
6
7
|
import { StorefrontRenderer } from './renderer'
|
|
@@ -24,5 +25,45 @@ export default async function Page({
|
|
|
24
25
|
const cartState = await readCartStateForRender()
|
|
25
26
|
const initialState = { ...(data.initialState ?? {}), ...cartState }
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
// Entity structured data is server-rendered from the real loaded entity and
|
|
29
|
+
// sanitized so entity text cannot terminate the script element.
|
|
30
|
+
const jsonLd = data.entitySeo?.jsonLd
|
|
31
|
+
// The entity OpenGraph type (`product`/`CollectionPage`) is emitted here, not
|
|
32
|
+
// through Next metadata: Next validates `og:type` against the OpenGraph
|
|
33
|
+
// vocabulary and rejects those entity types, so a raw `property` tag is the
|
|
34
|
+
// only way to carry the entity type into the rendered `<head>`.
|
|
35
|
+
const ogType = data.entitySeo?.openGraph?.type
|
|
36
|
+
// The navigability feature's real trail is server-rendered as a sanitized
|
|
37
|
+
// `BreadcrumbList` alongside the entity structured data. It is built from the
|
|
38
|
+
// loader's own state, so it always matches the rendered breadcrumb.
|
|
39
|
+
const breadcrumbItems = (data.initialState as Record<string, unknown> | undefined)
|
|
40
|
+
?.breadcrumbItems
|
|
41
|
+
const breadcrumbJsonLd = buildBreadcrumbListJsonLd(
|
|
42
|
+
Array.isArray(breadcrumbItems)
|
|
43
|
+
? (breadcrumbItems as { label?: string; href?: string }[])
|
|
44
|
+
: undefined,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
{ogType ? <meta property="og:type" content={ogType} /> : null}
|
|
50
|
+
{jsonLd ? (
|
|
51
|
+
<script
|
|
52
|
+
type="application/ld+json"
|
|
53
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(jsonLd) }}
|
|
54
|
+
/>
|
|
55
|
+
) : null}
|
|
56
|
+
{breadcrumbJsonLd ? (
|
|
57
|
+
<script
|
|
58
|
+
type="application/ld+json"
|
|
59
|
+
dangerouslySetInnerHTML={{ __html: serializeJsonLd(breadcrumbJsonLd) }}
|
|
60
|
+
/>
|
|
61
|
+
) : null}
|
|
62
|
+
<StorefrontRenderer
|
|
63
|
+
spec={data.spec}
|
|
64
|
+
layoutSpec={data.layoutSpec}
|
|
65
|
+
initialState={initialState}
|
|
66
|
+
/>
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
28
69
|
}
|
|
@@ -2,9 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
// Generated by Thor Commerce storefront (template: assets/templates/app/app/[[...slug]]/renderer.tsx)
|
|
4
4
|
|
|
5
|
+
import { useRef } from 'react'
|
|
5
6
|
import { PageRenderer } from '@json-render/next'
|
|
6
7
|
import type { PageData } from '@json-render/next'
|
|
7
8
|
|
|
8
|
-
export function StorefrontRenderer(data: PageData) {
|
|
9
|
-
|
|
9
|
+
export function StorefrontRenderer({ initialState, ...data }: PageData) {
|
|
10
|
+
// The server recomputes `initialState` on every render, and a Server Action
|
|
11
|
+
// triggers an RSC refresh — so the prop is a fresh object each time even when
|
|
12
|
+
// its content is unchanged. `@json-render/react`'s StateProvider resets the
|
|
13
|
+
// client store whenever the `initialState` reference changes, which would wipe
|
|
14
|
+
// client-owned state (for example the account order history) after an action.
|
|
15
|
+
// Keep a STABLE reference while the serialized content is unchanged; a genuine
|
|
16
|
+
// server-side change (for example the cart) still propagates a new value.
|
|
17
|
+
const key = JSON.stringify(initialState ?? {})
|
|
18
|
+
const stable = useRef({ key, value: initialState })
|
|
19
|
+
if (stable.current.key !== key) stable.current = { key, value: initialState }
|
|
20
|
+
return <PageRenderer {...data} initialState={stable.current.value} />
|
|
10
21
|
}
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
--color-border: var(--border);
|
|
29
29
|
--color-input: var(--input);
|
|
30
30
|
--color-ring: var(--ring);
|
|
31
|
-
--radius-sm: var(--radius-
|
|
32
|
-
--radius-md: var(--radius-
|
|
33
|
-
--radius-lg: var(--radius
|
|
34
|
-
--radius-xl: var(--radius
|
|
31
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
32
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
33
|
+
--radius-lg: var(--radius);
|
|
34
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
35
35
|
--font-sans: var(--font-geist-sans);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -59,10 +59,7 @@
|
|
|
59
59
|
--border: oklch(0.88 0.003 90);
|
|
60
60
|
--input: oklch(0.92 0.003 90);
|
|
61
61
|
--ring: oklch(0.15 0 0);
|
|
62
|
-
--radius
|
|
63
|
-
--radius-md: 0.375rem;
|
|
64
|
-
--radius-lg: 0.5rem;
|
|
65
|
-
--radius-xl: 0.75rem;
|
|
62
|
+
--radius: 0.5rem;
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
.dark {
|
|
@@ -4,8 +4,6 @@ import type { Metadata } from 'next'
|
|
|
4
4
|
import { Geist } from 'next/font/google'
|
|
5
5
|
import { ThemeProvider } from 'next-themes'
|
|
6
6
|
import { StorefrontProvider } from '@thorprovider/runtime/host-glue/providers'
|
|
7
|
-
import { themeVarsToCss, themeTokensToVars } from '@thorprovider/runtime/render-core/theme'
|
|
8
|
-
import { DEFAULT_THEME_TOKENS } from '@thorprovider/runtime/contracts'
|
|
9
7
|
import { registry } from '@/lib/__STOREFRONT__/registry'
|
|
10
8
|
import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
|
|
11
9
|
import './globals.css'
|
|
@@ -15,8 +13,6 @@ const geist = Geist({
|
|
|
15
13
|
subsets: ['latin'],
|
|
16
14
|
})
|
|
17
15
|
|
|
18
|
-
const themeCss = themeVarsToCss(themeTokensToVars(DEFAULT_THEME_TOKENS))
|
|
19
|
-
|
|
20
16
|
// The canonical public origin is optional: when THOR_SITE_URL is unset no base
|
|
21
17
|
// URL is invented. `metadataBase` is left undefined in that case.
|
|
22
18
|
const siteUrl = getSiteUrl()
|
|
@@ -39,7 +35,7 @@ export default function RootLayout({
|
|
|
39
35
|
<html lang="es" className={geist.variable} suppressHydrationWarning>
|
|
40
36
|
<body className="font-sans antialiased">
|
|
41
37
|
<ThemeProvider attribute="class" defaultTheme="__STOREFRONT_MODE__" enableSystem>
|
|
42
|
-
<StorefrontProvider
|
|
38
|
+
<StorefrontProvider registry={registry}>
|
|
43
39
|
{children}
|
|
44
40
|
</StorefrontProvider>
|
|
45
41
|
</ThemeProvider>
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
// Generated by Thor Commerce storefront (template: assets/templates/app/app/robots.ts)
|
|
2
|
-
// Always served.
|
|
2
|
+
// Always served. Allows public pages, disallows private surfaces, and points at
|
|
3
|
+
// the sitemap only when THOR_SITE_URL is configured.
|
|
3
4
|
|
|
4
5
|
import type { MetadataRoute } from 'next'
|
|
5
|
-
import {
|
|
6
|
+
import { __STOREFRONT_CAMEL__Spec } from '@/lib/__STOREFRONT__/spec'
|
|
7
|
+
import { robotsDisallowPaths, robotsSitemapUrl } from '@/lib/__STOREFRONT__/spec/seo'
|
|
6
8
|
import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
|
|
7
9
|
|
|
8
10
|
export default function robots(): MetadataRoute.Robots {
|
|
9
11
|
return {
|
|
10
|
-
rules: {
|
|
12
|
+
rules: {
|
|
13
|
+
userAgent: '*',
|
|
14
|
+
allow: '/',
|
|
15
|
+
disallow: robotsDisallowPaths(__STOREFRONT_CAMEL__Spec.routes ?? {}),
|
|
16
|
+
},
|
|
11
17
|
sitemap: robotsSitemapUrl(getSiteUrl()),
|
|
12
18
|
}
|
|
13
19
|
}
|
|
@@ -1,12 +1,71 @@
|
|
|
1
1
|
// Generated by Thor Commerce storefront (template: assets/templates/app/app/sitemap.ts)
|
|
2
|
-
// Lists the
|
|
3
|
-
//
|
|
2
|
+
// Lists the fixed public routes plus every public catalog entity, enumerated
|
|
3
|
+
// across adapter pagination with a bound and a cycle guard. Absolute URLs are
|
|
4
|
+
// emitted only when THOR_SITE_URL is configured; otherwise the sitemap is empty
|
|
5
|
+
// (no invented URL). An enumeration failure fails loudly (no partial sitemap).
|
|
4
6
|
|
|
5
7
|
import type { MetadataRoute } from 'next'
|
|
8
|
+
import { getCatalog, getCatalogResults } from '@thorprovider/runtime/commerce'
|
|
6
9
|
import { __STOREFRONT_CAMEL__Spec } from '@/lib/__STOREFRONT__/spec'
|
|
7
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
buildSitemapEntries,
|
|
12
|
+
enumeratePublicEntities,
|
|
13
|
+
SitemapEnumerationError,
|
|
14
|
+
type PublicEntityPage,
|
|
15
|
+
} from '@/lib/__STOREFRONT__/spec/seo'
|
|
8
16
|
import { getSiteUrl } from '@/lib/__STOREFRONT__/store-context'
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
const ENTITY_PAGE_SIZE = 100
|
|
19
|
+
const MAX_PAGES = 100
|
|
20
|
+
|
|
21
|
+
async function productPage(cursor: string | undefined): Promise<PublicEntityPage> {
|
|
22
|
+
const page = cursor ? Number(cursor) : 1
|
|
23
|
+
const outcome = await getCatalogResults({
|
|
24
|
+
page,
|
|
25
|
+
pageSize: ENTITY_PAGE_SIZE,
|
|
26
|
+
sort: 'name',
|
|
27
|
+
})
|
|
28
|
+
if (!outcome.ok) {
|
|
29
|
+
throw new SitemapEnumerationError({
|
|
30
|
+
kind: 'enumeration',
|
|
31
|
+
message: outcome.error.message,
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
const items = outcome.results.products.map((product) => ({
|
|
35
|
+
id: product.id,
|
|
36
|
+
path: `/products/${product.handle}`,
|
|
37
|
+
}))
|
|
38
|
+
const total = outcome.results.total
|
|
39
|
+
const hasMore =
|
|
40
|
+
typeof total === 'number' ? page * ENTITY_PAGE_SIZE < total : items.length >= ENTITY_PAGE_SIZE
|
|
41
|
+
return hasMore ? { items, nextCursor: String(page + 1) } : { items }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function categoryPage(): Promise<PublicEntityPage> {
|
|
45
|
+
const catalog = await getCatalog()
|
|
46
|
+
return {
|
|
47
|
+
items: catalog.categoriesRaw.map((category) => ({
|
|
48
|
+
id: category.id,
|
|
49
|
+
path: `/categories/${category.handle}`,
|
|
50
|
+
})),
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
55
|
+
const siteUrl = getSiteUrl()
|
|
56
|
+
if (!siteUrl) return []
|
|
57
|
+
|
|
58
|
+
const routes = __STOREFRONT_CAMEL__Spec.routes ?? {}
|
|
59
|
+
const [products, categories] = await Promise.all([
|
|
60
|
+
enumeratePublicEntities(productPage, { maxPages: MAX_PAGES }),
|
|
61
|
+
enumeratePublicEntities(categoryPage, { maxPages: MAX_PAGES }),
|
|
62
|
+
])
|
|
63
|
+
if (!products.ok) throw new SitemapEnumerationError(products.failure)
|
|
64
|
+
if (!categories.ok) throw new SitemapEnumerationError(categories.failure)
|
|
65
|
+
|
|
66
|
+
return buildSitemapEntries({
|
|
67
|
+
routes,
|
|
68
|
+
siteUrl,
|
|
69
|
+
entities: [...products.entities, ...categories.entities],
|
|
70
|
+
})
|
|
12
71
|
}
|
|
@@ -29,12 +29,15 @@ import {
|
|
|
29
29
|
loadCheckoutCountries,
|
|
30
30
|
loadCustomer,
|
|
31
31
|
loadOrders,
|
|
32
|
+
loadOrdersPage,
|
|
32
33
|
loadPaymentMethods,
|
|
34
|
+
loadProductDetailById,
|
|
33
35
|
loadShippingMethods,
|
|
34
36
|
readCart,
|
|
35
37
|
reconcileOrder,
|
|
36
38
|
registerShopper,
|
|
37
39
|
removeLine,
|
|
40
|
+
resolveProductCartTarget,
|
|
38
41
|
saveShippingAddress,
|
|
39
42
|
selectShipping,
|
|
40
43
|
setLineQuantity,
|
|
@@ -91,6 +94,11 @@ type OrdersActionResult =
|
|
|
91
94
|
| { ok: true; orders: AccountOrderView[] }
|
|
92
95
|
| { ok: false; error: string }
|
|
93
96
|
|
|
97
|
+
/** Structured result for a paginated customer order-history load. */
|
|
98
|
+
type OrdersPageActionResult =
|
|
99
|
+
| { ok: true; orders: AccountOrderView[]; count: number; limit: number; offset: number }
|
|
100
|
+
| { ok: false; error: string }
|
|
101
|
+
|
|
94
102
|
/** Basic email shape check; a boundary guard, not a full RFC validation. */
|
|
95
103
|
const EMAIL_PATTERN = /\S+@\S+\.\S+/
|
|
96
104
|
|
|
@@ -152,6 +160,34 @@ export async function addToCartAction(input: {
|
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Add a product's FIRST purchasable variant by backend product id — the
|
|
165
|
+
* product-card shortcut. The variant is resolved server-side from the backend
|
|
166
|
+
* detail (card data carries no variant ids); a product with no purchasable
|
|
167
|
+
* variant returns a truthful error and adds nothing. The product detail remains
|
|
168
|
+
* the explicit-selection surface (change storefront-card-add-to-cart).
|
|
169
|
+
*/
|
|
170
|
+
export async function addToCartByProductAction(input: {
|
|
171
|
+
productId: string
|
|
172
|
+
quantity?: number
|
|
173
|
+
}): Promise<CartActionResult> {
|
|
174
|
+
try {
|
|
175
|
+
const productId = typeof input?.productId === 'string' ? input.productId : ''
|
|
176
|
+
if (productId === '') return { ok: false, error: 'Producto no válido.' }
|
|
177
|
+
const quantity =
|
|
178
|
+
Number.isFinite(input?.quantity) && (input.quantity as number) > 0
|
|
179
|
+
? Math.floor(input.quantity as number)
|
|
180
|
+
: 1
|
|
181
|
+
const detail = await loadProductDetailById(productId)
|
|
182
|
+
const target = resolveProductCartTarget(detail)
|
|
183
|
+
if (!target.ok) return { ok: false, error: target.error }
|
|
184
|
+
const { cartId, context } = await ensureActiveCartId()
|
|
185
|
+
return { ok: true, cart: await addVariant(cartId, target.variantId, quantity, context) }
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return { ok: false, error: toError(error, 'No se pudo agregar el producto.') }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
155
191
|
/** Set a backend cart line to an ABSOLUTE quantity by its line-item id. */
|
|
156
192
|
export async function setLineQuantityAction(input: {
|
|
157
193
|
lineId: string
|
|
@@ -540,3 +576,37 @@ export async function loadOrdersAction(): Promise<OrdersActionResult> {
|
|
|
540
576
|
return { ok: false, error: toError(error, 'No se pudieron cargar tus pedidos.') }
|
|
541
577
|
}
|
|
542
578
|
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Load ONE bounded page of the authenticated customer's channel-scoped order
|
|
582
|
+
* history through the additive paginated contract, returning the backend's real
|
|
583
|
+
* `count`. A missing token resolves to an empty page with `count: 0` without
|
|
584
|
+
* contacting the backend. This action touches ONLY the auth cookie — never the
|
|
585
|
+
* guest session cookie or the cart — and no token is logged or returned.
|
|
586
|
+
*/
|
|
587
|
+
export async function loadOrdersPageAction(input?: {
|
|
588
|
+
page?: number
|
|
589
|
+
pageSize?: number
|
|
590
|
+
}): Promise<OrdersPageActionResult> {
|
|
591
|
+
const context = getStoreContext()
|
|
592
|
+
const token = await readAuthToken(context)
|
|
593
|
+
const page = Number.isInteger(input?.page) && (input?.page as number) > 0 ? (input?.page as number) : 1
|
|
594
|
+
const pageSize =
|
|
595
|
+
Number.isInteger(input?.pageSize) && (input?.pageSize as number) > 0
|
|
596
|
+
? (input?.pageSize as number)
|
|
597
|
+
: 10
|
|
598
|
+
if (!token) return { ok: true, orders: [], count: 0, limit: pageSize, offset: (page - 1) * pageSize }
|
|
599
|
+
try {
|
|
600
|
+
const result = await loadOrdersPage(token, { page, pageSize }, context)
|
|
601
|
+
return {
|
|
602
|
+
ok: true,
|
|
603
|
+
orders: result.orders,
|
|
604
|
+
count: result.count,
|
|
605
|
+
limit: result.limit,
|
|
606
|
+
offset: result.offset,
|
|
607
|
+
}
|
|
608
|
+
} catch (error) {
|
|
609
|
+
logServerError('loadOrdersPageAction', error)
|
|
610
|
+
return { ok: false, error: toError(error, 'No se pudieron cargar tus pedidos.') }
|
|
611
|
+
}
|
|
612
|
+
}
|
|
@@ -7,11 +7,13 @@ import { defineCatalog } from '@json-render/core'
|
|
|
7
7
|
import { schema } from '@json-render/react/schema'
|
|
8
8
|
import { z } from 'zod'
|
|
9
9
|
import {
|
|
10
|
+
backToTopPropsSchema,
|
|
10
11
|
bannerPropsSchema,
|
|
11
|
-
cartDrawerPropsSchema,
|
|
12
12
|
footerPropsSchema,
|
|
13
|
+
mobileBottomNavPropsSchema,
|
|
13
14
|
navbarPropsSchema,
|
|
14
15
|
productCardPropsSchema,
|
|
16
|
+
skipToContentPropsSchema,
|
|
15
17
|
storefrontNavbarPropsSchema,
|
|
16
18
|
} from './schema'
|
|
17
19
|
import { views } from './views'
|
|
@@ -43,10 +45,6 @@ const baseComponents = {
|
|
|
43
45
|
props: storefrontNavbarPropsSchema,
|
|
44
46
|
description: 'Store navigation bar with category menu and cart.',
|
|
45
47
|
},
|
|
46
|
-
CartDrawer: {
|
|
47
|
-
props: cartDrawerPropsSchema,
|
|
48
|
-
description: 'Slide-over shopping cart.',
|
|
49
|
-
},
|
|
50
48
|
Banner: {
|
|
51
49
|
props: bannerPropsSchema,
|
|
52
50
|
description: 'Announcement banner above the header.',
|
|
@@ -59,6 +57,18 @@ const baseComponents = {
|
|
|
59
57
|
props: footerPropsSchema,
|
|
60
58
|
description: 'Site footer.',
|
|
61
59
|
},
|
|
60
|
+
SkipToContent: {
|
|
61
|
+
props: skipToContentPropsSchema,
|
|
62
|
+
description: 'Keyboard bypass link to the main content landmark.',
|
|
63
|
+
},
|
|
64
|
+
BackToTop: {
|
|
65
|
+
props: backToTopPropsSchema,
|
|
66
|
+
description: 'Scroll-to-top control revealed past a threshold.',
|
|
67
|
+
},
|
|
68
|
+
MobileBottomNav: {
|
|
69
|
+
props: mobileBottomNavPropsSchema,
|
|
70
|
+
description: 'Mobile-only bottom navigation for primary destinations.',
|
|
71
|
+
},
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
const viewComponents = Object.assign({}, ...views.map((view) => view.catalog))
|
|
@@ -39,14 +39,17 @@ import {
|
|
|
39
39
|
getCheckoutCountries,
|
|
40
40
|
getCurrentCustomer,
|
|
41
41
|
getCustomerOrders,
|
|
42
|
+
getCustomerOrdersPage,
|
|
42
43
|
getEligibleShippingMethods,
|
|
43
44
|
getPaymentMethodsByRegion,
|
|
45
|
+
getProductDetailById,
|
|
44
46
|
getProductDetailByHandle,
|
|
45
47
|
loginCustomer,
|
|
46
48
|
logoutCustomer,
|
|
47
49
|
reconcileCart,
|
|
48
50
|
registerCustomer,
|
|
49
51
|
removeCartLines,
|
|
52
|
+
selectFirstPurchasableVariant,
|
|
50
53
|
selectShippingMethod,
|
|
51
54
|
toAbsoluteLineUpdate,
|
|
52
55
|
updateCartLines,
|
|
@@ -263,6 +266,38 @@ export async function loadProductDetail(handle: string): Promise<CatalogDetailRe
|
|
|
263
266
|
return { detail, source }
|
|
264
267
|
}
|
|
265
268
|
|
|
269
|
+
/** Directed product-detail lookup by backend id, priced in the store context region. */
|
|
270
|
+
export async function loadProductDetailById(
|
|
271
|
+
productId: string,
|
|
272
|
+
): Promise<CatalogProductDetail | undefined> {
|
|
273
|
+
const context = getStoreContext()
|
|
274
|
+
assertBackendConfigured()
|
|
275
|
+
return getProductDetailById(productId, context)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** The resolved add-to-cart target for a product-card shortcut. */
|
|
279
|
+
export type ProductCartTarget =
|
|
280
|
+
| { ok: true; variantId: string }
|
|
281
|
+
| { ok: false; error: string }
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolve the product-card add-to-cart target: the product's FIRST purchasable
|
|
285
|
+
* variant (available for sale AND with a resolvable price), identified by its
|
|
286
|
+
* backend variant id. This is the explicit card exception to the product-detail
|
|
287
|
+
* rule that never falls back to a first variant (change
|
|
288
|
+
* storefront-card-add-to-cart). A product with no purchasable variant — or an
|
|
289
|
+
* unknown product — returns a truthful error and adds nothing.
|
|
290
|
+
*/
|
|
291
|
+
export function resolveProductCartTarget(
|
|
292
|
+
detail: CatalogProductDetail | undefined,
|
|
293
|
+
): ProductCartTarget {
|
|
294
|
+
const variant = detail ? selectFirstPurchasableVariant(detail) : undefined
|
|
295
|
+
if (!variant) {
|
|
296
|
+
return { ok: false, error: 'Este producto no tiene variantes disponibles.' }
|
|
297
|
+
}
|
|
298
|
+
return { ok: true, variantId: variant.id }
|
|
299
|
+
}
|
|
300
|
+
|
|
266
301
|
/** The context-scoped backend cart projected for the UI, or undefined when absent. */
|
|
267
302
|
export async function readCart(
|
|
268
303
|
cartId: string,
|
|
@@ -605,4 +640,40 @@ export async function loadOrders(
|
|
|
605
640
|
return orders.map(toAccountOrderView)
|
|
606
641
|
}
|
|
607
642
|
|
|
643
|
+
/**
|
|
644
|
+
* The additive paginated history window projected for the account view.
|
|
645
|
+
* `count` is the backend's real total for the authenticated customer on the
|
|
646
|
+
* store's channel; it is never the page length.
|
|
647
|
+
*/
|
|
648
|
+
export interface AccountOrdersPage {
|
|
649
|
+
orders: AccountOrderView[]
|
|
650
|
+
count: number
|
|
651
|
+
limit: number
|
|
652
|
+
offset: number
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Load one bounded page of the authenticated customer's channel-scoped history
|
|
657
|
+
* through the additive paginated contract. A missing token yields an empty page
|
|
658
|
+
* with `count: 0` (the runtime builds no provider); it never fabricates an order.
|
|
659
|
+
*/
|
|
660
|
+
export async function loadOrdersPage(
|
|
661
|
+
token: string | null,
|
|
662
|
+
options: { page?: number; pageSize?: number } = {},
|
|
663
|
+
context: StoreContext = getStoreContext(),
|
|
664
|
+
): Promise<AccountOrdersPage> {
|
|
665
|
+
const page = await getCustomerOrdersPage({
|
|
666
|
+
token,
|
|
667
|
+
page: options.page,
|
|
668
|
+
pageSize: options.pageSize,
|
|
669
|
+
salesChannelId: context.salesChannelId,
|
|
670
|
+
})
|
|
671
|
+
return {
|
|
672
|
+
orders: page.orders.map(toAccountOrderView),
|
|
673
|
+
count: page.count,
|
|
674
|
+
limit: page.limit,
|
|
675
|
+
offset: page.offset,
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
608
679
|
export type { Money }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (composition composer: features).
|
|
2
|
+
// Do not edit by hand — the manifest is the source of truth; run `storefront sync`.
|
|
3
|
+
// Installed manifest catalog features, in stable order, with their persisted
|
|
4
|
+
// config. This is the generation seam the catalog URL boundary consumes to
|
|
5
|
+
// reject parameters owned by a feature that is not installed, and the seam the
|
|
6
|
+
// single controlled CategoryFilter2 reads to select its control
|
|
7
|
+
// visibility/config from the installed manifest only.
|
|
8
|
+
|
|
9
|
+
import type { CatalogFeatureName } from '@thorprovider/runtime/contracts'
|
|
10
|
+
|
|
11
|
+
export const mountedFeatures: readonly CatalogFeatureName[] = []
|
|
12
|
+
|
|
13
|
+
// Every installed feature name, unfiltered by the catalog URL boundary. The
|
|
14
|
+
// general presence seam cross-cutting features read to gate their own
|
|
15
|
+
// contributions (e.g. the navigability feature's shell chrome).
|
|
16
|
+
export const mountedFeatureNames: readonly string[] = []
|
|
17
|
+
|
|
18
|
+
export const mountedFeatureConfigs: Partial<Record<string, Record<string, unknown>>> = {
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Typed route/action availability derived from the installed manifest. The
|
|
22
|
+
// generated host uses these to hide links to routes that are not installed,
|
|
23
|
+
// so a removed view never leaves an active dead-end navigation target.
|
|
24
|
+
export const cartViewAvailable = false
|
|
25
|
+
|
|
26
|
+
export const checkoutAvailable = false
|
|
@@ -15,6 +15,7 @@ import { useActions, useStateStore } from '@json-render/react'
|
|
|
15
15
|
import { useNextApp } from '@json-render/next'
|
|
16
16
|
import {
|
|
17
17
|
addToCartAction,
|
|
18
|
+
addToCartByProductAction,
|
|
18
19
|
applyQuantityDeltaAction,
|
|
19
20
|
ensureCartAction,
|
|
20
21
|
loadCartStateAction,
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
setLineQuantityAction,
|
|
23
24
|
} from './actions'
|
|
24
25
|
import type { CartStatePatch } from './commerce'
|
|
26
|
+
import { cartViewAvailable } from './composition'
|
|
27
|
+
import { recordConfirmedTransition } from './section-observation'
|
|
25
28
|
|
|
26
29
|
/** A product-like object the views leave in `/lastClicked` (card/hero shortcut). */
|
|
27
30
|
interface ClickedProduct {
|
|
@@ -81,10 +84,30 @@ export function useCoreHandlers() {
|
|
|
81
84
|
if (result.ok) {
|
|
82
85
|
applyCart(result.cart)
|
|
83
86
|
set('/cartOpen', true)
|
|
87
|
+
// A confirmed cart transition — observed only after the mutation
|
|
88
|
+
// succeeded, and only when a section origin exists (default: off).
|
|
89
|
+
recordConfirmedTransition('cart_add_confirmed')
|
|
84
90
|
} else fail(result.error)
|
|
85
91
|
})
|
|
86
92
|
.catch(onError('addToCart'))
|
|
87
93
|
},
|
|
94
|
+
addClickedToCart: () => {
|
|
95
|
+
// Product-card shortcut: add the clicked product's first purchasable
|
|
96
|
+
// variant. The backend variant id is resolved server-side from the
|
|
97
|
+
// product id (the card carries no variant ids).
|
|
98
|
+
const product = get('/lastClicked') as ClickedProduct | null
|
|
99
|
+
if (!product?.id) return
|
|
100
|
+
begin()
|
|
101
|
+
void addToCartByProductAction({ productId: String(product.id) })
|
|
102
|
+
.then((result) => {
|
|
103
|
+
if (result.ok) {
|
|
104
|
+
applyCart(result.cart)
|
|
105
|
+
set('/cartOpen', true)
|
|
106
|
+
recordConfirmedTransition('cart_add_confirmed')
|
|
107
|
+
} else fail(result.error)
|
|
108
|
+
})
|
|
109
|
+
.catch(onError('addClickedToCart'))
|
|
110
|
+
},
|
|
88
111
|
ensureCart: () => {
|
|
89
112
|
begin()
|
|
90
113
|
void ensureCartAction()
|
|
@@ -141,17 +164,26 @@ export function useCoreHandlers() {
|
|
|
141
164
|
openProduct: () => {
|
|
142
165
|
const product = get('/lastClicked') as ClickedProduct | null
|
|
143
166
|
if (!product?.id) return
|
|
144
|
-
navigate(`/
|
|
167
|
+
navigate(`/products/${product.id}`)
|
|
145
168
|
},
|
|
146
169
|
openCategory: () => {
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
// `/lastCategory` already holds the category href the card was given
|
|
171
|
+
// (`StoreCategory.href`, e.g. `/categories/jackets`); navigate it as-is
|
|
172
|
+
// so the route scope is not prefixed twice.
|
|
173
|
+
const href = get('/lastCategory') as string | null
|
|
174
|
+
navigate(href ?? '/categories')
|
|
149
175
|
},
|
|
150
176
|
goHome: () => {
|
|
151
177
|
set('/cartOpen', false)
|
|
152
178
|
navigate('/')
|
|
153
179
|
},
|
|
154
|
-
goToCart: () =>
|
|
180
|
+
goToCart: () => {
|
|
181
|
+
// Visibility only: dismiss the lateral and go to the review route. The
|
|
182
|
+
// navigation is guarded by the installed composition so a store without
|
|
183
|
+
// the cart view never routes to a missing page.
|
|
184
|
+
set('/cartOpen', false)
|
|
185
|
+
if (cartViewAvailable) navigate('/cart')
|
|
186
|
+
},
|
|
155
187
|
submitReview: () => {},
|
|
156
188
|
toggleReviewLike: () => {},
|
|
157
189
|
}
|