@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,342 +0,0 @@
1
- 'use client'
2
-
3
- import { ArrowRight, Clock, Heart, Minus, Plus, ShoppingBag, Trash2 } from 'lucide-react'
4
- import type { LucideIcon } from 'lucide-react'
5
- import { Button } from '../../../ui/button'
6
- import { CheckboxCheck } from '../checkout-forms/checkbox-check'
7
- import { Separator } from '../../../ui/separator'
8
- import { Badge } from '../../../ui/badge'
9
- import { Progress } from '../../../ui/progress'
10
- import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
11
- import { ShoppingCart3SpinningCounter } from './shopping-cart-3-spinning-counter'
12
- import { cn } from '../../../lib/utils'
13
-
14
- export type ShoppingCart3Tag = 'Limited Edition' | 'Best Seller' | 'New Arrival'
15
-
16
- export interface ShoppingCart3CartItem {
17
- id: string
18
- name: string
19
- color: string
20
- size: string
21
- price: number
22
- originalPrice: number
23
- quantity: number
24
- image: string
25
- selected: boolean
26
- tag: ShoppingCart3Tag
27
- estimatedDelivery: string
28
- }
29
-
30
- export interface ShoppingCart3Feature {
31
- icon: LucideIcon
32
- title: string
33
- description: string
34
- }
35
-
36
- export interface ShoppingCart3Props {
37
- items: ShoppingCart3CartItem[]
38
- freeShippingThreshold?: number
39
- features?: ShoppingCart3Feature[]
40
- title?: string
41
- itemCountLabel?: (count: number) => string
42
- selectAllLabel?: string
43
- saveForLaterLabel?: string
44
- removeItemLabel?: (name: string) => string
45
- decreaseQuantityLabel?: (name: string) => string
46
- increaseQuantityLabel?: (name: string) => string
47
- estimatedDeliveryLabel?: string
48
- orderSummaryLabel?: string
49
- subtotalLabel?: string
50
- shippingLabel?: string
51
- freeShippingLabel?: string
52
- youSaveLabel?: string
53
- totalLabel?: string
54
- taxNoteLabel?: string
55
- checkoutLabel?: string
56
- securePaymentLabel?: string
57
- freeShippingProgressLabel?: (remaining: number) => string
58
- freeShippingReachedLabel?: string
59
- tagLabels?: Partial<Record<ShoppingCart3Tag, string>>
60
- onUpdateQuantity?: (id: string, quantity: number) => void
61
- onRemoveItem?: (id: string) => void
62
- onCheckout?: () => void
63
- onSaveForLater?: (ids: string[]) => void
64
- onToggleSelect?: (id: string, selected: boolean) => void
65
- }
66
-
67
- const DEFAULT_TAG_LABELS: Record<ShoppingCart3Tag, string> = {
68
- 'Limited Edition': 'Edición limitada',
69
- 'Best Seller': 'Más vendido',
70
- 'New Arrival': 'Nuevo ingreso',
71
- }
72
-
73
- export function ShoppingCart3({
74
- items,
75
- freeShippingThreshold,
76
- features,
77
- title = 'Tu carrito de compras',
78
- itemCountLabel = (count) => `${count} ${count === 1 ? 'artículo' : 'artículos'} en tu carrito`,
79
- selectAllLabel = 'Seleccionar todo',
80
- saveForLaterLabel = 'Guardar para después',
81
- removeItemLabel = (name) => `Quitar ${name}`,
82
- decreaseQuantityLabel = (name) => `Disminuir cantidad de ${name}`,
83
- increaseQuantityLabel = (name) => `Aumentar cantidad de ${name}`,
84
- estimatedDeliveryLabel = 'Entrega estimada',
85
- orderSummaryLabel = 'Resumen del pedido',
86
- subtotalLabel = 'Subtotal',
87
- shippingLabel = 'Envío',
88
- freeShippingLabel = 'Gratis',
89
- youSaveLabel = 'Ahorras',
90
- totalLabel = 'Total',
91
- taxNoteLabel = 'incluye IVA si corresponde',
92
- checkoutLabel = 'Finalizar compra',
93
- securePaymentLabel = 'Pago seguro con cifrado SSL',
94
- freeShippingProgressLabel = (remaining) =>
95
- `Agrega $${remaining.toFixed(2)} más para obtener envío gratis`,
96
- freeShippingReachedLabel = '¡Ahorra con envío gratis!',
97
- tagLabels,
98
- onUpdateQuantity,
99
- onRemoveItem,
100
- onCheckout,
101
- onSaveForLater,
102
- onToggleSelect,
103
- }: ShoppingCart3Props) {
104
- const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
105
- const savings = items.reduce((sum, item) => sum + (item.originalPrice - item.price) * item.quantity, 0)
106
- const shippingTotal: number = 0
107
- const total = subtotal + shippingTotal
108
-
109
- const allSelected = items.length > 0 && items.every((item) => item.selected)
110
- const selectedIds = items.filter((item) => item.selected).map((item) => item.id)
111
- const resolveTag = (tag: ShoppingCart3Tag) => (tagLabels ? (tagLabels[tag] ?? tag) : tag)
112
- const freeShippingProgress =
113
- freeShippingThreshold != null ? Math.min(100, (subtotal / freeShippingThreshold) * 100) : 0
114
-
115
- return (
116
- <section className='bg-background py-8'>
117
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
118
- <header className='mb-6 text-center'>
119
- <h1 className='text-2xl font-semibold text-balance md:text-3xl'>{title}</h1>
120
- <p className='text-muted-foreground mt-1 text-sm'>{itemCountLabel(items.length)}</p>
121
- </header>
122
-
123
- <div className='flex flex-col gap-6 lg:flex-row lg:gap-8'>
124
- {/* Cart Items Section */}
125
- <section className='flex-1 flex flex-col gap-4'>
126
- <div className='flex min-h-8 items-center justify-between px-4'>
127
- <div className='flex items-center gap-2'>
128
- <CheckboxCheck
129
- checked={allSelected}
130
- onCheckedChange={() => items.forEach((item) => onToggleSelect?.(item.id, !allSelected))}
131
- className='size-4'
132
- disabled={!onToggleSelect}
133
- id='select-all'
134
- />
135
- <label htmlFor='select-all' className='text-sm font-medium'>
136
- {selectAllLabel}
137
- </label>
138
- </div>
139
- <Button
140
- variant='ghost'
141
- size='sm'
142
- className='h-8 cursor-pointer text-sm'
143
- disabled={selectedIds.length === 0}
144
- onClick={onSaveForLater ? () => onSaveForLater(selectedIds) : undefined}
145
- >
146
- <Heart data-icon='inline-start' />
147
- {saveForLaterLabel}
148
- </Button>
149
- </div>
150
-
151
- <div className='flex flex-col gap-4'>
152
- {items.map((item) => (
153
- <div
154
- key={item.id}
155
- className='rounded-lg transition-all'
156
- onClick={onToggleSelect ? () => onToggleSelect(item.id, !item.selected) : undefined}
157
- >
158
- <Card className={cn('overflow-hidden transition-all', item.selected ? 'shadow-md' : 'shadow-none')}>
159
- <CardContent className='flex items-start gap-4' onClick={(e) => e.stopPropagation()}>
160
- <div onClick={(e) => e.stopPropagation()}>
161
- <CheckboxCheck
162
- checked={item.selected}
163
- onCheckedChange={() => onToggleSelect?.(item.id, !item.selected)}
164
- className='mt-1 size-4'
165
- disabled={!onToggleSelect}
166
- />
167
- </div>
168
-
169
- <div className='flex flex-1 gap-4'>
170
- <div className='relative aspect-square size-20 overflow-hidden rounded-md border sm:size-24'>
171
- <img src={item.image || undefined} alt={item.name} className='size-full object-cover' />
172
- </div>
173
-
174
- <div className='flex flex-1 flex-col'>
175
- <div className='flex items-start justify-between'>
176
- <div>
177
- <CardTitle className='text-base text-balance'>{item.name}</CardTitle>
178
- <div className='text-muted-foreground mt-1 flex gap-2 text-xs'>
179
- <span>{item.color}</span>
180
- {item.size ? (
181
- <>
182
- <span>•</span>
183
- <span>{item.size}</span>
184
- </>
185
- ) : null}
186
- </div>
187
- </div>
188
- <Badge variant='secondary' className='h-5.5 rounded-sm px-2.5 text-xs font-semibold'>
189
- {resolveTag(item.tag)}
190
- </Badge>
191
- </div>
192
-
193
- <div className='mt-2 flex items-center gap-2'>
194
- <span className='text-base font-medium'>${item.price.toFixed(2)}</span>
195
- {item.originalPrice > item.price ? (
196
- <span className='text-muted-foreground text-sm line-through'>
197
- ${item.originalPrice.toFixed(2)}
198
- </span>
199
- ) : null}
200
- </div>
201
- <div className='mt-2 flex flex-wrap items-end justify-between gap-2'>
202
- <p className='text-muted-foreground text-xs'>
203
- <Clock className='me-1 inline-block size-3' />
204
- {estimatedDeliveryLabel}: {item.estimatedDelivery}
205
- </p>
206
-
207
- <div className='ms-auto flex items-center justify-end gap-4 max-md:ms-0'>
208
- <div className='flex items-center gap-2'>
209
- <Button
210
- variant='outline'
211
- size='icon'
212
- className='size-8 cursor-pointer rounded'
213
- onClick={
214
- onUpdateQuantity
215
- ? () => onUpdateQuantity(item.id, Math.max(1, item.quantity - 1))
216
- : undefined
217
- }
218
- disabled={item.quantity <= 1}
219
- aria-label={decreaseQuantityLabel(item.name)}
220
- >
221
- <Minus />
222
- </Button>
223
- <span className='w-6 text-center text-sm'>{item.quantity}</span>
224
- <Button
225
- variant='outline'
226
- size='icon'
227
- className='size-8 cursor-pointer rounded'
228
- onClick={
229
- onUpdateQuantity
230
- ? () => onUpdateQuantity(item.id, item.quantity + 1)
231
- : undefined
232
- }
233
- aria-label={increaseQuantityLabel(item.name)}
234
- >
235
- <Plus />
236
- </Button>
237
- </div>
238
- <Button
239
- variant='ghost'
240
- size='icon'
241
- className='size-8 cursor-pointer rounded-sm text-muted-foreground hover:bg-destructive/10 hover:text-destructive'
242
- onClick={onRemoveItem ? () => onRemoveItem(item.id) : undefined}
243
- aria-label={removeItemLabel(item.name)}
244
- >
245
- <Trash2 />
246
- </Button>
247
- </div>
248
- </div>
249
- </div>
250
- </div>
251
- </CardContent>
252
- </Card>
253
- </div>
254
- ))}
255
- </div>
256
- </section>
257
-
258
- {/* Summary Section */}
259
- <aside className='w-full lg:w-[380px]'>
260
- <div className='sticky top-8 flex flex-col gap-4'>
261
- {freeShippingThreshold != null ? (
262
- <Card className='py-4'>
263
- <CardContent className='px-4'>
264
- {subtotal < freeShippingThreshold ? (
265
- <p className='text-xs'>{freeShippingProgressLabel(freeShippingThreshold - subtotal)}</p>
266
- ) : (
267
- <p className='text-xs font-medium'>{freeShippingReachedLabel}</p>
268
- )}
269
- <Progress value={subtotal < freeShippingThreshold ? freeShippingProgress : 100} className='mt-2 h-1.5' />
270
- </CardContent>
271
- </Card>
272
- ) : null}
273
-
274
- <Card className='gap-4 py-4'>
275
- <CardHeader className='px-4'>
276
- <CardTitle className='text-lg'>{orderSummaryLabel}</CardTitle>
277
- </CardHeader>
278
- <CardContent className='flex flex-col gap-3 px-4'>
279
- <div className='flex justify-between text-sm'>
280
- <span className='text-muted-foreground'>{subtotalLabel}</span>
281
- <span className='font-medium'>${subtotal.toFixed(2)}</span>
282
- </div>
283
- <div className='flex justify-between text-sm'>
284
- <span className='text-muted-foreground'>{shippingLabel}</span>
285
- <span className='font-medium'>
286
- {shippingTotal === 0 ? freeShippingLabel : `$${shippingTotal.toFixed(2)}`}
287
- </span>
288
- </div>
289
- {savings > 0 && (
290
- <div className='flex justify-between text-sm'>
291
- <span>{youSaveLabel}</span>
292
- <span className='font-medium'>-${savings.toFixed(2)}</span>
293
- </div>
294
- )}
295
- <Separator />
296
- <div className='flex justify-between text-base font-medium'>
297
- <span>{totalLabel}</span>
298
- <span>
299
- <ShoppingCart3SpinningCounter value={total} decimals={2} prefix='$' />
300
- </span>
301
- </div>
302
-
303
- <Button
304
- className='mt-4 h-8 w-full cursor-pointer px-3 text-xs'
305
- size='sm'
306
- onClick={onCheckout}
307
- >
308
- <ShoppingBag data-icon='inline-start' />
309
- {checkoutLabel}
310
- <ArrowRight data-icon='inline-end' />
311
- </Button>
312
-
313
- <p className='text-muted-foreground text-center text-xs'>{securePaymentLabel}</p>
314
- </CardContent>
315
- </Card>
316
-
317
- {features && features.length > 0 ? (
318
- <div className='grid grid-cols-1 gap-3'>
319
- {features.map((feature, index) => (
320
- <Card key={index} className='py-3'>
321
- <CardContent className='flex items-center gap-3 px-3'>
322
- <div className='bg-secondary rounded-full p-1.5'>
323
- <feature.icon className='size-4' />
324
- </div>
325
- <div>
326
- <h3 className='text-sm font-medium'>{feature.title}</h3>
327
- <p className='text-muted-foreground text-xs'>{feature.description}</p>
328
- </div>
329
- </CardContent>
330
- </Card>
331
- ))}
332
- </div>
333
- ) : null}
334
- </div>
335
- </aside>
336
- </div>
337
- </div>
338
- </section>
339
- )
340
- }
341
-
342
- export default ShoppingCart3
@@ -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 ShoppingCart4SpinningCounterProps = {
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 ShoppingCart4SpinningCounter({ value, decimals = 0, prefix, className, digitClassName }: ShoppingCart4SpinningCounterProps) {
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 ShoppingCart4SpinningCounter