@thorprovider/blocks 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +156 -40
- package/package.json +1 -1
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
- package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
- package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +80 -81
- package/src/lib/contracts-curated.test.ts +5 -5
- package/src/lib/contracts.test.ts +55 -0
- package/src/lib/contracts.ts +64 -2
- package/tsup.config.ts +1 -1
- package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
4
|
+
import { render, screen } from '@testing-library/react'
|
|
5
|
+
import userEvent from '@testing-library/user-event'
|
|
6
|
+
import * as blockExports from '../../../index'
|
|
7
|
+
import { TestimonialSection, type TestimonialSectionProps } from './testimonial-section'
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
11
|
+
|
|
12
|
+
window.matchMedia = (query: string) =>
|
|
13
|
+
({
|
|
14
|
+
matches: false,
|
|
15
|
+
media: query,
|
|
16
|
+
onchange: null,
|
|
17
|
+
addListener: () => {},
|
|
18
|
+
removeListener: () => {},
|
|
19
|
+
addEventListener: () => {},
|
|
20
|
+
removeEventListener: () => {},
|
|
21
|
+
dispatchEvent: () => true,
|
|
22
|
+
}) as MediaQueryList
|
|
23
|
+
|
|
24
|
+
window.IntersectionObserver = class {
|
|
25
|
+
disconnect() {}
|
|
26
|
+
|
|
27
|
+
observe() {}
|
|
28
|
+
|
|
29
|
+
takeRecords() {
|
|
30
|
+
return []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
unobserve() {}
|
|
34
|
+
} as unknown as typeof IntersectionObserver
|
|
35
|
+
|
|
36
|
+
window.ResizeObserver = class {
|
|
37
|
+
disconnect() {}
|
|
38
|
+
|
|
39
|
+
observe() {}
|
|
40
|
+
|
|
41
|
+
unobserve() {}
|
|
42
|
+
} as unknown as typeof ResizeObserver
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const renderToHtml = (props: TestimonialSectionProps) =>
|
|
46
|
+
renderToStaticMarkup(<TestimonialSection {...props} />)
|
|
47
|
+
|
|
48
|
+
describe('TestimonialSection — shared content', () => {
|
|
49
|
+
it('is the only public testimonial component export', () => {
|
|
50
|
+
expect(blockExports.TestimonialSection).toBe(TestimonialSection)
|
|
51
|
+
expect('Testimonial1' in blockExports).toBe(false)
|
|
52
|
+
expect('Testimonial2' in blockExports).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('renders supplied testimonial identity, quote, and optional attribution', () => {
|
|
56
|
+
const html = renderToHtml({
|
|
57
|
+
presentation: 'bento',
|
|
58
|
+
items: [
|
|
59
|
+
{
|
|
60
|
+
id: 't-1',
|
|
61
|
+
name: 'Rosa Díaz',
|
|
62
|
+
role: 'Propietaria de taller',
|
|
63
|
+
avatar: '/rosa.png',
|
|
64
|
+
content: 'El servicio es excelente.',
|
|
65
|
+
rating: 4,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(html).toContain('Rosa Díaz')
|
|
71
|
+
expect(html).toContain('Propietaria de taller')
|
|
72
|
+
expect(html).toContain('El servicio es excelente.')
|
|
73
|
+
expect(html).toContain('aria-label="4 out of 5 stars"')
|
|
74
|
+
expect(html).not.toContain('src=""')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('does not invent absent attribution or emit an empty image source', () => {
|
|
78
|
+
const html = renderToHtml({
|
|
79
|
+
presentation: 'bento',
|
|
80
|
+
items: [{ id: 't-1', name: 'Rosa Díaz', content: 'El servicio es excelente.' }],
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(html).toContain('Rosa Díaz')
|
|
84
|
+
expect(html).toContain('El servicio es excelente.')
|
|
85
|
+
expect(html).not.toContain('src=""')
|
|
86
|
+
expect(html).not.toContain('rating')
|
|
87
|
+
expect(html).not.toContain('Propietaria')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('renders no section or controls when the collection is empty', () => {
|
|
91
|
+
expect(
|
|
92
|
+
renderToHtml({
|
|
93
|
+
presentation: 'carousel',
|
|
94
|
+
items: [],
|
|
95
|
+
carouselLabels: { previous: 'Prev', next: 'Next', slide: 'Slide {n}' },
|
|
96
|
+
}),
|
|
97
|
+
).toBe('')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('supports host-provided carousel labels', () => {
|
|
101
|
+
const html = renderToHtml({
|
|
102
|
+
presentation: 'carousel',
|
|
103
|
+
items: [{ id: 't-1', name: 'Rosa Díaz', content: 'El servicio es excelente.' }],
|
|
104
|
+
carouselLabels: {
|
|
105
|
+
previous: 'Testimonio anterior',
|
|
106
|
+
next: 'Testimonio siguiente',
|
|
107
|
+
slide: 'Ir al testimonio {n}',
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(html).toContain('aria-label="Testimonio anterior"')
|
|
112
|
+
expect(html).toContain('aria-label="Testimonio siguiente"')
|
|
113
|
+
expect(html).toContain('aria-label="Ir al testimonio 1"')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it.each([1, 2, 3])('renders every bento entry when the collection has %i items', count => {
|
|
117
|
+
const items = Array.from({ length: count }, (_, index) => ({
|
|
118
|
+
id: `t-${index + 1}`,
|
|
119
|
+
name: `Cliente ${index + 1}`,
|
|
120
|
+
content: `Cita ${index + 1}`,
|
|
121
|
+
}))
|
|
122
|
+
|
|
123
|
+
render(<TestimonialSection presentation='bento' items={items} />)
|
|
124
|
+
|
|
125
|
+
for (const item of items) {
|
|
126
|
+
expect(screen.getByText(item.content)).toBeTruthy()
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('renders every bento entry when the collection has more than four items', () => {
|
|
131
|
+
const items = Array.from({ length: 6 }, (_, index) => ({
|
|
132
|
+
id: `t-${index + 1}`,
|
|
133
|
+
name: `Cliente ${index + 1}`,
|
|
134
|
+
content: `Cita ${index + 1}`,
|
|
135
|
+
}))
|
|
136
|
+
|
|
137
|
+
render(<TestimonialSection presentation='bento' items={items} />)
|
|
138
|
+
|
|
139
|
+
for (const item of items) {
|
|
140
|
+
expect(screen.getByText(item.content)).toBeTruthy()
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('lets keyboard users select carousel entries with localized controls', async () => {
|
|
145
|
+
const user = userEvent.setup()
|
|
146
|
+
render(
|
|
147
|
+
<TestimonialSection
|
|
148
|
+
presentation='carousel'
|
|
149
|
+
items={[
|
|
150
|
+
{ id: 't-1', name: 'Cliente 1', content: 'Cita 1' },
|
|
151
|
+
{ id: 't-2', name: 'Cliente 2', content: 'Cita 2' },
|
|
152
|
+
{ id: 't-3', name: 'Cliente 3', content: 'Cita 3' },
|
|
153
|
+
]}
|
|
154
|
+
carouselLabels={{
|
|
155
|
+
previous: 'Testimonio anterior',
|
|
156
|
+
next: 'Testimonio siguiente',
|
|
157
|
+
slide: 'Ir al testimonio {n}',
|
|
158
|
+
}}
|
|
159
|
+
/>,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
const lastSlide = screen.getByRole('button', { name: 'Ir al testimonio 3' })
|
|
163
|
+
while (document.activeElement !== lastSlide) {
|
|
164
|
+
await user.tab()
|
|
165
|
+
}
|
|
166
|
+
await user.keyboard('{Enter}')
|
|
167
|
+
|
|
168
|
+
expect(lastSlide.getAttribute('aria-current')).toBe('true')
|
|
169
|
+
expect(screen.getByText('Cita 3')).toBeTruthy()
|
|
170
|
+
})
|
|
171
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { Quote, Star } from 'lucide-react'
|
|
5
|
+
import { cn } from '../../../lib/utils'
|
|
6
|
+
import { Avatar, AvatarFallback, AvatarImage } from '../../../ui/avatar'
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from '../../../ui/card'
|
|
8
|
+
import {
|
|
9
|
+
type CarouselApi,
|
|
10
|
+
Carousel,
|
|
11
|
+
CarouselContent,
|
|
12
|
+
CarouselItem,
|
|
13
|
+
CarouselNext,
|
|
14
|
+
CarouselPrevious,
|
|
15
|
+
} from '../../../ui/carousel'
|
|
16
|
+
|
|
17
|
+
export interface TestimonialItem {
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
content: string
|
|
21
|
+
role?: string
|
|
22
|
+
avatar?: string
|
|
23
|
+
rating?: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TestimonialCarouselLabels {
|
|
27
|
+
previous: string
|
|
28
|
+
next: string
|
|
29
|
+
slide: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TestimonialSectionBaseProps {
|
|
33
|
+
items: TestimonialItem[]
|
|
34
|
+
title?: string
|
|
35
|
+
description?: string
|
|
36
|
+
className?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TestimonialSectionProps = TestimonialSectionBaseProps &
|
|
40
|
+
(
|
|
41
|
+
| {
|
|
42
|
+
presentation: 'carousel'
|
|
43
|
+
carouselLabels: TestimonialCarouselLabels
|
|
44
|
+
}
|
|
45
|
+
| {
|
|
46
|
+
presentation: 'bento'
|
|
47
|
+
carouselLabels?: never
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const bentoGridClasses = [
|
|
52
|
+
'grid grid-rows-[auto_1fr] gap-8 ring-1 ring-foreground/10 sm:p-6 md:col-span-2 lg:col-span-2 lg:row-span-2 shadow-sm',
|
|
53
|
+
'ring-1 ring-foreground/10 md:col-span-2 lg:col-span-2 py-6 shadow-sm',
|
|
54
|
+
'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
|
|
55
|
+
'pt-0 ring-1 ring-foreground/10 lg:col-span-1 shadow-sm',
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const contentClasses = ['', 'h-full px-6', 'h-full pt-6 px-6', 'h-full pt-6 px-6']
|
|
59
|
+
const textClasses = ['text-xl text-balance', 'text-base text-balance', 'text-base text-balance', 'text-base text-balance']
|
|
60
|
+
|
|
61
|
+
function initials(name: string) {
|
|
62
|
+
return name
|
|
63
|
+
.split(' ')
|
|
64
|
+
.map(part => part[0])
|
|
65
|
+
.join('')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Rating({ value }: { value: number }) {
|
|
69
|
+
return (
|
|
70
|
+
<div className='flex items-center gap-1' aria-label={`${value} out of 5 stars`}>
|
|
71
|
+
{Array.from({ length: 5 }, (_, index) => (
|
|
72
|
+
<Star
|
|
73
|
+
key={index}
|
|
74
|
+
aria-hidden='true'
|
|
75
|
+
className={cn('size-4', index < Math.floor(value) ? 'fill-foreground' : 'fill-none')}
|
|
76
|
+
/>
|
|
77
|
+
))}
|
|
78
|
+
<span className='text-muted-foreground ms-2 text-sm'>{value}</span>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function TestimonialAttribution({ item }: { item: TestimonialItem }) {
|
|
84
|
+
return (
|
|
85
|
+
<div className='grid grid-cols-[auto_1fr] items-center gap-3'>
|
|
86
|
+
<Avatar className='bg-muted size-12'>
|
|
87
|
+
{item.avatar && <AvatarImage src={item.avatar || undefined} alt={item.name} loading='lazy' />}
|
|
88
|
+
<AvatarFallback>{initials(item.name)}</AvatarFallback>
|
|
89
|
+
</Avatar>
|
|
90
|
+
<div>
|
|
91
|
+
<cite className='text-sm font-medium not-italic'>{item.name}</cite>
|
|
92
|
+
{item.role && <span className='text-muted-foreground block text-sm'>{item.role}</span>}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function BentoPresentation({
|
|
99
|
+
items,
|
|
100
|
+
title,
|
|
101
|
+
description,
|
|
102
|
+
className,
|
|
103
|
+
}: TestimonialSectionBaseProps) {
|
|
104
|
+
return (
|
|
105
|
+
<section className={cn('py-12 md:py-16', className)}>
|
|
106
|
+
<div className='mx-auto flex w-full max-w-7xl flex-col gap-8 px-4 sm:px-6 md:gap-16 lg:px-8'>
|
|
107
|
+
{(title || description) && (
|
|
108
|
+
<header className='relative z-10 mx-auto flex w-full max-w-4xl flex-col gap-6 text-center md:gap-12'>
|
|
109
|
+
{title && <h2 className='text-4xl font-medium max-lg:text-balance lg:text-5xl'>{title}</h2>}
|
|
110
|
+
{description && (
|
|
111
|
+
<p className='text-muted-foreground mx-auto max-w-2xl max-lg:text-balance'>
|
|
112
|
+
{description}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
</header>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4 lg:grid-rows-2'>
|
|
119
|
+
{items.map((item, index) => {
|
|
120
|
+
const tileIndex = index % bentoGridClasses.length
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Card key={item.id} className={bentoGridClasses[tileIndex]}>
|
|
124
|
+
{tileIndex === 0 && (
|
|
125
|
+
<CardHeader className='pb-6'>
|
|
126
|
+
<div className='flex items-center justify-start'>
|
|
127
|
+
<Quote className='text-muted-foreground/30 size-16 rotate-180' aria-hidden='true' />
|
|
128
|
+
</div>
|
|
129
|
+
</CardHeader>
|
|
130
|
+
)}
|
|
131
|
+
<CardContent className={contentClasses[tileIndex]}>
|
|
132
|
+
<blockquote className='grid h-full grid-rows-[1fr_auto] gap-6'>
|
|
133
|
+
<p className={textClasses[tileIndex]}>{item.content}</p>
|
|
134
|
+
<TestimonialAttribution item={item} />
|
|
135
|
+
{item.rating !== undefined && <Rating value={item.rating} />}
|
|
136
|
+
</blockquote>
|
|
137
|
+
</CardContent>
|
|
138
|
+
</Card>
|
|
139
|
+
)
|
|
140
|
+
})}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</section>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function CarouselPresentation({
|
|
148
|
+
items,
|
|
149
|
+
title,
|
|
150
|
+
description,
|
|
151
|
+
className,
|
|
152
|
+
carouselLabels,
|
|
153
|
+
}: TestimonialSectionBaseProps & {
|
|
154
|
+
carouselLabels: TestimonialCarouselLabels
|
|
155
|
+
}) {
|
|
156
|
+
const [carouselApi, setCarouselApi] = useState<CarouselApi>()
|
|
157
|
+
const [selectedSlide, setSelectedSlide] = useState(0)
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!carouselApi) return
|
|
161
|
+
|
|
162
|
+
const updateSelectedSlide = () => setSelectedSlide(carouselApi.selectedScrollSnap())
|
|
163
|
+
updateSelectedSlide()
|
|
164
|
+
carouselApi.on('select', updateSelectedSlide)
|
|
165
|
+
carouselApi.on('reInit', updateSelectedSlide)
|
|
166
|
+
|
|
167
|
+
return () => {
|
|
168
|
+
carouselApi.off('select', updateSelectedSlide)
|
|
169
|
+
carouselApi.off('reInit', updateSelectedSlide)
|
|
170
|
+
}
|
|
171
|
+
}, [carouselApi])
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<section className={cn('py-12 lg:py-20', className)}>
|
|
175
|
+
<div className='mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8'>
|
|
176
|
+
{(title || description) && (
|
|
177
|
+
<header className='mb-12 text-center md:mb-16'>
|
|
178
|
+
{title && <h2 className='text-3xl font-bold text-balance md:text-4xl'>{title}</h2>}
|
|
179
|
+
{description && <p className='text-muted-foreground mx-auto mt-4 max-w-2xl'>{description}</p>}
|
|
180
|
+
</header>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<Carousel
|
|
184
|
+
className='w-full'
|
|
185
|
+
opts={{ align: 'start', loop: true }}
|
|
186
|
+
setApi={setCarouselApi}
|
|
187
|
+
aria-label={title}
|
|
188
|
+
>
|
|
189
|
+
<CarouselContent className='-ml-1'>
|
|
190
|
+
{items.map(item => (
|
|
191
|
+
<CarouselItem key={item.id} className='basis-full px-4 last:pe-0 sm:basis-1/2 lg:basis-1/3'>
|
|
192
|
+
<Card className='h-full overflow-hidden border border-border py-6'>
|
|
193
|
+
<CardHeader className='gap-0 px-6'>
|
|
194
|
+
<TestimonialAttribution item={item} />
|
|
195
|
+
</CardHeader>
|
|
196
|
+
<CardContent className='px-6'>
|
|
197
|
+
<blockquote className='text-muted-foreground text-base'>{item.content}</blockquote>
|
|
198
|
+
{item.rating !== undefined && <Rating value={item.rating} />}
|
|
199
|
+
</CardContent>
|
|
200
|
+
</Card>
|
|
201
|
+
</CarouselItem>
|
|
202
|
+
))}
|
|
203
|
+
</CarouselContent>
|
|
204
|
+
<CarouselPrevious className='cursor-pointer' aria-label={carouselLabels.previous} />
|
|
205
|
+
<CarouselNext className='cursor-pointer' aria-label={carouselLabels.next} />
|
|
206
|
+
</Carousel>
|
|
207
|
+
<div className='mt-6 flex justify-center gap-2'>
|
|
208
|
+
{items.map((item, index) => (
|
|
209
|
+
<button
|
|
210
|
+
key={item.id}
|
|
211
|
+
type='button'
|
|
212
|
+
className={cn(
|
|
213
|
+
'size-2 rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
214
|
+
selectedSlide === index ? 'bg-foreground' : 'bg-muted-foreground/40',
|
|
215
|
+
)}
|
|
216
|
+
aria-label={carouselLabels.slide.replace('{n}', String(index + 1))}
|
|
217
|
+
aria-current={selectedSlide === index ? 'true' : undefined}
|
|
218
|
+
onClick={() => carouselApi?.scrollTo(index)}
|
|
219
|
+
/>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</section>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function TestimonialSection(props: TestimonialSectionProps) {
|
|
228
|
+
if (props.items.length === 0) return null
|
|
229
|
+
|
|
230
|
+
if (props.presentation === 'carousel') {
|
|
231
|
+
return <CarouselPresentation {...props} />
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return <BentoPresentation {...props} />
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default TestimonialSection
|
package/src/index.ts
CHANGED
|
@@ -16,16 +16,13 @@ export {
|
|
|
16
16
|
CardContent,
|
|
17
17
|
} from './ui/card'
|
|
18
18
|
|
|
19
|
-
export { default as
|
|
20
|
-
export { default as
|
|
21
|
-
export { default as Navbar3 } from './blocks/marketing/navbars/navbar-3'
|
|
22
|
-
export { default as Navbar4 } from './blocks/marketing/navbars/navbar-4'
|
|
19
|
+
export { default as Navbar } from './blocks/marketing/navbars/navbar'
|
|
20
|
+
export { default as StorefrontNavbar } from './blocks/e-commerce/navigation/storefront-navbar'
|
|
23
21
|
export { default as Banner1 } from './blocks/marketing/banners/banner-1'
|
|
24
22
|
export { default as HeroSection1 } from './blocks/marketing/hero-sections/hero-section-1'
|
|
25
23
|
export { default as CtaSection1 } from './blocks/marketing/cta/cta-section-1'
|
|
26
24
|
export { default as Feature } from './blocks/marketing/features/feature'
|
|
27
|
-
export {
|
|
28
|
-
export { default as Testimonial2 } from './blocks/marketing/testimonials/testimonial-2'
|
|
25
|
+
export { TestimonialSection } from './blocks/marketing/testimonials/testimonial-section'
|
|
29
26
|
export { default as FaqBlock1 } from './blocks/marketing/faq/faq-block-1'
|
|
30
27
|
export { default as PricingSection1 } from './blocks/marketing/pricing/pricing-section-1'
|
|
31
28
|
export type { CtaSection1Props } from './blocks/marketing/cta/cta-section-1'
|
|
@@ -38,22 +35,37 @@ export type {
|
|
|
38
35
|
export type { LogoCloud1Props, LogoCloud1Platform } from './blocks/marketing/logo-clouds/logo-cloud-1'
|
|
39
36
|
export type { PricingSection1Props, PricingTier } from './blocks/marketing/pricing/pricing-section-1'
|
|
40
37
|
export type { Statistic1Props, StatisticItem } from './blocks/marketing/statistics/statistic-1'
|
|
41
|
-
export type {
|
|
42
|
-
|
|
38
|
+
export type {
|
|
39
|
+
TestimonialItem,
|
|
40
|
+
TestimonialCarouselLabels,
|
|
41
|
+
TestimonialSectionProps,
|
|
42
|
+
} from './blocks/marketing/testimonials/testimonial-section'
|
|
43
43
|
export { default as Statistic1 } from './blocks/marketing/statistics/statistic-1'
|
|
44
44
|
export { default as LogoCloud1 } from './blocks/marketing/logo-clouds/logo-cloud-1'
|
|
45
|
-
export {
|
|
46
|
-
export {
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
export { FooterSection } from './blocks/marketing/footers/footer-section'
|
|
46
|
+
export type {
|
|
47
|
+
FooterBrandAccent,
|
|
48
|
+
FooterContact,
|
|
49
|
+
FooterContactItem,
|
|
50
|
+
FooterLink as CanonicalFooterLink,
|
|
51
|
+
FooterLinkBehavior,
|
|
52
|
+
FooterLinkColumn as CanonicalFooterLinkColumn,
|
|
53
|
+
FooterNewsletter,
|
|
54
|
+
FooterPresentation,
|
|
55
|
+
FooterSectionProps,
|
|
56
|
+
FooterSocialLink,
|
|
57
|
+
} from './blocks/marketing/footers/footer-section'
|
|
49
58
|
export type {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
NavbarFeatured,
|
|
60
|
+
NavbarGroup,
|
|
61
|
+
NavbarIcon,
|
|
62
|
+
NavbarIconMap,
|
|
63
|
+
NavbarItem,
|
|
64
|
+
NavbarLink,
|
|
65
|
+
NavbarPresentation,
|
|
66
|
+
NavbarProps,
|
|
67
|
+
} from './blocks/marketing/navbars/navbar'
|
|
68
|
+
export type { StorefrontNavbarProps } from './blocks/e-commerce/navigation/storefront-navbar'
|
|
57
69
|
export { ProductCard } from './blocks/e-commerce/product-list/product-card'
|
|
58
70
|
export type {
|
|
59
71
|
ProductCardProps,
|
|
@@ -107,31 +119,22 @@ export { MobileBottomNav, mobileBottomNavItemSchema, mobileBottomNavPropsSchema
|
|
|
107
119
|
export type { MobileBottomNavItem, MobileBottomNavProps } from './blocks/e-commerce/navigation/mobile-bottom-nav'
|
|
108
120
|
export { menuItemSchema, collectionItemSchema } from './lib/types/menu'
|
|
109
121
|
export type { MenuItem, CollectionItem } from './lib/types/menu'
|
|
110
|
-
export {
|
|
122
|
+
export { CategoryListing, CategoryListingStatusView, buildFilterChange, reconcileSearchDraft, toggleFilterValue } from './blocks/e-commerce/category-filters/category-listing'
|
|
111
123
|
export type {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
CategoryFilter2Filters,
|
|
127
|
-
CategoryFilter2Status,
|
|
128
|
-
CategoryFilter2StatusViewProps,
|
|
129
|
-
CategoryFilter2QueryError,
|
|
130
|
-
CategoryFilter2ActiveFilter,
|
|
131
|
-
CategoryFilter2ActiveFilterKind,
|
|
132
|
-
} from './blocks/e-commerce/category-filters/category-filter-2'
|
|
133
|
-
export { default as CategoryFilter4 } from './blocks/e-commerce/category-filters/category-filter-4'
|
|
134
|
-
export type { CategoryFilter4Props, CategoryFilter4Product, CategoryFilter4Facets } from './blocks/e-commerce/category-filters/category-filter-4'
|
|
124
|
+
CategoryListingActiveFilter,
|
|
125
|
+
CategoryListingActiveFilterKind,
|
|
126
|
+
CategoryListingFacetGroup,
|
|
127
|
+
CategoryListingFacetOption,
|
|
128
|
+
CategoryListingFilters,
|
|
129
|
+
CategoryListingOption,
|
|
130
|
+
CategoryListingPresentation,
|
|
131
|
+
CategoryListingPriceRange,
|
|
132
|
+
CategoryListingProps,
|
|
133
|
+
CategoryListingQueryError,
|
|
134
|
+
CategoryListingSortOption,
|
|
135
|
+
CategoryListingStatus,
|
|
136
|
+
CategoryListingStatusViewProps,
|
|
137
|
+
} from './blocks/e-commerce/category-filters/category-listing'
|
|
135
138
|
export { default as ShoppingCart } from './blocks/e-commerce/shopping-carts/shopping-cart'
|
|
136
139
|
export type {
|
|
137
140
|
ShoppingCartProps,
|
|
@@ -141,38 +144,24 @@ export type {
|
|
|
141
144
|
} from './blocks/e-commerce/shopping-carts/shopping-cart'
|
|
142
145
|
export { default as StoreContainer1 } from './blocks/e-commerce/store-containers/store-container-1'
|
|
143
146
|
export type { StoreContainer1Props } from './blocks/e-commerce/store-containers/store-container-1'
|
|
144
|
-
export { default as
|
|
145
|
-
export { default as CheckoutForm2 } from './blocks/e-commerce/checkout-forms/checkout-form-2'
|
|
146
|
-
export { default as CheckoutForm3 } from './blocks/e-commerce/checkout-forms/checkout-form-3'
|
|
147
|
-
export { default as CheckoutConfirmation } from './blocks/e-commerce/checkout-forms/checkout-confirmation'
|
|
147
|
+
export { default as Checkout } from './blocks/e-commerce/checkout-forms/checkout'
|
|
148
148
|
export type {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
CheckoutForm2FieldErrors,
|
|
166
|
-
CheckoutForm2FieldDescriptions,
|
|
167
|
-
CheckoutForm2Totals,
|
|
168
|
-
} from './blocks/e-commerce/checkout-forms/checkout-form-2'
|
|
169
|
-
export type {
|
|
170
|
-
CheckoutForm3Props,
|
|
171
|
-
CheckoutForm3CartItem,
|
|
172
|
-
CheckoutForm3FormData,
|
|
173
|
-
CheckoutForm3ShippingMethod,
|
|
174
|
-
CheckoutForm3CountryOption,
|
|
175
|
-
} from './blocks/e-commerce/checkout-forms/checkout-form-3'
|
|
149
|
+
CheckoutProps,
|
|
150
|
+
CheckoutValues,
|
|
151
|
+
CheckoutPresentation,
|
|
152
|
+
CheckoutStepId,
|
|
153
|
+
CheckoutLabels,
|
|
154
|
+
CheckoutValuesFieldErrors,
|
|
155
|
+
CheckoutFieldDescriptions,
|
|
156
|
+
CheckoutChangeHandler,
|
|
157
|
+
CheckoutOrderSummary,
|
|
158
|
+
CheckoutOrderLine,
|
|
159
|
+
CheckoutTotals,
|
|
160
|
+
CheckoutTotalsState,
|
|
161
|
+
CheckoutCountryOption,
|
|
162
|
+
CheckoutShippingOption,
|
|
163
|
+
} from './blocks/e-commerce/checkout-forms/checkout'
|
|
164
|
+
export { default as CheckoutConfirmation } from './blocks/e-commerce/checkout-forms/checkout-confirmation'
|
|
176
165
|
export type {
|
|
177
166
|
CheckoutConfirmationProps,
|
|
178
167
|
FulfilmentStage,
|
|
@@ -198,13 +187,11 @@ export type {
|
|
|
198
187
|
StorefrontHeroProps,
|
|
199
188
|
} from './blocks/e-commerce/storefront-hero/storefront-hero'
|
|
200
189
|
export { default as AppShell1 } from './blocks/application/app-shells/app-shell-1'
|
|
201
|
-
export { default as
|
|
202
|
-
export { default as LoginPage2 } from './blocks/application/login-forms/login-page-2'
|
|
190
|
+
export { default as LoginPage } from './blocks/application/login-forms/login-page'
|
|
203
191
|
export { default as SignUpPage1 } from './blocks/application/signup-forms/signup-page-1'
|
|
204
192
|
export { default as ForgotPasswordPage1 } from './blocks/application/password-recovery/forgot-password-page-1'
|
|
205
193
|
export { default as Verify1 } from './blocks/application/verification/verify-1'
|
|
206
|
-
export type {
|
|
207
|
-
export type { LoginPage2Props, LoginPage2SocialButton } from './blocks/application/login-forms/login-page-2'
|
|
194
|
+
export type { LoginPageProps, LoginPageFormData, LoginPageProvider } from './blocks/application/login-forms/login-page'
|
|
208
195
|
export type { SignUpPage1Props, SignUpPage1Data, SignUpPage1SocialButton } from './blocks/application/signup-forms/signup-page-1'
|
|
209
196
|
export type { ForgotPasswordPage1Props } from './blocks/application/password-recovery/forgot-password-page-1'
|
|
210
197
|
export type { Verify1Props } from './blocks/application/verification/verify-1'
|
|
@@ -235,7 +222,13 @@ export {
|
|
|
235
222
|
shoppingCartPropsSchema,
|
|
236
223
|
shoppingCartItemSchema,
|
|
237
224
|
shoppingCartSuggestionSchema,
|
|
238
|
-
|
|
225
|
+
navbarPresentationSchema,
|
|
226
|
+
navbarItemSchema,
|
|
227
|
+
navbarGroupSchema,
|
|
228
|
+
navbarLinkSchema,
|
|
229
|
+
navbarFeaturedSchema,
|
|
230
|
+
navbarPropsSchema,
|
|
231
|
+
storefrontNavbarPropsSchema,
|
|
239
232
|
paymentMethodSchema,
|
|
240
233
|
cardProductSchema,
|
|
241
234
|
cartItemSchema,
|
|
@@ -266,7 +259,13 @@ export type {
|
|
|
266
259
|
ShoppingCartItem,
|
|
267
260
|
ShoppingCartSuggestion,
|
|
268
261
|
ShoppingCartSpecProps,
|
|
269
|
-
|
|
262
|
+
NavbarPresentationSpec,
|
|
263
|
+
NavbarItemSpec,
|
|
264
|
+
NavbarGroupSpec,
|
|
265
|
+
NavbarLinkSpec,
|
|
266
|
+
NavbarFeaturedSpec,
|
|
267
|
+
NavbarSpecProps,
|
|
268
|
+
StorefrontNavbarSpecProps,
|
|
270
269
|
CardProduct,
|
|
271
270
|
CartItem,
|
|
272
271
|
Promotion,
|
|
@@ -282,4 +281,4 @@ export type {
|
|
|
282
281
|
ProductCategorySpecProps,
|
|
283
282
|
CategoryCardSpecProps,
|
|
284
283
|
CategoryGridSpecProps,
|
|
285
|
-
} from './lib/contracts'
|
|
284
|
+
} from './lib/contracts'
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
logoAccentSchema,
|
|
11
11
|
moneySchema,
|
|
12
12
|
navCartItemSchema,
|
|
13
|
-
|
|
13
|
+
storefrontNavbarPropsSchema,
|
|
14
14
|
productOverviewPropsSchema,
|
|
15
15
|
shoppingCartPropsSchema,
|
|
16
16
|
} from './contracts'
|
|
@@ -113,12 +113,12 @@ describe('optional brand seams — present, omitted, invalid', () => {
|
|
|
113
113
|
utilityLinks: [],
|
|
114
114
|
logoText: 'ElectroCuba',
|
|
115
115
|
}
|
|
116
|
-
const parsed =
|
|
116
|
+
const parsed = storefrontNavbarPropsSchema.parse(base)
|
|
117
117
|
expect(parsed.logoAccent).toBeUndefined()
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
it('accepts navbar props with the accent present', () => {
|
|
121
|
-
const parsed =
|
|
121
|
+
const parsed = storefrontNavbarPropsSchema.parse({
|
|
122
122
|
storeCategories: [],
|
|
123
123
|
shippingNote: 'Catálogo en línea',
|
|
124
124
|
utilityLinks: [],
|
|
@@ -135,8 +135,8 @@ describe('optional brand seams — present, omitted, invalid', () => {
|
|
|
135
135
|
utilityLinks: [],
|
|
136
136
|
logoText: 'My Shop',
|
|
137
137
|
}
|
|
138
|
-
expect(
|
|
139
|
-
expect(
|
|
138
|
+
expect(storefrontNavbarPropsSchema.parse(base).className).toBeUndefined()
|
|
139
|
+
expect(storefrontNavbarPropsSchema.parse({ ...base, className: 'bg-brand-nav text-brand-nav-foreground' }).className).toBe(
|
|
140
140
|
'bg-brand-nav text-brand-nav-foreground',
|
|
141
141
|
)
|
|
142
142
|
})
|