@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,19 @@
|
|
|
1
|
+
// Optional section fragment: cta (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the Cta/CtaSection1Props block contracts, shared by the server
|
|
3
|
+
// catalog (cta.tsx) and the client registry (cta.client.tsx). The React-only
|
|
4
|
+
// callbacks (onPrimaryClick/onSecondaryClick) are intentionally excluded.
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
export const ctaSchema = z.object({
|
|
9
|
+
label: z.string(),
|
|
10
|
+
href: z.string().optional(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const ctaSection1PropsSchema = z.object({
|
|
14
|
+
badgeText: z.string().optional(),
|
|
15
|
+
title: z.string().optional(),
|
|
16
|
+
description: z.string().optional(),
|
|
17
|
+
primaryCta: ctaSchema.optional(),
|
|
18
|
+
secondaryCta: ctaSchema.optional(),
|
|
19
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Optional section fragment: faq (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the FaqItem/FaqBlock1Props block contracts, shared by the server
|
|
3
|
+
// catalog (faq.tsx) and the client registry (faq.client.tsx).
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
|
|
7
|
+
export const faqItemSchema = z.object({
|
|
8
|
+
value: z.string(),
|
|
9
|
+
question: z.string(),
|
|
10
|
+
answer: z.string(),
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const faqPropsSchema = z.object({
|
|
14
|
+
title: z.string().optional(),
|
|
15
|
+
description: z.string().optional(),
|
|
16
|
+
items: z.array(faqItemSchema),
|
|
17
|
+
accordionType: z.enum(['single', 'multiple']).optional(),
|
|
18
|
+
})
|
|
@@ -2,28 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
import { Statistic1 as StatisticBlock } from '@thorprovider/blocks'
|
|
4
4
|
import { z } from 'zod'
|
|
5
|
+
import { resolveIcon } from '../../icon-map'
|
|
6
|
+
|
|
7
|
+
// Mirrors the StatisticItem/Statistic1Props block contracts. The block expects
|
|
8
|
+
// `icon` as a Lucide component; specs ship the icon NAME, resolved here.
|
|
9
|
+
const statisticItemSchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
title: z.string(),
|
|
12
|
+
value: z.string(),
|
|
13
|
+
description: z.string(),
|
|
14
|
+
progress: z.number(),
|
|
15
|
+
progressLabel: z.string(),
|
|
16
|
+
icon: z.string().optional(),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const statisticsPropsSchema = z.object({
|
|
20
|
+
title: z.string().optional(),
|
|
21
|
+
badge: z.string().optional(),
|
|
22
|
+
description: z.string().optional(),
|
|
23
|
+
items: z.array(statisticItemSchema),
|
|
24
|
+
})
|
|
5
25
|
|
|
6
26
|
export const statisticsSection = {
|
|
7
27
|
elementType: 'Statistics',
|
|
8
28
|
catalog: {
|
|
9
29
|
Statistics: {
|
|
10
|
-
props:
|
|
11
|
-
title: z.string().optional(),
|
|
12
|
-
badge: z.string().optional(),
|
|
13
|
-
description: z.string().optional(),
|
|
14
|
-
items: z.array(z.record(z.string(), z.unknown())),
|
|
15
|
-
}),
|
|
30
|
+
props: statisticsPropsSchema,
|
|
16
31
|
description: 'Key metrics or trust statistics.',
|
|
17
32
|
},
|
|
18
33
|
},
|
|
19
34
|
registry: {
|
|
20
|
-
Statistics: ({ props }: { props: Record<string, unknown> }) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
Statistics: ({ props }: { props: Record<string, unknown> }) => {
|
|
36
|
+
const parsed = statisticsPropsSchema.parse(props)
|
|
37
|
+
return (
|
|
38
|
+
<StatisticBlock
|
|
39
|
+
badge={parsed.badge}
|
|
40
|
+
title={parsed.title ?? 'Números que nos respaldan'}
|
|
41
|
+
description={parsed.description}
|
|
42
|
+
items={parsed.items.map((item) => ({
|
|
43
|
+
...item,
|
|
44
|
+
icon: resolveIcon(item.icon),
|
|
45
|
+
}))}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
},
|
|
28
49
|
},
|
|
29
50
|
}
|
|
@@ -3,26 +3,44 @@
|
|
|
3
3
|
import { Testimonial1 as TestimonialBlock } from '@thorprovider/blocks'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
|
|
6
|
+
// Mirrors the Testimonial1Item/Testimonial1Props block contracts.
|
|
7
|
+
const testimonialItemSchema = z.object({
|
|
8
|
+
id: z.string(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
role: z.string(),
|
|
11
|
+
avatar: z.string(),
|
|
12
|
+
content: z.string(),
|
|
13
|
+
rating: z.number(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const testimonialsPropsSchema = z.object({
|
|
17
|
+
title: z.string().optional(),
|
|
18
|
+
prevLabel: z.string().optional(),
|
|
19
|
+
nextLabel: z.string().optional(),
|
|
20
|
+
slideAriaLabel: z.string().optional(),
|
|
21
|
+
items: z.array(testimonialItemSchema),
|
|
22
|
+
})
|
|
23
|
+
|
|
6
24
|
export const testimonialsSection = {
|
|
7
25
|
elementType: 'Testimonials',
|
|
8
26
|
catalog: {
|
|
9
27
|
Testimonials: {
|
|
10
|
-
props:
|
|
11
|
-
title: z.string().optional(),
|
|
12
|
-
items: z.array(z.record(z.string(), z.unknown())),
|
|
13
|
-
}),
|
|
28
|
+
props: testimonialsPropsSchema,
|
|
14
29
|
description: 'Customer testimonials or social proof.',
|
|
15
30
|
},
|
|
16
31
|
},
|
|
17
32
|
registry: {
|
|
18
|
-
Testimonials: ({ props }: { props: Record<string, unknown> }) =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
Testimonials: ({ props }: { props: Record<string, unknown> }) => {
|
|
34
|
+
const parsed = testimonialsPropsSchema.parse(props)
|
|
35
|
+
return (
|
|
36
|
+
<TestimonialBlock
|
|
37
|
+
title={parsed.title ?? 'Lo que dicen nuestros clientes'}
|
|
38
|
+
items={parsed.items}
|
|
39
|
+
prevLabel={parsed.prevLabel ?? 'Anterior'}
|
|
40
|
+
nextLabel={parsed.nextLabel ?? 'Siguiente'}
|
|
41
|
+
slideAriaLabel={parsed.slideAriaLabel ?? 'Testimonio'}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
},
|
|
27
45
|
},
|
|
28
46
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ReactNode } from 'react'
|
|
7
7
|
import type { BaseComponentProps } from '@json-render/react'
|
|
8
|
-
import type {
|
|
8
|
+
import type { StorefrontLoaderFn } from '@thorprovider/runtime/render-core/app'
|
|
9
9
|
import type { ZodType } from 'zod'
|
|
10
10
|
import type { StorefrontElement } from '../spec/types'
|
|
11
11
|
|
|
@@ -41,6 +41,8 @@ export interface ViewModule {
|
|
|
41
41
|
elements: Record<string, StorefrontElement>
|
|
42
42
|
catalog: Record<string, ViewCatalogEntry>
|
|
43
43
|
registry: ViewRegistry
|
|
44
|
-
route
|
|
45
|
-
|
|
44
|
+
// A view may contribute one route or several (a content-driven view declares
|
|
45
|
+
// one fixed route per page). The spec barrel normalises and flattens them.
|
|
46
|
+
route: ViewRoute | ViewRoute[]
|
|
47
|
+
loader: Record<string, StorefrontLoaderFn>
|
|
46
48
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Generated by Thor Commerce storefront (feature: checkout — handlers)
|
|
4
|
+
// The checkout capability's action handlers. Installed only when the feature is
|
|
5
|
+
// in the manifest; the handlers composer calls this hook. Every mutation goes
|
|
6
|
+
// through a Server Action; the backend stays authoritative for totals.
|
|
7
|
+
|
|
8
|
+
import { useEffect } from 'react'
|
|
9
|
+
import { useActions, useStateStore } from '@json-render/react'
|
|
10
|
+
import { useNextApp } from '@json-render/next'
|
|
11
|
+
import {
|
|
12
|
+
placeOrderAction,
|
|
13
|
+
reconcileReceiptAction,
|
|
14
|
+
selectShippingMethodAction,
|
|
15
|
+
syncCheckoutAddressAction,
|
|
16
|
+
} from '../../actions'
|
|
17
|
+
import type { CartStatePatch, PlaceOrderResult } from '../../commerce'
|
|
18
|
+
|
|
19
|
+
// The always-required core of an address. Optional fields (province, phone,
|
|
20
|
+
// postal code) are left to the runtime validator so the host does not impose a
|
|
21
|
+
// stricter contract than the backend; the validator returns structured errors.
|
|
22
|
+
const REQUIRED_ADDRESS_FIELDS = ['email', 'firstName', 'lastName', 'city', 'country'] as const
|
|
23
|
+
|
|
24
|
+
function addressComplete(values: Record<string, unknown>): boolean {
|
|
25
|
+
return REQUIRED_ADDRESS_FIELDS.every(
|
|
26
|
+
(field) => typeof values[field] === 'string' && (values[field] as string).trim() !== '',
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useCheckoutHandlers() {
|
|
31
|
+
const { get, set } = useStateStore()
|
|
32
|
+
const { navigate } = useNextApp()
|
|
33
|
+
const { registerHandler } = useActions()
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const applyCart = (patch: CartStatePatch) => {
|
|
37
|
+
set('/cart', patch.cart)
|
|
38
|
+
set('/cartTotals', patch.cartTotals)
|
|
39
|
+
set('/cartId', patch.cartId)
|
|
40
|
+
set('/cartQuantity', patch.cartQuantity)
|
|
41
|
+
set('/cartCompleted', patch.cartCompleted)
|
|
42
|
+
set('/cartSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
|
|
43
|
+
// Rehydrate the checkout selection from the backend cart so a reload or
|
|
44
|
+
// an applyCart restores the highlighted shipping option.
|
|
45
|
+
set('/checkoutSelectedShippingOptionId', patch.cartSelectedShippingOptionId)
|
|
46
|
+
set('/cartReadError', null)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const applyResult = (result: PlaceOrderResult) => {
|
|
50
|
+
set('/checkoutPending', false)
|
|
51
|
+
if (result.status === 'order') {
|
|
52
|
+
// ONE atomic write carries the order and its backend lines.
|
|
53
|
+
set('/lastReceipt', { order: result.order, lines: result.lines })
|
|
54
|
+
set('/orderStatus', 'complete')
|
|
55
|
+
set('/checkoutError', null)
|
|
56
|
+
set('/checkoutErrorKind', null)
|
|
57
|
+
set('/cartNotice', null)
|
|
58
|
+
// The active cart is cleared by the session action; mirror it in state.
|
|
59
|
+
set('/cart', [])
|
|
60
|
+
set('/cartTotals', null)
|
|
61
|
+
set('/cartId', null)
|
|
62
|
+
set('/cartQuantity', 0)
|
|
63
|
+
set('/cartCompleted', true)
|
|
64
|
+
set('/checkoutSelectedShippingOptionId', null)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
// error | cart-error | unresolved. `orderStatus` only carries the UI
|
|
68
|
+
// states; the native discriminator is preserved in `checkoutErrorKind`.
|
|
69
|
+
// The active cart is NEVER cleared here, so a cart-with-error keeps its
|
|
70
|
+
// inputs and lines.
|
|
71
|
+
set('/orderStatus', result.status === 'cart-error' ? 'error' : result.status)
|
|
72
|
+
set('/checkoutErrorKind', result.status)
|
|
73
|
+
set('/checkoutError', result.message)
|
|
74
|
+
set(
|
|
75
|
+
'/cartNotice',
|
|
76
|
+
result.status === 'unresolved'
|
|
77
|
+
? 'El pedido pudo haberse registrado. Usa "Reconciliar" para recuperar el comprobante; no lo reenvíes.'
|
|
78
|
+
: null,
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A rejected Server Action (network/RPC) must clear pending and surface a
|
|
83
|
+
// message; never swallow it. Only a non-secret context line is logged.
|
|
84
|
+
const onError = (scope: string) => (error: unknown) => {
|
|
85
|
+
console.error(
|
|
86
|
+
`[storefront/checkout] ${scope} failed`,
|
|
87
|
+
error instanceof Error ? error.message : error,
|
|
88
|
+
)
|
|
89
|
+
set('/checkoutPending', false)
|
|
90
|
+
set('/checkoutError', 'No se pudo completar la operación. Inténtalo de nuevo.')
|
|
91
|
+
set('/checkoutErrorKind', 'error')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handlers: Record<string, () => void> = {
|
|
95
|
+
goToCheckout: () => {
|
|
96
|
+
set('/cartOpen', false)
|
|
97
|
+
navigate('/checkout')
|
|
98
|
+
},
|
|
99
|
+
continueShopping: () => navigate('/'),
|
|
100
|
+
placeOrder: () => {
|
|
101
|
+
set('/checkoutPending', true)
|
|
102
|
+
set('/checkoutError', null)
|
|
103
|
+
set('/cartNotice', null)
|
|
104
|
+
void placeOrderAction().then(applyResult).catch(onError('placeOrder'))
|
|
105
|
+
},
|
|
106
|
+
reconcileOrder: () => {
|
|
107
|
+
set('/checkoutPending', true)
|
|
108
|
+
set('/checkoutError', null)
|
|
109
|
+
void reconcileReceiptAction().then(applyResult).catch(onError('reconcileOrder'))
|
|
110
|
+
},
|
|
111
|
+
syncCheckoutAddress: () => {
|
|
112
|
+
const values = (get('/checkout') ?? {}) as Record<string, unknown>
|
|
113
|
+
if (!addressComplete(values)) return
|
|
114
|
+
set('/checkoutPending', true)
|
|
115
|
+
void syncCheckoutAddressAction(values)
|
|
116
|
+
.then((result) => {
|
|
117
|
+
set('/checkoutPending', false)
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
applyCart(result.cart)
|
|
120
|
+
if (result.shippingOptions) set('/checkoutShippingOptions', result.shippingOptions)
|
|
121
|
+
set('/checkoutFieldErrors', result.fieldErrors ?? {})
|
|
122
|
+
set('/checkoutError', null)
|
|
123
|
+
} else {
|
|
124
|
+
set('/checkoutError', result.error)
|
|
125
|
+
set('/checkoutFieldErrors', result.fieldErrors ?? {})
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
.catch(onError('syncCheckoutAddress'))
|
|
129
|
+
},
|
|
130
|
+
selectShippingMethod: () => {
|
|
131
|
+
const methodId = get('/checkoutSelectedShippingOptionId') as string | null
|
|
132
|
+
if (!methodId) return
|
|
133
|
+
set('/checkoutPending', true)
|
|
134
|
+
void selectShippingMethodAction({ methodId })
|
|
135
|
+
.then((result) => {
|
|
136
|
+
set('/checkoutPending', false)
|
|
137
|
+
if (result.ok) {
|
|
138
|
+
applyCart(result.cart)
|
|
139
|
+
set('/checkoutError', null)
|
|
140
|
+
} else {
|
|
141
|
+
set('/checkoutError', result.error)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
.catch(onError('selectShippingMethod'))
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
149
|
+
registerHandler(name, handler)
|
|
150
|
+
}
|
|
151
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
152
|
+
}, [])
|
|
153
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (feature: checkout — state slice)
|
|
2
|
+
// The checkout capability's state. Installed only when the feature is in the
|
|
3
|
+
// manifest; the state composer spreads it over the core state.
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CheckoutFieldErrors,
|
|
7
|
+
CheckoutFormData,
|
|
8
|
+
CheckoutShippingOption,
|
|
9
|
+
OrderStatus,
|
|
10
|
+
StorefrontReceipt,
|
|
11
|
+
} from '../../state.core'
|
|
12
|
+
import type { PaymentMethod } from '@thorprovider/types'
|
|
13
|
+
|
|
14
|
+
const checkoutFormDefaults: CheckoutFormData = {
|
|
15
|
+
email: '',
|
|
16
|
+
firstName: '',
|
|
17
|
+
lastName: '',
|
|
18
|
+
phone: '',
|
|
19
|
+
address: '',
|
|
20
|
+
city: '',
|
|
21
|
+
state: '',
|
|
22
|
+
zipCode: '',
|
|
23
|
+
country: '',
|
|
24
|
+
paymentMethod: '',
|
|
25
|
+
cardNumber: '',
|
|
26
|
+
expiryDate: '',
|
|
27
|
+
cvv: '',
|
|
28
|
+
cardName: '',
|
|
29
|
+
saveInfo: false,
|
|
30
|
+
sameAsBilling: false,
|
|
31
|
+
newsletter: false,
|
|
32
|
+
promoCode: '',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CheckoutState {
|
|
36
|
+
checkout: CheckoutFormData
|
|
37
|
+
checkoutCountries: { value: string; label: string }[]
|
|
38
|
+
checkoutPaymentMethods: PaymentMethod[]
|
|
39
|
+
checkoutShippingOptions: CheckoutShippingOption[]
|
|
40
|
+
checkoutSelectedShippingOptionId: string | null
|
|
41
|
+
checkoutFieldErrors: CheckoutFieldErrors
|
|
42
|
+
checkoutPending: boolean
|
|
43
|
+
checkoutError: string | null
|
|
44
|
+
checkoutErrorKind: 'error' | 'cart-error' | 'unresolved' | null
|
|
45
|
+
lastReceipt: StorefrontReceipt | null
|
|
46
|
+
orderStatus: OrderStatus
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const checkoutState: CheckoutState = {
|
|
50
|
+
checkout: checkoutFormDefaults,
|
|
51
|
+
checkoutCountries: [],
|
|
52
|
+
checkoutPaymentMethods: [],
|
|
53
|
+
checkoutShippingOptions: [],
|
|
54
|
+
checkoutSelectedShippingOptionId: null,
|
|
55
|
+
checkoutFieldErrors: {},
|
|
56
|
+
checkoutPending: false,
|
|
57
|
+
checkoutError: null,
|
|
58
|
+
checkoutErrorKind: null,
|
|
59
|
+
lastReceipt: null,
|
|
60
|
+
orderStatus: 'idle',
|
|
61
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Generated by Thor Commerce storefront (feature: customer-auth — handlers)
|
|
4
|
+
// The customer-auth capability's action handlers. Installed only when the
|
|
5
|
+
// feature is in the manifest; the handlers composer calls this hook. Login and
|
|
6
|
+
// registration are invoked directly by the view registry (their credential
|
|
7
|
+
// payload must never enter the state store); these handlers own the non-secret
|
|
8
|
+
// state transitions: the sign-in / sign-up toggle and logout.
|
|
9
|
+
|
|
10
|
+
import { useEffect } from 'react'
|
|
11
|
+
import { useActions, useStateStore } from '@json-render/react'
|
|
12
|
+
import { logoutAction } from '../../actions'
|
|
13
|
+
|
|
14
|
+
export function useCustomerAuthHandlers() {
|
|
15
|
+
const { set } = useStateStore()
|
|
16
|
+
const { registerHandler } = useActions()
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const handlers: Record<string, () => void> = {
|
|
20
|
+
showRegister: () => {
|
|
21
|
+
set('/accountError', null)
|
|
22
|
+
set('/accountMode', 'register')
|
|
23
|
+
},
|
|
24
|
+
showLogin: () => {
|
|
25
|
+
set('/accountError', null)
|
|
26
|
+
set('/accountMode', 'login')
|
|
27
|
+
},
|
|
28
|
+
logout: () => {
|
|
29
|
+
set('/accountPending', true)
|
|
30
|
+
set('/accountError', null)
|
|
31
|
+
void logoutAction()
|
|
32
|
+
.then((result) => {
|
|
33
|
+
set('/accountPending', false)
|
|
34
|
+
if (result.ok) {
|
|
35
|
+
set('/accountCustomer', null)
|
|
36
|
+
set('/accountAuthenticated', false)
|
|
37
|
+
set('/accountMode', 'login')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
set('/accountError', result.error)
|
|
41
|
+
})
|
|
42
|
+
.catch((error) => {
|
|
43
|
+
// Never swallow: log a non-secret context line and surface a message.
|
|
44
|
+
console.error(
|
|
45
|
+
'[storefront/account] logout failed',
|
|
46
|
+
error instanceof Error ? error.message : error,
|
|
47
|
+
)
|
|
48
|
+
set('/accountPending', false)
|
|
49
|
+
set('/accountError', 'No se pudo cerrar la sesión. Inténtalo de nuevo.')
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
55
|
+
registerHandler(name, handler)
|
|
56
|
+
}
|
|
57
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
+
}, [])
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (feature: customer-auth — state slice)
|
|
2
|
+
// The customer-auth capability's state. Installed only when the feature is in
|
|
3
|
+
// the manifest; the state composer spreads it over the core state.
|
|
4
|
+
|
|
5
|
+
import type { Customer } from '@thorprovider/types'
|
|
6
|
+
import type { AccountOrderView } from '../../state.core'
|
|
7
|
+
|
|
8
|
+
export interface CustomerAuthState {
|
|
9
|
+
/** The resolved customer, or null when unauthenticated. Never a token. */
|
|
10
|
+
accountCustomer: Customer | null
|
|
11
|
+
accountAuthenticated: boolean
|
|
12
|
+
/** Which auth surface is mounted: the sign-in page or the registration page. */
|
|
13
|
+
accountMode: 'login' | 'register'
|
|
14
|
+
accountPending: boolean
|
|
15
|
+
accountError: string | null
|
|
16
|
+
/** The customer's fetched order-history rows; empty until (or without) a load. */
|
|
17
|
+
accountOrders: AccountOrderView[]
|
|
18
|
+
/** True while the order history is being loaded. */
|
|
19
|
+
accountOrdersPending: boolean
|
|
20
|
+
/** The order-history load failure, or null when it loaded. */
|
|
21
|
+
accountOrdersError: string | null
|
|
22
|
+
/** The order number selected for the inline detail; null when none is open. */
|
|
23
|
+
accountSelectedOrderId: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const customerAuthState: CustomerAuthState = {
|
|
27
|
+
accountCustomer: null,
|
|
28
|
+
accountAuthenticated: false,
|
|
29
|
+
accountMode: 'login',
|
|
30
|
+
accountPending: false,
|
|
31
|
+
accountError: null,
|
|
32
|
+
accountOrders: [],
|
|
33
|
+
accountOrdersPending: false,
|
|
34
|
+
accountOrdersError: null,
|
|
35
|
+
accountSelectedOrderId: null,
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (feature: recommendations — state slice)
|
|
2
|
+
// The recommendations capability's state. Installed only when the feature is in
|
|
3
|
+
// the manifest; the state composer spreads it over the core state.
|
|
4
|
+
|
|
5
|
+
export interface RecommendationsState {
|
|
6
|
+
recommendedProducts: unknown[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const recommendationsState: RecommendationsState = {
|
|
10
|
+
recommendedProducts: [],
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Thor Commerce view content: account
|
|
2
|
+
// User-owned view content — account copy for the sign-in and registration
|
|
3
|
+
// surfaces. Reconcile seeds this file once and never rewrites it; the derived
|
|
4
|
+
// view module (./index.tsx) imports it and wires it into the element tree, so
|
|
5
|
+
// your edits here survive `storefront sync` and unit removal. Plain data only:
|
|
6
|
+
// no React and no server imports.
|
|
7
|
+
|
|
8
|
+
export const accountContent = {
|
|
9
|
+
login: {
|
|
10
|
+
logoText: '__SITE_NAME__',
|
|
11
|
+
welcomeTitleLabel: 'Bienvenido de nuevo',
|
|
12
|
+
welcomeDescriptionLabel: 'Inicia sesión para gestionar tu cuenta.',
|
|
13
|
+
emailLabel: 'Correo electrónico',
|
|
14
|
+
passwordLabel: 'Contraseña',
|
|
15
|
+
signInLabel: 'Iniciar sesión',
|
|
16
|
+
createAccountLabel: 'Crea una cuenta',
|
|
17
|
+
},
|
|
18
|
+
register: {
|
|
19
|
+
logoText: '__SITE_NAME__',
|
|
20
|
+
createAccountTitleLabel: 'Crea tu cuenta',
|
|
21
|
+
createAccountDescriptionLabel: 'Regístrate para gestionar tus pedidos.',
|
|
22
|
+
fullNameLabel: 'Nombre completo',
|
|
23
|
+
emailLabel: 'Correo electrónico',
|
|
24
|
+
passwordLabel: 'Contraseña',
|
|
25
|
+
createAccountLabel: 'Crear cuenta',
|
|
26
|
+
signInLabel: 'Inicia sesión',
|
|
27
|
+
},
|
|
28
|
+
orders: {
|
|
29
|
+
titleLabel: 'Mis pedidos',
|
|
30
|
+
descriptionLabel: 'Consulta tus pedidos anteriores',
|
|
31
|
+
emptyLabel: 'Aún no tienes pedidos.',
|
|
32
|
+
errorLabel: 'No se pudieron cargar tus pedidos.',
|
|
33
|
+
loadingLabel: 'Cargando pedidos…',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Thor Commerce view module: account
|
|
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 -> "account".
|
|
6
|
+
//
|
|
7
|
+
// The initial render does NOT read the auth cookie here (this module is part of
|
|
8
|
+
// the client catalog graph). The catch-all page mounts the route and the
|
|
9
|
+
// headless `AccountBoot` resolves the current customer through the
|
|
10
|
+
// cookie-authorized Server Action, so the token never reaches this module.
|
|
11
|
+
|
|
12
|
+
import { getDataSource } from '../../store-context'
|
|
13
|
+
import { pageRoot } from '../../spec/page-root'
|
|
14
|
+
import type { StorefrontElement } from '../../spec/types'
|
|
15
|
+
import type { ViewModule } from '../types'
|
|
16
|
+
import { registry } from './registry'
|
|
17
|
+
import { accountContent } from './content'
|
|
18
|
+
import {
|
|
19
|
+
accountEmptyPropsSchema,
|
|
20
|
+
accountLoginPropsSchema,
|
|
21
|
+
accountOrdersPropsSchema,
|
|
22
|
+
accountRegisterPropsSchema,
|
|
23
|
+
} from './schema'
|
|
24
|
+
|
|
25
|
+
// Four surfaces: bootstrap (headless), sign-in, registration, and the signed-in
|
|
26
|
+
// panel. The sign-in and registration surfaces are toggled by the `/accountMode`
|
|
27
|
+
// state; the panel shows only when `/accountAuthenticated` is true.
|
|
28
|
+
const elements: Record<string, StorefrontElement> = {
|
|
29
|
+
'account.boot': {
|
|
30
|
+
type: 'AccountBoot',
|
|
31
|
+
props: {},
|
|
32
|
+
},
|
|
33
|
+
'account.login': {
|
|
34
|
+
type: 'AccountLogin',
|
|
35
|
+
visible: {
|
|
36
|
+
$and: [
|
|
37
|
+
{ $state: '/accountAuthenticated', not: true },
|
|
38
|
+
{ $state: '/accountMode', eq: 'login' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
props: accountContent.login,
|
|
42
|
+
on: { showRegister: { action: 'showRegister' } },
|
|
43
|
+
},
|
|
44
|
+
'account.register': {
|
|
45
|
+
type: 'AccountRegister',
|
|
46
|
+
visible: {
|
|
47
|
+
$and: [
|
|
48
|
+
{ $state: '/accountAuthenticated', not: true },
|
|
49
|
+
{ $state: '/accountMode', eq: 'register' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
props: accountContent.register,
|
|
53
|
+
on: { showLogin: { action: 'showLogin' } },
|
|
54
|
+
},
|
|
55
|
+
'account.panel': {
|
|
56
|
+
type: 'AccountPanel',
|
|
57
|
+
visible: { $state: '/accountAuthenticated' },
|
|
58
|
+
props: {},
|
|
59
|
+
on: { logout: { action: 'logout' } },
|
|
60
|
+
},
|
|
61
|
+
'account.orders': {
|
|
62
|
+
type: 'AccountOrders',
|
|
63
|
+
visible: { $state: '/accountAuthenticated' },
|
|
64
|
+
props: accountContent.orders,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const catalog = {
|
|
69
|
+
AccountBoot: {
|
|
70
|
+
props: accountEmptyPropsSchema,
|
|
71
|
+
description: 'Headless account bootstrap: resolves the current customer from the auth cookie.',
|
|
72
|
+
},
|
|
73
|
+
AccountLogin: {
|
|
74
|
+
props: accountLoginPropsSchema,
|
|
75
|
+
description: 'Sign-in page with email and password and a link to create an account.',
|
|
76
|
+
},
|
|
77
|
+
AccountRegister: {
|
|
78
|
+
props: accountRegisterPropsSchema,
|
|
79
|
+
description: 'Registration page with name, email, and password and a link to sign in.',
|
|
80
|
+
},
|
|
81
|
+
AccountPanel: {
|
|
82
|
+
props: accountEmptyPropsSchema,
|
|
83
|
+
description: 'Minimal signed-in state with the customer name and a logout control.',
|
|
84
|
+
},
|
|
85
|
+
AccountOrders: {
|
|
86
|
+
props: accountOrdersPropsSchema,
|
|
87
|
+
description:
|
|
88
|
+
"Signed-in customer's order history rendered by OrderHistory2, with loading, empty, and error states and an inline order detail.",
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const account: ViewModule = {
|
|
93
|
+
name: 'account',
|
|
94
|
+
elements,
|
|
95
|
+
catalog,
|
|
96
|
+
registry,
|
|
97
|
+
route: {
|
|
98
|
+
path: '/cuenta',
|
|
99
|
+
loaderName: 'account',
|
|
100
|
+
layout: 'default',
|
|
101
|
+
metadata: {
|
|
102
|
+
title: 'Mi cuenta',
|
|
103
|
+
description: 'Inicia sesión o crea una cuenta para gestionar tu cuenta.',
|
|
104
|
+
},
|
|
105
|
+
entry: pageRoot(
|
|
106
|
+
'account',
|
|
107
|
+
['account.boot', 'account.login', 'account.register', 'account.panel', 'account.orders'],
|
|
108
|
+
elements,
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
loader: {
|
|
112
|
+
account: async () => ({ dataSource: getDataSource(), dataError: null }),
|
|
113
|
+
},
|
|
114
|
+
}
|