@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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Generated by Thor Commerce storefront (composition core: handlers)
|
|
4
|
+
// Core json-render action handlers, independent of any feature. Installed
|
|
5
|
+
// features register their own handlers; `handlers.ts` (the composer) calls both.
|
|
6
|
+
//
|
|
7
|
+
// Cart handlers call the Server Actions (never mutate the cart locally) and
|
|
8
|
+
// write the backend-authoritative snapshot back into state. Payloads the blocks
|
|
9
|
+
// emit are preserved: adds carry the selected backend variant id and quantity,
|
|
10
|
+
// cart updates/removals carry the backend line-item id, and the Navbar delta is
|
|
11
|
+
// normalized against the authoritative line in the action.
|
|
12
|
+
|
|
13
|
+
import { useEffect } from 'react'
|
|
14
|
+
import { useActions, useStateStore } from '@json-render/react'
|
|
15
|
+
import { useNextApp } from '@json-render/next'
|
|
16
|
+
import {
|
|
17
|
+
addToCartAction,
|
|
18
|
+
addToCartByProductAction,
|
|
19
|
+
applyQuantityDeltaAction,
|
|
20
|
+
ensureCartAction,
|
|
21
|
+
loadCartStateAction,
|
|
22
|
+
removeLineAction,
|
|
23
|
+
setLineQuantityAction,
|
|
24
|
+
} from './actions'
|
|
25
|
+
import type { CartStatePatch } from './commerce'
|
|
26
|
+
import { cartViewAvailable } from './composition'
|
|
27
|
+
import { recordConfirmedTransition } from './section-observation'
|
|
28
|
+
|
|
29
|
+
/** A product-like object the views leave in `/lastClicked` (card/hero shortcut). */
|
|
30
|
+
interface ClickedProduct {
|
|
31
|
+
id?: string | number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The explicit add-to-cart payload the product overview emits. */
|
|
35
|
+
interface AddToCartPayload {
|
|
36
|
+
variantId: string
|
|
37
|
+
quantity: number
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useCoreHandlers() {
|
|
41
|
+
const { get, set } = useStateStore()
|
|
42
|
+
const { navigate } = useNextApp()
|
|
43
|
+
const { registerHandler } = useActions()
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const applyCart = (patch: CartStatePatch) => {
|
|
47
|
+
set('/cart', patch.cart)
|
|
48
|
+
set('/cartTotals', patch.cartTotals)
|
|
49
|
+
set('/cartId', patch.cartId)
|
|
50
|
+
set('/cartQuantity', patch.cartQuantity)
|
|
51
|
+
set('/cartCompleted', patch.cartCompleted)
|
|
52
|
+
set('/cartSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
|
|
53
|
+
set('/checkoutSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
|
|
54
|
+
set('/cartError', null)
|
|
55
|
+
set('/cartReadError', null)
|
|
56
|
+
set('/cartPending', false)
|
|
57
|
+
}
|
|
58
|
+
const fail = (error: string) => {
|
|
59
|
+
set('/cartError', error)
|
|
60
|
+
set('/cartPending', false)
|
|
61
|
+
}
|
|
62
|
+
const begin = () => {
|
|
63
|
+
set('/cartPending', true)
|
|
64
|
+
set('/cartError', null)
|
|
65
|
+
set('/cartNotice', null)
|
|
66
|
+
}
|
|
67
|
+
// A rejected Server Action (network/RPC) must clear pending and surface a
|
|
68
|
+
// message; never swallow it. Only a non-secret context line is logged.
|
|
69
|
+
const onError = (scope: string) => (error: unknown) => {
|
|
70
|
+
console.error(
|
|
71
|
+
`[storefront/handlers] ${scope} failed`,
|
|
72
|
+
error instanceof Error ? error.message : error,
|
|
73
|
+
)
|
|
74
|
+
fail('No se pudo completar la operación. Inténtalo de nuevo.')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const handlers: Record<string, () => void> = {
|
|
78
|
+
addToCart: () => {
|
|
79
|
+
const payload = get('/lastAddToCart') as AddToCartPayload | null
|
|
80
|
+
if (!payload?.variantId) return
|
|
81
|
+
begin()
|
|
82
|
+
void addToCartAction({ variantId: payload.variantId, quantity: payload.quantity })
|
|
83
|
+
.then((result) => {
|
|
84
|
+
if (result.ok) {
|
|
85
|
+
applyCart(result.cart)
|
|
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')
|
|
90
|
+
} else fail(result.error)
|
|
91
|
+
})
|
|
92
|
+
.catch(onError('addToCart'))
|
|
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
|
+
},
|
|
111
|
+
ensureCart: () => {
|
|
112
|
+
begin()
|
|
113
|
+
void ensureCartAction()
|
|
114
|
+
.then((result) => {
|
|
115
|
+
if (result.ok) applyCart(result.cart)
|
|
116
|
+
else fail(result.error)
|
|
117
|
+
})
|
|
118
|
+
.catch(onError('ensureCart'))
|
|
119
|
+
},
|
|
120
|
+
refreshCart: () => {
|
|
121
|
+
void loadCartStateAction().then(applyCart).catch(onError('refreshCart'))
|
|
122
|
+
},
|
|
123
|
+
updateQuantity: () => {
|
|
124
|
+
const { id, merchandiseId, quantity } = (get('/qtyUpdate') ?? {}) as {
|
|
125
|
+
id?: string
|
|
126
|
+
merchandiseId?: string
|
|
127
|
+
quantity?: number
|
|
128
|
+
}
|
|
129
|
+
if (!id || !merchandiseId || quantity == null) return
|
|
130
|
+
begin()
|
|
131
|
+
void setLineQuantityAction({ lineId: id, merchandiseId, quantity })
|
|
132
|
+
.then((result) => {
|
|
133
|
+
if (result.ok) applyCart(result.cart)
|
|
134
|
+
else fail(result.error)
|
|
135
|
+
})
|
|
136
|
+
.catch(onError('updateQuantity'))
|
|
137
|
+
},
|
|
138
|
+
applyQuantityDelta: () => {
|
|
139
|
+
const { id, delta } = (get('/qtyDelta') ?? {}) as { id?: string; delta?: number }
|
|
140
|
+
if (!id || delta == null) return
|
|
141
|
+
begin()
|
|
142
|
+
void applyQuantityDeltaAction({ lineId: id, delta })
|
|
143
|
+
.then((result) => {
|
|
144
|
+
if (result.ok) applyCart(result.cart)
|
|
145
|
+
else fail(result.error)
|
|
146
|
+
})
|
|
147
|
+
.catch(onError('applyQuantityDelta'))
|
|
148
|
+
},
|
|
149
|
+
removeFromCart: () => {
|
|
150
|
+
const id = get('/lastRemoved') as string | null
|
|
151
|
+
if (!id) return
|
|
152
|
+
begin()
|
|
153
|
+
void removeLineAction({ lineId: id })
|
|
154
|
+
.then((result) => {
|
|
155
|
+
if (result.ok) applyCart(result.cart)
|
|
156
|
+
else fail(result.error)
|
|
157
|
+
})
|
|
158
|
+
.catch(onError('removeFromCart'))
|
|
159
|
+
},
|
|
160
|
+
openCart: () => set('/cartOpen', true),
|
|
161
|
+
closeCart: () => set('/cartOpen', false),
|
|
162
|
+
dismissBanner: () => set('/bannerDismissed', true),
|
|
163
|
+
dismissDataNotice: () => set('/dataNoticeDismissed', true),
|
|
164
|
+
openProduct: () => {
|
|
165
|
+
const product = get('/lastClicked') as ClickedProduct | null
|
|
166
|
+
if (!product?.id) return
|
|
167
|
+
navigate(`/products/${product.id}`)
|
|
168
|
+
},
|
|
169
|
+
openCategory: () => {
|
|
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')
|
|
175
|
+
},
|
|
176
|
+
goHome: () => {
|
|
177
|
+
set('/cartOpen', false)
|
|
178
|
+
navigate('/')
|
|
179
|
+
},
|
|
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
|
+
},
|
|
187
|
+
submitReview: () => {},
|
|
188
|
+
toggleReviewLike: () => {},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
192
|
+
registerHandler(name, handler)
|
|
193
|
+
}
|
|
194
|
+
// registerHandler uses functional state updates; store/navigate captured once.
|
|
195
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
196
|
+
}, [])
|
|
197
|
+
}
|
|
@@ -1,42 +1,11 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
// Generated by Thor Commerce storefront (
|
|
4
|
-
//
|
|
3
|
+
// Generated by Thor Commerce storefront (composition composer: handlers).
|
|
4
|
+
// Do not edit by hand — the manifest is the source of truth; run `storefront sync`.
|
|
5
|
+
// Installed features contribute their handlers below.
|
|
5
6
|
|
|
6
|
-
import {
|
|
7
|
-
import { useActions, useStateStore } from '@json-render/react'
|
|
8
|
-
import { useNextApp } from '@json-render/next'
|
|
7
|
+
import { useCoreHandlers } from './handlers.core'
|
|
9
8
|
|
|
10
9
|
export function useStorefrontHandlers() {
|
|
11
|
-
|
|
12
|
-
const { navigate } = useNextApp()
|
|
13
|
-
const { registerHandler } = useActions()
|
|
14
|
-
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
const handlers: Record<string, () => void> = {
|
|
17
|
-
addToCart: () => {},
|
|
18
|
-
openCart: () => {},
|
|
19
|
-
openProduct: () => navigate('/productos'),
|
|
20
|
-
openCategory: () => {
|
|
21
|
-
const handle = ''
|
|
22
|
-
navigate(handle ? `/categorias/${handle}` : '/categorias')
|
|
23
|
-
},
|
|
24
|
-
goHome: () => navigate('/'),
|
|
25
|
-
applyFilters: () => {},
|
|
26
|
-
placeOrder: () => navigate('/checkout'),
|
|
27
|
-
continueShopping: () => navigate('/'),
|
|
28
|
-
viewOrder: () => {},
|
|
29
|
-
downloadInvoice: () => {},
|
|
30
|
-
submitReview: () => {},
|
|
31
|
-
toggleReviewLike: () => {},
|
|
32
|
-
newsletterSubmit: () => {},
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
for (const [name, handler] of Object.entries(handlers)) {
|
|
36
|
-
registerHandler(name, handler)
|
|
37
|
-
}
|
|
38
|
-
void set
|
|
39
|
-
// registerHandler uses functional state updates; store/navigate captured once.
|
|
40
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
|
-
}, [])
|
|
10
|
+
useCoreHandlers()
|
|
42
11
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/icon-map.ts)
|
|
2
|
+
// Static icon-name → lucide component map. View specs reference icons by
|
|
3
|
+
// kebab-case name (they are data, serialized through state/spec); view
|
|
4
|
+
// registries resolve them here so blocks receive real components — never raw
|
|
5
|
+
// strings, which React renders as unknown HTML tags. Extend this map when a
|
|
6
|
+
// view introduces a new icon name.
|
|
7
|
+
|
|
8
|
+
import type { LucideIcon } from 'lucide-react'
|
|
9
|
+
import { Circle, Gift, Headphones, Percent, ShieldCheck, Star, Truck, Zap } from 'lucide-react'
|
|
10
|
+
|
|
11
|
+
/** Neutral fallback rendered for names not present in the map. */
|
|
12
|
+
export const FALLBACK_ICON: LucideIcon = Circle
|
|
13
|
+
|
|
14
|
+
export const iconMap: Record<string, LucideIcon> = {
|
|
15
|
+
gift: Gift,
|
|
16
|
+
headphones: Headphones,
|
|
17
|
+
percent: Percent,
|
|
18
|
+
'shield-check': ShieldCheck,
|
|
19
|
+
star: Star,
|
|
20
|
+
truck: Truck,
|
|
21
|
+
zap: Zap,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Resolve a spec icon name to a lucide component, falling back safely. */
|
|
25
|
+
export function resolveIcon(name: string | undefined): LucideIcon {
|
|
26
|
+
return (name && iconMap[name]) || FALLBACK_ICON
|
|
27
|
+
}
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// Kept out of the views so a view module stays a pure server spec; mappers are
|
|
4
4
|
// data-only and imported where the loader builds block props.
|
|
5
5
|
|
|
6
|
-
import type { CardProduct, FeaturedProduct, ProductCategory2Category } from '@thorprovider/blocks'
|
|
7
|
-
import type { ProductCategory } from '@thorprovider/types'
|
|
6
|
+
import type { CardProduct, FeaturedProduct, ProductCategory2Category, StoreCategory } from '@thorprovider/blocks'
|
|
8
7
|
|
|
9
8
|
export const CATEGORY_IMAGE_FALLBACK =
|
|
10
9
|
"data:image/svg+xml;charset=utf-8," +
|
|
@@ -32,16 +31,19 @@ export function toFeaturedProduct(card: CardProduct): FeaturedProduct {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
/**
|
|
36
|
-
* fallbacks for description
|
|
37
|
-
|
|
34
|
+
/** StoreCategory (what @thorprovider/runtime/commerce's getCatalog returns) →
|
|
35
|
+
* ProductCategory2Category (block), with fallbacks for description and image.
|
|
36
|
+
* The runtime projection keeps only label/href and carries no product count, so
|
|
37
|
+
* `itemCount` is unknown here: it is OMITTED (the block then renders no count)
|
|
38
|
+
* rather than fabricated as `0`. A genuine `0` supplied by a richer source is
|
|
39
|
+
* preserved because `itemCount` is optional, not defaulted. */
|
|
40
|
+
export function toCategoryCard(category: StoreCategory): ProductCategory2Category {
|
|
38
41
|
return {
|
|
39
|
-
id: category.
|
|
40
|
-
handle: category.
|
|
41
|
-
name: category.
|
|
42
|
-
description:
|
|
43
|
-
|
|
44
|
-
image: category.image ?? category.metadata?.image ?? CATEGORY_IMAGE_FALLBACK,
|
|
42
|
+
id: category.label,
|
|
43
|
+
handle: category.href,
|
|
44
|
+
name: category.label,
|
|
45
|
+
description: '',
|
|
46
|
+
image: CATEGORY_IMAGE_FALLBACK,
|
|
45
47
|
trending: false,
|
|
46
48
|
}
|
|
47
49
|
}
|
|
@@ -2,31 +2,250 @@
|
|
|
2
2
|
|
|
3
3
|
// Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/registry.tsx)
|
|
4
4
|
// Base registry for the app shell plus every registered view's registry fragment.
|
|
5
|
+
//
|
|
6
|
+
// The shell renders the live backend cart: the Navbar mini-cart and the cart
|
|
7
|
+
// drawer read the authoritative cart state and dispatch cart mutations to the
|
|
8
|
+
// Server Actions. A listing card's add-to-cart adds the product's first
|
|
9
|
+
// purchasable variant (the card shortcut); opening a product from a listing
|
|
10
|
+
// navigates to the detail, where a variant is chosen explicitly.
|
|
5
11
|
|
|
6
|
-
import type { Components } from '@json-render/react'
|
|
7
12
|
import { defineRegistry, useStateStore } from '@json-render/react'
|
|
8
|
-
import {
|
|
13
|
+
import { useEffect } from 'react'
|
|
14
|
+
import { usePathname } from 'next/navigation'
|
|
15
|
+
import {
|
|
16
|
+
BackToTop as BackToTopBlock,
|
|
17
|
+
Banner1,
|
|
18
|
+
FooterSection1,
|
|
19
|
+
MobileBottomNav as MobileBottomNavBlock,
|
|
20
|
+
Navbar1,
|
|
21
|
+
Navbar4,
|
|
22
|
+
ProductCard1,
|
|
23
|
+
SkipToContent as SkipToContentBlock,
|
|
24
|
+
type BackToTopProps,
|
|
25
|
+
type MobileBottomNavProps,
|
|
26
|
+
type SkipToContentProps,
|
|
27
|
+
} from '@thorprovider/blocks'
|
|
28
|
+
import { isAvailableMoney } from '@thorprovider/blocks/contracts'
|
|
29
|
+
import type { Money } from '@thorprovider/types'
|
|
9
30
|
import { catalog } from './catalog'
|
|
31
|
+
import type { CartLineState, CartTotalsState } from './state'
|
|
10
32
|
import { useStorefrontHandlers } from './handlers'
|
|
11
|
-
import {
|
|
33
|
+
import { cartViewAvailable, checkoutAvailable, mountedFeatureNames } from './composition'
|
|
34
|
+
import {
|
|
35
|
+
bannerPropsSchema,
|
|
36
|
+
footerPropsSchema,
|
|
37
|
+
navbarPropsSchema,
|
|
38
|
+
productCardPropsSchema,
|
|
39
|
+
storefrontNavbarPropsSchema,
|
|
40
|
+
} from './schema'
|
|
41
|
+
import { viewRegistries } from './views/registry'
|
|
42
|
+
|
|
43
|
+
// Navbar4 accepts its own mini-cart line shape (`qty`), distinct from the block
|
|
44
|
+
// `CartItem` contract (`quantity`). Types are local so the asymmetry is explicit.
|
|
45
|
+
interface NavCartItem {
|
|
46
|
+
id: string
|
|
47
|
+
name: string
|
|
48
|
+
variant?: string
|
|
49
|
+
price: number
|
|
50
|
+
qty: number
|
|
51
|
+
image?: string
|
|
52
|
+
lineTotal?: Money
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** CartLineState -> Navbar4 cart line. `price` is legacy; the authoritative
|
|
56
|
+
* subtotal and per-line total are passed separately, so neither is computed
|
|
57
|
+
* from the line. */
|
|
58
|
+
function toNavCartItems(cart: CartLineState[]): NavCartItem[] {
|
|
59
|
+
return cart.map((line) => ({
|
|
60
|
+
id: line.id,
|
|
61
|
+
name: line.name,
|
|
62
|
+
variant: line.variant,
|
|
63
|
+
price: 0,
|
|
64
|
+
qty: line.quantity,
|
|
65
|
+
image: line.image,
|
|
66
|
+
lineTotal: line.lineTotal,
|
|
67
|
+
}))
|
|
68
|
+
}
|
|
12
69
|
|
|
13
70
|
const baseComponents = {
|
|
14
|
-
Layout: ({ children }: { children?: React.ReactNode }) => {
|
|
71
|
+
Layout: ({ children, slots }: { children?: React.ReactNode; slots?: Record<string, React.ReactNode> }) => {
|
|
15
72
|
useStorefrontHandlers()
|
|
16
|
-
|
|
73
|
+
const { set } = useStateStore()
|
|
74
|
+
const pathname = usePathname()
|
|
75
|
+
// The mobile bottom navigation is feature-gated; when it is installed the
|
|
76
|
+
// main content reserves bottom padding so the fixed bar never obscures it.
|
|
77
|
+
const hasBottomNav = mountedFeatureNames.includes('navigability')
|
|
78
|
+
// Central dismissal: any route or history change closes the shared cart
|
|
79
|
+
// lateral, so it never lingers across navigation (forward/back included).
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
set('/cartOpen', false)
|
|
82
|
+
}, [pathname, set])
|
|
83
|
+
return (
|
|
84
|
+
<div className="flex min-h-screen flex-col">
|
|
85
|
+
{slots?.chrome}
|
|
86
|
+
{slots?.notices}
|
|
87
|
+
<header className="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
88
|
+
{slots?.header}
|
|
89
|
+
</header>
|
|
90
|
+
<main
|
|
91
|
+
id="main-content"
|
|
92
|
+
className={hasBottomNav ? 'flex-1 pb-16 md:pb-0' : 'flex-1'}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</main>
|
|
96
|
+
<footer>{slots?.footer}</footer>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
// Neutral page root: renders the route page without shell chrome. Using it
|
|
101
|
+
// instead of `Layout` keeps the shell from mounting twice per page.
|
|
102
|
+
Page: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
|
|
103
|
+
// Navigability chrome, feature-gated by the shell spec (only referenced when
|
|
104
|
+
// the feature is installed). Props are cast to the block contracts; the
|
|
105
|
+
// catalog schema is the runtime guard.
|
|
106
|
+
SkipToContent: ({ props }: { props: Record<string, unknown> }) => {
|
|
107
|
+
const p = props as unknown as SkipToContentProps
|
|
108
|
+
return <SkipToContentBlock {...p} />
|
|
109
|
+
},
|
|
110
|
+
BackToTop: ({ props }: { props: Record<string, unknown> }) => {
|
|
111
|
+
const p = props as unknown as BackToTopProps
|
|
112
|
+
return <BackToTopBlock {...p} />
|
|
113
|
+
},
|
|
114
|
+
MobileBottomNav: ({ props }: { props: Record<string, unknown> }) => {
|
|
115
|
+
const pathname = usePathname()
|
|
116
|
+
const p = props as unknown as MobileBottomNavProps
|
|
117
|
+
return <MobileBottomNavBlock {...p} currentHref={p.currentHref ?? pathname} />
|
|
118
|
+
},
|
|
119
|
+
// Reserved top region for system/status messages. Self-gates on state and
|
|
120
|
+
// collapses to nothing when there is no notice. A configured mock is labelled
|
|
121
|
+
// as demo data; a real-mode failure is surfaced visibly, never hidden.
|
|
122
|
+
SystemNotice: () => {
|
|
123
|
+
const { state, set } = useStateStore()
|
|
124
|
+
if (state.dataNoticeDismissed) return null
|
|
125
|
+
const cartError = typeof state.cartReadError === 'string' ? state.cartReadError : null
|
|
126
|
+
const error = cartError ?? (typeof state.dataError === 'string' ? state.dataError : null)
|
|
127
|
+
const isMock = state.dataSource === 'mock'
|
|
128
|
+
if (!error && !isMock) return null
|
|
129
|
+
const destructive = Boolean(error)
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
role="alert"
|
|
133
|
+
className={
|
|
134
|
+
destructive
|
|
135
|
+
? 'flex items-center justify-between gap-4 border-b border-destructive/40 bg-destructive/10 px-4 py-2 text-sm text-destructive'
|
|
136
|
+
: 'flex items-center justify-between gap-4 border-b border-border bg-muted px-4 py-2 text-sm text-muted-foreground'
|
|
137
|
+
}
|
|
138
|
+
>
|
|
139
|
+
<span>
|
|
140
|
+
{error ??
|
|
141
|
+
'Datos de demostración (mock): no hay un backend real configurado.'}
|
|
142
|
+
</span>
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
aria-label="Cerrar aviso"
|
|
146
|
+
onClick={() => set('/dataNoticeDismissed', true)}
|
|
147
|
+
className="shrink-0 rounded p-1 hover:bg-destructive/20"
|
|
148
|
+
>
|
|
149
|
+
X
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
},
|
|
154
|
+
StorefrontNavbar: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => {
|
|
155
|
+
const parsed = storefrontNavbarPropsSchema.parse(props)
|
|
156
|
+
const { state, set } = useStateStore()
|
|
157
|
+
const cart = (state.cart ?? []) as CartLineState[]
|
|
158
|
+
const totals = state.cartTotals as CartTotalsState | null
|
|
159
|
+
const cartError = typeof state.cartError === 'string' ? state.cartError : undefined
|
|
160
|
+
const readError = typeof state.cartReadError === 'string' ? state.cartReadError : undefined
|
|
161
|
+
const errorLabel = cartError ?? readError
|
|
162
|
+
const cartState = state.cartPending ? 'pending' : errorLabel ? 'error' : 'idle'
|
|
163
|
+
// Checkout is offered only when the required backend amounts are present and
|
|
164
|
+
// valid: a missing/malformed subtotal, total, or line amount blocks it, while
|
|
165
|
+
// an undetermined shipping amount does not (AC-CART-4.4 / UC-CART-7).
|
|
166
|
+
const amountsValid =
|
|
167
|
+
cart.every((line) => isAvailableMoney(line.lineTotal)) &&
|
|
168
|
+
totals !== null &&
|
|
169
|
+
isAvailableMoney(totals.subtotal) &&
|
|
170
|
+
isAvailableMoney(totals.total)
|
|
171
|
+
return (
|
|
172
|
+
<Navbar4
|
|
173
|
+
storeCategories={parsed.storeCategories}
|
|
174
|
+
shippingNote={parsed.shippingNote}
|
|
175
|
+
utilityLinks={parsed.utilityLinks}
|
|
176
|
+
logoText={parsed.logoText}
|
|
177
|
+
logoHref={parsed.logoHref}
|
|
178
|
+
logoAccent={parsed.logoAccent}
|
|
179
|
+
className={parsed.className}
|
|
180
|
+
accountHref={parsed.accountHref}
|
|
181
|
+
// The current route marks the matching category link as the current page
|
|
182
|
+
// (accessibly, beyond color alone); no link is marked when it does not
|
|
183
|
+
// match any destination.
|
|
184
|
+
currentPath={usePathname()}
|
|
185
|
+
// Held global search (E2 task 5.4): default to hidden so no global
|
|
186
|
+
// search is presented while the contract is unverified; the user-owned
|
|
187
|
+
// shell seam may opt in with `showSearch: true`.
|
|
188
|
+
showSearch={parsed.showSearch ?? false}
|
|
189
|
+
items={toNavCartItems(cart)}
|
|
190
|
+
subtotal={totals?.subtotal}
|
|
191
|
+
// One shared lateral: visibility is driven by the shared `cartOpen`
|
|
192
|
+
// state (a successful add opens it; navigation dismisses it). The
|
|
193
|
+
// canonical line amounts and lifecycle states come from the same cart
|
|
194
|
+
// state the standalone `/cart` view reads.
|
|
195
|
+
canonicalCart
|
|
196
|
+
cartState={cartState}
|
|
197
|
+
cartErrorLabel={errorLabel}
|
|
198
|
+
onCartRetry={() => emit('refreshCart')}
|
|
199
|
+
cartOpen={state.cartOpen === true}
|
|
200
|
+
onCartOpenChange={(open) => set('/cartOpen', open)}
|
|
201
|
+
checkoutUnavailable={!checkoutAvailable}
|
|
202
|
+
checkoutDisabled={cartState !== 'idle' || cart.length === 0 || !amountsValid}
|
|
203
|
+
viewCartUnavailable={!cartViewAvailable}
|
|
204
|
+
onViewCart={() => emit('goToCart')}
|
|
205
|
+
onCheckout={() => emit('goToCheckout')}
|
|
206
|
+
onQtyChange={(id, delta) => {
|
|
207
|
+
set('/qtyDelta', { id, delta })
|
|
208
|
+
emit('applyQuantityDelta')
|
|
209
|
+
}}
|
|
210
|
+
onRemove={(id) => {
|
|
211
|
+
set('/lastRemoved', id)
|
|
212
|
+
emit('removeFromCart')
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
},
|
|
217
|
+
Banner: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => {
|
|
218
|
+
const parsed = bannerPropsSchema.parse(props)
|
|
219
|
+
const { set } = useStateStore()
|
|
220
|
+
return (
|
|
221
|
+
<Banner1
|
|
222
|
+
badge={parsed.badge}
|
|
223
|
+
message={parsed.message}
|
|
224
|
+
href={parsed.href}
|
|
225
|
+
linkLabel={parsed.linkLabel}
|
|
226
|
+
avatars={parsed.avatars}
|
|
227
|
+
onDismiss={() => {
|
|
228
|
+
set('/bannerDismissed', true)
|
|
229
|
+
emit('dismissBanner')
|
|
230
|
+
}}
|
|
231
|
+
/>
|
|
232
|
+
)
|
|
233
|
+
},
|
|
234
|
+
Navbar: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => {
|
|
235
|
+
const parsed = navbarPropsSchema.parse(props)
|
|
236
|
+
return (
|
|
237
|
+
<Navbar1
|
|
238
|
+
navLinks={parsed.navLinks}
|
|
239
|
+
logoText={parsed.logoText}
|
|
240
|
+
logoHref={parsed.logoHref}
|
|
241
|
+
currentPath={parsed.currentPath}
|
|
242
|
+
signIn={parsed.signIn}
|
|
243
|
+
cta={parsed.cta}
|
|
244
|
+
cartCount={parsed.cartCount}
|
|
245
|
+
onOpenCart={() => emit('openCart')}
|
|
246
|
+
/>
|
|
247
|
+
)
|
|
17
248
|
},
|
|
18
|
-
Navbar: ({ props, emit }: { props: Record<string, unknown>; emit: (event: string) => void }) => (
|
|
19
|
-
<Navbar1
|
|
20
|
-
navLinks={props.navLinks as never}
|
|
21
|
-
logoText={props.logoText as string}
|
|
22
|
-
logoHref={props.logoHref as string}
|
|
23
|
-
currentPath={props.currentPath as string}
|
|
24
|
-
signIn={props.signIn as never}
|
|
25
|
-
cta={props.cta as never}
|
|
26
|
-
cartCount={props.cartCount as number}
|
|
27
|
-
onOpenCart={() => emit('openCart')}
|
|
28
|
-
/>
|
|
29
|
-
),
|
|
30
249
|
ProductCard: ({
|
|
31
250
|
props,
|
|
32
251
|
emit,
|
|
@@ -34,18 +253,20 @@ const baseComponents = {
|
|
|
34
253
|
props: Record<string, unknown>
|
|
35
254
|
emit: (event: string) => void
|
|
36
255
|
}) => {
|
|
256
|
+
const parsed = productCardPropsSchema.parse(props)
|
|
37
257
|
const { set } = useStateStore()
|
|
38
258
|
const select = (product: unknown) => {
|
|
39
259
|
set('/lastClicked', product)
|
|
40
260
|
}
|
|
41
261
|
return (
|
|
42
262
|
<ProductCard1
|
|
43
|
-
products={
|
|
44
|
-
heading={
|
|
45
|
-
maxItems={
|
|
263
|
+
products={parsed.products}
|
|
264
|
+
heading={parsed.heading}
|
|
265
|
+
maxItems={parsed.maxItems}
|
|
266
|
+
// This base card opens the detail so a variant is chosen explicitly.
|
|
46
267
|
onAddToCart={(product) => {
|
|
47
268
|
select(product)
|
|
48
|
-
emit('
|
|
269
|
+
emit('openProduct')
|
|
49
270
|
}}
|
|
50
271
|
onProductClick={(product) => {
|
|
51
272
|
select(product)
|
|
@@ -55,44 +276,36 @@ const baseComponents = {
|
|
|
55
276
|
)
|
|
56
277
|
},
|
|
57
278
|
Footer: ({ props }: { props: Record<string, unknown> }) => {
|
|
58
|
-
const
|
|
59
|
-
const brandDescription = props.brandDescription as string | undefined
|
|
60
|
-
const linkColumns =
|
|
61
|
-
(props.linkColumns as { title: string; links: { label: string; href?: string }[] }[]) ?? []
|
|
279
|
+
const parsed = footerPropsSchema.parse(props)
|
|
62
280
|
return (
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
</ul>
|
|
83
|
-
</div>
|
|
84
|
-
))}
|
|
85
|
-
</div>
|
|
86
|
-
</footer>
|
|
281
|
+
<FooterSection1
|
|
282
|
+
brandName={parsed.brandName}
|
|
283
|
+
brandAccent={parsed.brandAccent}
|
|
284
|
+
brandDescription={parsed.brandDescription}
|
|
285
|
+
linkColumns={parsed.linkColumns}
|
|
286
|
+
className={parsed.className}
|
|
287
|
+
// The scaffold ships no newsletter submit callback. The seam's
|
|
288
|
+
// `newsletter` is content only, so the block swallows it and renders no
|
|
289
|
+
// form. Wiring a real host callback (which would enable the form) is an
|
|
290
|
+
// external integration, out of scope for the scaffold.
|
|
291
|
+
newsletter={
|
|
292
|
+
parsed.newsletter
|
|
293
|
+
? {
|
|
294
|
+
placeholder: parsed.newsletter.placeholder,
|
|
295
|
+
buttonLabel: parsed.newsletter.buttonLabel ?? 'Suscribirme',
|
|
296
|
+
}
|
|
297
|
+
: undefined
|
|
298
|
+
}
|
|
299
|
+
/>
|
|
87
300
|
)
|
|
88
301
|
},
|
|
89
302
|
}
|
|
90
303
|
|
|
91
|
-
const viewComponents = Object.assign({}, ...
|
|
304
|
+
const viewComponents = Object.assign({}, ...viewRegistries)
|
|
92
305
|
|
|
93
306
|
export const { registry } = defineRegistry(catalog, {
|
|
94
307
|
components: {
|
|
95
308
|
...baseComponents,
|
|
96
309
|
...viewComponents,
|
|
97
|
-
}
|
|
310
|
+
},
|
|
98
311
|
})
|