@thorprovider/blocks 0.1.3 → 1.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 (99) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +73 -29
  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/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  7. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  8. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  9. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  10. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  11. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  12. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  13. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  14. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  15. package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
  16. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  17. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  18. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  19. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  20. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  21. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  22. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  23. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  24. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  25. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  26. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  27. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  28. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  29. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  30. package/src/blocks/marketing/features/feature.tsx +195 -0
  31. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +27 -9
  32. package/src/blocks/marketing/navbars/navbar-4.test.tsx +33 -2
  33. package/src/blocks/marketing/navbars/navbar-4.tsx +29 -11
  34. package/src/index.ts +77 -48
  35. package/src/lib/contracts-curated.test.ts +42 -24
  36. package/src/lib/contracts.test.ts +143 -2
  37. package/src/lib/contracts.ts +346 -14
  38. package/src/ui/button-group.tsx +1 -1
  39. package/src/ui/carousel.tsx +1 -1
  40. package/src/ui/dialog.tsx +1 -1
  41. package/src/ui/field.tsx +2 -2
  42. package/src/ui/input-group.tsx +3 -3
  43. package/src/ui/pagination.tsx +1 -1
  44. package/src/ui/sidebar.tsx +7 -7
  45. package/src/ui/toggle-group.tsx +1 -1
  46. package/tsconfig.build.json +12 -0
  47. package/tsup.config.ts +11 -7
  48. package/.turbo/turbo-build.log +0 -24
  49. package/.turbo/turbo-test.log +0 -47
  50. package/.turbo/turbo-type-check.log +0 -0
  51. package/dist/.tsbuildinfo +0 -1
  52. package/dist/contracts.d.mts +0 -1732
  53. package/dist/contracts.d.ts +0 -1732
  54. package/dist/contracts.js +0 -423
  55. package/dist/contracts.js.map +0 -1
  56. package/dist/contracts.mjs +0 -365
  57. package/dist/contracts.mjs.map +0 -1
  58. package/dist/index.d.mts +0 -3068
  59. package/dist/index.d.ts +0 -3068
  60. package/dist/index.js +0 -17834
  61. package/dist/index.js.map +0 -1
  62. package/dist/index.mjs +0 -17740
  63. package/dist/index.mjs.map +0 -1
  64. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  65. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  66. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  67. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  68. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  69. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  70. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  71. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  72. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  73. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  74. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  75. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  76. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  77. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  78. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  79. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  80. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  81. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  82. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  83. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  84. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  85. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  86. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  87. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  88. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  89. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  90. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  91. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  92. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  93. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  94. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  95. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  96. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  97. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  98. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  99. package/src/blocks/marketing/features/feature-2.tsx +0 -82
