@thorprovider/blocks 0.1.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +155 -0
- package/README.md +228 -68
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- 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/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
- 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} +67 -36
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
- package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
- package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
- package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
- package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
- package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
- package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
- package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
- package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
- package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
- package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
- package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
- package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
- package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
- package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
- package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
- package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
- package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
- package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
- package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
- package/src/blocks/marketing/features/feature.test.tsx +153 -0
- package/src/blocks/marketing/features/feature.tsx +195 -0
- 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 +156 -128
- package/src/lib/contracts-curated.test.ts +47 -29
- package/src/lib/contracts.test.ts +198 -2
- package/src/lib/contracts.ts +410 -16
- package/src/ui/button-group.tsx +1 -1
- package/src/ui/carousel.tsx +1 -1
- package/src/ui/dialog.tsx +1 -1
- package/src/ui/field.tsx +2 -2
- package/src/ui/input-group.tsx +3 -3
- package/src/ui/pagination.tsx +1 -1
- package/src/ui/sidebar.tsx +7 -7
- package/src/ui/toggle-group.tsx +1 -1
- package/tsconfig.build.json +12 -0
- package/tsup.config.ts +11 -7
- package/.turbo/turbo-build.log +0 -24
- package/.turbo/turbo-test.log +0 -47
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1732
- package/dist/contracts.d.ts +0 -1732
- package/dist/contracts.js +0 -423
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -365
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -3068
- package/dist/index.d.ts +0 -3068
- package/dist/index.js +0 -17834
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17740
- package/dist/index.mjs.map +0 -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/e-commerce/product-categories/avatar-group.tsx +0 -86
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
- package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
- package/src/blocks/marketing/features/feature-1.tsx +0 -97
- package/src/blocks/marketing/features/feature-2.tsx +0 -82
- 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-account-menu.test.tsx +0 -38
- 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
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { ShoppingCart4, type ShoppingCart4Item, type ShoppingCart4Totals } from './shopping-cart-4'
|
|
4
|
-
|
|
5
|
-
const items: ShoppingCart4Item[] = [
|
|
6
|
-
{
|
|
7
|
-
id: 'line_1',
|
|
8
|
-
name: 'Taladro',
|
|
9
|
-
price: 45,
|
|
10
|
-
quantity: 2,
|
|
11
|
-
image: 'https://cdn.example/taladro.jpg',
|
|
12
|
-
lineTotal: { amount: '90.00', currencyCode: 'USD' },
|
|
13
|
-
},
|
|
14
|
-
{ id: 'line_2', name: 'Batería', price: 20, quantity: 1 },
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
const totals: ShoppingCart4Totals = {
|
|
18
|
-
subtotal: { amount: '110.00', currencyCode: 'USD' },
|
|
19
|
-
discount: { amount: '10.00', currencyCode: 'USD' },
|
|
20
|
-
tax: { amount: '8.00', currencyCode: 'USD' },
|
|
21
|
-
shipping: { amount: '0', currencyCode: 'USD' },
|
|
22
|
-
total: { amount: '108.00', currencyCode: 'USD' },
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const formatMoney = (money: { amount: string; currencyCode: string }) =>
|
|
26
|
-
`${money.currencyCode} ${money.amount}`
|
|
27
|
-
|
|
28
|
-
describe('ShoppingCart4 — backend-authoritative totals', () => {
|
|
29
|
-
it('renders supplied backend totals verbatim (no local arithmetic)', () => {
|
|
30
|
-
const html = renderToStaticMarkup(
|
|
31
|
-
<ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
|
|
32
|
-
)
|
|
33
|
-
expect(html).toContain('USD 110.00')
|
|
34
|
-
expect(html).toContain('USD 10.00')
|
|
35
|
-
expect(html).toContain('USD 8.00')
|
|
36
|
-
expect(html).toContain('Gratis')
|
|
37
|
-
expect(html).toContain('USD 108.00')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('does not invent subtotal or total when none are supplied', () => {
|
|
41
|
-
const html = renderToStaticMarkup(
|
|
42
|
-
<ShoppingCart4 items={items} promotions={[]} formatMoney={formatMoney} />,
|
|
43
|
-
)
|
|
44
|
-
expect(html).not.toContain('Subtotal')
|
|
45
|
-
expect(html).not.toContain('Total')
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('renders a controlled pending state', () => {
|
|
49
|
-
const html = renderToStaticMarkup(
|
|
50
|
-
<ShoppingCart4 items={items} promotions={[]} totalsState='pending' />,
|
|
51
|
-
)
|
|
52
|
-
expect(html).toContain('Actualizando totales')
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('renders a controlled error state with a recovery action', () => {
|
|
56
|
-
const html = renderToStaticMarkup(
|
|
57
|
-
<ShoppingCart4
|
|
58
|
-
items={items}
|
|
59
|
-
promotions={[]}
|
|
60
|
-
totalsState='error'
|
|
61
|
-
onRetryTotals={() => {}}
|
|
62
|
-
/>,
|
|
63
|
-
)
|
|
64
|
-
expect(html).toContain('No se pudieron actualizar los totales')
|
|
65
|
-
expect(html).toContain('Reintentar')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('omits an absent line image without emitting an empty src', () => {
|
|
69
|
-
const html = renderToStaticMarkup(
|
|
70
|
-
<ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
|
|
71
|
-
)
|
|
72
|
-
expect(html).not.toMatch(/src=""/)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('does not fabricate a delivery estimate or express payment controls', () => {
|
|
76
|
-
const html = renderToStaticMarkup(
|
|
77
|
-
<ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
|
|
78
|
-
)
|
|
79
|
-
expect(html).not.toContain('Entrega estimada')
|
|
80
|
-
expect(html).not.toContain('Apple Pay')
|
|
81
|
-
expect(html).not.toContain('Google Pay')
|
|
82
|
-
expect(html).not.toContain('Compra rápida')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
it('never renders a fake zero for absent or malformed required totals', () => {
|
|
86
|
-
const html = renderToStaticMarkup(
|
|
87
|
-
<ShoppingCart4
|
|
88
|
-
items={items}
|
|
89
|
-
promotions={[]}
|
|
90
|
-
totals={{
|
|
91
|
-
subtotal: { amount: '', currencyCode: 'USD' },
|
|
92
|
-
total: { amount: 'abc', currencyCode: 'USD' },
|
|
93
|
-
}}
|
|
94
|
-
formatMoney={formatMoney}
|
|
95
|
-
/>,
|
|
96
|
-
)
|
|
97
|
-
expect(html).not.toContain('USD 0')
|
|
98
|
-
expect(html).toContain('Totales no disponibles')
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('preserves a legitimate zero total', () => {
|
|
102
|
-
const html = renderToStaticMarkup(
|
|
103
|
-
<ShoppingCart4
|
|
104
|
-
items={items}
|
|
105
|
-
promotions={[]}
|
|
106
|
-
totals={{
|
|
107
|
-
subtotal: { amount: '0', currencyCode: 'USD' },
|
|
108
|
-
total: { amount: '0', currencyCode: 'USD' },
|
|
109
|
-
}}
|
|
110
|
-
formatMoney={formatMoney}
|
|
111
|
-
/>,
|
|
112
|
-
)
|
|
113
|
-
expect(html).toContain('USD 0')
|
|
114
|
-
expect(html).not.toContain('Totales no disponibles')
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('shows an unavailable marker (not zero) for a supplied but malformed line total', () => {
|
|
118
|
-
const html = renderToStaticMarkup(
|
|
119
|
-
<ShoppingCart4
|
|
120
|
-
items={[{ id: 'line_x', name: 'Pieza', price: 10, quantity: 1, lineTotal: { amount: '', currencyCode: 'USD' } }]}
|
|
121
|
-
promotions={[]}
|
|
122
|
-
totals={totals}
|
|
123
|
-
formatMoney={formatMoney}
|
|
124
|
-
/>,
|
|
125
|
-
)
|
|
126
|
-
expect(html).toContain('—')
|
|
127
|
-
expect(html).not.toContain('USD 0')
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('renders absolute quantities from props (cart contract, not a delta)', () => {
|
|
131
|
-
const html = renderToStaticMarkup(
|
|
132
|
-
<ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
|
|
133
|
-
)
|
|
134
|
-
expect(html).toContain('>2<')
|
|
135
|
-
expect(html).toContain('>1<')
|
|
136
|
-
})
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
describe('ShoppingCart4 — presentation and action availability', () => {
|
|
140
|
-
it('defaults to the compact collapsible card', () => {
|
|
141
|
-
const html = renderToStaticMarkup(
|
|
142
|
-
<ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
|
|
143
|
-
)
|
|
144
|
-
expect(html).toContain('max-w-md')
|
|
145
|
-
expect(html).toContain('Contraer carrito')
|
|
146
|
-
expect(html).toContain('max-h-64')
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('renders page presentation as a wide, always-expanded review with a heading', () => {
|
|
150
|
-
const html = renderToStaticMarkup(
|
|
151
|
-
<ShoppingCart4
|
|
152
|
-
items={items}
|
|
153
|
-
promotions={[]}
|
|
154
|
-
totals={totals}
|
|
155
|
-
formatMoney={formatMoney}
|
|
156
|
-
presentation='page'
|
|
157
|
-
/>,
|
|
158
|
-
)
|
|
159
|
-
expect(html).toContain('max-w-3xl')
|
|
160
|
-
expect(html).toContain('<h1')
|
|
161
|
-
expect(html).not.toContain('Contraer carrito')
|
|
162
|
-
expect(html).not.toContain('max-h-64')
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('offers a continue-shopping action in page presentation', () => {
|
|
166
|
-
const html = renderToStaticMarkup(
|
|
167
|
-
<ShoppingCart4
|
|
168
|
-
items={items}
|
|
169
|
-
promotions={[]}
|
|
170
|
-
totals={totals}
|
|
171
|
-
formatMoney={formatMoney}
|
|
172
|
-
presentation='page'
|
|
173
|
-
onContinueShopping={() => {}}
|
|
174
|
-
/>,
|
|
175
|
-
)
|
|
176
|
-
expect(html).toContain('Seguir comprando')
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('suppresses checkout when the checkout feature is unavailable', () => {
|
|
180
|
-
const html = renderToStaticMarkup(
|
|
181
|
-
<ShoppingCart4
|
|
182
|
-
items={items}
|
|
183
|
-
promotions={[]}
|
|
184
|
-
totals={totals}
|
|
185
|
-
formatMoney={formatMoney}
|
|
186
|
-
presentation='page'
|
|
187
|
-
checkoutUnavailable
|
|
188
|
-
/>,
|
|
189
|
-
)
|
|
190
|
-
expect(html).not.toContain('Ver carrito y finalizar')
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('disables checkout while the cart is not ready', () => {
|
|
194
|
-
const html = renderToStaticMarkup(
|
|
195
|
-
<ShoppingCart4
|
|
196
|
-
items={items}
|
|
197
|
-
promotions={[]}
|
|
198
|
-
totals={totals}
|
|
199
|
-
formatMoney={formatMoney}
|
|
200
|
-
presentation='page'
|
|
201
|
-
checkoutDisabled
|
|
202
|
-
/>,
|
|
203
|
-
)
|
|
204
|
-
expect(html).toMatch(/<button[^>]*disabled[^>]*>[^<]*Ver carrito y finalizar/)
|
|
205
|
-
})
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
describe('ShoppingCart4 — accessibility', () => {
|
|
209
|
-
it('names every line mutation control', () => {
|
|
210
|
-
const html = renderToStaticMarkup(
|
|
211
|
-
<ShoppingCart4 items={items} promotions={[]} totals={totals} formatMoney={formatMoney} />,
|
|
212
|
-
)
|
|
213
|
-
expect(html).toContain('aria-label="Disminuir cantidad, Taladro"')
|
|
214
|
-
expect(html).toContain('aria-label="Aumentar cantidad, Taladro"')
|
|
215
|
-
expect(html).toContain('aria-label="Quitar Taladro"')
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('announces pending totals politely and errors as an alert', () => {
|
|
219
|
-
const pending = renderToStaticMarkup(
|
|
220
|
-
<ShoppingCart4 items={items} promotions={[]} totalsState='pending' />,
|
|
221
|
-
)
|
|
222
|
-
expect(pending).toContain('aria-live="polite"')
|
|
223
|
-
expect(pending).toContain('Actualizando totales')
|
|
224
|
-
const error = renderToStaticMarkup(
|
|
225
|
-
<ShoppingCart4 items={items} promotions={[]} totalsState='error' />,
|
|
226
|
-
)
|
|
227
|
-
expect(error).toMatch(/role="alert"/)
|
|
228
|
-
})
|
|
229
|
-
})
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import StorefrontHero1, { type FeaturedProduct } from './storefront-hero-1'
|
|
4
|
-
|
|
5
|
-
const product: FeaturedProduct = {
|
|
6
|
-
id: 'p1',
|
|
7
|
-
name: 'Taladro percutor',
|
|
8
|
-
brand: 'INGCO',
|
|
9
|
-
price: 69.99,
|
|
10
|
-
originalPrice: 99.99,
|
|
11
|
-
rating: 4.8,
|
|
12
|
-
reviewCount: 236,
|
|
13
|
-
inStock: true,
|
|
14
|
-
isNew: false,
|
|
15
|
-
isOnSale: true,
|
|
16
|
-
image: '/products/drill.png',
|
|
17
|
-
colors: ['#111111'],
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const base = {
|
|
21
|
-
title: 'Potencia para cada proyecto',
|
|
22
|
-
description: 'Herramientas profesionales',
|
|
23
|
-
products: [],
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe('StorefrontHero1 — optional editorial image', () => {
|
|
27
|
-
it('renders no editorial image when none is supplied', () => {
|
|
28
|
-
const html = renderToStaticMarkup(<StorefrontHero1 {...base} />)
|
|
29
|
-
expect(html).not.toContain('<img')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('renders a native editorial image with its src and alt text', () => {
|
|
33
|
-
const html = renderToStaticMarkup(
|
|
34
|
-
<StorefrontHero1 {...base} heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico y accesorios' }} />,
|
|
35
|
-
)
|
|
36
|
-
expect(html).toContain('src="/hero-tools.png"')
|
|
37
|
-
expect(html).toContain('alt="Taladro inalámbrico y accesorios"')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('never renders an empty image src', () => {
|
|
41
|
-
const blank = renderToStaticMarkup(
|
|
42
|
-
<StorefrontHero1 {...base} heroImage={{ src: ' ', alt: 'Vacío' }} />,
|
|
43
|
-
)
|
|
44
|
-
expect(blank).not.toContain('<img')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('preserves the product grid when the editorial image is omitted', () => {
|
|
48
|
-
const html = renderToStaticMarkup(<StorefrontHero1 {...base} products={[product]} />)
|
|
49
|
-
expect(html).toContain('Taladro percutor')
|
|
50
|
-
expect(html).toContain('src="/products/drill.png"')
|
|
51
|
-
expect(html).not.toContain('/hero-tools.png')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('keeps the product grid when the editorial image is supplied', () => {
|
|
55
|
-
const html = renderToStaticMarkup(
|
|
56
|
-
<StorefrontHero1
|
|
57
|
-
{...base}
|
|
58
|
-
products={[product]}
|
|
59
|
-
heroImage={{ src: '/hero-tools.png', alt: 'Taladro inalámbrico' }}
|
|
60
|
-
/>,
|
|
61
|
-
)
|
|
62
|
-
expect(html).toContain('src="/hero-tools.png"')
|
|
63
|
-
expect(html).toContain('Taladro percutor')
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
describe('StorefrontHero1 — product card secondary action label', () => {
|
|
68
|
-
it('defaults the second product-card action to "Ver producto"', () => {
|
|
69
|
-
const html = renderToStaticMarkup(<StorefrontHero1 {...base} products={[product]} />)
|
|
70
|
-
expect(html).toContain('Ver producto')
|
|
71
|
-
})
|
|
72
|
-
})
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { motion, useReducedMotion } from 'motion/react'
|
|
4
|
-
|
|
5
|
-
import { Badge } from '../../../ui/badge'
|
|
6
|
-
import { Button } from '../../../ui/button'
|
|
7
|
-
import { ShoppingBag, Star, ArrowRight } from 'lucide-react'
|
|
8
|
-
import type { ComponentProps, ReactNode } from 'react'
|
|
9
|
-
import type { CTA } from '../../../lib/types'
|
|
10
|
-
|
|
11
|
-
export interface FeaturedProduct {
|
|
12
|
-
id: string
|
|
13
|
-
name: string
|
|
14
|
-
brand: string
|
|
15
|
-
price: number
|
|
16
|
-
originalPrice: number
|
|
17
|
-
rating: number
|
|
18
|
-
reviewCount: number
|
|
19
|
-
inStock: boolean
|
|
20
|
-
isNew: boolean
|
|
21
|
-
isOnSale: boolean
|
|
22
|
-
image: string
|
|
23
|
-
colors: string[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface HeroImage {
|
|
27
|
-
src: string
|
|
28
|
-
alt: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface StorefrontHero1Props {
|
|
32
|
-
title: string
|
|
33
|
-
description: string
|
|
34
|
-
products: FeaturedProduct[]
|
|
35
|
-
titleAccent?: string
|
|
36
|
-
badge?: string
|
|
37
|
-
primaryCta?: CTA
|
|
38
|
-
secondaryCta?: CTA
|
|
39
|
-
/**
|
|
40
|
-
* Optional editorial image. Rendered as a native `<img>` when `src` is a
|
|
41
|
-
* non-empty string; omitted entirely when absent or blank (never an empty
|
|
42
|
-
* `src`). The product grid is unaffected.
|
|
43
|
-
*/
|
|
44
|
-
heroImage?: HeroImage
|
|
45
|
-
onAddToCart?: (product: FeaturedProduct) => void
|
|
46
|
-
onBuyNow?: (product: FeaturedProduct) => void
|
|
47
|
-
onSelectColor?: (product: FeaturedProduct, index: number, color: string) => void
|
|
48
|
-
addToCartLabel?: string
|
|
49
|
-
buyNowLabel?: string
|
|
50
|
-
newLabel?: string
|
|
51
|
-
outOfStockLabel?: string
|
|
52
|
-
colorsLabel?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function CtaButton({
|
|
56
|
-
cta,
|
|
57
|
-
variant,
|
|
58
|
-
children,
|
|
59
|
-
}: {
|
|
60
|
-
cta: CTA
|
|
61
|
-
variant?: ComponentProps<typeof Button>['variant']
|
|
62
|
-
children: ReactNode
|
|
63
|
-
}) {
|
|
64
|
-
if (cta.href) {
|
|
65
|
-
return (
|
|
66
|
-
<Button
|
|
67
|
-
variant={variant}
|
|
68
|
-
size='lg'
|
|
69
|
-
className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
|
|
70
|
-
asChild
|
|
71
|
-
>
|
|
72
|
-
<a href={cta.href}>{children}</a>
|
|
73
|
-
</Button>
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<Button
|
|
79
|
-
variant={variant}
|
|
80
|
-
size='lg'
|
|
81
|
-
className='group relative cursor-pointer overflow-hidden px-8 py-6 text-base font-medium'
|
|
82
|
-
>
|
|
83
|
-
{children}
|
|
84
|
-
</Button>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function StorefrontHero1({
|
|
89
|
-
title,
|
|
90
|
-
description,
|
|
91
|
-
products,
|
|
92
|
-
titleAccent,
|
|
93
|
-
badge,
|
|
94
|
-
primaryCta,
|
|
95
|
-
secondaryCta,
|
|
96
|
-
heroImage,
|
|
97
|
-
onAddToCart,
|
|
98
|
-
onBuyNow,
|
|
99
|
-
onSelectColor,
|
|
100
|
-
addToCartLabel = 'Agregar al carrito',
|
|
101
|
-
buyNowLabel = 'Ver producto',
|
|
102
|
-
newLabel = 'Nuevo',
|
|
103
|
-
outOfStockLabel = 'Agotado',
|
|
104
|
-
colorsLabel = 'Colores:',
|
|
105
|
-
}: StorefrontHero1Props) {
|
|
106
|
-
const reduceMotion = useReducedMotion()
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<div className='from-primary/5 to-background relative overflow-hidden bg-gradient-to-br'>
|
|
110
|
-
<div className='relative z-10 mx-auto w-full max-w-7xl flex flex-col items-center justify-between gap-12 px-4 sm:px-6 lg:px-8 py-16 lg:py-20 xl:flex-row xl:items-start'>
|
|
111
|
-
<motion.div
|
|
112
|
-
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
113
|
-
animate={{ opacity: 1, y: 0 }}
|
|
114
|
-
transition={{ duration: 0.5, ease: 'easeOut' }}
|
|
115
|
-
className='mx-auto max-w-xl text-center lg:mx-0 xl:text-left'
|
|
116
|
-
>
|
|
117
|
-
{badge && (
|
|
118
|
-
<div className='border-primary/20 bg-primary/5 text-primary inline-flex items-center rounded-full border px-4 py-1.5 text-sm font-medium backdrop-blur-sm'>
|
|
119
|
-
{badge}
|
|
120
|
-
</div>
|
|
121
|
-
)}
|
|
122
|
-
<h1 className='text-foreground mt-8 text-4xl font-bold tracking-tight sm:text-5xl md:text-7xl'>
|
|
123
|
-
{title}{' '}
|
|
124
|
-
{titleAccent && (
|
|
125
|
-
<span className='from-primary to-primary/70 bg-gradient-to-r bg-clip-text text-transparent'>
|
|
126
|
-
{titleAccent}
|
|
127
|
-
</span>
|
|
128
|
-
)}
|
|
129
|
-
</h1>
|
|
130
|
-
<p className='text-muted-foreground mt-8 text-lg'>{description}</p>
|
|
131
|
-
{(primaryCta || secondaryCta) && (
|
|
132
|
-
<div className='mt-10 flex flex-col gap-4 sm:flex-row sm:justify-center xl:justify-start'>
|
|
133
|
-
{primaryCta && (
|
|
134
|
-
<CtaButton cta={primaryCta}>
|
|
135
|
-
<span className='relative z-10 flex items-center gap-2'>
|
|
136
|
-
{primaryCta.label}
|
|
137
|
-
<ArrowRight className='transition-transform group-hover:translate-x-1' />
|
|
138
|
-
</span>
|
|
139
|
-
<span className='bg-primary/5 absolute inset-0 -z-0 opacity-0 transition-opacity group-hover:opacity-100'></span>
|
|
140
|
-
</CtaButton>
|
|
141
|
-
)}
|
|
142
|
-
{secondaryCta && (
|
|
143
|
-
<CtaButton cta={secondaryCta} variant='outline'>
|
|
144
|
-
<span className='relative z-10 flex items-center gap-2'>
|
|
145
|
-
<ShoppingBag />
|
|
146
|
-
{secondaryCta.label}
|
|
147
|
-
</span>
|
|
148
|
-
</CtaButton>
|
|
149
|
-
)}
|
|
150
|
-
</div>
|
|
151
|
-
)}
|
|
152
|
-
{heroImage && heroImage.src.trim().length > 0 ? (
|
|
153
|
-
<img
|
|
154
|
-
src={heroImage.src}
|
|
155
|
-
alt={heroImage.alt}
|
|
156
|
-
className='mt-10 aspect-video w-full rounded-lg object-cover'
|
|
157
|
-
/>
|
|
158
|
-
) : null}
|
|
159
|
-
</motion.div>
|
|
160
|
-
|
|
161
|
-
<div className='w-full max-w-2xl lg:mt-0'>
|
|
162
|
-
<div className='grid gap-6 sm:grid-cols-2'>
|
|
163
|
-
{products.map((product, index) => (
|
|
164
|
-
<motion.div
|
|
165
|
-
key={product.id}
|
|
166
|
-
initial={reduceMotion ? false : { opacity: 0, y: 24 }}
|
|
167
|
-
whileInView={{ opacity: 1, y: 0 }}
|
|
168
|
-
viewport={{ once: true, margin: '-40px' }}
|
|
169
|
-
transition={{
|
|
170
|
-
duration: 0.45,
|
|
171
|
-
delay: reduceMotion ? 0 : index * 0.1,
|
|
172
|
-
ease: 'easeOut',
|
|
173
|
-
}}
|
|
174
|
-
className='bg-card relative h-full overflow-hidden rounded-lg border shadow-sm transition-shadow hover:shadow-md'
|
|
175
|
-
>
|
|
176
|
-
<div className='absolute top-4 left-4 z-10 flex flex-col gap-2'>
|
|
177
|
-
{product.isNew && (
|
|
178
|
-
<Badge variant='secondary' className='rounded-full'>{newLabel}</Badge>
|
|
179
|
-
)}
|
|
180
|
-
{product.isOnSale && (
|
|
181
|
-
<Badge variant='destructive' className='rounded-full'>
|
|
182
|
-
{Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)}% OFF
|
|
183
|
-
</Badge>
|
|
184
|
-
)}
|
|
185
|
-
</div>
|
|
186
|
-
|
|
187
|
-
<div className='relative aspect-square overflow-hidden'>
|
|
188
|
-
<img
|
|
189
|
-
src={product.image || undefined}
|
|
190
|
-
alt={`${product.brand} ${product.name}`}
|
|
191
|
-
className='size-full object-cover'
|
|
192
|
-
loading='lazy'
|
|
193
|
-
/>
|
|
194
|
-
{!product.inStock && (
|
|
195
|
-
<div className='absolute inset-0 flex items-center justify-center bg-black/50'>
|
|
196
|
-
<span className='rounded bg-white/90 px-3 py-1 text-sm font-medium'>{outOfStockLabel}</span>
|
|
197
|
-
</div>
|
|
198
|
-
)}
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
<div className='p-4'>
|
|
202
|
-
<div className='text-muted-foreground mb-1 text-sm font-medium'>{product.brand}</div>
|
|
203
|
-
<h3 className='mb-2 font-medium'>{product.name}</h3>
|
|
204
|
-
|
|
205
|
-
<div className='mb-3'>
|
|
206
|
-
<span className='text-lg font-bold'>${product.price.toFixed(2)}</span>
|
|
207
|
-
{product.isOnSale && (
|
|
208
|
-
<span className='text-muted-foreground ms-2 text-sm line-through'>
|
|
209
|
-
${product.originalPrice.toFixed(2)}
|
|
210
|
-
</span>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<div className='mb-4 flex items-center'>
|
|
215
|
-
<span className='text-muted-foreground me-2 text-sm'>{colorsLabel}</span>
|
|
216
|
-
<div className='flex gap-1'>
|
|
217
|
-
{product.colors.map((color, i) => (
|
|
218
|
-
<button
|
|
219
|
-
key={i}
|
|
220
|
-
className='size-5 rounded-full border'
|
|
221
|
-
style={{ backgroundColor: color }}
|
|
222
|
-
aria-label={`Opción de color ${i + 1}`}
|
|
223
|
-
onClick={onSelectColor ? () => onSelectColor(product, i, product.colors[i]) : undefined}
|
|
224
|
-
/>
|
|
225
|
-
))}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
|
|
229
|
-
<div className='mb-4 flex items-center'>
|
|
230
|
-
<div className='flex'>
|
|
231
|
-
{[...Array(5)].map((_, i) => (
|
|
232
|
-
<Star
|
|
233
|
-
key={i}
|
|
234
|
-
className={`size-4 ${i < Math.floor(product.rating) ? 'fill-foreground text-foreground' : 'text-muted-foreground'}`}
|
|
235
|
-
/>
|
|
236
|
-
))}
|
|
237
|
-
</div>
|
|
238
|
-
<span className='text-muted-foreground ms-2 text-sm'>({product.reviewCount.toLocaleString()})</span>
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
<div className='flex flex-col gap-2'>
|
|
242
|
-
<Button variant='outline' className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onAddToCart ? () => onAddToCart(product) : undefined}>
|
|
243
|
-
<ShoppingBag />
|
|
244
|
-
{addToCartLabel}
|
|
245
|
-
</Button>
|
|
246
|
-
<Button className="h-9 px-4 py-2 cursor-pointer" disabled={!product.inStock} onClick={onBuyNow ? () => onBuyNow(product) : undefined}>
|
|
247
|
-
{buyNowLabel}
|
|
248
|
-
</Button>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
</motion.div>
|
|
252
|
-
))}
|
|
253
|
-
</div>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
|
|
257
|
-
<div className='bg-primary/10 absolute -top-32 -right-32 size-96 rounded-full blur-3xl' />
|
|
258
|
-
<div className='bg-secondary/30 absolute -bottom-32 -left-32 size-96 rounded-full blur-3xl' />
|
|
259
|
-
</div>
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export default StorefrontHero1
|