@thorprovider/blocks 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +15 -15
- package/.turbo/turbo-test.log +32 -5
- package/AGENTS.md +1 -1
- package/README.md +52 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/contracts.d.mts +1111 -90
- package/dist/contracts.d.ts +1111 -90
- package/dist/contracts.js +189 -6
- package/dist/contracts.js.map +1 -1
- package/dist/contracts.mjs +173 -7
- package/dist/contracts.mjs.map +1 -1
- package/dist/index.d.mts +467 -89
- package/dist/index.d.ts +467 -89
- package/dist/index.js +2967 -2250
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2989 -2282
- package/dist/index.mjs.map +1 -1
- package/docs/standards/theming.md +32 -5
- package/package.json +4 -3
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +136 -93
- package/src/blocks/e-commerce/category-filters/category-filter-2.test.tsx +513 -0
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +728 -148
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation-fulfilment-progress.tsx +14 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.test.tsx +136 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-confirmation.tsx +145 -99
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +215 -102
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +6 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-shipping-step.tsx +51 -2
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +216 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +114 -11
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.test.tsx +42 -2
- package/src/blocks/e-commerce/navigation/mobile-category-drawer.tsx +10 -8
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +51 -0
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +56 -36
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +182 -0
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +298 -129
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +137 -0
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.tsx +231 -89
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +65 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +19 -0
- package/src/blocks/marketing/footers/footer-section-1.test.tsx +117 -0
- package/src/blocks/marketing/footers/footer-section-1.tsx +46 -9
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.test.tsx +54 -0
- package/src/blocks/marketing/navbars/navbar-4-cart-sheet.tsx +43 -13
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +112 -0
- package/src/blocks/marketing/navbars/navbar-4.tsx +101 -28
- package/src/index.ts +38 -0
- package/src/lib/cart.test.ts +15 -0
- package/src/lib/cart.ts +11 -0
- package/src/lib/contracts-curated.test.ts +207 -0
- package/src/lib/contracts-server-boundary.test.ts +52 -0
- package/src/lib/contracts.ts +182 -4
- package/src/lib/money.test.ts +64 -0
- package/src/lib/money.ts +65 -0
- package/styles.css +1 -0
- package/tsup.config.ts +0 -1
- package/vitest.config.ts +9 -1
|
@@ -9,7 +9,12 @@ export interface ProductCategory2Category {
|
|
|
9
9
|
handle?: string
|
|
10
10
|
name: string
|
|
11
11
|
description: string
|
|
12
|
-
|
|
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
|
|
13
18
|
image: string
|
|
14
19
|
trending: boolean
|
|
15
20
|
}
|
|
@@ -22,6 +27,18 @@ export interface ProductCategory2Props {
|
|
|
22
27
|
onBrowse?: (category: ProductCategory2Category) => void
|
|
23
28
|
}
|
|
24
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
|
+
|
|
25
42
|
export function ProductCategory2({
|
|
26
43
|
title,
|
|
27
44
|
description,
|
|
@@ -42,44 +59,47 @@ export function ProductCategory2({
|
|
|
42
59
|
)}
|
|
43
60
|
|
|
44
61
|
<div className='grid gap-6 sm:grid-cols-2 lg:grid-cols-3'>
|
|
45
|
-
{categories.map(category =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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' />
|
|
57
76
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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}
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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>
|
|
78
98
|
</div>
|
|
79
99
|
</div>
|
|
80
|
-
</
|
|
81
|
-
|
|
82
|
-
)
|
|
100
|
+
</Card>
|
|
101
|
+
)
|
|
102
|
+
})}
|
|
83
103
|
</div>
|
|
84
104
|
|
|
85
105
|
{cta ? (
|
|
@@ -97,4 +117,4 @@ export function ProductCategory2({
|
|
|
97
117
|
)
|
|
98
118
|
}
|
|
99
119
|
|
|
100
|
-
export default ProductCategory2
|
|
120
|
+
export default ProductCategory2
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import ProductOverview1, {
|
|
4
|
+
galleryImages,
|
|
5
|
+
resolveAddToCartPayload,
|
|
6
|
+
variantLabelOf,
|
|
7
|
+
type ProductDetailProduct,
|
|
8
|
+
} from './product-overview-1'
|
|
9
|
+
|
|
10
|
+
const product: ProductDetailProduct = {
|
|
11
|
+
id: 'p1',
|
|
12
|
+
handle: 'taladro',
|
|
13
|
+
title: 'Taladro inalámbrico',
|
|
14
|
+
description: 'Un taladro',
|
|
15
|
+
vendor: 'Acme',
|
|
16
|
+
options: [{ id: 'o1', name: 'Color', values: ['Rojo', 'Azul'] }],
|
|
17
|
+
variants: [
|
|
18
|
+
{
|
|
19
|
+
id: 'v-red',
|
|
20
|
+
title: 'Rojo',
|
|
21
|
+
availableForSale: true,
|
|
22
|
+
selectedOptions: [{ name: 'Color', value: 'Rojo' }],
|
|
23
|
+
price: { amount: '10.00', currencyCode: 'USD' },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'v-blue',
|
|
27
|
+
title: 'Azul',
|
|
28
|
+
availableForSale: true,
|
|
29
|
+
selectedOptions: [{ name: 'Color', value: 'Azul' }],
|
|
30
|
+
price: { amount: '25.50', currencyCode: 'USD' },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'v-sold',
|
|
34
|
+
title: 'Verde',
|
|
35
|
+
availableForSale: false,
|
|
36
|
+
selectedOptions: [{ name: 'Color', value: 'Verde' }],
|
|
37
|
+
price: { amount: '9.00', currencyCode: 'USD' },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'v-noprice',
|
|
41
|
+
title: 'Gris',
|
|
42
|
+
availableForSale: true,
|
|
43
|
+
selectedOptions: [{ name: 'Color', value: 'Gris' }],
|
|
44
|
+
price: null,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'v-free',
|
|
48
|
+
title: 'Blanco',
|
|
49
|
+
availableForSale: true,
|
|
50
|
+
selectedOptions: [{ name: 'Color', value: 'Blanco' }],
|
|
51
|
+
price: { amount: '0', currencyCode: 'USD' },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
images: [],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const formatPrice = (money: { amount: string; currencyCode: string }) =>
|
|
58
|
+
`${money.currencyCode} ${money.amount}`
|
|
59
|
+
|
|
60
|
+
describe('ProductOverview1 — explicit, controlled variant selection', () => {
|
|
61
|
+
it('does not default to the first variant and prompts for an explicit selection', () => {
|
|
62
|
+
const html = renderToStaticMarkup(
|
|
63
|
+
<ProductOverview1 product={product} formatPrice={formatPrice} onAddToCart={() => {}} />,
|
|
64
|
+
)
|
|
65
|
+
expect(html).toContain('Selecciona una variante')
|
|
66
|
+
expect(html).not.toContain('USD 10.00')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('shows the selected non-default variant at its current price', () => {
|
|
70
|
+
const html = renderToStaticMarkup(
|
|
71
|
+
<ProductOverview1
|
|
72
|
+
product={product}
|
|
73
|
+
selectedVariantId='v-blue'
|
|
74
|
+
formatPrice={formatPrice}
|
|
75
|
+
onAddToCart={() => {}}
|
|
76
|
+
/>,
|
|
77
|
+
)
|
|
78
|
+
expect(html).toContain('USD 25.50')
|
|
79
|
+
expect(html).not.toContain('USD 10.00')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('reports a missing price as unavailable instead of zero', () => {
|
|
83
|
+
const html = renderToStaticMarkup(
|
|
84
|
+
<ProductOverview1 product={product} selectedVariantId='v-noprice' formatPrice={formatPrice} />,
|
|
85
|
+
)
|
|
86
|
+
expect(html).toContain('Precio no disponible')
|
|
87
|
+
expect(html).not.toContain('USD 0')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('preserves a backend-resolved valid zero', () => {
|
|
91
|
+
const html = renderToStaticMarkup(
|
|
92
|
+
<ProductOverview1 product={product} selectedVariantId='v-free' formatPrice={formatPrice} />,
|
|
93
|
+
)
|
|
94
|
+
expect(html).toContain('USD 0')
|
|
95
|
+
expect(html).not.toContain('Precio no disponible')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('enables purchase only for an available, priced, explicitly selected variant', () => {
|
|
99
|
+
const onlyAvailable: ProductDetailProduct = {
|
|
100
|
+
...product,
|
|
101
|
+
variants: [product.variants[0]],
|
|
102
|
+
}
|
|
103
|
+
const enabled = renderToStaticMarkup(
|
|
104
|
+
<ProductOverview1
|
|
105
|
+
product={onlyAvailable}
|
|
106
|
+
selectedVariantId='v-red'
|
|
107
|
+
formatPrice={formatPrice}
|
|
108
|
+
onAddToCart={() => {}}
|
|
109
|
+
/>,
|
|
110
|
+
)
|
|
111
|
+
expect(enabled).not.toContain('disabled=""')
|
|
112
|
+
|
|
113
|
+
const noSelection = renderToStaticMarkup(
|
|
114
|
+
<ProductOverview1 product={onlyAvailable} formatPrice={formatPrice} onAddToCart={() => {}} />,
|
|
115
|
+
)
|
|
116
|
+
expect(noSelection).toContain('disabled=""')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('ProductOverview1 — truthful optional data', () => {
|
|
121
|
+
it('renders no rating, delivery estimate, or shipping when the backend supplies none', () => {
|
|
122
|
+
const html = renderToStaticMarkup(<ProductOverview1 product={product} formatPrice={formatPrice} />)
|
|
123
|
+
expect(html).not.toContain('Reseñas')
|
|
124
|
+
expect(html).not.toContain('Entrega estimada')
|
|
125
|
+
expect(html).not.toContain('Envío')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders rating, delivery estimate, and shipping only when supplied', () => {
|
|
129
|
+
const html = renderToStaticMarkup(
|
|
130
|
+
<ProductOverview1
|
|
131
|
+
product={product}
|
|
132
|
+
formatPrice={formatPrice}
|
|
133
|
+
rating={{ value: 4, count: 12 }}
|
|
134
|
+
deliveryEstimate='3-5 días'
|
|
135
|
+
shipping={{ amount: '4.99', currencyCode: 'USD' }}
|
|
136
|
+
/>,
|
|
137
|
+
)
|
|
138
|
+
expect(html).toContain('Reseñas')
|
|
139
|
+
expect(html).toContain('3-5 días')
|
|
140
|
+
expect(html).toContain('USD 4.99')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('never emits an empty image src when the product has no images', () => {
|
|
144
|
+
const html = renderToStaticMarkup(<ProductOverview1 product={product} formatPrice={formatPrice} />)
|
|
145
|
+
expect(html).not.toMatch(/src=""/)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('resolveAddToCartPayload', () => {
|
|
150
|
+
const variant = product.variants[1]
|
|
151
|
+
|
|
152
|
+
it('returns null when no variant is selected (no first-variant shortcut)', () => {
|
|
153
|
+
expect(resolveAddToCartPayload(null, 2)).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('uses the selected backend variant id and defaults the quantity to 1', () => {
|
|
157
|
+
expect(resolveAddToCartPayload(variant, undefined)).toEqual({ variantId: 'v-blue', quantity: 1 })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('carries the requested quantity', () => {
|
|
161
|
+
expect(resolveAddToCartPayload(variant, 3)).toEqual({ variantId: 'v-blue', quantity: 3 })
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('variant helpers', () => {
|
|
166
|
+
it('prefers the variant title', () => {
|
|
167
|
+
expect(variantLabelOf(product.variants[0])).toBe('Rojo')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('falls back to option values when a variant has no title', () => {
|
|
171
|
+
expect(variantLabelOf({ ...product.variants[0], title: '' })).toBe('Rojo')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('uses the product images when present and never a fabricated URL', () => {
|
|
175
|
+
const withImages: ProductDetailProduct = {
|
|
176
|
+
...product,
|
|
177
|
+
images: [{ url: 'https://cdn.example/p1.jpg', altText: 'p1' }],
|
|
178
|
+
}
|
|
179
|
+
expect(galleryImages(withImages, 'FALLBACK')).toEqual(['https://cdn.example/p1.jpg'])
|
|
180
|
+
expect(galleryImages(product, 'FALLBACK')).toEqual(['FALLBACK'])
|
|
181
|
+
})
|
|
182
|
+
})
|