@thorprovider/blocks 0.1.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/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { ShoppingCart, Zap, Clock, CreditCard, Wallet, Minus, Plus, X } from 'lucide-react'
|
|
5
|
+
import type { LucideIcon } from 'lucide-react'
|
|
6
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
7
|
+
import { Button } from '../../../ui/button'
|
|
8
|
+
import { Separator } from '../../../ui/separator'
|
|
9
|
+
import { Progress } from '../../../ui/progress'
|
|
10
|
+
import { ShoppingCart4SpinningCounter } from './shopping-cart-4-spinning-counter'
|
|
11
|
+
import { cn } from '../../../lib/utils'
|
|
12
|
+
|
|
13
|
+
export interface ShoppingCart4Item {
|
|
14
|
+
id: string // serialization-friendly (seed used number)
|
|
15
|
+
name: string
|
|
16
|
+
price: number
|
|
17
|
+
quantity: number
|
|
18
|
+
image: string
|
|
19
|
+
variant?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ShoppingCart4Promotion {
|
|
23
|
+
threshold: number
|
|
24
|
+
reward: string
|
|
25
|
+
icon: LucideIcon
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ShoppingCart4RecentlyViewed {
|
|
29
|
+
id: string
|
|
30
|
+
name: string
|
|
31
|
+
price: number
|
|
32
|
+
image: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ShoppingCart4Props {
|
|
36
|
+
items: ShoppingCart4Item[]
|
|
37
|
+
promotions: ShoppingCart4Promotion[]
|
|
38
|
+
recentlyViewed?: ShoppingCart4RecentlyViewed[]
|
|
39
|
+
// business rules externalized:
|
|
40
|
+
taxRate?: number // default 0.08
|
|
41
|
+
// labels (Spanish defaults):
|
|
42
|
+
titleLabel?: string // 'Carrito de compras'
|
|
43
|
+
subtotalLabel?: string // 'Subtotal'
|
|
44
|
+
taxLabel?: string // 'Impuestos'
|
|
45
|
+
shippingLabel?: string // 'Envío'
|
|
46
|
+
shippingFreeLabel?: string // 'Gratis'
|
|
47
|
+
totalLabel?: string // 'Total'
|
|
48
|
+
awayFromLabel?: string // 'Te faltan ${amount} para'
|
|
49
|
+
activatedLabel?: string // '¡{reward} activada!'
|
|
50
|
+
itemsLabel?: string // 'artículos'
|
|
51
|
+
expressCheckoutLabel?: string // 'Compra rápida'
|
|
52
|
+
expressLabel?: string // 'Rápido'
|
|
53
|
+
standardLabel?: string // 'Estándar'
|
|
54
|
+
estimatedDeliveryLabel?: string // 'Entrega estimada: 2-3 días hábiles'
|
|
55
|
+
secureCheckoutLabel?: string // 'Pago seguro • SSL cifrado'
|
|
56
|
+
recentlyViewedLabel?: string // 'Vistos recientemente'
|
|
57
|
+
collapseLabel?: string // 'Contraer carrito'
|
|
58
|
+
expandLabel?: string // 'Expandir carrito'
|
|
59
|
+
checkoutLabel?: string // 'Ver carrito y finalizar'
|
|
60
|
+
quickCheckoutLabel?: string // 'Compra rápida'
|
|
61
|
+
checkoutNowLabel?: string // 'Finalizar ahora'
|
|
62
|
+
// callbacks:
|
|
63
|
+
onQuantityChange?: (id: string, quantity: number) => void
|
|
64
|
+
onRemove?: (id: string) => void
|
|
65
|
+
onCheckout?: () => void
|
|
66
|
+
onQuickCheckout?: () => void // Express mode checkout button
|
|
67
|
+
onPayWithApple?: () => void
|
|
68
|
+
onPayWithGoogle?: () => void
|
|
69
|
+
onRecentlyViewedClick?: (id: string) => void
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const interpolate = (template: string, values: Record<string, string>) =>
|
|
73
|
+
template.replace(/\$\{(\w+)\}|\{(\w+)\}/g, (match, dollarKey, braceKey) => {
|
|
74
|
+
const key = dollarKey ?? braceKey
|
|
75
|
+
return values[key] ?? match
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export function ShoppingCart4({
|
|
79
|
+
items,
|
|
80
|
+
promotions,
|
|
81
|
+
recentlyViewed,
|
|
82
|
+
taxRate = 0.08,
|
|
83
|
+
titleLabel = 'Carrito de compras',
|
|
84
|
+
subtotalLabel = 'Subtotal',
|
|
85
|
+
taxLabel = 'Impuestos',
|
|
86
|
+
shippingLabel = 'Envío',
|
|
87
|
+
shippingFreeLabel = 'Gratis',
|
|
88
|
+
totalLabel = 'Total',
|
|
89
|
+
awayFromLabel = 'Te faltan ${amount} para',
|
|
90
|
+
activatedLabel = '¡{reward} activada!',
|
|
91
|
+
itemsLabel = 'artículos',
|
|
92
|
+
expressCheckoutLabel = 'Compra rápida',
|
|
93
|
+
expressLabel = 'Rápido',
|
|
94
|
+
standardLabel = 'Estándar',
|
|
95
|
+
estimatedDeliveryLabel = 'Entrega estimada: 2-3 días hábiles',
|
|
96
|
+
secureCheckoutLabel = 'Pago seguro • SSL cifrado',
|
|
97
|
+
recentlyViewedLabel = 'Vistos recientemente',
|
|
98
|
+
collapseLabel = 'Contraer carrito',
|
|
99
|
+
expandLabel = 'Expandir carrito',
|
|
100
|
+
checkoutLabel = 'Ver carrito y finalizar',
|
|
101
|
+
quickCheckoutLabel = 'Compra rápida',
|
|
102
|
+
checkoutNowLabel = 'Finalizar ahora',
|
|
103
|
+
onQuantityChange,
|
|
104
|
+
onRemove,
|
|
105
|
+
onCheckout,
|
|
106
|
+
onQuickCheckout,
|
|
107
|
+
onPayWithApple,
|
|
108
|
+
onPayWithGoogle,
|
|
109
|
+
onRecentlyViewedClick,
|
|
110
|
+
}: ShoppingCart4Props) {
|
|
111
|
+
const [isExpanded, setIsExpanded] = useState(true)
|
|
112
|
+
const [quickCheckout, setQuickCheckout] = useState(false)
|
|
113
|
+
|
|
114
|
+
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
|
|
115
|
+
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
|
116
|
+
const tax = subtotal * taxRate
|
|
117
|
+
const total = subtotal + tax
|
|
118
|
+
|
|
119
|
+
const sortedPromotions = [...promotions].sort((a, b) => a.threshold - b.threshold)
|
|
120
|
+
const currentPromo = (() => {
|
|
121
|
+
for (let i = sortedPromotions.length - 1; i >= 0; i--) {
|
|
122
|
+
if (subtotal >= sortedPromotions[i].threshold) {
|
|
123
|
+
return { ...sortedPromotions[i], achieved: true }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const next = sortedPromotions.find(p => subtotal < p.threshold)
|
|
127
|
+
return next ? { ...next, achieved: false } : null
|
|
128
|
+
})()
|
|
129
|
+
const progressPercentage =
|
|
130
|
+
currentPromo && !currentPromo.achieved
|
|
131
|
+
? Math.min(100, (subtotal / currentPromo.threshold) * 100)
|
|
132
|
+
: 100
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<section className='py-8'>
|
|
136
|
+
<div className='mx-auto max-w-md px-4'>
|
|
137
|
+
<Card className='shadow-lg'>
|
|
138
|
+
{/* Header */}
|
|
139
|
+
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-3'>
|
|
140
|
+
<CardTitle className='flex items-center gap-2'>
|
|
141
|
+
<ShoppingCart className='size-5' />
|
|
142
|
+
{titleLabel} ({itemCount})
|
|
143
|
+
</CardTitle>
|
|
144
|
+
<Button
|
|
145
|
+
variant='ghost'
|
|
146
|
+
className='h-8 cursor-pointer px-3 text-xs'
|
|
147
|
+
size='sm'
|
|
148
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
149
|
+
aria-expanded={isExpanded}
|
|
150
|
+
aria-label={isExpanded ? collapseLabel : expandLabel}
|
|
151
|
+
>
|
|
152
|
+
{isExpanded ? <X /> : <Plus />}
|
|
153
|
+
</Button>
|
|
154
|
+
</CardHeader>
|
|
155
|
+
|
|
156
|
+
<CardContent className='flex flex-col gap-6'>
|
|
157
|
+
{items.length === 0 ? (
|
|
158
|
+
<div className='flex flex-col items-center justify-center gap-2 py-6 text-center'>
|
|
159
|
+
<ShoppingCart className='text-muted-foreground/50 size-10' />
|
|
160
|
+
<p className='text-muted-foreground text-sm'>Tu carrito está vacío</p>
|
|
161
|
+
</div>
|
|
162
|
+
) : (
|
|
163
|
+
<>
|
|
164
|
+
{/* Promotion Progress */}
|
|
165
|
+
{currentPromo && !currentPromo.achieved ? (
|
|
166
|
+
<div className='flex flex-col gap-3'>
|
|
167
|
+
<div className='flex items-center justify-between text-sm'>
|
|
168
|
+
<span className='text-muted-foreground'>
|
|
169
|
+
{interpolate(awayFromLabel, {
|
|
170
|
+
amount: `$${(currentPromo.threshold - subtotal).toFixed(2)}`,
|
|
171
|
+
})}
|
|
172
|
+
</span>
|
|
173
|
+
<div className='flex items-center gap-1'>
|
|
174
|
+
<currentPromo.icon className='text-primary size-4' />
|
|
175
|
+
<span className='text-primary font-medium'>{currentPromo.reward}</span>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<Progress value={progressPercentage} className='h-2' />
|
|
179
|
+
</div>
|
|
180
|
+
) : null}
|
|
181
|
+
|
|
182
|
+
{/* Active Promotion */}
|
|
183
|
+
{currentPromo?.achieved ? (
|
|
184
|
+
<div className='rounded-lg bg-success/10 p-3'>
|
|
185
|
+
<div className='flex items-center gap-2 text-success'>
|
|
186
|
+
<currentPromo.icon className='size-4' />
|
|
187
|
+
<span className='text-sm font-medium'>
|
|
188
|
+
{interpolate(activatedLabel, { reward: currentPromo.reward })}
|
|
189
|
+
</span>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
) : null}
|
|
193
|
+
|
|
194
|
+
{isExpanded && (
|
|
195
|
+
<>
|
|
196
|
+
{/* Cart Items */}
|
|
197
|
+
<div className='scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-gray-600 scrollbar-track-transparent flex max-h-64 flex-col gap-4 overflow-y-auto'>
|
|
198
|
+
{items.map(item => (
|
|
199
|
+
<div key={item.id} className='flex items-start gap-3'>
|
|
200
|
+
<img src={item.image || undefined} alt={item.name} className='h-12 w-12 rounded-md object-cover' />
|
|
201
|
+
|
|
202
|
+
<div className='flex flex-1 flex-col gap-1'>
|
|
203
|
+
<h3 className='line-clamp-2 text-sm font-medium'>{item.name}</h3>
|
|
204
|
+
{item.variant ? <p className='text-muted-foreground text-xs'>{item.variant}</p> : null}
|
|
205
|
+
|
|
206
|
+
<div className='flex items-center justify-between'>
|
|
207
|
+
<span className='text-sm font-semibold'>${item.price.toFixed(2)}</span>
|
|
208
|
+
|
|
209
|
+
<div className='flex items-center'>
|
|
210
|
+
<Button
|
|
211
|
+
variant='ghost'
|
|
212
|
+
size='sm'
|
|
213
|
+
className='size-6 h-8 cursor-pointer p-0 px-3 text-xs'
|
|
214
|
+
onClick={
|
|
215
|
+
onQuantityChange
|
|
216
|
+
? () => onQuantityChange(item.id, item.quantity - 1)
|
|
217
|
+
: undefined
|
|
218
|
+
}
|
|
219
|
+
disabled={item.quantity <= 1}
|
|
220
|
+
aria-label={`Disminuir cantidad de ${item.name}`}
|
|
221
|
+
>
|
|
222
|
+
<Minus />
|
|
223
|
+
</Button>
|
|
224
|
+
<span className='mx-2 w-6 text-center text-sm'>{item.quantity}</span>
|
|
225
|
+
<Button
|
|
226
|
+
variant='ghost'
|
|
227
|
+
size='sm'
|
|
228
|
+
className='size-6 h-8 cursor-pointer p-0 px-3 text-xs'
|
|
229
|
+
onClick={
|
|
230
|
+
onQuantityChange
|
|
231
|
+
? () => onQuantityChange(item.id, item.quantity + 1)
|
|
232
|
+
: undefined
|
|
233
|
+
}
|
|
234
|
+
aria-label={`Aumentar cantidad de ${item.name}`}
|
|
235
|
+
>
|
|
236
|
+
<Plus />
|
|
237
|
+
</Button>
|
|
238
|
+
<Button
|
|
239
|
+
variant='ghost'
|
|
240
|
+
size='sm'
|
|
241
|
+
className='ms-2 size-6 h-8 cursor-pointer p-0 px-3 text-xs text-destructive hover:text-destructive'
|
|
242
|
+
onClick={onRemove ? () => onRemove(item.id) : undefined}
|
|
243
|
+
aria-label={`Quitar ${item.name}`}
|
|
244
|
+
>
|
|
245
|
+
<X />
|
|
246
|
+
</Button>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<Separator />
|
|
255
|
+
|
|
256
|
+
{/* Order Summary */}
|
|
257
|
+
<div className='flex flex-col gap-2'>
|
|
258
|
+
<div className='flex justify-between text-sm'>
|
|
259
|
+
<span>{subtotalLabel}</span>
|
|
260
|
+
<span>${subtotal.toFixed(2)}</span>
|
|
261
|
+
</div>
|
|
262
|
+
<div className='flex justify-between text-sm'>
|
|
263
|
+
<span>{taxLabel}</span>
|
|
264
|
+
<span>${tax.toFixed(2)}</span>
|
|
265
|
+
</div>
|
|
266
|
+
<div className='flex justify-between text-sm text-success'>
|
|
267
|
+
<span>{shippingLabel}</span>
|
|
268
|
+
<span>{shippingFreeLabel}</span>
|
|
269
|
+
</div>
|
|
270
|
+
<Separator />
|
|
271
|
+
<div className='flex justify-between font-semibold'>
|
|
272
|
+
<span>{totalLabel}</span>
|
|
273
|
+
<span>
|
|
274
|
+
<ShoppingCart4SpinningCounter value={total} decimals={2} prefix='$' />
|
|
275
|
+
</span>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{/* Quick Actions */}
|
|
280
|
+
<div className='flex flex-col gap-3'>
|
|
281
|
+
{/* Express Checkout Toggle */}
|
|
282
|
+
<div className='flex items-center justify-between text-sm'>
|
|
283
|
+
<span>{expressCheckoutLabel}</span>
|
|
284
|
+
<Button
|
|
285
|
+
variant='outline'
|
|
286
|
+
size='sm'
|
|
287
|
+
onClick={() => setQuickCheckout(!quickCheckout)}
|
|
288
|
+
className='h-8 cursor-pointer'
|
|
289
|
+
aria-pressed={quickCheckout}
|
|
290
|
+
>
|
|
291
|
+
<Zap />
|
|
292
|
+
{quickCheckout ? standardLabel : expressLabel}
|
|
293
|
+
</Button>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{quickCheckout && (
|
|
297
|
+
<div className='grid grid-cols-2 gap-2'>
|
|
298
|
+
<Button size='sm' variant='outline' className='h-8 cursor-pointer px-3 text-xs' onClick={onPayWithApple}>
|
|
299
|
+
<Wallet />
|
|
300
|
+
Apple Pay
|
|
301
|
+
</Button>
|
|
302
|
+
<Button size='sm' variant='outline' className='h-8 cursor-pointer px-3 text-xs' onClick={onPayWithGoogle}>
|
|
303
|
+
<CreditCard />
|
|
304
|
+
Google Pay
|
|
305
|
+
</Button>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
{/* Checkout Button */}
|
|
310
|
+
<Button
|
|
311
|
+
size='lg'
|
|
312
|
+
className='h-10 w-full cursor-pointer px-8'
|
|
313
|
+
onClick={quickCheckout ? onQuickCheckout : onCheckout}
|
|
314
|
+
>
|
|
315
|
+
{quickCheckout ? quickCheckoutLabel : checkoutLabel}
|
|
316
|
+
</Button>
|
|
317
|
+
|
|
318
|
+
{/* Estimated Delivery */}
|
|
319
|
+
<div className='text-muted-foreground flex items-center justify-center gap-2 text-xs'>
|
|
320
|
+
<Clock className='size-3' />
|
|
321
|
+
<span>{estimatedDeliveryLabel}</span>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{/* Security Badge */}
|
|
326
|
+
<div className='bg-muted/50 rounded-lg p-3 text-center'>
|
|
327
|
+
<div className='text-muted-foreground flex items-center justify-center gap-2 text-xs'>
|
|
328
|
+
<div className='size-2 rounded-full bg-success' aria-hidden='true' />
|
|
329
|
+
<span>{secureCheckoutLabel}</span>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
|
|
335
|
+
{/* Collapsed Summary */}
|
|
336
|
+
{!isExpanded && (
|
|
337
|
+
<div className='flex flex-col gap-3'>
|
|
338
|
+
<div className='flex items-center justify-between'>
|
|
339
|
+
<span className='text-muted-foreground text-sm'>
|
|
340
|
+
{itemCount} {itemsLabel}
|
|
341
|
+
</span>
|
|
342
|
+
<span className='font-semibold'>
|
|
343
|
+
<ShoppingCart4SpinningCounter value={total} decimals={2} prefix='$' />
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<Button size='sm' className='h-8 w-full cursor-pointer px-3 text-xs' onClick={onCheckout}>
|
|
348
|
+
{checkoutNowLabel}
|
|
349
|
+
</Button>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
|
|
353
|
+
{/* Recently Viewed */}
|
|
354
|
+
{isExpanded && recentlyViewed && recentlyViewed.length > 0 && (
|
|
355
|
+
<div className='flex flex-col gap-3'>
|
|
356
|
+
<h4 className='text-sm font-medium'>{recentlyViewedLabel}</h4>
|
|
357
|
+
<div className='grid grid-cols-3 gap-2'>
|
|
358
|
+
{recentlyViewed.map(item => (
|
|
359
|
+
<button
|
|
360
|
+
key={item.id}
|
|
361
|
+
type='button'
|
|
362
|
+
className={cn(
|
|
363
|
+
'group text-start',
|
|
364
|
+
onRecentlyViewedClick && 'cursor-pointer',
|
|
365
|
+
)}
|
|
366
|
+
onClick={onRecentlyViewedClick ? () => onRecentlyViewedClick(item.id) : undefined}
|
|
367
|
+
>
|
|
368
|
+
<div className='mb-1 aspect-square overflow-hidden'>
|
|
369
|
+
<img
|
|
370
|
+
src={item.image || undefined}
|
|
371
|
+
alt={item.name}
|
|
372
|
+
className='size-full rounded-md object-cover'
|
|
373
|
+
/>
|
|
374
|
+
</div>
|
|
375
|
+
<p className='text-muted-foreground truncate text-xs'>{item.name}</p>
|
|
376
|
+
<p className='text-xs font-medium'>${item.price.toFixed(2)}</p>
|
|
377
|
+
</button>
|
|
378
|
+
))}
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
</>
|
|
383
|
+
)}
|
|
384
|
+
</CardContent>
|
|
385
|
+
</Card>
|
|
386
|
+
</div>
|
|
387
|
+
</section>
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export default ShoppingCart4
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { cn } from '../../../lib/utils'
|
|
3
|
+
|
|
4
|
+
export interface StoreContainer1Props {
|
|
5
|
+
size?: 'default' | 'narrow' | 'wide' | 'full'
|
|
6
|
+
className?: string
|
|
7
|
+
children?: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sizeClasses: Record<NonNullable<StoreContainer1Props['size']>, string> = {
|
|
11
|
+
default: 'mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8',
|
|
12
|
+
narrow: 'mx-auto w-full max-w-3xl px-4 sm:px-6 lg:px-8',
|
|
13
|
+
wide: 'mx-auto w-full max-w-screen-2xl px-4 sm:px-6 lg:px-8',
|
|
14
|
+
full: 'w-full',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Trivial presentational layout container used as the root of spec-driven
|
|
19
|
+
* storefront pages. Constrains content width; children render as-is.
|
|
20
|
+
*/
|
|
21
|
+
export function StoreContainer1({
|
|
22
|
+
size = 'default',
|
|
23
|
+
className,
|
|
24
|
+
children,
|
|
25
|
+
}: StoreContainer1Props) {
|
|
26
|
+
return <div className={cn(sizeClasses[size], className)}>{children}</div>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default StoreContainer1
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { motion, useReducedMotion } from 'motion/react'
|
|
4
|
+
|
|
5
|
+
import { Badge } from '../../../ui/badge'
|
|
6
|
+
import { Button } from '../../../ui/button'
|
|
7
|
+
import { ShoppingBag, Star, ArrowRight } from 'lucide-react'
|
|
8
|
+
import type { ComponentProps, ReactNode } from 'react'
|
|
9
|
+
import type { CTA } from '../../../lib/types'
|
|
10
|
+
|
|
11
|
+
export interface FeaturedProduct {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
brand: string
|
|
15
|
+
price: number
|
|
16
|
+
originalPrice: number
|
|
17
|
+
rating: number
|
|
18
|
+
reviewCount: number
|
|
19
|
+
inStock: boolean
|
|
20
|
+
isNew: boolean
|
|
21
|
+
isOnSale: boolean
|
|
22
|
+
image: string
|
|
23
|
+
colors: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StorefrontHero1Props {
|
|
27
|
+
title: string
|
|
28
|
+
description: string
|
|
29
|
+
products: FeaturedProduct[]
|
|
30
|
+
titleAccent?: string
|
|
31
|
+
badge?: string
|
|
32
|
+
primaryCta?: CTA
|
|
33
|
+
secondaryCta?: CTA
|
|
34
|
+
onAddToCart?: (product: FeaturedProduct) => void
|
|
35
|
+
onBuyNow?: (product: FeaturedProduct) => void
|
|
36
|
+
onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
|
|
37
|
+
addToCartLabel?: string
|
|
38
|
+
buyNowLabel?: string
|
|
39
|
+
newLabel?: string
|
|
40
|
+
outOfStockLabel?: string
|
|
41
|
+
colorsLabel?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function CtaButton({
|
|
45
|
+
cta,
|
|
46
|
+
variant,
|
|
47
|
+
children,
|
|
48
|
+
}: {
|
|
49
|
+
cta: CTA
|
|
50
|
+
variant?: ComponentProps<typeof Button>['variant']
|
|
51
|
+
children: ReactNode
|
|
52
|
+
}) {
|
|
53
|
+
if (cta.href) {
|
|
54
|
+
return (
|
|
55
|
+
<Button
|
|
56
|
+
variant={variant}
|
|
57
|
+
size='lg'
|
|
58
|
+
className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
|
|
59
|
+
asChild
|
|
60
|
+
>
|
|
61
|
+
<a href={cta.href}>{children}</a>
|
|
62
|
+
</Button>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Button
|
|
68
|
+
variant={variant}
|
|
69
|
+
size='lg'
|
|
70
|
+
className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</Button>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function StorefrontHero1({
|
|
78
|
+
title,
|
|
79
|
+
description,
|
|
80
|
+
products,
|
|
81
|
+
titleAccent,
|
|
82
|
+
badge,
|
|
83
|
+
primaryCta,
|
|
84
|
+
secondaryCta,
|
|
85
|
+
onAddToCart,
|
|
86
|
+
onBuyNow,
|
|
87
|
+
onSelectColor,
|
|
88
|
+
addToCartLabel = 'Agregar al carrito',
|
|
89
|
+
buyNowLabel = 'Comprar ahora',
|
|
90
|
+
newLabel = 'Nuevo',
|
|
91
|
+
outOfStockLabel = 'Agotado',
|
|
92
|
+
colorsLabel = 'Colores:',
|
|
93
|
+
}: StorefrontHero1Props) {
|
|
94
|
+
const reduceMotion = useReducedMotion()
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div className='from-primary/5 to-background relative overflow-hidden bg-gradient-to-br'>
|
|
98
|
+
<div className='relative z-10 mx-auto w-full max-w-7xl flex flex-col items-center justify-between gap-12 px-4 sm:px-6 lg:px-8 py-16 lg:py-20 xl:flex-row xl:items-start'>
|
|
99
|
+
<motion.div
|
|
100
|
+
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
101
|
+
animate={{ opacity: 1, y: 0 }}
|
|
102
|
+
transition={{ duration: 0.5, ease: 'easeOut' }}
|
|
103
|
+
className='mx-auto max-w-xl text-center lg:mx-0 xl:text-left'
|
|
104
|
+
>
|
|
105
|
+
{badge && (
|
|
106
|
+
<div className='border-primary/20 bg-primary/5 text-primary inline-flex items-center rounded-full border px-4 py-1.5 text-sm font-medium backdrop-blur-sm'>
|
|
107
|
+
{badge}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
<h1 className='text-foreground mt-8 text-4xl font-bold tracking-tight sm:text-5xl md:text-7xl'>
|
|
111
|
+
{title}{' '}
|
|
112
|
+
{titleAccent && (
|
|
113
|
+
<span className='from-primary to-primary/70 bg-gradient-to-r bg-clip-text text-transparent'>
|
|
114
|
+
{titleAccent}
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</h1>
|
|
118
|
+
<p className='text-muted-foreground mt-8 text-lg'>{description}</p>
|
|
119
|
+
{(primaryCta || secondaryCta) && (
|
|
120
|
+
<div className='mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center xl:justify-start'>
|
|
121
|
+
{primaryCta && (
|
|
122
|
+
<CtaButton cta={primaryCta}>
|
|
123
|
+
<span className='relative z-10 flex items-center gap-2'>
|
|
124
|
+
{primaryCta.label}
|
|
125
|
+
<ArrowRight className='transition-transform group-hover:translate-x-1' />
|
|
126
|
+
</span>
|
|
127
|
+
<span className='bg-primary/5 absolute inset-0 -z-0 opacity-0 transition-opacity group-hover:opacity-100'></span>
|
|
128
|
+
</CtaButton>
|
|
129
|
+
)}
|
|
130
|
+
{secondaryCta && (
|
|
131
|
+
<CtaButton cta={secondaryCta} variant='outline'>
|
|
132
|
+
<span className='relative z-10 flex items-center gap-2'>
|
|
133
|
+
<ShoppingBag />
|
|
134
|
+
{secondaryCta.label}
|
|
135
|
+
</span>
|
|
136
|
+
</CtaButton>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
</motion.div>
|
|
141
|
+
|
|
142
|
+
<div className='w-full max-w-2xl lg:mt-0'>
|
|
143
|
+
<div className='grid gap-6 sm:grid-cols-2'>
|
|
144
|
+
{products.map((product, index) => (
|
|
145
|
+
<motion.div
|
|
146
|
+
key={product.id}
|
|
147
|
+
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
148
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
149
|
+
viewport={{ once: true, margin: '-40px' }}
|
|
150
|
+
transition={{
|
|
151
|
+
duration: 0.45,
|
|
152
|
+
delay: reduceMotion ? 0 : index * 0.1,
|
|
153
|
+
ease: 'easeOut',
|
|
154
|
+
}}
|
|
155
|
+
className='bg-card relative h-full overflow-hidden rounded-lg border shadow-sm transition-shadow hover:shadow-md'
|
|
156
|
+
>
|
|
157
|
+
<div className='absolute top-4 left-4 z-10 flex flex-col gap-2'>
|
|
158
|
+
{product.isNew && (
|
|
159
|
+
<Badge variant='secondary' className='rounded-full'>{newLabel}</Badge>
|
|
160
|
+
)}
|
|
161
|
+
{product.isOnSale && (
|
|
162
|
+
<Badge variant='destructive' className='rounded-full'>
|
|
163
|
+
{Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)}% OFF
|
|
164
|
+
</Badge>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div className='relative aspect-square overflow-hidden'>
|
|
169
|
+
<img
|
|
170
|
+
src={product.image || undefined}
|
|
171
|
+
alt={`${product.brand} ${product.name}`}
|
|
172
|
+
className='size-full object-cover'
|
|
173
|
+
loading='lazy'
|
|
174
|
+
/>
|
|
175
|
+
{!product.inStock && (
|
|
176
|
+
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
|
|
177
|
+
<span className='rounded bg-white/90 px-3 py-1 text-sm font-medium'>{outOfStockLabel}</span>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div className='p-4'>
|
|
183
|
+
<div className='text-muted-foreground mb-1 text-sm font-medium'>{product.brand}</div>
|
|
184
|
+
<h3 className='mb-2 font-medium'>{product.name}</h3>
|
|
185
|
+
|
|
186
|
+
<div className='mb-3'>
|
|
187
|
+
<span className='text-lg font-bold'>${product.price.toFixed(2)}</span>
|
|
188
|
+
{product.isOnSale && (
|
|
189
|
+
<span className='text-muted-foreground ms-2 text-sm line-through'>
|
|
190
|
+
${product.originalPrice.toFixed(2)}
|
|
191
|
+
</span>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className='mb-4 flex items-center'>
|
|
196
|
+
<span className='text-muted-foreground me-2 text-sm'>{colorsLabel}</span>
|
|
197
|
+
<div className='flex gap-1'>
|
|
198
|
+
{product.colors.map((color, i) => (
|
|
199
|
+
<button
|
|
200
|
+
key={i}
|
|
201
|
+
className='size-5 rounded-full border'
|
|
202
|
+
style={{ backgroundColor: color }}
|
|
203
|
+
aria-label={`Opción de color ${i + 1}`}
|
|
204
|
+
onClick={onSelectColor ? () => onSelectColor(product, i, product.colors[i]) : undefined}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className='mb-4 flex items-center'>
|
|
211
|
+
<div className='flex'>
|
|
212
|
+
{[...Array(5)].map((_, i) => (
|
|
213
|
+
<Star
|
|
214
|
+
key={i}
|
|
215
|
+
className={`size-4 ${i < Math.floor(product.rating) ? 'fill-foreground text-foreground' : 'text-muted-foreground'}`}
|
|
216
|
+
/>
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
<span className='text-muted-foreground ms-2 text-sm'>({product.reviewCount.toLocaleString()})</span>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className='flex flex-col gap-2'>
|
|
223
|
+
<Button variant='outline' className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onAddToCart ? () => onAddToCart(product) : undefined}>
|
|
224
|
+
<ShoppingBag />
|
|
225
|
+
{addToCartLabel}
|
|
226
|
+
</Button>
|
|
227
|
+
<Button className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onBuyNow ? () => onBuyNow(product) : undefined}>
|
|
228
|
+
{buyNowLabel}
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
</motion.div>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<div className='bg-primary/10 absolute -top-32 -right-32 size-96 rounded-full blur-3xl' />
|
|
239
|
+
<div className='bg-secondary/30 absolute -bottom-32 -left-32 size-96 rounded-full blur-3xl' />
|
|
240
|
+
</div>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export default StorefrontHero1
|