@thorprovider/storefront 0.3.1 → 0.4.0-mvp.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +248 -99
- package/assets/templates/app/app/[[...slug]]/page.tsx +13 -2
- package/assets/templates/app/app/layout.tsx +18 -4
- package/assets/templates/app/app/robots.ts +13 -0
- package/assets/templates/app/app/sitemap.ts +12 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +542 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +40 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +608 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +165 -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 +232 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +121 -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 +96 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +34 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +100 -11
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +26 -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 +142 -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 +17 -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/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/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 +153 -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 +36 -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 +378 -0
- package/assets/templates/views/account/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +28 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +193 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +33 -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 +230 -0
- package/assets/templates/views/category/props.ts +16 -0
- package/assets/templates/views/category/registry.tsx +103 -0
- package/assets/templates/views/category/schema.ts +78 -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 +24 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +72 -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 +15 -0
- package/assets/templates/views/home/index.tsx +38 -84
- 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 +48 -0
- package/assets/templates/views/product/index.tsx +104 -0
- package/assets/templates/views/product/props.ts +20 -0
- package/assets/templates/views/product/registry.tsx +78 -0
- package/assets/templates/views/product/schema.ts +108 -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/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/index.tsx +8 -70
- package/{skills/sf-view-gen/assets → assets}/templates/views/user-panel/registry.tsx +23 -18
- package/assets/templates/views/user-panel/schema.ts +79 -0
- package/bin/storefront.js +1278 -79
- package/commands/sf-add-view.md +44 -13
- package/commands/sf-feature.md +28 -0
- package/commands/sf-theme.md +14 -8
- package/commands/sf-view.md +54 -12
- package/package.json +6 -4
- package/recipes/archetypes.json +28 -1
- package/recipes/catalog.json +361 -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 +139 -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 +60 -5
- package/recipes/sections.json +24 -5
- package/recipes/validate.mjs +784 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +140 -0
- package/scripts/file-classes.mjs +116 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -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 +504 -0
- package/scripts/plan-apply.mjs +843 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +511 -0
- package/scripts/render-templates.mjs +245 -28
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/view-assembly.mjs +291 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +67 -0
- package/skills/sf-theme-gen/SKILL.md +12 -5
- package/skills/sf-view-gen/SKILL.md +123 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -1
- package/skills/sf-view-gen/assets/recipes/catalog.json +361 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +139 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +60 -5
- package/skills/sf-view-gen/assets/recipes/sections.json +24 -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/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.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/sf-view-gen/references/views.md +44 -10
- 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 +784 -32
- package/skills/storefront-workflow/SKILL.md +116 -0
- 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
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (composition core: state)
|
|
2
|
+
// Shared, feature-independent storefront state. Installed features add their
|
|
3
|
+
// own slices; `state.ts` (the composer) spreads them over `storefrontCoreState`.
|
|
4
|
+
//
|
|
5
|
+
// The cart slice mirrors the backend-authoritative projection: line totals and
|
|
6
|
+
// cart totals are canonical `Money` from the backend, never locally computed.
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CardProduct,
|
|
10
|
+
FeaturedProduct,
|
|
11
|
+
ProductCategory2Category,
|
|
12
|
+
ProductDetailProduct,
|
|
13
|
+
} from '@thorprovider/blocks'
|
|
14
|
+
import type { Money, PaymentMethod } from '@thorprovider/types'
|
|
15
|
+
|
|
16
|
+
/** Real review, rendered only when the backend supplies it. */
|
|
17
|
+
export interface ProductReview {
|
|
18
|
+
id: number
|
|
19
|
+
userName: string
|
|
20
|
+
userAvatar?: string
|
|
21
|
+
date: string
|
|
22
|
+
rating: number
|
|
23
|
+
content: string
|
|
24
|
+
likes?: number
|
|
25
|
+
comments?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A cart line projected from the backend cart. */
|
|
29
|
+
export interface CartLineState {
|
|
30
|
+
/** Backend cart line-item id — the update/removal identity. */
|
|
31
|
+
id: string
|
|
32
|
+
/** Backend variant id — the add identity (carried for line updates). */
|
|
33
|
+
merchandiseId: string
|
|
34
|
+
name: string
|
|
35
|
+
variant?: string
|
|
36
|
+
quantity: number
|
|
37
|
+
/** Backend line total; rendered verbatim, never `unit × qty` locally. */
|
|
38
|
+
lineTotal: Money
|
|
39
|
+
/** Backend unit price, when supplied; never derived from `lineTotal / qty`. */
|
|
40
|
+
unitPrice?: Money
|
|
41
|
+
/** Normalized optional image; omitted when the backend supplies none. */
|
|
42
|
+
image?: string
|
|
43
|
+
handle?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Backend-authoritative cart totals. */
|
|
47
|
+
export interface CartTotalsState {
|
|
48
|
+
subtotal: Money
|
|
49
|
+
discount?: Money
|
|
50
|
+
tax?: Money
|
|
51
|
+
shipping?: Money
|
|
52
|
+
total: Money
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Mirrors the CheckoutForm1FormData block contract (card fields stay unused). */
|
|
56
|
+
export interface CheckoutFormData {
|
|
57
|
+
email: string
|
|
58
|
+
firstName: string
|
|
59
|
+
lastName: string
|
|
60
|
+
phone: string
|
|
61
|
+
address: string
|
|
62
|
+
city: string
|
|
63
|
+
state: string
|
|
64
|
+
zipCode: string
|
|
65
|
+
country: string
|
|
66
|
+
paymentMethod: string
|
|
67
|
+
cardNumber: string
|
|
68
|
+
expiryDate: string
|
|
69
|
+
cvv: string
|
|
70
|
+
cardName: string
|
|
71
|
+
saveInfo: boolean
|
|
72
|
+
sameAsBilling: boolean
|
|
73
|
+
newsletter: boolean
|
|
74
|
+
promoCode: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** An eligible backend shipping option. */
|
|
78
|
+
export interface CheckoutShippingOption {
|
|
79
|
+
id: string
|
|
80
|
+
name: string
|
|
81
|
+
price?: Money
|
|
82
|
+
description?: string
|
|
83
|
+
estimatedDays?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Structured per-field checkout errors, mirroring the block contract. */
|
|
87
|
+
export type CheckoutFieldErrors = Record<string, { message?: string }[]>
|
|
88
|
+
|
|
89
|
+
/** Confirmation order, mirroring the CheckoutConfirmation block contract. */
|
|
90
|
+
export interface StorefrontOrder {
|
|
91
|
+
number: string
|
|
92
|
+
placedAt: string
|
|
93
|
+
email?: string
|
|
94
|
+
currentStage?: 'Confirmed' | 'Packed' | 'Shipped' | 'Delivered'
|
|
95
|
+
estimatedDelivery?: string | null
|
|
96
|
+
trackingHref?: string
|
|
97
|
+
shipping?: { name: string; lines: string[]; method?: string }
|
|
98
|
+
billing?: { name: string; lines: string[]; method?: string }
|
|
99
|
+
totals: { subtotal?: Money; discount?: Money; shipping?: Money; tax?: Money; total?: Money }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Confirmation line: backend Money line total, optional image omitted when absent. */
|
|
103
|
+
export interface StorefrontOrderLineView {
|
|
104
|
+
id: string
|
|
105
|
+
name: string
|
|
106
|
+
variant?: string
|
|
107
|
+
quantity: number
|
|
108
|
+
lineTotal: Money
|
|
109
|
+
unitPrice?: Money
|
|
110
|
+
image?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** A single line of a customer order-history row (display strings, no Money). */
|
|
114
|
+
export interface AccountOrderItemView {
|
|
115
|
+
id: string
|
|
116
|
+
name: string
|
|
117
|
+
variant?: string
|
|
118
|
+
quantity: number
|
|
119
|
+
lineTotal: string
|
|
120
|
+
image?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A customer order-history row, mapped from the backend order for the account
|
|
125
|
+
* view. Money is formatted to a display string from the order's own currency;
|
|
126
|
+
* an optional field the backend omits is omitted, never invented.
|
|
127
|
+
*/
|
|
128
|
+
export interface AccountOrderView {
|
|
129
|
+
id: string
|
|
130
|
+
number: string
|
|
131
|
+
date: string
|
|
132
|
+
status: string
|
|
133
|
+
total: string
|
|
134
|
+
subtotal?: string
|
|
135
|
+
tax?: string
|
|
136
|
+
shipping?: string
|
|
137
|
+
address?: string
|
|
138
|
+
items: AccountOrderItemView[]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The completed-order receipt as ONE atomic value.
|
|
143
|
+
*
|
|
144
|
+
* Order and its backend lines live together so a single state write carries
|
|
145
|
+
* them, and the SSR initial value is a stable `null`. json-render's
|
|
146
|
+
* StateProvider re-applies the SSR `initialState` on every RSC refresh and
|
|
147
|
+
* updates any key whose flattened initial value changed by reference; a
|
|
148
|
+
* separate `lastOrderLines: []` array would be reset to a fresh empty array
|
|
149
|
+
* after a Server Action, dropping the backend lines. A single nullable object
|
|
150
|
+
* keeps the receipt out of that re-apply.
|
|
151
|
+
*/
|
|
152
|
+
export interface StorefrontReceipt {
|
|
153
|
+
order: StorefrontOrder
|
|
154
|
+
lines: StorefrontOrderLineView[]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface NavCategory {
|
|
158
|
+
label: string
|
|
159
|
+
href: string
|
|
160
|
+
children?: { label: string; href: string }[]
|
|
161
|
+
emphasis?: boolean
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type OrderStatus = 'idle' | 'pending' | 'error' | 'unresolved' | 'complete'
|
|
165
|
+
|
|
166
|
+
export interface StorefrontCoreState {
|
|
167
|
+
heroProducts: FeaturedProduct[]
|
|
168
|
+
categories: ProductCategory2Category[]
|
|
169
|
+
navCategories: NavCategory[]
|
|
170
|
+
categoryProducts: CardProduct[]
|
|
171
|
+
/** Canonical product detail: real variants, options, availability, and Money. */
|
|
172
|
+
selectedProduct: ProductDetailProduct | null
|
|
173
|
+
/** Explicitly selected backend variant id; null until the shopper selects one. */
|
|
174
|
+
selectedVariantId: string | null
|
|
175
|
+
/** Requested add-to-cart quantity for the selected variant. */
|
|
176
|
+
productQuantity: number
|
|
177
|
+
/** Real reviews, supplied by the backend only. */
|
|
178
|
+
productReviews: ProductReview[]
|
|
179
|
+
hasProductReviews: boolean
|
|
180
|
+
lastClicked: CardProduct | null
|
|
181
|
+
lastCategory: string | null
|
|
182
|
+
bannerDismissed: boolean
|
|
183
|
+
/** True when the explicit data-source / error notice has been dismissed. */
|
|
184
|
+
dataNoticeDismissed: boolean
|
|
185
|
+
/** Explicit data source; `'mock'` is always labelled as demo data. */
|
|
186
|
+
dataSource: 'mock' | 'medusa'
|
|
187
|
+
/** Visible real-mode failure; null when the data loaded. */
|
|
188
|
+
dataError: string | null
|
|
189
|
+
cartOpen: boolean
|
|
190
|
+
// ---- cart (backend-authoritative) ----
|
|
191
|
+
cart: CartLineState[]
|
|
192
|
+
cartTotals: CartTotalsState | null
|
|
193
|
+
cartId: string | null
|
|
194
|
+
cartQuantity: number
|
|
195
|
+
cartCompleted: boolean
|
|
196
|
+
/** True when the verified session carries a completed receipt to recover. */
|
|
197
|
+
receiptAvailable: boolean
|
|
198
|
+
cartPending: boolean
|
|
199
|
+
cartError: string | null
|
|
200
|
+
/** Recovery/notice message (for example an unresolved completion). */
|
|
201
|
+
cartNotice: string | null
|
|
202
|
+
/** Shipping OPTION id (`so_`) the checkout UI selects/highlights. */
|
|
203
|
+
cartSelectedShippingOptionId: string | null
|
|
204
|
+
/** Visible backend cart-read failure; null when the cart read succeeded. */
|
|
205
|
+
cartReadError: string | null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Full storefront state: core plus the checkout feature's slice. */
|
|
209
|
+
export interface StorefrontState extends StorefrontCoreState {
|
|
210
|
+
checkout: CheckoutFormData
|
|
211
|
+
checkoutCountries: { value: string; label: string }[]
|
|
212
|
+
checkoutPaymentMethods: PaymentMethod[]
|
|
213
|
+
checkoutShippingOptions: CheckoutShippingOption[]
|
|
214
|
+
checkoutSelectedShippingOptionId: string | null
|
|
215
|
+
checkoutFieldErrors: CheckoutFieldErrors
|
|
216
|
+
checkoutPending: boolean
|
|
217
|
+
checkoutError: string | null
|
|
218
|
+
/**
|
|
219
|
+
* The completion discriminator behind `checkoutError`, preserved separately
|
|
220
|
+
* from `orderStatus`: a native cart-with-error (`cart-error`), a definite
|
|
221
|
+
* pre-completion failure (`error`), or an ambiguous completion (`unresolved`).
|
|
222
|
+
*/
|
|
223
|
+
checkoutErrorKind: 'error' | 'cart-error' | 'unresolved' | null
|
|
224
|
+
/** Atomic completed-order receipt (order + backend lines), stable-null initial. */
|
|
225
|
+
lastReceipt: StorefrontReceipt | null
|
|
226
|
+
orderStatus: OrderStatus
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const storefrontCoreState: StorefrontCoreState = {
|
|
230
|
+
heroProducts: [],
|
|
231
|
+
categories: [],
|
|
232
|
+
navCategories: [],
|
|
233
|
+
categoryProducts: [],
|
|
234
|
+
selectedProduct: null,
|
|
235
|
+
selectedVariantId: null,
|
|
236
|
+
productQuantity: 1,
|
|
237
|
+
productReviews: [],
|
|
238
|
+
hasProductReviews: false,
|
|
239
|
+
lastClicked: null,
|
|
240
|
+
lastCategory: null,
|
|
241
|
+
bannerDismissed: false,
|
|
242
|
+
dataNoticeDismissed: false,
|
|
243
|
+
dataSource: 'mock',
|
|
244
|
+
dataError: null,
|
|
245
|
+
cartOpen: false,
|
|
246
|
+
cart: [],
|
|
247
|
+
cartTotals: null,
|
|
248
|
+
cartId: null,
|
|
249
|
+
cartQuantity: 0,
|
|
250
|
+
cartCompleted: false,
|
|
251
|
+
receiptAvailable: false,
|
|
252
|
+
cartPending: false,
|
|
253
|
+
cartError: null,
|
|
254
|
+
cartNotice: null,
|
|
255
|
+
cartSelectedShippingOptionId: null,
|
|
256
|
+
cartReadError: null,
|
|
257
|
+
}
|
|
@@ -1,35 +1,11 @@
|
|
|
1
|
-
// Generated by Thor Commerce storefront (
|
|
2
|
-
//
|
|
1
|
+
// Generated by Thor Commerce storefront (composition composer: state).
|
|
2
|
+
// Do not edit by hand — the manifest is the source of truth; run `storefront sync`.
|
|
3
|
+
// Installed features contribute their state slices below.
|
|
3
4
|
|
|
4
|
-
import
|
|
5
|
+
import { storefrontCoreState } from './state.core'
|
|
5
6
|
|
|
6
|
-
export
|
|
7
|
-
id: string
|
|
8
|
-
brand: string
|
|
9
|
-
name: string
|
|
10
|
-
description: string
|
|
11
|
-
price: number
|
|
12
|
-
currency: string
|
|
13
|
-
rating: number
|
|
14
|
-
images: { id: number; src: string; alt: string }[]
|
|
15
|
-
sizes: number[]
|
|
16
|
-
colors: { name: string; value: string }[]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface StorefrontState {
|
|
20
|
-
heroProducts: CardProduct[]
|
|
21
|
-
categories: ProductCategory2Category[]
|
|
22
|
-
selectedProduct: StoreOverviewProduct | null
|
|
23
|
-
lastClicked: CardProduct | null
|
|
24
|
-
lastCategory: string | null
|
|
25
|
-
bannerDismissed: boolean
|
|
26
|
-
}
|
|
7
|
+
export * from './state.core'
|
|
27
8
|
|
|
28
|
-
export const storefrontInitialState
|
|
29
|
-
|
|
30
|
-
categories: [],
|
|
31
|
-
selectedProduct: null,
|
|
32
|
-
lastClicked: null,
|
|
33
|
-
lastCategory: null,
|
|
34
|
-
bannerDismissed: false,
|
|
9
|
+
export const storefrontInitialState = {
|
|
10
|
+
...storefrontCoreState,
|
|
35
11
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/store-context.ts)
|
|
2
|
+
//
|
|
3
|
+
// Host store context — the explicit single-store configuration the generated
|
|
4
|
+
// store runs on. It is resolved and validated from the host environment BEFORE
|
|
5
|
+
// any commerce call, so an incomplete or invalid configuration fails visibly
|
|
6
|
+
// instead of silently falling back to demo data.
|
|
7
|
+
//
|
|
8
|
+
// Boundary: this module is server-only and reads no Next.js or React API. It
|
|
9
|
+
// owns the environment contract; the commerce bridge owns the backend calls.
|
|
10
|
+
//
|
|
11
|
+
// The values are the multitenancy seam: a future tenancy layer varies
|
|
12
|
+
// storeId/channel/region/currency without redefining the commerce shapes. The
|
|
13
|
+
// currency is NEVER hardcoded — it always comes from the host environment.
|
|
14
|
+
|
|
15
|
+
import type { StoreContext } from '@thorprovider/types'
|
|
16
|
+
import { storeContextSchema } from '@thorprovider/runtime/contracts'
|
|
17
|
+
|
|
18
|
+
/** Data source selected explicitly by the host; never inferred from a failure. */
|
|
19
|
+
export type StoreDataSource = 'mock' | 'medusa'
|
|
20
|
+
|
|
21
|
+
/** Host environment variable names that make up the store context. */
|
|
22
|
+
export const STORE_CONTEXT_ENV = {
|
|
23
|
+
storeId: 'THOR_STORE_ID',
|
|
24
|
+
salesChannelId: 'MEDUSA_SALES_CHANNEL_ID',
|
|
25
|
+
regionId: 'MEDUSA_REGION_ID',
|
|
26
|
+
currencyCode: 'THOR_CURRENCY_CODE',
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
/** Real-mode backend configuration required before any provider call. */
|
|
30
|
+
export const BACKEND_ENV = {
|
|
31
|
+
baseUrl: 'MEDUSA_BASE_URL',
|
|
32
|
+
publishableKey: 'MEDUSA_PUBLISHABLE_KEY',
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
/** Host environment variable that selects mock vs real mode. */
|
|
36
|
+
export const DATA_SOURCE_ENV = 'THOR_DATA_SOURCE'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Host environment variable with the canonical public origin of the store
|
|
40
|
+
* (used for `metadataBase`, `robots.txt`, and `sitemap.xml`). Optional: when
|
|
41
|
+
* unset or invalid no absolute URL is invented.
|
|
42
|
+
*/
|
|
43
|
+
export const SITE_URL_ENV = 'THOR_SITE_URL'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Visible configuration failure. Thrown before commerce so a missing or invalid
|
|
47
|
+
* store context surfaces as an error instead of fictional data.
|
|
48
|
+
*/
|
|
49
|
+
export class StoreConfigError extends Error {
|
|
50
|
+
readonly code = 'STOREFRONT_CONFIG_ERROR'
|
|
51
|
+
constructor(message: string) {
|
|
52
|
+
super(message)
|
|
53
|
+
this.name = 'StoreConfigError'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The explicitly selected data source. Defaults to `mock` (the safe, offline,
|
|
59
|
+
* explicitly labelled mode) so a store never silently talks to a half-configured
|
|
60
|
+
* backend; real mode must be selected on purpose.
|
|
61
|
+
*/
|
|
62
|
+
export function getDataSource(): StoreDataSource {
|
|
63
|
+
return process.env[DATA_SOURCE_ENV] === 'medusa' ? 'medusa' : 'mock'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** True when the host explicitly selected real (Medusa) mode. */
|
|
67
|
+
export function isRealMode(): boolean {
|
|
68
|
+
return getDataSource() === 'medusa'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Normalise a configured site URL to its origin (`https://example.com`), or
|
|
73
|
+
* `undefined` when it is absent, blank, not a valid URL, or not http(s). Pure
|
|
74
|
+
* and total: it never throws, so an invalid value degrades to "no site URL"
|
|
75
|
+
* instead of failing the build.
|
|
76
|
+
*/
|
|
77
|
+
export function parseSiteUrl(value: string | undefined): string | undefined {
|
|
78
|
+
if (typeof value !== 'string' || value.trim() === '') return undefined
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(value)
|
|
81
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return undefined
|
|
82
|
+
return url.origin
|
|
83
|
+
} catch {
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The canonical public origin from the host environment, or `undefined`. */
|
|
89
|
+
export function getSiteUrl(): string | undefined {
|
|
90
|
+
return parseSiteUrl(process.env[SITE_URL_ENV])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function requireEnv(name: string): string {
|
|
94
|
+
const value = process.env[name]
|
|
95
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
96
|
+
throw new StoreConfigError(
|
|
97
|
+
`Missing required environment variable ${name}. Set it in the host environment before the store can resolve its context.`,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
return value
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve and validate the single-store context from the host environment.
|
|
105
|
+
*
|
|
106
|
+
* Validated through the shared runtime schema, so an invalid context is rejected
|
|
107
|
+
* before any commerce call. Throws `StoreConfigError` naming the missing
|
|
108
|
+
* variable — never a secret value.
|
|
109
|
+
*/
|
|
110
|
+
export function getStoreContext(): StoreContext {
|
|
111
|
+
const candidate = {
|
|
112
|
+
storeId: requireEnv(STORE_CONTEXT_ENV.storeId),
|
|
113
|
+
salesChannelId: requireEnv(STORE_CONTEXT_ENV.salesChannelId),
|
|
114
|
+
regionId: requireEnv(STORE_CONTEXT_ENV.regionId),
|
|
115
|
+
currencyCode: requireEnv(STORE_CONTEXT_ENV.currencyCode),
|
|
116
|
+
}
|
|
117
|
+
const parsed = storeContextSchema.safeParse(candidate)
|
|
118
|
+
if (!parsed.success) {
|
|
119
|
+
const issues = parsed.error.issues
|
|
120
|
+
.map((issue) => `${issue.path.join('.') || 'context'}: ${issue.message}`)
|
|
121
|
+
.join('; ')
|
|
122
|
+
throw new StoreConfigError(`Invalid store context: ${issues}`)
|
|
123
|
+
}
|
|
124
|
+
return parsed.data
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Assert that real-mode backend configuration is present before any provider
|
|
129
|
+
* call. Mock mode needs no backend. Throws `StoreConfigError` (never printing a
|
|
130
|
+
* key value) so a misconfigured real store fails visibly.
|
|
131
|
+
*/
|
|
132
|
+
export function assertBackendConfigured(source: StoreDataSource = getDataSource()): void {
|
|
133
|
+
if (source !== 'medusa') return
|
|
134
|
+
const missing: string[] = []
|
|
135
|
+
if (!process.env[BACKEND_ENV.baseUrl]?.trim()) missing.push(BACKEND_ENV.baseUrl)
|
|
136
|
+
if (!process.env[BACKEND_ENV.publishableKey]?.trim()) missing.push(BACKEND_ENV.publishableKey)
|
|
137
|
+
if (missing.length > 0) {
|
|
138
|
+
throw new StoreConfigError(
|
|
139
|
+
`THOR_DATA_SOURCE=medusa requires ${missing.join(', ')}. Configure the backend or select THOR_DATA_SOURCE=mock explicitly.`,
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: cta (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog lives in cta.tsx. @thorprovider/blocks ships a
|
|
5
|
+
// 'use client' boundary, but this fragment keeps the explicit client boundary so
|
|
6
|
+
// the section composes identically in any consumer graph.
|
|
7
|
+
|
|
8
|
+
import { CtaSection1 as CtaBlock } from '@thorprovider/blocks'
|
|
9
|
+
import { ctaSection1PropsSchema } from './schema.cta'
|
|
10
|
+
import type { ViewRegistry } from '../types'
|
|
11
|
+
|
|
12
|
+
export const ctaRegistry: ViewRegistry = {
|
|
13
|
+
Cta: ({ props }) => {
|
|
14
|
+
const parsed = ctaSection1PropsSchema.parse(props)
|
|
15
|
+
return (
|
|
16
|
+
<CtaBlock
|
|
17
|
+
badgeText={parsed.badgeText}
|
|
18
|
+
title={parsed.title}
|
|
19
|
+
description={parsed.description}
|
|
20
|
+
primaryCta={parsed.primaryCta}
|
|
21
|
+
secondaryCta={parsed.secondaryCta}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
},
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Optional section fragment: cta (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in cta.client.tsx ('use client') and are composed via composeSections.
|
|
4
|
+
|
|
5
|
+
import { ctaSection1PropsSchema } from './schema.cta'
|
|
6
|
+
|
|
7
|
+
export const ctaSection = {
|
|
8
|
+
elementType: 'Cta',
|
|
9
|
+
catalog: {
|
|
10
|
+
Cta: {
|
|
11
|
+
props: ctaSection1PropsSchema,
|
|
12
|
+
description: 'Call-to-action banner with optional primary and secondary links.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
registryModule: './cta.client',
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: faq (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog lives in faq.tsx. @thorprovider/blocks ships a
|
|
5
|
+
// 'use client' boundary, but this fragment keeps the explicit client boundary so
|
|
6
|
+
// the section composes identically in any consumer graph.
|
|
7
|
+
|
|
8
|
+
import { FaqBlock1 as FaqBlock } from '@thorprovider/blocks'
|
|
9
|
+
import { faqPropsSchema } from './schema.faq'
|
|
10
|
+
import type { ViewRegistry } from '../types'
|
|
11
|
+
|
|
12
|
+
export const faqRegistry: ViewRegistry = {
|
|
13
|
+
Faq: ({ props }) => {
|
|
14
|
+
const parsed = faqPropsSchema.parse(props)
|
|
15
|
+
return (
|
|
16
|
+
<FaqBlock
|
|
17
|
+
title={parsed.title ?? 'Preguntas frecuentes'}
|
|
18
|
+
description={parsed.description}
|
|
19
|
+
items={parsed.items}
|
|
20
|
+
accordionType={parsed.accordionType ?? 'single'}
|
|
21
|
+
/>
|
|
22
|
+
)
|
|
23
|
+
},
|
|
24
|
+
}
|
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
// Optional section fragment: faq (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in faq.client.tsx ('use client') and are composed via composeSections.
|
|
2
4
|
|
|
3
|
-
import {
|
|
4
|
-
import { z } from 'zod'
|
|
5
|
+
import { faqPropsSchema } from './schema.faq'
|
|
5
6
|
|
|
6
7
|
export const faqSection = {
|
|
7
8
|
elementType: 'Faq',
|
|
8
9
|
catalog: {
|
|
9
10
|
Faq: {
|
|
10
|
-
props:
|
|
11
|
-
title: z.string().optional(),
|
|
12
|
-
description: z.string().optional(),
|
|
13
|
-
items: z.array(z.record(z.string(), z.unknown())),
|
|
14
|
-
accordionType: z.enum(['single', 'multiple']).optional(),
|
|
15
|
-
}),
|
|
11
|
+
props: faqPropsSchema,
|
|
16
12
|
description: 'Frequently asked questions accordion.',
|
|
17
13
|
},
|
|
18
14
|
},
|
|
19
|
-
|
|
20
|
-
Faq: ({ props }: { props: Record<string, unknown> }) => (
|
|
21
|
-
<FaqBlock
|
|
22
|
-
title={(props.title as string) ?? 'Preguntas frecuentes'}
|
|
23
|
-
description={props.description as string}
|
|
24
|
-
items={props.items as never}
|
|
25
|
-
accordionType={(props.accordionType as 'single' | 'multiple') ?? 'single'}
|
|
26
|
-
/>
|
|
27
|
-
),
|
|
28
|
-
},
|
|
15
|
+
registryModule: './faq.client',
|
|
29
16
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Section catalog composition. Each section fragment is a server-side part
|
|
2
|
+
// (element type + zod catalog entry); this module merges their catalogs the same
|
|
3
|
+
// way `catalog.ts` merges view catalogs, so the generated storefront catalog can
|
|
4
|
+
// advertise the editorial element types. A fragment's client registry is
|
|
5
|
+
// addressed by its `registryModule` and never imported here, so the server
|
|
6
|
+
// catalog never pulls the React graph.
|
|
7
|
+
|
|
8
|
+
import type { ViewCatalogEntry } from '../types'
|
|
9
|
+
import { ctaSection } from './cta'
|
|
10
|
+
import { faqSection } from './faq'
|
|
11
|
+
|
|
12
|
+
export const sectionModules = [faqSection, ctaSection] as const
|
|
13
|
+
|
|
14
|
+
export const sectionCatalog: Record<string, ViewCatalogEntry> = Object.assign(
|
|
15
|
+
{},
|
|
16
|
+
...sectionModules.map((section) => section.catalog),
|
|
17
|
+
)
|
|
@@ -3,27 +3,39 @@
|
|
|
3
3
|
import { LogoCloud1 as LogoCloudBlock } from '@thorprovider/blocks'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
6
|
+
// Mirrors the LogoCloud1Platform/LogoCloud1Props block contracts. `id` is a
|
|
7
|
+
// simple-icons slug resolved internally by the block.
|
|
8
|
+
const logoCloudPlatformSchema = z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
id: z.string(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const logoCloudPropsSchema = z.object({
|
|
14
|
+
title: z.string().optional(),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
moreLabel: z.string().optional(),
|
|
17
|
+
platforms: z.array(logoCloudPlatformSchema),
|
|
18
|
+
})
|
|
19
|
+
|
|
6
20
|
export const logoCloudSection = {
|
|
7
21
|
elementType: 'LogoCloud',
|
|
8
22
|
catalog: {
|
|
9
23
|
LogoCloud: {
|
|
10
|
-
props:
|
|
11
|
-
title: z.string().optional(),
|
|
12
|
-
description: z.string().optional(),
|
|
13
|
-
moreLabel: z.string().optional(),
|
|
14
|
-
platforms: z.array(z.record(z.string(), z.unknown())),
|
|
15
|
-
}),
|
|
24
|
+
props: logoCloudPropsSchema,
|
|
16
25
|
description: 'Brand or partner logo strip.',
|
|
17
26
|
},
|
|
18
27
|
},
|
|
19
28
|
registry: {
|
|
20
|
-
LogoCloud: ({ props }: { props: Record<string, unknown> }) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
LogoCloud: ({ props }: { props: Record<string, unknown> }) => {
|
|
30
|
+
const parsed = logoCloudPropsSchema.parse(props)
|
|
31
|
+
return (
|
|
32
|
+
<LogoCloudBlock
|
|
33
|
+
title={parsed.title ?? 'Marcas que confían en nosotros'}
|
|
34
|
+
description={parsed.description}
|
|
35
|
+
moreLabel={parsed.moreLabel}
|
|
36
|
+
platforms={parsed.platforms}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
},
|
|
28
40
|
},
|
|
29
41
|
}
|
|
@@ -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
|
}
|