@fayz-ai/storefront 0.2.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/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/auth.d.ts +29 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/blocks.d.ts +6 -0
- package/dist/blocks.d.ts.map +1 -0
- package/dist/components/CartDrawer.d.ts +3 -0
- package/dist/components/CartDrawer.d.ts.map +1 -0
- package/dist/components/CustomerAccountShell.d.ts +13 -0
- package/dist/components/CustomerAccountShell.d.ts.map +1 -0
- package/dist/components/FiltersPanel.d.ts +3 -0
- package/dist/components/FiltersPanel.d.ts.map +1 -0
- package/dist/components/OrderTrackingTimeline.d.ts +7 -0
- package/dist/components/OrderTrackingTimeline.d.ts.map +1 -0
- package/dist/components/Price.d.ts +11 -0
- package/dist/components/Price.d.ts.map +1 -0
- package/dist/components/ProductCard.d.ts +7 -0
- package/dist/components/ProductCard.d.ts.map +1 -0
- package/dist/components/ProductGrid.d.ts +7 -0
- package/dist/components/ProductGrid.d.ts.map +1 -0
- package/dist/components/ProductOptionSelector.d.ts +9 -0
- package/dist/components/ProductOptionSelector.d.ts.map +1 -0
- package/dist/components/ProductSpecs.d.ts +8 -0
- package/dist/components/ProductSpecs.d.ts.map +1 -0
- package/dist/components/QuantityInput.d.ts +12 -0
- package/dist/components/QuantityInput.d.ts.map +1 -0
- package/dist/components/RelatedProducts.d.ts +9 -0
- package/dist/components/RelatedProducts.d.ts.map +1 -0
- package/dist/components/SignInModal.d.ts +19 -0
- package/dist/components/SignInModal.d.ts.map +1 -0
- package/dist/components/StorefrontErrorBoundary.d.ts +20 -0
- package/dist/components/StorefrontErrorBoundary.d.ts.map +1 -0
- package/dist/components/StorefrontFooter.d.ts +3 -0
- package/dist/components/StorefrontFooter.d.ts.map +1 -0
- package/dist/components/StorefrontHeader.d.ts +3 -0
- package/dist/components/StorefrontHeader.d.ts.map +1 -0
- package/dist/components/Toaster.d.ts +3 -0
- package/dist/components/Toaster.d.ts.map +1 -0
- package/dist/components/sections/CategoryShowcase.d.ts +6 -0
- package/dist/components/sections/CategoryShowcase.d.ts.map +1 -0
- package/dist/components/sections/HeroSection.d.ts +9 -0
- package/dist/components/sections/HeroSection.d.ts.map +1 -0
- package/dist/components/sections/MiscSections.d.ts +33 -0
- package/dist/components/sections/MiscSections.d.ts.map +1 -0
- package/dist/components/sections/ProductRail.d.ts +9 -0
- package/dist/components/sections/ProductRail.d.ts.map +1 -0
- package/dist/config.d.ts +149 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/createStorefrontApp.d.ts +16 -0
- package/dist/createStorefrontApp.d.ts.map +1 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useCategories.d.ts +6 -0
- package/dist/hooks/useCategories.d.ts.map +1 -0
- package/dist/hooks/useDiscountValidator.d.ts +14 -0
- package/dist/hooks/useDiscountValidator.d.ts.map +1 -0
- package/dist/hooks/useMyOrders.d.ts +7 -0
- package/dist/hooks/useMyOrders.d.ts.map +1 -0
- package/dist/hooks/useProduct.d.ts +7 -0
- package/dist/hooks/useProduct.d.ts.map +1 -0
- package/dist/hooks/useProducts.d.ts +8 -0
- package/dist/hooks/useProducts.d.ts.map +1 -0
- package/dist/hooks/useStorefrontHead.d.ts +11 -0
- package/dist/hooks/useStorefrontHead.d.ts.map +1 -0
- package/dist/index.cjs +4186 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4085 -0
- package/dist/index.js.map +1 -0
- package/dist/motion.d.ts +19 -0
- package/dist/motion.d.ts.map +1 -0
- package/dist/order-status.d.ts +10 -0
- package/dist/order-status.d.ts.map +1 -0
- package/dist/pages/CatalogPage.d.ts +3 -0
- package/dist/pages/CatalogPage.d.ts.map +1 -0
- package/dist/pages/CheckoutPage.d.ts +3 -0
- package/dist/pages/CheckoutPage.d.ts.map +1 -0
- package/dist/pages/HomePage.d.ts +6 -0
- package/dist/pages/HomePage.d.ts.map +1 -0
- package/dist/pages/MyPurchasesPage.d.ts +3 -0
- package/dist/pages/MyPurchasesPage.d.ts.map +1 -0
- package/dist/pages/NotFoundPage.d.ts +4 -0
- package/dist/pages/NotFoundPage.d.ts.map +1 -0
- package/dist/pages/OrderConfirmationPage.d.ts +5 -0
- package/dist/pages/OrderConfirmationPage.d.ts.map +1 -0
- package/dist/pages/PolicyPage.d.ts +15 -0
- package/dist/pages/PolicyPage.d.ts.map +1 -0
- package/dist/pages/ProductDetailPage.d.ts +5 -0
- package/dist/pages/ProductDetailPage.d.ts.map +1 -0
- package/dist/presets.d.ts +33 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/product-options.d.ts +12 -0
- package/dist/product-options.d.ts.map +1 -0
- package/dist/router.d.ts +285 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/scaffold.d.ts +14 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/sections.d.ts +84 -0
- package/dist/sections.d.ts.map +1 -0
- package/dist/slot-contracts.d.ts +34 -0
- package/dist/slot-contracts.d.ts.map +1 -0
- package/dist/stores/cart.store.d.ts +58 -0
- package/dist/stores/cart.store.d.ts.map +1 -0
- package/dist/stores/catalog.store.d.ts +18 -0
- package/dist/stores/catalog.store.d.ts.map +1 -0
- package/dist/stores/session.store.d.ts +23 -0
- package/dist/stores/session.store.d.ts.map +1 -0
- package/dist/stores/toast.store.d.ts +27 -0
- package/dist/stores/toast.store.d.ts.map +1 -0
- package/dist/testids.d.ts +93 -0
- package/dist/testids.d.ts.map +1 -0
- package/dist/theme.d.ts +59 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/workflows/checkout.d.ts +33 -0
- package/dist/workflows/checkout.d.ts.map +1 -0
- package/package.json +56 -0
- package/src/auth.ts +134 -0
- package/src/blocks.tsx +47 -0
- package/src/components/CartDrawer.tsx +250 -0
- package/src/components/CustomerAccountShell.tsx +102 -0
- package/src/components/FiltersPanel.tsx +109 -0
- package/src/components/OrderTrackingTimeline.tsx +135 -0
- package/src/components/Price.tsx +32 -0
- package/src/components/ProductCard.tsx +104 -0
- package/src/components/ProductGrid.tsx +34 -0
- package/src/components/ProductOptionSelector.tsx +50 -0
- package/src/components/ProductSpecs.tsx +75 -0
- package/src/components/QuantityInput.tsx +52 -0
- package/src/components/RelatedProducts.tsx +31 -0
- package/src/components/SignInModal.tsx +120 -0
- package/src/components/StorefrontErrorBoundary.tsx +51 -0
- package/src/components/StorefrontFooter.tsx +100 -0
- package/src/components/StorefrontHeader.tsx +342 -0
- package/src/components/Toaster.tsx +60 -0
- package/src/components/sections/CategoryShowcase.tsx +79 -0
- package/src/components/sections/HeroSection.tsx +177 -0
- package/src/components/sections/MiscSections.tsx +157 -0
- package/src/components/sections/ProductRail.tsx +73 -0
- package/src/config.ts +176 -0
- package/src/createStorefrontApp.tsx +171 -0
- package/src/format.ts +7 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useCategories.ts +26 -0
- package/src/hooks/useDiscountValidator.ts +43 -0
- package/src/hooks/useMyOrders.ts +41 -0
- package/src/hooks/useProduct.ts +36 -0
- package/src/hooks/useProducts.ts +39 -0
- package/src/hooks/useStorefrontHead.ts +25 -0
- package/src/index.ts +132 -0
- package/src/motion.tsx +94 -0
- package/src/order-status.ts +33 -0
- package/src/pages/CatalogPage.tsx +118 -0
- package/src/pages/CheckoutPage.tsx +578 -0
- package/src/pages/HomePage.tsx +12 -0
- package/src/pages/MyPurchasesPage.tsx +310 -0
- package/src/pages/NotFoundPage.tsx +25 -0
- package/src/pages/OrderConfirmationPage.tsx +189 -0
- package/src/pages/PolicyPage.tsx +47 -0
- package/src/pages/ProductDetailPage.tsx +184 -0
- package/src/presets.ts +258 -0
- package/src/product-options.ts +63 -0
- package/src/router.ts +49 -0
- package/src/scaffold.tsx +109 -0
- package/src/sections.ts +69 -0
- package/src/slot-contracts.ts +20 -0
- package/src/stores/cart.store.ts +149 -0
- package/src/stores/catalog.store.ts +39 -0
- package/src/stores/session.store.ts +28 -0
- package/src/stores/toast.store.ts +43 -0
- package/src/testids.ts +103 -0
- package/src/theme.ts +160 -0
- package/src/workflows/checkout.ts +91 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { ChevronLeft, RefreshCcw, ShieldCheck, Truck } from 'lucide-react'
|
|
3
|
+
import { useProduct } from '../hooks/useProduct'
|
|
4
|
+
import { useCartStore } from '../stores/cart.store'
|
|
5
|
+
import { useStorefrontConfig } from '../config'
|
|
6
|
+
import { useStorefrontHead } from '../hooks/useStorefrontHead'
|
|
7
|
+
import { Link } from '../router'
|
|
8
|
+
import { Price } from '../components/Price'
|
|
9
|
+
import { QuantityInput } from '../components/QuantityInput'
|
|
10
|
+
import { ProductOptionSelector } from '../components/ProductOptionSelector'
|
|
11
|
+
import { ProductSpecs } from '../components/ProductSpecs'
|
|
12
|
+
import { RelatedProducts } from '../components/RelatedProducts'
|
|
13
|
+
import { getProductOptionGroups, type ProductOptionSelection } from '../product-options'
|
|
14
|
+
import { TID } from '../testids'
|
|
15
|
+
|
|
16
|
+
export function ProductDetailPage({ slug }: { slug: string }) {
|
|
17
|
+
const config = useStorefrontConfig()
|
|
18
|
+
const { product, loading } = useProduct(slug)
|
|
19
|
+
const addItem = useCartStore((s) => s.addItem)
|
|
20
|
+
const [qty, setQty] = useState(1)
|
|
21
|
+
const [selectedOptions, setSelectedOptions] = useState<ProductOptionSelection>({})
|
|
22
|
+
|
|
23
|
+
const optionGroups = product ? getProductOptionGroups(product) : []
|
|
24
|
+
useStorefrontHead({
|
|
25
|
+
title: product ? `${product.name} — ${config.name}` : config.name,
|
|
26
|
+
description: product?.description,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!product) return
|
|
31
|
+
const defaults = Object.fromEntries(
|
|
32
|
+
getProductOptionGroups(product).map((group) => [group.label, group.values[0] ?? '']),
|
|
33
|
+
)
|
|
34
|
+
setSelectedOptions(defaults)
|
|
35
|
+
}, [product?.id])
|
|
36
|
+
|
|
37
|
+
if (loading) {
|
|
38
|
+
return (
|
|
39
|
+
<main className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
|
|
40
|
+
<div className="grid animate-pulse gap-10 md:grid-cols-2">
|
|
41
|
+
<div className="aspect-square rounded-2xl bg-muted" />
|
|
42
|
+
<div className="space-y-4 py-4">
|
|
43
|
+
<div className="h-8 w-3/4 rounded bg-muted" />
|
|
44
|
+
<div className="h-6 w-1/3 rounded bg-muted" />
|
|
45
|
+
<div className="h-24 rounded bg-muted" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</main>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!product) {
|
|
53
|
+
return (
|
|
54
|
+
<main className="mx-auto max-w-5xl px-4 py-20 text-center sm:px-6">
|
|
55
|
+
<p className="text-lg text-muted-foreground">Produto não encontrado.</p>
|
|
56
|
+
<Link to="/" className="mt-4 inline-block text-primary underline">Voltar à loja</Link>
|
|
57
|
+
</main>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const soldOut = product.inventoryCount <= 0
|
|
62
|
+
const lowStock = !soldOut && product.inventoryCount <= 5
|
|
63
|
+
const image = product.images.find((i) => i.isPrimary) ?? product.images[0]
|
|
64
|
+
const discountPct =
|
|
65
|
+
product.compareAtPrice && product.compareAtPrice > product.price
|
|
66
|
+
? Math.round((1 - product.price / product.compareAtPrice) * 100)
|
|
67
|
+
: 0
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<main className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
|
|
71
|
+
<Link to="/" className="mb-6 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
|
72
|
+
<ChevronLeft className="h-4 w-4" /> Continuar comprando
|
|
73
|
+
</Link>
|
|
74
|
+
|
|
75
|
+
<div className="grid animate-fade-up gap-10 md:grid-cols-2">
|
|
76
|
+
<div className="group overflow-hidden border bg-muted" style={{ borderRadius: 'var(--sf-radius-card)' }}>
|
|
77
|
+
{image && (
|
|
78
|
+
<img
|
|
79
|
+
src={image.url}
|
|
80
|
+
alt={image.altText ?? product.name}
|
|
81
|
+
className="aspect-square w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex flex-col py-2 lg:sticky lg:top-24 lg:self-start">
|
|
87
|
+
<nav className="mb-3 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
|
88
|
+
<Link to="/" className="transition-colors hover:text-foreground">Início</Link>
|
|
89
|
+
{product.categoryName && (
|
|
90
|
+
<>
|
|
91
|
+
<span aria-hidden>/</span>
|
|
92
|
+
<span>{product.categoryName}</span>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
<span aria-hidden>/</span>
|
|
96
|
+
<span className="text-foreground">{product.name}</span>
|
|
97
|
+
</nav>
|
|
98
|
+
|
|
99
|
+
<h1 data-testid={TID.pdpName} className="sf-heading text-3xl font-bold tracking-tight">
|
|
100
|
+
{product.name}
|
|
101
|
+
</h1>
|
|
102
|
+
|
|
103
|
+
<div className="mt-3 flex items-center gap-3">
|
|
104
|
+
<div className="text-xl">
|
|
105
|
+
<Price
|
|
106
|
+
value={product.price}
|
|
107
|
+
compareAt={product.compareAtPrice}
|
|
108
|
+
testId={TID.pdpPrice}
|
|
109
|
+
compareTestId={TID.pdpComparePrice}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
{discountPct > 0 && (
|
|
113
|
+
<span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-bold text-primary">
|
|
114
|
+
−{discountPct}%
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<p data-testid={TID.pdpDescription} className="mt-5 leading-relaxed text-muted-foreground">
|
|
120
|
+
{product.description}
|
|
121
|
+
</p>
|
|
122
|
+
|
|
123
|
+
<div className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
|
124
|
+
{soldOut ? (
|
|
125
|
+
<span className="inline-flex items-center gap-2 font-medium text-muted-foreground">
|
|
126
|
+
<span className="h-2 w-2 rounded-full bg-muted-foreground" /> Sem estoque
|
|
127
|
+
</span>
|
|
128
|
+
) : lowStock ? (
|
|
129
|
+
<span className="inline-flex items-center gap-2 font-semibold text-amber-600">
|
|
130
|
+
<span className="h-2 w-2 animate-pulse rounded-full bg-amber-500" />
|
|
131
|
+
Últimas unidades — só {product.inventoryCount} restantes
|
|
132
|
+
</span>
|
|
133
|
+
) : (
|
|
134
|
+
<span className="inline-flex items-center gap-2 font-medium text-emerald-600">
|
|
135
|
+
<span className="h-2 w-2 rounded-full bg-emerald-500" /> Em estoque
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
{product.sku && <span className="text-muted-foreground">SKU: {product.sku}</span>}
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<ProductOptionSelector
|
|
142
|
+
groups={optionGroups}
|
|
143
|
+
value={selectedOptions}
|
|
144
|
+
onChange={setSelectedOptions}
|
|
145
|
+
/>
|
|
146
|
+
|
|
147
|
+
<div className="mt-8 flex items-center gap-4">
|
|
148
|
+
<QuantityInput
|
|
149
|
+
value={qty}
|
|
150
|
+
onChange={setQty}
|
|
151
|
+
max={soldOut ? 1 : product.inventoryCount}
|
|
152
|
+
testId={TID.pdpQtyInput}
|
|
153
|
+
/>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
data-testid={TID.pdpAddToCart}
|
|
157
|
+
disabled={soldOut}
|
|
158
|
+
onClick={() => addItem(product, qty, selectedOptions)}
|
|
159
|
+
className="sf-cta flex-1 bg-primary py-3.5 font-semibold text-primary-foreground shadow-md transition-all hover:-translate-y-0.5 hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-40 disabled:shadow-none disabled:hover:translate-y-0"
|
|
160
|
+
style={{ borderRadius: 'var(--sf-radius-button)' }}
|
|
161
|
+
>
|
|
162
|
+
{soldOut ? 'Esgotado' : 'Adicionar ao carrinho'}
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div className="mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center">
|
|
167
|
+
<div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
|
|
168
|
+
<Truck className="h-5 w-5 text-primary" /> Envio rápido
|
|
169
|
+
</div>
|
|
170
|
+
<div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
|
|
171
|
+
<RefreshCcw className="h-5 w-5 text-primary" /> Troca fácil
|
|
172
|
+
</div>
|
|
173
|
+
<div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
|
|
174
|
+
<ShieldCheck className="h-5 w-5 text-primary" /> Pagamento seguro
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<ProductSpecs product={product} />
|
|
181
|
+
<RelatedProducts product={product} />
|
|
182
|
+
</main>
|
|
183
|
+
)
|
|
184
|
+
}
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { StorefrontTheme } from './theme'
|
|
2
|
+
import type { HomeConfig } from './sections'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Template presets — recreations of the four Nuvemshop patterns selected in
|
|
6
|
+
// docs/storefront-templates-research.md. Each is { theme, home() } so an app
|
|
7
|
+
// (or the Fayz agent) starts from a complete professional store and overrides
|
|
8
|
+
// copy, colors, hero images and sections — the saas-core theming workflow.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/** Real imagery for a template home — resolved at build time (e.g. via
|
|
12
|
+
* scripts/fetch-unsplash.mjs); falls back to gradient placeholders when omitted. */
|
|
13
|
+
export interface TemplateImages {
|
|
14
|
+
hero?: Array<string | undefined>
|
|
15
|
+
banners?: Array<string | undefined>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StorefrontTemplate {
|
|
19
|
+
id: string
|
|
20
|
+
/** The Nuvemshop pattern this recreates */
|
|
21
|
+
inspiration: string
|
|
22
|
+
theme: StorefrontTheme
|
|
23
|
+
/** Default home blueprint — pass the store name for copy interpolation */
|
|
24
|
+
home: (storeName: string, images?: TemplateImages) => HomeConfig
|
|
25
|
+
announcement?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** maré — elegant fashion (Rio): airy warm paper, olive ink, sand CTA, centered logo. */
|
|
29
|
+
export const mareTemplate: StorefrontTemplate = {
|
|
30
|
+
id: 'mare',
|
|
31
|
+
inspiration: 'Nuvemshop Rio',
|
|
32
|
+
announcement: 'FRETE GRÁTIS EM COMPRAS ACIMA DE R$ 300',
|
|
33
|
+
theme: {
|
|
34
|
+
name: 'mare',
|
|
35
|
+
colors: {
|
|
36
|
+
background: '40 14% 93%',
|
|
37
|
+
foreground: '90 8% 38%',
|
|
38
|
+
primary: '42 32% 61%',
|
|
39
|
+
primaryForeground: '40 30% 12%',
|
|
40
|
+
card: '40 20% 97%',
|
|
41
|
+
cardForeground: '90 10% 25%',
|
|
42
|
+
muted: '40 12% 88%',
|
|
43
|
+
mutedForeground: '90 6% 50%',
|
|
44
|
+
border: '40 10% 82%',
|
|
45
|
+
announcementBackground: '90 10% 30%',
|
|
46
|
+
announcementForeground: '40 20% 95%',
|
|
47
|
+
},
|
|
48
|
+
font: { heading: 'Plus Jakarta Sans', body: 'Plus Jakarta Sans' },
|
|
49
|
+
radius: 'soft',
|
|
50
|
+
header: { variant: 'centered' },
|
|
51
|
+
productCard: { style: 'editorial', imageAspect: 'portrait' },
|
|
52
|
+
},
|
|
53
|
+
home: (storeName, images) => ({
|
|
54
|
+
sections: [
|
|
55
|
+
{
|
|
56
|
+
type: 'hero',
|
|
57
|
+
variant: 'banner',
|
|
58
|
+
height: 'tall',
|
|
59
|
+
slides: [
|
|
60
|
+
{ title: 'Feito para durar', subtitle: `A nova coleção ${storeName} chegou — peças atemporais em materiais honestos.`, cta: 'Ver coleção', hue: 75, image: images?.hero?.[0] },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{ type: 'categories', style: 'bubbles' },
|
|
64
|
+
{ type: 'products', title: 'Destaques', filter: 'all', limit: 4 },
|
|
65
|
+
{ type: 'banner', title: 'Até 30% off', eyebrow: 'por tempo limitado', subtitle: 'Peças selecionadas da estação', cta: 'Aproveitar', hue: 40, image: images?.banners?.[0] },
|
|
66
|
+
{ type: 'products', title: 'Promoções', filter: 'sale', limit: 4 },
|
|
67
|
+
{ type: 'products', title: 'Lançamentos', filter: 'new', limit: 4 },
|
|
68
|
+
{ type: 'newsletter', title: 'Assine nossa newsletter', subtitle: 'Novidades e ofertas exclusivas, sem spam.' },
|
|
69
|
+
],
|
|
70
|
+
}),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** sertão — editorial organic (Uyuni): cream paper, coffee serif ink, boxed hero, manifesto. */
|
|
74
|
+
export const sertaoTemplate: StorefrontTemplate = {
|
|
75
|
+
id: 'sertao',
|
|
76
|
+
inspiration: 'Nuvemshop Uyuni',
|
|
77
|
+
theme: {
|
|
78
|
+
name: 'sertao',
|
|
79
|
+
colors: {
|
|
80
|
+
background: '30 100% 98%',
|
|
81
|
+
foreground: '28 42% 32%',
|
|
82
|
+
primary: '28 42% 32%',
|
|
83
|
+
primaryForeground: '30 100% 97%',
|
|
84
|
+
card: '32 60% 96%',
|
|
85
|
+
cardForeground: '28 40% 26%',
|
|
86
|
+
muted: '32 40% 92%',
|
|
87
|
+
mutedForeground: '28 22% 48%',
|
|
88
|
+
border: '28 30% 80%',
|
|
89
|
+
},
|
|
90
|
+
font: { heading: 'Marcellus', body: 'Marcellus', fallback: 'serif' },
|
|
91
|
+
radius: 'none',
|
|
92
|
+
header: { variant: 'minimal', uppercaseNav: true },
|
|
93
|
+
productCard: { style: 'editorial', imageAspect: 'portrait' },
|
|
94
|
+
uppercaseButtons: true,
|
|
95
|
+
},
|
|
96
|
+
home: (storeName, images) => ({
|
|
97
|
+
sections: [
|
|
98
|
+
{
|
|
99
|
+
type: 'hero',
|
|
100
|
+
variant: 'banner',
|
|
101
|
+
height: 'tall',
|
|
102
|
+
slides: [
|
|
103
|
+
{ title: 'Essenciais naturais', subtitle: 'Conheça nossa coleção', cta: 'Ver mais', hue: 28, boxed: true, image: images?.hero?.[0] },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'manifesto',
|
|
108
|
+
text: `Num mundo de pressa e excesso, ${storeName} escolhe o caminho lento: ingredientes honestos, produção pequena e cuidado em cada detalhe.`,
|
|
109
|
+
},
|
|
110
|
+
{ type: 'categories', style: 'tiles' },
|
|
111
|
+
{ type: 'products', title: 'ESSENCIAIS', filter: 'all', limit: 4 },
|
|
112
|
+
{
|
|
113
|
+
type: 'testimonials',
|
|
114
|
+
title: 'Nossos clientes',
|
|
115
|
+
items: [
|
|
116
|
+
{ quote: 'Qualidade impecável e um cheiro que abraça.', author: 'Helena M.' },
|
|
117
|
+
{ quote: 'Compro há dois anos e nunca me decepcionou.', author: 'Rafael T.' },
|
|
118
|
+
{ quote: 'Entrega rápida e embalagem linda, dá gosto de presentear.', author: 'Cláudia P.' },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
{ type: 'products', title: 'NOVIDADES', filter: 'new', limit: 4 },
|
|
122
|
+
{
|
|
123
|
+
type: 'benefits',
|
|
124
|
+
items: [
|
|
125
|
+
{ icon: 'Truck', title: 'Frete para todo o Brasil', text: 'Envio em até 48h' },
|
|
126
|
+
{ icon: 'Leaf', title: '100% vegano', text: 'Sem testes em animais' },
|
|
127
|
+
{ icon: 'RotateCcw', title: 'Troca fácil', text: '30 dias para trocar' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{ type: 'newsletter', title: 'NEWSLETTER', subtitle: 'Histórias, lançamentos e cuidado — direto no seu e-mail.' },
|
|
131
|
+
],
|
|
132
|
+
}),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** volt — bold tech (Brasília): white canvas, near-black ink, vivid CTA, dark search-first header, split hero. */
|
|
136
|
+
export const voltTemplate: StorefrontTemplate = {
|
|
137
|
+
id: 'volt',
|
|
138
|
+
inspiration: 'Nuvemshop Brasília',
|
|
139
|
+
announcement: 'CUPOM BEMVINDO10 — 10% DE DESCONTO NA PRIMEIRA COMPRA',
|
|
140
|
+
theme: {
|
|
141
|
+
name: 'volt',
|
|
142
|
+
colors: {
|
|
143
|
+
background: '0 0% 100%',
|
|
144
|
+
foreground: '0 0% 7%',
|
|
145
|
+
primary: '341 100% 45%',
|
|
146
|
+
primaryForeground: '0 0% 100%',
|
|
147
|
+
card: '0 0% 98%',
|
|
148
|
+
cardForeground: '0 0% 10%',
|
|
149
|
+
muted: '0 0% 94%',
|
|
150
|
+
mutedForeground: '0 0% 40%',
|
|
151
|
+
border: '0 0% 88%',
|
|
152
|
+
headerBackground: '0 0% 6%',
|
|
153
|
+
headerForeground: '0 0% 98%',
|
|
154
|
+
announcementBackground: '341 100% 45%',
|
|
155
|
+
announcementForeground: '0 0% 100%',
|
|
156
|
+
},
|
|
157
|
+
font: { heading: 'Chakra Petch', body: 'Rubik' },
|
|
158
|
+
radius: 'soft',
|
|
159
|
+
header: { variant: 'search', uppercaseNav: true },
|
|
160
|
+
productCard: { style: 'card', imageAspect: 'square' },
|
|
161
|
+
},
|
|
162
|
+
home: (storeName, images) => ({
|
|
163
|
+
sections: [
|
|
164
|
+
{
|
|
165
|
+
type: 'hero',
|
|
166
|
+
variant: 'split',
|
|
167
|
+
slides: [
|
|
168
|
+
{ title: 'Tecnologia', subtitle: 'O melhor do hardware e dos acessórios', hue: 250, image: images?.hero?.[0] },
|
|
169
|
+
{ title: 'Ofertas', subtitle: `Descontos semanais ${storeName}`, hue: 341, href: '/catalog', image: images?.hero?.[1] },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'benefits',
|
|
174
|
+
items: [
|
|
175
|
+
{ icon: 'CreditCard', title: '3x sem juros', text: 'Em todo o site' },
|
|
176
|
+
{ icon: 'Truck', title: 'Envio em 24h', text: 'Para todo o Brasil' },
|
|
177
|
+
{ icon: 'ShieldCheck', title: 'Compra segura', text: 'Seus dados protegidos' },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{ type: 'products', title: 'NOVIDADES', filter: 'new', limit: 4 },
|
|
181
|
+
{ type: 'banner', title: 'DESCONTOS SEMANAIS', eyebrow: 'toda sexta', subtitle: 'Ofertas novas, estoque limitado', cta: 'Conferir', hue: 341, image: images?.banners?.[0] },
|
|
182
|
+
{ type: 'products', title: 'PROMOÇÕES', filter: 'sale', limit: 4 },
|
|
183
|
+
{ type: 'newsletter', title: 'Entre para o clube', subtitle: 'Cupons e lançamentos antes de todo mundo.' },
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** atelier — classic premium retail (Flex): white, near-black, gold accent, slider hero, trust badges. */
|
|
189
|
+
export const atelierTemplate: StorefrontTemplate = {
|
|
190
|
+
id: 'atelier',
|
|
191
|
+
inspiration: 'Nuvemshop Flex',
|
|
192
|
+
theme: {
|
|
193
|
+
name: 'atelier',
|
|
194
|
+
colors: {
|
|
195
|
+
background: '0 0% 100%',
|
|
196
|
+
foreground: '0 0% 9%',
|
|
197
|
+
primary: '40 45% 48%',
|
|
198
|
+
primaryForeground: '0 0% 100%',
|
|
199
|
+
card: '0 0% 99%',
|
|
200
|
+
cardForeground: '0 0% 9%',
|
|
201
|
+
muted: '40 10% 95%',
|
|
202
|
+
mutedForeground: '0 0% 42%',
|
|
203
|
+
border: '0 0% 88%',
|
|
204
|
+
announcementBackground: '0 0% 9%',
|
|
205
|
+
announcementForeground: '0 0% 98%',
|
|
206
|
+
},
|
|
207
|
+
font: { heading: 'Outfit', body: 'Outfit' },
|
|
208
|
+
radius: 'none',
|
|
209
|
+
header: { variant: 'centered', uppercaseNav: true },
|
|
210
|
+
productCard: { style: 'card', imageAspect: 'square' },
|
|
211
|
+
uppercaseButtons: true,
|
|
212
|
+
},
|
|
213
|
+
announcement: 'PARCELE EM ATÉ 10X · 5% OFF NO PIX',
|
|
214
|
+
home: (storeName, images) => ({
|
|
215
|
+
sections: [
|
|
216
|
+
{
|
|
217
|
+
type: 'hero',
|
|
218
|
+
variant: 'slider',
|
|
219
|
+
height: 'medium',
|
|
220
|
+
slides: [
|
|
221
|
+
{ title: 'Feito para quem valoriza detalhes', subtitle: `Coleção exclusiva ${storeName}`, cta: 'APROVEITE', hue: 40, image: images?.hero?.[0] },
|
|
222
|
+
{ title: 'Clássicos que não saem de moda', subtitle: 'Peças autorais, produção limitada', cta: 'VER COLEÇÃO', hue: 210, image: images?.hero?.[1] },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: 'benefits',
|
|
227
|
+
items: [
|
|
228
|
+
{ icon: 'CreditCard', title: 'Parcele em até 10x', text: 'Ou 5% off no PIX' },
|
|
229
|
+
{ icon: 'Lock', title: 'Site 100% seguro', text: 'Seus dados protegidos' },
|
|
230
|
+
{ icon: 'Award', title: 'Peças exclusivas', text: 'Produtos 100% autorais' },
|
|
231
|
+
],
|
|
232
|
+
},
|
|
233
|
+
{ type: 'categories', style: 'tiles' },
|
|
234
|
+
{ type: 'products', title: 'Novidades', filter: 'new', limit: 4 },
|
|
235
|
+
{ type: 'banner', title: 'Promoções', eyebrow: 'seleção especial', subtitle: 'Selecionados com até 25% off', cta: 'Ver ofertas', hue: 40, image: images?.banners?.[0] },
|
|
236
|
+
{ type: 'products', title: 'Promoções', filter: 'sale', limit: 4 },
|
|
237
|
+
{
|
|
238
|
+
type: 'testimonials',
|
|
239
|
+
title: 'O que dizem sobre nós',
|
|
240
|
+
items: [
|
|
241
|
+
{ quote: 'Acabamento perfeito, superou a expectativa.', author: 'Mariana C.' },
|
|
242
|
+
{ quote: 'Atendimento impecável do pedido à entrega.', author: 'Jorge L.' },
|
|
243
|
+
{ quote: 'Virou minha loja favorita para presentes.', author: 'Paula R.' },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
{ type: 'newsletter' },
|
|
247
|
+
],
|
|
248
|
+
}),
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const storefrontTemplates = {
|
|
252
|
+
mare: mareTemplate,
|
|
253
|
+
sertao: sertaoTemplate,
|
|
254
|
+
volt: voltTemplate,
|
|
255
|
+
atelier: atelierTemplate,
|
|
256
|
+
} as const
|
|
257
|
+
|
|
258
|
+
export type StorefrontTemplateId = keyof typeof storefrontTemplates
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Product } from '@fayz-ai/shop/types'
|
|
2
|
+
|
|
3
|
+
export interface ProductOptionGroup {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
values: string[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type ProductOptionSelection = Record<string, string>
|
|
10
|
+
|
|
11
|
+
function cleanValues(value: unknown): string[] {
|
|
12
|
+
if (!Array.isArray(value)) return []
|
|
13
|
+
return Array.from(new Set(value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cleanOptionGroup(value: unknown): ProductOptionGroup | null {
|
|
17
|
+
if (!value || typeof value !== 'object') return null
|
|
18
|
+
const record = value as Record<string, unknown>
|
|
19
|
+
const id = typeof record.id === 'string' ? record.id : typeof record.name === 'string' ? record.name : null
|
|
20
|
+
const label = typeof record.label === 'string' ? record.label : typeof record.name === 'string' ? record.name : id
|
|
21
|
+
const values = cleanValues(record.values)
|
|
22
|
+
if (!id || !label || values.length === 0) return null
|
|
23
|
+
return { id, label, values }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getProductOptionGroups(product: Product): ProductOptionGroup[] {
|
|
27
|
+
const options = product.metadata.options
|
|
28
|
+
const explicit = Array.isArray(options)
|
|
29
|
+
? options.map(cleanOptionGroup).filter((group): group is ProductOptionGroup => Boolean(group))
|
|
30
|
+
: []
|
|
31
|
+
|
|
32
|
+
if (explicit.length > 0) return explicit
|
|
33
|
+
|
|
34
|
+
const groups: ProductOptionGroup[] = []
|
|
35
|
+
const sizes = cleanValues(product.metadata.sizes)
|
|
36
|
+
const colors = cleanValues(product.metadata.colors)
|
|
37
|
+
|
|
38
|
+
if (sizes.length > 0) groups.push({ id: 'Tamanho', label: 'Tamanho', values: sizes })
|
|
39
|
+
if (colors.length > 0) groups.push({ id: 'Cor', label: 'Cor', values: colors })
|
|
40
|
+
|
|
41
|
+
return groups
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeProductOptionSelection(selection?: ProductOptionSelection | null): ProductOptionSelection {
|
|
45
|
+
if (!selection) return {}
|
|
46
|
+
return Object.fromEntries(
|
|
47
|
+
Object.entries(selection)
|
|
48
|
+
.filter((entry): entry is [string, string] => entry[0].trim().length > 0 && entry[1].trim().length > 0)
|
|
49
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatProductOptionSelection(selection?: ProductOptionSelection | null): string {
|
|
54
|
+
return Object.entries(normalizeProductOptionSelection(selection))
|
|
55
|
+
.map(([label, value]) => `${label}: ${value}`)
|
|
56
|
+
.join(' · ')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function productOptionSelectionKey(selection?: ProductOptionSelection | null): string {
|
|
60
|
+
const normalized = normalizeProductOptionSelection(selection)
|
|
61
|
+
const entries = Object.entries(normalized)
|
|
62
|
+
return entries.length > 0 ? JSON.stringify(entries) : ''
|
|
63
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { hashRouterAdapter } from '@fayz-ai/core'
|
|
3
|
+
|
|
4
|
+
// Minimal hash router for the storefront. Deliberately not react-router:
|
|
5
|
+
// an SDK package shipping a router dependency creates singleton hazards
|
|
6
|
+
// under source-alias dev setups, and six routes don't justify one.
|
|
7
|
+
|
|
8
|
+
const adapter = hashRouterAdapter()
|
|
9
|
+
|
|
10
|
+
export function navigateTo(to: string): void {
|
|
11
|
+
adapter.navigate(to)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useHashPath(): string {
|
|
15
|
+
const [path, setPath] = useState(() => adapter.getCurrentPath() || '/')
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const unsub = adapter.onPathChange((p) => {
|
|
18
|
+
setPath(p || '/')
|
|
19
|
+
window.scrollTo(0, 0)
|
|
20
|
+
})
|
|
21
|
+
return unsub
|
|
22
|
+
}, [])
|
|
23
|
+
return path
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Match '/product/:slug' against '/product/leather-backpack' → { slug: 'leather-backpack' } */
|
|
27
|
+
export function matchPath(pattern: string, path: string): Record<string, string> | null {
|
|
28
|
+
const cleanPath = (path.split('?')[0] ?? '').replace(/\/+$/, '') || '/'
|
|
29
|
+
const patternParts = pattern.split('/').filter(Boolean)
|
|
30
|
+
const pathParts = cleanPath.split('/').filter(Boolean)
|
|
31
|
+
if (patternParts.length !== pathParts.length) return null
|
|
32
|
+
const params: Record<string, string> = {}
|
|
33
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
34
|
+
const pat = patternParts[i]!
|
|
35
|
+
const part = pathParts[i]!
|
|
36
|
+
if (pat.startsWith(':')) params[pat.slice(1)] = decodeURIComponent(part)
|
|
37
|
+
else if (pat !== part) return null
|
|
38
|
+
}
|
|
39
|
+
return params
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
43
|
+
to: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Real anchor (#/path) — middle-click/cmd-click friendly, Playwright friendly. */
|
|
47
|
+
export function Link({ to, children, ...rest }: LinkProps) {
|
|
48
|
+
return React.createElement('a', { href: `#${to}`, ...rest }, children)
|
|
49
|
+
}
|
package/src/scaffold.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { registerScaffold, defineApp } from '@fayz-ai/core'
|
|
3
|
+
import type { AppManifest, BackendProvider } from '@fayz-ai/core'
|
|
4
|
+
import { StorefrontConfigProvider, resolveConfig } from './config'
|
|
5
|
+
import type { StorefrontConfig } from './config'
|
|
6
|
+
import type { StorefrontTheme } from './theme'
|
|
7
|
+
import type { HomeConfig, NavLink, FooterConfig } from './sections'
|
|
8
|
+
import { initStorefrontRuntime, StorefrontShell } from './createStorefrontApp'
|
|
9
|
+
import './blocks' // ensure section blocks are registered
|
|
10
|
+
|
|
11
|
+
type StorefrontBackendProvider = NonNullable<StorefrontConfig['backend']>['provider']
|
|
12
|
+
const runtimeConfigRegistry = new Map<string, StorefrontConfig>()
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Storefront scaffold — renders a store from a pure-data AppManifest. The
|
|
16
|
+
// storefront surface keeps its declarative config under surface.options, so the
|
|
17
|
+
// mapping is loss-free for the serializable fields (the code escape hatches —
|
|
18
|
+
// custom provider/adapter/logo — arrive via the registries instead).
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function slug(name: string): string {
|
|
22
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'app'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** StorefrontConfig → AppManifest (the sugar-to-data direction). */
|
|
26
|
+
export function defineStorefront(config: StorefrontConfig): AppManifest {
|
|
27
|
+
const id = slug(config.name)
|
|
28
|
+
runtimeConfigRegistry.set(id, config)
|
|
29
|
+
const backendProvider = (config.backend?.provider ?? (config.provider ? 'custom' : 'mock')) as BackendProvider
|
|
30
|
+
|
|
31
|
+
return defineApp({
|
|
32
|
+
id,
|
|
33
|
+
name: config.name,
|
|
34
|
+
backend: {
|
|
35
|
+
provider: backendProvider,
|
|
36
|
+
url: config.backend?.url,
|
|
37
|
+
},
|
|
38
|
+
locale: {
|
|
39
|
+
default: config.locale ?? 'pt-BR',
|
|
40
|
+
supported: [config.locale ?? 'pt-BR'],
|
|
41
|
+
currency: config.currency ?? 'BRL',
|
|
42
|
+
},
|
|
43
|
+
theme: config.theme as Record<string, unknown> | undefined,
|
|
44
|
+
surfaces: {
|
|
45
|
+
storefront: {
|
|
46
|
+
scaffold: 'storefront',
|
|
47
|
+
options: {
|
|
48
|
+
announcement: config.announcement,
|
|
49
|
+
home: config.home,
|
|
50
|
+
nav: config.nav,
|
|
51
|
+
footer: config.footer,
|
|
52
|
+
shipping: config.shipping,
|
|
53
|
+
features: config.features,
|
|
54
|
+
catalog: config.catalog,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** AppManifest → StorefrontConfig (the data-to-render direction). */
|
|
62
|
+
function manifestToStorefrontConfig(manifest: AppManifest, surfaceName: string): StorefrontConfig {
|
|
63
|
+
const runtimeConfig = runtimeConfigRegistry.get(manifest.id)
|
|
64
|
+
const surface = manifest.surfaces[surfaceName]
|
|
65
|
+
const o = (surface?.options ?? {}) as Record<string, unknown>
|
|
66
|
+
return {
|
|
67
|
+
...runtimeConfig,
|
|
68
|
+
name: manifest.name,
|
|
69
|
+
currency: (manifest.locale as { currency?: string } | undefined)?.currency,
|
|
70
|
+
locale: (manifest.locale as { default?: string } | undefined)?.default,
|
|
71
|
+
theme: manifest.theme as StorefrontTheme | undefined,
|
|
72
|
+
announcement: o.announcement as string | undefined,
|
|
73
|
+
home: o.home as HomeConfig | undefined,
|
|
74
|
+
nav: o.nav as NavLink[] | undefined,
|
|
75
|
+
footer: o.footer as FooterConfig | undefined,
|
|
76
|
+
shipping: o.shipping as StorefrontConfig['shipping'],
|
|
77
|
+
features: o.features as StorefrontConfig['features'],
|
|
78
|
+
catalog: o.catalog as StorefrontConfig['catalog'],
|
|
79
|
+
backend: {
|
|
80
|
+
provider: manifest.backend?.provider as StorefrontBackendProvider,
|
|
81
|
+
url: manifest.backend?.url,
|
|
82
|
+
},
|
|
83
|
+
slots: runtimeConfig?.slots,
|
|
84
|
+
routes: runtimeConfig?.routes,
|
|
85
|
+
provider: runtimeConfig?.provider,
|
|
86
|
+
auth: runtimeConfig?.auth,
|
|
87
|
+
logo: runtimeConfig?.logo,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function StorefrontScaffold({ manifest, surface }: { manifest: AppManifest; surface: string }) {
|
|
92
|
+
const config = React.useMemo(() => manifestToStorefrontConfig(manifest, surface), [manifest, surface])
|
|
93
|
+
const resolved = React.useMemo(() => resolveConfig(config), [config])
|
|
94
|
+
const inited = React.useRef(false)
|
|
95
|
+
if (!inited.current) {
|
|
96
|
+
initStorefrontRuntime(config)
|
|
97
|
+
inited.current = true
|
|
98
|
+
}
|
|
99
|
+
return (
|
|
100
|
+
<StorefrontConfigProvider value={resolved}>
|
|
101
|
+
<StorefrontShell />
|
|
102
|
+
</StorefrontConfigProvider>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
StorefrontScaffold.displayName = 'StorefrontScaffold'
|
|
106
|
+
|
|
107
|
+
// Self-register so `renderApp(manifest)` can resolve the 'storefront' scaffold
|
|
108
|
+
// whenever this package is imported.
|
|
109
|
+
registerScaffold('storefront', StorefrontScaffold, { source: 'sdk', label: 'Storefront' })
|