@thorprovider/blocks 0.1.3 → 2.0.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 (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -1,142 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect, useRef } from 'react'
4
-
5
- import { cn } from '../../../lib/utils'
6
-
7
- const CELL_SIZE = 20
8
- const DURATION = 450
9
- const STAGGER = 60
10
- const SPINS = 1
11
- const SPIN_BLUR = 2
12
-
13
- const REEL_DIGITS = Array.from({ length: (SPINS + 1) * 10 }, (_, i) => i % 10)
14
-
15
- type ShoppingCart1SpinningCounterProps = {
16
- value: number
17
- decimals?: number
18
- prefix?: string
19
- className?: string
20
- digitClassName?: string
21
- }
22
-
23
- export function ShoppingCart1SpinningCounter({
24
- value,
25
- decimals = 0,
26
- prefix,
27
- className,
28
- digitClassName,
29
- }: ShoppingCart1SpinningCounterProps) {
30
- const safeValue = Math.max(0, value)
31
- const formatted = safeValue.toFixed(decimals)
32
- const chars = formatted.split('')
33
- const containerRef = useRef<HTMLSpanElement>(null)
34
- const isFirstRender = useRef(true)
35
-
36
- useEffect(() => {
37
- const container = containerRef.current
38
- if (!container) return
39
-
40
- const strips = Array.from(container.querySelectorAll<HTMLElement>('[data-reel-strip]'))
41
- const timeouts: number[] = []
42
-
43
- const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
44
-
45
- strips.forEach((strip, index) => {
46
- const digit = Number(strip.dataset.digit)
47
- const delay = (strips.length - 1 - index) * STAGGER
48
-
49
- if (isFirstRender.current || !strip.dataset.initialized || prefersReducedMotion) {
50
- strip.style.transition = 'none'
51
- strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
52
- strip.style.filter = 'blur(0px)'
53
- strip.dataset.initialized = 'true'
54
- void strip.offsetHeight
55
- return
56
- }
57
-
58
- const landingIndex = SPINS * 10 + digit
59
- strip.style.transition = `transform ${DURATION}ms cubic-bezier(0.16, 1, 0.3, 1) ${delay}ms, filter ${DURATION}ms ease-out ${delay}ms`
60
- strip.style.transform = `translateY(-${landingIndex * CELL_SIZE}px)`
61
- strip.style.filter = `blur(${SPIN_BLUR}px)`
62
-
63
- const settleTimeout = window.setTimeout(() => {
64
- strip.style.filter = 'blur(0px)'
65
- }, delay + 30)
66
-
67
- const resetTimeout = window.setTimeout(() => {
68
- strip.style.transition = 'none'
69
- strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
70
- void strip.offsetHeight
71
- }, delay + DURATION + 30)
72
-
73
- timeouts.push(settleTimeout, resetTimeout)
74
- })
75
-
76
- isFirstRender.current = false
77
-
78
- return () => {
79
- timeouts.forEach(window.clearTimeout)
80
- }
81
- }, [formatted])
82
-
83
- return (
84
- <span className='relative inline-flex items-center'>
85
- <span className='sr-only'>{prefix}{formatted}</span>
86
- <span
87
- ref={containerRef}
88
- className={cn('inline-flex items-center font-[inherit] tabular-nums', className)}
89
- style={{ height: CELL_SIZE }}
90
- aria-hidden='true'
91
- >
92
- {prefix ? (
93
- <span
94
- className={digitClassName}
95
- style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}
96
- >
97
- {prefix}
98
- </span>
99
- ) : null}
100
- {chars.map((char, index) => {
101
- if (!/\d/.test(char)) {
102
- return (
103
- <span
104
- key={index}
105
- className={digitClassName}
106
- style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}
107
- >
108
- {char}
109
- </span>
110
- )
111
- }
112
-
113
- return (
114
- <span
115
- key={index}
116
- className='relative inline-block overflow-hidden'
117
- style={{
118
- height: CELL_SIZE,
119
- maskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
120
- WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
121
- }}
122
- >
123
- <span data-reel-strip data-digit={char} className='flex flex-col will-change-[transform,filter]'>
124
- {REEL_DIGITS.map((cellDigit, cellIndex) => (
125
- <span
126
- key={cellIndex}
127
- className={cn('flex items-center justify-center', digitClassName)}
128
- style={{ height: CELL_SIZE }}
129
- >
130
- {cellDigit}
131
- </span>
132
- ))}
133
- </span>
134
- </span>
135
- )
136
- })}
137
- </span>
138
- </span>
139
- )
140
- }
141
-
142
- export default ShoppingCart1SpinningCounter
@@ -1,291 +0,0 @@
1
- import { Minus, MoveRight, Package, Plus, Shield, ShoppingBag, Store, Trash2, CreditCard } from 'lucide-react'
2
- import { Button } from '../../../ui/button'
3
- import { Separator } from '../../../ui/separator'
4
- import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '../../../ui/card'
5
- import { ShoppingCart1SpinningCounter } from './shopping-cart-1-spinning-counter'
6
- import { cn } from '../../../lib/utils'
7
-
8
- export interface ShoppingCart1CartItem {
9
- id: string
10
- name: string
11
- color: string
12
- size: string
13
- price: number
14
- originalPrice: number
15
- quantity: number
16
- image: string
17
- estimatedDelivery: string
18
- }
19
-
20
- export interface ShoppingCart1Shipping {
21
- freeThreshold?: number
22
- cost?: number
23
- }
24
-
25
- export interface ShoppingCart1Props {
26
- items: ShoppingCart1CartItem[]
27
- shipping?: ShoppingCart1Shipping
28
- title?: string
29
- itemCountLabel?: (count: number) => string
30
- emptyTitle?: string
31
- emptyDescription?: string
32
- continueShoppingLabel?: string
33
- removeItemLabel?: (name: string) => string
34
- decreaseQuantityLabel?: (name: string) => string
35
- increaseQuantityLabel?: (name: string) => string
36
- estimatedDeliveryLabel?: string
37
- orderSummaryLabel?: string
38
- subtotalLabel?: string
39
- shippingLabel?: string
40
- freeShippingLabel?: string
41
- youSaveLabel?: string
42
- totalLabel?: string
43
- taxNoteLabel?: string
44
- checkoutLabel?: string
45
- securePaymentLabel?: string
46
- secureCheckoutLabel?: string
47
- secureCheckoutDescription?: string
48
- onUpdateQuantity?: (id: string, quantity: number) => void
49
- onRemoveItem?: (id: string) => void
50
- onCheckout?: () => void
51
- onContinueShopping?: () => void
52
- }
53
-
54
- export function ShoppingCart1({
55
- items,
56
- shipping,
57
- title = 'Tu carrito de compras',
58
- itemCountLabel = (count) => `${count} ${count === 1 ? 'artículo' : 'artículos'} en tu carrito`,
59
- emptyTitle = 'Tu carrito está vacío',
60
- emptyDescription = 'Agrega productos para empezar',
61
- continueShoppingLabel = 'Seguir comprando',
62
- removeItemLabel = (name) => `Quitar ${name}`,
63
- decreaseQuantityLabel = (name) => `Disminuir cantidad de ${name}`,
64
- increaseQuantityLabel = (name) => `Aumentar cantidad de ${name}`,
65
- estimatedDeliveryLabel = 'Entrega estimada',
66
- orderSummaryLabel = 'Resumen del pedido',
67
- subtotalLabel = 'Subtotal',
68
- shippingLabel = 'Envío',
69
- freeShippingLabel = 'Gratis',
70
- youSaveLabel = 'Ahorras',
71
- totalLabel = 'Total',
72
- taxNoteLabel = 'incluye IVA si corresponde',
73
- checkoutLabel = 'Finalizar compra',
74
- securePaymentLabel = 'Pago seguro con cifrado SSL',
75
- secureCheckoutLabel = 'Pago seguro',
76
- secureCheckoutDescription = 'Tu información de pago está cifrada y protegida.',
77
- onUpdateQuantity,
78
- onRemoveItem,
79
- onCheckout,
80
- onContinueShopping,
81
- }: ShoppingCart1Props) {
82
- const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
83
- const savings = items.reduce((sum, item) => sum + (item.originalPrice - item.price) * item.quantity, 0)
84
- const freeThreshold = shipping?.freeThreshold ?? Infinity
85
- const shippingCost = shipping?.cost ?? 0
86
- const shippingTotal = subtotal >= freeThreshold ? 0 : shippingCost
87
- const total = subtotal + shippingTotal
88
-
89
- const changeQuantity = (id: string, quantity: number, delta: number) => {
90
- onUpdateQuantity?.(id, Math.max(1, quantity + delta))
91
- }
92
-
93
- return (
94
- <div className='mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8'>
95
- <div className='mb-8 flex flex-col gap-2 text-center'>
96
- <h1 className='text-3xl font-bold tracking-tight sm:text-4xl'>{title}</h1>
97
- <p className='text-muted-foreground'>
98
- {itemCountLabel(items.length)} •{' '}
99
- <span className='text-foreground font-semibold'>${subtotal.toFixed(2)}</span>
100
- </p>
101
- </div>
102
-
103
- <div className='flex flex-col gap-8 lg:flex-row'>
104
- <div className='flex flex-1 flex-col gap-6'>
105
- {items.length === 0 ? (
106
- <Card className='border-dashed'>
107
- <CardContent className='flex flex-col items-center justify-center py-12 text-center'>
108
- <ShoppingBag className='text-muted-foreground/50 mb-4 size-12' />
109
- <h3 className='text-lg font-medium'>{emptyTitle}</h3>
110
- <p className='text-muted-foreground mt-1 text-sm'>{emptyDescription}</p>
111
- <Button
112
- className='mt-4 h-9 cursor-pointer px-4 py-2'
113
- variant='outline'
114
- onClick={onContinueShopping}
115
- >
116
- {continueShoppingLabel}
117
- </Button>
118
- </CardContent>
119
- </Card>
120
- ) : (
121
- items.map(item => (
122
- <Card key={item.id} className='gap-0 overflow-hidden py-0'>
123
- <div className='flex flex-col sm:flex-row'>
124
- <div className='relative h-auto w-full sm:w-40'>
125
- <img
126
- src={item.image || undefined}
127
- alt={item.name}
128
- className='h-36 w-full object-cover object-center'
129
- />
130
- </div>
131
-
132
- <div className='flex-1 p-4'>
133
- <div className='flex items-start justify-between'>
134
- <div>
135
- <h3 className='text-foreground text-lg font-medium'>{item.name}</h3>
136
- <p className='text-muted-foreground mt-1 text-sm'>
137
- {item.color} {item.size ? `• ${item.size}` : ''}
138
- </p>
139
- </div>
140
- <Button
141
- variant='ghost'
142
- size='icon'
143
- className='size-8 cursor-pointer text-muted-foreground hover:bg-destructive/10 hover:text-destructive'
144
- onClick={onRemoveItem ? () => onRemoveItem(item.id) : undefined}
145
- aria-label={removeItemLabel(item.name)}
146
- >
147
- <Trash2 />
148
- </Button>
149
- </div>
150
-
151
- <div className='mt-4 flex items-center justify-between'>
152
- <div className='flex items-center gap-2'>
153
- <Button
154
- variant='outline'
155
- size='icon'
156
- className='size-8 cursor-pointer'
157
- onClick={
158
- onUpdateQuantity
159
- ? () => changeQuantity(item.id, item.quantity, -1)
160
- : undefined
161
- }
162
- disabled={item.quantity <= 1}
163
- aria-label={decreaseQuantityLabel(item.name)}
164
- >
165
- <Minus />
166
- </Button>
167
- <span className='w-8 text-center text-sm font-medium'>{item.quantity}</span>
168
- <Button
169
- variant='outline'
170
- size='icon'
171
- className='size-8 cursor-pointer'
172
- onClick={
173
- onUpdateQuantity
174
- ? () => changeQuantity(item.id, item.quantity, 1)
175
- : undefined
176
- }
177
- aria-label={increaseQuantityLabel(item.name)}
178
- >
179
- <Plus />
180
- </Button>
181
- </div>
182
-
183
- <div className='text-end'>
184
- <p className='text-lg font-semibold'>${(item.price * item.quantity).toFixed(2)}</p>
185
- {item.originalPrice > item.price && (
186
- <p className='text-muted-foreground text-xs line-through'>
187
- ${item.originalPrice.toFixed(2)}
188
- </p>
189
- )}
190
- </div>
191
- </div>
192
- </div>
193
- </div>
194
-
195
- <CardFooter className='bg-muted/20 border-t px-4 !py-2'>
196
- <div className='text-muted-foreground flex items-center text-sm'>
197
- <Package className='me-2 size-4' />
198
- <span>{estimatedDeliveryLabel}: {item.estimatedDelivery}</span>
199
- </div>
200
- </CardFooter>
201
- </Card>
202
- ))
203
- )}
204
- </div>
205
-
206
- <div className='flex w-full flex-col gap-4 lg:w-96'>
207
- <Card className='sticky top-4 gap-0'>
208
- <CardHeader className='pb-4'>
209
- <CardTitle className='text-xl'>{orderSummaryLabel}</CardTitle>
210
- </CardHeader>
211
- <CardContent className='flex flex-col gap-4'>
212
- <div className='flex flex-col gap-3'>
213
- <div className='flex justify-between text-sm'>
214
- <span className='text-muted-foreground'>{subtotalLabel}</span>
215
- <span>${subtotal.toFixed(2)}</span>
216
- </div>
217
- <div className='flex justify-between text-sm'>
218
- <span className='text-muted-foreground'>{shippingLabel}</span>
219
- <span className={cn(shippingTotal === 0 && 'text-success')}>
220
- {shippingTotal === 0 ? freeShippingLabel : `$${shippingTotal.toFixed(2)}`}
221
- </span>
222
- </div>
223
- {savings > 0 && (
224
- <div className='flex justify-between text-sm font-medium'>
225
- <span>{youSaveLabel}</span>
226
- <span>-${savings.toFixed(2)}</span>
227
- </div>
228
- )}
229
- </div>
230
-
231
- <Separator className='my-2' />
232
-
233
- <div className='flex items-center justify-between text-base font-medium'>
234
- <span>{totalLabel}</span>
235
- <div className='text-end'>
236
- <p className='text-xl font-bold'>
237
- <ShoppingCart1SpinningCounter value={total} decimals={2} prefix='$' />
238
- </p>
239
- <p className='text-muted-foreground text-xs'>{taxNoteLabel}</p>
240
- </div>
241
- </div>
242
-
243
- <Button
244
- size='lg'
245
- className='mt-4 h-10 w-full cursor-pointer px-8 text-base font-medium'
246
- disabled={items.length === 0}
247
- onClick={onCheckout}
248
- >
249
- <ShoppingBag data-icon='inline-start' />
250
- {checkoutLabel}
251
- </Button>
252
-
253
- <div className='text-muted-foreground flex items-center justify-center gap-2 text-xs'>
254
- <CreditCard className='size-3.5' />
255
- <span>{securePaymentLabel}</span>
256
- </div>
257
- </CardContent>
258
- </Card>
259
-
260
- <Card className='border-dashed py-4'>
261
- <CardContent className='px-4'>
262
- <div className='flex items-start gap-3'>
263
- <div className='flex size-10 items-center justify-center rounded-full bg-warning/10 text-warning'>
264
- <Shield className='size-5' />
265
- </div>
266
- <div>
267
- <h4 className='font-medium'>{secureCheckoutLabel}</h4>
268
- <p className='text-muted-foreground mt-1 text-xs'>
269
- {secureCheckoutDescription}
270
- </p>
271
- </div>
272
- </div>
273
- </CardContent>
274
- </Card>
275
-
276
- <Button
277
- variant='outline'
278
- className='h-9 w-full cursor-pointer px-4 py-2'
279
- onClick={onContinueShopping}
280
- >
281
- <Store data-icon='inline-start' />
282
- {continueShoppingLabel}
283
- <MoveRight data-icon='inline-end' />
284
- </Button>
285
- </div>
286
- </div>
287
- </div>
288
- )
289
- }
290
-
291
- export default ShoppingCart1
@@ -1,110 +0,0 @@
1
- import { Heart, Minus, Plus, Trash2 } from 'lucide-react'
2
- import { Badge } from '../../../ui/badge'
3
- import { Button } from '../../../ui/button'
4
- import { Separator } from '../../../ui/separator'
5
- import type { ShoppingCart2Item } from './shopping-cart-2'
6
-
7
- export interface ShoppingCart2ItemRowProps {
8
- item: ShoppingCart2Item
9
- isLast: boolean
10
- onRemove?: (id: string) => void
11
- onQuantityChange?: (id: string, quantity: number) => void
12
- onAddToWishlist?: (item: ShoppingCart2Item) => void
13
- }
14
-
15
- export function ShoppingCart2ItemRow({
16
- item,
17
- isLast,
18
- onRemove,
19
- onQuantityChange,
20
- onAddToWishlist,
21
- }: ShoppingCart2ItemRowProps) {
22
- const formatPrice = (value: number) => `$${value.toFixed(2)}`
23
-
24
- return (
25
- <div>
26
- <div className='flex flex-col sm:flex-row sm:items-start gap-4'>
27
- <img src={item.image || undefined} alt={item.name} className='w-20 h-20 rounded-lg object-cover shrink-0 sm:w-20 sm:h-20' />
28
-
29
- <div className='flex-1 flex flex-col gap-3 min-w-0'>
30
- <div className='flex items-start justify-between gap-2'>
31
- <div className='flex-1 min-w-0'>
32
- <h3 className='line-clamp-2 font-medium'>{item.name}</h3>
33
- {item.brand ? <p className='text-muted-foreground text-sm'>{item.brand}</p> : null}
34
- <div className='text-muted-foreground mt-1 flex gap-3 text-sm flex-wrap'>
35
- {item.color ? <span className='whitespace-nowrap'>Color: {item.color}</span> : null}
36
- {item.size ? <span className='whitespace-nowrap'>Talla: {item.size}</span> : null}
37
- </div>
38
- </div>
39
- <Button
40
- variant='ghost'
41
- size='sm'
42
- onClick={onRemove ? () => onRemove(item.id) : undefined}
43
- className='h-8 shrink-0 px-3 text-xs cursor-pointer text-destructive'
44
- aria-label={`Quitar ${item.name}`}
45
- >
46
- <Trash2 />
47
- </Button>
48
- </div>
49
-
50
- <div className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2'>
51
- <div className='flex items-center gap-2 flex-wrap'>
52
- <span className='font-semibold text-base'>{formatPrice(item.price)}</span>
53
- {item.originalPrice ? (
54
- <span className='text-muted-foreground text-sm line-through'>
55
- {formatPrice(item.originalPrice)}
56
- </span>
57
- ) : null}
58
- {item.originalPrice ? (
59
- <Badge variant='destructive' className='rounded-sm px-2 py-0.5 font-semibold text-xs'>
60
- Ahorra {formatPrice(item.originalPrice - item.price)}
61
- </Badge>
62
- ) : null}
63
- </div>
64
-
65
- <div className='flex items-center gap-2 justify-between sm:justify-end'>
66
- <Button
67
- variant='ghost'
68
- size='sm'
69
- className='h-8 cursor-pointer px-2 text-xs'
70
- onClick={onAddToWishlist ? () => onAddToWishlist(item) : undefined}
71
- aria-label={`Añadir ${item.name} a la lista de deseos`}
72
- >
73
- <Heart className='size-4' />
74
- </Button>
75
- <div className='flex items-center rounded-md border'>
76
- <Button
77
- variant='ghost'
78
- size='sm'
79
- className='h-8 cursor-pointer px-2 text-xs'
80
- onClick={
81
- onQuantityChange ? () => onQuantityChange(item.id, item.quantity - 1) : undefined
82
- }
83
- disabled={item.quantity <= 1}
84
- aria-label={`Disminuir cantidad de ${item.name}`}
85
- >
86
- <Minus className='size-4' />
87
- </Button>
88
- <span className='min-w-[2rem] px-2 py-1 text-center text-sm'>{item.quantity}</span>
89
- <Button
90
- variant='ghost'
91
- size='sm'
92
- className='h-8 cursor-pointer px-2 text-xs'
93
- onClick={
94
- onQuantityChange ? () => onQuantityChange(item.id, item.quantity + 1) : undefined
95
- }
96
- aria-label={`Aumentar cantidad de ${item.name}`}
97
- >
98
- <Plus className='size-4' />
99
- </Button>
100
- </div>
101
- </div>
102
- </div>
103
- </div>
104
- </div>
105
- {!isLast ? <Separator className='mt-6' /> : null}
106
- </div>
107
- )
108
- }
109
-
110
- export default ShoppingCart2ItemRow
@@ -1,51 +0,0 @@
1
- import { Card, CardContent } from '../../../ui/card'
2
- import { cn } from '../../../lib/utils'
3
- import type { ShoppingCart2Recommendation } from './shopping-cart-2'
4
-
5
- export interface ShoppingCart2RecommendationsProps {
6
- recommendations: ShoppingCart2Recommendation[]
7
- title?: string
8
- onRecommendationClick?: (product: ShoppingCart2Recommendation) => void
9
- }
10
-
11
- export function ShoppingCart2Recommendations({
12
- recommendations,
13
- title = 'También te puede gustar',
14
- onRecommendationClick,
15
- }: ShoppingCart2RecommendationsProps) {
16
- if (recommendations.length === 0) return null
17
-
18
- const formatPrice = (value: number) => `$${value.toFixed(2)}`
19
-
20
- return (
21
- <div className='mt-12'>
22
- <h3 className='mb-4 text-lg font-semibold'>{title}</h3>
23
- <div className='grid gap-4 sm:grid-cols-2 lg:grid-cols-4'>
24
- {recommendations.map(product => (
25
- <Card
26
- key={product.id}
27
- className={cn(
28
- 'group py-0 transition-shadow',
29
- onRecommendationClick && 'cursor-pointer hover:shadow-md'
30
- )}
31
- onClick={onRecommendationClick ? () => onRecommendationClick(product) : undefined}
32
- >
33
- <CardContent className='p-4'>
34
- <div className='mb-3 aspect-square overflow-hidden'>
35
- <img
36
- src={product.image || undefined}
37
- alt={product.name}
38
- className='size-full rounded-md object-cover'
39
- />
40
- </div>
41
- <h4 className='mb-1 line-clamp-2 text-sm font-medium'>{product.name}</h4>
42
- <p className='text-sm font-semibold'>{formatPrice(product.price)}</p>
43
- </CardContent>
44
- </Card>
45
- ))}
46
- </div>
47
- </div>
48
- )
49
- }
50
-
51
- export default ShoppingCart2Recommendations