@thorprovider/storefront 0.3.1 → 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 +321 -152
- 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 +54 -2
- 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 +17 -7
- package/assets/templates/app/app/robots.ts +19 -0
- package/assets/templates/app/app/sitemap.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
- 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__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- 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.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +159 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +45 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +439 -0
- package/assets/templates/views/account/schema.ts +53 -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 +29 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
- 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/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +299 -0
- package/assets/templates/views/category/props.ts +23 -0
- package/assets/templates/views/category/registry.tsx +122 -0
- package/assets/templates/views/category/schema.ts +91 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +33 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +24 -0
- package/assets/templates/views/home/index.tsx +57 -85
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +50 -0
- package/assets/templates/views/product/index.tsx +148 -0
- package/assets/templates/views/product/props.ts +27 -0
- package/assets/templates/views/product/registry.tsx +103 -0
- package/assets/templates/views/product/schema.ts +121 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/bin/storefront.js +2424 -148
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +15 -9
- package/recipes/archetypes.json +28 -14
- package/recipes/catalog.json +400 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +162 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +76 -19
- package/recipes/sections.json +34 -5
- package/recipes/validate.mjs +838 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +160 -0
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +121 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +573 -0
- package/scripts/plan-apply.mjs +987 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +549 -0
- package/scripts/render-templates.mjs +338 -68
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-config.mjs +253 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/sync-assets.mjs +22 -15
- 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 +337 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +80 -0
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +100 -30
- 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 +146 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
- package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +162 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
- package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- 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.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- 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 +58 -33
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +838 -32
- package/skills/storefront-workflow/SKILL.md +179 -0
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -61
- package/commands/sf-add-view.md +0 -26
- package/commands/sf-theme.md +0 -20
- package/commands/sf-view.md +0 -25
- 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/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
- 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
|
@@ -3,31 +3,49 @@
|
|
|
3
3
|
import { PricingSection1 as PricingBlock } from '@thorprovider/blocks'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
6
|
+
// Mirrors the PricingTier/PricingSection1Props block contracts.
|
|
7
|
+
const pricingTierSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
description: z.string(),
|
|
11
|
+
// Precio sin símbolo de moneda; el símbolo se pasa aparte en `currencySymbol`.
|
|
12
|
+
price: z.string(),
|
|
13
|
+
// Frecuencia de facturación, p. ej. '/mes'.
|
|
14
|
+
frequency: z.string(),
|
|
15
|
+
features: z.array(z.string()),
|
|
16
|
+
popular: z.boolean().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const pricingPropsSchema = z.object({
|
|
20
|
+
sectionTitle: z.string().optional(),
|
|
21
|
+
sectionLabel: z.string().optional(),
|
|
22
|
+
currencySymbol: z.string().optional(),
|
|
23
|
+
popularBadgeLabel: z.string().optional(),
|
|
24
|
+
getStartedLabel: z.string().optional(),
|
|
25
|
+
tiers: z.array(pricingTierSchema),
|
|
26
|
+
})
|
|
27
|
+
|
|
6
28
|
export const pricingSection = {
|
|
7
29
|
elementType: 'Pricing',
|
|
8
30
|
catalog: {
|
|
9
31
|
Pricing: {
|
|
10
|
-
props:
|
|
11
|
-
sectionTitle: z.string().optional(),
|
|
12
|
-
sectionLabel: z.string().optional(),
|
|
13
|
-
currencySymbol: z.string().optional(),
|
|
14
|
-
popularBadgeLabel: z.string().optional(),
|
|
15
|
-
getStartedLabel: z.string().optional(),
|
|
16
|
-
tiers: z.array(z.record(z.string(), z.unknown())),
|
|
17
|
-
}),
|
|
32
|
+
props: pricingPropsSchema,
|
|
18
33
|
description: 'Pricing tiers or plans.',
|
|
19
34
|
},
|
|
20
35
|
},
|
|
21
36
|
registry: {
|
|
22
|
-
Pricing: ({ props }: { props: Record<string, unknown> }) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
Pricing: ({ props }: { props: Record<string, unknown> }) => {
|
|
38
|
+
const parsed = pricingPropsSchema.parse(props)
|
|
39
|
+
return (
|
|
40
|
+
<PricingBlock
|
|
41
|
+
sectionTitle={parsed.sectionTitle ?? 'Planes y precios'}
|
|
42
|
+
sectionLabel={parsed.sectionLabel}
|
|
43
|
+
currencySymbol={parsed.currencySymbol}
|
|
44
|
+
popularBadgeLabel={parsed.popularBadgeLabel ?? 'Popular'}
|
|
45
|
+
getStartedLabel={parsed.getStartedLabel ?? 'Empezar'}
|
|
46
|
+
tiers={parsed.tiers}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
},
|
|
32
50
|
},
|
|
33
51
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: products (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog lives in products.tsx. A data-bound section
|
|
5
|
+
// renders one of two verified presentations (ProductCard1 grid, or the combined
|
|
6
|
+
// grid-and-carousel ProductShowcase1) chosen by the resolved `variant`. The
|
|
7
|
+
// selection and resolved products arrive through the view state; this boundary
|
|
8
|
+
// only validates them and wires navigation/purchase callbacks.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ProductCard1 as ProductCard1Block,
|
|
12
|
+
ProductShowcase1 as ProductShowcase1Block,
|
|
13
|
+
type ProductCard1Product,
|
|
14
|
+
} from '@thorprovider/blocks'
|
|
15
|
+
import { useStateStore } from '@json-render/react'
|
|
16
|
+
import { useSectionObservation } from '../../section-observation'
|
|
17
|
+
import { productsPropsSchema, type ProductsStatus } from './schema.products'
|
|
18
|
+
import type { ViewRegistry } from '../types'
|
|
19
|
+
|
|
20
|
+
// Truthful, actionable messages for the non-`ready` states. A failed or
|
|
21
|
+
// unconfigured section never fabricates products.
|
|
22
|
+
function sectionStatusMessage(status: ProductsStatus): string {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'unconfigured':
|
|
25
|
+
return 'Esta sección de productos aún no tiene una selección configurada.'
|
|
26
|
+
case 'empty':
|
|
27
|
+
return 'Esta selección no tiene productos disponibles por ahora.'
|
|
28
|
+
case 'invalid-query':
|
|
29
|
+
return 'La selección de productos de esta sección no es válida.'
|
|
30
|
+
case 'unsupported-query':
|
|
31
|
+
return 'Esta selección usa criterios que el catálogo no admite.'
|
|
32
|
+
case 'unmounted-capability':
|
|
33
|
+
return 'Esta selección requiere una capacidad que este catálogo no tiene montada.'
|
|
34
|
+
case 'data-error':
|
|
35
|
+
return 'No se pudieron cargar los productos de esta sección.'
|
|
36
|
+
default:
|
|
37
|
+
return ''
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const productsRegistry: ViewRegistry = {
|
|
42
|
+
Products: ({ props, emit }) => {
|
|
43
|
+
const { set } = useStateStore()
|
|
44
|
+
const parsed = productsPropsSchema.parse(props)
|
|
45
|
+
const { containerRef, noteProductClick } = useSectionObservation({
|
|
46
|
+
sectionId: parsed.sectionId ?? '',
|
|
47
|
+
sectionType: parsed.sectionType ?? 'products',
|
|
48
|
+
...(parsed.variant ? { sectionVariant: parsed.variant } : {}),
|
|
49
|
+
...(parsed.objective ? { objective: parsed.objective } : {}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (parsed.status !== 'ready') {
|
|
53
|
+
return (
|
|
54
|
+
<section
|
|
55
|
+
aria-label={parsed.heading ?? 'Productos'}
|
|
56
|
+
className="mx-auto w-full max-w-7xl px-4 py-12"
|
|
57
|
+
>
|
|
58
|
+
{parsed.heading ? (
|
|
59
|
+
<h2 className="mb-2 text-2xl font-semibold tracking-tight">{parsed.heading}</h2>
|
|
60
|
+
) : null}
|
|
61
|
+
<p className="text-sm text-muted-foreground">{sectionStatusMessage(parsed.status)}</p>
|
|
62
|
+
</section>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The detail affordance opens the product so a variant is chosen explicitly.
|
|
67
|
+
// It is also the section's observation origin (disabled by default).
|
|
68
|
+
const openProduct = (product: ProductCard1Product) => {
|
|
69
|
+
noteProductClick(product)
|
|
70
|
+
set('/lastClicked', product)
|
|
71
|
+
emit('openProduct')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Product-card shortcut: the add affordance adds the product's first
|
|
75
|
+
// purchasable variant (resolved server-side from the product id), then opens
|
|
76
|
+
// the mini-cart. It is not the section's observation origin.
|
|
77
|
+
const addToCart = (product: ProductCard1Product) => {
|
|
78
|
+
set('/lastClicked', product)
|
|
79
|
+
emit('addClickedToCart')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const shared = {
|
|
83
|
+
products: parsed.products,
|
|
84
|
+
heading: parsed.heading,
|
|
85
|
+
description: parsed.description,
|
|
86
|
+
onAddToCart: addToCart,
|
|
87
|
+
onProductClick: openProduct,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div ref={containerRef}>
|
|
92
|
+
{parsed.variant === 'ProductShowcase1' ? (
|
|
93
|
+
<ProductShowcase1Block {...shared} />
|
|
94
|
+
) : (
|
|
95
|
+
<ProductCard1Block {...shared} />
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Optional section fragment: products (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in products.client.tsx ('use client'). Unlike the stateless editorial
|
|
4
|
+
// fragments (faq/cta), the products section is data-bound: its per-instance
|
|
5
|
+
// selection is resolved by the host loader and delivered through the view state,
|
|
6
|
+
// so this catalog entry describes the *resolved* props shape.
|
|
7
|
+
|
|
8
|
+
import { productsPropsSchema } from './schema.products'
|
|
9
|
+
|
|
10
|
+
export const productsSection = {
|
|
11
|
+
elementType: 'Products',
|
|
12
|
+
catalog: {
|
|
13
|
+
Products: {
|
|
14
|
+
props: productsPropsSchema,
|
|
15
|
+
description: 'Data-bound product presentation (grid or combined grid-and-carousel).',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
registryModule: './products.client',
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Optional section fragment: cta (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the Cta/CtaSection1Props block contracts, shared by the server
|
|
3
|
+
// catalog (cta.tsx) and the client registry (cta.client.tsx). The React-only
|
|
4
|
+
// callbacks (onPrimaryClick/onSecondaryClick) are intentionally excluded.
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
export const ctaSchema = z.object({
|
|
9
|
+
label: z.string(),
|
|
10
|
+
href: z.string().optional(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const ctaSection1PropsSchema = z.object({
|
|
14
|
+
badgeText: z.string().optional(),
|
|
15
|
+
title: z.string().optional(),
|
|
16
|
+
description: z.string().optional(),
|
|
17
|
+
primaryCta: ctaSchema.optional(),
|
|
18
|
+
secondaryCta: ctaSchema.optional(),
|
|
19
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Optional section fragment: faq (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the FaqItem/FaqBlock1Props block contracts, shared by the server
|
|
3
|
+
// catalog (faq.tsx) and the client registry (faq.client.tsx).
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const faqItemSchema = z.object({
|
|
8
|
+
value: z.string(),
|
|
9
|
+
question: z.string(),
|
|
10
|
+
answer: z.string(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const faqPropsSchema = z.object({
|
|
14
|
+
title: z.string().optional(),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
items: z.array(faqItemSchema),
|
|
17
|
+
accordionType: z.enum(['single', 'multiple']).optional(),
|
|
18
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Optional section fragment: products (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the resolved Products section props, shared by the server catalog
|
|
3
|
+
// (products.tsx) and the client registry (products.client.tsx). The product list
|
|
4
|
+
// reuses the canonical presentation contract owned by @thorprovider/blocks so the
|
|
5
|
+
// section renders exactly what the host mapped from the catalog result.
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import { productPresentationSchema } from '@thorprovider/blocks'
|
|
9
|
+
|
|
10
|
+
// The per-instance resolution status the host loader computes for a data-bound
|
|
11
|
+
// section. `ready`/`empty` carry resolved products; the remaining states carry no
|
|
12
|
+
// products and render an actionable, truthful message instead.
|
|
13
|
+
export const productsStatusSchema = z.enum([
|
|
14
|
+
'unconfigured',
|
|
15
|
+
'ready',
|
|
16
|
+
'empty',
|
|
17
|
+
'invalid-query',
|
|
18
|
+
'unsupported-query',
|
|
19
|
+
'unmounted-capability',
|
|
20
|
+
'data-error',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
export const productsPropsSchema = z.object({
|
|
24
|
+
sectionId: z.string().min(1).optional(),
|
|
25
|
+
sectionType: z.string().min(1).optional(),
|
|
26
|
+
variant: z.string().optional(),
|
|
27
|
+
// Defensive defaults: a missing resolved state (e.g. the view loader could not
|
|
28
|
+
// run) degrades to a truthful error instead of crashing the render.
|
|
29
|
+
status: productsStatusSchema.default('data-error'),
|
|
30
|
+
products: z.array(productPresentationSchema).default([]),
|
|
31
|
+
heading: z.string().optional(),
|
|
32
|
+
description: z.string().optional(),
|
|
33
|
+
objective: z.string().optional(),
|
|
34
|
+
ctaLabel: z.string().optional(),
|
|
35
|
+
ctaHref: z.string().optional(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export type ProductsProps = z.infer<typeof productsPropsSchema>
|
|
39
|
+
export type ProductsStatus = z.infer<typeof productsStatusSchema>
|
|
@@ -2,28 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
import { Statistic1 as StatisticBlock } from '@thorprovider/blocks'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
+
import { resolveIcon } from '../../icon-map'
|
|
6
|
+
|
|
7
|
+
// Mirrors the StatisticItem/Statistic1Props block contracts. The block expects
|
|
8
|
+
// `icon` as a Lucide component; specs ship the icon NAME, resolved here.
|
|
9
|
+
const statisticItemSchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
title: z.string(),
|
|
12
|
+
value: z.string(),
|
|
13
|
+
description: z.string(),
|
|
14
|
+
progress: z.number(),
|
|
15
|
+
progressLabel: z.string(),
|
|
16
|
+
icon: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const statisticsPropsSchema = z.object({
|
|
20
|
+
title: z.string().optional(),
|
|
21
|
+
badge: z.string().optional(),
|
|
22
|
+
description: z.string().optional(),
|
|
23
|
+
items: z.array(statisticItemSchema),
|
|
24
|
+
})
|
|
5
25
|
|
|
6
26
|
export const statisticsSection = {
|
|
7
27
|
elementType: 'Statistics',
|
|
8
28
|
catalog: {
|
|
9
29
|
Statistics: {
|
|
10
|
-
props:
|
|
11
|
-
title: z.string().optional(),
|
|
12
|
-
badge: z.string().optional(),
|
|
13
|
-
description: z.string().optional(),
|
|
14
|
-
items: z.array(z.record(z.string(), z.unknown())),
|
|
15
|
-
}),
|
|
30
|
+
props: statisticsPropsSchema,
|
|
16
31
|
description: 'Key metrics or trust statistics.',
|
|
17
32
|
},
|
|
18
33
|
},
|
|
19
34
|
registry: {
|
|
20
|
-
Statistics: ({ props }: { props: Record<string, unknown> }) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
Statistics: ({ props }: { props: Record<string, unknown> }) => {
|
|
36
|
+
const parsed = statisticsPropsSchema.parse(props)
|
|
37
|
+
return (
|
|
38
|
+
<StatisticBlock
|
|
39
|
+
badge={parsed.badge}
|
|
40
|
+
title={parsed.title ?? 'Números que nos respaldan'}
|
|
41
|
+
description={parsed.description}
|
|
42
|
+
items={parsed.items.map((item) => ({
|
|
43
|
+
...item,
|
|
44
|
+
icon: resolveIcon(item.icon),
|
|
45
|
+
}))}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
},
|
|
28
49
|
},
|
|
29
50
|
}
|
|
@@ -3,26 +3,44 @@
|
|
|
3
3
|
import { Testimonial1 as TestimonialBlock } from '@thorprovider/blocks'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
6
|
+
// Mirrors the Testimonial1Item/Testimonial1Props block contracts.
|
|
7
|
+
const testimonialItemSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
role: z.string(),
|
|
11
|
+
avatar: z.string(),
|
|
12
|
+
content: z.string(),
|
|
13
|
+
rating: z.number(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const testimonialsPropsSchema = z.object({
|
|
17
|
+
title: z.string().optional(),
|
|
18
|
+
prevLabel: z.string().optional(),
|
|
19
|
+
nextLabel: z.string().optional(),
|
|
20
|
+
slideAriaLabel: z.string().optional(),
|
|
21
|
+
items: z.array(testimonialItemSchema),
|
|
22
|
+
})
|
|
23
|
+
|
|
6
24
|
export const testimonialsSection = {
|
|
7
25
|
elementType: 'Testimonials',
|
|
8
26
|
catalog: {
|
|
9
27
|
Testimonials: {
|
|
10
|
-
props:
|
|
11
|
-
title: z.string().optional(),
|
|
12
|
-
items: z.array(z.record(z.string(), z.unknown())),
|
|
13
|
-
}),
|
|
28
|
+
props: testimonialsPropsSchema,
|
|
14
29
|
description: 'Customer testimonials or social proof.',
|
|
15
30
|
},
|
|
16
31
|
},
|
|
17
32
|
registry: {
|
|
18
|
-
Testimonials: ({ props }: { props: Record<string, unknown> }) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
Testimonials: ({ props }: { props: Record<string, unknown> }) => {
|
|
34
|
+
const parsed = testimonialsPropsSchema.parse(props)
|
|
35
|
+
return (
|
|
36
|
+
<TestimonialBlock
|
|
37
|
+
title={parsed.title ?? 'Lo que dicen nuestros clientes'}
|
|
38
|
+
items={parsed.items}
|
|
39
|
+
prevLabel={parsed.prevLabel ?? 'Anterior'}
|
|
40
|
+
nextLabel={parsed.nextLabel ?? 'Siguiente'}
|
|
41
|
+
slideAriaLabel={parsed.slideAriaLabel ?? 'Testimonio'}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
},
|
|
27
45
|
},
|
|
28
46
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ReactNode } from 'react'
|
|
7
7
|
import type { BaseComponentProps } from '@json-render/react'
|
|
8
|
-
import type {
|
|
8
|
+
import type { StorefrontLoaderFn } from '@thorprovider/runtime/render-core/app'
|
|
9
9
|
import type { ZodType } from 'zod'
|
|
10
10
|
import type { StorefrontElement } from '../spec/types'
|
|
11
11
|
|
|
@@ -41,6 +41,8 @@ export interface ViewModule {
|
|
|
41
41
|
elements: Record<string, StorefrontElement>
|
|
42
42
|
catalog: Record<string, ViewCatalogEntry>
|
|
43
43
|
registry: ViewRegistry
|
|
44
|
-
route
|
|
45
|
-
|
|
44
|
+
// A view may contribute one route or several (a content-driven view declares
|
|
45
|
+
// one fixed route per page). The spec barrel normalises and flattens them.
|
|
46
|
+
route: ViewRoute | ViewRoute[]
|
|
47
|
+
loader: Record<string, StorefrontLoaderFn>
|
|
46
48
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Generated by Thor Commerce storefront (feature: checkout — handlers)
|
|
4
|
+
// The checkout capability's action handlers. Installed only when the feature is
|
|
5
|
+
// in the manifest; the handlers composer calls this hook. Every mutation goes
|
|
6
|
+
// through a Server Action; the backend stays authoritative for totals.
|
|
7
|
+
|
|
8
|
+
import { useEffect } from 'react'
|
|
9
|
+
import { useActions, useStateStore } from '@json-render/react'
|
|
10
|
+
import { useNextApp } from '@json-render/next'
|
|
11
|
+
import {
|
|
12
|
+
placeOrderAction,
|
|
13
|
+
reconcileReceiptAction,
|
|
14
|
+
selectShippingMethodAction,
|
|
15
|
+
syncCheckoutAddressAction,
|
|
16
|
+
} from '../../actions'
|
|
17
|
+
import type { CartStatePatch, PlaceOrderResult } from '../../commerce'
|
|
18
|
+
import { recordConfirmedTransition } from '../../section-observation'
|
|
19
|
+
|
|
20
|
+
// The always-required core of an address. Optional fields (province, phone,
|
|
21
|
+
// postal code) are left to the runtime validator so the host does not impose a
|
|
22
|
+
// stricter contract than the backend; the validator returns structured errors.
|
|
23
|
+
const REQUIRED_ADDRESS_FIELDS = ['email', 'firstName', 'lastName', 'city', 'country'] as const
|
|
24
|
+
|
|
25
|
+
function addressComplete(values: Record<string, unknown>): boolean {
|
|
26
|
+
return REQUIRED_ADDRESS_FIELDS.every(
|
|
27
|
+
(field) => typeof values[field] === 'string' && (values[field] as string).trim() !== '',
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useCheckoutHandlers() {
|
|
32
|
+
const { get, set } = useStateStore()
|
|
33
|
+
const { navigate } = useNextApp()
|
|
34
|
+
const { registerHandler } = useActions()
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const applyCart = (patch: CartStatePatch) => {
|
|
38
|
+
set('/cart', patch.cart)
|
|
39
|
+
set('/cartTotals', patch.cartTotals)
|
|
40
|
+
set('/cartId', patch.cartId)
|
|
41
|
+
set('/cartQuantity', patch.cartQuantity)
|
|
42
|
+
set('/cartCompleted', patch.cartCompleted)
|
|
43
|
+
set('/cartSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
|
|
44
|
+
// Rehydrate the checkout selection from the backend cart so a reload or
|
|
45
|
+
// an applyCart restores the highlighted shipping option.
|
|
46
|
+
set('/checkoutSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
|
|
47
|
+
set('/cartReadError', null)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const applyResult = (result: PlaceOrderResult) => {
|
|
51
|
+
set('/checkoutPending', false)
|
|
52
|
+
if (result.status === 'order') {
|
|
53
|
+
// A confirmed, persisted order — observed only on a genuine order result
|
|
54
|
+
// (not captured funds) and only when a section origin exists.
|
|
55
|
+
recordConfirmedTransition('order_confirmed')
|
|
56
|
+
// ONE atomic write carries the order and its backend lines.
|
|
57
|
+
set('/lastReceipt', { order: result.order, lines: result.lines })
|
|
58
|
+
set('/orderStatus', 'complete')
|
|
59
|
+
set('/checkoutError', null)
|
|
60
|
+
set('/checkoutErrorKind', null)
|
|
61
|
+
set('/cartNotice', null)
|
|
62
|
+
// The active cart is cleared by the session action; mirror it in state.
|
|
63
|
+
set('/cart', [])
|
|
64
|
+
set('/cartTotals', null)
|
|
65
|
+
set('/cartId', null)
|
|
66
|
+
set('/cartQuantity', 0)
|
|
67
|
+
set('/cartCompleted', true)
|
|
68
|
+
set('/checkoutSelectedShippingOptionId', null)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
// error | cart-error | unresolved. `orderStatus` only carries the UI
|
|
72
|
+
// states; the native discriminator is preserved in `checkoutErrorKind`.
|
|
73
|
+
// The active cart is NEVER cleared here, so a cart-with-error keeps its
|
|
74
|
+
// inputs and lines.
|
|
75
|
+
set('/orderStatus', result.status === 'cart-error' ? 'error' : result.status)
|
|
76
|
+
set('/checkoutErrorKind', result.status)
|
|
77
|
+
set('/checkoutError', result.message)
|
|
78
|
+
set(
|
|
79
|
+
'/cartNotice',
|
|
80
|
+
result.status === 'unresolved'
|
|
81
|
+
? 'El pedido pudo haberse registrado. Usa "Reconciliar" para recuperar el comprobante; no lo reenvíes.'
|
|
82
|
+
: null,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A rejected Server Action (network/RPC) must clear pending and surface a
|
|
87
|
+
// message; never swallow it. Only a non-secret context line is logged.
|
|
88
|
+
const onError = (scope: string) => (error: unknown) => {
|
|
89
|
+
console.error(
|
|
90
|
+
`[storefront/checkout] ${scope} failed`,
|
|
91
|
+
error instanceof Error ? error.message : error,
|
|
92
|
+
)
|
|
93
|
+
set('/checkoutPending', false)
|
|
94
|
+
set('/checkoutError', 'No se pudo completar la operación. Inténtalo de nuevo.')
|
|
95
|
+
set('/checkoutErrorKind', 'error')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const handlers: Record<string, () => void> = {
|
|
99
|
+
goToCheckout: () => {
|
|
100
|
+
// A confirmed checkout transition (the shopper started checkout).
|
|
101
|
+
recordConfirmedTransition('checkout_started')
|
|
102
|
+
set('/cartOpen', false)
|
|
103
|
+
navigate('/checkout')
|
|
104
|
+
},
|
|
105
|
+
continueShopping: () => navigate('/'),
|
|
106
|
+
placeOrder: () => {
|
|
107
|
+
set('/checkoutPending', true)
|
|
108
|
+
set('/checkoutError', null)
|
|
109
|
+
set('/cartNotice', null)
|
|
110
|
+
void placeOrderAction().then(applyResult).catch(onError('placeOrder'))
|
|
111
|
+
},
|
|
112
|
+
reconcileOrder: () => {
|
|
113
|
+
set('/checkoutPending', true)
|
|
114
|
+
set('/checkoutError', null)
|
|
115
|
+
void reconcileReceiptAction().then(applyResult).catch(onError('reconcileOrder'))
|
|
116
|
+
},
|
|
117
|
+
syncCheckoutAddress: () => {
|
|
118
|
+
const values = (get('/checkout') ?? {}) as Record<string, unknown>
|
|
119
|
+
if (!addressComplete(values)) return
|
|
120
|
+
set('/checkoutPending', true)
|
|
121
|
+
void syncCheckoutAddressAction(values)
|
|
122
|
+
.then((result) => {
|
|
123
|
+
set('/checkoutPending', false)
|
|
124
|
+
if (result.ok) {
|
|
125
|
+
applyCart(result.cart)
|
|
126
|
+
if (result.shippingOptions) set('/checkoutShippingOptions', result.shippingOptions)
|
|
127
|
+
set('/checkoutFieldErrors', result.fieldErrors ?? {})
|
|
128
|
+
set('/checkoutError', null)
|
|
129
|
+
} else {
|
|
130
|
+
set('/checkoutError', result.error)
|
|
131
|
+
set('/checkoutFieldErrors', result.fieldErrors ?? {})
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.catch(onError('syncCheckoutAddress'))
|
|
135
|
+
},
|
|
136
|
+
selectShippingMethod: () => {
|
|
137
|
+
const methodId = get('/checkoutSelectedShippingOptionId') as string | null
|
|
138
|
+
if (!methodId) return
|
|
139
|
+
set('/checkoutPending', true)
|
|
140
|
+
void selectShippingMethodAction({ methodId })
|
|
141
|
+
.then((result) => {
|
|
142
|
+
set('/checkoutPending', false)
|
|
143
|
+
if (result.ok) {
|
|
144
|
+
applyCart(result.cart)
|
|
145
|
+
set('/checkoutError', null)
|
|
146
|
+
} else {
|
|
147
|
+
set('/checkoutError', result.error)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
.catch(onError('selectShippingMethod'))
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
155
|
+
registerHandler(name, handler)
|
|
156
|
+
}
|
|
157
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
158
|
+
}, [])
|
|
159
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (feature: checkout — state slice)
|
|
2
|
+
// The checkout capability's state. Installed only when the feature is in the
|
|
3
|
+
// manifest; the state composer spreads it over the core state.
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CheckoutFieldErrors,
|
|
7
|
+
CheckoutFormData,
|
|
8
|
+
CheckoutShippingOption,
|
|
9
|
+
OrderStatus,
|
|
10
|
+
StorefrontReceipt,
|
|
11
|
+
} from '../../state.core'
|
|
12
|
+
import type { PaymentMethod } from '@thorprovider/types'
|
|
13
|
+
|
|
14
|
+
const checkoutFormDefaults: CheckoutFormData = {
|
|
15
|
+
email: '',
|
|
16
|
+
firstName: '',
|
|
17
|
+
lastName: '',
|
|
18
|
+
phone: '',
|
|
19
|
+
address: '',
|
|
20
|
+
city: '',
|
|
21
|
+
state: '',
|
|
22
|
+
zipCode: '',
|
|
23
|
+
country: '',
|
|
24
|
+
paymentMethod: '',
|
|
25
|
+
cardNumber: '',
|
|
26
|
+
expiryDate: '',
|
|
27
|
+
cvv: '',
|
|
28
|
+
cardName: '',
|
|
29
|
+
saveInfo: false,
|
|
30
|
+
sameAsBilling: false,
|
|
31
|
+
newsletter: false,
|
|
32
|
+
promoCode: '',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CheckoutState {
|
|
36
|
+
checkout: CheckoutFormData
|
|
37
|
+
checkoutCountries: { value: string; label: string }[]
|
|
38
|
+
checkoutPaymentMethods: PaymentMethod[]
|
|
39
|
+
checkoutShippingOptions: CheckoutShippingOption[]
|
|
40
|
+
checkoutSelectedShippingOptionId: string | null
|
|
41
|
+
checkoutFieldErrors: CheckoutFieldErrors
|
|
42
|
+
checkoutPending: boolean
|
|
43
|
+
checkoutError: string | null
|
|
44
|
+
checkoutErrorKind: 'error' | 'cart-error' | 'unresolved' | null
|
|
45
|
+
lastReceipt: StorefrontReceipt | null
|
|
46
|
+
orderStatus: OrderStatus
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const checkoutState: CheckoutState = {
|
|
50
|
+
checkout: checkoutFormDefaults,
|
|
51
|
+
checkoutCountries: [],
|
|
52
|
+
checkoutPaymentMethods: [],
|
|
53
|
+
checkoutShippingOptions: [],
|
|
54
|
+
checkoutSelectedShippingOptionId: null,
|
|
55
|
+
checkoutFieldErrors: {},
|
|
56
|
+
checkoutPending: false,
|
|
57
|
+
checkoutError: null,
|
|
58
|
+
checkoutErrorKind: null,
|
|
59
|
+
lastReceipt: null,
|
|
60
|
+
orderStatus: 'idle',
|
|
61
|
+
}
|