@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,169 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (composition core: session host).
|
|
2
|
+
//
|
|
3
|
+
// Host session cookie — the Next-bound half of the guest session. `./session.core`
|
|
4
|
+
// owns the pure signing and validation; this module owns the cookie.
|
|
5
|
+
//
|
|
6
|
+
// Next.js 16 `cookies()` is async and may only be MUTATED from a Server Action
|
|
7
|
+
// or a Route Handler — never during a Server Component render. `readHostSession`
|
|
8
|
+
// is therefore the only render-safe export; every other export writes or clears
|
|
9
|
+
// the cookie and MUST be called from an action or route. This is why a guest
|
|
10
|
+
// identity is created in an action/route bootstrap, never in a read-only render.
|
|
11
|
+
//
|
|
12
|
+
// The signing secret is required host configuration (`THOR_SESSION_SECRET`),
|
|
13
|
+
// independent of the backend. It is never embedded, generated as a constant, or
|
|
14
|
+
// printed; a missing or too-short secret throws before any cookie is written.
|
|
15
|
+
// No external SDK is called here; commerce data stays behind adapters/runtime.
|
|
16
|
+
|
|
17
|
+
import { cookies } from 'next/headers'
|
|
18
|
+
import type { StoreContext } from '@thorprovider/types'
|
|
19
|
+
import {
|
|
20
|
+
AUTH_TOKEN_TTL_SECONDS,
|
|
21
|
+
SESSION_TTL_SECONDS,
|
|
22
|
+
authCookieName,
|
|
23
|
+
bindSessionCart,
|
|
24
|
+
completeSessionCart,
|
|
25
|
+
createHostAuth,
|
|
26
|
+
createHostSession,
|
|
27
|
+
decodeHostAuth,
|
|
28
|
+
decodeHostSession,
|
|
29
|
+
encodeHostAuth,
|
|
30
|
+
encodeHostSession,
|
|
31
|
+
sessionCookieName,
|
|
32
|
+
type HostSessionPayload,
|
|
33
|
+
} from './session.core'
|
|
34
|
+
|
|
35
|
+
/** Host environment variable that carries the cookie signing secret. */
|
|
36
|
+
export const SESSION_SECRET_ENV = 'THOR_SESSION_SECRET'
|
|
37
|
+
|
|
38
|
+
/** Read the required host signing secret; throws (loud) when unset. */
|
|
39
|
+
export function getSessionSecret(): string {
|
|
40
|
+
const secret = process.env[SESSION_SECRET_ENV]
|
|
41
|
+
if (!secret || secret.trim() === '') {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${SESSION_SECRET_ENV} is required to sign the storefront session cookie; set it in the host environment (never commit or log it).`,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return secret
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Hardened cookie flags: httpOnly, SameSite=Lax, secure in production, TTL. */
|
|
50
|
+
function cookieOptions() {
|
|
51
|
+
return {
|
|
52
|
+
httpOnly: true,
|
|
53
|
+
sameSite: 'lax' as const,
|
|
54
|
+
secure: process.env.NODE_ENV === 'production',
|
|
55
|
+
path: '/',
|
|
56
|
+
maxAge: SESSION_TTL_SECONDS,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* READ-ONLY — safe during a Server Component render.
|
|
62
|
+
*
|
|
63
|
+
* Returns the verified session, or null for a missing/tampered/expired/wrong-store
|
|
64
|
+
* cookie. A null result means "no trusted identity"; the caller must obtain one
|
|
65
|
+
* from a Server Action or Route Handler, never from client-supplied ids.
|
|
66
|
+
*/
|
|
67
|
+
export async function readHostSession(
|
|
68
|
+
context: StoreContext,
|
|
69
|
+
): Promise<HostSessionPayload | null> {
|
|
70
|
+
const store = await cookies()
|
|
71
|
+
const raw = store.get(sessionCookieName(context.storeId))?.value
|
|
72
|
+
return decodeHostSession(raw, getSessionSecret(), { expectedContext: context })
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** MUTATION — Server Action or Route Handler only. Issues a fresh identity. */
|
|
76
|
+
export async function issueHostSession(context: StoreContext): Promise<HostSessionPayload> {
|
|
77
|
+
const payload = createHostSession(context)
|
|
78
|
+
await writeHostSession(context, payload)
|
|
79
|
+
return payload
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* MUTATION — Server Action or Route Handler only. Attaches the active backend
|
|
84
|
+
* cart to the current identity, issuing one first if the cookie is absent or
|
|
85
|
+
* untrusted (regenerate instead of trusting a client id).
|
|
86
|
+
*/
|
|
87
|
+
export async function bindHostSessionCart(
|
|
88
|
+
context: StoreContext,
|
|
89
|
+
cartId: string,
|
|
90
|
+
): Promise<HostSessionPayload> {
|
|
91
|
+
const current = await readHostSession(context)
|
|
92
|
+
const next = bindSessionCart(current ?? createHostSession(context), cartId)
|
|
93
|
+
await writeHostSession(context, next)
|
|
94
|
+
return next
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* MUTATION — Server Action or Route Handler only.
|
|
99
|
+
*
|
|
100
|
+
* Clears the active cart ONLY on a confirmed order and preserves `receiptCartId`
|
|
101
|
+
* for recovery. `receiptCartId` must come from the host's own completed cart,
|
|
102
|
+
* never from client input. On any error or unresolved completion the caller
|
|
103
|
+
* must NOT call this, so the active cart is retained and no receipt is recorded.
|
|
104
|
+
*/
|
|
105
|
+
export async function completeHostSessionCart(
|
|
106
|
+
context: StoreContext,
|
|
107
|
+
receiptCartId: string,
|
|
108
|
+
): Promise<HostSessionPayload> {
|
|
109
|
+
const current = await readHostSession(context)
|
|
110
|
+
const next = completeSessionCart(current ?? createHostSession(context), receiptCartId)
|
|
111
|
+
await writeHostSession(context, next)
|
|
112
|
+
return next
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** MUTATION — Server Action or Route Handler only. Clears the session cookie. */
|
|
116
|
+
export async function clearHostSession(context: StoreContext): Promise<void> {
|
|
117
|
+
const store = await cookies()
|
|
118
|
+
store.delete(sessionCookieName(context.storeId))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function writeHostSession(
|
|
122
|
+
context: StoreContext,
|
|
123
|
+
payload: HostSessionPayload,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
const store = await cookies()
|
|
126
|
+
store.set(
|
|
127
|
+
sessionCookieName(context.storeId),
|
|
128
|
+
encodeHostSession(payload, getSessionSecret()),
|
|
129
|
+
cookieOptions(),
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Customer auth cookie (change: storefront-customer-auth, task 2.1)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
//
|
|
137
|
+
// The auth cookie is separate from the guest session cookie and reuses the same
|
|
138
|
+
// host-only secret and hardened flags. It carries only the customer JWT; the
|
|
139
|
+
// guest session payload/version and the guest cart are never read or written
|
|
140
|
+
// here. As with the guest session, only `readAuthToken` is render-safe.
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* READ-ONLY — safe during a Server Component render.
|
|
144
|
+
*
|
|
145
|
+
* Returns the verified customer token, or null for a missing/tampered/expired/
|
|
146
|
+
* wrong-store cookie. A null result means "not authenticated".
|
|
147
|
+
*/
|
|
148
|
+
export async function readAuthToken(context: StoreContext): Promise<string | null> {
|
|
149
|
+
const store = await cookies()
|
|
150
|
+
const raw = store.get(authCookieName(context.storeId))?.value
|
|
151
|
+
const payload = decodeHostAuth(raw, getSessionSecret(), { expectedStoreId: context.storeId })
|
|
152
|
+
return payload?.token ?? null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** MUTATION — Server Action or Route Handler only. Persists the customer token. */
|
|
156
|
+
export async function writeAuthToken(context: StoreContext, token: string): Promise<void> {
|
|
157
|
+
const store = await cookies()
|
|
158
|
+
store.set(
|
|
159
|
+
authCookieName(context.storeId),
|
|
160
|
+
encodeHostAuth(createHostAuth(context.storeId, token), getSessionSecret()),
|
|
161
|
+
{ ...cookieOptions(), maxAge: AUTH_TOKEN_TTL_SECONDS },
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** MUTATION — Server Action or Route Handler only. Clears the auth cookie. */
|
|
166
|
+
export async function clearAuthToken(context: StoreContext): Promise<void> {
|
|
167
|
+
const store = await cookies()
|
|
168
|
+
store.delete(authCookieName(context.storeId))
|
|
169
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Thor Commerce shell content: brand accents, surface classes, and footer copy.
|
|
2
|
+
//
|
|
3
|
+
// User-owned shell content — reconcile seeds this file once and never rewrites
|
|
4
|
+
// it, so your edits survive `storefront sync`. Plain data only: no React and no
|
|
5
|
+
// server imports.
|
|
6
|
+
//
|
|
7
|
+
// Neutral defaults only: no delivery, shipping, or promo promise is made here,
|
|
8
|
+
// and footer links point only at routes the generated storefront actually
|
|
9
|
+
// serves (`/`, `/categorias`, `/checkout`, and the static pages `/contacto`,
|
|
10
|
+
// `/sobre-nosotros`, `/terminos`, `/privacidad`). Brand colors and copy belong
|
|
11
|
+
// in your own content, never in the package.
|
|
12
|
+
//
|
|
13
|
+
// The scaffold ships no newsletter submit callback, so the footer newsletter is
|
|
14
|
+
// omitted by default. A content-only `newsletter` is swallowed by the block
|
|
15
|
+
// (no form renders); wiring a real host callback to enable it is an external
|
|
16
|
+
// integration, out of scope for the scaffold.
|
|
17
|
+
|
|
18
|
+
export interface ShellAccent {
|
|
19
|
+
/** Trailing segment of the site-name wordmark to render with the accent. */
|
|
20
|
+
text: string
|
|
21
|
+
/** Consumer-supplied classes for the accent span (e.g. a brand color token). */
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ShellLink {
|
|
26
|
+
label: string
|
|
27
|
+
/** A route the storefront actually serves. Omit for a non-link label. */
|
|
28
|
+
href?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ShellLinkColumn {
|
|
32
|
+
title: string
|
|
33
|
+
links: ShellLink[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ShellNewsletter {
|
|
37
|
+
placeholder?: string
|
|
38
|
+
buttonLabel: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Optional trailing accent for the navbar wordmark, e.g. the "Cuba" in
|
|
43
|
+
* "ElectroCuba". Leave `undefined` to render the wordmark in one colour. The
|
|
44
|
+
* accent only applies when `text` is a trailing segment of the site name.
|
|
45
|
+
*/
|
|
46
|
+
export const logoAccent: ShellAccent | undefined = undefined
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Optional trailing accent for the footer brand name. Same trailing-segment
|
|
50
|
+
* rule as `logoAccent`.
|
|
51
|
+
*/
|
|
52
|
+
export const brandAccent: ShellAccent | undefined = undefined
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Optional classes merged onto the navbar's outer surface (e.g.
|
|
56
|
+
* `bg-brand-nav text-brand-nav-foreground`). Leave `undefined` for the default.
|
|
57
|
+
*/
|
|
58
|
+
export const navbarClassName: string | undefined = undefined
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Optional classes merged onto the footer's outer surface. Leave `undefined`
|
|
62
|
+
* for the default.
|
|
63
|
+
*/
|
|
64
|
+
export const footerClassName: string | undefined = undefined
|
|
65
|
+
|
|
66
|
+
/** Neutral footer brand description. No delivery/shipping promise. */
|
|
67
|
+
export const brandDescription: string =
|
|
68
|
+
'Catálogo en línea de productos para tu hogar y tu negocio.'
|
|
69
|
+
|
|
70
|
+
/** Neutral footer links — only routes the storefront serves; no dead `#` links. */
|
|
71
|
+
export const linkColumns: ShellLinkColumn[] = [
|
|
72
|
+
{
|
|
73
|
+
title: 'Comprar',
|
|
74
|
+
links: [
|
|
75
|
+
{ label: 'Inicio', href: '/' },
|
|
76
|
+
{ label: 'Todas las categorías', href: '/categorias' },
|
|
77
|
+
{ label: 'Finalizar compra', href: '/checkout' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: 'Información',
|
|
82
|
+
links: [
|
|
83
|
+
{ label: 'Contacto', href: '/contacto' },
|
|
84
|
+
{ label: 'Sobre nosotros', href: '/sobre-nosotros' },
|
|
85
|
+
{ label: 'Términos y condiciones', href: '/terminos' },
|
|
86
|
+
{ label: 'Política de privacidad', href: '/privacidad' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Footer newsletter content. The scaffold has no working submit handler, so it
|
|
93
|
+
* is omitted by default (`false`). Setting it without a host-provided callback
|
|
94
|
+
* renders no form — never a fake one.
|
|
95
|
+
*/
|
|
96
|
+
export const newsletter: false | ShellNewsletter = false
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Thor Commerce shell props: the shell chrome element props composed from the
|
|
2
|
+
// user-owned shell content. Reconcile seeds this file once and never rewrites
|
|
3
|
+
// it; the derived spec (../spec/index.ts) imports these objects and binds them
|
|
4
|
+
// into the shell element tree, so edits here survive `storefront sync`.
|
|
5
|
+
// Plain data only: no React and no server imports.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
brandAccent,
|
|
9
|
+
brandDescription,
|
|
10
|
+
footerClassName,
|
|
11
|
+
linkColumns,
|
|
12
|
+
logoAccent,
|
|
13
|
+
navbarClassName,
|
|
14
|
+
newsletter,
|
|
15
|
+
} from './content'
|
|
16
|
+
|
|
17
|
+
export const navbarShellProps = {
|
|
18
|
+
logoAccent,
|
|
19
|
+
className: navbarClassName,
|
|
20
|
+
// Global navigation search is held until a verified backend search contract
|
|
21
|
+
// exists (E2 task 5.4), so the generated storefront presents NO search
|
|
22
|
+
// control. A merchant may set this to `true` to opt in once the contract is
|
|
23
|
+
// verified; the `Navbar4` block still defaults to visible for legacy
|
|
24
|
+
// consumers that do not set this prop.
|
|
25
|
+
showSearch: false,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const footerShellProps = {
|
|
29
|
+
brandAccent,
|
|
30
|
+
brandDescription,
|
|
31
|
+
linkColumns,
|
|
32
|
+
className: footerClassName,
|
|
33
|
+
...(newsletter ? { newsletter } : {}),
|
|
34
|
+
}
|
|
@@ -3,23 +3,110 @@
|
|
|
3
3
|
// Routes are derived from the view barrel; adding a view never edits this file.
|
|
4
4
|
|
|
5
5
|
import type { NextAppSpec } from '@json-render/next'
|
|
6
|
+
import { storefrontInitialState } from '../state'
|
|
6
7
|
import { views } from '../views'
|
|
8
|
+
import { footerShellProps, navbarShellProps } from '../shell/props'
|
|
7
9
|
import type { StorefrontElement } from './types'
|
|
8
10
|
|
|
9
11
|
export type { StorefrontElement }
|
|
10
12
|
|
|
13
|
+
// The spec's state is a plain record; spreading the typed initial state keeps
|
|
14
|
+
// the single source of truth in ../state while satisfying the index signature.
|
|
15
|
+
const initialState: Record<string, unknown> = { ...storefrontInitialState }
|
|
16
|
+
|
|
11
17
|
const routes = Object.fromEntries(
|
|
12
|
-
views.
|
|
13
|
-
view
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
views.flatMap((view) => {
|
|
19
|
+
// A view declares one route or several; normalise to an array and flatten so
|
|
20
|
+
// a multi-route view (content-driven static pages) contributes one page per
|
|
21
|
+
// declared route while a single-route view is unchanged.
|
|
22
|
+
const viewRoutes = Array.isArray(view.route) ? view.route : [view.route]
|
|
23
|
+
return viewRoutes.map((route) => [
|
|
24
|
+
route.path,
|
|
25
|
+
{
|
|
26
|
+
layout: route.layout,
|
|
27
|
+
loader: route.loaderName,
|
|
28
|
+
metadata: route.metadata,
|
|
29
|
+
...route.entry,
|
|
30
|
+
},
|
|
31
|
+
])
|
|
32
|
+
}),
|
|
21
33
|
)
|
|
22
34
|
|
|
35
|
+
// The default layout is the storefront chrome shared by every page: the
|
|
36
|
+
// system notice region sits in the notices slot (always first, above the
|
|
37
|
+
// header), the announcement banner and navbar live in the header slot, the
|
|
38
|
+
// footer in the footer slot, and the cart drawer is a page-level child of
|
|
39
|
+
// Layout (rendered outside the slots so it can overlay the whole shell). This
|
|
40
|
+
// mirrors the reference app so a fresh scaffold ships a real storefront, not a
|
|
41
|
+
// bare page.
|
|
42
|
+
const shellElements: Record<string, StorefrontElement> = {
|
|
43
|
+
// Reserved top region for system/status messages. Only the shell layout
|
|
44
|
+
// references it, so it renders once and always as the first element.
|
|
45
|
+
notice: {
|
|
46
|
+
type: 'SystemNotice',
|
|
47
|
+
props: {},
|
|
48
|
+
},
|
|
49
|
+
banner: {
|
|
50
|
+
type: 'Banner',
|
|
51
|
+
props: {
|
|
52
|
+
badge: 'Novedades',
|
|
53
|
+
message: 'Descubre el catálogo de __SITE_NAME__.',
|
|
54
|
+
href: '/categorias',
|
|
55
|
+
linkLabel: 'Ver catálogo',
|
|
56
|
+
},
|
|
57
|
+
visible: { $state: '/bannerDismissed', not: true },
|
|
58
|
+
on: { dismissBanner: { action: 'dismissBanner' } },
|
|
59
|
+
},
|
|
60
|
+
navbar: {
|
|
61
|
+
type: 'StorefrontNavbar',
|
|
62
|
+
props: {
|
|
63
|
+
storeCategories: { $state: '/navCategories' },
|
|
64
|
+
// Neutral store info: no delivery/COD/shipping promise is made by default.
|
|
65
|
+
shippingNote: 'Catálogo en línea',
|
|
66
|
+
utilityLinks: [],
|
|
67
|
+
logoText: '__SITE_NAME__',
|
|
68
|
+
logoHref: '/',
|
|
69
|
+
// Optional wordmark accent and surface classes, configured in the
|
|
70
|
+
// user-owned shell seam. Spread last so merchant edits win.
|
|
71
|
+
...navbarShellProps,
|
|
72
|
+
},
|
|
73
|
+
on: {
|
|
74
|
+
openCart: { action: 'openCart' },
|
|
75
|
+
goToCheckout: { action: 'goToCheckout' },
|
|
76
|
+
applyQuantityDelta: { action: 'applyQuantityDelta' },
|
|
77
|
+
removeFromCart: { action: 'removeFromCart' },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
'cart.drawer': {
|
|
81
|
+
type: 'CartDrawer',
|
|
82
|
+
props: {
|
|
83
|
+
// No fabricated free-shipping threshold or reward: the backend is the only
|
|
84
|
+
// source of shipping amounts, and a promotion promise is not supported by
|
|
85
|
+
// the default fixture. A merchant may add promotions as customization.
|
|
86
|
+
promotions: [],
|
|
87
|
+
taxRate: 0.0,
|
|
88
|
+
},
|
|
89
|
+
visible: { $state: '/cartOpen', eq: true },
|
|
90
|
+
on: {
|
|
91
|
+
updateQuantity: { action: 'updateQuantity' },
|
|
92
|
+
removeFromCart: { action: 'removeFromCart' },
|
|
93
|
+
goToCheckout: { action: 'goToCheckout' },
|
|
94
|
+
closeCart: { action: 'closeCart' },
|
|
95
|
+
refreshCart: { action: 'refreshCart' },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
footer: {
|
|
99
|
+
type: 'Footer',
|
|
100
|
+
props: {
|
|
101
|
+
brandName: '__SITE_NAME__',
|
|
102
|
+
// Footer brand description, links, accent, and surface classes all come
|
|
103
|
+
// from the user-owned shell seam. Spread last so merchant edits win; no
|
|
104
|
+
// delivery/shipping promise and no dead `#` link is shipped by default.
|
|
105
|
+
...footerShellProps,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
23
110
|
export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
|
|
24
111
|
metadata: {
|
|
25
112
|
title: {
|
|
@@ -28,7 +115,7 @@ export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
|
|
|
28
115
|
},
|
|
29
116
|
description: 'Tienda online de __SITE_NAME__.',
|
|
30
117
|
},
|
|
31
|
-
state:
|
|
118
|
+
state: initialState,
|
|
32
119
|
layouts: {
|
|
33
120
|
default: {
|
|
34
121
|
root: 'layout',
|
|
@@ -36,11 +123,13 @@ export const __STOREFRONT_CAMEL__Spec: NextAppSpec = {
|
|
|
36
123
|
layout: {
|
|
37
124
|
type: 'Layout',
|
|
38
125
|
props: {},
|
|
39
|
-
|
|
126
|
+
slots: { notices: ['notice'], header: ['banner', 'navbar'], footer: ['footer'] },
|
|
127
|
+
children: ['slot', 'cart.drawer'],
|
|
40
128
|
},
|
|
41
129
|
// The page content is injected at the Slot element by @json-render/next.
|
|
42
130
|
// Without it the rendered layout drops the page and the store looks empty.
|
|
43
131
|
slot: { type: 'Slot', props: {}, children: [] },
|
|
132
|
+
...shellElements,
|
|
44
133
|
},
|
|
45
134
|
},
|
|
46
135
|
},
|
|
@@ -19,7 +19,11 @@ export const pageRoot = (
|
|
|
19
19
|
page: {
|
|
20
20
|
root: id,
|
|
21
21
|
elements: {
|
|
22
|
-
|
|
22
|
+
// `Page` is a neutral container, not the shell `Layout`: the shell is
|
|
23
|
+
// already rendered by the route's layoutSpec, so nesting another Layout
|
|
24
|
+
// here would duplicate chrome (header/footer/main) and any shell-only
|
|
25
|
+
// notices.
|
|
26
|
+
[id]: { type: 'Page', props: {}, children },
|
|
23
27
|
...elements,
|
|
24
28
|
},
|
|
25
29
|
},
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by Thor Commerce storefront (template: assets/templates/app/lib/__STOREFRONT__/spec/seo.ts)
|
|
2
|
+
// Pure SEO helpers shared by the seeded-once `app/sitemap.ts` and `app/robots.ts`
|
|
3
|
+
// host files. Kept free of Next.js, React, and env imports so the logic is
|
|
4
|
+
// testable at the source and the host files stay thin.
|
|
5
|
+
|
|
6
|
+
/** The routes the storefront can serve as fixed pages (no dynamic `[` segment). */
|
|
7
|
+
export function fixedRoutePaths(routes: Record<string, unknown>): string[] {
|
|
8
|
+
return Object.keys(routes).filter((path) => !path.includes('['))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Absolute sitemap URLs for the fixed routes. Emits nothing when no site URL is
|
|
13
|
+
* configured, so no canonical URL is invented.
|
|
14
|
+
*/
|
|
15
|
+
export function sitemapUrls(
|
|
16
|
+
routes: Record<string, unknown>,
|
|
17
|
+
siteUrl: string | undefined,
|
|
18
|
+
): string[] {
|
|
19
|
+
if (!siteUrl) return []
|
|
20
|
+
return fixedRoutePaths(routes).map((path) => new URL(path, siteUrl).toString())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Absolute URL of the sitemap for `robots.txt`, or undefined without a site URL. */
|
|
24
|
+
export function robotsSitemapUrl(siteUrl: string | undefined): string | undefined {
|
|
25
|
+
return siteUrl ? new URL('/sitemap.xml', siteUrl).toString() : undefined
|
|
26
|
+
}
|