@@ -1,86 +0,0 @@
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
@@ -1,126 +0,0 @@
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
@@ -1,51 +0,0 @@
1
- import { renderToStaticMarkup } from 'react-dom/server'
2
- import { describe, expect, it } from 'vitest'
3
- import {
4
- ProductCategory2,
5
- type ProductCategory2Category,
6
- } from './product-category-2'
7
-
8
- const base: ProductCategory2Category = {
9
- id: 'cat-1',
10
- name: 'Herramientas',
11
- description: 'Taladros y accesorios',
12
- image: '/products/drill.png',
13
- trending: false,
14
- }
15
-
16
- const render = (category: ProductCategory2Category) =>
17
- renderToStaticMarkup(<ProductCategory2 categories={[category]} />)
18
-
19
- // The count is the only text node that ends in "items" inside a span (the
20
- // `items-center` utility lives in a class attribute and is never followed by
21
- // `</span>`).
22
- const hasItemCount = (html: string) => html.includes('items</span>')
23
-
24
- describe('ProductCategory2 — optional itemCount', () => {
25
- it('renders no count when itemCount is omitted (unknown projection)', () => {
26
- const html = render(base)
27
- expect(hasItemCount(html)).toBe(false)
28
- // The tile itself still renders.
29
- expect(html).toContain('Herramientas')
30
- expect(html).toContain('Browse')
31
- })
32
-
33
- it('preserves a real zero count', () => {
34
- const html = render({ ...base, itemCount: 0 })
35
- expect(html).toContain('0 items</span>')
36
- })
37
-
38
- it('renders a positive count', () => {
39
- const html = render({ ...base, itemCount: 7 })
40
- expect(html).toContain('7 items</span>')
41
- })
42
-
43
- it('omits a negative count', () => {
44
- expect(hasItemCount(render({ ...base, itemCount: -1 }))).toBe(false)
45
- })
46
-
47
- it('omits a non-finite count', () => {
48
- expect(hasItemCount(render({ ...base, itemCount: Number.NaN }))).toBe(false)
49
- expect(hasItemCount(render({ ...base, itemCount: Number.POSITIVE_INFINITY }))).toBe(false)
50
- })
51
- })
@@ -1,120 +0,0 @@
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
- /**
13
- * Number of items in the category. Optional: when omitted, or when it is not
14
- * a finite non-negative number, no count is rendered — an unknown projection
15
- * must not be shown as a fabricated `0`. A real `0` is preserved.
16
- */
17
- itemCount?: number
18
- image: string
19
- trending: boolean
20
- }
21
-
22
- export interface ProductCategory2Props {
23
- title?: string
24
- description?: string
25
- categories: ProductCategory2Category[]
26
- cta?: CTA
27
- onBrowse?: (category: ProductCategory2Category) => void
28
- }
29
-
30
- /**
31
- * Formats a category item count only when it is a supplied, finite,
32
- * non-negative number. Returns `undefined` for unknown or invalid values so the
33
- * caller omits the count entirely (a real `0` is preserved).
34
- */
35
- function formatItemCount(itemCount: number | undefined): string | undefined {
36
- if (itemCount === undefined || !Number.isFinite(itemCount) || itemCount < 0) {
37
- return undefined
38
- }
39
- return `${itemCount.toLocaleString()} items`
40
- }
41
-
42
- export function ProductCategory2({
43
- title,
44
- description,
45
- categories,
46
- cta,
47
- onBrowse,
48
- }: ProductCategory2Props) {
49
- return (
50
- <section className='py-12'>
51
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
52
- {(title || description) && (
53
- <div className='mb-12 text-center'>
54
- {title ? <h2 className='text-3xl font-bold tracking-tight text-balance'>{title}</h2> : null}
55
- {description ? (
56
- <p className='text-muted-foreground mt-4 text-lg'>{description}</p>
57
- ) : null}
58
- </div>
59
- )}
60
-
61
- <div className='grid gap-6 sm:grid-cols-2 lg:grid-cols-3'>
62
- {categories.map(category => {
63
- const itemCountLabel = formatItemCount(category.itemCount)
64
- return (
65
- <Card
66
- key={category.id}
67
- className='group cursor-pointer overflow-hidden py-0 transition-all duration-500 hover:shadow-lg'
68
- >
69
- <div className='relative aspect-[5/4] overflow-hidden'>
70
- <img
71
- src={category.image || undefined}
72
- alt={category.name}
73
- className='size-full object-cover transition-transform duration-500 group-hover:scale-105'
74
- />
75
- <div className='absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent' />
76
-
77
- {category.trending ? (
78
- <Badge className='absolute top-4 left-4 rounded-sm px-2.5 py-0.5 font-semibold'>
79
- Trending
80
- </Badge>
81
- ) : null}
82
-
83
- <div className='absolute right-0 bottom-0 left-0 p-6 text-white'>
84
- <h3 className='mb-1 text-xl font-bold'>{category.name}</h3>
85
- <p className='mb-3 text-sm text-white/90'>{category.description}</p>
86
- <div className='flex items-center justify-between'>
87
- {itemCountLabel ? <span className='text-sm'>{itemCountLabel}</span> : null}
88
- <Button
89
- size='sm'
90
- variant='secondary'
91
- className='ms-auto h-8 cursor-pointer border-white/30 bg-white/20 px-3 text-xs text-white backdrop-blur-sm hover:bg-white/30'
92
- onClick={onBrowse ? () => onBrowse(category) : undefined}
93
- >
94
- Browse
95
- <ArrowRight />
96
- </Button>
97
- </div>
98
- </div>
99
- </div>
100
- </Card>
101
- )
102
- })}
103
- </div>
104
-
105
- {cta ? (
106
- <div className='mt-12 text-center'>
107
- <Button size='lg' className='h-10 cursor-pointer gap-2 px-4' asChild>
108
- <a href={cta.href}>
109
- <ShoppingBag className='size-5' />
110
- {cta.label}
111
- </a>
112
- </Button>
113
- </div>
114
- ) : null}
115
- </div>
116
- </section>
117
- )
118
- }
119
-
120
- export default ProductCategory2
@@ -1,129 +0,0 @@
1
- 'use client'
2
-
3
- import type { LucideIcon } from 'lucide-react'
4
- import { Users, ShoppingBag } from 'lucide-react'
5
-
6
- import { Card, CardContent } from '../../../ui/card'
7
- import { Badge } from '../../../ui/badge'
8
- import { Avatar, AvatarImage, AvatarFallback } from '../../../ui/avatar'
9
- import { AvatarGroup } from './avatar-group'
10
-
11
- export interface ProductCategory4Category {
12
- id: string
13
- title: string
14
- description: string
15
- icon: LucideIcon
16
- customers: string
17
- products: number
18
- avatarImages?: string[]
19
- }
20
-
21
- export interface ProductCategory4Props {
22
- title?: string
23
- description?: string
24
- categories: ProductCategory4Category[]
25
- productsLabel?: string
26
- customersLabel?: string
27
- onCategoryClick?: (category: ProductCategory4Category) => void
28
- }
29
-
30
- function getInitials(title: string): string {
31
- return title
32
- .split(/\s+/)
33
- .filter(Boolean)
34
- .slice(0, 2)
35
- .map(word => word.charAt(0).toUpperCase())
36
- .join('')
37
- }
38
-
39
- export function ProductCategory4({
40
- title = 'Compra por categoría',
41
- description = 'Explora nuestra selección de repuestos y accesorios',
42
- categories,
43
- productsLabel = 'productos',
44
- customersLabel = 'clientes',
45
- onCategoryClick,
46
- }: ProductCategory4Props) {
47
- return (
48
- <section className='bg-background w-full py-12 md:py-16'>
49
- <div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
50
- <div className='mx-auto mb-12 max-w-3xl text-center'>
51
- <h2 className='text-foreground mb-4 text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl'>
52
- {title}
53
- </h2>
54
- <p className='text-muted-foreground text-lg md:text-xl'>{description}</p>
55
- </div>
56
-
57
- <div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3'>
58
- {categories.map(category => {
59
- const Icon = category.icon
60
- const clickable = Boolean(onCategoryClick)
61
- return (
62
- <Card
63
- key={category.id}
64
- className={`group relative overflow-hidden transition-all hover:shadow-md ${
65
- clickable ? 'cursor-pointer' : ''
66
- }`}
67
- onClick={onCategoryClick ? () => onCategoryClick(category) : undefined}
68
- aria-label={clickable ? category.title : undefined}
69
- >
70
- <CardContent>
71
- <div className='mb-4 flex items-center justify-between'>
72
- <div className='bg-accent text-primary rounded-lg p-2.5' aria-hidden='true'>
73
- <Icon className='size-5' />
74
- </div>
75
- <Badge
76
- variant='secondary'
77
- className='bg-muted/50 rounded-sm px-2 py-1 font-semibold'
78
- aria-label={`${category.products} ${productsLabel}`}
79
- >
80
- <ShoppingBag className='me-1 size-3' />
81
- {category.products} {productsLabel}
82
- </Badge>
83
- </div>
84
-
85
- <h3 className='text-foreground mb-2 text-lg font-semibold'>{category.title}</h3>
86
- <p className='text-muted-foreground mb-4 text-sm'>{category.description}</p>
87
-
88
- <div className='flex items-center justify-between'>
89
- <div className='flex items-center gap-2'>
90
- {category.avatarImages?.length ? (
91
- <>
92
- <AvatarGroup>
93
- {category.avatarImages.slice(0, 3).map((src, i) => (
94
- <Avatar key={i} className='bg-muted size-6 border'>
95
- <AvatarImage src={src || undefined} alt='' />
96
- <AvatarFallback>{getInitials(category.title)}</AvatarFallback>
97
- </Avatar>
98
- ))}
99
- </AvatarGroup>
100
- <span
101
- className='text-muted-foreground flex items-center gap-1 text-sm'
102
- aria-label={`${category.customers} ${customersLabel}`}
103
- >
104
- <Users className='h-3.5 w-3.5' aria-hidden='true' />
105
- {category.customers} {customersLabel}
106
- </span>
107
- </>
108
- ) : (
109
- <span
110
- className='text-muted-foreground flex items-center gap-1 text-sm'
111
- aria-label={`${category.customers} ${customersLabel}`}
112
- >
113
- <Users className='h-3.5 w-3.5' aria-hidden='true' />
114
- {category.customers} {customersLabel}
115
- </span>
116
- )}
117
- </div>
118
- </div>
119
- </CardContent>
120
- </Card>
121
- )
122
- })}
123
- </div>
124
- </div>
125
- </section>
126
- )
127
- }
128
-
129
- export default ProductCategory4