@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.
Files changed (102) hide show
  1. package/README.md +3 -1
  2. package/dist/auth.d.ts +12 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/blocks.d.ts +3 -3
  5. package/dist/blocks.d.ts.map +1 -1
  6. package/dist/component-contracts.d.ts +124 -0
  7. package/dist/component-contracts.d.ts.map +1 -0
  8. package/dist/component-selectors.d.ts +61 -0
  9. package/dist/component-selectors.d.ts.map +1 -0
  10. package/dist/components/CartDrawer.d.ts.map +1 -1
  11. package/dist/components/ProductCard.d.ts +2 -5
  12. package/dist/components/ProductCard.d.ts.map +1 -1
  13. package/dist/components/ProductEnquiryForm.d.ts +8 -0
  14. package/dist/components/ProductEnquiryForm.d.ts.map +1 -0
  15. package/dist/components/ProductGrid.d.ts.map +1 -1
  16. package/dist/components/RelatedProducts.d.ts +1 -1
  17. package/dist/components/RelatedProducts.d.ts.map +1 -1
  18. package/dist/components/SmoothImage.d.ts +7 -0
  19. package/dist/components/SmoothImage.d.ts.map +1 -0
  20. package/dist/components/StorefrontFooter.d.ts.map +1 -1
  21. package/dist/components/StorefrontHeader.d.ts.map +1 -1
  22. package/dist/components/sections/CategoryShowcase.d.ts.map +1 -1
  23. package/dist/components/sections/HeroSection.d.ts.map +1 -1
  24. package/dist/components/sections/MediaCarousel.d.ts +11 -0
  25. package/dist/components/sections/MediaCarousel.d.ts.map +1 -0
  26. package/dist/components/sections/MiscSections.d.ts.map +1 -1
  27. package/dist/components/sections/ProductRail.d.ts.map +1 -1
  28. package/dist/components/sections/ProductSlider.d.ts +15 -0
  29. package/dist/components/sections/ProductSlider.d.ts.map +1 -0
  30. package/dist/config.d.ts +38 -21
  31. package/dist/config.d.ts.map +1 -1
  32. package/dist/createStorefrontApp.d.ts +12 -0
  33. package/dist/createStorefrontApp.d.ts.map +1 -1
  34. package/dist/define.d.ts +17 -0
  35. package/dist/define.d.ts.map +1 -0
  36. package/dist/hooks/index.d.ts +1 -0
  37. package/dist/hooks/index.d.ts.map +1 -1
  38. package/dist/hooks/useStorefront.d.ts +35 -0
  39. package/dist/hooks/useStorefront.d.ts.map +1 -0
  40. package/dist/index.cjs +1113 -277
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.ts +21 -11
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1006 -188
  45. package/dist/index.js.map +1 -1
  46. package/dist/pages/CheckoutPage.d.ts.map +1 -1
  47. package/dist/pages/HomePage.d.ts +5 -2
  48. package/dist/pages/HomePage.d.ts.map +1 -1
  49. package/dist/pages/MyPurchasesPage.d.ts.map +1 -1
  50. package/dist/pages/OrderConfirmationPage.d.ts.map +1 -1
  51. package/dist/pages/ProductDetailPage.d.ts.map +1 -1
  52. package/dist/presets.d.ts +5 -0
  53. package/dist/presets.d.ts.map +1 -1
  54. package/dist/sections.d.ts +45 -2
  55. package/dist/sections.d.ts.map +1 -1
  56. package/dist/stores/cart.store.d.ts +5 -0
  57. package/dist/stores/cart.store.d.ts.map +1 -1
  58. package/dist/testids.d.ts +21 -0
  59. package/dist/testids.d.ts.map +1 -1
  60. package/dist/theme.d.ts +21 -1
  61. package/dist/theme.d.ts.map +1 -1
  62. package/dist/workflows/checkout.d.ts.map +1 -1
  63. package/package.json +10 -6
  64. package/src/auth.ts +47 -10
  65. package/src/blocks.tsx +7 -3
  66. package/src/component-contracts.ts +140 -0
  67. package/src/component-selectors.ts +31 -0
  68. package/src/components/CartDrawer.tsx +2 -1
  69. package/src/components/ProductCard.tsx +39 -23
  70. package/src/components/ProductEnquiryForm.tsx +175 -0
  71. package/src/components/ProductGrid.tsx +12 -3
  72. package/src/components/RelatedProducts.tsx +13 -4
  73. package/src/components/SmoothImage.tsx +59 -0
  74. package/src/components/StorefrontFooter.tsx +24 -14
  75. package/src/components/StorefrontHeader.tsx +74 -42
  76. package/src/components/sections/CategoryShowcase.tsx +3 -2
  77. package/src/components/sections/HeroSection.tsx +18 -2
  78. package/src/components/sections/MediaCarousel.tsx +141 -0
  79. package/src/components/sections/MiscSections.tsx +2 -1
  80. package/src/components/sections/ProductRail.tsx +11 -3
  81. package/src/components/sections/ProductSlider.tsx +119 -0
  82. package/src/config.ts +65 -24
  83. package/src/createStorefrontApp.tsx +80 -10
  84. package/src/define.ts +34 -0
  85. package/src/hooks/index.ts +1 -0
  86. package/src/hooks/useStorefront.ts +58 -0
  87. package/src/index.ts +62 -11
  88. package/src/pages/CheckoutPage.tsx +2 -1
  89. package/src/pages/HomePage.tsx +6 -2
  90. package/src/pages/MyPurchasesPage.tsx +2 -1
  91. package/src/pages/OrderConfirmationPage.tsx +2 -1
  92. package/src/pages/ProductDetailPage.tsx +138 -62
  93. package/src/presets.ts +66 -0
  94. package/src/scaffold.tsx +1 -1
  95. package/src/sections.ts +49 -2
  96. package/src/stores/cart.store.ts +16 -1
  97. package/src/testids.ts +21 -0
  98. package/src/theme.ts +36 -2
  99. package/src/workflows/checkout.ts +5 -2
  100. package/dist/slot-contracts.d.ts +0 -34
  101. package/dist/slot-contracts.d.ts.map +0 -1
  102. package/src/slot-contracts.ts +0 -20
