@thorprovider/blocks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.turbo/turbo-build.log +24 -0
  2. package/.turbo/turbo-test.log +10 -0
  3. package/.turbo/turbo-type-check.log +0 -0
  4. package/AGENTS.md +1 -0
  5. package/README.md +161 -0
  6. package/components.json +33 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/contracts.d.mts +563 -0
  9. package/dist/contracts.d.ts +563 -0
  10. package/dist/contracts.js +207 -0
  11. package/dist/contracts.js.map +1 -0
  12. package/dist/contracts.mjs +168 -0
  13. package/dist/contracts.mjs.map +1 -0
  14. package/dist/index.d.mts +2408 -0
  15. package/dist/index.d.ts +2408 -0
  16. package/dist/index.js +16422 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/index.mjs +16352 -0
  19. package/dist/index.mjs.map +1 -0
  20. package/docs/standards/data-contracts.md +70 -0
  21. package/docs/standards/theming.md +85 -0
  22. package/package.json +62 -0
  23. package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
  24. package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
  25. package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
  26. package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
  27. package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
  28. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
  29. package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
  30. package/src/blocks/application/verification/verify-1.tsx +307 -0
  31. package/src/blocks/application/verification/verify-confetti.tsx +192 -0
  32. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
  33. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
  34. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
  35. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
  36. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
  37. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
  38. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
  39. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
  40. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
  41. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
  42. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
  43. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
  44. package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
  45. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
  46. package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
  47. package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
  48. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
  49. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
  50. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
  51. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
  52. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
  53. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
  54. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
  55. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
  56. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
  57. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
  58. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
  59. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
  60. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
  61. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
  62. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
  63. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
  67. package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
  68. package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
  69. package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
  70. package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
  71. package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
  72. package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
  73. package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
  74. package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
  75. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
  76. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
  77. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
  78. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
  79. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
  80. package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
  81. package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
  82. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
  83. package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
  84. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
  85. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
  86. package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
  87. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
  88. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
  89. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
  90. package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
  91. package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
  92. package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
  93. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
  94. package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
  95. package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
  96. package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
  97. package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
  98. package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
  99. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
  100. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
  101. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
  102. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
  103. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
  104. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
  105. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
  106. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
  107. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
  108. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
  109. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
  110. package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
  111. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
  112. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
  113. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
  114. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
  115. package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
  116. package/src/blocks/marketing/banners/banner-1.tsx +188 -0
  117. package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
  118. package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
  119. package/src/blocks/marketing/features/feature-1.tsx +97 -0
  120. package/src/blocks/marketing/features/feature-2.tsx +82 -0
  121. package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
  122. package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
  123. package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
  124. package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
  125. package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
  126. package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
  127. package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
  128. package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
  129. package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
  130. package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
  131. package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
  132. package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
  133. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
  134. package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
  135. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
  136. package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
  137. package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
  138. package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
  139. package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
  140. package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
  141. package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
  142. package/src/blocks/shared/reaction-toggle.tsx +145 -0
  143. package/src/hooks/use-mobile.ts +19 -0
  144. package/src/index.ts +200 -0
  145. package/src/lib/contracts.test.ts +32 -0
  146. package/src/lib/contracts.ts +235 -0
  147. package/src/lib/types/menu.ts +43 -0
  148. package/src/lib/types.ts +4 -0
  149. package/src/lib/utils.ts +10 -0
  150. package/src/ui/accordion.tsx +65 -0
  151. package/src/ui/avatar.tsx +108 -0
  152. package/src/ui/badge.tsx +47 -0
  153. package/src/ui/breadcrumb.tsx +108 -0
  154. package/src/ui/button-group.tsx +83 -0
  155. package/src/ui/button.tsx +63 -0
  156. package/src/ui/card.tsx +91 -0
  157. package/src/ui/carousel.tsx +241 -0
  158. package/src/ui/dialog.tsx +158 -0
  159. package/src/ui/drawer.tsx +134 -0
  160. package/src/ui/dropdown-menu.tsx +256 -0
  161. package/src/ui/empty.tsx +103 -0
  162. package/src/ui/field.tsx +248 -0
  163. package/src/ui/input-group.tsx +170 -0
  164. package/src/ui/input-otp.tsx +76 -0
  165. package/src/ui/input.tsx +20 -0
  166. package/src/ui/label.tsx +23 -0
  167. package/src/ui/navigation-menu.tsx +167 -0
  168. package/src/ui/pagination.tsx +127 -0
  169. package/src/ui/progress.tsx +30 -0
  170. package/src/ui/radio-group.tsx +44 -0
  171. package/src/ui/select.tsx +189 -0
  172. package/src/ui/separator.tsx +27 -0
  173. package/src/ui/sheet.tsx +142 -0
  174. package/src/ui/sidebar.tsx +726 -0
  175. package/src/ui/skeleton.tsx +13 -0
  176. package/src/ui/table.tsx +115 -0
  177. package/src/ui/tabs.tsx +90 -0
  178. package/src/ui/textarea.tsx +17 -0
  179. package/src/ui/toggle-group.tsx +83 -0
  180. package/src/ui/toggle.tsx +46 -0
  181. package/src/ui/tooltip.tsx +56 -0
  182. package/tsconfig.json +20 -0
  183. package/tsup.config.ts +16 -0
  184. package/vitest.config.ts +9 -0
