@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,378 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Thor Commerce view registry fragment: account
|
|
4
|
+
// Client boundary for the account 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
|
+
// Security invariant: the block-owned credential payload (email/password) is
|
|
10
|
+
// passed straight from the block callback to the Server Action and is NEVER
|
|
11
|
+
// written to the json-render state store, so no password reaches client state,
|
|
12
|
+
// HTML, or logs. Only the non-secret result (customer/error) is stored.
|
|
13
|
+
|
|
14
|
+
import { useEffect, useRef } from 'react'
|
|
15
|
+
import {
|
|
16
|
+
Button,
|
|
17
|
+
LoginPage1 as LoginBlock,
|
|
18
|
+
OrderHistory2,
|
|
19
|
+
SignUpPage1 as SignUpBlock,
|
|
20
|
+
} from '@thorprovider/blocks'
|
|
21
|
+
import { useStateStore } from '@json-render/react'
|
|
22
|
+
import type { Customer } from '@thorprovider/types'
|
|
23
|
+
import {
|
|
24
|
+
loadAccountAction,
|
|
25
|
+
loadOrdersAction,
|
|
26
|
+
loginAction,
|
|
27
|
+
logoutAction,
|
|
28
|
+
registerAction,
|
|
29
|
+
} from '../../actions'
|
|
30
|
+
import type { AccountOrderView } from '../../state.core'
|
|
31
|
+
import type { ViewRegistry } from '../types'
|
|
32
|
+
import {
|
|
33
|
+
accountLoginPropsSchema,
|
|
34
|
+
accountOrdersPropsSchema,
|
|
35
|
+
accountRegisterPropsSchema,
|
|
36
|
+
} from './schema'
|
|
37
|
+
|
|
38
|
+
// The structured result the auth Server Actions return (see ../../actions).
|
|
39
|
+
type AuthResult = { ok: true; customer: Customer | null } | { ok: false; error: string }
|
|
40
|
+
type SetState = (path: string, value: unknown) => void
|
|
41
|
+
|
|
42
|
+
// Apply a settled auth result truthfully: a failure sets an error and NEVER a
|
|
43
|
+
// logged-in state; a success stores the customer (or null) and nothing else.
|
|
44
|
+
function applyAuthResult(set: SetState, result: AuthResult): void {
|
|
45
|
+
set('/accountPending', false)
|
|
46
|
+
if (result.ok) {
|
|
47
|
+
set('/accountCustomer', result.customer)
|
|
48
|
+
set('/accountAuthenticated', result.customer !== null)
|
|
49
|
+
set('/accountError', null)
|
|
50
|
+
set('/accountMode', 'login')
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
set('/accountError', result.error)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A rejected Server Action (network/RPC) must clear pending and surface a
|
|
57
|
+
// message; never swallow it. Only a non-secret context line is logged.
|
|
58
|
+
function authFailure(set: SetState, scope: string) {
|
|
59
|
+
return (error: unknown) => {
|
|
60
|
+
console.error(
|
|
61
|
+
`[storefront/account] ${scope} failed`,
|
|
62
|
+
error instanceof Error ? error.message : error,
|
|
63
|
+
)
|
|
64
|
+
set('/accountPending', false)
|
|
65
|
+
set('/accountError', 'No se pudo completar la operación. Inténtalo de nuevo.')
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Non-secret status surface for the block pages, which expose no error/pending
|
|
70
|
+
// prop of their own. Renders nothing when there is no pending work or error.
|
|
71
|
+
function AuthNotice({ pending, error }: { pending: boolean; error: string | null }) {
|
|
72
|
+
if (!pending && !error) return null
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
role="alert"
|
|
76
|
+
aria-live="polite"
|
|
77
|
+
className="bg-background text-foreground fixed inset-x-0 top-4 z-50 mx-auto w-fit max-w-md rounded-md border px-4 py-2 text-center text-sm shadow-lg"
|
|
78
|
+
>
|
|
79
|
+
{pending ? 'Procesando…' : error}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The OrderHistory2 block's order shape (see @thorprovider/blocks). Kept local
|
|
85
|
+
// so the registry maps the account order rows to the block contract without a
|
|
86
|
+
// shared dependency on block internals.
|
|
87
|
+
interface BlockOrderItem {
|
|
88
|
+
id: string
|
|
89
|
+
name: string
|
|
90
|
+
image: string
|
|
91
|
+
details: string
|
|
92
|
+
price: string
|
|
93
|
+
quantity: number
|
|
94
|
+
}
|
|
95
|
+
interface BlockOrder {
|
|
96
|
+
id: string
|
|
97
|
+
date: string
|
|
98
|
+
status: string
|
|
99
|
+
total: string
|
|
100
|
+
items: BlockOrderItem[]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Map a backend-sourced account order row to the block's order shape. The order
|
|
104
|
+
// number is the list key; an absent variant/image becomes '' because the block
|
|
105
|
+
// contract is a string — never an invented value.
|
|
106
|
+
function toBlockOrder(order: AccountOrderView): BlockOrder {
|
|
107
|
+
return {
|
|
108
|
+
id: order.number,
|
|
109
|
+
date: order.date,
|
|
110
|
+
status: order.status,
|
|
111
|
+
total: order.total,
|
|
112
|
+
items: order.items.map((item) => ({
|
|
113
|
+
id: item.id,
|
|
114
|
+
name: item.name,
|
|
115
|
+
image: item.image ?? '',
|
|
116
|
+
details: item.variant ?? '',
|
|
117
|
+
price: item.lineTotal,
|
|
118
|
+
quantity: item.quantity,
|
|
119
|
+
})),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Inline order detail assembled from the already-fetched list row (no second
|
|
124
|
+
// backend request). Only fields the backend supplied are shown.
|
|
125
|
+
function OrderDetail({ order }: { order: AccountOrderView }) {
|
|
126
|
+
const totals = (
|
|
127
|
+
[
|
|
128
|
+
{ label: 'Subtotal', value: order.subtotal },
|
|
129
|
+
{ label: 'Impuestos', value: order.tax },
|
|
130
|
+
{ label: 'Envío', value: order.shipping },
|
|
131
|
+
{ label: 'Total', value: order.total },
|
|
132
|
+
] as { label: string; value?: string }[]
|
|
133
|
+
).filter((row): row is { label: string; value: string } => row.value !== undefined)
|
|
134
|
+
return (
|
|
135
|
+
<div className="mx-auto w-full max-w-7xl px-4 pb-12 sm:px-6 lg:px-8">
|
|
136
|
+
<div className="rounded-lg border p-6">
|
|
137
|
+
<h3 className="text-lg font-semibold">Pedido {order.number}</h3>
|
|
138
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
139
|
+
{order.date} · {order.status}
|
|
140
|
+
</p>
|
|
141
|
+
{order.address ? (
|
|
142
|
+
<p className="text-muted-foreground mt-2 text-sm">{order.address}</p>
|
|
143
|
+
) : null}
|
|
144
|
+
<ul className="mt-4 divide-y">
|
|
145
|
+
{order.items.map((item) => (
|
|
146
|
+
<li key={item.id} className="flex items-center justify-between py-2 text-sm">
|
|
147
|
+
<span>
|
|
148
|
+
{item.name}
|
|
149
|
+
{item.variant ? (
|
|
150
|
+
<span className="text-muted-foreground"> · {item.variant}</span>
|
|
151
|
+
) : null}
|
|
152
|
+
<span className="text-muted-foreground"> × {item.quantity}</span>
|
|
153
|
+
</span>
|
|
154
|
+
<span>{item.lineTotal}</span>
|
|
155
|
+
</li>
|
|
156
|
+
))}
|
|
157
|
+
</ul>
|
|
158
|
+
<dl className="mt-4 space-y-1 border-t pt-4 text-sm">
|
|
159
|
+
{totals.map((row) => (
|
|
160
|
+
<div key={row.label} className="flex justify-between">
|
|
161
|
+
<dt className="text-muted-foreground">{row.label}</dt>
|
|
162
|
+
<dd>{row.value}</dd>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</dl>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const registry: ViewRegistry = {
|
|
172
|
+
/**
|
|
173
|
+
* Headless bootstrap rendered on the account route. It resolves the current
|
|
174
|
+
* customer from the cookie-authorized Server Action once, so a reload keeps
|
|
175
|
+
* the signed-in state without re-entering credentials. It never reads a token.
|
|
176
|
+
*/
|
|
177
|
+
AccountBoot: () => {
|
|
178
|
+
const { set } = useStateStore()
|
|
179
|
+
const booted = useRef(false)
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (booted.current) return
|
|
182
|
+
booted.current = true
|
|
183
|
+
void loadAccountAction()
|
|
184
|
+
.then((result) => {
|
|
185
|
+
if (!result.ok) {
|
|
186
|
+
set('/accountCustomer', null)
|
|
187
|
+
set('/accountAuthenticated', false)
|
|
188
|
+
set('/accountError', result.error)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
set('/accountCustomer', result.customer)
|
|
192
|
+
set('/accountAuthenticated', result.customer !== null)
|
|
193
|
+
set('/accountError', null)
|
|
194
|
+
})
|
|
195
|
+
.catch((error) => {
|
|
196
|
+
console.error(
|
|
197
|
+
'[storefront/account] loadAccount failed',
|
|
198
|
+
error instanceof Error ? error.message : error,
|
|
199
|
+
)
|
|
200
|
+
set('/accountCustomer', null)
|
|
201
|
+
set('/accountAuthenticated', false)
|
|
202
|
+
set('/accountError', 'No se pudo cargar la cuenta. Inténtalo de nuevo.')
|
|
203
|
+
})
|
|
204
|
+
}, [])
|
|
205
|
+
return null
|
|
206
|
+
},
|
|
207
|
+
AccountLogin: ({ props, emit }) => {
|
|
208
|
+
const { state, set } = useStateStore()
|
|
209
|
+
const parsed = accountLoginPropsSchema.parse(props)
|
|
210
|
+
const error = typeof state.accountError === 'string' ? state.accountError : null
|
|
211
|
+
const pending = Boolean(state.accountPending)
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
<AuthNotice pending={pending} error={error} />
|
|
215
|
+
<LoginBlock
|
|
216
|
+
logoText={parsed.logoText}
|
|
217
|
+
welcomeTitleLabel={parsed.welcomeTitleLabel}
|
|
218
|
+
welcomeDescriptionLabel={parsed.welcomeDescriptionLabel}
|
|
219
|
+
emailLabel={parsed.emailLabel}
|
|
220
|
+
passwordLabel={parsed.passwordLabel}
|
|
221
|
+
signInLabel={parsed.signInLabel}
|
|
222
|
+
createAccountLabel={parsed.createAccountLabel}
|
|
223
|
+
// The credentials stay in the block's local state and go straight to
|
|
224
|
+
// the Server Action; they are never written to the store.
|
|
225
|
+
onSubmit={(data) => {
|
|
226
|
+
set('/accountPending', true)
|
|
227
|
+
set('/accountError', null)
|
|
228
|
+
void loginAction({ email: data.email, password: data.password })
|
|
229
|
+
.then((result) => applyAuthResult(set, result))
|
|
230
|
+
.catch(authFailure(set, 'login'))
|
|
231
|
+
}}
|
|
232
|
+
onCreateAccount={() => emit('showRegister')}
|
|
233
|
+
/>
|
|
234
|
+
</>
|
|
235
|
+
)
|
|
236
|
+
},
|
|
237
|
+
AccountRegister: ({ props, emit }) => {
|
|
238
|
+
const { state, set } = useStateStore()
|
|
239
|
+
const parsed = accountRegisterPropsSchema.parse(props)
|
|
240
|
+
const error = typeof state.accountError === 'string' ? state.accountError : null
|
|
241
|
+
const pending = Boolean(state.accountPending)
|
|
242
|
+
return (
|
|
243
|
+
<>
|
|
244
|
+
<AuthNotice pending={pending} error={error} />
|
|
245
|
+
<SignUpBlock
|
|
246
|
+
logoText={parsed.logoText}
|
|
247
|
+
createAccountTitleLabel={parsed.createAccountTitleLabel}
|
|
248
|
+
createAccountDescriptionLabel={parsed.createAccountDescriptionLabel}
|
|
249
|
+
fullNameLabel={parsed.fullNameLabel}
|
|
250
|
+
emailLabel={parsed.emailLabel}
|
|
251
|
+
passwordLabel={parsed.passwordLabel}
|
|
252
|
+
createAccountLabel={parsed.createAccountLabel}
|
|
253
|
+
signInLabel={parsed.signInLabel}
|
|
254
|
+
onSubmit={(data) => {
|
|
255
|
+
const parts = data.fullName.trim().split(/\s+/).filter(Boolean)
|
|
256
|
+
set('/accountPending', true)
|
|
257
|
+
set('/accountError', null)
|
|
258
|
+
void registerAction({
|
|
259
|
+
email: data.email,
|
|
260
|
+
password: data.password,
|
|
261
|
+
first_name: parts[0] ?? '',
|
|
262
|
+
last_name: parts.slice(1).join(' '),
|
|
263
|
+
})
|
|
264
|
+
.then((result) => applyAuthResult(set, result))
|
|
265
|
+
.catch(authFailure(set, 'register'))
|
|
266
|
+
}}
|
|
267
|
+
onSignIn={() => emit('showLogin')}
|
|
268
|
+
/>
|
|
269
|
+
</>
|
|
270
|
+
)
|
|
271
|
+
},
|
|
272
|
+
AccountPanel: ({ emit }) => {
|
|
273
|
+
const { state } = useStateStore()
|
|
274
|
+
const customer = (state.accountCustomer ?? null) as Customer | null
|
|
275
|
+
const error = typeof state.accountError === 'string' ? state.accountError : null
|
|
276
|
+
const pending = Boolean(state.accountPending)
|
|
277
|
+
const name = customer
|
|
278
|
+
? [customer.firstName, customer.lastName].filter(Boolean).join(' ') || customer.email
|
|
279
|
+
: ''
|
|
280
|
+
return (
|
|
281
|
+
<div className="mx-auto max-w-md px-4 py-16 text-center">
|
|
282
|
+
<h1 className="text-2xl font-bold">Mi cuenta</h1>
|
|
283
|
+
<p className="text-muted-foreground mt-2 text-sm">Has iniciado sesión como</p>
|
|
284
|
+
<p className="mt-1 text-lg font-medium">{name}</p>
|
|
285
|
+
{customer?.email ? <p className="text-muted-foreground text-sm">{customer.email}</p> : null}
|
|
286
|
+
{error ? (
|
|
287
|
+
<p role="alert" className="text-destructive mt-4 text-sm">
|
|
288
|
+
{error}
|
|
289
|
+
</p>
|
|
290
|
+
) : null}
|
|
291
|
+
<Button className="mt-6" disabled={pending} onClick={() => emit('logout')}>
|
|
292
|
+
{pending ? 'Cerrando sesión…' : 'Cerrar sesión'}
|
|
293
|
+
</Button>
|
|
294
|
+
</div>
|
|
295
|
+
)
|
|
296
|
+
},
|
|
297
|
+
/**
|
|
298
|
+
* Signed-in order history. The load runs once from the cookie-authorized
|
|
299
|
+
* Server Action; loading/empty/error are distinct and a failure never renders
|
|
300
|
+
* a fabricated order. `onViewDetails` records the selected order in state and
|
|
301
|
+
* the detail is assembled below from the already-fetched row — no second
|
|
302
|
+
* backend request. No token is read, stored, or rendered here.
|
|
303
|
+
*/
|
|
304
|
+
AccountOrders: ({ props }) => {
|
|
305
|
+
const { state, set } = useStateStore()
|
|
306
|
+
const parsed = accountOrdersPropsSchema.parse(props)
|
|
307
|
+
const loaded = useRef(false)
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (loaded.current) return
|
|
310
|
+
loaded.current = true
|
|
311
|
+
set('/accountOrdersPending', true)
|
|
312
|
+
set('/accountOrdersError', null)
|
|
313
|
+
void loadOrdersAction()
|
|
314
|
+
.then((result) => {
|
|
315
|
+
set('/accountOrdersPending', false)
|
|
316
|
+
if (result.ok) {
|
|
317
|
+
set('/accountOrders', result.orders)
|
|
318
|
+
set('/accountOrdersError', null)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
set('/accountOrdersError', result.error)
|
|
322
|
+
})
|
|
323
|
+
.catch((error) => {
|
|
324
|
+
console.error(
|
|
325
|
+
'[storefront/account] loadOrders failed',
|
|
326
|
+
error instanceof Error ? error.message : error,
|
|
327
|
+
)
|
|
328
|
+
set('/accountOrdersPending', false)
|
|
329
|
+
set('/accountOrdersError', parsed.errorLabel ?? 'No se pudieron cargar tus pedidos.')
|
|
330
|
+
})
|
|
331
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
332
|
+
}, [])
|
|
333
|
+
|
|
334
|
+
const pending = Boolean(state.accountOrdersPending)
|
|
335
|
+
const error = typeof state.accountOrdersError === 'string' ? state.accountOrdersError : null
|
|
336
|
+
const orders = Array.isArray(state.accountOrders)
|
|
337
|
+
? (state.accountOrders as AccountOrderView[])
|
|
338
|
+
: []
|
|
339
|
+
const selectedId =
|
|
340
|
+
typeof state.accountSelectedOrderId === 'string' ? state.accountSelectedOrderId : null
|
|
341
|
+
const selected = selectedId
|
|
342
|
+
? orders.find((order) => order.number === selectedId) ?? null
|
|
343
|
+
: null
|
|
344
|
+
|
|
345
|
+
if (pending) {
|
|
346
|
+
return (
|
|
347
|
+
<p className="text-muted-foreground mx-auto max-w-7xl px-4 py-8 text-sm">
|
|
348
|
+
{parsed.loadingLabel ?? 'Cargando pedidos…'}
|
|
349
|
+
</p>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
if (error) {
|
|
353
|
+
return (
|
|
354
|
+
<p role="alert" className="text-destructive mx-auto max-w-7xl px-4 py-8 text-sm">
|
|
355
|
+
{error}
|
|
356
|
+
</p>
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
if (orders.length === 0) {
|
|
360
|
+
return (
|
|
361
|
+
<p className="text-muted-foreground mx-auto max-w-7xl px-4 py-8 text-sm">
|
|
362
|
+
{parsed.emptyLabel ?? 'Aún no tienes pedidos.'}
|
|
363
|
+
</p>
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
return (
|
|
367
|
+
<>
|
|
368
|
+
<OrderHistory2
|
|
369
|
+
orders={orders.map(toBlockOrder)}
|
|
370
|
+
titleLabel={parsed.titleLabel}
|
|
371
|
+
descriptionLabel={parsed.descriptionLabel}
|
|
372
|
+
onViewDetails={(order) => set('/accountSelectedOrderId', order.id)}
|
|
373
|
+
/>
|
|
374
|
+
{selected ? <OrderDetail order={selected} /> : null}
|
|
375
|
+
</>
|
|
376
|
+
)
|
|
377
|
+
},
|
|
378
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Thor Commerce view schema fragment: account
|
|
2
|
+
// Zod mirrors of the block contracts for this view — the single source for the
|
|
3
|
+
// catalog entries and the client-side registry parsing (json-render does not
|
|
4
|
+
// validate props against the catalog itself, so the registry parses here).
|
|
5
|
+
// Kept free of React and server imports so the Server Component module and the
|
|
6
|
+
// 'use client' registry can share one contract. Block zod contracts are
|
|
7
|
+
// mirrored, not imported: @thorprovider/blocks ships zod v3 while the scaffold
|
|
8
|
+
// runs zod v4 and cross-major schema assignment breaks the catalog contract.
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod'
|
|
11
|
+
|
|
12
|
+
// Mirrors the LoginPage1 optional props. Every prop is optional with a neutral
|
|
13
|
+
// default in the block, so the curated view forwards only the labels it owns.
|
|
14
|
+
export const accountLoginPropsSchema = z.object({
|
|
15
|
+
logoText: z.string().optional(),
|
|
16
|
+
welcomeTitleLabel: z.string().optional(),
|
|
17
|
+
welcomeDescriptionLabel: z.string().optional(),
|
|
18
|
+
emailLabel: z.string().optional(),
|
|
19
|
+
passwordLabel: z.string().optional(),
|
|
20
|
+
signInLabel: z.string().optional(),
|
|
21
|
+
createAccountLabel: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Mirrors the SignUpPage1 optional props.
|
|
25
|
+
export const accountRegisterPropsSchema = z.object({
|
|
26
|
+
logoText: z.string().optional(),
|
|
27
|
+
createAccountTitleLabel: z.string().optional(),
|
|
28
|
+
createAccountDescriptionLabel: z.string().optional(),
|
|
29
|
+
fullNameLabel: z.string().optional(),
|
|
30
|
+
emailLabel: z.string().optional(),
|
|
31
|
+
passwordLabel: z.string().optional(),
|
|
32
|
+
createAccountLabel: z.string().optional(),
|
|
33
|
+
signInLabel: z.string().optional(),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Mirrors the OrderHistory2 label props the curated view forwards, plus the
|
|
37
|
+
// explicit loading/empty/error copy the registry renders for the orders states.
|
|
38
|
+
export const accountOrdersPropsSchema = z.object({
|
|
39
|
+
titleLabel: z.string().optional(),
|
|
40
|
+
descriptionLabel: z.string().optional(),
|
|
41
|
+
emptyLabel: z.string().optional(),
|
|
42
|
+
errorLabel: z.string().optional(),
|
|
43
|
+
loadingLabel: z.string().optional(),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// The headless bootstrap and the signed-in panel read their data from the
|
|
47
|
+
// feature state, not from element props; declared so the catalog is complete.
|
|
48
|
+
export const accountEmptyPropsSchema = z.object({})
|
|
49
|
+
|
|
50
|
+
export type AccountLoginProps = z.infer<typeof accountLoginPropsSchema>
|
|
51
|
+
export type AccountRegisterProps = z.infer<typeof accountRegisterPropsSchema>
|
|
52
|
+
export type AccountOrdersProps = z.infer<typeof accountOrdersPropsSchema>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"view": "category",
|
|
3
|
+
"slots": [
|
|
4
|
+
{ "id": "category.filter", "slot": "filter", "default": "CategoryFilter2" },
|
|
5
|
+
{
|
|
6
|
+
"id": "category.productCard",
|
|
7
|
+
"slot": "productCard",
|
|
8
|
+
"default": "ProductCard1",
|
|
9
|
+
"nested": true
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"sections": {
|
|
13
|
+
"faq": {
|
|
14
|
+
"elementType": "Faq",
|
|
15
|
+
"registryModule": "../sections/faq.client",
|
|
16
|
+
"registryExport": "faqRegistry",
|
|
17
|
+
"propsModule": "./content",
|
|
18
|
+
"defaultProps": "{ items: [] }"
|
|
19
|
+
},
|
|
20
|
+
"cta": {
|
|
21
|
+
"elementType": "Cta",
|
|
22
|
+
"registryModule": "../sections/cta.client",
|
|
23
|
+
"registryExport": "ctaRegistry",
|
|
24
|
+
"propsModule": "./content",
|
|
25
|
+
"defaultProps": "{}"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Thor Commerce view module: category
|
|
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 -> "category".
|
|
6
|
+
//
|
|
7
|
+
// E2 task 6.1: the category listing is ONE controlled `CategoryFilter2` — the
|
|
8
|
+
// base listing slot `category.filter`, always present. The optional
|
|
9
|
+
// `catalog-search` / `category-filters` features are control contributions to
|
|
10
|
+
// that single block (logical mounts `category.search` / `category.filters`),
|
|
11
|
+
// never a second listing/controller. Their visibility and config are read from
|
|
12
|
+
// the installed manifest (`mountedFeatures` / `mountedFeatureConfigs`) that
|
|
13
|
+
// `sync` derives from `storefront.config.json`; no loader is hand-edited to add
|
|
14
|
+
// or remove them.
|
|
15
|
+
|
|
16
|
+
import { z } from 'zod'
|
|
17
|
+
import {
|
|
18
|
+
buildActiveFilterChips,
|
|
19
|
+
getCatalog,
|
|
20
|
+
getCatalogSortKeys,
|
|
21
|
+
getCollectionOptions,
|
|
22
|
+
loadCategoryListing,
|
|
23
|
+
mapCatalogOutcomeToStatus,
|
|
24
|
+
mapCatalogResultsToFilterProps,
|
|
25
|
+
} from '@thorprovider/runtime/commerce'
|
|
26
|
+
import { getDataSource, getStoreContext, isRealMode } from '../../store-context'
|
|
27
|
+
import { mountedFeatures, mountedFeatureConfigs } from '../../composition'
|
|
28
|
+
import { pageRoot } from '../../spec/page-root'
|
|
29
|
+
import type { StorefrontElement } from '../../spec/types'
|
|
30
|
+
import type { ViewModule } from '../types'
|
|
31
|
+
import { registry } from './registry'
|
|
32
|
+
import { categoryFilterProps } from './props'
|
|
33
|
+
import { categoryFilterPropsSchema } from './schema'@@SECTION_IMPORTS@@
|
|
34
|
+
|
|
35
|
+
// The single controlled `CategoryFilter2` base listing. The search/filter
|
|
36
|
+
// controls are shown only when the matching feature is installed; the base
|
|
37
|
+
// listing always owns the route scope, `sort`, `page`, `pageSize`, the products,
|
|
38
|
+
// and pagination.
|
|
39
|
+
const hasSearch = mountedFeatures.includes('catalog-search')
|
|
40
|
+
const hasFilters = mountedFeatures.includes('category-filters')
|
|
41
|
+
const searchConfig = (mountedFeatureConfigs['catalog-search'] ?? {}) as { placeholder?: string }
|
|
42
|
+
const filtersConfig = (mountedFeatureConfigs['category-filters'] ?? {}) as {
|
|
43
|
+
filterSet?: string[]
|
|
44
|
+
showPrice?: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// UI labels for the adapter-advertised sort keys. The keys themselves come from
|
|
48
|
+
// the provider (`getCatalogSortKeys`); no global price sort is ever assumed.
|
|
49
|
+
const SORT_LABELS: Record<string, string> = {
|
|
50
|
+
featured: 'Destacados',
|
|
51
|
+
name: 'Nombre',
|
|
52
|
+
newest: 'Más recientes',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const elements: Record<string, StorefrontElement> = {
|
|
56
|
+
@@ELEMENTS@@
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const catalog = {
|
|
60
|
+
@@CATALOG@@
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Empty state for the single listing, used on every failure path. */
|
|
64
|
+
const emptyFilterState = () => ({
|
|
65
|
+
categoryFilterProducts: [],
|
|
66
|
+
categoryFilterPagination: [] as (number | string)[],
|
|
67
|
+
categoryFilterActiveFilters: [],
|
|
68
|
+
categoryFilterSearchValue: '',
|
|
69
|
+
categoryFilterSelectedCategories: [] as string[],
|
|
70
|
+
categoryFilterSelectedCollections: [] as string[],
|
|
71
|
+
categoryFilterSelectedSort: 'featured',
|
|
72
|
+
categoryFilterSelectedPage: 1,
|
|
73
|
+
categoryFilterSelectedPageSize: 50,
|
|
74
|
+
categoryFilterSortOptions: [],
|
|
75
|
+
categoryFilterCategories: [],
|
|
76
|
+
categoryFilterCollections: [],
|
|
77
|
+
categoryFilterPriceRange: undefined,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const category: ViewModule = {
|
|
81
|
+
name: 'category',
|
|
82
|
+
elements,
|
|
83
|
+
catalog,
|
|
84
|
+
registry,
|
|
85
|
+
route: {
|
|
86
|
+
path: '/categorias/[[...slug]]',
|
|
87
|
+
loaderName: 'category',
|
|
88
|
+
layout: 'default',
|
|
89
|
+
metadata: {
|
|
90
|
+
title: 'Categoría',
|
|
91
|
+
description: 'Explora los productos de esta categoría en __SITE_NAME__.',
|
|
92
|
+
},
|
|
93
|
+
entry: pageRoot('category', [@@SLOT_IDS@@], elements),
|
|
94
|
+
},
|
|
95
|
+
loader: {
|
|
96
|
+
category: async (params, searchParams) => {
|
|
97
|
+
const slug = Array.isArray(params.slug) ? params.slug[0] : params.slug
|
|
98
|
+
try {
|
|
99
|
+
// Validate the raw URL through the pure boundary before any commerce
|
|
100
|
+
// call. An invalid, repeated, or unknown parameter — or one owned by a
|
|
101
|
+
// feature that is not installed — returns a typed error with zero
|
|
102
|
+
// provider resolution, not even the sidebar catalog (E2 task 5.1/6.1).
|
|
103
|
+
const listing = await loadCategoryListing({
|
|
104
|
+
searchParams: searchParams ?? {},
|
|
105
|
+
...(slug ? { categoryScope: slug } : {}),
|
|
106
|
+
mountedFeatures,
|
|
107
|
+
...(isRealMode() ? { context: getStoreContext() } : {}),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (!listing.ok) {
|
|
111
|
+
return {
|
|
112
|
+
storeCategories: [],
|
|
113
|
+
dataSource: getDataSource(),
|
|
114
|
+
dataError: null,
|
|
115
|
+
...emptyFilterState(),
|
|
116
|
+
categoryFilterStatus: mapCatalogOutcomeToStatus({ ok: false, error: listing.error }),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const catalog = await getCatalog()
|
|
121
|
+
const advertisedSortKeys = await getCatalogSortKeys()
|
|
122
|
+
const sortOptions = advertisedSortKeys.map((key) => ({
|
|
123
|
+
value: key,
|
|
124
|
+
label: SORT_LABELS[key] ?? key,
|
|
125
|
+
}))
|
|
126
|
+
// Real adapter category options (handle + name). No fake counts/ranges.
|
|
127
|
+
const allCategories = catalog.categoriesRaw.map((entry) => ({
|
|
128
|
+
id: entry.handle,
|
|
129
|
+
label: entry.name,
|
|
130
|
+
}))
|
|
131
|
+
const filterSet = filtersConfig.filterSet ?? ['categories', 'collections']
|
|
132
|
+
const categories = filterSet.includes('categories') ? allCategories : undefined
|
|
133
|
+
// Real, backend-scoped collection options (handle + title), fetched only
|
|
134
|
+
// when `category-filters` is installed and owns the collections control.
|
|
135
|
+
// An empty backend list omits the control; no option is ever fabricated.
|
|
136
|
+
const showCollections = hasFilters && filterSet.includes('collections')
|
|
137
|
+
const allCollections = showCollections
|
|
138
|
+
? (await getCollectionOptions()).map((entry) => ({
|
|
139
|
+
id: entry.id,
|
|
140
|
+
label: entry.label,
|
|
141
|
+
}))
|
|
142
|
+
: []
|
|
143
|
+
const collections = allCollections.length > 0 ? allCollections : undefined
|
|
144
|
+
const presentation = mapCatalogResultsToFilterProps({
|
|
145
|
+
query: listing.query,
|
|
146
|
+
results: listing.results,
|
|
147
|
+
// A scoped listing (`/categorias/<handle>`) must not offer the
|
|
148
|
+
// category facet: the scope already fixes the category. The root
|
|
149
|
+
// catalog passes no scope and keeps it.
|
|
150
|
+
...(slug ? { categoryScope: slug } : {}),
|
|
151
|
+
// Product links point at the product-detail route. The runtime stays
|
|
152
|
+
// route-agnostic, so this view supplies the href builder.
|
|
153
|
+
productHref: (handle: string) => `/productos/${handle}`,
|
|
154
|
+
...(categories ? { categories } : {}),
|
|
155
|
+
...(collections ? { collections } : {}),
|
|
156
|
+
sortOptions,
|
|
157
|
+
})
|
|
158
|
+
const activeFilters = buildActiveFilterChips(listing.query, {
|
|
159
|
+
categories: Object.fromEntries(allCategories.map((entry) => [entry.id, entry.label])),
|
|
160
|
+
collections: Object.fromEntries(allCollections.map((entry) => [entry.id, entry.label])),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
storeCategories: catalog.categories,
|
|
165
|
+
dataSource: catalog.source,
|
|
166
|
+
dataError: null,
|
|
167
|
+
categoryFilterProducts: presentation.products,
|
|
168
|
+
categoryFilterPagination: presentation.pagination,
|
|
169
|
+
categoryFilterStatus: mapCatalogOutcomeToStatus({ ok: true, results: listing.results }),
|
|
170
|
+
categoryFilterActiveFilters: activeFilters,
|
|
171
|
+
categoryFilterSearchValue: presentation.searchValue,
|
|
172
|
+
categoryFilterSelectedCategories: presentation.selectedCategories,
|
|
173
|
+
categoryFilterSelectedCollections: presentation.selectedCollections,
|
|
174
|
+
categoryFilterSelectedSort: presentation.selectedSort,
|
|
175
|
+
categoryFilterSelectedPage: presentation.selectedPage,
|
|
176
|
+
categoryFilterSelectedPageSize: presentation.selectedPageSize,
|
|
177
|
+
categoryFilterSortOptions: presentation.sortOptions ?? [],
|
|
178
|
+
categoryFilterCategories: presentation.categories ?? [],
|
|
179
|
+
categoryFilterCollections: presentation.collections ?? [],
|
|
180
|
+
categoryFilterPriceRange: presentation.priceRange,
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
storeCategories: [],
|
|
185
|
+
dataSource: getDataSource(),
|
|
186
|
+
dataError: error instanceof Error ? error.message : 'No se pudo cargar la categoría.',
|
|
187
|
+
...emptyFilterState(),
|
|
188
|
+
categoryFilterStatus: { state: 'error' as const },
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Thor Commerce view registry fragment: category
|
|
4
|
+
// The category view's single client boundary: one controlled `CategoryFilter2`
|
|
5
|
+
// (the base listing slot `category.filter`). Search/filters/sort/pagination are
|
|
6
|
+
// URL-backed: the block emits a change, this registry rewrites the URL through
|
|
7
|
+
// the pure `buildCategoryQueryHref` seam, and the server loader re-seeds the
|
|
8
|
+
// controlled props from the new URL. No fetch and no provider call happens on
|
|
9
|
+
// the client. The search and filter controls are only present because the
|
|
10
|
+
// installed manifest enabled them; the block never adds a second listing.
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
CategoryFilter2 as CategoryFilter2Block,
|
|
14
|
+
type CategoryFilter2ActiveFilter,
|
|
15
|
+
type CategoryFilter2Filters,
|
|
16
|
+
type CategoryFilter2Props,
|
|
17
|
+
} from '@thorprovider/blocks'@@BLOCK_IMPORTS@@@@SECTION_REGISTRY_IMPORTS@@
|
|
18
|
+
import { buildCategoryQueryHref, catalogHistoryMode, type CategoryQueryChange } from '@thorprovider/runtime/commerce'
|
|
19
|
+
import type { CatalogFeatureName } from '@thorprovider/runtime/contracts'
|
|
20
|
+
import { useStateStore } from '@json-render/react'
|
|
21
|
+
import { usePathname, useRouter } from 'next/navigation'
|
|
22
|
+
import type { ViewRegistry } from '../types'
|
|
23
|
+
|
|
24
|
+
// The generated view module injects the installed-feature context as an element
|
|
25
|
+
// prop (task 4.11); it is composition context, not a `CategoryFilter2` prop, so
|
|
26
|
+
// it is read separately from the block props.
|
|
27
|
+
type CategoryFilter2RegistryProps = CategoryFilter2Props & {
|
|
28
|
+
mountedFeatures?: readonly CatalogFeatureName[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const registry: ViewRegistry = {
|
|
32
|
+
@@REGISTRY@@
|
|
33
|
+
}
|