@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,146 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect, useRef } from 'react'
4
-
5
- import { cn } from '../../../lib/utils'
6
-
7
- /**
8
- * Spinning counter: each digit is a clipped vertical reel of 0-9 cells that
9
- * spins forward and lands on the target digit. Adapted from Transitions.dev
10
- * "Spinning counter" (26-spinning-counter.md), re-tuned for cart totals:
11
- * one loop instead of several, and a snappier duration so rapid +/- clicks
12
- * don't queue up sluggish animations. The spec's directional SVG blur is
13
- * approximated here with a CSS `filter: blur()` transition for simplicity.
14
- *
15
- * Non-digit characters (currency symbol, decimal point, thousands separator)
16
- * render as static text so only the digits spin.
17
- */
18
- const CELL_SIZE = 20 // px
19
- const DURATION = 450 // ms
20
- const STAGGER = 60 // ms per column
21
- const SPINS = 1 // extra full loops before landing
22
- const SPIN_BLUR = 2 // px
23
-
24
- const REEL_DIGITS = Array.from({ length: (SPINS + 1) * 10 }, (_, i) => i % 10)
25
-
26
- type ShoppingCart2SpinningCounterProps = {
27
- value: number
28
- /** Fixed decimal places to format the value with, e.g. 2 for currency. */
29
- decimals?: number
30
- /** Static text rendered before the formatted value, e.g. '$'. */
31
- prefix?: string
32
- className?: string
33
- digitClassName?: string
34
- }
35
-
36
- export function ShoppingCart2SpinningCounter({ value, decimals = 0, prefix, className, digitClassName }: ShoppingCart2SpinningCounterProps) {
37
- const safeValue = Math.max(0, value)
38
- const formatted = safeValue.toFixed(decimals)
39
- const chars = formatted.split('')
40
- const containerRef = useRef<HTMLSpanElement>(null)
41
- const isFirstRender = useRef(true)
42
-
43
- useEffect(() => {
44
- const container = containerRef.current
45
- if (!container) return
46
-
47
- const strips = Array.from(container.querySelectorAll<HTMLElement>('[data-reel-strip]'))
48
- const timeouts: number[] = []
49
-
50
- const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
51
-
52
- strips.forEach((strip, index) => {
53
- const digit = Number(strip.dataset.digit)
54
- const delay = (strips.length - 1 - index) * STAGGER
55
-
56
- if (isFirstRender.current || !strip.dataset.initialized || prefersReducedMotion) {
57
- strip.style.transition = 'none'
58
- strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
59
- strip.style.filter = 'blur(0px)'
60
- strip.dataset.initialized = 'true'
61
- void strip.offsetHeight
62
- return
63
- }
64
-
65
- const landingIndex = SPINS * 10 + digit
66
- strip.style.transition = `transform ${DURATION}ms cubic-bezier(0.16, 1, 0.3, 1) ${delay}ms, filter ${DURATION}ms ease-out ${delay}ms`
67
- strip.style.transform = `translateY(-${landingIndex * CELL_SIZE}px)`
68
- strip.style.filter = `blur(${SPIN_BLUR}px)`
69
-
70
- const settleTimeout = window.setTimeout(() => {
71
- strip.style.filter = 'blur(0px)'
72
- }, delay + 30)
73
-
74
- const resetTimeout = window.setTimeout(() => {
75
- strip.style.transition = 'none'
76
- strip.style.transform = `translateY(-${digit * CELL_SIZE}px)`
77
- void strip.offsetHeight
78
- }, delay + DURATION + 30)
79
-
80
- timeouts.push(settleTimeout, resetTimeout)
81
- })
82
-
83
- isFirstRender.current = false
84
-
85
- return () => {
86
- timeouts.forEach(window.clearTimeout)
87
- }
88
- }, [formatted])
89
-
90
- return (
91
- <span className='relative inline-flex items-center'>
92
- <span className='sr-only'>{prefix}{formatted}</span>
93
- <span
94
- ref={containerRef}
95
- className={cn('inline-flex items-center font-[inherit] tabular-nums', className)}
96
- style={{ height: CELL_SIZE }}
97
- aria-hidden='true'
98
- >
99
- {prefix ? (
100
- <span className={digitClassName} style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}>
101
- {prefix}
102
- </span>
103
- ) : null}
104
- {chars.map((char, index) => {
105
- if (!/\d/.test(char)) {
106
- return (
107
- <span
108
- key={index}
109
- className={digitClassName}
110
- style={{ display: 'inline-flex', alignItems: 'center', height: CELL_SIZE }}
111
- >
112
- {char}
113
- </span>
114
- )
115
- }
116
-
117
- return (
118
- <span
119
- key={index}
120
- className='relative inline-block overflow-hidden'
121
- style={{
122
- height: CELL_SIZE,
123
- maskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
124
- WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, #000 22%, #000 78%, transparent 100%)',
125
- }}
126
- >
127
- <span data-reel-strip data-digit={char} className='flex flex-col will-change-[transform,filter]'>
128
- {REEL_DIGITS.map((cellDigit, cellIndex) => (
129
- <span
130
- key={cellIndex}
131
- className={cn('flex items-center justify-center', digitClassName)}
132
- style={{ height: CELL_SIZE }}
133
- >
134
- {cellDigit}
135
- </span>
136
- ))}
137
- </span>
138
- </span>
139
- )
140
- })}
141
- </span>
142
- </span>
143
- )
144
- }
145
-
146
- export default ShoppingCart2SpinningCounter
@@ -1,295 +0,0 @@
1
- 'use client'
2
-
3
- import { useState } from 'react'
4
- import { ArrowLeft, Shield, ShoppingBag, Tag, Truck } from 'lucide-react'
5
- import { Button } from '../../../ui/button'
6
- import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
7
- import { Input } from '../../../ui/input'
8
- import { Separator } from '../../../ui/separator'
9
- import { ShoppingCart2ItemRow } from './shopping-cart-2-item-row'
10
- import { ShoppingCart2Recommendations } from './shopping-cart-2-recommendations'
11
- import { ShoppingCart2SpinningCounter } from './shopping-cart-2-spinning-counter'
12
-
13
- export interface ShoppingCart2Item {
14
- id: string
15
- name: string
16
- brand?: string
17
- price: number
18
- originalPrice?: number
19
- quantity: number
20
- image: string
21
- color?: string
22
- size?: string
23
- }
24
-
25
- export interface ShoppingCart2Recommendation {
26
- id: string
27
- name: string
28
- price: number
29
- image: string
30
- }
31
-
32
- export interface ShoppingCart2Props {
33
- items: ShoppingCart2Item[]
34
- recommendations?: ShoppingCart2Recommendation[]
35
- taxRate?: number
36
- shippingCost?: number
37
- freeShippingThreshold?: number
38
- validatePromo?: (code: string) => number | null
39
- titleLabel?: string
40
- itemsCountLabel?: string
41
- cartItemsLabel?: string
42
- orderSummaryLabel?: string
43
- subtotalLabel?: string
44
- savingsLabel?: string
45
- shippingLabel?: string
46
- freeShippingLabel?: string
47
- taxLabel?: string
48
- totalLabel?: string
49
- promoPlaceholder?: string
50
- applyPromoLabel?: string
51
- promoAppliedLabel?: string
52
- continueShoppingLabel?: string
53
- emptyCartTitleLabel?: string
54
- emptyCartDescriptionLabel?: string
55
- checkoutLabel?: string
56
- freeShippingNoteLabel?: string
57
- freeShippingThresholdNoteLabel?: string
58
- secureCheckoutLabel?: string
59
- recommendationsLabel?: string
60
- onQuantityChange?: (id: string, quantity: number) => void
61
- onRemove?: (id: string) => void
62
- onAddToWishlist?: (item: ShoppingCart2Item) => void
63
- onCheckout?: () => void
64
- onContinueShopping?: () => void
65
- onRecommendationClick?: (product: ShoppingCart2Recommendation) => void
66
- }
67
-
68
- export function ShoppingCart2({
69
- items,
70
- recommendations,
71
- taxRate = 0.08,
72
- shippingCost = 9.99,
73
- freeShippingThreshold = 75,
74
- validatePromo,
75
- titleLabel = 'Carrito de compras',
76
- itemsCountLabel = 'artículos en tu carrito',
77
- cartItemsLabel = 'Artículos',
78
- orderSummaryLabel = 'Resumen del pedido',
79
- subtotalLabel = 'Subtotal',
80
- savingsLabel = 'Ahorro',
81
- shippingLabel = 'Envío',
82
- freeShippingLabel = 'Gratis',
83
- taxLabel = 'Impuestos',
84
- totalLabel = 'Total',
85
- promoPlaceholder = 'Código de descuento',
86
- applyPromoLabel = 'Aplicar código',
87
- promoAppliedLabel = 'Código "{code}" aplicado',
88
- continueShoppingLabel = 'Seguir comprando',
89
- emptyCartTitleLabel = 'Tu carrito está vacío',
90
- emptyCartDescriptionLabel = 'Añade algunos artículos para empezar',
91
- checkoutLabel = 'Finalizar compra',
92
- freeShippingNoteLabel = 'Envío gratis en este pedido',
93
- freeShippingThresholdNoteLabel = 'Envío gratis en pedidos superiores a ${threshold}',
94
- secureCheckoutLabel = 'Pago seguro garantizado',
95
- recommendationsLabel = 'También te puede gustar',
96
- onQuantityChange,
97
- onRemove,
98
- onAddToWishlist,
99
- onCheckout,
100
- onContinueShopping,
101
- onRecommendationClick,
102
- }: ShoppingCart2Props) {
103
- const [promoCode, setPromoCode] = useState('')
104
- const [appliedPromo, setAppliedPromo] = useState<string | null>(null)
105
-
106
- const applyPromoCode = () => {
107
- if (!validatePromo) return
108
- const code = promoCode.trim()
109
- const discount = validatePromo(code)
110
- if (discount !== null && discount > 0) {
111
- setAppliedPromo(code)
112
- setPromoCode('')
113
- }
114
- }
115
-
116
- const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
117
- const itemCount = items.reduce((sum, item) => sum + item.quantity, 0)
118
- const savings = items.reduce((sum, item) => {
119
- const originalPrice = item.originalPrice ?? item.price
120
- return sum + (originalPrice - item.price) * item.quantity
121
- }, 0)
122
- const promoDiscount = appliedPromo && validatePromo ? subtotal * ((validatePromo(appliedPromo) ?? 0) / 100) : 0
123
- const shipping = subtotal > freeShippingThreshold ? 0 : shippingCost
124
- const tax = (subtotal - promoDiscount) * taxRate
125
- const total = subtotal - promoDiscount + shipping + tax
126
-
127
- const formatPrice = (value: number) => `$${value.toFixed(2)}`
128
-
129
- return (
130
- <section className='py-8'>
131
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
132
- {items.length === 0 ? (
133
- <Card>
134
- <CardContent className='py-12 text-center'>
135
- <ShoppingBag className='text-muted-foreground mx-auto mb-4 size-16' />
136
- <h2 className='mb-2 text-2xl font-semibold'>{emptyCartTitleLabel}</h2>
137
- <p className='text-muted-foreground mb-6'>{emptyCartDescriptionLabel}</p>
138
- <Button size='lg' className='h-10 px-8 cursor-pointer' onClick={onContinueShopping}>
139
- {continueShoppingLabel}
140
- </Button>
141
- </CardContent>
142
- </Card>
143
- ) : (
144
- <>
145
- <div className='mb-8'>
146
- <Button
147
- variant='ghost'
148
- size='sm'
149
- className='mb-4 h-8 px-3 text-xs cursor-pointer'
150
- onClick={onContinueShopping}
151
- >
152
- <ArrowLeft />
153
- {continueShoppingLabel}
154
- </Button>
155
- <h1 className='text-3xl font-bold tracking-tight text-balance'>{titleLabel}</h1>
156
- <p className='text-muted-foreground mt-2'>
157
- {items.length} {itemsCountLabel}
158
- </p>
159
- </div>
160
-
161
- <div className='grid gap-8 lg:grid-cols-3'>
162
- <div className='lg:col-span-2'>
163
- <Card>
164
- <CardHeader>
165
- <CardTitle>{cartItemsLabel}</CardTitle>
166
- </CardHeader>
167
- <CardContent className='flex flex-col gap-6'>
168
- {items.map((item, index) => (
169
- <ShoppingCart2ItemRow
170
- key={item.id}
171
- item={item}
172
- isLast={index === items.length - 1}
173
- onRemove={onRemove}
174
- onQuantityChange={onQuantityChange}
175
- onAddToWishlist={onAddToWishlist}
176
- />
177
- ))}
178
- </CardContent>
179
- </Card>
180
- </div>
181
-
182
- <div className='lg:col-span-1'>
183
- <Card className='lg:sticky lg:top-6'>
184
- <CardHeader>
185
- <CardTitle>{orderSummaryLabel}</CardTitle>
186
- </CardHeader>
187
- <CardContent className='flex flex-col gap-4'>
188
- <div className='flex flex-col gap-3 text-sm'>
189
- <div className='flex justify-between gap-4 flex-wrap'>
190
- <span className='flex-1'>
191
- {subtotalLabel} ({itemCount} {cartItemsLabel})
192
- </span>
193
- <span className='font-medium shrink-0'>{formatPrice(subtotal)}</span>
194
- </div>
195
-
196
- {savings > 0 ? (
197
- <div className='flex justify-between gap-4 text-success flex-wrap'>
198
- <span className='flex-1'>{savingsLabel}</span>
199
- <span className='font-medium shrink-0'>-{formatPrice(savings)}</span>
200
- </div>
201
- ) : null}
202
-
203
- {appliedPromo ? (
204
- <div className='flex justify-between gap-4 text-success flex-wrap'>
205
- <span className='flex-1'>{promoAppliedLabel.replace('{code}', appliedPromo)}</span>
206
- <span className='font-medium shrink-0'>-{formatPrice(promoDiscount)}</span>
207
- </div>
208
- ) : null}
209
-
210
- <div className='flex justify-between gap-4 flex-wrap'>
211
- <span className='flex-1'>{shippingLabel}</span>
212
- <span className='font-medium shrink-0'>
213
- {shipping === 0 ? freeShippingLabel : formatPrice(shipping)}
214
- </span>
215
- </div>
216
-
217
- <div className='flex justify-between gap-4 flex-wrap'>
218
- <span className='flex-1'>{taxLabel}</span>
219
- <span className='font-medium shrink-0'>{formatPrice(tax)}</span>
220
- </div>
221
- </div>
222
-
223
- <Separator />
224
-
225
- <div className='flex justify-between gap-4 text-lg font-semibold flex-wrap'>
226
- <span>{totalLabel}</span>
227
- <span className='shrink-0'>
228
- <ShoppingCart2SpinningCounter value={total} decimals={2} prefix='$' />
229
- </span>
230
- </div>
231
-
232
- <div className='flex flex-col gap-3'>
233
- <Input
234
- placeholder={promoPlaceholder}
235
- value={promoCode}
236
- onChange={e => setPromoCode(e.target.value)}
237
- aria-label={promoPlaceholder}
238
- className='h-9 flex-1'
239
- />
240
- <Button
241
- variant='outline'
242
- onClick={applyPromoCode}
243
- className='h-9 cursor-pointer px-4 py-2'
244
- disabled={!promoCode || !validatePromo}
245
- aria-label={applyPromoLabel}
246
- >
247
- <Tag />
248
- </Button>
249
- </div>
250
-
251
- {appliedPromo ? (
252
- <div className='flex items-center gap-2 text-sm text-success'>
253
- <Tag className='size-4' />
254
- {promoAppliedLabel.replace('{code}', appliedPromo)}
255
- </div>
256
- ) : null}
257
-
258
- <Button size='lg' className='h-10 w-full px-8 cursor-pointer' onClick={onCheckout}>
259
- {checkoutLabel}
260
- </Button>
261
-
262
- <div className='flex flex-col gap-3 text-sm'>
263
- <div className='flex items-center gap-2 text-success'>
264
- <Truck className='size-4' />
265
- <span>
266
- {shipping === 0
267
- ? freeShippingNoteLabel
268
- : freeShippingThresholdNoteLabel.replace('${threshold}', String(freeShippingThreshold))}
269
- </span>
270
- </div>
271
- <div className='text-muted-foreground flex items-center gap-2'>
272
- <Shield className='size-4' />
273
- <span>{secureCheckoutLabel}</span>
274
- </div>
275
- </div>
276
- </CardContent>
277
- </Card>
278
- </div>
279
- </div>
280
-
281
- {recommendations && recommendations.length > 0 ? (
282
- <ShoppingCart2Recommendations
283
- recommendations={recommendations}
284
- title={recommendationsLabel}
285
- onRecommendationClick={onRecommendationClick}
286
- />
287
- ) : null}
288
- </>
289
- )}
290
- </div>
291
- </section>
292
- )
293
- }
294
-
295
- export default ShoppingCart2
@@ -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 ShoppingCart3SpinningCounterProps = {
16
- value: number
17
- decimals?: number
18
- prefix?: string
19
- className?: string
20
- digitClassName?: string
21
- }
22
-
23
- export function ShoppingCart3SpinningCounter({
24
- value,
25
- decimals = 0,
26
- prefix,
27
- className,
28
- digitClassName,
29
- }: ShoppingCart3SpinningCounterProps) {
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 ShoppingCart3SpinningCounter