@@ -0,0 +1,159 @@
1
+ import { Button } from '../../../ui/button'
2
+ import {
3
+ Card,
4
+ CardContent,
5
+ CardDescription,
6
+ CardFooter,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '../../../ui/card'
10
+ import {
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableFooter,
15
+ TableHead,
16
+ TableHeader,
17
+ TableRow,
18
+ } from '../../../ui/table'
19
+
20
+ export interface OrderHistory1Order {
21
+ name: string
22
+ image: string
23
+ color: string
24
+ size: string
25
+ quantity: number
26
+ price: number
27
+ orderDate: string
28
+ }
29
+
30
+ export interface OrderHistory1Props {
31
+ orders: OrderHistory1Order[]
32
+ title?: string
33
+ description?: string
34
+ totalOrdersLabel?: string
35
+ lastOrderLabel?: string
36
+ itemLabel?: string
37
+ orderDateLabel?: string
38
+ quantityLabel?: string
39
+ priceLabel?: string
40
+ colorLabel?: string
41
+ sizeLabel?: string
42
+ viewDetailsLabel?: string
43
+ downloadInvoiceLabel?: string
44
+ onViewDetails?: () => void
45
+ onDownloadInvoice?: () => void
46
+ }
47
+
48
+ function getLastOrder(orders: OrderHistory1Order[]): string {
49
+ if (orders.length === 0) return ''
50
+ return orders.reduce((latest, order) => {
51
+ const current = new Date(order.orderDate)
52
+ const previous = new Date(latest)
53
+ if (Number.isNaN(current.getTime())) return latest
54
+ if (Number.isNaN(previous.getTime())) return order.orderDate
55
+ return current > previous ? order.orderDate : latest
56
+ }, orders[0].orderDate)
57
+ }
58
+
59
+ export function OrderHistory1({
60
+ orders,
61
+ title,
62
+ description,
63
+ totalOrdersLabel = 'Total de pedidos',
64
+ lastOrderLabel = 'Último pedido',
65
+ itemLabel = 'Artículo',
66
+ orderDateLabel = 'Fecha',
67
+ quantityLabel = 'Cantidad',
68
+ priceLabel = 'Precio',
69
+ colorLabel = 'Color',
70
+ sizeLabel = 'Talla',
71
+ viewDetailsLabel = 'Ver detalles del pedido',
72
+ downloadInvoiceLabel = 'Descargar factura',
73
+ onViewDetails,
74
+ onDownloadInvoice,
75
+ }: OrderHistory1Props) {
76
+ const totalAmount = orders.reduce((sum, order) => sum + order.price, 0)
77
+ const lastOrder = getLastOrder(orders)
78
+
79
+ return (
80
+ <div className="mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
81
+ <Card>
82
+ <CardHeader className="flex flex-col gap-2 md:flex-row md:items-start md:justify-between md:gap-x-6">
83
+ <div>
84
+ <CardTitle className="text-2xl">{title ?? 'Historial de pedidos'}</CardTitle>
85
+ <CardDescription className="text-balance">
86
+ {description ?? 'Ver tus pedidos anteriores y su estado'}
87
+ </CardDescription>
88
+ </div>
89
+ <div className="text-end text-sm text-muted-foreground max-sm:text-start">
90
+ <p>{totalOrdersLabel}: {orders.length}</p>
91
+ <p>{lastOrderLabel}: {lastOrder}</p>
92
+ </div>
93
+ </CardHeader>
94
+ <CardContent>
95
+ <Table>
96
+ <TableHeader>
97
+ <TableRow className="hover:bg-transparent">
98
+ <TableHead className="font-semibold">{itemLabel}</TableHead>
99
+ <TableHead className="hidden text-end font-semibold sm:table-cell">{orderDateLabel}</TableHead>
100
+ <TableHead className="hidden text-end font-semibold md:table-cell">{quantityLabel}</TableHead>
101
+ <TableHead className="text-end font-semibold">{priceLabel}</TableHead>
102
+ </TableRow>
103
+ </TableHeader>
104
+ <TableBody>
105
+ {orders.map((order) => (
106
+ <TableRow key={order.name}>
107
+ <TableCell className="flex flex-col gap-3 py-3 sm:flex-row sm:items-center">
108
+ <img
109
+ src={order.image || undefined}
110
+ alt={order.name}
111
+ className="h-16 w-16 shrink-0 rounded-md object-cover"
112
+ />
113
+ <div className="min-w-0 flex-1">
114
+ <p className="truncate font-medium">{order.name}</p>
115
+ <p className="truncate text-sm text-muted-foreground">
116
+ {colorLabel}: {order.color} • {sizeLabel}: {order.size}
117
+ </p>
118
+ <p className="mt-1 text-xs text-muted-foreground sm:hidden">{order.orderDate}</p>
119
+ </div>
120
+ </TableCell>
121
+ <TableCell className="hidden text-end sm:table-cell">{order.orderDate}</TableCell>
122
+ <TableCell className="hidden text-end md:table-cell">{order.quantity}</TableCell>
123
+ <TableCell className="text-end">${order.price.toFixed(2)}</TableCell>
124
+ </TableRow>
125
+ ))}
126
+ </TableBody>
127
+ <TableFooter className="bg-transparent">
128
+ <TableRow className="font-semibold hover:bg-transparent md:hidden">
129
+ <TableCell colSpan={1} />
130
+ <TableCell className="text-end">${totalAmount.toFixed(2)}</TableCell>
131
+ </TableRow>
132
+ <TableRow className="hidden font-semibold hover:bg-transparent md:table-row">
133
+ <TableCell colSpan={2} />
134
+ <TableCell className="text-end">${totalAmount.toFixed(2)}</TableCell>
135
+ </TableRow>
136
+ </TableFooter>
137
+ </Table>
138
+ </CardContent>
139
+ <CardFooter className="flex flex-wrap gap-4 border-t-0 bg-transparent pt-0">
140
+ <Button
141
+ className="h-9 cursor-pointer px-4 py-2"
142
+ onClick={onViewDetails}
143
+ >
144
+ {viewDetailsLabel}
145
+ </Button>
146
+ <Button
147
+ className="h-9 cursor-pointer px-4 py-2"
148
+ variant="secondary"
149
+ onClick={onDownloadInvoice}
150
+ >
151
+ {downloadInvoiceLabel}
152
+ </Button>
153
+ </CardFooter>
154
+ </Card>
155
+ </div>
156
+ )
157
+ }
158
+
159
+ export default OrderHistory1
@@ -0,0 +1,222 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "../../../ui/card"
11
+ import { Badge } from "../../../ui/badge"
12
+ import { Button } from "../../../ui/button"
13
+ import {
14
+ Carousel,
15
+ CarouselContent,
16
+ CarouselItem,
17
+ CarouselNext,
18
+ CarouselPrevious,
19
+ } from "../../../ui/carousel"
20
+
21
+ export interface OrderItem {
22
+ id: string
23
+ name: string
24
+ image: string
25
+ details: string
26
+ price: string
27
+ quantity: number
28
+ }
29
+
30
+ export interface Order {
31
+ id: string
32
+ date: string
33
+ status: string
34
+ total: string
35
+ items: OrderItem[]
36
+ }
37
+
38
+ export interface OrderHistory2Props {
39
+ orders: Order[]
40
+ titleLabel?: string
41
+ descriptionLabel?: string
42
+ totalOrdersLabel?: string
43
+ lastOrderLabel?: string
44
+ orderLabel?: string
45
+ placedOnLabel?: string
46
+ trackOrderLabel?: string
47
+ viewDetailsLabel?: string
48
+ qtyLabel?: string
49
+ orderTotalLabel?: string
50
+ onTrackOrder?: (order: Order) => void
51
+ onViewDetails?: (order: Order) => void
52
+ }
53
+
54
+ function useWindowSize() {
55
+ const [windowSize, setWindowSize] = useState({
56
+ width: typeof window !== "undefined" ? window.innerWidth : 0,
57
+ })
58
+
59
+ useEffect(() => {
60
+ const handleResize = () => {
61
+ setWindowSize({
62
+ width: window.innerWidth,
63
+ })
64
+ }
65
+
66
+ window.addEventListener("resize", handleResize)
67
+ handleResize()
68
+
69
+ return () => window.removeEventListener("resize", handleResize)
70
+ }, [])
71
+
72
+ return windowSize
73
+ }
74
+
75
+ function getVisibleItemsCount(width: number) {
76
+ if (width >= 1024) return 4
77
+ if (width >= 640) return 2
78
+ return 1
79
+ }
80
+
81
+ function formatLabel(label: string, values: Record<string, string | number>): string {
82
+ return label.replace(/\{(\w+)\}/g, (match, key) => {
83
+ return key in values ? String(values[key]) : match
84
+ })
85
+ }
86
+
87
+ export function OrderHistory2({
88
+ orders,
89
+ titleLabel = "Tus pedidos",
90
+ descriptionLabel = "Consulta y gestiona tus pedidos recientes",
91
+ totalOrdersLabel = "Total de pedidos: {n}",
92
+ lastOrderLabel = "Último pedido realizado el {date}",
93
+ orderLabel = "Pedido {id}",
94
+ placedOnLabel = "Realizado el {date}",
95
+ trackOrderLabel = "Seguir pedido",
96
+ viewDetailsLabel = "Ver detalles",
97
+ qtyLabel = "Cantidad: {n}",
98
+ orderTotalLabel = "Total del pedido",
99
+ onTrackOrder,
100
+ onViewDetails,
101
+ }: OrderHistory2Props) {
102
+ const { width } = useWindowSize()
103
+ const visibleItemsCount = getVisibleItemsCount(width)
104
+ const hasOrders = orders.length > 0
105
+
106
+ const shouldShowArrows = (items: OrderItem[]) => {
107
+ return items.length > visibleItemsCount
108
+ }
109
+
110
+ return (
111
+ <div className="mx-auto w-full max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
112
+ <Card className="overflow-visible border-0 bg-transparent shadow-none ring-0">
113
+ <CardHeader className="flex flex-col gap-4 px-0 sm:flex-row sm:items-center sm:justify-between">
114
+ <div>
115
+ <CardTitle className="text-2xl text-balance">{titleLabel}</CardTitle>
116
+ <CardDescription className="text-balance">
117
+ {descriptionLabel}
118
+ </CardDescription>
119
+ </div>
120
+ <div className="flex flex-col items-start gap-1 sm:items-end">
121
+ <p className="text-sm font-medium">{formatLabel(totalOrdersLabel, { n: orders.length })}</p>
122
+ {hasOrders ? (
123
+ <p className="text-sm text-muted-foreground">
124
+ {formatLabel(lastOrderLabel, { date: orders[0].date })}
125
+ </p>
126
+ ) : null}
127
+ </div>
128
+ </CardHeader>
129
+ <CardContent className="grid gap-6 p-0">
130
+ {orders.map((order) => (
131
+ <Card key={order.id} className="overflow-hidden pt-0">
132
+ <CardHeader className="flex flex-col gap-4 bg-muted/50 p-6 md:flex-row md:items-center md:justify-between">
133
+ <div className="flex flex-col gap-1.5">
134
+ <div className="flex items-center gap-3">
135
+ <CardTitle className="text-lg text-balance">
136
+ {formatLabel(orderLabel, { id: order.id })}
137
+ </CardTitle>
138
+ <Badge
139
+ variant={order.status === "Delivered" ? "default" : "secondary"}
140
+ className="px-2.5 py-0.5 font-semibold"
141
+ >
142
+ {order.status}
143
+ </Badge>
144
+ </div>
145
+ <CardDescription className="text-sm text-balance">
146
+ {formatLabel(placedOnLabel, { date: order.date })}
147
+ </CardDescription>
148
+ </div>
149
+ <div className="flex flex-wrap items-center gap-3">
150
+ {order.status === "Processing" ? (
151
+ <Button
152
+ size="sm"
153
+ className="h-8 cursor-pointer px-3 text-xs"
154
+ onClick={onTrackOrder ? () => onTrackOrder(order) : undefined}
155
+ >
156
+ {trackOrderLabel}
157
+ </Button>
158
+ ) : null}
159
+ <Button
160
+ variant="outline"
161
+ size="sm"
162
+ className="h-8 cursor-pointer px-3 text-xs"
163
+ onClick={onViewDetails ? () => onViewDetails(order) : undefined}
164
+ >
165
+ {viewDetailsLabel}
166
+ </Button>
167
+ </div>
168
+ </CardHeader>
169
+ <CardContent className="px-16">
170
+ <Carousel opts={{ align: "start", loop: false }} className="w-full [&>div]:p-4">
171
+ <CarouselContent className="-ml-5" id={`carousel-${order.id}`}>
172
+ {order.items.map((item) => (
173
+ <CarouselItem
174
+ key={item.id}
175
+ className="basis-full pl-5.5 sm:basis-1/2 lg:basis-1/4"
176
+ >
177
+ <Card className="py-0">
178
+ <CardContent className="p-4">
179
+ <div className="aspect-square w-full overflow-hidden rounded-lg">
180
+ <img
181
+ src={item.image || undefined}
182
+ alt={item.name}
183
+ className="size-full object-contain"
184
+ />
185
+ </div>
186
+ <div className="mt-4 flex flex-col gap-2">
187
+ <h4 className="font-medium">{item.name}</h4>
188
+ <p className="text-sm text-muted-foreground">{item.details}</p>
189
+ <div className="flex items-center justify-between">
190
+ <p className="font-medium">{item.price}</p>
191
+ <p className="text-sm text-muted-foreground">
192
+ {formatLabel(qtyLabel, { n: item.quantity })}
193
+ </p>
194
+ </div>
195
+ </div>
196
+ </CardContent>
197
+ </Card>
198
+ </CarouselItem>
199
+ ))}
200
+ </CarouselContent>
201
+
202
+ {shouldShowArrows(order.items) ? (
203
+ <>
204
+ <CarouselPrevious className="-inset-inline-start-10" />
205
+ <CarouselNext className="-inset-inline-end-10" />
206
+ </>
207
+ ) : null}
208
+ </Carousel>
209
+ <div className="mt-6 flex items-center justify-between border-t pt-4">
210
+ <p className="text-sm text-muted-foreground">{orderTotalLabel}</p>
211
+ <p className="text-lg font-semibold">{order.total}</p>
212
+ </div>
213
+ </CardContent>
214
+ </Card>
215
+ ))}
216
+ </CardContent>
217
+ </Card>
218
+ </div>
219
+ )
220
+ }
221
+
222
+ export default OrderHistory2
@@ -0,0 +1,86 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useEffect, useState } from 'react'
5
+
6
+ import { cn } from '../../../lib/utils'
7
+
8
+ /**
9
+ * @title Avatar group
10
+ * @description Overlapping avatar stack whose neighbours lift with a distance falloff on hover.
11
+ *
12
+ * Adapted from Transitions.dev "Avatar group hover".
13
+ *
14
+ * A dock, not a switch. Lifting only the hovered item makes the row twitch — one element jumps
15
+ * while the rest sit rigid — so the response falls off with distance: the neighbour rises about
16
+ * half as far, the next barely at all. The falloff is what makes the row read as one soft object.
17
+ *
18
+ * The return is deliberately springier than the approach: a strong overshoot curve on the way back
19
+ * reads as elastic rather than mechanical.
20
+ */
21
+ const LIFT = -4
22
+ const FALLOFF = 0.45
23
+ const ACTIVE_SCALE = 1.05
24
+ const DURATION = 320
25
+ const EASE_IN = 'cubic-bezier(0.22, 1, 0.36, 1)'
26
+ const EASE_OUT = 'cubic-bezier(0.34, 3.85, 0.64, 1)'
27
+
28
+ export function AvatarGroup({
29
+ children,
30
+ className,
31
+ overlap = '-ml-2',
32
+ }: {
33
+ children: React.ReactNode
34
+ className?: string
35
+ /** Negative margin applied to every item but the first. Set to `''` for a non-overlapping row. */
36
+ overlap?: string
37
+ }) {
38
+ const items = React.Children.toArray(children)
39
+ const [activeIndex, setActiveIndex] = useState<number | null>(null)
40
+ const [reducedMotion, setReducedMotion] = useState(false)
41
+
42
+ useEffect(() => {
43
+ const query = window.matchMedia('(prefers-reduced-motion: reduce)')
44
+ const sync = () => setReducedMotion(query.matches)
45
+
46
+ sync()
47
+ query.addEventListener('change', sync)
48
+ return () => query.removeEventListener('change', sync)
49
+ }, [])
50
+
51
+ const itemStyle = (index: number): React.CSSProperties | undefined => {
52
+ if (reducedMotion) {
53
+ return undefined
54
+ }
55
+
56
+ const isActive = index === activeIndex
57
+ const shift = activeIndex === null ? 0 : LIFT * FALLOFF ** Math.abs(index - activeIndex)
58
+
59
+ return {
60
+ // translateY before scale, so scaling never amplifies the lift distance.
61
+ transform: `translateY(${shift.toFixed(3)}px) scale(${isActive ? ACTIVE_SCALE : 1})`,
62
+ transition: `transform ${DURATION}ms ${activeIndex === null ? EASE_OUT : EASE_IN}`,
63
+ willChange: 'transform',
64
+ // A lifted item has to sit above the neighbour overlapping it, or the lift happens underneath.
65
+ zIndex: isActive ? 1 : undefined,
66
+ }
67
+ }
68
+
69
+ return (
70
+ <div className={cn('flex', className)} onMouseLeave={() => setActiveIndex(null)}>
71
+ {items.map((child, index) => (
72
+ <div
73
+ // eslint-disable-next-line react/no-array-index-key -- position is the identity here
74
+ key={index}
75
+ className={cn('relative', index > 0 && overlap)}
76
+ style={itemStyle(index)}
77
+ onMouseEnter={() => setActiveIndex(index)}
78
+ >
79
+ {child}
80
+ </div>
81
+ ))}
82
+ </div>
83
+ )
84
+ }
85
+
86
+ export default AvatarGroup
@@ -0,0 +1,126 @@
1
+ import { Button } from '../../../ui/button'
2
+ import { Card, CardContent } from '../../../ui/card'
3
+ import { cn } from '../../../lib/utils'
4
+ import {
5
+ ArrowRight,
6
+ BatteryCharging,
7
+ Cable,
8
+ Camera,
9
+ Lightbulb,
10
+ ShieldCheck,
11
+ Smartphone,
12
+ Sparkles,
13
+ Wrench,
14
+ X,
15
+ } from 'lucide-react'
16
+ import type { CTA } from '../../../lib/types'
17
+
18
+ const categoryIcons = {
19
+ Smartphone,
20
+ BatteryCharging,
21
+ Wrench,
22
+ Camera,
23
+ Cable,
24
+ ShieldCheck,
25
+ Sparkles,
26
+ Lightbulb,
27
+ } as const
28
+
29
+ export type ProductCategory1CategoryIcon =
30
+ | 'Smartphone'
31
+ | 'BatteryCharging'
32
+ | 'Wrench'
33
+ | 'Camera'
34
+ | 'Cable'
35
+ | 'ShieldCheck'
36
+ | 'Sparkles'
37
+ | 'Lightbulb'
38
+
39
+ export interface ProductCategory1Category {
40
+ id: string
41
+ name: string
42
+ subtitle: string
43
+ icon: ProductCategory1CategoryIcon
44
+ }
45
+
46
+ export interface ProductCategory1Props {
47
+ badge?: string
48
+ title: string
49
+ description?: string
50
+ cta?: CTA
51
+ categories: ProductCategory1Category[]
52
+ className?: string
53
+ }
54
+
55
+ export function ProductCategory1({
56
+ badge,
57
+ title,
58
+ description,
59
+ cta,
60
+ categories,
61
+ className,
62
+ }: ProductCategory1Props) {
63
+ return (
64
+ <section className={cn('relative w-full py-12 md:py-16 lg:py-20', className)}>
65
+ <div className='relative z-10 mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
66
+ <header className='mx-auto mb-12 max-w-5xl'>
67
+ <div className='flex flex-col items-center justify-between gap-6 md:flex-row md:items-start md:text-start'>
68
+ <div className='flex flex-col items-center gap-4 text-center md:items-start md:text-start'>
69
+ {badge ? (
70
+ <div className='inline-flex'>
71
+ <span className='bg-secondary/80 text-secondary-foreground rounded-full px-4 py-1.5 text-sm font-medium'>
72
+ {badge}
73
+ </span>
74
+ </div>
75
+ ) : null}
76
+ <div className='flex flex-col gap-2'>
77
+ <h2 className='text-foreground text-3xl font-bold tracking-tight md:text-4xl'>{title}</h2>
78
+ {description ? (
79
+ <p className='text-muted-foreground max-w-2xl'>{description}</p>
80
+ ) : null}
81
+ </div>
82
+ </div>
83
+ {cta ? (
84
+ <Button
85
+ className='h-10 cursor-pointer whitespace-nowrap px-4'
86
+ size='lg'
87
+ asChild
88
+ >
89
+ <a href={cta.href}>
90
+ {cta.label}
91
+ <ArrowRight />
92
+ </a>
93
+ </Button>
94
+ ) : null}
95
+ </div>
96
+ </header>
97
+
98
+ <div className='mx-auto grid max-w-5xl grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4'>
99
+ {categories.map(category => {
100
+ const Icon = categoryIcons[category.icon] ?? X
101
+ return (
102
+ <Card
103
+ key={category.id}
104
+ className='group relative overflow-hidden py-6 px-2 shadow-xs transition-all hover:shadow-md'
105
+ >
106
+ <CardContent>
107
+ <div className='flex flex-col gap-4'>
108
+ <div className='bg-secondary/80 text-secondary-foreground flex size-12 items-center justify-center rounded-lg'>
109
+ <Icon className='size-5' strokeWidth={1.5} />
110
+ </div>
111
+ <div>
112
+ <h3 className='text-foreground text-lg font-semibold'>{category.name}</h3>
113
+ <p className='text-muted-foreground text-sm'>{category.subtitle}</p>
114
+ </div>
115
+ </div>
116
+ </CardContent>
117
+ </Card>
118
+ )
119
+ })}
120
+ </div>
121
+ </div>
122
+ </section>
123
+ )
124
+ }
125
+
126
+ export default ProductCategory1
@@ -0,0 +1,100 @@
1
+ import { Card } from '../../../ui/card'
2
+ import { Badge } from '../../../ui/badge'
3
+ import { Button } from '../../../ui/button'
4
+ import { ArrowRight, ShoppingBag } from 'lucide-react'
5
+ import type { CTA } from '../../../lib/types'
6
+
7
+ export interface ProductCategory2Category {
8
+ id: string
9
+ handle?: string
10
+ name: string
11
+ description: string
12
+ itemCount: number
13
+ image: string
14
+ trending: boolean
15
+ }
16
+
17
+ export interface ProductCategory2Props {
18
+ title?: string
19
+ description?: string
20
+ categories: ProductCategory2Category[]
21
+ cta?: CTA
22
+ onBrowse?: (category: ProductCategory2Category) => void
23
+ }
24
+
25
+ export function ProductCategory2({
26
+ title,
27
+ description,
28
+ categories,
29
+ cta,
30
+ onBrowse,
31
+ }: ProductCategory2Props) {
32
+ return (
33
+ <section className='py-12'>
34
+ <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
35
+ {(title || description) && (
36
+ <div className='mb-12 text-center'>
37
+ {title ? <h2 className='text-3xl font-bold tracking-tight text-balance'>{title}</h2> : null}
38
+ {description ? (
39
+ <p className='text-muted-foreground mt-4 text-lg'>{description}</p>
40
+ ) : null}
41
+ </div>
42
+ )}
43
+
44
+ <div className='grid gap-6 sm:grid-cols-2 lg:grid-cols-3'>
45
+ {categories.map(category => (
46
+ <Card
47
+ key={category.id}
48
+ className='group cursor-pointer overflow-hidden py-0 transition-all duration-500 hover:shadow-lg'
49
+ >
50
+ <div className='relative aspect-[5/4] overflow-hidden'>
51
+ <img
52
+ src={category.image || undefined}
53
+ alt={category.name}
54
+ className='size-full object-cover transition-transform duration-500 group-hover:scale-105'
55
+ />
56
+ <div className='absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent' />
57
+
58
+ {category.trending ? (
59
+ <Badge className='absolute top-4 left-4 rounded-sm px-2.5 py-0.5 font-semibold'>
60
+ Trending
61
+ </Badge>
62
+ ) : null}
63
+
64
+ <div className='absolute right-0 bottom-0 left-0 p-6 text-white'>
65
+ <h3 className='mb-1 text-xl font-bold'>{category.name}</h3>
66
+ <p className='mb-3 text-sm text-white/90'>{category.description}</p>
67
+ <div className='flex items-center justify-between'>
68
+ <span className='text-sm'>{category.itemCount.toLocaleString()} items</span>
69
+ <Button
70
+ size='sm'
71
+ variant='secondary'
72
+ className='h-8 cursor-pointer border-white/30 bg-white/20 px-3 text-xs text-white backdrop-blur-sm hover:bg-white/30'
73
+ onClick={onBrowse ? () => onBrowse(category) : undefined}
74
+ >
75
+ Browse
76
+ <ArrowRight />
77
+ </Button>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </Card>
82
+ ))}
83
+ </div>
84
+
85
+ {cta ? (
86
+ <div className='mt-12 text-center'>
87
+ <Button size='lg' className='h-10 cursor-pointer gap-2 px-4' asChild>
88
+ <a href={cta.href}>
89
+ <ShoppingBag className='size-5' />
90
+ {cta.label}
91
+ </a>
92
+ </Button>
93
+ </div>
94
+ ) : null}
95
+ </div>
96
+ </section>
97
+ )
98
+ }
99
+
100
+ export default ProductCategory2