@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
package/src/config.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import React, { createContext, useContext } from 'react'
|
|
2
|
-
import type { Product } from '@fayz-ai/shop/types'
|
|
3
2
|
import type { ShopProvider } from '@fayz-ai/shop/provider'
|
|
4
3
|
import type { MockShopSeed } from '@fayz-ai/shop/mock'
|
|
5
4
|
import type { StorefrontTheme } from './theme'
|
|
6
5
|
import type { HomeConfig, NavLink, FooterConfig } from './sections'
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
export interface ProductCardSlotProps {
|
|
10
|
-
product: Product
|
|
11
|
-
}
|
|
6
|
+
import type { StorefrontAuthConfig } from './auth'
|
|
7
|
+
import type { StorefrontComponents } from './component-contracts'
|
|
12
8
|
|
|
13
9
|
export type StorefrontRouteKind =
|
|
14
10
|
| 'home'
|
|
@@ -28,6 +24,15 @@ export interface StorefrontRouteComponentProps {
|
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
export type StorefrontRouteChrome = 'default' | 'focused'
|
|
27
|
+
export type StorefrontCommerceMode = 'checkout' | 'catalog' | 'enquiry'
|
|
28
|
+
export type StorefrontImageLoadingMode = 'fade' | 'none'
|
|
29
|
+
|
|
30
|
+
export interface StorefrontImageLoadingConfig {
|
|
31
|
+
mode?: StorefrontImageLoadingMode
|
|
32
|
+
durationMs?: number
|
|
33
|
+
easing?: string
|
|
34
|
+
blur?: boolean
|
|
35
|
+
}
|
|
31
36
|
|
|
32
37
|
export interface StorefrontRouteDefinition {
|
|
33
38
|
/**
|
|
@@ -54,22 +59,21 @@ export interface StorefrontRouteDefinition {
|
|
|
54
59
|
component: React.ComponentType<StorefrontRouteComponentProps>
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
export interface StorefrontSlots {
|
|
58
|
-
/**
|
|
59
|
-
* Product card renderer used by catalog grids and product rails.
|
|
60
|
-
*
|
|
61
|
-
* Custom renderers must preserve `productCardSlotContract` selectors so
|
|
62
|
-
* checkout smoke tests, QA, and agents can still operate the storefront.
|
|
63
|
-
*/
|
|
64
|
-
ProductCard?: React.ComponentType<ProductCardSlotProps>
|
|
65
|
-
}
|
|
66
|
-
|
|
67
62
|
export interface StorefrontDiscountConfig {
|
|
68
63
|
code: string
|
|
69
64
|
percent: number
|
|
70
65
|
title?: string
|
|
71
66
|
}
|
|
72
67
|
|
|
68
|
+
export interface StorefrontEnquiryConfig {
|
|
69
|
+
label?: string
|
|
70
|
+
successMessage?: string
|
|
71
|
+
subjectPrefix?: string
|
|
72
|
+
email?: string
|
|
73
|
+
whatsappUrl?: string
|
|
74
|
+
requirePhone?: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
73
77
|
export interface StorefrontConfig {
|
|
74
78
|
/** Store display name (header, page title) */
|
|
75
79
|
name: string
|
|
@@ -81,6 +85,8 @@ export interface StorefrontConfig {
|
|
|
81
85
|
locale?: string
|
|
82
86
|
/** Visual identity: colors, fonts, radius, header/card personality */
|
|
83
87
|
theme?: StorefrontTheme
|
|
88
|
+
/** Checkout-first default, catalog-only browsing, or product enquiry flow. */
|
|
89
|
+
commerceMode?: StorefrontCommerceMode
|
|
84
90
|
/** Promo message above the header (e.g. 'FRETE GRÁTIS ACIMA DE R$ 300') */
|
|
85
91
|
announcement?: string
|
|
86
92
|
/** Home page blueprint. When set, `/` renders it and the catalog moves to /catalog. */
|
|
@@ -108,7 +114,7 @@ export interface StorefrontConfig {
|
|
|
108
114
|
*/
|
|
109
115
|
legal?: { privacy?: string; terms?: string; returns?: string }
|
|
110
116
|
/** Customer auth — same AuthAdapter contract as createSaasApp. Default: mock. */
|
|
111
|
-
auth?:
|
|
117
|
+
auth?: StorefrontAuthConfig
|
|
112
118
|
/** Shipping pricing — default { flatRate: 0 } */
|
|
113
119
|
shipping?: { flatRate?: number; freeAbove?: number }
|
|
114
120
|
/**
|
|
@@ -117,12 +123,16 @@ export interface StorefrontConfig {
|
|
|
117
123
|
* a real payment webhook confirms settlement.
|
|
118
124
|
*/
|
|
119
125
|
payments?: { mode?: 'mock' | 'pix-mercadopago' }
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
|
|
126
|
+
/** Product enquiry behavior for catalog/enquiry stores. */
|
|
127
|
+
enquiry?: StorefrontEnquiryConfig
|
|
128
|
+
/** Feature toggles — defaults depend on commerceMode. */
|
|
129
|
+
features?: { discounts?: boolean; accounts?: boolean; cart?: boolean; checkout?: boolean }
|
|
130
|
+
/** Global storefront image reveal behavior. Default is a soft fade. */
|
|
131
|
+
imageLoading?: StorefrontImageLoadingConfig
|
|
132
|
+
/** Code-level component replacements. Kept out of serialized manifests. */
|
|
133
|
+
components?: StorefrontComponents
|
|
124
134
|
/** Code-level route overrides/custom routes. Kept out of serialized manifests. */
|
|
125
|
-
routes?: StorefrontRouteDefinition[]
|
|
135
|
+
routes?: readonly StorefrontRouteDefinition[]
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
export interface ResolvedStorefrontConfig extends StorefrontConfig {
|
|
@@ -130,7 +140,11 @@ export interface ResolvedStorefrontConfig extends StorefrontConfig {
|
|
|
130
140
|
locale: string
|
|
131
141
|
shipping: { flatRate: number; freeAbove?: number }
|
|
132
142
|
payments: { mode: 'mock' | 'pix-mercadopago' }
|
|
133
|
-
|
|
143
|
+
commerceMode: StorefrontCommerceMode
|
|
144
|
+
enquiry: Required<Pick<StorefrontEnquiryConfig, 'label' | 'successMessage' | 'subjectPrefix'>> &
|
|
145
|
+
Omit<StorefrontEnquiryConfig, 'label' | 'successMessage' | 'subjectPrefix'>
|
|
146
|
+
features: { discounts: boolean; accounts: boolean; cart: boolean; checkout: boolean }
|
|
147
|
+
imageLoading: Required<StorefrontImageLoadingConfig>
|
|
134
148
|
/** Where the full product list lives ('/' without a home page, '/catalog' with one) */
|
|
135
149
|
catalogPath: string
|
|
136
150
|
nav: NavLink[]
|
|
@@ -138,12 +152,23 @@ export interface ResolvedStorefrontConfig extends StorefrontConfig {
|
|
|
138
152
|
|
|
139
153
|
export function resolveConfig(config: StorefrontConfig): ResolvedStorefrontConfig {
|
|
140
154
|
const catalogPath = config.home ? '/catalog' : '/'
|
|
155
|
+
const commerceMode = config.commerceMode ?? 'checkout'
|
|
156
|
+
const checkoutEnabled = commerceMode === 'checkout'
|
|
141
157
|
return {
|
|
142
158
|
...config,
|
|
143
159
|
currency: config.currency ?? 'BRL',
|
|
144
160
|
locale: config.locale ?? 'pt-BR',
|
|
145
161
|
shipping: { flatRate: config.shipping?.flatRate ?? 0, freeAbove: config.shipping?.freeAbove },
|
|
146
162
|
payments: { mode: config.payments?.mode ?? 'mock' },
|
|
163
|
+
commerceMode,
|
|
164
|
+
enquiry: {
|
|
165
|
+
label: config.enquiry?.label ?? 'Contact me',
|
|
166
|
+
successMessage: config.enquiry?.successMessage ?? 'Thanks. We received your enquiry.',
|
|
167
|
+
subjectPrefix: config.enquiry?.subjectPrefix ?? 'Product enquiry',
|
|
168
|
+
email: config.enquiry?.email,
|
|
169
|
+
whatsappUrl: config.enquiry?.whatsappUrl,
|
|
170
|
+
requirePhone: config.enquiry?.requirePhone,
|
|
171
|
+
},
|
|
147
172
|
features: {
|
|
148
173
|
// Auto-enable the coupon UI when the store actually ships discount codes
|
|
149
174
|
// (via config.discounts or a seeded catalog), unless explicitly overridden —
|
|
@@ -151,7 +176,15 @@ export function resolveConfig(config: StorefrontConfig): ResolvedStorefrontConfi
|
|
|
151
176
|
discounts:
|
|
152
177
|
config.features?.discounts ??
|
|
153
178
|
Boolean(config.discounts?.length || config.catalog?.discounts?.length),
|
|
154
|
-
accounts: config.features?.accounts ??
|
|
179
|
+
accounts: config.features?.accounts ?? checkoutEnabled,
|
|
180
|
+
cart: config.features?.cart ?? checkoutEnabled,
|
|
181
|
+
checkout: config.features?.checkout ?? checkoutEnabled,
|
|
182
|
+
},
|
|
183
|
+
imageLoading: {
|
|
184
|
+
mode: config.imageLoading?.mode ?? 'fade',
|
|
185
|
+
durationMs: config.imageLoading?.durationMs ?? 420,
|
|
186
|
+
easing: config.imageLoading?.easing ?? 'cubic-bezier(0.22, 1, 0.36, 1)',
|
|
187
|
+
blur: config.imageLoading?.blur ?? true,
|
|
155
188
|
},
|
|
156
189
|
catalogPath,
|
|
157
190
|
nav:
|
|
@@ -174,3 +207,11 @@ export function useStorefrontConfig(): ResolvedStorefrontConfig {
|
|
|
174
207
|
if (!ctx) throw new Error('useStorefrontConfig must be used inside createStorefrontApp')
|
|
175
208
|
return ctx
|
|
176
209
|
}
|
|
210
|
+
|
|
211
|
+
export function useStorefrontConfigOptional(): ResolvedStorefrontConfig | null {
|
|
212
|
+
return useContext(StorefrontConfigContext)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getStorefrontComponents(config: ResolvedStorefrontConfig): StorefrontComponents {
|
|
216
|
+
return config.components ?? {}
|
|
217
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { setShopProvider } from '@fayz-ai/shop/runtime'
|
|
2
|
+
import { setShopProvider, getShopProvider } from '@fayz-ai/shop/runtime'
|
|
3
3
|
import { createMockShopProvider } from '@fayz-ai/shop/mock'
|
|
4
4
|
import type { MockShopSeed } from '@fayz-ai/shop/mock'
|
|
5
5
|
import type { Discount } from '@fayz-ai/shop/types'
|
|
6
|
+
import type { ShopProvider } from '@fayz-ai/shop/provider'
|
|
6
7
|
import { initCustomerAuth, resolveAuthAdapter } from './auth'
|
|
7
|
-
import { StorefrontConfigProvider, resolveConfig, useStorefrontConfig } from './config'
|
|
8
|
+
import { getStorefrontComponents, StorefrontConfigProvider, resolveConfig, useStorefrontConfig } from './config'
|
|
8
9
|
import type { StorefrontConfig, StorefrontDiscountConfig } from './config'
|
|
10
|
+
import type { StorefrontComponents } from './component-contracts'
|
|
11
|
+
import type { StorefrontSection } from './sections'
|
|
9
12
|
import { useHashPath, matchPath } from './router'
|
|
13
|
+
import { useCartStore } from './stores/cart.store'
|
|
10
14
|
import { StorefrontThemeStyle } from './theme'
|
|
11
15
|
import { StorefrontHeader } from './components/StorefrontHeader'
|
|
12
16
|
import { StorefrontFooter } from './components/StorefrontFooter'
|
|
@@ -51,8 +55,8 @@ function RouteSwitch({ path }: { path: string }) {
|
|
|
51
55
|
const order = matchPath('/order/:id', path)
|
|
52
56
|
if (order?.id) return <OrderConfirmationPage orderId={order.id} />
|
|
53
57
|
|
|
54
|
-
if (matchPath('/checkout', path)) return <CheckoutPage />
|
|
55
|
-
if (matchPath('/account', path)) return <MyPurchasesPage />
|
|
58
|
+
if (matchPath('/checkout', path) && config.features.checkout) return <CheckoutPage />
|
|
59
|
+
if (matchPath('/account', path) && config.features.accounts) return <MyPurchasesPage />
|
|
56
60
|
if (matchPath('/catalog', path)) return <CatalogPage />
|
|
57
61
|
if (matchPath('/privacy', path)) return <PolicyPage kind="privacy" />
|
|
58
62
|
if (matchPath('/terms', path)) return <PolicyPage kind="terms" />
|
|
@@ -112,7 +116,12 @@ function buildMockSeed(config: StorefrontConfig): MockShopSeed | undefined {
|
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
export function initStorefrontRuntime(config: StorefrontConfig): void {
|
|
115
|
-
|
|
119
|
+
if (resolveConfig(config).features.accounts) {
|
|
120
|
+
initCustomerAuth(resolveAuthAdapter(config.auth, {
|
|
121
|
+
supabaseUrl: config.supabaseUrl,
|
|
122
|
+
supabaseAnonKey: config.supabaseAnonKey,
|
|
123
|
+
}))
|
|
124
|
+
}
|
|
116
125
|
|
|
117
126
|
if (config.provider) {
|
|
118
127
|
setShopProvider(config.provider)
|
|
@@ -120,6 +129,22 @@ export function initStorefrontRuntime(config: StorefrontConfig): void {
|
|
|
120
129
|
setShopProvider(createMockShopProvider(buildMockSeed(config)))
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
// Drop any persisted cart line the active backend can't resolve (e.g. a cart
|
|
133
|
+
// built in an earlier mock-mode session, whose ids don't exist live) so stale
|
|
134
|
+
// ids never reach checkout and 400 the order. Keeps a line on any error so a
|
|
135
|
+
// transient failure never discards a valid item. Runs AFTER the persisted cart
|
|
136
|
+
// has hydrated — zustand/persist rehydrates asynchronously, so reconciling at
|
|
137
|
+
// init would see an empty cart and no-op.
|
|
138
|
+
const reconcileCart = () => {
|
|
139
|
+
const provider = getShopProvider()
|
|
140
|
+
void useCartStore.getState().reconcile(async (line) => {
|
|
141
|
+
const product = await provider.getProduct(line.productId)
|
|
142
|
+
return product != null
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
if (useCartStore.persist.hasHydrated()) reconcileCart()
|
|
146
|
+
else useCartStore.persist.onFinishHydration(reconcileCart)
|
|
147
|
+
|
|
123
148
|
if (config.supabaseUrl || config.supabaseAnonKey) {
|
|
124
149
|
console.warn(
|
|
125
150
|
'@fayz-ai/shop: supabaseUrl/supabaseAnonKey are legacy fields. Pass an explicit provider/adapter or use the Fayz SDK broker path.',
|
|
@@ -133,16 +158,34 @@ export function StorefrontShell() {
|
|
|
133
158
|
const config = useStorefrontConfig()
|
|
134
159
|
const path = useHashPath()
|
|
135
160
|
const focused = isFocusedRoute(config, path)
|
|
136
|
-
|
|
137
|
-
|
|
161
|
+
const components = getStorefrontComponents(config)
|
|
162
|
+
const chromeProps = { config, commerceMode: config.commerceMode }
|
|
163
|
+
const body = (
|
|
164
|
+
<>
|
|
138
165
|
{config.theme && <StorefrontThemeStyle theme={config.theme} />}
|
|
139
|
-
{!focused &&
|
|
166
|
+
{!focused &&
|
|
167
|
+
(components.Header ? <components.Header {...chromeProps} /> : <StorefrontHeader />)}
|
|
140
168
|
<StorefrontErrorBoundary key={path}>
|
|
141
169
|
<RouteSwitch path={path} />
|
|
142
170
|
</StorefrontErrorBoundary>
|
|
143
|
-
{!focused && <CartDrawer />}
|
|
144
|
-
{!focused &&
|
|
171
|
+
{!focused && config.features.cart && <CartDrawer />}
|
|
172
|
+
{!focused &&
|
|
173
|
+
(components.Footer ? <components.Footer {...chromeProps} /> : <StorefrontFooter />)}
|
|
145
174
|
<Toaster />
|
|
175
|
+
</>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if (components.Shell) {
|
|
179
|
+
return (
|
|
180
|
+
<components.Shell {...chromeProps}>
|
|
181
|
+
{body}
|
|
182
|
+
</components.Shell>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="min-h-screen bg-background text-foreground">
|
|
188
|
+
{body}
|
|
146
189
|
</div>
|
|
147
190
|
)
|
|
148
191
|
}
|
|
@@ -169,3 +212,30 @@ export function createStorefrontApp(config: StorefrontConfig): React.ComponentTy
|
|
|
169
212
|
StorefrontApp.displayName = 'StorefrontApp'
|
|
170
213
|
return StorefrontApp
|
|
171
214
|
}
|
|
215
|
+
|
|
216
|
+
export interface CreateStorefrontOptions {
|
|
217
|
+
config: StorefrontConfig
|
|
218
|
+
provider?: ShopProvider
|
|
219
|
+
components?: StorefrontComponents
|
|
220
|
+
routes?: StorefrontConfig['routes']
|
|
221
|
+
pages?: StorefrontConfig['routes']
|
|
222
|
+
sections?: readonly StorefrontSection[]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function createStorefront(options: CreateStorefrontOptions): React.ComponentType {
|
|
226
|
+
const routes = [...(options.pages ?? []), ...(options.routes ?? [])]
|
|
227
|
+
const config: StorefrontConfig = {
|
|
228
|
+
...options.config,
|
|
229
|
+
provider: options.provider ?? options.config.provider,
|
|
230
|
+
components: {
|
|
231
|
+
...options.config.components,
|
|
232
|
+
...options.components,
|
|
233
|
+
},
|
|
234
|
+
routes: routes.length > 0 ? routes : options.config.routes,
|
|
235
|
+
home: options.sections
|
|
236
|
+
? { ...(options.config.home ?? { sections: [] }), sections: options.sections }
|
|
237
|
+
: options.config.home,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return createStorefrontApp(config)
|
|
241
|
+
}
|
package/src/define.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StorefrontConfig,
|
|
3
|
+
StorefrontRouteDefinition,
|
|
4
|
+
} from './config'
|
|
5
|
+
import type { StorefrontComponents } from './component-contracts'
|
|
6
|
+
import type { StorefrontSection } from './sections'
|
|
7
|
+
import type { CreateStorefrontOptions } from './createStorefrontApp'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Identity helpers for generated/custom storefronts.
|
|
11
|
+
*
|
|
12
|
+
* They do not add runtime behavior; they make the intended client-app shape
|
|
13
|
+
* explicit and keep TypeScript inference tight across config, components, routes,
|
|
14
|
+
* sections and app creation options.
|
|
15
|
+
*/
|
|
16
|
+
export function defineStorefrontConfig<const T extends StorefrontConfig>(config: T): T {
|
|
17
|
+
return config
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defineStorefrontComponents<const T extends StorefrontComponents>(components: T): T {
|
|
21
|
+
return components
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function defineStorefrontRoutes<const T extends readonly StorefrontRouteDefinition[]>(routes: T): T {
|
|
25
|
+
return routes
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function defineStorefrontSections<const T extends readonly StorefrontSection[]>(sections: T): T {
|
|
29
|
+
return sections
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defineStorefrontApp<const T extends CreateStorefrontOptions>(options: T): T {
|
|
33
|
+
return options
|
|
34
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { useProducts } from './useProducts'
|
|
2
2
|
export { useProduct } from './useProduct'
|
|
3
3
|
export { useCategories } from './useCategories'
|
|
4
|
+
export { useStorefront, useCatalog, useCart, useEnquiry, useStorefrontActions } from './useStorefront'
|
|
4
5
|
export { useMyOrders } from './useMyOrders'
|
|
5
6
|
export { useDiscountValidator } from './useDiscountValidator'
|
|
@@ -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,
|
|
@@ -101,14 +144,19 @@ export {
|
|
|
101
144
|
sertaoTemplate,
|
|
102
145
|
voltTemplate,
|
|
103
146
|
atelierTemplate,
|
|
147
|
+
foodTemplate,
|
|
104
148
|
} from './presets'
|
|
105
149
|
export type { StorefrontTemplate, StorefrontTemplateId } from './presets'
|
|
106
150
|
|
|
107
151
|
// Home sections (for custom compositions)
|
|
108
|
-
export { HomePage } from './pages/HomePage'
|
|
152
|
+
export { HomePage, StorefrontSections } from './pages/HomePage'
|
|
109
153
|
export { HeroSection } from './components/sections/HeroSection'
|
|
154
|
+
export { MediaCarousel } from './components/sections/MediaCarousel'
|
|
155
|
+
export type { MediaCarouselProps } from './components/sections/MediaCarousel'
|
|
110
156
|
export { CategoryShowcase } from './components/sections/CategoryShowcase'
|
|
111
157
|
export { ProductRail } from './components/sections/ProductRail'
|
|
158
|
+
export { ProductSlider } from './components/sections/ProductSlider'
|
|
159
|
+
export type { ProductSliderProps } from './components/sections/ProductSlider'
|
|
112
160
|
export {
|
|
113
161
|
BenefitsRow,
|
|
114
162
|
PromoBanner,
|
|
@@ -126,10 +174,13 @@ export type { RevealProps } from './motion'
|
|
|
126
174
|
export { formatMoney, roundCents } from './format'
|
|
127
175
|
export { TID } from './testids'
|
|
128
176
|
export {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
} from './
|
|
132
|
-
export type {
|
|
177
|
+
productCardComponentContract,
|
|
178
|
+
storefrontComponentContracts,
|
|
179
|
+
} from './component-selectors'
|
|
180
|
+
export type {
|
|
181
|
+
ProductCardComponentContract,
|
|
182
|
+
ProductDetailComponentContract,
|
|
183
|
+
} from './component-selectors'
|
|
133
184
|
|
|
134
185
|
// ---------------------------------------------------------------------------
|
|
135
186
|
// 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>
|