@fayz-ai/storefront 0.6.0 → 0.8.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/README.md +3 -1
- package/dist/auth.d.ts +12 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/blocks.d.ts +3 -3
- package/dist/blocks.d.ts.map +1 -1
- package/dist/component-contracts.d.ts +124 -0
- package/dist/component-contracts.d.ts.map +1 -0
- package/dist/component-selectors.d.ts +61 -0
- package/dist/component-selectors.d.ts.map +1 -0
- package/dist/components/CartDrawer.d.ts.map +1 -1
- package/dist/components/ProductCard.d.ts +2 -5
- package/dist/components/ProductCard.d.ts.map +1 -1
- package/dist/components/ProductEnquiryForm.d.ts +8 -0
- package/dist/components/ProductEnquiryForm.d.ts.map +1 -0
- package/dist/components/ProductGrid.d.ts.map +1 -1
- package/dist/components/RelatedProducts.d.ts +1 -1
- package/dist/components/RelatedProducts.d.ts.map +1 -1
- package/dist/components/SmoothImage.d.ts +7 -0
- package/dist/components/SmoothImage.d.ts.map +1 -0
- package/dist/components/StorefrontFooter.d.ts.map +1 -1
- package/dist/components/StorefrontHeader.d.ts.map +1 -1
- package/dist/components/sections/CategoryShowcase.d.ts.map +1 -1
- package/dist/components/sections/HeroSection.d.ts.map +1 -1
- package/dist/components/sections/MediaCarousel.d.ts +11 -0
- package/dist/components/sections/MediaCarousel.d.ts.map +1 -0
- package/dist/components/sections/MiscSections.d.ts.map +1 -1
- package/dist/components/sections/ProductRail.d.ts.map +1 -1
- package/dist/components/sections/ProductSlider.d.ts +15 -0
- package/dist/components/sections/ProductSlider.d.ts.map +1 -0
- package/dist/config.d.ts +38 -21
- package/dist/config.d.ts.map +1 -1
- package/dist/createStorefrontApp.d.ts +12 -0
- package/dist/createStorefrontApp.d.ts.map +1 -1
- package/dist/define.d.ts +17 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useStorefront.d.ts +35 -0
- package/dist/hooks/useStorefront.d.ts.map +1 -0
- package/dist/index.cjs +1113 -277
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +21 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1006 -188
- package/dist/index.js.map +1 -1
- package/dist/pages/CheckoutPage.d.ts.map +1 -1
- package/dist/pages/HomePage.d.ts +5 -2
- package/dist/pages/HomePage.d.ts.map +1 -1
- package/dist/pages/MyPurchasesPage.d.ts.map +1 -1
- package/dist/pages/OrderConfirmationPage.d.ts.map +1 -1
- package/dist/pages/ProductDetailPage.d.ts.map +1 -1
- package/dist/presets.d.ts +5 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/sections.d.ts +45 -2
- package/dist/sections.d.ts.map +1 -1
- package/dist/stores/cart.store.d.ts +5 -0
- package/dist/stores/cart.store.d.ts.map +1 -1
- package/dist/testids.d.ts +21 -0
- package/dist/testids.d.ts.map +1 -1
- package/dist/theme.d.ts +21 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/workflows/checkout.d.ts.map +1 -1
- package/package.json +10 -6
- package/src/auth.ts +47 -10
- package/src/blocks.tsx +7 -3
- package/src/component-contracts.ts +140 -0
- package/src/component-selectors.ts +31 -0
- package/src/components/CartDrawer.tsx +2 -1
- package/src/components/ProductCard.tsx +39 -23
- package/src/components/ProductEnquiryForm.tsx +175 -0
- package/src/components/ProductGrid.tsx +12 -3
- package/src/components/RelatedProducts.tsx +13 -4
- package/src/components/SmoothImage.tsx +59 -0
- package/src/components/StorefrontFooter.tsx +24 -14
- package/src/components/StorefrontHeader.tsx +74 -42
- package/src/components/sections/CategoryShowcase.tsx +3 -2
- package/src/components/sections/HeroSection.tsx +18 -2
- package/src/components/sections/MediaCarousel.tsx +141 -0
- package/src/components/sections/MiscSections.tsx +2 -1
- package/src/components/sections/ProductRail.tsx +11 -3
- package/src/components/sections/ProductSlider.tsx +119 -0
- package/src/config.ts +65 -24
- package/src/createStorefrontApp.tsx +80 -10
- package/src/define.ts +34 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useStorefront.ts +58 -0
- package/src/index.ts +62 -11
- package/src/pages/CheckoutPage.tsx +2 -1
- package/src/pages/HomePage.tsx +6 -2
- package/src/pages/MyPurchasesPage.tsx +2 -1
- package/src/pages/OrderConfirmationPage.tsx +2 -1
- package/src/pages/ProductDetailPage.tsx +138 -62
- package/src/presets.ts +66 -0
- package/src/scaffold.tsx +1 -1
- package/src/sections.ts +49 -2
- package/src/stores/cart.store.ts +16 -1
- package/src/testids.ts +21 -0
- package/src/theme.ts +36 -2
- package/src/workflows/checkout.ts +5 -2
- package/dist/slot-contracts.d.ts +0 -34
- package/dist/slot-contracts.d.ts.map +0 -1
- package/src/slot-contracts.ts +0 -20
|
@@ -2,19 +2,24 @@ import React, { useEffect, useState } from 'react'
|
|
|
2
2
|
import { ChevronLeft, RefreshCcw, ShieldCheck, Truck } from 'lucide-react'
|
|
3
3
|
import { useProduct } from '../hooks/useProduct'
|
|
4
4
|
import { useCartStore } from '../stores/cart.store'
|
|
5
|
-
import { useStorefrontConfig } from '../config'
|
|
5
|
+
import { getStorefrontComponents, useStorefrontConfig } from '../config'
|
|
6
6
|
import { useStorefrontHead } from '../hooks/useStorefrontHead'
|
|
7
|
+
import { useStorefrontActions } from '../hooks/useStorefront'
|
|
7
8
|
import { Link } from '../router'
|
|
8
9
|
import { Price } from '../components/Price'
|
|
9
10
|
import { QuantityInput } from '../components/QuantityInput'
|
|
10
11
|
import { ProductOptionSelector } from '../components/ProductOptionSelector'
|
|
11
12
|
import { ProductSpecs } from '../components/ProductSpecs'
|
|
12
13
|
import { RelatedProducts } from '../components/RelatedProducts'
|
|
14
|
+
import { ProductEnquiryForm } from '../components/ProductEnquiryForm'
|
|
15
|
+
import { SmoothImage } from '../components/SmoothImage'
|
|
13
16
|
import { getProductOptionGroups, type ProductOptionSelection } from '../product-options'
|
|
17
|
+
import { storefrontComponentContracts } from '../component-selectors'
|
|
14
18
|
import { TID } from '../testids'
|
|
15
19
|
|
|
16
20
|
export function ProductDetailPage({ slug }: { slug: string }) {
|
|
17
21
|
const config = useStorefrontConfig()
|
|
22
|
+
const actions = useStorefrontActions()
|
|
18
23
|
const { product, loading } = useProduct(slug)
|
|
19
24
|
const addItem = useCartStore((s) => s.addItem)
|
|
20
25
|
const [qty, setQty] = useState(1)
|
|
@@ -61,21 +66,52 @@ export function ProductDetailPage({ slug }: { slug: string }) {
|
|
|
61
66
|
const soldOut = product.inventoryCount <= 0
|
|
62
67
|
const lowStock = !soldOut && product.inventoryCount <= 5
|
|
63
68
|
const image = product.images.find((i) => i.isPrimary) ?? product.images[0]
|
|
69
|
+
const components = getStorefrontComponents(config)
|
|
70
|
+
const ProductDetailComponent = components.ProductDetail
|
|
71
|
+
const addToCart = () => addItem(product, qty, selectedOptions)
|
|
72
|
+
const openEnquiry = () => {
|
|
73
|
+
document.getElementById('storefront-product-enquiry')?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
74
|
+
}
|
|
64
75
|
const discountPct =
|
|
65
76
|
product.compareAtPrice && product.compareAtPrice > product.price
|
|
66
77
|
? Math.round((1 - product.price / product.compareAtPrice) * 100)
|
|
67
78
|
: 0
|
|
68
79
|
|
|
80
|
+
if (ProductDetailComponent) {
|
|
81
|
+
return (
|
|
82
|
+
<ProductDetailComponent
|
|
83
|
+
product={product}
|
|
84
|
+
config={config}
|
|
85
|
+
commerceMode={config.commerceMode}
|
|
86
|
+
primaryImage={image}
|
|
87
|
+
optionGroups={optionGroups}
|
|
88
|
+
selectedOptions={selectedOptions}
|
|
89
|
+
setSelectedOptions={setSelectedOptions}
|
|
90
|
+
quantity={qty}
|
|
91
|
+
setQuantity={setQty}
|
|
92
|
+
soldOut={soldOut}
|
|
93
|
+
lowStock={lowStock}
|
|
94
|
+
actions={actions}
|
|
95
|
+
addToCart={addToCart}
|
|
96
|
+
openEnquiry={openEnquiry}
|
|
97
|
+
/>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
69
101
|
return (
|
|
70
|
-
<main className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
|
|
102
|
+
<main {...storefrontComponentContracts.productDetail.root} className="mx-auto max-w-5xl px-4 py-8 sm:px-6">
|
|
71
103
|
<Link to="/" className="mb-6 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
|
|
72
104
|
<ChevronLeft className="h-4 w-4" /> Continuar comprando
|
|
73
105
|
</Link>
|
|
74
106
|
|
|
75
107
|
<div className="grid animate-fade-up gap-10 md:grid-cols-2">
|
|
76
|
-
<div
|
|
108
|
+
<div
|
|
109
|
+
{...storefrontComponentContracts.productDetail.gallery}
|
|
110
|
+
className="group overflow-hidden border bg-muted"
|
|
111
|
+
style={{ borderRadius: 'var(--sf-radius-card)' }}
|
|
112
|
+
>
|
|
77
113
|
{image && (
|
|
78
|
-
<
|
|
114
|
+
<SmoothImage
|
|
79
115
|
src={image.url}
|
|
80
116
|
alt={image.altText ?? product.name}
|
|
81
117
|
className="aspect-square w-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
|
@@ -100,43 +136,47 @@ export function ProductDetailPage({ slug }: { slug: string }) {
|
|
|
100
136
|
{product.name}
|
|
101
137
|
</h1>
|
|
102
138
|
|
|
103
|
-
|
|
104
|
-
<div className="
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
{config.commerceMode === 'checkout' && (
|
|
140
|
+
<div className="mt-3 flex items-center gap-3">
|
|
141
|
+
<div className="text-xl">
|
|
142
|
+
<Price
|
|
143
|
+
value={product.price}
|
|
144
|
+
compareAt={product.compareAtPrice}
|
|
145
|
+
testId={TID.pdpPrice}
|
|
146
|
+
compareTestId={TID.pdpComparePrice}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
{discountPct > 0 && (
|
|
150
|
+
<span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-bold text-primary">
|
|
151
|
+
−{discountPct}%
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
111
154
|
</div>
|
|
112
|
-
|
|
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>
|
|
155
|
+
)}
|
|
118
156
|
|
|
119
157
|
<p data-testid={TID.pdpDescription} className="mt-5 leading-relaxed text-muted-foreground">
|
|
120
158
|
{product.description}
|
|
121
159
|
</p>
|
|
122
160
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<span className="
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<span className="
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<span className="
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
{config.commerceMode === 'checkout' && (
|
|
162
|
+
<div className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
|
163
|
+
{soldOut ? (
|
|
164
|
+
<span className="inline-flex items-center gap-2 font-medium text-muted-foreground">
|
|
165
|
+
<span className="h-2 w-2 rounded-full bg-muted-foreground" /> Sem estoque
|
|
166
|
+
</span>
|
|
167
|
+
) : lowStock ? (
|
|
168
|
+
<span className="inline-flex items-center gap-2 font-semibold text-amber-600">
|
|
169
|
+
<span className="h-2 w-2 animate-pulse rounded-full bg-amber-500" />
|
|
170
|
+
Últimas unidades — só {product.inventoryCount} restantes
|
|
171
|
+
</span>
|
|
172
|
+
) : (
|
|
173
|
+
<span className="inline-flex items-center gap-2 font-medium text-emerald-600">
|
|
174
|
+
<span className="h-2 w-2 rounded-full bg-emerald-500" /> Em estoque
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
{product.sku && <span className="text-muted-foreground">SKU: {product.sku}</span>}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
140
180
|
|
|
141
181
|
<ProductOptionSelector
|
|
142
182
|
groups={optionGroups}
|
|
@@ -144,39 +184,75 @@ export function ProductDetailPage({ slug }: { slug: string }) {
|
|
|
144
184
|
onChange={setSelectedOptions}
|
|
145
185
|
/>
|
|
146
186
|
|
|
147
|
-
<div className="mt-8
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
<div {...storefrontComponentContracts.productDetail.actions} className="mt-8">
|
|
188
|
+
{config.commerceMode === 'checkout' ? (
|
|
189
|
+
<div className="flex items-center gap-4">
|
|
190
|
+
<QuantityInput
|
|
191
|
+
value={qty}
|
|
192
|
+
onChange={setQty}
|
|
193
|
+
max={soldOut ? 1 : product.inventoryCount}
|
|
194
|
+
testId={TID.pdpQtyInput}
|
|
195
|
+
/>
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
data-testid={TID.pdpAddToCart}
|
|
199
|
+
disabled={soldOut}
|
|
200
|
+
onClick={addToCart}
|
|
201
|
+
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"
|
|
202
|
+
style={{ borderRadius: 'var(--sf-radius-button)' }}
|
|
203
|
+
>
|
|
204
|
+
{soldOut ? 'Esgotado' : 'Adicionar ao carrinho'}
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
) : config.commerceMode === 'enquiry' ? (
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
{...storefrontComponentContracts.productDetail.enquiryButton}
|
|
211
|
+
onClick={openEnquiry}
|
|
212
|
+
className="sf-cta w-full bg-primary py-3.5 font-semibold text-primary-foreground shadow-md transition-all hover:-translate-y-0.5 hover:shadow-lg"
|
|
213
|
+
style={{ borderRadius: 'var(--sf-radius-button)' }}
|
|
214
|
+
>
|
|
215
|
+
{config.enquiry.label}
|
|
216
|
+
</button>
|
|
217
|
+
) : (
|
|
218
|
+
<Link
|
|
219
|
+
to={config.catalogPath}
|
|
220
|
+
className="inline-flex border px-5 py-3 text-sm font-semibold transition-colors hover:bg-muted"
|
|
221
|
+
style={{ borderRadius: 'var(--sf-radius-button)' }}
|
|
222
|
+
>
|
|
223
|
+
Voltar ao catálogo
|
|
224
|
+
</Link>
|
|
225
|
+
)}
|
|
164
226
|
</div>
|
|
165
227
|
|
|
166
|
-
|
|
167
|
-
<div className="
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
<
|
|
228
|
+
{config.commerceMode === 'checkout' && (
|
|
229
|
+
<div className="mt-6 grid grid-cols-3 gap-3 border-t pt-6 text-center">
|
|
230
|
+
<div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
|
|
231
|
+
<Truck className="h-5 w-5 text-primary" /> Envio rápido
|
|
232
|
+
</div>
|
|
233
|
+
<div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
|
|
234
|
+
<RefreshCcw className="h-5 w-5 text-primary" /> Troca fácil
|
|
235
|
+
</div>
|
|
236
|
+
<div className="flex flex-col items-center gap-1.5 text-[11px] font-medium text-muted-foreground">
|
|
237
|
+
<ShieldCheck className="h-5 w-5 text-primary" /> Pagamento seguro
|
|
238
|
+
</div>
|
|
175
239
|
</div>
|
|
176
|
-
|
|
240
|
+
)}
|
|
177
241
|
</div>
|
|
178
242
|
</div>
|
|
179
243
|
|
|
244
|
+
{config.commerceMode === 'enquiry' && (
|
|
245
|
+
<section id="storefront-product-enquiry" className="mt-12 border-t pt-10">
|
|
246
|
+
<div className="mx-auto max-w-3xl">
|
|
247
|
+
<h2 className="sf-heading text-2xl font-semibold">Need more information?</h2>
|
|
248
|
+
<p className="mt-2 text-muted-foreground">Send an enquiry about this product.</p>
|
|
249
|
+
<div className="mt-6">
|
|
250
|
+
<ProductEnquiryForm product={product} />
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</section>
|
|
254
|
+
)}
|
|
255
|
+
|
|
180
256
|
<ProductSpecs product={product} />
|
|
181
257
|
<RelatedProducts product={product} />
|
|
182
258
|
</main>
|
package/src/presets.ts
CHANGED
|
@@ -248,11 +248,77 @@ export const atelierTemplate: StorefrontTemplate = {
|
|
|
248
248
|
}),
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/** food — warm appetizing delivery (steakhouse / burger / comfort food): dark inviting
|
|
252
|
+
* header, ember CTA, cream canvas, rounded cards, menu-first home with "mais pedidos"
|
|
253
|
+
* rail and social proof. Built for restaurant/food stores — override to go fully dark. */
|
|
254
|
+
export const foodTemplate: StorefrontTemplate = {
|
|
255
|
+
id: 'food',
|
|
256
|
+
inspiration: 'Delivery / restaurant storefront',
|
|
257
|
+
announcement: 'ENTREGA GRÁTIS ACIMA DE R$ 150 · PEÇA JÁ',
|
|
258
|
+
theme: {
|
|
259
|
+
name: 'food',
|
|
260
|
+
colors: {
|
|
261
|
+
background: '30 33% 97%',
|
|
262
|
+
foreground: '20 14% 15%',
|
|
263
|
+
primary: '14 84% 50%',
|
|
264
|
+
primaryForeground: '0 0% 100%',
|
|
265
|
+
card: '30 33% 99%',
|
|
266
|
+
cardForeground: '20 14% 15%',
|
|
267
|
+
muted: '28 20% 92%',
|
|
268
|
+
mutedForeground: '22 10% 42%',
|
|
269
|
+
border: '28 18% 85%',
|
|
270
|
+
headerBackground: '20 14% 11%',
|
|
271
|
+
headerForeground: '30 24% 96%',
|
|
272
|
+
announcementBackground: '14 84% 50%',
|
|
273
|
+
announcementForeground: '0 0% 100%',
|
|
274
|
+
},
|
|
275
|
+
font: { heading: 'Poppins', body: 'Inter' },
|
|
276
|
+
radius: 'round',
|
|
277
|
+
header: { variant: 'classic' },
|
|
278
|
+
productCard: { style: 'card', imageAspect: 'square' },
|
|
279
|
+
},
|
|
280
|
+
home: (storeName, images) => ({
|
|
281
|
+
sections: [
|
|
282
|
+
{
|
|
283
|
+
type: 'hero',
|
|
284
|
+
variant: 'banner',
|
|
285
|
+
height: 'tall',
|
|
286
|
+
slides: [
|
|
287
|
+
{ title: 'Sabor de verdade, na sua porta', subtitle: `${storeName} — feito na hora, do jeito que você ama.`, cta: 'Ver cardápio', href: '/catalog', hue: 20, image: images?.hero?.[0] },
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
type: 'benefits',
|
|
292
|
+
items: [
|
|
293
|
+
{ icon: 'Flame', title: 'Feito na hora', text: 'Preparado com capricho' },
|
|
294
|
+
{ icon: 'Truck', title: 'Entrega rápida', text: 'Frete grátis acima de R$ 150' },
|
|
295
|
+
{ icon: 'CreditCard', title: 'Pagamento fácil', text: 'Pix, cartão e mais' },
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
{ type: 'categories', style: 'tiles', title: 'Nosso cardápio' },
|
|
299
|
+
{ type: 'products', title: 'Mais pedidos', eyebrow: 'favoritos da casa', filter: 'all', limit: 8 },
|
|
300
|
+
{ type: 'banner', title: 'Combo do dia', eyebrow: 'oferta', subtitle: 'Seleção especial com desconto', cta: 'Aproveitar', hue: 14, image: images?.banners?.[0] },
|
|
301
|
+
{ type: 'products', title: 'Novidades no cardápio', filter: 'new', limit: 4 },
|
|
302
|
+
{
|
|
303
|
+
type: 'testimonials',
|
|
304
|
+
title: 'O que dizem por aí',
|
|
305
|
+
items: [
|
|
306
|
+
{ quote: 'Chegou quentinho e absurdo de bom. Virei cliente!', author: 'Rafael M.' },
|
|
307
|
+
{ quote: 'Sabor de restaurante em casa, sem complicação.', author: 'Beatriz L.' },
|
|
308
|
+
{ quote: 'Entrega rápida e porção generosa. Recomendo demais.', author: 'Diego S.' },
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
{ type: 'newsletter', title: 'Receba nossas ofertas', subtitle: 'Promoções e novidades do cardápio, direto no seu e-mail.' },
|
|
312
|
+
],
|
|
313
|
+
}),
|
|
314
|
+
}
|
|
315
|
+
|
|
251
316
|
export const storefrontTemplates = {
|
|
252
317
|
mare: mareTemplate,
|
|
253
318
|
sertao: sertaoTemplate,
|
|
254
319
|
volt: voltTemplate,
|
|
255
320
|
atelier: atelierTemplate,
|
|
321
|
+
food: foodTemplate,
|
|
256
322
|
} as const
|
|
257
323
|
|
|
258
324
|
export type StorefrontTemplateId = keyof typeof storefrontTemplates
|
package/src/scaffold.tsx
CHANGED
|
@@ -80,7 +80,7 @@ function manifestToStorefrontConfig(manifest: AppManifest, surfaceName: string):
|
|
|
80
80
|
provider: manifest.backend?.provider as StorefrontBackendProvider,
|
|
81
81
|
url: manifest.backend?.url,
|
|
82
82
|
},
|
|
83
|
-
|
|
83
|
+
components: runtimeConfig?.components,
|
|
84
84
|
routes: runtimeConfig?.routes,
|
|
85
85
|
provider: runtimeConfig?.provider,
|
|
86
86
|
auth: runtimeConfig?.auth,
|
package/src/sections.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HeroVariant } from './theme'
|
|
2
|
+
import type React from 'react'
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// Home-page section blueprint — the storefront's PageConfig equivalent.
|
|
@@ -19,8 +20,27 @@ export interface HeroSlide {
|
|
|
19
20
|
boxed?: boolean
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
export
|
|
23
|
+
export interface MediaCarouselItem {
|
|
24
|
+
title: string
|
|
25
|
+
subtitle?: string
|
|
26
|
+
eyebrow?: string
|
|
27
|
+
cta?: string
|
|
28
|
+
href?: string
|
|
29
|
+
image: string
|
|
30
|
+
imageAlt?: string
|
|
31
|
+
align?: 'start' | 'center' | 'end'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type StorefrontSection =
|
|
23
35
|
| { type: 'hero'; variant: HeroVariant; slides: HeroSlide[]; height?: 'tall' | 'medium' }
|
|
36
|
+
| {
|
|
37
|
+
type: 'mediaCarousel'
|
|
38
|
+
items: MediaCarouselItem[]
|
|
39
|
+
height?: 'medium' | 'tall' | 'screen'
|
|
40
|
+
autoplayMs?: number
|
|
41
|
+
overlay?: 'dark' | 'soft' | 'none'
|
|
42
|
+
className?: string
|
|
43
|
+
}
|
|
24
44
|
| { type: 'categories'; style: 'bubbles' | 'tiles'; title?: string }
|
|
25
45
|
| {
|
|
26
46
|
type: 'products'
|
|
@@ -32,14 +52,30 @@ export type HomeSection =
|
|
|
32
52
|
filter?: 'sale' | 'new' | 'all'
|
|
33
53
|
limit?: number
|
|
34
54
|
}
|
|
55
|
+
| {
|
|
56
|
+
type: 'productSlider'
|
|
57
|
+
title: string
|
|
58
|
+
subtitle?: string
|
|
59
|
+
eyebrow?: string
|
|
60
|
+
filter?: 'sale' | 'new' | 'all'
|
|
61
|
+
categoryName?: string
|
|
62
|
+
collection?: string
|
|
63
|
+
limit?: number
|
|
64
|
+
cta?: string
|
|
65
|
+
href?: string
|
|
66
|
+
className?: string
|
|
67
|
+
}
|
|
35
68
|
| { type: 'benefits'; items: Array<{ icon: string; title: string; text?: string }> }
|
|
36
69
|
| { type: 'banner'; title: string; subtitle?: string; eyebrow?: string; cta?: string; href?: string; image?: string; hue?: number }
|
|
37
70
|
| { type: 'manifesto'; title?: string; text: string }
|
|
38
71
|
| { type: 'testimonials'; title?: string; items: Array<{ quote: string; author: string }> }
|
|
39
72
|
| { type: 'newsletter'; title?: string; subtitle?: string }
|
|
40
73
|
|
|
74
|
+
/** @deprecated Use StorefrontSection. Kept for existing generated stores. */
|
|
75
|
+
export type HomeSection = StorefrontSection
|
|
76
|
+
|
|
41
77
|
export interface HomeConfig {
|
|
42
|
-
sections:
|
|
78
|
+
sections: readonly StorefrontSection[]
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
export interface NavLink {
|
|
@@ -51,6 +87,17 @@ export interface FooterConfig {
|
|
|
51
87
|
about?: string
|
|
52
88
|
contact?: { phone?: string; email?: string; address?: string }
|
|
53
89
|
social?: Array<{ label: string; href: string }>
|
|
90
|
+
credit?: React.ReactNode
|
|
91
|
+
labels?: {
|
|
92
|
+
categories?: string
|
|
93
|
+
navigation?: string
|
|
94
|
+
contact?: string
|
|
95
|
+
viewAll?: string
|
|
96
|
+
purchases?: string
|
|
97
|
+
privacy?: string
|
|
98
|
+
terms?: string
|
|
99
|
+
returns?: string
|
|
100
|
+
}
|
|
54
101
|
}
|
|
55
102
|
|
|
56
103
|
/** Wide gradient SVG data-URI — deterministic hero/banner art, no network. */
|
package/src/stores/cart.store.ts
CHANGED
|
@@ -40,11 +40,16 @@ export interface CartState {
|
|
|
40
40
|
clear(): void
|
|
41
41
|
openDrawer(): void
|
|
42
42
|
closeDrawer(): void
|
|
43
|
+
/** Drop persisted lines the active backend can no longer resolve (e.g. a cart
|
|
44
|
+
* built in an earlier mock-mode session, whose ids don't exist live). The
|
|
45
|
+
* resolver returns false only for a definitive "not found"; on error it must
|
|
46
|
+
* return true so a transient failure never discards a legit line. */
|
|
47
|
+
reconcile(resolve: (line: CartLine) => Promise<boolean>): Promise<void>
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export const useCartStore = create<CartState>()(
|
|
46
51
|
persist(
|
|
47
|
-
(set) => ({
|
|
52
|
+
(set, get) => ({
|
|
48
53
|
lines: [],
|
|
49
54
|
discountCode: null,
|
|
50
55
|
discountPercent: 0,
|
|
@@ -106,6 +111,16 @@ export const useCartStore = create<CartState>()(
|
|
|
106
111
|
clear: () => set({ lines: [], discountCode: null, discountPercent: 0 }),
|
|
107
112
|
openDrawer: () => set({ isOpen: true }),
|
|
108
113
|
closeDrawer: () => set({ isOpen: false, justAddedLineId: null }),
|
|
114
|
+
|
|
115
|
+
reconcile: async (resolve) => {
|
|
116
|
+
const lines = get().lines
|
|
117
|
+
if (lines.length === 0) return
|
|
118
|
+
const keep = await Promise.all(
|
|
119
|
+
lines.map((line) => resolve(line).catch(() => true)),
|
|
120
|
+
)
|
|
121
|
+
const kept = lines.filter((_, i) => keep[i])
|
|
122
|
+
if (kept.length !== lines.length) set({ lines: kept })
|
|
123
|
+
},
|
|
109
124
|
}),
|
|
110
125
|
{
|
|
111
126
|
name: 'fayz.storefront.cart.v1',
|
package/src/testids.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
export const TID = {
|
|
4
4
|
// header
|
|
5
5
|
headerSearch: 'header-search',
|
|
6
|
+
headerSearchToggle: 'header-search-toggle',
|
|
6
7
|
cartButton: 'cart-button',
|
|
7
8
|
cartCount: 'cart-count',
|
|
8
9
|
accountLink: 'account-link',
|
|
@@ -15,6 +16,13 @@ export const TID = {
|
|
|
15
16
|
hero: 'home-hero',
|
|
16
17
|
heroPrev: 'hero-prev',
|
|
17
18
|
heroNext: 'hero-next',
|
|
19
|
+
mediaCarousel: 'media-carousel',
|
|
20
|
+
mediaCarouselPrev: 'media-carousel-prev',
|
|
21
|
+
mediaCarouselNext: 'media-carousel-next',
|
|
22
|
+
productSlider: 'product-slider',
|
|
23
|
+
productSliderViewport: 'product-slider-viewport',
|
|
24
|
+
productSliderPrev: 'product-slider-prev',
|
|
25
|
+
productSliderNext: 'product-slider-next',
|
|
18
26
|
categoryShowcase: 'category-showcase',
|
|
19
27
|
productRail: 'product-rail',
|
|
20
28
|
benefits: 'benefits-row',
|
|
@@ -49,6 +57,19 @@ export const TID = {
|
|
|
49
57
|
productOptionValue: (id: string, value: string) => `product-option-${id.toLowerCase().replace(/\s+/g, '-')}-${value.toLowerCase().replace(/\s+/g, '-')}`,
|
|
50
58
|
pdpQtyInput: 'pdp-qty-input',
|
|
51
59
|
pdpAddToCart: 'pdp-add-to-cart',
|
|
60
|
+
pdpRoot: 'pdp-root',
|
|
61
|
+
pdpGallery: 'pdp-gallery',
|
|
62
|
+
pdpActions: 'pdp-actions',
|
|
63
|
+
pdpEnquiryButton: 'pdp-enquiry-button',
|
|
64
|
+
enquiryForm: 'enquiry-form',
|
|
65
|
+
enquiryName: 'enquiry-name',
|
|
66
|
+
enquiryEmail: 'enquiry-email',
|
|
67
|
+
enquiryPhone: 'enquiry-phone',
|
|
68
|
+
enquirySubject: 'enquiry-subject',
|
|
69
|
+
enquiryMessage: 'enquiry-message',
|
|
70
|
+
enquirySubmit: 'enquiry-submit',
|
|
71
|
+
enquirySuccess: 'enquiry-success',
|
|
72
|
+
enquiryError: 'enquiry-error',
|
|
52
73
|
// cart
|
|
53
74
|
cartDrawer: 'cart-drawer',
|
|
54
75
|
cartLine: 'cart-line',
|
package/src/theme.ts
CHANGED
|
@@ -33,6 +33,10 @@ export interface StorefrontThemeColors {
|
|
|
33
33
|
/** Announcement bar */
|
|
34
34
|
announcementBackground?: string
|
|
35
35
|
announcementForeground?: string
|
|
36
|
+
/** Secondary brand accent (e.g. a gourmet gold beside the primary). Emitted
|
|
37
|
+
* as `--sf-accent` / `--sf-accent-foreground` and the `accent` tailwind token. */
|
|
38
|
+
accent?: string
|
|
39
|
+
accentForeground?: string
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
export interface StorefrontThemeFont {
|
|
@@ -40,7 +44,11 @@ export interface StorefrontThemeFont {
|
|
|
40
44
|
heading: string
|
|
41
45
|
/** Body font, e.g. 'Rubik' */
|
|
42
46
|
body: string
|
|
43
|
-
/**
|
|
47
|
+
/** Optional oversized display face for hero words (emitted as `--sf-font-display`). */
|
|
48
|
+
display?: string
|
|
49
|
+
/** Optional serif accent for quotes/editorial (emitted as `--sf-font-serif`). */
|
|
50
|
+
serif?: string
|
|
51
|
+
/** Google Fonts families to load (defaults to heading+body+display+serif) */
|
|
44
52
|
googleFonts?: string[]
|
|
45
53
|
/** Fallback stack — default sans-serif; use 'serif' for editorial themes */
|
|
46
54
|
fallback?: 'sans-serif' | 'serif'
|
|
@@ -56,6 +64,13 @@ export interface StorefrontTheme {
|
|
|
56
64
|
variant?: HeaderVariant
|
|
57
65
|
/** Uppercase nav links (premium retail tone) */
|
|
58
66
|
uppercaseNav?: boolean
|
|
67
|
+
/** Show category shortcuts after primary nav links. Default true. */
|
|
68
|
+
showCategories?: boolean
|
|
69
|
+
/** Show the storefront search in the header. Default true. */
|
|
70
|
+
showSearch?: boolean
|
|
71
|
+
/** Header search presentation: a full input ('full', default) or a compact
|
|
72
|
+
* icon button that expands into an input on click ('icon'). */
|
|
73
|
+
searchStyle?: 'full' | 'icon'
|
|
59
74
|
}
|
|
60
75
|
productCard?: {
|
|
61
76
|
style?: CardStyle
|
|
@@ -63,6 +78,11 @@ export interface StorefrontTheme {
|
|
|
63
78
|
}
|
|
64
79
|
/** Uppercase CTA buttons with letter-spacing (Uyuni/Flex tone) */
|
|
65
80
|
uppercaseButtons?: boolean
|
|
81
|
+
/** Brand-token passthrough. Each entry is emitted verbatim as a `--<key>` CSS
|
|
82
|
+
* custom property, so a store can add gradients, glows, extra colors, or
|
|
83
|
+
* letter-spacing the fixed schema doesn't enumerate — without editing the SDK.
|
|
84
|
+
* e.g. { 'grad-ember': 'linear-gradient(...)', 'glow-ember': '0 0 0 1px ...' }. */
|
|
85
|
+
tokens?: Record<string, string>
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
const RADIUS_MAP: Record<StorefrontRadius, { button: string; card: string; input: string }> = {
|
|
@@ -86,6 +106,8 @@ const COLOR_VAR_MAP: Record<keyof StorefrontThemeColors, string> = {
|
|
|
86
106
|
headerForeground: '--sf-header-fg',
|
|
87
107
|
announcementBackground: '--sf-announcement-bg',
|
|
88
108
|
announcementForeground: '--sf-announcement-fg',
|
|
109
|
+
accent: '--sf-accent',
|
|
110
|
+
accentForeground: '--sf-accent-foreground',
|
|
89
111
|
}
|
|
90
112
|
|
|
91
113
|
export function themeToCss(theme: StorefrontTheme): string {
|
|
@@ -107,6 +129,14 @@ export function themeToCss(theme: StorefrontTheme): string {
|
|
|
107
129
|
const fallback = theme.font.fallback ?? 'sans-serif'
|
|
108
130
|
lines.push(`--font-family: '${theme.font.body}', ${fallback};`)
|
|
109
131
|
lines.push(`--sf-font-heading: '${theme.font.heading}', ${fallback};`)
|
|
132
|
+
if (theme.font.display) lines.push(`--sf-font-display: '${theme.font.display}', ${fallback};`)
|
|
133
|
+
if (theme.font.serif) lines.push(`--sf-font-serif: '${theme.font.serif}', serif;`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Brand-token passthrough — emit each entry as a raw CSS custom property.
|
|
137
|
+
for (const [key, value] of Object.entries(theme.tokens ?? {})) {
|
|
138
|
+
const name = key.startsWith('--') ? key : `--${key}`
|
|
139
|
+
lines.push(`${name}: ${value};`)
|
|
110
140
|
}
|
|
111
141
|
|
|
112
142
|
const radius = RADIUS_MAP[theme.radius ?? 'soft']
|
|
@@ -131,7 +161,11 @@ export function themeToCss(theme: StorefrontTheme): string {
|
|
|
131
161
|
|
|
132
162
|
function googleFontsHref(theme: StorefrontTheme): string | null {
|
|
133
163
|
if (!theme.font) return null
|
|
134
|
-
const families =
|
|
164
|
+
const families =
|
|
165
|
+
theme.font.googleFonts ??
|
|
166
|
+
[theme.font.heading, theme.font.body, theme.font.display, theme.font.serif].filter(
|
|
167
|
+
(f): f is string => Boolean(f),
|
|
168
|
+
)
|
|
135
169
|
const unique = [...new Set(families)]
|
|
136
170
|
const params = unique
|
|
137
171
|
.map((f) => `family=${encodeURIComponent(f).replace(/%20/g, '+')}:wght@300;400;500;600;700`)
|
|
@@ -67,7 +67,7 @@ export async function placeStorefrontOrder({
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Trusted placement: send only product ids + quantities. The provider
|
|
70
|
-
// (shop_place_order RPC on Supabase, in-memory
|
|
70
|
+
// (shop_place_order RPC on Supabase, in-memory provider in mock) re-reads
|
|
71
71
|
// prices, validates the discount, and decrements inventory server-side.
|
|
72
72
|
const order = await provider.placeOrder({
|
|
73
73
|
customerId: customerId ?? undefined,
|
|
@@ -84,7 +84,10 @@ export async function placeStorefrontOrder({
|
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
if (markPaid) {
|
|
87
|
-
|
|
87
|
+
// Prefer the RPC seam — anon storefronts have no UPDATE grant on the
|
|
88
|
+
// orders table, so a direct updateOrder 401s on pool backends.
|
|
89
|
+
if (provider.confirmPayment) await provider.confirmPayment(order.id)
|
|
90
|
+
else await provider.updateOrder(order.id, { financialStatus: 'paid' })
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
return { order, customerId: customerId ?? order.customerId ?? '' }
|
package/dist/slot-contracts.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stable selectors that custom storefront slots must preserve.
|
|
3
|
-
*
|
|
4
|
-
* Apps can fully customize the visual tree, but catalog/product-rail slots still
|
|
5
|
-
* need these anchors so checkout smoke tests, QA, and agents can operate stores.
|
|
6
|
-
*/
|
|
7
|
-
export declare const productCardSlotContract: {
|
|
8
|
-
readonly root: {
|
|
9
|
-
readonly 'data-testid': "product-card";
|
|
10
|
-
};
|
|
11
|
-
readonly name: {
|
|
12
|
-
readonly 'data-testid': "product-card-name";
|
|
13
|
-
};
|
|
14
|
-
readonly priceTestId: "product-card-price";
|
|
15
|
-
readonly addButton: {
|
|
16
|
-
readonly 'data-testid': "product-card-add";
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
export declare const storefrontSlotContracts: {
|
|
20
|
-
readonly productCard: {
|
|
21
|
-
readonly root: {
|
|
22
|
-
readonly 'data-testid': "product-card";
|
|
23
|
-
};
|
|
24
|
-
readonly name: {
|
|
25
|
-
readonly 'data-testid': "product-card-name";
|
|
26
|
-
};
|
|
27
|
-
readonly priceTestId: "product-card-price";
|
|
28
|
-
readonly addButton: {
|
|
29
|
-
readonly 'data-testid': "product-card-add";
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
};
|
|
33
|
-
export type ProductCardSlotContract = typeof productCardSlotContract;
|
|
34
|
-
//# sourceMappingURL=slot-contracts.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"slot-contracts.d.ts","sourceRoot":"","sources":["../src/slot-contracts.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;CAK1B,CAAA;AAEV,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;CAE1B,CAAA;AAEV,MAAM,MAAM,uBAAuB,GAAG,OAAO,uBAAuB,CAAA"}
|