@fayz-ai/storefront 0.6.0 → 0.7.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/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 +904 -185
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +20 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +797 -95
- 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/sections.d.ts +45 -2
- package/dist/sections.d.ts.map +1 -1
- package/dist/testids.d.ts +20 -0
- package/dist/testids.d.ts.map +1 -1
- package/dist/theme.d.ts +4 -0
- package/dist/theme.d.ts.map +1 -1
- package/package.json +7 -9
- 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 +43 -38
- 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 +62 -9
- package/src/define.ts +34 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useStorefront.ts +58 -0
- package/src/index.ts +61 -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/scaffold.tsx +1 -1
- package/src/sections.ts +49 -2
- package/src/testids.ts +20 -0
- package/src/theme.ts +4 -0
- package/src/workflows/checkout.ts +1 -1
- package/dist/slot-contracts.d.ts +0 -34
- package/dist/slot-contracts.d.ts.map +0 -1
- package/src/slot-contracts.ts +0 -20
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getShopProvider } from '@fayz-ai/shop/runtime'
|
|
2
|
+
import type { CreateProductEnquiryInput } from '@fayz-ai/shop/types'
|
|
3
|
+
import { useStorefrontConfig } from '../config'
|
|
4
|
+
import { navigateTo } from '../router'
|
|
5
|
+
import { useCatalogStore } from '../stores/catalog.store'
|
|
6
|
+
import { useCartStore } from '../stores/cart.store'
|
|
7
|
+
import type { ProductOptionSelection } from '../product-options'
|
|
8
|
+
import type { Product } from '@fayz-ai/shop/types'
|
|
9
|
+
|
|
10
|
+
export function useStorefront() {
|
|
11
|
+
return useStorefrontConfig()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useCatalog() {
|
|
15
|
+
const config = useStorefrontConfig()
|
|
16
|
+
const catalog = useCatalogStore()
|
|
17
|
+
return {
|
|
18
|
+
...catalog,
|
|
19
|
+
catalogPath: config.catalogPath,
|
|
20
|
+
config,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useCart() {
|
|
25
|
+
return useCartStore()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useEnquiry() {
|
|
29
|
+
const config = useStorefrontConfig()
|
|
30
|
+
const createProductEnquiry = async (input: CreateProductEnquiryInput) => {
|
|
31
|
+
const provider = getShopProvider()
|
|
32
|
+
if (!provider.createProductEnquiry) {
|
|
33
|
+
throw new Error('The active shop provider does not support product enquiries.')
|
|
34
|
+
}
|
|
35
|
+
return provider.createProductEnquiry(input)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
enquiry: config.enquiry,
|
|
40
|
+
createProductEnquiry,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useStorefrontActions() {
|
|
45
|
+
const addItem = useCartStore((state) => state.addItem)
|
|
46
|
+
const openDrawer = useCartStore((state) => state.openDrawer)
|
|
47
|
+
const closeDrawer = useCartStore((state) => state.closeDrawer)
|
|
48
|
+
const { createProductEnquiry } = useEnquiry()
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
navigateTo,
|
|
52
|
+
addToCart: (product: Product, quantity?: number, options?: ProductOptionSelection) =>
|
|
53
|
+
addItem(product, quantity, options),
|
|
54
|
+
openCart: openDrawer,
|
|
55
|
+
closeCart: closeDrawer,
|
|
56
|
+
createProductEnquiry,
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
1
1
|
// Factory
|
|
2
|
-
export { createStorefrontApp, initStorefrontRuntime, StorefrontShell } from './createStorefrontApp'
|
|
2
|
+
export { createStorefront, createStorefrontApp, initStorefrontRuntime, StorefrontShell } from './createStorefrontApp'
|
|
3
|
+
export type { CreateStorefrontOptions } from './createStorefrontApp'
|
|
4
|
+
export {
|
|
5
|
+
defineStorefrontConfig,
|
|
6
|
+
defineStorefrontComponents,
|
|
7
|
+
defineStorefrontRoutes,
|
|
8
|
+
defineStorefrontSections,
|
|
9
|
+
defineStorefrontApp,
|
|
10
|
+
} from './define'
|
|
3
11
|
export type {
|
|
4
12
|
StorefrontConfig,
|
|
5
13
|
ResolvedStorefrontConfig,
|
|
6
14
|
StorefrontRouteComponentProps,
|
|
7
15
|
StorefrontRouteChrome,
|
|
16
|
+
StorefrontCommerceMode,
|
|
17
|
+
StorefrontImageLoadingConfig,
|
|
18
|
+
StorefrontImageLoadingMode,
|
|
8
19
|
StorefrontRouteDefinition,
|
|
9
20
|
StorefrontRouteKind,
|
|
10
21
|
StorefrontRouteParams,
|
|
22
|
+
StorefrontEnquiryConfig,
|
|
11
23
|
} from './config'
|
|
12
|
-
export {
|
|
24
|
+
export type {
|
|
25
|
+
StorefrontActions,
|
|
26
|
+
StorefrontComponents,
|
|
27
|
+
ProductCardProps,
|
|
28
|
+
ProductDetailProps,
|
|
29
|
+
ProductGalleryProps,
|
|
30
|
+
ProductPriceProps,
|
|
31
|
+
ProductActionsProps,
|
|
32
|
+
ProductEnquiryFormContractProps,
|
|
33
|
+
CategoryCardProps,
|
|
34
|
+
CollectionSectionProps,
|
|
35
|
+
HeroSectionContractProps,
|
|
36
|
+
MediaCarouselProps as MediaCarouselContractProps,
|
|
37
|
+
ProductSliderProps as ProductSliderContractProps,
|
|
38
|
+
StorefrontChromeProps,
|
|
39
|
+
StorefrontStateProps,
|
|
40
|
+
} from './component-contracts'
|
|
41
|
+
export { useStorefrontConfig, useStorefrontConfigOptional } from './config'
|
|
13
42
|
|
|
14
43
|
// Manifest path (renderApp(defineStorefront(config)) / storefront scaffold)
|
|
15
44
|
export { defineStorefront, StorefrontScaffold } from './scaffold'
|
|
@@ -41,17 +70,31 @@ export {
|
|
|
41
70
|
getCustomerAuthAdapter,
|
|
42
71
|
resolveAuthAdapter,
|
|
43
72
|
} from './auth'
|
|
44
|
-
export type { StorefrontAuthAdapter, EstablishSessionOptions } from './auth'
|
|
73
|
+
export type { StorefrontAuthAdapter, StorefrontAuthConfig, EstablishSessionOptions } from './auth'
|
|
45
74
|
export { useCatalogStore } from './stores/catalog.store'
|
|
46
75
|
export type { CatalogState, CatalogSort } from './stores/catalog.store'
|
|
47
76
|
|
|
48
77
|
// Hooks
|
|
49
|
-
export {
|
|
78
|
+
export {
|
|
79
|
+
useProducts,
|
|
80
|
+
useProduct,
|
|
81
|
+
useCategories,
|
|
82
|
+
useStorefront,
|
|
83
|
+
useCatalog,
|
|
84
|
+
useCart,
|
|
85
|
+
useEnquiry,
|
|
86
|
+
useStorefrontActions,
|
|
87
|
+
useMyOrders,
|
|
88
|
+
useDiscountValidator,
|
|
89
|
+
} from './hooks'
|
|
50
90
|
|
|
51
91
|
// Components (for custom layouts)
|
|
52
92
|
export { StorefrontHeader } from './components/StorefrontHeader'
|
|
93
|
+
export { SmoothImage } from './components/SmoothImage'
|
|
94
|
+
export type { SmoothImageProps } from './components/SmoothImage'
|
|
53
95
|
export { ProductCard } from './components/ProductCard'
|
|
54
|
-
export
|
|
96
|
+
export { ProductEnquiryForm } from './components/ProductEnquiryForm'
|
|
97
|
+
export type { ProductEnquiryFormProps } from './components/ProductEnquiryForm'
|
|
55
98
|
export { OrderTrackingTimeline } from './components/OrderTrackingTimeline'
|
|
56
99
|
export { ProductGrid } from './components/ProductGrid'
|
|
57
100
|
export { FiltersPanel } from './components/FiltersPanel'
|
|
@@ -93,7 +136,7 @@ export type {
|
|
|
93
136
|
HeroVariant,
|
|
94
137
|
CardStyle,
|
|
95
138
|
} from './theme'
|
|
96
|
-
export type { HomeSection, HomeConfig, HeroSlide, NavLink, FooterConfig } from './sections'
|
|
139
|
+
export type { StorefrontSection, HomeSection, HomeConfig, HeroSlide, MediaCarouselItem, NavLink, FooterConfig } from './sections'
|
|
97
140
|
export { bannerPlaceholder } from './sections'
|
|
98
141
|
export {
|
|
99
142
|
storefrontTemplates,
|
|
@@ -105,10 +148,14 @@ export {
|
|
|
105
148
|
export type { StorefrontTemplate, StorefrontTemplateId } from './presets'
|
|
106
149
|
|
|
107
150
|
// Home sections (for custom compositions)
|
|
108
|
-
export { HomePage } from './pages/HomePage'
|
|
151
|
+
export { HomePage, StorefrontSections } from './pages/HomePage'
|
|
109
152
|
export { HeroSection } from './components/sections/HeroSection'
|
|
153
|
+
export { MediaCarousel } from './components/sections/MediaCarousel'
|
|
154
|
+
export type { MediaCarouselProps } from './components/sections/MediaCarousel'
|
|
110
155
|
export { CategoryShowcase } from './components/sections/CategoryShowcase'
|
|
111
156
|
export { ProductRail } from './components/sections/ProductRail'
|
|
157
|
+
export { ProductSlider } from './components/sections/ProductSlider'
|
|
158
|
+
export type { ProductSliderProps } from './components/sections/ProductSlider'
|
|
112
159
|
export {
|
|
113
160
|
BenefitsRow,
|
|
114
161
|
PromoBanner,
|
|
@@ -126,10 +173,13 @@ export type { RevealProps } from './motion'
|
|
|
126
173
|
export { formatMoney, roundCents } from './format'
|
|
127
174
|
export { TID } from './testids'
|
|
128
175
|
export {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} from './
|
|
132
|
-
export type {
|
|
176
|
+
productCardComponentContract,
|
|
177
|
+
storefrontComponentContracts,
|
|
178
|
+
} from './component-selectors'
|
|
179
|
+
export type {
|
|
180
|
+
ProductCardComponentContract,
|
|
181
|
+
ProductDetailComponentContract,
|
|
182
|
+
} from './component-selectors'
|
|
133
183
|
|
|
134
184
|
// ---------------------------------------------------------------------------
|
|
135
185
|
// Front-door re-exports: storefront apps depend on @fayz-ai/storefront alone.
|
|
@@ -18,6 +18,7 @@ import { useDiscountValidator } from '../hooks/useDiscountValidator'
|
|
|
18
18
|
import { toast } from '../stores/toast.store'
|
|
19
19
|
import { useStorefrontHead } from '../hooks/useStorefrontHead'
|
|
20
20
|
import { SignInModal } from '../components/SignInModal'
|
|
21
|
+
import { SmoothImage } from '../components/SmoothImage'
|
|
21
22
|
|
|
22
23
|
interface CheckoutForm {
|
|
23
24
|
email: string
|
|
@@ -471,7 +472,7 @@ export function CheckoutPage() {
|
|
|
471
472
|
{cart.lines.map((line) => (
|
|
472
473
|
<li key={line.lineId ?? line.productId} className="flex gap-3 text-sm">
|
|
473
474
|
<div className="relative h-16 w-16 flex-none overflow-hidden rounded-lg border bg-background">
|
|
474
|
-
{line.imageUrl && <
|
|
475
|
+
{line.imageUrl && <SmoothImage src={line.imageUrl} alt={line.name} className="h-full w-full object-cover" />}
|
|
475
476
|
<span className="absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-muted-foreground px-1.5 text-[10px] font-bold text-background">
|
|
476
477
|
{line.quantity}
|
|
477
478
|
</span>
|
package/src/pages/HomePage.tsx
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { renderBlocks } from '@fayz-ai/core'
|
|
3
|
-
import type {
|
|
3
|
+
import type { StorefrontSection } from '../sections'
|
|
4
4
|
import { sectionsToBlocks } from '../blocks'
|
|
5
5
|
|
|
6
6
|
// The home page is now a block tree resolved through the universal block
|
|
7
7
|
// registry (see ../blocks). Section components and their props are unchanged;
|
|
8
8
|
// only the dispatch mechanism moved from a switch to the registry, so the same
|
|
9
9
|
// composition works from a pure-data AppManifest.
|
|
10
|
-
export function
|
|
10
|
+
export function StorefrontSections({ sections }: { sections: readonly StorefrontSection[] }) {
|
|
11
11
|
return <main>{renderBlocks(sectionsToBlocks(sections))}</main>
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
export function HomePage({ sections }: { sections: readonly StorefrontSection[] }) {
|
|
15
|
+
return <StorefrontSections sections={sections} />
|
|
16
|
+
}
|
|
@@ -12,6 +12,7 @@ import { CustomerAccountShell, type AccountSection } from '../components/Custome
|
|
|
12
12
|
import { OrderTrackingTimeline } from '../components/OrderTrackingTimeline'
|
|
13
13
|
import { financialStatusBadge } from '../order-status'
|
|
14
14
|
import { useStorefrontHead } from '../hooks/useStorefrontHead'
|
|
15
|
+
import { SmoothImage } from '../components/SmoothImage'
|
|
15
16
|
|
|
16
17
|
function AuthForm() {
|
|
17
18
|
const [mode, setMode] = useState<'signin' | 'signup'>('signin')
|
|
@@ -155,7 +156,7 @@ function OrdersPanel() {
|
|
|
155
156
|
<ul className="mt-3 flex flex-wrap gap-2">
|
|
156
157
|
{order.items.map((item) => (
|
|
157
158
|
<li key={item.id} className="flex items-center gap-2 rounded-lg border px-2 py-1 text-xs">
|
|
158
|
-
{item.imageUrl && <
|
|
159
|
+
{item.imageUrl && <SmoothImage src={item.imageUrl} alt={item.name} className="h-6 w-6 rounded object-cover" />}
|
|
159
160
|
{item.name} × {item.quantity}
|
|
160
161
|
</li>
|
|
161
162
|
))}
|
|
@@ -10,6 +10,7 @@ import { CustomerAccountShell } from '../components/CustomerAccountShell'
|
|
|
10
10
|
import { OrderTrackingTimeline } from '../components/OrderTrackingTimeline'
|
|
11
11
|
import { financialStatusBadge } from '../order-status'
|
|
12
12
|
import { useStorefrontHead } from '../hooks/useStorefrontHead'
|
|
13
|
+
import { SmoothImage } from '../components/SmoothImage'
|
|
13
14
|
|
|
14
15
|
const CONFETTI_COLORS = ['hsl(var(--primary))', '#f59e0b', '#10b981', '#3b82f6', '#ec4899']
|
|
15
16
|
|
|
@@ -139,7 +140,7 @@ export function OrderConfirmationPage({ orderId }: { orderId: string }) {
|
|
|
139
140
|
<ul className="mt-8 space-y-3 text-left">
|
|
140
141
|
{order.items.map((item) => (
|
|
141
142
|
<li key={item.id} className="flex items-center gap-3 text-sm">
|
|
142
|
-
{item.imageUrl && <
|
|
143
|
+
{item.imageUrl && <SmoothImage src={item.imageUrl} alt={item.name} className="h-12 w-12 rounded-lg border object-cover" />}
|
|
143
144
|
<span className="flex-1">
|
|
144
145
|
{item.name} <span className="text-muted-foreground">× {item.quantity}</span>
|
|
145
146
|
</span>
|
|
@@ -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/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/testids.ts
CHANGED
|
@@ -15,6 +15,13 @@ export const TID = {
|
|
|
15
15
|
hero: 'home-hero',
|
|
16
16
|
heroPrev: 'hero-prev',
|
|
17
17
|
heroNext: 'hero-next',
|
|
18
|
+
mediaCarousel: 'media-carousel',
|
|
19
|
+
mediaCarouselPrev: 'media-carousel-prev',
|
|
20
|
+
mediaCarouselNext: 'media-carousel-next',
|
|
21
|
+
productSlider: 'product-slider',
|
|
22
|
+
productSliderViewport: 'product-slider-viewport',
|
|
23
|
+
productSliderPrev: 'product-slider-prev',
|
|
24
|
+
productSliderNext: 'product-slider-next',
|
|
18
25
|
categoryShowcase: 'category-showcase',
|
|
19
26
|
productRail: 'product-rail',
|
|
20
27
|
benefits: 'benefits-row',
|
|
@@ -49,6 +56,19 @@ export const TID = {
|
|
|
49
56
|
productOptionValue: (id: string, value: string) => `product-option-${id.toLowerCase().replace(/\s+/g, '-')}-${value.toLowerCase().replace(/\s+/g, '-')}`,
|
|
50
57
|
pdpQtyInput: 'pdp-qty-input',
|
|
51
58
|
pdpAddToCart: 'pdp-add-to-cart',
|
|
59
|
+
pdpRoot: 'pdp-root',
|
|
60
|
+
pdpGallery: 'pdp-gallery',
|
|
61
|
+
pdpActions: 'pdp-actions',
|
|
62
|
+
pdpEnquiryButton: 'pdp-enquiry-button',
|
|
63
|
+
enquiryForm: 'enquiry-form',
|
|
64
|
+
enquiryName: 'enquiry-name',
|
|
65
|
+
enquiryEmail: 'enquiry-email',
|
|
66
|
+
enquiryPhone: 'enquiry-phone',
|
|
67
|
+
enquirySubject: 'enquiry-subject',
|
|
68
|
+
enquiryMessage: 'enquiry-message',
|
|
69
|
+
enquirySubmit: 'enquiry-submit',
|
|
70
|
+
enquirySuccess: 'enquiry-success',
|
|
71
|
+
enquiryError: 'enquiry-error',
|
|
52
72
|
// cart
|
|
53
73
|
cartDrawer: 'cart-drawer',
|
|
54
74
|
cartLine: 'cart-line',
|