@@ -1,31 +1,31 @@
1
1
  import React from 'react'
2
2
  import { ShoppingBag } from 'lucide-react'
3
- import type { Product } from '@fayz-ai/shop/types'
4
- import { useCartStore } from '../stores/cart.store'
5
3
  import { useStorefrontConfig } from '../config'
6
4
  import { Link, navigateTo } from '../router'
7
5
  import { getProductOptionGroups } from '../product-options'
8
6
  import { Price } from './Price'
9
7
  import { TID } from '../testids'
10
- import { productCardSlotContract } from '../slot-contracts'
8
+ import { productCardComponentContract } from '../component-selectors'
9
+ import { useStorefrontActions } from '../hooks/useStorefront'
10
+ import type { ProductCardProps } from '../component-contracts'
11
+ import { SmoothImage } from './SmoothImage'
11
12
 
12
- export interface ProductCardProps {
13
- product: Product
14
- }
15
-
16
- export function ProductCard({ product }: ProductCardProps) {
17
- const config = useStorefrontConfig()
18
- const addItem = useCartStore((s) => s.addItem)
13
+ export function ProductCard({ product, config: providedConfig, actions: providedActions }: ProductCardProps) {
14
+ const runtimeConfig = useStorefrontConfig()
15
+ const runtimeActions = useStorefrontActions()
16
+ const config = providedConfig ?? runtimeConfig
17
+ const actions = providedActions ?? runtimeActions
19
18
  const hasOptions = getProductOptionGroups(product).length > 0
20
19
  const soldOut = product.inventoryCount <= 0
21
20
  const onSale = product.compareAtPrice != null && product.compareAtPrice > product.price
22
21
  const image = product.images.find((i) => i.isPrimary) ?? product.images[0]
23
22
  const cardStyle = config.theme?.productCard?.style ?? 'card'
24
23
  const aspect = config.theme?.productCard?.imageAspect === 'portrait' ? 'aspect-[3/4]' : 'aspect-square'
24
+ const purchaseMode = config.commerceMode === 'checkout'
25
25
 
26
26
  return (
27
27
  <div
28
- {...productCardSlotContract.root}
28
+ {...productCardComponentContract.root}
29
29
  data-slug={product.slug}
30
30
  className={`group flex flex-col overflow-hidden transition-all duration-300 ${
31
31
  cardStyle === 'editorial'
@@ -36,7 +36,7 @@ export function ProductCard({ product }: ProductCardProps) {
36
36
  >
37
37
  <Link to={`/product/${product.slug}`} className={`relative block ${aspect} overflow-hidden bg-muted`} style={{ borderRadius: cardStyle === 'editorial' ? 'var(--sf-radius-card)' : undefined }}>
38
38
  {image && (
39
- <img
39
+ <SmoothImage
40
40
  src={image.url}
41
41
  alt={image.altText ?? product.name}
42
42
  className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
@@ -67,23 +67,39 @@ export function ProductCard({ product }: ProductCardProps) {
67
67
  <span className="text-xs text-muted-foreground">{product.categoryName}</span>
68
68
  <Link
69
69
  to={`/product/${product.slug}`}
70
- {...productCardSlotContract.name}
70
+ {...productCardComponentContract.name}
71
71
  className="font-medium leading-snug hover:underline"
72
72
  >
73
73
  {product.name}
74
74
  </Link>
75
- <div className="mt-auto flex items-center justify-between pt-2">
76
- <Price
77
- value={product.price}
78
- compareAt={product.compareAtPrice}
79
- testId={productCardSlotContract.priceTestId}
80
- />
75
+ <div className="mt-auto flex items-center justify-between gap-3 pt-2">
76
+ {purchaseMode ? (
77
+ <Price
78
+ value={product.price}
79
+ compareAt={product.compareAtPrice}
80
+ testId={productCardComponentContract.priceTestId}
81
+ />
82
+ ) : (
83
+ <span data-testid={productCardComponentContract.priceTestId} className="text-sm text-muted-foreground">
84
+ {config.commerceMode === 'enquiry' ? config.enquiry.label : 'Ver detalhes'}
85
+ </span>
86
+ )}
81
87
  <button
82
88
  type="button"
83
- {...productCardSlotContract.addButton}
84
- disabled={soldOut}
85
- aria-label={hasOptions ? `Escolher opções de ${product.name}` : `Adicionar ${product.name} ao carrinho`}
89
+ {...productCardComponentContract.addButton}
90
+ disabled={purchaseMode && soldOut}
91
+ aria-label={
92
+ purchaseMode
93
+ ? hasOptions
94
+ ? `Escolher opções de ${product.name}`
95
+ : `Adicionar ${product.name} ao carrinho`
96
+ : `${config.enquiry.label} ${product.name}`
97
+ }
86
98
  onClick={() => {
99
+ if (!purchaseMode) {
100
+ navigateTo(`/product/${product.slug}`)
101
+ return
102
+ }
87
103
  // Products with variants (size/color) must pick an option on the PDP
88
104
  // — quick-adding would create an option-less line. Others add inline
89
105
  // (addItem auto-opens the cart drawer).
@@ -91,7 +107,7 @@ export function ProductCard({ product }: ProductCardProps) {
91
107
  navigateTo(`/product/${product.slug}`)
92
108
  return
93
109
  }
94
- addItem(product)
110
+ actions.addToCart(product)
95
111
  }}
96
112
  className="rounded-full border bg-background/90 p-2.5 text-foreground shadow-sm transition-all duration-200 hover:scale-110 hover:bg-primary hover:text-primary-foreground hover:shadow disabled:cursor-not-allowed disabled:opacity-40 lg:translate-y-1 lg:opacity-0 lg:group-hover:translate-y-0 lg:group-hover:opacity-100 lg:disabled:opacity-0 lg:group-hover:disabled:opacity-40"
97
113
  >
@@ -0,0 +1,175 @@
1
+ import React, { useState } from 'react'
2
+ import { Mail, MessageCircle, Send } from 'lucide-react'
3
+ import { getShopProvider } from '@fayz-ai/shop/runtime'
4
+ import type { Product } from '@fayz-ai/shop/types'
5
+ import { useStorefrontConfig } from '../config'
6
+ import { storefrontComponentContracts } from '../component-selectors'
7
+ import { TID } from '../testids'
8
+
9
+ export interface ProductEnquiryFormProps {
10
+ product: Product
11
+ onSuccess?: () => void
12
+ }
13
+
14
+ function defaultSubject(prefix: string, product: Product): string {
15
+ return `${prefix}: ${product.name}`
16
+ }
17
+
18
+ function buildWhatsAppHref(url: string, product: Product): string {
19
+ const text = encodeURIComponent(`Hello, I would like more information about ${product.name}.`)
20
+ const sep = url.includes('?') ? '&' : '?'
21
+ return `${url}${sep}text=${text}`
22
+ }
23
+
24
+ export function ProductEnquiryForm({ product, onSuccess }: ProductEnquiryFormProps) {
25
+ const config = useStorefrontConfig()
26
+ const [customerName, setCustomerName] = useState('')
27
+ const [customerEmail, setCustomerEmail] = useState('')
28
+ const [customerPhone, setCustomerPhone] = useState('')
29
+ const [subject, setSubject] = useState(defaultSubject(config.enquiry.subjectPrefix, product))
30
+ const [message, setMessage] = useState(`I would like more information about ${product.name}.`)
31
+ const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle')
32
+ const [error, setError] = useState<string | null>(null)
33
+
34
+ const submit = async (event: React.FormEvent) => {
35
+ event.preventDefault()
36
+ setStatus('submitting')
37
+ setError(null)
38
+ try {
39
+ const provider = getShopProvider()
40
+ if (provider.createProductEnquiry) {
41
+ await provider.createProductEnquiry({
42
+ productId: product.id,
43
+ productName: product.name,
44
+ productSlug: product.slug,
45
+ customerName,
46
+ customerEmail,
47
+ customerPhone: customerPhone || undefined,
48
+ subject,
49
+ message,
50
+ sourceUrl: typeof window === 'undefined' ? undefined : window.location.href,
51
+ metadata: { sku: product.sku, categoryName: product.categoryName },
52
+ })
53
+ } else if (config.enquiry.email) {
54
+ window.location.href = `mailto:${config.enquiry.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(message)}`
55
+ }
56
+ setStatus('success')
57
+ onSuccess?.()
58
+ } catch (e) {
59
+ setStatus('error')
60
+ setError(e instanceof Error ? e.message : String(e))
61
+ }
62
+ }
63
+
64
+ return (
65
+ <form
66
+ {...storefrontComponentContracts.productDetail.enquiryForm}
67
+ onSubmit={submit}
68
+ className="space-y-4"
69
+ >
70
+ <div className="grid gap-4 sm:grid-cols-2">
71
+ <label className="space-y-1.5 text-sm font-medium">
72
+ <span>Your name</span>
73
+ <input
74
+ data-testid={TID.enquiryName}
75
+ required
76
+ value={customerName}
77
+ onChange={(e) => setCustomerName(e.target.value)}
78
+ className="w-full border bg-background px-3 py-2 outline-none focus:border-primary"
79
+ style={{ borderRadius: 'var(--sf-radius-input)' }}
80
+ />
81
+ </label>
82
+ <label className="space-y-1.5 text-sm font-medium">
83
+ <span>Your email</span>
84
+ <input
85
+ data-testid={TID.enquiryEmail}
86
+ required
87
+ type="email"
88
+ value={customerEmail}
89
+ onChange={(e) => setCustomerEmail(e.target.value)}
90
+ className="w-full border bg-background px-3 py-2 outline-none focus:border-primary"
91
+ style={{ borderRadius: 'var(--sf-radius-input)' }}
92
+ />
93
+ </label>
94
+ </div>
95
+ <label className="block space-y-1.5 text-sm font-medium">
96
+ <span>Phone{config.enquiry.requirePhone ? '' : ' (optional)'}</span>
97
+ <input
98
+ data-testid={TID.enquiryPhone}
99
+ required={config.enquiry.requirePhone}
100
+ value={customerPhone}
101
+ onChange={(e) => setCustomerPhone(e.target.value)}
102
+ className="w-full border bg-background px-3 py-2 outline-none focus:border-primary"
103
+ style={{ borderRadius: 'var(--sf-radius-input)' }}
104
+ />
105
+ </label>
106
+ <label className="block space-y-1.5 text-sm font-medium">
107
+ <span>Subject</span>
108
+ <input
109
+ data-testid={TID.enquirySubject}
110
+ required
111
+ value={subject}
112
+ onChange={(e) => setSubject(e.target.value)}
113
+ className="w-full border bg-background px-3 py-2 outline-none focus:border-primary"
114
+ style={{ borderRadius: 'var(--sf-radius-input)' }}
115
+ />
116
+ </label>
117
+ <label className="block space-y-1.5 text-sm font-medium">
118
+ <span>Message</span>
119
+ <textarea
120
+ data-testid={TID.enquiryMessage}
121
+ required
122
+ rows={5}
123
+ value={message}
124
+ onChange={(e) => setMessage(e.target.value)}
125
+ className="w-full resize-none border bg-background px-3 py-2 outline-none focus:border-primary"
126
+ style={{ borderRadius: 'var(--sf-radius-input)' }}
127
+ />
128
+ </label>
129
+ <div className="flex flex-wrap items-center gap-3">
130
+ <button
131
+ data-testid={TID.enquirySubmit}
132
+ type="submit"
133
+ disabled={status === 'submitting'}
134
+ className="sf-cta inline-flex items-center gap-2 bg-primary px-5 py-3 font-semibold text-primary-foreground disabled:cursor-wait disabled:opacity-60"
135
+ style={{ borderRadius: 'var(--sf-radius-button)' }}
136
+ >
137
+ <Send className="h-4 w-4" />
138
+ {status === 'submitting' ? 'Sending...' : 'Send enquiry'}
139
+ </button>
140
+ {config.enquiry.whatsappUrl && (
141
+ <a
142
+ href={buildWhatsAppHref(config.enquiry.whatsappUrl, product)}
143
+ target="_blank"
144
+ rel="noreferrer"
145
+ className="inline-flex items-center gap-2 border px-5 py-3 text-sm font-semibold transition-colors hover:bg-muted"
146
+ style={{ borderRadius: 'var(--sf-radius-button)' }}
147
+ >
148
+ <MessageCircle className="h-4 w-4" />
149
+ WhatsApp
150
+ </a>
151
+ )}
152
+ {config.enquiry.email && (
153
+ <a
154
+ href={`mailto:${config.enquiry.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(message)}`}
155
+ className="inline-flex items-center gap-2 border px-5 py-3 text-sm font-semibold transition-colors hover:bg-muted"
156
+ style={{ borderRadius: 'var(--sf-radius-button)' }}
157
+ >
158
+ <Mail className="h-4 w-4" />
159
+ Email
160
+ </a>
161
+ )}
162
+ </div>
163
+ {status === 'success' && (
164
+ <p data-testid={TID.enquirySuccess} className="text-sm font-medium text-emerald-600">
165
+ {config.enquiry.successMessage}
166
+ </p>
167
+ )}
168
+ {error && (
169
+ <p data-testid={TID.enquiryError} className="text-sm font-medium text-red-600">
170
+ {error}
171
+ </p>
172
+ )}
173
+ </form>
174
+ )
175
+ }
@@ -1,12 +1,15 @@
1
1
  import React from 'react'
2
2
  import type { Product } from '@fayz-ai/shop/types'
3
3
  import { ProductCard } from './ProductCard'
4
- import { useStorefrontConfig } from '../config'
4
+ import { getStorefrontComponents, useStorefrontConfig } from '../config'
5
+ import { useStorefrontActions } from '../hooks/useStorefront'
5
6
  import { TID } from '../testids'
6
7
 
7
8
  export function ProductGrid({ products, loading }: { products: Product[]; loading: boolean }) {
8
9
  const config = useStorefrontConfig()
9
- const ProductCardComponent = config.slots?.ProductCard ?? ProductCard
10
+ const actions = useStorefrontActions()
11
+ const components = getStorefrontComponents(config)
12
+ const ProductCardComponent = components.ProductCard ?? ProductCard
10
13
 
11
14
  if (loading) {
12
15
  return (
@@ -27,7 +30,13 @@ export function ProductGrid({ products, loading }: { products: Product[]; loadin
27
30
  return (
28
31
  <div data-testid={TID.productGrid} className="grid grid-cols-2 gap-5 md:grid-cols-3 xl:grid-cols-4">
29
32
  {products.map((p) => (
30
- <ProductCardComponent key={p.id} product={p} />
33
+ <ProductCardComponent
34
+ key={p.id}
35
+ product={p}
36
+ config={config}
37
+ commerceMode={config.commerceMode}
38
+ actions={actions}
39
+ />
31
40
  ))}
32
41
  </div>
33
42
  )
@@ -1,15 +1,18 @@
1
1
  import React from 'react'
2
2
  import type { ListProductsOptions, Product } from '@fayz-ai/shop/types'
3
3
  import { useProducts } from '../hooks/useProducts'
4
- import { useStorefrontConfig } from '../config'
4
+ import { getStorefrontComponents, useStorefrontConfig } from '../config'
5
+ import { useStorefrontActions } from '../hooks/useStorefront'
5
6
  import { ProductCard } from './ProductCard'
6
7
 
7
8
  /** "Você também pode gostar" — products from the same category, excluding the
8
- * current one. Renders through the store's configured card slot for visual
9
+ * current one. Renders through the store's configured card component for visual
9
10
  * consistency with the catalog. Hidden when there's nothing relevant. */
10
11
  export function RelatedProducts({ product }: { product: Product }) {
11
12
  const config = useStorefrontConfig()
12
- const Card = config.slots?.ProductCard ?? ProductCard
13
+ const actions = useStorefrontActions()
14
+ const components = getStorefrontComponents(config)
15
+ const Card = components.ProductCard ?? ProductCard
13
16
  const opts: ListProductsOptions = product.categoryId
14
17
  ? { categoryId: product.categoryId, status: 'active', limit: 5 }
15
18
  : { status: 'active', limit: 5 }
@@ -23,7 +26,13 @@ export function RelatedProducts({ product }: { product: Product }) {
23
26
  <h2 className="sf-heading text-xl font-semibold tracking-tight">Você também pode gostar</h2>
24
27
  <div className="mt-6 grid grid-cols-2 gap-5 md:grid-cols-4">
25
28
  {related.map((p) => (
26
- <Card key={p.id} product={p} />
29
+ <Card
30
+ key={p.id}
31
+ product={p}
32
+ config={config}
33
+ commerceMode={config.commerceMode}
34
+ actions={actions}
35
+ />
27
36
  ))}
28
37
  </div>
29
38
  </section>
@@ -0,0 +1,59 @@
1
+ import React from 'react'
2
+ import { useStorefrontConfigOptional, type StorefrontImageLoadingConfig } from '../config'
3
+
4
+ export interface SmoothImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
5
+ imageLoading?: StorefrontImageLoadingConfig
6
+ }
7
+
8
+ export function SmoothImage({
9
+ imageLoading,
10
+ loading = 'lazy',
11
+ decoding = 'async',
12
+ onLoad,
13
+ style,
14
+ ...props
15
+ }: SmoothImageProps) {
16
+ const config = useStorefrontConfigOptional()
17
+ const resolved = {
18
+ ...config?.imageLoading,
19
+ ...imageLoading,
20
+ }
21
+ const mode = resolved.mode ?? 'fade'
22
+ const durationMs = resolved.durationMs ?? 420
23
+ const easing = resolved.easing ?? 'cubic-bezier(0.22, 1, 0.36, 1)'
24
+ const blur = resolved.blur ?? true
25
+ const ref = React.useRef<HTMLImageElement | null>(null)
26
+ const [loaded, setLoaded] = React.useState(mode === 'none')
27
+
28
+ React.useEffect(() => {
29
+ if (mode === 'none') {
30
+ setLoaded(true)
31
+ return
32
+ }
33
+ if (ref.current?.complete && ref.current.naturalWidth > 0) setLoaded(true)
34
+ }, [mode, props.src])
35
+
36
+ const revealStyle: React.CSSProperties =
37
+ mode === 'none'
38
+ ? {}
39
+ : {
40
+ opacity: loaded ? 1 : 0,
41
+ filter: blur && !loaded ? 'blur(10px)' : 'blur(0px)',
42
+ transition: `opacity ${durationMs}ms ${easing}, filter ${durationMs}ms ${easing}, transform ${durationMs}ms ${easing}`,
43
+ }
44
+
45
+ return (
46
+ <img
47
+ {...props}
48
+ ref={ref}
49
+ loading={loading}
50
+ decoding={decoding}
51
+ data-sf-image-loaded={loaded ? 'true' : 'false'}
52
+ style={{ ...style, ...revealStyle }}
53
+ onLoad={(event) => {
54
+ setLoaded(true)
55
+ onLoad?.(event)
56
+ }}
57
+ />
58
+ )
59
+ }
@@ -9,6 +9,17 @@ export function StorefrontFooter() {
9
9
  const { categories } = useCategories()
10
10
  const visibleCategories = categories.slice(0, 8)
11
11
  const hiddenCategoryCount = Math.max(categories.length - visibleCategories.length, 0)
12
+ const labels = {
13
+ categories: 'Departamentos',
14
+ navigation: 'Navegação',
15
+ contact: 'Contato',
16
+ viewAll: 'Ver todos',
17
+ purchases: 'Minhas compras',
18
+ privacy: 'Privacidade',
19
+ terms: 'Termos',
20
+ returns: 'Trocas e devoluções',
21
+ ...config.footer?.labels,
22
+ }
12
23
 
13
24
  const goCategory = (categoryId: string) => {
14
25
  useCatalogStore.getState().reset()
@@ -27,7 +38,7 @@ export function StorefrontFooter() {
27
38
  </div>
28
39
 
29
40
  <div>
30
- <h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">Departamentos</h3>
41
+ <h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">{labels.categories}</h3>
31
42
  <ul className="space-y-2 text-sm">
32
43
  {visibleCategories.map((c) => (
33
44
  <li key={c.id}>
@@ -43,7 +54,7 @@ export function StorefrontFooter() {
43
54
  {hiddenCategoryCount > 0 && (
44
55
  <li>
45
56
  <Link to={config.catalogPath} className="font-medium underline-offset-2 transition-opacity hover:underline">
46
- Ver todos ({categories.length})
57
+ {labels.viewAll} ({categories.length})
47
58
  </Link>
48
59
  </li>
49
60
  )}
@@ -51,21 +62,23 @@ export function StorefrontFooter() {
51
62
  </div>
52
63
 
53
64
  <div>
54
- <h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">Navegação</h3>
65
+ <h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">{labels.navigation}</h3>
55
66
  <ul className="space-y-2 text-sm">
56
67
  {config.nav.map((link) => (
57
68
  <li key={link.to}>
58
69
  <Link to={link.to} className="transition-opacity hover:opacity-70">{link.label}</Link>
59
70
  </li>
60
71
  ))}
61
- <li>
62
- <Link to="/account" className="transition-opacity hover:opacity-70">Minhas compras</Link>
63
- </li>
72
+ {config.features.accounts && (
73
+ <li>
74
+ <Link to="/account" className="transition-opacity hover:opacity-70">{labels.purchases}</Link>
75
+ </li>
76
+ )}
64
77
  </ul>
65
78
  </div>
66
79
 
67
80
  <div>
68
- <h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">Contato</h3>
81
+ <h3 className="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">{labels.contact}</h3>
69
82
  <ul className="space-y-2 text-sm text-muted-foreground">
70
83
  {config.footer?.contact?.phone && <li>{config.footer.contact.phone}</li>}
71
84
  {config.footer?.contact?.email && <li>{config.footer.contact.email}</li>}
@@ -83,15 +96,12 @@ export function StorefrontFooter() {
83
96
  <div className="border-t">
84
97
  <div className="mx-auto flex max-w-7xl flex-col items-center gap-3 px-4 py-5 text-xs text-muted-foreground sm:flex-row sm:justify-between sm:px-6">
85
98
  <nav className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
86
- <Link to="/privacy" className="transition-colors hover:text-foreground">Privacidade</Link>
87
- <Link to="/terms" className="transition-colors hover:text-foreground">Termos</Link>
88
- <Link to="/refunds" className="transition-colors hover:text-foreground">Trocas e devoluções</Link>
99
+ <Link to="/privacy" className="transition-colors hover:text-foreground">{labels.privacy}</Link>
100
+ <Link to="/terms" className="transition-colors hover:text-foreground">{labels.terms}</Link>
101
+ <Link to="/refunds" className="transition-colors hover:text-foreground">{labels.returns}</Link>
89
102
  </nav>
90
103
  <p className="text-center">
91
- © {config.name} — powered by Fayz · Fotos:{' '}
92
- <a href="https://unsplash.com" target="_blank" rel="noreferrer" className="underline-offset-2 hover:underline">
93
- Unsplash
94
- </a>
104
+ {config.footer?.credit ?? <>© {config.name} — powered by Fayz</>}
95
105
  </p>
96
106
  </div>
97
107
  </div>