@thorprovider/storefront 0.3.1 → 0.4.0-mvp.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +321 -152
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +54 -2
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +17 -7
- package/assets/templates/app/app/robots.ts +19 -0
- package/assets/templates/app/app/sitemap.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +612 -0
- package/assets/templates/app/lib/__STOREFRONT__/cart-state.ts +66 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +50 -57
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +679 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +197 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +5 -36
- package/assets/templates/app/lib/__STOREFRONT__/icon-map.ts +27 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +13 -11
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +264 -51
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +6 -3
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +136 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.core.ts +470 -0
- package/assets/templates/app/lib/__STOREFRONT__/session.ts +169 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +120 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +38 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +129 -10
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +5 -1
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +191 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.core.ts +257 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +7 -31
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +161 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.client.tsx +25 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/cta.tsx +16 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.client.tsx +24 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +5 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +26 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +36 -18
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.cta.ts +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.faq.ts +18 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +35 -14
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +31 -13
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +5 -3
- package/assets/templates/features/checkout/handlers.ts +159 -0
- package/assets/templates/features/checkout/state.ts +61 -0
- package/assets/templates/features/customer-auth/handlers.ts +59 -0
- package/assets/templates/features/customer-auth/state.ts +45 -0
- package/assets/templates/features/recommendations/state.ts +11 -0
- package/assets/templates/views/account/content.ts +35 -0
- package/assets/templates/views/account/index.tsx +114 -0
- package/assets/templates/views/account/registry.tsx +439 -0
- package/assets/templates/views/account/schema.ts +53 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +29 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +251 -0
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +34 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/catalog.part +5 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/element.part +34 -0
- package/assets/templates/views/category/assembly/slots/filter/CategoryFilter2/registry.part +71 -0
- package/assets/templates/views/category/content.ts +30 -0
- package/assets/templates/views/category/data.ts +12 -0
- package/assets/templates/views/category/index.tsx +299 -0
- package/assets/templates/views/category/props.ts +23 -0
- package/assets/templates/views/category/registry.tsx +122 -0
- package/assets/templates/views/category/schema.ts +91 -0
- package/assets/templates/views/checkout/content.ts +105 -0
- package/assets/templates/views/checkout/data.ts +23 -0
- package/assets/templates/views/checkout/index.tsx +111 -0
- package/assets/templates/views/checkout/props.ts +49 -0
- package/assets/templates/views/checkout/registry.tsx +217 -0
- package/assets/templates/views/checkout/schema.ts +229 -0
- package/assets/templates/views/home/assembly/assembly.json +33 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +90 -0
- package/assets/templates/views/home/assembly/registry.skeleton.tsx +25 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature1/registry.part +11 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/element.part +4 -0
- package/assets/templates/views/home/assembly/slots/benefits/Feature2/registry.part +10 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/categories/ProductCategory2/registry.part +16 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/binding.json +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/catalog.part +4 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +5 -0
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +26 -0
- package/assets/templates/views/home/content.ts +70 -0
- package/assets/templates/views/home/data.ts +24 -0
- package/assets/templates/views/home/index.tsx +57 -85
- package/assets/templates/views/home/props.ts +22 -0
- package/assets/templates/views/home/registry.tsx +38 -22
- package/assets/templates/views/home/schema.ts +93 -0
- package/assets/templates/views/product/content.ts +25 -0
- package/assets/templates/views/product/data.ts +50 -0
- package/assets/templates/views/product/index.tsx +148 -0
- package/assets/templates/views/product/props.ts +27 -0
- package/assets/templates/views/product/registry.tsx +103 -0
- package/assets/templates/views/product/schema.ts +121 -0
- package/assets/templates/views/static/content.ts +99 -0
- package/assets/templates/views/static/index.tsx +60 -0
- package/assets/templates/views/static/registry.tsx +66 -0
- package/assets/templates/views/static/schema.ts +45 -0
- package/bin/storefront.js +2424 -148
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +15 -9
- package/recipes/archetypes.json +28 -14
- package/recipes/catalog.json +400 -0
- package/recipes/catalog.mjs +128 -0
- package/recipes/catalog.schema.json +64 -0
- package/recipes/feature.schema.json +89 -0
- package/recipes/features.json +162 -0
- package/recipes/manifest.schema.json +69 -0
- package/recipes/operations.schema.json +43 -0
- package/recipes/recipe.schema.json +11 -2
- package/recipes/recipes.json +76 -19
- package/recipes/sections.json +34 -5
- package/recipes/validate.mjs +838 -32
- package/scripts/candidate.mjs +236 -0
- package/scripts/composers.mjs +160 -0
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +121 -0
- package/scripts/logical-controls.mjs +61 -0
- package/scripts/manifest.mjs +298 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/operation-registry.mjs +256 -0
- package/scripts/operations.mjs +720 -0
- package/scripts/ownership-conflict.mjs +356 -0
- package/scripts/pinning.mjs +573 -0
- package/scripts/plan-apply.mjs +987 -0
- package/scripts/preflight.mjs +51 -0
- package/scripts/reconcile.mjs +549 -0
- package/scripts/render-templates.mjs +338 -68
- package/scripts/schema-validate.mjs +132 -0
- package/scripts/section-compat.mjs +300 -0
- package/scripts/section-config.mjs +253 -0
- package/scripts/section-operations.mjs +508 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/sync-assets.mjs +22 -15
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +337 -0
- package/scripts/view-selection.mjs +211 -0
- package/skills/sf-feature/SKILL.md +80 -0
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +100 -30
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +146 -93
- package/skills/sf-view-gen/assets/recipes/archetypes.json +28 -14
- package/skills/sf-view-gen/assets/recipes/catalog.json +400 -0
- package/skills/sf-view-gen/assets/recipes/catalog.schema.json +64 -0
- package/skills/sf-view-gen/assets/recipes/features.json +162 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +76 -19
- package/skills/sf-view-gen/assets/recipes/sections.json +34 -5
- package/skills/sf-view-gen/assets/templates/views/sections/cta.client.tsx +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/cta.tsx +16 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +12 -8
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +14 -10
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +2 -9
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.cta.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.faq.ts +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.logoCloud.ts +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.pricing.ts +26 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.statistics.ts +25 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.testimonials.ts +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +16 -8
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +2 -7
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +13 -9
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +2 -5
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +58 -33
- package/skills/sf-view-gen/scripts/catalog.mjs +128 -0
- package/skills/sf-view-gen/scripts/schema-validate.mjs +132 -0
- package/skills/sf-view-gen/scripts/validate.mjs +838 -32
- package/skills/storefront-workflow/SKILL.md +179 -0
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -61
- package/commands/sf-add-view.md +0 -26
- package/commands/sf-theme.md +0 -20
- package/commands/sf-view.md +0 -25
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +0 -86
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +0 -48
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +0 -192
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +0 -45
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +0 -122
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +0 -49
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +0 -168
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +0 -49
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
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 { mountedFeatureConfigs, mountedFeatureNames, mountedFeatures } 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 { categoryBreadcrumbProps, categoryFilterProps } from './props'
|
|
33
|
+
import { breadcrumbPropsSchema, categoryFilterPropsSchema } from './schema'
|
|
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
|
+
// The navigability feature contributes the entity-derived breadcrumb trail. When
|
|
42
|
+
// it is not installed the trail is empty and the Breadcrumb block renders
|
|
43
|
+
// nothing, so removal leaves no dangling navigation aid.
|
|
44
|
+
const hasNavigability = mountedFeatureNames.includes('navigability')
|
|
45
|
+
const searchConfig = (mountedFeatureConfigs['catalog-search'] ?? {}) as { placeholder?: string }
|
|
46
|
+
const filtersConfig = (mountedFeatureConfigs['category-filters'] ?? {}) as {
|
|
47
|
+
filterSet?: string[]
|
|
48
|
+
showPrice?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// UI labels for the adapter-advertised sort keys. The keys themselves come from
|
|
52
|
+
// the provider (`getCatalogSortKeys`); no global price sort is ever assumed.
|
|
53
|
+
const SORT_LABELS: Record<string, string> = {
|
|
54
|
+
featured: 'Destacados',
|
|
55
|
+
name: 'Nombre',
|
|
56
|
+
newest: 'Más recientes',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const elements: Record<string, StorefrontElement> = {
|
|
60
|
+
'category.breadcrumb': {
|
|
61
|
+
type: 'Breadcrumb',
|
|
62
|
+
props: {
|
|
63
|
+
...categoryBreadcrumbProps,
|
|
64
|
+
items: { $state: '/breadcrumbItems' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
'category.filter': {
|
|
68
|
+
type: 'CategoryFilter2',
|
|
69
|
+
props: {
|
|
70
|
+
// User-owned copy first, so a customization can be overridden by the
|
|
71
|
+
// feature config (search placeholder) and the derived state bindings.
|
|
72
|
+
...categoryFilterProps,
|
|
73
|
+
showSearch: hasSearch,
|
|
74
|
+
showFilters: hasFilters,
|
|
75
|
+
showSort: true,
|
|
76
|
+
showPrice: filtersConfig.showPrice === true,
|
|
77
|
+
...(hasSearch && searchConfig.placeholder
|
|
78
|
+
? { searchPlaceholder: searchConfig.placeholder }
|
|
79
|
+
: {}),
|
|
80
|
+
// Composition context for generated navigation (task 4.11): the client
|
|
81
|
+
// registry drops a removed feature's owned orphan parameters on a UI
|
|
82
|
+
// action. Read separately from the block props; never a `CategoryFilter2`
|
|
83
|
+
// prop and never bound to state.
|
|
84
|
+
mountedFeatures,
|
|
85
|
+
products: { $state: '/categoryFilterProducts' },
|
|
86
|
+
pagination: { $state: '/categoryFilterPagination' },
|
|
87
|
+
status: { $state: '/categoryFilterStatus' },
|
|
88
|
+
activeFilters: { $state: '/categoryFilterActiveFilters' },
|
|
89
|
+
searchValue: { $state: '/categoryFilterSearchValue' },
|
|
90
|
+
selectedCategories: { $state: '/categoryFilterSelectedCategories' },
|
|
91
|
+
selectedCollections: { $state: '/categoryFilterSelectedCollections' },
|
|
92
|
+
selectedSort: { $state: '/categoryFilterSelectedSort' },
|
|
93
|
+
selectedPage: { $state: '/categoryFilterSelectedPage' },
|
|
94
|
+
selectedPageSize: { $state: '/categoryFilterSelectedPageSize' },
|
|
95
|
+
sortOptions: { $state: '/categoryFilterSortOptions' },
|
|
96
|
+
categories: { $state: '/categoryFilterCategories' },
|
|
97
|
+
collections: { $state: '/categoryFilterCollections' },
|
|
98
|
+
priceRange: { $state: '/categoryFilterPriceRange' },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const catalog = {
|
|
104
|
+
Breadcrumb: {
|
|
105
|
+
props: breadcrumbPropsSchema,
|
|
106
|
+
description: 'Migas de pan derivadas de la jerarquía real de categorías.',
|
|
107
|
+
},
|
|
108
|
+
CategoryFilter2: {
|
|
109
|
+
props: categoryFilterPropsSchema,
|
|
110
|
+
description:
|
|
111
|
+
'Listado de categoría: una sola presentación controlada CategoryFilter2 con productos, paginación, búsqueda y filtros opcionales.',
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Empty state for the single listing, used on every failure path. */
|
|
116
|
+
const emptyFilterState = () => ({
|
|
117
|
+
categoryFilterProducts: [],
|
|
118
|
+
categoryFilterPagination: [] as (number | string)[],
|
|
119
|
+
categoryFilterActiveFilters: [],
|
|
120
|
+
categoryFilterSearchValue: '',
|
|
121
|
+
categoryFilterSelectedCategories: [] as string[],
|
|
122
|
+
categoryFilterSelectedCollections: [] as string[],
|
|
123
|
+
categoryFilterSelectedSort: 'featured',
|
|
124
|
+
categoryFilterSelectedPage: 1,
|
|
125
|
+
categoryFilterSelectedPageSize: 50,
|
|
126
|
+
categoryFilterSortOptions: [],
|
|
127
|
+
categoryFilterCategories: [],
|
|
128
|
+
categoryFilterCollections: [],
|
|
129
|
+
categoryFilterPriceRange: undefined,
|
|
130
|
+
breadcrumbItems: [] as { label: string; href?: string }[],
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The real category entity for the current route scope, resolved from the
|
|
135
|
+
* backend catalog. Entity SEO reads this; an unknown/absent scope yields no
|
|
136
|
+
* entity (no metadata or structured data is fabricated).
|
|
137
|
+
*/
|
|
138
|
+
function resolveCategoryEntity(
|
|
139
|
+
slug: string | undefined,
|
|
140
|
+
catalog: Awaited<ReturnType<typeof getCatalog>>,
|
|
141
|
+
): { handle: string; name: string; description?: string } | undefined {
|
|
142
|
+
if (!slug) return undefined
|
|
143
|
+
const match = catalog.categoriesRaw.find((entry) => entry.handle === slug)
|
|
144
|
+
if (!match) return undefined
|
|
145
|
+
const description =
|
|
146
|
+
typeof match.description === 'string' && match.description.trim() !== ''
|
|
147
|
+
? match.description
|
|
148
|
+
: undefined
|
|
149
|
+
return {
|
|
150
|
+
handle: match.handle,
|
|
151
|
+
name: match.name,
|
|
152
|
+
...(description !== undefined ? { description } : {}),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Entity-derived breadcrumb trail for the current category scope: Home, the
|
|
158
|
+
* real ancestor chain (root -> leaf), and the current category. Derived from
|
|
159
|
+
* the backend category hierarchy (`parentCategoryId`); no level is fabricated,
|
|
160
|
+
* a cycle cannot loop forever, and no trail is produced when the navigability
|
|
161
|
+
* feature is not installed or the scope is unknown.
|
|
162
|
+
*/
|
|
163
|
+
function resolveBreadcrumbItems(
|
|
164
|
+
slug: string | undefined,
|
|
165
|
+
catalog: Awaited<ReturnType<typeof getCatalog>>,
|
|
166
|
+
): { label: string; href?: string }[] {
|
|
167
|
+
if (!hasNavigability || !slug) return []
|
|
168
|
+
const byHandle = new Map(catalog.categoriesRaw.map((entry) => [entry.handle, entry]))
|
|
169
|
+
const byId = new Map(catalog.categoriesRaw.map((entry) => [entry.id, entry]))
|
|
170
|
+
const chain: { label: string; href: string }[] = []
|
|
171
|
+
const visited = new Set<string>()
|
|
172
|
+
let current = byHandle.get(slug)
|
|
173
|
+
while (current && !visited.has(current.handle)) {
|
|
174
|
+
visited.add(current.handle)
|
|
175
|
+
chain.unshift({ label: current.name, href: `/categories/${current.handle}` })
|
|
176
|
+
current = current.parentCategoryId ? byId.get(current.parentCategoryId) : undefined
|
|
177
|
+
}
|
|
178
|
+
if (chain.length === 0) return []
|
|
179
|
+
const leaf = chain[chain.length - 1]
|
|
180
|
+
// The current (leaf) item is not a link; only ancestors are navigable.
|
|
181
|
+
return [{ label: 'Inicio', href: '/' }, ...chain.slice(0, -1), { label: leaf.label }]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const category: ViewModule = {
|
|
185
|
+
name: 'category',
|
|
186
|
+
elements,
|
|
187
|
+
catalog,
|
|
188
|
+
registry,
|
|
189
|
+
route: {
|
|
190
|
+
path: '/categories/[[...slug]]',
|
|
191
|
+
loaderName: 'category',
|
|
192
|
+
layout: 'default',
|
|
193
|
+
metadata: {
|
|
194
|
+
title: 'Categoría',
|
|
195
|
+
description: 'Explora los productos de esta categoría en __SITE_NAME__.',
|
|
196
|
+
},
|
|
197
|
+
entry: pageRoot('category', ['category.breadcrumb', 'category.filter'], elements),
|
|
198
|
+
},
|
|
199
|
+
loader: {
|
|
200
|
+
category: async (params, searchParams) => {
|
|
201
|
+
const slug = Array.isArray(params.slug) ? params.slug[0] : params.slug
|
|
202
|
+
try {
|
|
203
|
+
// Validate the raw URL through the pure boundary before any commerce
|
|
204
|
+
// call. An invalid, repeated, or unknown parameter — or one owned by a
|
|
205
|
+
// feature that is not installed — returns a typed error with zero
|
|
206
|
+
// provider resolution, not even the sidebar catalog (E2 task 5.1/6.1).
|
|
207
|
+
const listing = await loadCategoryListing({
|
|
208
|
+
searchParams: searchParams ?? {},
|
|
209
|
+
...(slug ? { categoryScope: slug } : {}),
|
|
210
|
+
mountedFeatures,
|
|
211
|
+
...(isRealMode() ? { context: getStoreContext() } : {}),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
if (!listing.ok) {
|
|
215
|
+
return {
|
|
216
|
+
storeCategories: [],
|
|
217
|
+
dataSource: getDataSource(),
|
|
218
|
+
dataError: null,
|
|
219
|
+
...emptyFilterState(),
|
|
220
|
+
categoryFilterStatus: mapCatalogOutcomeToStatus({ ok: false, error: listing.error }),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const catalog = await getCatalog()
|
|
225
|
+
const advertisedSortKeys = await getCatalogSortKeys()
|
|
226
|
+
const sortOptions = advertisedSortKeys.map((key) => ({
|
|
227
|
+
value: key,
|
|
228
|
+
label: SORT_LABELS[key] ?? key,
|
|
229
|
+
}))
|
|
230
|
+
// Real adapter category options (handle + name). No fake counts/ranges.
|
|
231
|
+
const allCategories = catalog.categoriesRaw.map((entry) => ({
|
|
232
|
+
id: entry.handle,
|
|
233
|
+
label: entry.name,
|
|
234
|
+
}))
|
|
235
|
+
const filterSet = filtersConfig.filterSet ?? ['categories', 'collections']
|
|
236
|
+
const categories = filterSet.includes('categories') ? allCategories : undefined
|
|
237
|
+
// Real, backend-scoped collection options (handle + title), fetched only
|
|
238
|
+
// when `category-filters` is installed and owns the collections control.
|
|
239
|
+
// An empty backend list omits the control; no option is ever fabricated.
|
|
240
|
+
const showCollections = hasFilters && filterSet.includes('collections')
|
|
241
|
+
const allCollections = showCollections
|
|
242
|
+
? (await getCollectionOptions()).map((entry) => ({
|
|
243
|
+
id: entry.id,
|
|
244
|
+
label: entry.label,
|
|
245
|
+
}))
|
|
246
|
+
: []
|
|
247
|
+
const collections = allCollections.length > 0 ? allCollections : undefined
|
|
248
|
+
const presentation = mapCatalogResultsToFilterProps({
|
|
249
|
+
query: listing.query,
|
|
250
|
+
results: listing.results,
|
|
251
|
+
// A scoped listing (`/categories/<handle>`) must not offer the
|
|
252
|
+
// category facet: the scope already fixes the category. The root
|
|
253
|
+
// catalog passes no scope and keeps it.
|
|
254
|
+
...(slug ? { categoryScope: slug } : {}),
|
|
255
|
+
// Product links point at the product-detail route. The runtime stays
|
|
256
|
+
// route-agnostic, so this view supplies the href builder.
|
|
257
|
+
productHref: (handle: string) => `/products/${handle}`,
|
|
258
|
+
...(categories ? { categories } : {}),
|
|
259
|
+
...(collections ? { collections } : {}),
|
|
260
|
+
sortOptions,
|
|
261
|
+
})
|
|
262
|
+
const activeFilters = buildActiveFilterChips(listing.query, {
|
|
263
|
+
categories: Object.fromEntries(allCategories.map((entry) => [entry.id, entry.label])),
|
|
264
|
+
collections: Object.fromEntries(allCollections.map((entry) => [entry.id, entry.label])),
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
storeCategories: catalog.categories,
|
|
269
|
+
categoryEntity: resolveCategoryEntity(slug, catalog),
|
|
270
|
+
breadcrumbItems: resolveBreadcrumbItems(slug, catalog),
|
|
271
|
+
dataSource: catalog.source,
|
|
272
|
+
dataError: null,
|
|
273
|
+
categoryFilterProducts: presentation.products,
|
|
274
|
+
categoryFilterPagination: presentation.pagination,
|
|
275
|
+
categoryFilterStatus: mapCatalogOutcomeToStatus({ ok: true, results: listing.results }),
|
|
276
|
+
categoryFilterActiveFilters: activeFilters,
|
|
277
|
+
categoryFilterSearchValue: presentation.searchValue,
|
|
278
|
+
categoryFilterSelectedCategories: presentation.selectedCategories,
|
|
279
|
+
categoryFilterSelectedCollections: presentation.selectedCollections,
|
|
280
|
+
categoryFilterSelectedSort: presentation.selectedSort,
|
|
281
|
+
categoryFilterSelectedPage: presentation.selectedPage,
|
|
282
|
+
categoryFilterSelectedPageSize: presentation.selectedPageSize,
|
|
283
|
+
categoryFilterSortOptions: presentation.sortOptions ?? [],
|
|
284
|
+
categoryFilterCategories: presentation.categories ?? [],
|
|
285
|
+
categoryFilterCollections: presentation.collections ?? [],
|
|
286
|
+
categoryFilterPriceRange: presentation.priceRange,
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
storeCategories: [],
|
|
291
|
+
dataSource: getDataSource(),
|
|
292
|
+
dataError: error instanceof Error ? error.message : 'No se pudo cargar la categoría.',
|
|
293
|
+
...emptyFilterState(),
|
|
294
|
+
categoryFilterStatus: { state: 'error' as const },
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Thor Commerce view props: category
|
|
2
|
+
// User-owned element props for the category view. Reconcile seeds this file once
|
|
3
|
+
// and never rewrites it; the derived view module (./index.tsx) imports these
|
|
4
|
+
// objects and binds them into the element tree, so edits here survive
|
|
5
|
+
// `storefront sync` and unit removal. Plain data only: no React and no server
|
|
6
|
+
// imports.
|
|
7
|
+
//
|
|
8
|
+
// The listing state (products, pagination, selected query values, status) is
|
|
9
|
+
// derived wiring, not copy: it is bound to `$state` in ./index.tsx from the
|
|
10
|
+
// URL-backed loader. Only presentation copy lives here.
|
|
11
|
+
|
|
12
|
+
import { filterContent } from './content'
|
|
13
|
+
|
|
14
|
+
export const categoryFilterProps = {
|
|
15
|
+
...filterContent,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Presentational defaults for the breadcrumb; the trail itself is bound to
|
|
19
|
+
// `$state` from the loader (entity-derived, never invented).
|
|
20
|
+
export const categoryBreadcrumbProps = {
|
|
21
|
+
ariaLabel: 'Migas de pan',
|
|
22
|
+
backLabel: 'Volver',
|
|
23
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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'
|
|
18
|
+
import { Breadcrumb as BreadcrumbBlock } from '@thorprovider/blocks'
|
|
19
|
+
import { buildCategoryQueryHref, catalogHistoryMode, type CategoryQueryChange } from '@thorprovider/runtime/commerce'
|
|
20
|
+
import type { CatalogFeatureName } from '@thorprovider/runtime/contracts'
|
|
21
|
+
import { useStateStore } from '@json-render/react'
|
|
22
|
+
import { usePathname, useRouter } from 'next/navigation'
|
|
23
|
+
import type { ViewRegistry } from '../types'
|
|
24
|
+
|
|
25
|
+
// The generated view module injects the installed-feature context as an element
|
|
26
|
+
// prop (task 4.11); it is composition context, not a `CategoryFilter2` prop, so
|
|
27
|
+
// it is read separately from the block props.
|
|
28
|
+
type CategoryFilter2RegistryProps = CategoryFilter2Props & {
|
|
29
|
+
mountedFeatures?: readonly CatalogFeatureName[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const registry: ViewRegistry = {
|
|
33
|
+
Breadcrumb: ({ props }) => {
|
|
34
|
+
const p = props as {
|
|
35
|
+
items: { label: string; href?: string }[]
|
|
36
|
+
maxItems?: number
|
|
37
|
+
ariaLabel?: string
|
|
38
|
+
backLabel?: string
|
|
39
|
+
className?: string
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
<BreadcrumbBlock
|
|
43
|
+
items={p.items}
|
|
44
|
+
maxItems={p.maxItems}
|
|
45
|
+
ariaLabel={p.ariaLabel}
|
|
46
|
+
backLabel={p.backLabel}
|
|
47
|
+
className={p.className}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
},
|
|
51
|
+
CategoryFilter2: ({ props, emit }) => {
|
|
52
|
+
const router = useRouter()
|
|
53
|
+
const pathname = usePathname()
|
|
54
|
+
const { set } = useStateStore()
|
|
55
|
+
const p = props as unknown as CategoryFilter2RegistryProps
|
|
56
|
+
|
|
57
|
+
// Minimal `updateURL` seam (task 6.1; task 5.2 verified the criterion and is
|
|
58
|
+
// complete): apply one change to the current browser URL and let the server
|
|
59
|
+
// loader rebuild the listing. The page reset rules live in the pure helper,
|
|
60
|
+
// not here. The installed-feature context is passed so generated navigation
|
|
61
|
+
// drops a removed feature's owned orphan parameters (task 4.11) while never
|
|
62
|
+
// dropping the base listing's route scope, `sort`, `page`, or `pageSize`.
|
|
63
|
+
//
|
|
64
|
+
// History policy (task 5.3): an intentional filter/sort/page/pageSize commit
|
|
65
|
+
// pushes a history entry so reload and back/forward restore the previous
|
|
66
|
+
// query and products; repair and the immediate per-keystroke search replace
|
|
67
|
+
// the current entry (no per-keystroke history explosion). The choice is the
|
|
68
|
+
// pure `catalogHistoryMode` seam, not ad-hoc logic here.
|
|
69
|
+
const apply = (change: CategoryQueryChange) => {
|
|
70
|
+
const search = typeof window === 'undefined' ? '' : window.location.search
|
|
71
|
+
const href = buildCategoryQueryHref(pathname, search, change, p.mountedFeatures)
|
|
72
|
+
if (catalogHistoryMode(change) === 'push') router.push(href)
|
|
73
|
+
else router.replace(href)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<CategoryFilter2Block
|
|
78
|
+
products={p.products}
|
|
79
|
+
categories={p.categories}
|
|
80
|
+
collections={p.collections}
|
|
81
|
+
sortOptions={p.sortOptions}
|
|
82
|
+
pagination={p.pagination}
|
|
83
|
+
priceRange={p.priceRange}
|
|
84
|
+
showSearch={p.showSearch}
|
|
85
|
+
showFilters={p.showFilters}
|
|
86
|
+
showSort={p.showSort}
|
|
87
|
+
showPrice={p.showPrice}
|
|
88
|
+
searchValue={p.searchValue}
|
|
89
|
+
selectedCategories={p.selectedCategories}
|
|
90
|
+
selectedCollections={p.selectedCollections}
|
|
91
|
+
selectedSort={p.selectedSort}
|
|
92
|
+
selectedPage={p.selectedPage}
|
|
93
|
+
selectedPageSize={p.selectedPageSize}
|
|
94
|
+
selectedPriceRange={p.selectedPriceRange}
|
|
95
|
+
status={p.status}
|
|
96
|
+
activeFilters={p.activeFilters}
|
|
97
|
+
filtersTitle={p.filtersTitle}
|
|
98
|
+
searchLabel={p.searchLabel}
|
|
99
|
+
searchPlaceholder={p.searchPlaceholder}
|
|
100
|
+
sortPlaceholder={p.sortPlaceholder}
|
|
101
|
+
productsTitle={p.productsTitle}
|
|
102
|
+
onSearchChange={(text) => apply({ kind: 'search', text })}
|
|
103
|
+
onSortChange={(sort) => apply({ kind: 'sort', sort })}
|
|
104
|
+
onPageChange={(page) => apply({ kind: 'page', page })}
|
|
105
|
+
onPageSizeChange={(size) => apply({ kind: 'pageSize', pageSize: size })}
|
|
106
|
+
onFilterChange={(filters: CategoryFilter2Filters) =>
|
|
107
|
+
apply({ kind: 'filters', categories: filters.categories, collections: filters.collections ?? [] })
|
|
108
|
+
}
|
|
109
|
+
onClearFilters={() => apply({ kind: 'clearFilters' })}
|
|
110
|
+
onRepair={(remove) => apply({ kind: 'repair', remove })}
|
|
111
|
+
onRetry={() => router.refresh()}
|
|
112
|
+
onRemoveFilter={(filter: CategoryFilter2ActiveFilter) =>
|
|
113
|
+
apply({ kind: 'removeFilter', filter })
|
|
114
|
+
}
|
|
115
|
+
onProductClick={(product) => {
|
|
116
|
+
set('/lastClicked', { id: product.id })
|
|
117
|
+
emit('openProduct')
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
)
|
|
121
|
+
},
|
|
122
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Thor Commerce view schema fragment: category
|
|
2
|
+
// Zod mirror of the single controlled `CategoryFilter2` prop contract for this
|
|
3
|
+
// view — the source for the catalog entry and the client-side registry parsing
|
|
4
|
+
// (json-render does not validate props against the catalog itself, so the
|
|
5
|
+
// registry parses here). Kept free of React and server imports so the Server
|
|
6
|
+
// Component module and the 'use client' registry can share one contract. Block
|
|
7
|
+
// zod contracts are mirrored, not imported: @thorprovider/blocks ships zod v3
|
|
8
|
+
// while the scaffold runs zod v4 and cross-major schema assignment breaks the
|
|
9
|
+
// catalog contract.
|
|
10
|
+
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
|
|
13
|
+
export const filterProductSchema = z.object({
|
|
14
|
+
id: z.string(),
|
|
15
|
+
name: z.string(),
|
|
16
|
+
price: z.string(),
|
|
17
|
+
image: z.string().optional(),
|
|
18
|
+
// Product-detail link supplied by the presentation layer; when absent the
|
|
19
|
+
// block falls back to its onClick path.
|
|
20
|
+
href: z.string().optional(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const categoryFilterSortOptionSchema = z.object({
|
|
24
|
+
value: z.string(),
|
|
25
|
+
label: z.string(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const categoryFilterOptionSchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
label: z.string(),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export const categoryFilterActiveFilterSchema = z.object({
|
|
34
|
+
id: z.string(),
|
|
35
|
+
kind: z.string(),
|
|
36
|
+
label: z.string(),
|
|
37
|
+
params: z.array(z.string()),
|
|
38
|
+
value: z.string().optional(),
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
export const categoryFilterPriceRangeSchema = z.object({
|
|
42
|
+
min: z.number(),
|
|
43
|
+
max: z.number(),
|
|
44
|
+
step: z.number(),
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
export const categoryFilterStatusSchema = z.object({ state: z.string() }).catchall(z.unknown())
|
|
48
|
+
|
|
49
|
+
export const categoryFilterPropsSchema = z
|
|
50
|
+
.object({
|
|
51
|
+
products: z.array(filterProductSchema),
|
|
52
|
+
pagination: z.array(z.union([z.number(), z.string()])),
|
|
53
|
+
status: categoryFilterStatusSchema,
|
|
54
|
+
activeFilters: z.array(categoryFilterActiveFilterSchema),
|
|
55
|
+
searchValue: z.string(),
|
|
56
|
+
selectedCategories: z.array(z.string()),
|
|
57
|
+
selectedCollections: z.array(z.string()),
|
|
58
|
+
selectedSort: z.string(),
|
|
59
|
+
selectedPage: z.union([z.number(), z.string()]),
|
|
60
|
+
selectedPageSize: z.union([z.number(), z.string()]),
|
|
61
|
+
selectedPriceRange: z.array(z.number()).optional(),
|
|
62
|
+
sortOptions: z.array(categoryFilterSortOptionSchema),
|
|
63
|
+
categories: z.array(categoryFilterOptionSchema),
|
|
64
|
+
collections: z.array(categoryFilterOptionSchema),
|
|
65
|
+
priceRange: categoryFilterPriceRangeSchema.optional(),
|
|
66
|
+
showSearch: z.boolean(),
|
|
67
|
+
showFilters: z.boolean(),
|
|
68
|
+
showSort: z.boolean(),
|
|
69
|
+
showPrice: z.boolean().optional(),
|
|
70
|
+
filtersTitle: z.string().optional(),
|
|
71
|
+
searchLabel: z.string().optional(),
|
|
72
|
+
searchPlaceholder: z.string().optional(),
|
|
73
|
+
sortPlaceholder: z.string().optional(),
|
|
74
|
+
productsTitle: z.string().optional(),
|
|
75
|
+
})
|
|
76
|
+
.catchall(z.unknown())
|
|
77
|
+
|
|
78
|
+
export const breadcrumbItemSchema = z.object({
|
|
79
|
+
label: z.string(),
|
|
80
|
+
href: z.string().optional(),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
export const breadcrumbPropsSchema = z.object({
|
|
84
|
+
items: z.array(breadcrumbItemSchema),
|
|
85
|
+
maxItems: z.number().int().min(2).optional(),
|
|
86
|
+
ariaLabel: z.string().optional(),
|
|
87
|
+
backLabel: z.string().optional(),
|
|
88
|
+
className: z.string().optional(),
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export type CategoryFilterProps = z.infer<typeof categoryFilterPropsSchema>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Thor Commerce view content: checkout
|
|
2
|
+
// User-owned view content — checkout copy, field validation rules, field
|
|
3
|
+
// descriptions, and the confirmation fulfilment timeline. Reconcile seeds this
|
|
4
|
+
// file once and never rewrites it; the derived view module (./index.tsx)
|
|
5
|
+
// imports it and wires it into the element tree, so your edits here survive
|
|
6
|
+
// `storefront sync` and unit removal. Plain data only: no React and no server
|
|
7
|
+
// imports.
|
|
8
|
+
|
|
9
|
+
export const checkoutContent = {
|
|
10
|
+
title: 'Finalizar compra',
|
|
11
|
+
description: 'Completa tus datos para recibir tu pedido.',
|
|
12
|
+
validation: {
|
|
13
|
+
email: {
|
|
14
|
+
validateOn: 'blur',
|
|
15
|
+
checks: [
|
|
16
|
+
{ type: 'required', message: 'Ingresa tu correo electrónico.' },
|
|
17
|
+
{ type: 'email', message: 'Ingresa un correo válido.' },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
firstName: {
|
|
21
|
+
checks: [{ type: 'required', message: 'Ingresa tu nombre.' }],
|
|
22
|
+
},
|
|
23
|
+
lastName: {
|
|
24
|
+
checks: [{ type: 'required', message: 'Ingresa tus apellidos.' }],
|
|
25
|
+
},
|
|
26
|
+
phone: {
|
|
27
|
+
checks: [
|
|
28
|
+
{
|
|
29
|
+
type: 'pattern',
|
|
30
|
+
args: { pattern: '^[+]?[0-9]{7,15}$' },
|
|
31
|
+
message: 'Ingresa un teléfono válido.',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
address: {
|
|
36
|
+
checks: [{ type: 'required', message: 'Ingresa tu dirección.' }],
|
|
37
|
+
},
|
|
38
|
+
city: {
|
|
39
|
+
checks: [{ type: 'required', message: 'Ingresa la ciudad.' }],
|
|
40
|
+
},
|
|
41
|
+
state: {
|
|
42
|
+
checks: [{ type: 'required', message: 'Ingresa la provincia.' }],
|
|
43
|
+
},
|
|
44
|
+
country: {
|
|
45
|
+
checks: [{ type: 'required', message: 'Selecciona tu país.' }],
|
|
46
|
+
},
|
|
47
|
+
zipCode: {
|
|
48
|
+
checks: [
|
|
49
|
+
{ type: 'required', message: 'Ingresa el código postal.' },
|
|
50
|
+
{
|
|
51
|
+
type: 'pattern',
|
|
52
|
+
args: { pattern: '^[0-9]{4,5}$' },
|
|
53
|
+
message: 'Código postal inválido.',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
paymentMethod: {
|
|
58
|
+
checks: [{ type: 'required', message: 'Selecciona un método de pago.' }],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
fieldDescriptions: {
|
|
62
|
+
email: 'Usaremos este correo para el seguimiento del pedido.',
|
|
63
|
+
phone: 'Te contactaremos para coordinar la entrega.',
|
|
64
|
+
promoCode: 'Aplica códigos promocionales válidos.',
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const confirmationContent = {
|
|
69
|
+
// Neutral default copy. The launch flow must not claim a delivery, a shipping
|
|
70
|
+
// promise, or that an email was sent: the backend order may be pending, and
|
|
71
|
+
// no email is guaranteed. The email line only shows the contact the backend
|
|
72
|
+
// actually supplied (`{email}` is filled only when `order.email` is present).
|
|
73
|
+
titleLabel: 'Pedido registrado',
|
|
74
|
+
orderPlacedLabel: 'Pedido {number} · {date} · Correo de contacto: {email}',
|
|
75
|
+
orderPlacedNoEmailLabel: 'Pedido {number} · {date}',
|
|
76
|
+
itemsLabel: 'Comprobante del pedido',
|
|
77
|
+
// Stage descriptions are generic expectations, rendered only when the backend
|
|
78
|
+
// reports a fulfilment stage; `at: null` keeps them as expectations, not facts.
|
|
79
|
+
fulfilmentDetails: {
|
|
80
|
+
Confirmed: {
|
|
81
|
+
stage: 'Confirmed',
|
|
82
|
+
summary: 'Pedido registrado en el sistema.',
|
|
83
|
+
location: 'Tienda',
|
|
84
|
+
at: null,
|
|
85
|
+
},
|
|
86
|
+
Packed: {
|
|
87
|
+
stage: 'Packed',
|
|
88
|
+
summary: 'Preparación del pedido.',
|
|
89
|
+
location: 'Almacén',
|
|
90
|
+
at: null,
|
|
91
|
+
},
|
|
92
|
+
Shipped: {
|
|
93
|
+
stage: 'Shipped',
|
|
94
|
+
summary: 'Pedido despachado.',
|
|
95
|
+
location: 'En tránsito',
|
|
96
|
+
at: null,
|
|
97
|
+
},
|
|
98
|
+
Delivered: {
|
|
99
|
+
stage: 'Delivered',
|
|
100
|
+
summary: 'Pedido entregado.',
|
|
101
|
+
location: 'Destino',
|
|
102
|
+
at: null,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Thor Commerce view data: checkout
|
|
2
|
+
// User-owned loader shaping for the checkout view. Reconcile seeds this file once
|
|
3
|
+
// and never rewrites it; the derived view module (./index.tsx) imports these
|
|
4
|
+
// helpers and calls them from its loader, so edits here survive `storefront sync`
|
|
5
|
+
// and unit removal. Pure functions only: no React and no server imports.
|
|
6
|
+
|
|
7
|
+
export function buildCheckoutData(input: {
|
|
8
|
+
countries: readonly { code: string; name: string }[]
|
|
9
|
+
paymentMethods: readonly unknown[]
|
|
10
|
+
source: 'mock' | 'medusa'
|
|
11
|
+
}) {
|
|
12
|
+
return {
|
|
13
|
+
checkoutCountries: input.countries.map((country) => ({
|
|
14
|
+
value: country.code,
|
|
15
|
+
label: country.name,
|
|
16
|
+
})),
|
|
17
|
+
checkoutPaymentMethods: input.paymentMethods,
|
|
18
|
+
// The explicit data source: a configured mock is labelled mock; a real-mode
|
|
19
|
+
// failure is surfaced separately through `dataError`, never hidden.
|
|
20
|
+
dataSource: input.source,
|
|
21
|
+
dataError: null,
|
|
22
|
+
}
|
|
23
|
+
}
|