@thorprovider/blocks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +10 -0
- package/.turbo/turbo-type-check.log +0 -0
- package/AGENTS.md +1 -0
- package/README.md +161 -0
- package/components.json +33 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/contracts.d.mts +563 -0
- package/dist/contracts.d.ts +563 -0
- package/dist/contracts.js +207 -0
- package/dist/contracts.js.map +1 -0
- package/dist/contracts.mjs +168 -0
- package/dist/contracts.mjs.map +1 -0
- package/dist/index.d.mts +2408 -0
- package/dist/index.d.ts +2408 -0
- package/dist/index.js +16422 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +16352 -0
- package/dist/index.mjs.map +1 -0
- package/docs/standards/data-contracts.md +70 -0
- package/docs/standards/theming.md +85 -0
- package/package.json +62 -0
- package/src/blocks/application/app-shells/app-shell-1-sidebar.tsx +198 -0
- package/src/blocks/application/app-shells/app-shell-1.tsx +196 -0
- package/src/blocks/application/login-forms/login-page-1.tsx +246 -0
- package/src/blocks/application/login-forms/login-page-2.tsx +207 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1-brand-mark.tsx +38 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +320 -0
- package/src/blocks/application/signup-forms/signup-page-1.tsx +231 -0
- package/src/blocks/application/verification/verify-1.tsx +307 -0
- package/src/blocks/application/verification/verify-confetti.tsx +192 -0
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +260 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +179 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +341 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +124 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +170 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +52 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +57 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +68 -0
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +298 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +47 -0
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +9 -0
- package/src/blocks/e-commerce/checkout-forms/checkbox-check.tsx +74 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +189 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +283 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-field-feedback.tsx +29 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +96 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-country-field.tsx +51 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +139 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +184 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +36 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +107 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +235 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +130 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +175 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +190 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +312 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +91 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +110 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +137 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +68 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +79 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +172 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +392 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-types.ts +7 -0
- package/src/blocks/e-commerce/navigation/category-icon.test.tsx +54 -0
- package/src/blocks/e-commerce/navigation/category-icon.tsx +94 -0
- package/src/blocks/e-commerce/navigation/mega-menu.test.tsx +139 -0
- package/src/blocks/e-commerce/navigation/mega-menu.tsx +271 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +152 -0
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +276 -0
- package/src/blocks/e-commerce/order-history/order-history-1.tsx +159 -0
- package/src/blocks/e-commerce/order-history/order-history-2.tsx +222 -0
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +86 -0
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +126 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +100 -0
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +129 -0
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +218 -0
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +131 -0
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +71 -0
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +166 -0
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +215 -0
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +98 -0
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +171 -0
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +367 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +201 -0
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +272 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +59 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-lightbox-tab.tsx +104 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-reviews-tab.tsx +82 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5-tabs.tsx +69 -0
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +274 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-card.tsx +135 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view-dialog.tsx +393 -0
- package/src/blocks/e-commerce/product-overview/product-quick-view.tsx +136 -0
- package/src/blocks/e-commerce/reviews/review-rating-1-review-form.tsx +133 -0
- package/src/blocks/e-commerce/reviews/review-rating-1.tsx +178 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +246 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +291 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +110 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +51 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +295 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +142 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +342 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +146 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +391 -0
- package/src/blocks/e-commerce/store-containers/store-container-1.tsx +29 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +244 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +283 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +318 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +205 -0
- package/src/blocks/marketing/banners/avatar-group.tsx +69 -0
- package/src/blocks/marketing/banners/banner-1.tsx +188 -0
- package/src/blocks/marketing/cta/cta-section-1.tsx +112 -0
- package/src/blocks/marketing/faq/faq-block-1.tsx +45 -0
- package/src/blocks/marketing/features/feature-1.tsx +97 -0
- package/src/blocks/marketing/features/feature-2.tsx +82 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +141 -0
- package/src/blocks/marketing/footers/footer-section-3.tsx +192 -0
- package/src/blocks/marketing/footers/footer-section-4.tsx +355 -0
- package/src/blocks/marketing/hero-sections/avatar-group.tsx +86 -0
- package/src/blocks/marketing/hero-sections/hero-section-1.tsx +135 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1-logo-icon.tsx +52 -0
- package/src/blocks/marketing/logo-clouds/logo-cloud-1.tsx +56 -0
- package/src/blocks/marketing/navbars/navbar-1.tsx +267 -0
- package/src/blocks/marketing/navbars/navbar-2.tsx +351 -0
- package/src/blocks/marketing/navbars/navbar-3.tsx +303 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.tsx +118 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +248 -0
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +177 -0
- package/src/blocks/marketing/navbars/navbar-4-mobile-nav.tsx +104 -0
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +219 -0
- package/src/blocks/marketing/navbars/navbar-4-search-bar.tsx +52 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +411 -0
- package/src/blocks/marketing/pricing/pricing-section-1.tsx +94 -0
- package/src/blocks/marketing/statistics/statistic-1.tsx +93 -0
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +141 -0
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +108 -0
- package/src/blocks/shared/reaction-toggle.tsx +145 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/index.ts +200 -0
- package/src/lib/contracts.test.ts +32 -0
- package/src/lib/contracts.ts +235 -0
- package/src/lib/types/menu.ts +43 -0
- package/src/lib/types.ts +4 -0
- package/src/lib/utils.ts +10 -0
- package/src/ui/accordion.tsx +65 -0
- package/src/ui/avatar.tsx +108 -0
- package/src/ui/badge.tsx +47 -0
- package/src/ui/breadcrumb.tsx +108 -0
- package/src/ui/button-group.tsx +83 -0
- package/src/ui/button.tsx +63 -0
- package/src/ui/card.tsx +91 -0
- package/src/ui/carousel.tsx +241 -0
- package/src/ui/dialog.tsx +158 -0
- package/src/ui/drawer.tsx +134 -0
- package/src/ui/dropdown-menu.tsx +256 -0
- package/src/ui/empty.tsx +103 -0
- package/src/ui/field.tsx +248 -0
- package/src/ui/input-group.tsx +170 -0
- package/src/ui/input-otp.tsx +76 -0
- package/src/ui/input.tsx +20 -0
- package/src/ui/label.tsx +23 -0
- package/src/ui/navigation-menu.tsx +167 -0
- package/src/ui/pagination.tsx +127 -0
- package/src/ui/progress.tsx +30 -0
- package/src/ui/radio-group.tsx +44 -0
- package/src/ui/select.tsx +189 -0
- package/src/ui/separator.tsx +27 -0
- package/src/ui/sheet.tsx +142 -0
- package/src/ui/sidebar.tsx +726 -0
- package/src/ui/skeleton.tsx +13 -0
- package/src/ui/table.tsx +115 -0
- package/src/ui/tabs.tsx +90 -0
- package/src/ui/textarea.tsx +17 -0
- package/src/ui/toggle-group.tsx +83 -0
- package/src/ui/toggle.tsx +46 -0
- package/src/ui/tooltip.tsx +56 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,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
|