@thorprovider/storefront 0.3.1
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 +61 -0
- package/README.md +333 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +17 -0
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +10 -0
- package/assets/templates/app/app/globals.css +101 -0
- package/assets/templates/app/app/layout.tsx +35 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +88 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +42 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +47 -0
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +98 -0
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +17 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +48 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/types.ts +14 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +35 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/index.ts +7 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +29 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +29 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +33 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +29 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +28 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +46 -0
- package/assets/templates/views/home/index.tsx +142 -0
- package/assets/templates/views/home/registry.tsx +65 -0
- package/bin/storefront.js +531 -0
- package/commands/sf-add-view.md +26 -0
- package/commands/sf-theme.md +20 -0
- package/commands/sf-view.md +25 -0
- package/package.json +48 -0
- package/recipes/archetype.schema.json +39 -0
- package/recipes/archetypes.json +148 -0
- package/recipes/recipe.schema.json +59 -0
- package/recipes/recipes.json +90 -0
- package/recipes/sections.json +46 -0
- package/recipes/validate.mjs +190 -0
- package/scripts/render-templates.mjs +255 -0
- package/scripts/sync-assets.mjs +87 -0
- package/skills/building-storefronts/SKILL.md +178 -0
- package/skills/building-storefronts/references/frontend-integration.md +229 -0
- package/skills/json-render-core/SKILL.md +291 -0
- package/skills/json-render-next/SKILL.md +194 -0
- package/skills/json-render-react/SKILL.md +298 -0
- package/skills/json-render-remotion/SKILL.md +111 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/json-render-solid/SKILL.md +204 -0
- package/skills/nextjs-shadcn/SKILL.md +367 -0
- package/skills/nextjs-shadcn/references/architecture.md +499 -0
- package/skills/nextjs-shadcn/references/project-setup.md +162 -0
- package/skills/nextjs-shadcn/references/shadcn-platform.md +261 -0
- package/skills/nextjs-shadcn/references/sidebar.md +274 -0
- package/skills/nextjs-shadcn/references/styling.md +555 -0
- package/skills/sf-theme-gen/SKILL.md +61 -0
- package/skills/sf-view-gen/SKILL.md +116 -0
- package/skills/sf-view-gen/assets/recipes/archetypes.json +148 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +90 -0
- package/skills/sf-view-gen/assets/recipes/sections.json +46 -0
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +86 -0
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +48 -0
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +192 -0
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +45 -0
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +122 -0
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +49 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +20 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +21 -0
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +21 -0
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +20 -0
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +21 -0
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +168 -0
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +49 -0
- package/skills/sf-view-gen/references/views.md +69 -0
- package/skills/sf-view-gen/scripts/validate.mjs +190 -0
- package/skills/shadcn-component-discovery/SKILL.md +273 -0
- package/skills/shadcn-component-discovery/references/registries.md +226 -0
- package/skills/shadcn-theming/SKILL.md +104 -0
- package/skills/shadcn-theming/references/templates/theme-setup.md +109 -0
- package/skills/shadcn-theming/references/theming-guide.md +90 -0
- package/skills/storefront-best-practices/SKILL.md +421 -0
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +123 -0
- package/skills/storefront-best-practices/reference/components/cart-popup.md +189 -0
- package/skills/storefront-best-practices/reference/components/country-selector.md +298 -0
- package/skills/storefront-best-practices/reference/components/footer.md +112 -0
- package/skills/storefront-best-practices/reference/components/hero.md +241 -0
- package/skills/storefront-best-practices/reference/components/megamenu.md +239 -0
- package/skills/storefront-best-practices/reference/components/navbar.md +397 -0
- package/skills/storefront-best-practices/reference/components/popups.md +221 -0
- package/skills/storefront-best-practices/reference/components/product-card.md +125 -0
- package/skills/storefront-best-practices/reference/components/product-reviews.md +217 -0
- package/skills/storefront-best-practices/reference/components/product-slider.md +174 -0
- package/skills/storefront-best-practices/reference/components/search.md +101 -0
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +391 -0
- package/skills/storefront-best-practices/reference/design.md +388 -0
- package/skills/storefront-best-practices/reference/features/promotions.md +307 -0
- package/skills/storefront-best-practices/reference/features/wishlist.md +230 -0
- package/skills/storefront-best-practices/reference/layouts/account.md +380 -0
- package/skills/storefront-best-practices/reference/layouts/cart.md +316 -0
- package/skills/storefront-best-practices/reference/layouts/checkout.md +486 -0
- package/skills/storefront-best-practices/reference/layouts/home-page.md +264 -0
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +231 -0
- package/skills/storefront-best-practices/reference/layouts/product-details.md +527 -0
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +520 -0
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +356 -0
- package/skills/storefront-best-practices/reference/medusa.md +307 -0
- package/skills/storefront-best-practices/reference/mobile-responsiveness.md +183 -0
- package/skills/storefront-best-practices/reference/seo.md +195 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Thor Commerce view module: checkout
|
|
2
|
+
// Server Component module: elements + catalog entries + route + loader only.
|
|
3
|
+
// The client-side component mappings live in ./registry.tsx ('use client'); this
|
|
4
|
+
// module imports them as data so the RSC graph never evaluates @json-render/react
|
|
5
|
+
// on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "checkout".
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getPaymentMethodsByRegion,
|
|
9
|
+
getRegionCountries,
|
|
10
|
+
} from '@thorprovider/runtime/commerce'
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
import { pageRoot } from '../../spec/page-root'
|
|
13
|
+
import type { StorefrontElement } from '../../spec/types'
|
|
14
|
+
import type { ViewModule } from '../types'
|
|
15
|
+
import { registry } from './registry'
|
|
16
|
+
|
|
17
|
+
const elements: Record<string, StorefrontElement> = {
|
|
18
|
+
'checkout.view': {
|
|
19
|
+
type: 'Checkout',
|
|
20
|
+
visible: { $state: '/lastOrder', not: true },
|
|
21
|
+
props: {
|
|
22
|
+
title: 'Finalizar compra',
|
|
23
|
+
description: 'Completa tus datos para recibir tu pedido.',
|
|
24
|
+
orderSummary: {
|
|
25
|
+
items: { $state: '/cart' },
|
|
26
|
+
shipping: { $state: '/cartTotals/shipping' },
|
|
27
|
+
tax: { $state: '/cartTotals/tax' },
|
|
28
|
+
discount: { $state: '/cartTotals/discount' },
|
|
29
|
+
promoDiscount: { $state: '/cartTotals/promoDiscount' },
|
|
30
|
+
},
|
|
31
|
+
countries: { $state: '/checkoutCountries' },
|
|
32
|
+
paymentMethods: { $state: '/checkoutPaymentMethods' },
|
|
33
|
+
values: {
|
|
34
|
+
email: { $state: '/checkout/email' },
|
|
35
|
+
firstName: { $state: '/checkout/firstName' },
|
|
36
|
+
lastName: { $state: '/checkout/lastName' },
|
|
37
|
+
phone: { $state: '/checkout/phone' },
|
|
38
|
+
address: { $state: '/checkout/address' },
|
|
39
|
+
city: { $state: '/checkout/city' },
|
|
40
|
+
state: { $state: '/checkout/state' },
|
|
41
|
+
zipCode: { $state: '/checkout/zipCode' },
|
|
42
|
+
country: { $state: '/checkout/country' },
|
|
43
|
+
paymentMethod: { $state: '/checkout/paymentMethod' },
|
|
44
|
+
cardNumber: { $state: '/checkout/cardNumber' },
|
|
45
|
+
expiryDate: { $state: '/checkout/expiryDate' },
|
|
46
|
+
cvv: { $state: '/checkout/cvv' },
|
|
47
|
+
cardName: { $state: '/checkout/cardName' },
|
|
48
|
+
saveInfo: { $state: '/checkout/saveInfo' },
|
|
49
|
+
sameAsBilling: { $state: '/checkout/sameAsBilling' },
|
|
50
|
+
newsletter: { $state: '/checkout/newsletter' },
|
|
51
|
+
promoCode: { $state: '/checkout/promoCode' },
|
|
52
|
+
},
|
|
53
|
+
validation: {
|
|
54
|
+
email: {
|
|
55
|
+
validateOn: 'blur',
|
|
56
|
+
checks: [
|
|
57
|
+
{ type: 'required', message: 'Ingresa tu correo electrónico.' },
|
|
58
|
+
{ type: 'email', message: 'Ingresa un correo válido.' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
firstName: {
|
|
62
|
+
checks: [{ type: 'required', message: 'Ingresa tu nombre.' }],
|
|
63
|
+
},
|
|
64
|
+
lastName: {
|
|
65
|
+
checks: [{ type: 'required', message: 'Ingresa tus apellidos.' }],
|
|
66
|
+
},
|
|
67
|
+
phone: {
|
|
68
|
+
checks: [
|
|
69
|
+
{
|
|
70
|
+
type: 'pattern',
|
|
71
|
+
args: { pattern: '^[+]?[0-9]{7,15}$' },
|
|
72
|
+
message: 'Ingresa un teléfono válido.',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
address: {
|
|
77
|
+
checks: [{ type: 'required', message: 'Ingresa tu dirección.' }],
|
|
78
|
+
},
|
|
79
|
+
city: {
|
|
80
|
+
checks: [{ type: 'required', message: 'Ingresa la ciudad.' }],
|
|
81
|
+
},
|
|
82
|
+
state: {
|
|
83
|
+
checks: [{ type: 'required', message: 'Ingresa la provincia.' }],
|
|
84
|
+
},
|
|
85
|
+
country: {
|
|
86
|
+
checks: [{ type: 'required', message: 'Selecciona tu país.' }],
|
|
87
|
+
},
|
|
88
|
+
zipCode: {
|
|
89
|
+
checks: [
|
|
90
|
+
{ type: 'required', message: 'Ingresa el código postal.' },
|
|
91
|
+
{
|
|
92
|
+
type: 'pattern',
|
|
93
|
+
args: { pattern: '^[0-9]{4,5}$' },
|
|
94
|
+
message: 'Código postal inválido.',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
paymentMethod: {
|
|
99
|
+
checks: [{ type: 'required', message: 'Selecciona un método de pago.' }],
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
fieldDescriptions: {
|
|
103
|
+
email: 'Usaremos este correo para el seguimiento del pedido.',
|
|
104
|
+
phone: 'Te contactaremos para coordinar la entrega.',
|
|
105
|
+
promoCode: 'Aplica códigos promocionales válidos.',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
on: { placeOrder: { action: 'placeOrder' } },
|
|
109
|
+
},
|
|
110
|
+
'checkout.confirmation': {
|
|
111
|
+
type: 'CheckoutConfirmation',
|
|
112
|
+
visible: { $state: '/lastOrder' },
|
|
113
|
+
props: {
|
|
114
|
+
order: { $state: '/lastOrder' },
|
|
115
|
+
orderLines: { $state: '/lastOrderLines' },
|
|
116
|
+
fulfilmentDetails: {
|
|
117
|
+
Confirmed: {
|
|
118
|
+
stage: 'Confirmed',
|
|
119
|
+
summary: 'Pago autorizado y el pedido fue enviado a preparación.',
|
|
120
|
+
location: 'Centro de distribución',
|
|
121
|
+
at: { $state: '/orderPlaced' },
|
|
122
|
+
},
|
|
123
|
+
Packed: {
|
|
124
|
+
stage: 'Packed',
|
|
125
|
+
summary: 'Los artículos fueron seleccionados y empacados.',
|
|
126
|
+
location: 'Centro de distribución',
|
|
127
|
+
at: null,
|
|
128
|
+
},
|
|
129
|
+
Shipped: {
|
|
130
|
+
stage: 'Shipped',
|
|
131
|
+
summary: 'El paquete fue entregado al transportista.',
|
|
132
|
+
location: 'En tránsito',
|
|
133
|
+
at: null,
|
|
134
|
+
},
|
|
135
|
+
Delivered: {
|
|
136
|
+
stage: 'Delivered',
|
|
137
|
+
summary: 'Entregado en tu dirección.',
|
|
138
|
+
location: 'Tu dirección',
|
|
139
|
+
at: null,
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
on: { continueShopping: { action: 'continueShopping' } },
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const catalog = {
|
|
148
|
+
Checkout: {
|
|
149
|
+
props: z.record(z.string(), z.unknown()),
|
|
150
|
+
description: 'Checkout form with order summary, shipping and payment steps.',
|
|
151
|
+
},
|
|
152
|
+
CheckoutConfirmation: {
|
|
153
|
+
props: z.record(z.string(), z.unknown()),
|
|
154
|
+
description: 'Order confirmation with fulfilment progress.',
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const checkout: ViewModule = {
|
|
159
|
+
name: 'checkout',
|
|
160
|
+
elements,
|
|
161
|
+
catalog,
|
|
162
|
+
registry,
|
|
163
|
+
route: {
|
|
164
|
+
path: '/checkout',
|
|
165
|
+
loaderName: 'checkout',
|
|
166
|
+
layout: 'default',
|
|
167
|
+
metadata: {
|
|
168
|
+
title: 'Finalizar compra',
|
|
169
|
+
description: 'Completa tus datos de envío y pago para recibir tu pedido.',
|
|
170
|
+
},
|
|
171
|
+
entry: pageRoot(
|
|
172
|
+
'checkout',
|
|
173
|
+
['checkout.view', 'checkout.confirmation'],
|
|
174
|
+
elements,
|
|
175
|
+
),
|
|
176
|
+
},
|
|
177
|
+
loader: {
|
|
178
|
+
checkout: async () => {
|
|
179
|
+
const [countries, paymentMethods] = await Promise.all([
|
|
180
|
+
getRegionCountries(),
|
|
181
|
+
getPaymentMethodsByRegion(),
|
|
182
|
+
])
|
|
183
|
+
return {
|
|
184
|
+
checkoutCountries: countries.map((country) => ({
|
|
185
|
+
value: country.code,
|
|
186
|
+
label: country.name,
|
|
187
|
+
})),
|
|
188
|
+
checkoutPaymentMethods: paymentMethods,
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Thor Commerce view registry fragment: checkout
|
|
4
|
+
// Client boundary for the checkout view. Imports value bindings from
|
|
5
|
+
// @json-render/react and components from @thorprovider/blocks — both must stay
|
|
6
|
+
// on the client side of the RSC graph. Kept separate from index.tsx so the view
|
|
7
|
+
// spec (elements/catalog/route/loader) stays a pure Server Component module.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CheckoutForm1 as CheckoutBlock,
|
|
11
|
+
CheckoutConfirmation as CheckoutConfirmationBlock,
|
|
12
|
+
} from '@thorprovider/blocks'
|
|
13
|
+
import { useStateStore } from '@json-render/react'
|
|
14
|
+
import type { ViewRegistry } from '../types'
|
|
15
|
+
|
|
16
|
+
export const registry: ViewRegistry = {
|
|
17
|
+
Checkout: ({ props, emit }) => {
|
|
18
|
+
const { state, set } = useStateStore()
|
|
19
|
+
return (
|
|
20
|
+
<CheckoutBlock
|
|
21
|
+
title={props.title as string}
|
|
22
|
+
description={props.description as string}
|
|
23
|
+
orderSummary={props.orderSummary as never}
|
|
24
|
+
countries={props.countries as never}
|
|
25
|
+
paymentMethods={props.paymentMethods as never}
|
|
26
|
+
values={props.values as never}
|
|
27
|
+
fieldDescriptions={props.fieldDescriptions as never}
|
|
28
|
+
onChange={(field, value) => set(`/checkout/${field}`, value)}
|
|
29
|
+
onSubmit={() => emit('placeOrder')}
|
|
30
|
+
submitting={Boolean(state.checkoutPending)}
|
|
31
|
+
submitError={(state.checkoutError as string | null) ?? undefined}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
CheckoutConfirmation: ({ props, emit }) =>
|
|
36
|
+
props.order ? (
|
|
37
|
+
<CheckoutConfirmationBlock
|
|
38
|
+
order={props.order as never}
|
|
39
|
+
orderLines={props.orderLines as never}
|
|
40
|
+
fulfilmentDetails={props.fulfilmentDetails as never}
|
|
41
|
+
continueShoppingHref="#"
|
|
42
|
+
onContinueShopping={() => emit('continueShopping')}
|
|
43
|
+
/>
|
|
44
|
+
) : null,
|
|
45
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Thor Commerce view module: product
|
|
2
|
+
// Server Component module: elements + catalog entries + route + loader only.
|
|
3
|
+
// The client-side component mappings live in ./registry.tsx ('use client'); this
|
|
4
|
+
// module imports them as data so the RSC graph never evaluates @json-render/react
|
|
5
|
+
// on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "product".
|
|
6
|
+
|
|
7
|
+
import { getProductByHandle } from '@thorprovider/runtime/commerce'
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
import { pageRoot } from '../../spec/page-root'
|
|
10
|
+
import type { StorefrontElement } from '../../spec/types'
|
|
11
|
+
import type { ViewModule } from '../types'
|
|
12
|
+
import { registry } from './registry'
|
|
13
|
+
|
|
14
|
+
const elements: Record<string, StorefrontElement> = {
|
|
15
|
+
'product.overview': {
|
|
16
|
+
type: 'ProductOverview',
|
|
17
|
+
props: {
|
|
18
|
+
product: { $state: '/selectedProduct' },
|
|
19
|
+
},
|
|
20
|
+
on: { addToCart: { action: 'addToCart' } },
|
|
21
|
+
},
|
|
22
|
+
'product.reviews': {
|
|
23
|
+
type: 'Reviews',
|
|
24
|
+
props: {
|
|
25
|
+
pageTitle: 'Opiniones de clientes',
|
|
26
|
+
pageDescription: 'Lo que dicen quienes ya compraron este producto.',
|
|
27
|
+
reviews: [
|
|
28
|
+
{
|
|
29
|
+
id: 1,
|
|
30
|
+
userName: 'Carlos M.',
|
|
31
|
+
date: '2026-07-12',
|
|
32
|
+
rating: 5,
|
|
33
|
+
content: 'Excelente calidad y llegó antes de lo esperado. Muy buena compra.',
|
|
34
|
+
likes: 12,
|
|
35
|
+
comments: 2,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 2,
|
|
39
|
+
userName: 'Ana R.',
|
|
40
|
+
date: '2026-07-03',
|
|
41
|
+
rating: 4,
|
|
42
|
+
content: 'Funciona muy bien y la atención fue rápida. Lo recomiendo.',
|
|
43
|
+
likes: 7,
|
|
44
|
+
comments: 1,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 3,
|
|
48
|
+
userName: 'Luis P.',
|
|
49
|
+
date: '2026-06-28',
|
|
50
|
+
rating: 5,
|
|
51
|
+
content: 'Producto robusto, tal como aparece en la descripción.',
|
|
52
|
+
likes: 5,
|
|
53
|
+
comments: 0,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
form: {
|
|
57
|
+
title: 'Deja tu opinión',
|
|
58
|
+
description: 'Tu experiencia ayuda a otros clientes.',
|
|
59
|
+
nameLabel: 'Nombre',
|
|
60
|
+
emailLabel: 'Correo electrónico',
|
|
61
|
+
reviewLabel: 'Tu reseña',
|
|
62
|
+
namePlaceholder: 'Tu nombre',
|
|
63
|
+
emailPlaceholder: 'tucorreo@ejemplo.cu',
|
|
64
|
+
reviewPlaceholder: 'Cuéntanos tu experiencia...',
|
|
65
|
+
submitButtonText: 'Publicar reseña',
|
|
66
|
+
submittingButtonText: 'Publicando...',
|
|
67
|
+
},
|
|
68
|
+
reviewsPerPage: 3,
|
|
69
|
+
},
|
|
70
|
+
on: {
|
|
71
|
+
submitReview: { action: 'submitReview' },
|
|
72
|
+
toggleReviewLike: { action: 'toggleReviewLike' },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const catalog = {
|
|
78
|
+
ProductOverview: {
|
|
79
|
+
props: z.object({
|
|
80
|
+
product: z.record(z.string(), z.unknown()),
|
|
81
|
+
sizesLabel: z.string().optional(),
|
|
82
|
+
colorLabel: z.string().optional(),
|
|
83
|
+
reviewsLabel: z.string().optional(),
|
|
84
|
+
addToCartLabel: z.string().optional(),
|
|
85
|
+
}),
|
|
86
|
+
description: 'Detalle de producto: galería, precio, variantes y añadir al carrito.',
|
|
87
|
+
},
|
|
88
|
+
Reviews: {
|
|
89
|
+
props: z.object({
|
|
90
|
+
pageTitle: z.string().optional(),
|
|
91
|
+
pageDescription: z.string().optional(),
|
|
92
|
+
reviews: z.array(z.record(z.string(), z.unknown())),
|
|
93
|
+
form: z.record(z.string(), z.unknown()).optional(),
|
|
94
|
+
reviewsPerPage: z.number().optional(),
|
|
95
|
+
}),
|
|
96
|
+
description: 'Opiniones de clientes con resumen de valoración.',
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const product: ViewModule = {
|
|
101
|
+
name: 'product',
|
|
102
|
+
elements,
|
|
103
|
+
catalog,
|
|
104
|
+
registry,
|
|
105
|
+
route: {
|
|
106
|
+
path: '/productos/[id]',
|
|
107
|
+
loaderName: 'product',
|
|
108
|
+
layout: 'default',
|
|
109
|
+
metadata: {
|
|
110
|
+
title: 'Producto',
|
|
111
|
+
description: 'Detalles del producto seleccionado.',
|
|
112
|
+
},
|
|
113
|
+
entry: pageRoot('product', ['product.overview', 'product.reviews'], elements),
|
|
114
|
+
},
|
|
115
|
+
loader: {
|
|
116
|
+
product: async (params) => {
|
|
117
|
+
const id = Array.isArray(params.id) ? params.id[0] : params.id
|
|
118
|
+
const selectedProduct = await getProductByHandle(String(id))
|
|
119
|
+
return { selectedProduct }
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Thor Commerce view registry fragment: product
|
|
4
|
+
// Client boundary for the product view. Imports value bindings from
|
|
5
|
+
// @json-render/react and components from @thorprovider/blocks — both must stay
|
|
6
|
+
// on the client side of the RSC graph. Kept separate from index.tsx so the view
|
|
7
|
+
// spec (elements/catalog/route/loader) stays a pure Server Component module.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
ProductOverview1 as ProductOverviewBlock,
|
|
11
|
+
ReviewRating1 as ReviewsBlock,
|
|
12
|
+
} from '@thorprovider/blocks'
|
|
13
|
+
import { useStateStore } from '@json-render/react'
|
|
14
|
+
import type { ViewRegistry } from '../types'
|
|
15
|
+
|
|
16
|
+
export const registry: ViewRegistry = {
|
|
17
|
+
ProductOverview: ({ props, emit }) => {
|
|
18
|
+
const { set } = useStateStore()
|
|
19
|
+
return (
|
|
20
|
+
<ProductOverviewBlock
|
|
21
|
+
product={props.product as never}
|
|
22
|
+
onAddToCart={(product) => {
|
|
23
|
+
set('/lastClicked', product)
|
|
24
|
+
emit('addToCart')
|
|
25
|
+
}}
|
|
26
|
+
onToggleWishlist={() => {}}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
},
|
|
30
|
+
Reviews: ({ props, emit }) => {
|
|
31
|
+
const { set } = useStateStore()
|
|
32
|
+
return (
|
|
33
|
+
<ReviewsBlock
|
|
34
|
+
pageTitle={props.pageTitle as string}
|
|
35
|
+
pageDescription={props.pageDescription as string}
|
|
36
|
+
reviews={props.reviews as never}
|
|
37
|
+
form={props.form as never}
|
|
38
|
+
reviewsPerPage={props.reviewsPerPage as number}
|
|
39
|
+
onToggleReviewLike={() => {
|
|
40
|
+
emit('toggleReviewLike')
|
|
41
|
+
}}
|
|
42
|
+
onSubmitReview={(data) => {
|
|
43
|
+
set('/lastReview', data)
|
|
44
|
+
emit('submitReview')
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
},
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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 type { ViewRegistry } from '../types'
|
|
10
|
+
|
|
11
|
+
export const registry: ViewRegistry = {
|
|
12
|
+
Faq: ({ props }) => (
|
|
13
|
+
<FaqBlock
|
|
14
|
+
title={(props.title as string) ?? 'Preguntas frecuentes'}
|
|
15
|
+
description={props.description as string}
|
|
16
|
+
items={props.items as never}
|
|
17
|
+
accordionType={(props.accordionType as 'single' | 'multiple') ?? 'single'}
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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.
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const faqSection = {
|
|
8
|
+
elementType: 'Faq',
|
|
9
|
+
catalog: {
|
|
10
|
+
Faq: {
|
|
11
|
+
props: z.object({
|
|
12
|
+
title: z.string().optional(),
|
|
13
|
+
description: z.string().optional(),
|
|
14
|
+
items: z.array(z.record(z.string(), z.unknown())),
|
|
15
|
+
accordionType: z.enum(['single', 'multiple']).optional(),
|
|
16
|
+
}),
|
|
17
|
+
description: 'Frequently asked questions accordion.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
registryModule: './faq.client',
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: logoCloud (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog entry lives in logoCloud.tsx.
|
|
5
|
+
|
|
6
|
+
import { LogoCloud1 as LogoCloudBlock } from '@thorprovider/blocks'
|
|
7
|
+
import type { ViewRegistry } from '../types'
|
|
8
|
+
|
|
9
|
+
export const registry: ViewRegistry = {
|
|
10
|
+
LogoCloud: ({ props }) => (
|
|
11
|
+
<LogoCloudBlock
|
|
12
|
+
title={(props.title as string) ?? 'Marcas que confían en nosotros'}
|
|
13
|
+
description={props.description as string}
|
|
14
|
+
moreLabel={props.moreLabel as string}
|
|
15
|
+
platforms={props.platforms as never}
|
|
16
|
+
/>
|
|
17
|
+
),
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Optional section fragment: logoCloud (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in logoCloud.client.tsx ('use client').
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const logoCloudSection = {
|
|
8
|
+
elementType: 'LogoCloud',
|
|
9
|
+
catalog: {
|
|
10
|
+
LogoCloud: {
|
|
11
|
+
props: z.object({
|
|
12
|
+
title: z.string().optional(),
|
|
13
|
+
description: z.string().optional(),
|
|
14
|
+
moreLabel: z.string().optional(),
|
|
15
|
+
platforms: z.array(z.record(z.string(), z.unknown())),
|
|
16
|
+
}),
|
|
17
|
+
description: 'Brand or partner logo strip.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
registryModule: './logoCloud.client',
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: pricing (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog entry lives in pricing.tsx.
|
|
5
|
+
|
|
6
|
+
import { PricingSection1 as PricingBlock } from '@thorprovider/blocks'
|
|
7
|
+
import type { ViewRegistry } from '../types'
|
|
8
|
+
|
|
9
|
+
export const registry: ViewRegistry = {
|
|
10
|
+
Pricing: ({ props }) => (
|
|
11
|
+
<PricingBlock
|
|
12
|
+
sectionTitle={(props.sectionTitle as string) ?? 'Planes y precios'}
|
|
13
|
+
sectionLabel={props.sectionLabel as string}
|
|
14
|
+
currencySymbol={props.currencySymbol as string}
|
|
15
|
+
popularBadgeLabel={(props.popularBadgeLabel as string) ?? 'Popular'}
|
|
16
|
+
getStartedLabel={(props.getStartedLabel as string) ?? 'Empezar'}
|
|
17
|
+
tiers={props.tiers as never}
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Optional section fragment: pricing (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in pricing.client.tsx ('use client').
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const pricingSection = {
|
|
8
|
+
elementType: 'Pricing',
|
|
9
|
+
catalog: {
|
|
10
|
+
Pricing: {
|
|
11
|
+
props: z.object({
|
|
12
|
+
sectionTitle: z.string().optional(),
|
|
13
|
+
sectionLabel: z.string().optional(),
|
|
14
|
+
currencySymbol: z.string().optional(),
|
|
15
|
+
popularBadgeLabel: z.string().optional(),
|
|
16
|
+
getStartedLabel: z.string().optional(),
|
|
17
|
+
tiers: z.array(z.record(z.string(), z.unknown())),
|
|
18
|
+
}),
|
|
19
|
+
description: 'Pricing tiers or plans.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
registryModule: './pricing.client',
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: statistics (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog entry lives in statistics.tsx.
|
|
5
|
+
|
|
6
|
+
import { Statistic1 as StatisticBlock } from '@thorprovider/blocks'
|
|
7
|
+
import type { ViewRegistry } from '../types'
|
|
8
|
+
|
|
9
|
+
export const registry: ViewRegistry = {
|
|
10
|
+
Statistics: ({ props }) => (
|
|
11
|
+
<StatisticBlock
|
|
12
|
+
badge={props.badge as string}
|
|
13
|
+
title={(props.title as string) ?? 'Números que nos respaldan'}
|
|
14
|
+
description={props.description as string}
|
|
15
|
+
items={props.items as never}
|
|
16
|
+
/>
|
|
17
|
+
),
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Optional section fragment: statistics (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in statistics.client.tsx ('use client').
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const statisticsSection = {
|
|
8
|
+
elementType: 'Statistics',
|
|
9
|
+
catalog: {
|
|
10
|
+
Statistics: {
|
|
11
|
+
props: z.object({
|
|
12
|
+
title: z.string().optional(),
|
|
13
|
+
badge: z.string().optional(),
|
|
14
|
+
description: z.string().optional(),
|
|
15
|
+
items: z.array(z.record(z.string(), z.unknown())),
|
|
16
|
+
}),
|
|
17
|
+
description: 'Key metrics or trust statistics.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
registryModule: './statistics.client',
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: testimonials (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog entry lives in testimonials.tsx.
|
|
5
|
+
|
|
6
|
+
import { Testimonial1 as TestimonialBlock } from '@thorprovider/blocks'
|
|
7
|
+
import type { ViewRegistry } from '../types'
|
|
8
|
+
|
|
9
|
+
export const registry: ViewRegistry = {
|
|
10
|
+
Testimonials: ({ props }) => (
|
|
11
|
+
<TestimonialBlock
|
|
12
|
+
title={(props.title as string) ?? 'Lo que dicen nuestros clientes'}
|
|
13
|
+
items={props.items as never}
|
|
14
|
+
prevLabel={(props.prevLabel as string) ?? 'Anterior'}
|
|
15
|
+
nextLabel={(props.nextLabel as string) ?? 'Siguiente'}
|
|
16
|
+
slideAriaLabel={(props.slideAriaLabel as string) ?? 'Testimonio'}
|
|
17
|
+
/>
|
|
18
|
+
),
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Optional section fragment: testimonials (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in testimonials.client.tsx ('use client').
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const testimonialsSection = {
|
|
8
|
+
elementType: 'Testimonials',
|
|
9
|
+
catalog: {
|
|
10
|
+
Testimonials: {
|
|
11
|
+
props: z.object({
|
|
12
|
+
title: z.string().optional(),
|
|
13
|
+
items: z.array(z.record(z.string(), z.unknown())),
|
|
14
|
+
}),
|
|
15
|
+
description: 'Customer testimonials or social proof.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
registryModule: './testimonials.client',
|
|
19
|
+
}
|