@rudra-js/react 0.7.3 → 0.7.5
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/README.md +15 -7
- package/dist/blocks/block-renderers.d.ts +5 -10
- package/dist/blocks/block-renderers.d.ts.map +1 -1
- package/dist/blocks/block-renderers.js +15 -2
- package/dist/blocks/block-renderers.js.map +1 -1
- package/dist/blocks/product-card.d.ts +0 -8
- package/dist/blocks/product-card.d.ts.map +1 -1
- package/dist/blocks/product-card.js +1 -11
- package/dist/blocks/product-card.js.map +1 -1
- package/dist/registry.js +1 -2
- package/dist/registry.js.map +1 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +12 -16
- package/dist/render-context.js.map +1 -1
- package/dist/rudra-component.d.ts +0 -43
- package/dist/rudra-component.d.ts.map +1 -1
- package/dist/rudra-component.js +11 -32
- package/dist/rudra-component.js.map +1 -1
- package/package.json +3 -3
- package/src/blocks/block-renderers.tsx +51 -17
- package/src/blocks/product-card.tsx +3 -13
- package/src/registry.ts +1 -1
- package/src/render-context.ts +12 -20
- package/src/rudra-component.tsx +14 -75
- package/dist/blocks/bundle-block.d.ts +0 -7
- package/dist/blocks/bundle-block.d.ts.map +0 -1
- package/dist/blocks/bundle-block.js +0 -17
- package/dist/blocks/bundle-block.js.map +0 -1
- package/src/blocks/bundle-block.tsx +0 -43
package/README.md
CHANGED
|
@@ -16,13 +16,13 @@ still reads it.
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
npm install @rudra-js/react @rudra-js/core react
|
|
19
|
+
npm install @rudra-js/react @rudra-js/core @rudra-js/attested react
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Both `@rudra-js/core` and `react` are peer dependencies. The specification you pass in comes from
|
|
23
23
|
your copy of core, and the elements this renders have to come from the same React your app renders.
|
|
24
24
|
With two copies of either, you'd get a spec that fails its own type check, or a component tree React
|
|
25
|
-
refuses to render.
|
|
25
|
+
refuses to render. `@rudra-js/attested` is core's peer: npm and pnpm add it for you, Yarn doesn't.
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
28
|
import { RudraComponent } from '@rudra-js/react';
|
|
@@ -69,8 +69,9 @@ later and may be the fresher of the two.
|
|
|
69
69
|
## Styling
|
|
70
70
|
|
|
71
71
|
We ship no CSS. A stylesheet of ours would only fight whatever your site already has. So out of the
|
|
72
|
-
box the block renders as a run-on line — every card element is inline, so
|
|
73
|
-
|
|
72
|
+
box the block renders as a run-on line — every card element is inline, so a card's parts sit on one
|
|
73
|
+
line with spaces between them, and nothing separates one card from the next. That's the starting
|
|
74
|
+
point you style from.
|
|
74
75
|
|
|
75
76
|
Check out `examples/shop/public/demo-styles.css`. It's a working stylesheet written against nothing
|
|
76
77
|
but the table below, so copy it as a starting point rather than as a supported API. The example shop applies it by default, and `?styles=off`
|
|
@@ -98,6 +99,12 @@ keep working.
|
|
|
98
99
|
here uses JavaScript to scroll it for you. `.rudra-card--featured` is applied alongside
|
|
99
100
|
`.rudra-card`, so write `.rudra-card--featured { ... }` after the base rule and let it layer on top.
|
|
100
101
|
|
|
102
|
+
`.rudra-banner__cta` and `.rudra-bundle__cta` are plain text, not links or buttons, because there is
|
|
103
|
+
nothing for them to do. Don't style them as buttons unless a renderer of your own wraps them in one.
|
|
104
|
+
The hero's call to action sits inside the product link, so that one is clickable. Every card in a
|
|
105
|
+
carousel is a link, so tabbing through them scrolls the track, which keeps it usable by keyboard
|
|
106
|
+
even with the scrollbar hidden.
|
|
107
|
+
|
|
101
108
|
### Attributes
|
|
102
109
|
|
|
103
110
|
The same markup carries what the model decided, so you can hang styling or analytics off it.
|
|
@@ -162,9 +169,10 @@ reach the renderer, since they don't depend on the catalog.
|
|
|
162
169
|
### What `products` may be
|
|
163
170
|
|
|
164
171
|
A list of products, or anything keyed by SKU that answers `get(sku)` and `has(sku)`. A `Map` does
|
|
165
|
-
it, and so does your own index.
|
|
166
|
-
your catalog is too big to copy into a `Map` on every request, hand over
|
|
167
|
-
instead.
|
|
172
|
+
it, and so does your own index. The renderers only ever call `get`, and `has` is how we tell a keyed
|
|
173
|
+
catalog from a list, so if your catalog is too big to copy into a `Map` on every request, hand over
|
|
174
|
+
a view of your own store instead. The prop is typed as a `ReadonlyMap`, so in TypeScript either
|
|
175
|
+
implement `ReadonlyMap` or cast with `as unknown as ReadonlyMap<string, Product>`.
|
|
168
176
|
|
|
169
177
|
We look for those two methods rather than for `instanceof Map`, which is per-realm. A `Map` arriving
|
|
170
178
|
from a worker or a `node:vm` sandbox is a perfectly good catalog and fails `instanceof` anyway.
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Five of the six block renderers. The bundle one lives in `bundle-block.tsx`,
|
|
3
|
-
* next door, since it draws a set the shop picked rather than the model.
|
|
4
|
-
*
|
|
5
|
-
* Each takes a block the model produced and a context the host owns. None of
|
|
6
|
-
* them interpolate markup, and none of them read a product fact from the block
|
|
7
|
-
* — that is what keeps generated output to the role of deciding layout, order
|
|
8
|
-
* and wording.
|
|
9
|
-
*/
|
|
10
|
-
import type { BannerBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
1
|
+
import type { BannerBlock, BundleBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
11
2
|
import { type BlockRenderContext } from '../render-context.js';
|
|
12
3
|
export declare function HeroRenderer({ block, context, }: {
|
|
13
4
|
block: HeroBlock;
|
|
@@ -27,4 +18,8 @@ export declare function BannerRenderer({ block }: {
|
|
|
27
18
|
export declare function CopyRenderer({ block }: {
|
|
28
19
|
block: CopyBlock;
|
|
29
20
|
}): import("react").JSX.Element;
|
|
21
|
+
export declare function BundleRenderer({ block, context, }: {
|
|
22
|
+
block: BundleBlock;
|
|
23
|
+
context: BlockRenderContext;
|
|
24
|
+
}): import("react").JSX.Element | null;
|
|
30
25
|
//# sourceMappingURL=block-renderers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-renderers.d.ts","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"block-renderers.d.ts","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EAEV,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAGhF,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAmBA;AAED,wBAAgB,YAAY,CAAC,EAC3B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAWA;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,+BAWA;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,WAAW,CAAA;CAAE,+BAO/D;AAED,wBAAgB,YAAY,CAAC,EAAE,KAAK,EAAE,EAAE;IAAE,KAAK,EAAE,SAAS,CAAA;CAAE,+BAO3D;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCA+BA"}
|
|
@@ -3,7 +3,7 @@ import { sellableProduct } from '../render-context.js';
|
|
|
3
3
|
import { ProductCard } from './product-card.js';
|
|
4
4
|
export function HeroRenderer({ block, context, }) {
|
|
5
5
|
const product = block.sku ? sellableProduct(context.products, block.sku) : undefined;
|
|
6
|
-
return (_jsxs("section", { className: "rudra-hero", children: [_jsx("h3", { className: "rudra-hero__headline", children: block.headline }), block.body ? _jsx("p", { className: "rudra-hero__body", children: block.body }) : null, product ? (_jsxs("a", { className: "rudra-hero__link", href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, children: [product.title, _jsx("span", { className: "rudra-hero__price", children: context.formatPrice(product) }), block.ctaLabel ? _jsx("span", { className: "rudra-hero__cta", children: block.ctaLabel }) : null] })) : null] }));
|
|
6
|
+
return (_jsxs("section", { className: "rudra-hero", children: [block.headline ? _jsx("h3", { className: "rudra-hero__headline", children: block.headline }) : null, block.body ? _jsx("p", { className: "rudra-hero__body", children: block.body }) : null, product ? (_jsxs("a", { className: "rudra-hero__link", href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, children: [product.title, " ", _jsx("span", { className: "rudra-hero__price", children: context.formatPrice(product) }), ' ', block.ctaLabel ? _jsx("span", { className: "rudra-hero__cta", children: block.ctaLabel }) : null] })) : null] }));
|
|
7
7
|
}
|
|
8
8
|
export function GridRenderer({ block, context, }) {
|
|
9
9
|
return (_jsxs("section", { className: "rudra-grid", "data-rudra-columns": block.columns, children: [block.title ? _jsx("h3", { className: "rudra-grid__title", children: block.title }) : null, _jsx("div", { className: "rudra-grid__items", children: block.items.map((reference) => (_jsx(ProductCard, { reference: reference, context: context }, reference.sku))) })] }));
|
|
@@ -12,9 +12,22 @@ export function CarouselRenderer({ block, context, }) {
|
|
|
12
12
|
return (_jsxs("section", { className: "rudra-carousel", children: [block.title ? _jsx("h3", { className: "rudra-carousel__title", children: block.title }) : null, _jsx("div", { className: "rudra-carousel__track", children: block.items.map((reference) => (_jsx(ProductCard, { reference: reference, context: context }, reference.sku))) })] }));
|
|
13
13
|
}
|
|
14
14
|
export function BannerRenderer({ block }) {
|
|
15
|
-
return (_jsxs("aside", { className: "rudra-banner", "data-rudra-banner-tone": block.tone, children: [_jsx("span", { className: "rudra-banner__text", children: block.text }), block.ctaLabel ? _jsx("span", { className: "rudra-banner__cta", children: block.ctaLabel }) : null] }));
|
|
15
|
+
return (_jsxs("aside", { className: "rudra-banner", "data-rudra-banner-tone": block.tone, children: [_jsx("span", { className: "rudra-banner__text", children: block.text }), ' ', block.ctaLabel ? _jsx("span", { className: "rudra-banner__cta", children: block.ctaLabel }) : null] }));
|
|
16
16
|
}
|
|
17
17
|
export function CopyRenderer({ block }) {
|
|
18
18
|
return (_jsxs("section", { className: "rudra-copy", children: [block.title ? _jsx("h3", { className: "rudra-copy__title", children: block.title }) : null, _jsx("p", { className: "rudra-copy__body", children: block.body })] }));
|
|
19
19
|
}
|
|
20
|
+
export function BundleRenderer({ block, context, }) {
|
|
21
|
+
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
22
|
+
if (!bundle)
|
|
23
|
+
return null;
|
|
24
|
+
const products = [];
|
|
25
|
+
for (const sku of bundle.skus) {
|
|
26
|
+
const product = sellableProduct(context.products, sku);
|
|
27
|
+
if (!product)
|
|
28
|
+
return null;
|
|
29
|
+
products.push(product);
|
|
30
|
+
}
|
|
31
|
+
return (_jsxs("section", { className: "rudra-bundle", children: [bundle.label ? _jsx("h3", { className: "rudra-bundle__label", children: bundle.label }) : null, block.title ? _jsx("p", { className: "rudra-bundle__title", children: block.title }) : null, block.body ? _jsx("p", { className: "rudra-bundle__body", children: block.body }) : null, _jsx("ul", { className: "rudra-bundle__items", children: products.map((product) => (_jsx("li", { className: "rudra-bundle__item", "data-rudra-sku": product.sku, children: _jsx("a", { className: "rudra-bundle__link", href: context.hrefForSku(product.sku), children: product.title }) }, product.sku))) }), _jsx("p", { className: "rudra-bundle__price", children: context.formatBundlePrice(bundle) }), block.ctaLabel ? _jsx("span", { className: "rudra-bundle__cta", children: block.ctaLabel }) : null] }));
|
|
32
|
+
}
|
|
20
33
|
//# sourceMappingURL=block-renderers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"block-renderers.js","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"block-renderers.js","sourceRoot":"","sources":["../../src/blocks/block-renderers.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import type { ProductReference } from '@rudra-js/core';
|
|
2
2
|
import { type BlockRenderContext } from '../render-context.js';
|
|
3
|
-
/**
|
|
4
|
-
* One product.
|
|
5
|
-
*
|
|
6
|
-
* Every fact here — the title, the price, the image, the link — is read from
|
|
7
|
-
* the catalog, not from the specification. The specification contributes only
|
|
8
|
-
* `reason`, `badge` and `emphasis`, and React escapes all three on the way
|
|
9
|
-
* into the markup.
|
|
10
|
-
*/
|
|
11
3
|
export declare function ProductCard({ reference, context, }: {
|
|
12
4
|
reference: ProductReference;
|
|
13
5
|
context: BlockRenderContext;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-card.d.ts","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF
|
|
1
|
+
{"version":3,"file":"product-card.d.ts","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,wBAAgB,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GACR,EAAE;IACD,SAAS,EAAE,gBAAgB,CAAC;IAC5B,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCAyBA"}
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { sellableProduct } from '../render-context.js';
|
|
3
|
-
/**
|
|
4
|
-
* One product.
|
|
5
|
-
*
|
|
6
|
-
* Every fact here — the title, the price, the image, the link — is read from
|
|
7
|
-
* the catalog, not from the specification. The specification contributes only
|
|
8
|
-
* `reason`, `badge` and `emphasis`, and React escapes all three on the way
|
|
9
|
-
* into the markup.
|
|
10
|
-
*/
|
|
11
3
|
export function ProductCard({ reference, context, }) {
|
|
12
4
|
const product = sellableProduct(context.products, reference.sku);
|
|
13
|
-
// Reconciliation drops a SKU the catalog does not have, so this should be
|
|
14
|
-
// unreachable. Rendering nothing beats rendering a card with holes in it.
|
|
15
5
|
if (!product)
|
|
16
6
|
return null;
|
|
17
7
|
const isFeatured = reference.emphasis === 'featured';
|
|
18
|
-
return (_jsxs("a", { href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, "data-rudra-basis": reference.basis, className: isFeatured ? 'rudra-card rudra-card--featured' : 'rudra-card', children: [product.imageUrl ? (_jsx("img", { className: "rudra-card__image", src: product.imageUrl, alt: "", loading: "lazy" })) : null, _jsxs("span", { className: "rudra-card__body", children: [reference.badge ? _jsx("span", { className: "rudra-card__badge", children: reference.badge }) : null, _jsx("span", { className: "rudra-card__title", children: product.title }), _jsx("span", { className: "rudra-card__price", children: context.formatPrice(product) }), reference.reason ? _jsx("span", { className: "rudra-card__reason", children: reference.reason }) : null] })] }));
|
|
8
|
+
return (_jsxs("a", { href: context.hrefForSku(product.sku), "data-rudra-sku": product.sku, "data-rudra-basis": reference.basis, className: isFeatured ? 'rudra-card rudra-card--featured' : 'rudra-card', children: [product.imageUrl ? (_jsx("img", { className: "rudra-card__image", src: product.imageUrl, alt: "", loading: "lazy" })) : null, _jsxs("span", { className: "rudra-card__body", children: [reference.badge ? _jsx("span", { className: "rudra-card__badge", children: reference.badge }) : null, ' ', _jsx("span", { className: "rudra-card__title", children: product.title }), ' ', _jsx("span", { className: "rudra-card__price", children: context.formatPrice(product) }), ' ', reference.reason ? _jsx("span", { className: "rudra-card__reason", children: reference.reason }) : null] })] }));
|
|
19
9
|
}
|
|
20
10
|
//# sourceMappingURL=product-card.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-card.js","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF
|
|
1
|
+
{"version":3,"file":"product-card.js","sourceRoot":"","sources":["../../src/blocks/product-card.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF,MAAM,UAAU,WAAW,CAAC,EAC1B,SAAS,EACT,OAAO,GAIR;IACC,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACjE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,UAAU,GAAG,SAAS,CAAC,QAAQ,KAAK,UAAU,CAAC;IAErD,OAAO,CACL,aACE,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,oBACrB,OAAO,CAAC,GAAG,sBACT,SAAS,CAAC,KAAK,EACjC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC,iCAAiC,CAAC,CAAC,CAAC,YAAY,aAEvE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAClB,cAAK,SAAS,EAAC,mBAAmB,EAAC,GAAG,EAAE,OAAO,CAAC,QAAQ,EAAE,GAAG,EAAC,EAAE,EAAC,OAAO,EAAC,MAAM,GAAG,CACnF,CAAC,CAAC,CAAC,IAAI,EAER,gBAAM,SAAS,EAAC,kBAAkB,aAC/B,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,mBAAmB,YAAE,SAAS,CAAC,KAAK,GAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,EAC3F,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,KAAK,GAAQ,EAAC,GAAG,EAC9D,eAAM,SAAS,EAAC,mBAAmB,YAAE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,GAAQ,EAAC,GAAG,EAC5E,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,oBAAoB,YAAE,SAAS,CAAC,MAAM,GAAQ,CAAC,CAAC,CAAC,IAAI,IACpF,IACL,CACL,CAAC;AACJ,CAAC"}
|
package/dist/registry.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { BannerRenderer, CarouselRenderer, CopyRenderer, GridRenderer, HeroRenderer, } from './blocks/block-renderers.js';
|
|
2
|
-
import { BundleRenderer } from './blocks/bundle-block.js';
|
|
1
|
+
import { BannerRenderer, BundleRenderer, CarouselRenderer, CopyRenderer, GridRenderer, HeroRenderer, } from './blocks/block-renderers.js';
|
|
3
2
|
export const defaultRegistry = {
|
|
4
3
|
hero: HeroRenderer,
|
|
5
4
|
grid: GridRenderer,
|
package/dist/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,YAAY,GACb,MAAM,6BAA6B,CAAC;AAWrC,MAAM,CAAC,MAAM,eAAe,GAAkB;IAC5C,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,YAAY;IAClB,QAAQ,EAAE,gBAAgB;IAC1B,MAAM,EAAE,cAAc;IACtB,IAAI,EAAE,YAAY;IAClB,MAAM,EAAE,cAAc;CACvB,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,SAAiC;IAC9D,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;QAC5C,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;QAC5C,QAAQ,EAAE,SAAS,CAAC,QAAQ,IAAI,eAAe,CAAC,QAAQ;QACxD,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,eAAe,CAAC,MAAM;QAClD,IAAI,EAAE,SAAS,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;QAC5C,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,eAAe,CAAC,MAAM;KACnD,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"render-context.d.ts","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IACnD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACxD;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,GAAG,EAAE,MAAM,GACV,OAAO,GAAG,SAAS,CAGrB;AAYD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAQhF"}
|
package/dist/render-context.js
CHANGED
|
@@ -5,32 +5,28 @@ export function sellableProduct(products, sku) {
|
|
|
5
5
|
const product = products.get(sku);
|
|
6
6
|
return product?.isInStock === false ? undefined : product;
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
if (!Number.isFinite(product.price)) {
|
|
10
|
-
throw new TypeError(`price for SKU ${product.sku} is ${String(product.price)}, not a finite number — ` +
|
|
11
|
-
'catalog objects must satisfy productSchema from @rudra-js/core');
|
|
12
|
-
}
|
|
8
|
+
function formatMoney(price, currency, locale) {
|
|
13
9
|
let formatter;
|
|
14
10
|
try {
|
|
15
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency
|
|
11
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
|
|
16
12
|
}
|
|
17
13
|
catch {
|
|
18
|
-
return `${
|
|
14
|
+
return `${currency} ${price}`;
|
|
15
|
+
}
|
|
16
|
+
return formatter.format(price);
|
|
17
|
+
}
|
|
18
|
+
export function defaultFormatPrice(product, locale) {
|
|
19
|
+
if (!Number.isFinite(product.price)) {
|
|
20
|
+
throw new TypeError(`price for SKU ${product.sku} is ${String(product.price)}, not a finite number — ` +
|
|
21
|
+
'catalog objects must satisfy productSchema from @rudra-js/core');
|
|
19
22
|
}
|
|
20
|
-
return
|
|
23
|
+
return formatMoney(product.price, product.currency, locale);
|
|
21
24
|
}
|
|
22
25
|
export function defaultFormatBundlePrice(bundle, locale) {
|
|
23
26
|
if (!Number.isFinite(bundle.price)) {
|
|
24
27
|
throw new TypeError(`price for bundle ${bundle.id} is ${String(bundle.price)}, not a finite number — ` +
|
|
25
28
|
'bundle objects must satisfy bundleSchema from @rudra-js/core');
|
|
26
29
|
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return `${bundle.currency} ${bundle.price}`;
|
|
33
|
-
}
|
|
34
|
-
return formatter.format(bundle.price);
|
|
30
|
+
return formatMoney(bundle.price, bundle.currency, locale);
|
|
35
31
|
}
|
|
36
32
|
//# sourceMappingURL=render-context.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-context.js","sourceRoot":"","sources":["../src/render-context.ts"],"names":[],"mappings":"AAUA,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,YAAY,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,QAAsC,EACtC,GAAW;IAEX,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClC,OAAO,OAAO,EAAE,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5D,CAAC;AAED,SAAS,WAAW,CAAC,KAAa,EAAE,QAAgB,EAAE,MAAe;IACnE,IAAI,SAA4B,CAAC;IACjC,IAAI,CAAC;QACH,SAAS,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,QAAQ,IAAI,KAAK,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAgB,EAAE,MAAe;IAClE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,SAAS,CACjB,iBAAiB,OAAO,CAAC,GAAG,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,0BAA0B;YAChF,gEAAgE,CACnE,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,MAAc,EAAE,MAAe;IACtE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,SAAS,CACjB,oBAAoB,MAAM,CAAC,EAAE,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,0BAA0B;YAChF,8DAA8D,CACjE,CAAC;IACJ,CAAC;IACD,OAAO,WAAW,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -2,59 +2,16 @@ import type { Bundle, ComponentSpec, Product } from '@rudra-js/core';
|
|
|
2
2
|
import { type BlockRegistry } from './registry.js';
|
|
3
3
|
export interface RudraComponentProps {
|
|
4
4
|
spec: ComponentSpec;
|
|
5
|
-
/**
|
|
6
|
-
* The host catalog. Every product fact on the page comes from here rather
|
|
7
|
-
* than from the specification.
|
|
8
|
-
*
|
|
9
|
-
* A list of products, or anything keyed by SKU — a `Map`, or your own view
|
|
10
|
-
* over a catalog too large to hold in one. The renderers only ever call
|
|
11
|
-
* `get(sku)` and `has(sku)`, so a view needs nothing else to be fast.
|
|
12
|
-
*
|
|
13
|
-
* Validate them with `productSchema` from `@rudra-js/core` — the same schema
|
|
14
|
-
* your candidates already passed — not with `parseTrackingInput`, which
|
|
15
|
-
* parses a whole tracking payload and will reject a bare catalog.
|
|
16
|
-
*
|
|
17
|
-
* This is a second door into the framework. `imageUrl` lands in an
|
|
18
|
-
* `<img src>`, and `productSchema` is the only thing that rejects a
|
|
19
|
-
* protocol-relative `//evil.example/pixel.png` or a `data:` URL — React
|
|
20
|
-
* neutralises `javascript:` on its own, but not those. A price that is not a
|
|
21
|
-
* finite number throws rather than rendering as free.
|
|
22
|
-
*/
|
|
23
5
|
products: ProductCatalog;
|
|
24
|
-
/** Sets the shop sells together. Only needed if a spec can carry a bundle block. */
|
|
25
6
|
bundles?: readonly Bundle[];
|
|
26
7
|
registry?: BlockRegistry;
|
|
27
8
|
hrefForSku?: (sku: string) => string;
|
|
28
9
|
formatPrice?: (product: Product) => string;
|
|
29
|
-
/** Same as `formatPrice`, but for a bundle — the shop's price, not a sum of the parts. */
|
|
30
10
|
formatBundlePrice?: (bundle: Bundle) => string;
|
|
31
|
-
/**
|
|
32
|
-
* The shopper's locale, used to punctuate prices. Defaults to the server's,
|
|
33
|
-
* which is almost never the shopper's — pass it if the shop serves more than
|
|
34
|
-
* one. Ignored when `formatPrice` and `formatBundlePrice` are supplied.
|
|
35
|
-
*/
|
|
36
11
|
locale?: string;
|
|
37
|
-
/**
|
|
38
|
-
* Adds the model's own reasoning, the provider and the model name to the
|
|
39
|
-
* markup. Useful while developing and while benchmarking; it publishes which
|
|
40
|
-
* vendor a shop uses and whether the component is currently degraded, so it
|
|
41
|
-
* is off unless asked for.
|
|
42
|
-
*/
|
|
43
12
|
hasDiagnostics?: boolean;
|
|
44
13
|
className?: string;
|
|
45
14
|
}
|
|
46
|
-
/** A list of products, or anything keyed by SKU that answers `get` and `has`. */
|
|
47
15
|
export type ProductCatalog = readonly Product[] | ReadonlyMap<string, Product>;
|
|
48
|
-
/**
|
|
49
|
-
* Renders a component specification.
|
|
50
|
-
*
|
|
51
|
-
* A Server Component: no hooks, no state, no effects, and therefore no client
|
|
52
|
-
* bundle and no hydration for the recommendation area. The whole component
|
|
53
|
-
* arrives in the initial HTML response, which is what removes the pop-in a
|
|
54
|
-
* client-fetched recommendation rail has — and what makes the content visible
|
|
55
|
-
* to a crawler that does not run JavaScript.
|
|
56
|
-
*
|
|
57
|
-
* Renders nothing at all when no block produced markup.
|
|
58
|
-
*/
|
|
59
16
|
export declare function RudraComponent({ spec, products, bundles, registry, hrefForSku, formatPrice, formatBundlePrice, locale, hasDiagnostics, className, }: RudraComponentProps): import("react").JSX.Element | null;
|
|
60
17
|
//# sourceMappingURL=rudra-component.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rudra-component.d.ts","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAQ5E,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,aAAa,CAAC;IACpB
|
|
1
|
+
{"version":3,"file":"rudra-component.d.ts","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAQ5E,OAAO,EAAmB,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEpE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACrC,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC;IAC3C,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,SAAS,OAAO,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAsE/E,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,QAAQ,EACR,OAAO,EACP,QAA0B,EAC1B,UAA8B,EAC9B,WAAW,EACX,iBAAiB,EACjB,MAAM,EACN,cAAsB,EACtB,SAAS,GACV,EAAE,mBAAmB,sCAuCrB"}
|
package/dist/rudra-component.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { defaultFormatBundlePrice, defaultFormatPrice, defaultHrefForSku, sellableProduct, } from './render-context.js';
|
|
3
3
|
import { defaultRegistry } from './registry.js';
|
|
4
|
-
// Asks what the renderers call rather than which class the host built, since
|
|
5
|
-
// `instanceof Map` is per-realm. Checked before the list branch because a
|
|
6
|
-
// collection can answer both: an Immutable.js map has `map`, and converting
|
|
7
|
-
// through it yields `[sku, product]` pairs rather than products.
|
|
8
4
|
function isKeyedBySku(catalog) {
|
|
9
5
|
const candidate = catalog;
|
|
10
6
|
return typeof candidate.get === 'function' && typeof candidate.has === 'function';
|
|
@@ -18,25 +14,24 @@ function toProductMap(catalog) {
|
|
|
18
14
|
}
|
|
19
15
|
return new Map(catalog.map((product) => [product.sku, product]));
|
|
20
16
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// is flagged out of stock, and both read the same here: grid and carousel lose
|
|
24
|
-
// the products that did, a bundle loses itself if any one member did.
|
|
25
|
-
function hasContent(block, products, bundles) {
|
|
17
|
+
function hasContent(block, context) {
|
|
18
|
+
const isSellable = (sku) => sellableProduct(context.products, sku) !== undefined;
|
|
26
19
|
switch (block.kind) {
|
|
27
20
|
case 'grid':
|
|
28
21
|
case 'carousel':
|
|
29
|
-
return block.items.some((reference) =>
|
|
22
|
+
return block.items.some((reference) => isSellable(reference.sku));
|
|
30
23
|
case 'hero':
|
|
24
|
+
return (block.headline.length > 0 ||
|
|
25
|
+
(block.body !== null && block.body.length > 0) ||
|
|
26
|
+
(block.sku !== null && isSellable(block.sku)));
|
|
31
27
|
case 'banner':
|
|
32
28
|
case 'copy':
|
|
33
29
|
return true;
|
|
34
30
|
case 'bundle': {
|
|
35
31
|
if (block.bundleId === null)
|
|
36
32
|
return false;
|
|
37
|
-
const bundle = bundles.get(block.bundleId);
|
|
38
|
-
return
|
|
39
|
-
bundle.skus.every((sku) => sellableProduct(products, sku) !== undefined));
|
|
33
|
+
const bundle = context.bundles.get(block.bundleId);
|
|
34
|
+
return bundle !== undefined && bundle.skus.every((sku) => isSellable(sku));
|
|
40
35
|
}
|
|
41
36
|
default:
|
|
42
37
|
block;
|
|
@@ -58,37 +53,21 @@ function renderBlock(block, context, registry, index) {
|
|
|
58
53
|
case 'bundle':
|
|
59
54
|
return _jsx(registry.bundle, { block: block, context: context }, index);
|
|
60
55
|
default:
|
|
61
|
-
// A block kind this renderer predates loses that block, not the page.
|
|
62
56
|
block;
|
|
63
57
|
return null;
|
|
64
58
|
}
|
|
65
59
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Renders a component specification.
|
|
68
|
-
*
|
|
69
|
-
* A Server Component: no hooks, no state, no effects, and therefore no client
|
|
70
|
-
* bundle and no hydration for the recommendation area. The whole component
|
|
71
|
-
* arrives in the initial HTML response, which is what removes the pop-in a
|
|
72
|
-
* client-fetched recommendation rail has — and what makes the content visible
|
|
73
|
-
* to a crawler that does not run JavaScript.
|
|
74
|
-
*
|
|
75
|
-
* Renders nothing at all when no block produced markup.
|
|
76
|
-
*/
|
|
77
60
|
export function RudraComponent({ spec, products, bundles, registry = defaultRegistry, hrefForSku = defaultHrefForSku, formatPrice, formatBundlePrice, locale, hasDiagnostics = false, className, }) {
|
|
78
|
-
const productMap = toProductMap(products);
|
|
79
|
-
const bundlesById = new Map((bundles ?? []).map((bundle) => [bundle.id, bundle]));
|
|
80
61
|
const context = {
|
|
81
|
-
products:
|
|
82
|
-
bundles:
|
|
62
|
+
products: toProductMap(products),
|
|
63
|
+
bundles: new Map((bundles ?? []).map((bundle) => [bundle.id, bundle])),
|
|
83
64
|
hrefForSku,
|
|
84
65
|
formatPrice: formatPrice ?? ((product) => defaultFormatPrice(product, locale)),
|
|
85
66
|
formatBundlePrice: formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, locale)),
|
|
86
67
|
};
|
|
87
|
-
const visible = spec.blocks.filter((block) => hasContent(block, context
|
|
68
|
+
const visible = spec.blocks.filter((block) => hasContent(block, context));
|
|
88
69
|
if (visible.length === 0)
|
|
89
70
|
return null;
|
|
90
|
-
// React drops a data-* attribute whose value is undefined, so degradedReason
|
|
91
|
-
// needs no branch of its own.
|
|
92
71
|
const diagnosticAttributes = hasDiagnostics
|
|
93
72
|
? {
|
|
94
73
|
'data-rudra-provider': spec.provider ?? 'none',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rudra-component.js","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":";AACA,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,GAEhB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAsB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"rudra-component.js","sourceRoot":"","sources":["../src/rudra-component.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudra-js/react",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Renders a rudra component specification with React 18 or 19, on the server or in the browser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@rudra-js/core": "^0.7.
|
|
54
|
+
"@rudra-js/core": "^0.7.5",
|
|
55
55
|
"react": "^18.2.0 || ^19.0.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@rudra-js/core": "0.7.
|
|
58
|
+
"@rudra-js/core": "0.7.5",
|
|
59
59
|
"@types/react": "^19.3.0",
|
|
60
60
|
"@types/react-dom": "^19.3.0",
|
|
61
61
|
"react": "^19.3.0",
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import type { BannerBlock, CarouselBlock, CopyBlock, GridBlock, HeroBlock } from '@rudra-js/core';
|
|
1
|
+
import type {
|
|
2
|
+
BannerBlock,
|
|
3
|
+
BundleBlock,
|
|
4
|
+
CarouselBlock,
|
|
5
|
+
CopyBlock,
|
|
6
|
+
GridBlock,
|
|
7
|
+
HeroBlock,
|
|
8
|
+
Product,
|
|
9
|
+
} from '@rudra-js/core';
|
|
12
10
|
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
13
11
|
import { ProductCard } from './product-card.js';
|
|
14
12
|
|
|
@@ -23,7 +21,7 @@ export function HeroRenderer({
|
|
|
23
21
|
|
|
24
22
|
return (
|
|
25
23
|
<section className="rudra-hero">
|
|
26
|
-
<h3 className="rudra-hero__headline">{block.headline}</h3>
|
|
24
|
+
{block.headline ? <h3 className="rudra-hero__headline">{block.headline}</h3> : null}
|
|
27
25
|
{block.body ? <p className="rudra-hero__body">{block.body}</p> : null}
|
|
28
26
|
{product ? (
|
|
29
27
|
<a
|
|
@@ -31,8 +29,7 @@ export function HeroRenderer({
|
|
|
31
29
|
href={context.hrefForSku(product.sku)}
|
|
32
30
|
data-rudra-sku={product.sku}
|
|
33
31
|
>
|
|
34
|
-
{product.title}
|
|
35
|
-
<span className="rudra-hero__price">{context.formatPrice(product)}</span>
|
|
32
|
+
{product.title} <span className="rudra-hero__price">{context.formatPrice(product)}</span>{' '}
|
|
36
33
|
{block.ctaLabel ? <span className="rudra-hero__cta">{block.ctaLabel}</span> : null}
|
|
37
34
|
</a>
|
|
38
35
|
) : null}
|
|
@@ -69,8 +66,6 @@ export function CarouselRenderer({
|
|
|
69
66
|
return (
|
|
70
67
|
<section className="rudra-carousel">
|
|
71
68
|
{block.title ? <h3 className="rudra-carousel__title">{block.title}</h3> : null}
|
|
72
|
-
{/* Scrolls with CSS overflow rather than JavaScript, so the whole
|
|
73
|
-
component still needs no client bundle. */}
|
|
74
69
|
<div className="rudra-carousel__track">
|
|
75
70
|
{block.items.map((reference) => (
|
|
76
71
|
<ProductCard key={reference.sku} reference={reference} context={context} />
|
|
@@ -83,7 +78,7 @@ export function CarouselRenderer({
|
|
|
83
78
|
export function BannerRenderer({ block }: { block: BannerBlock }) {
|
|
84
79
|
return (
|
|
85
80
|
<aside className="rudra-banner" data-rudra-banner-tone={block.tone}>
|
|
86
|
-
<span className="rudra-banner__text">{block.text}</span>
|
|
81
|
+
<span className="rudra-banner__text">{block.text}</span>{' '}
|
|
87
82
|
{block.ctaLabel ? <span className="rudra-banner__cta">{block.ctaLabel}</span> : null}
|
|
88
83
|
</aside>
|
|
89
84
|
);
|
|
@@ -97,3 +92,42 @@ export function CopyRenderer({ block }: { block: CopyBlock }) {
|
|
|
97
92
|
</section>
|
|
98
93
|
);
|
|
99
94
|
}
|
|
95
|
+
|
|
96
|
+
export function BundleRenderer({
|
|
97
|
+
block,
|
|
98
|
+
context,
|
|
99
|
+
}: {
|
|
100
|
+
block: BundleBlock;
|
|
101
|
+
context: BlockRenderContext;
|
|
102
|
+
}) {
|
|
103
|
+
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
104
|
+
if (!bundle) return null;
|
|
105
|
+
|
|
106
|
+
const products: Product[] = [];
|
|
107
|
+
for (const sku of bundle.skus) {
|
|
108
|
+
const product = sellableProduct(context.products, sku);
|
|
109
|
+
if (!product) return null;
|
|
110
|
+
products.push(product);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<section className="rudra-bundle">
|
|
115
|
+
{bundle.label ? <h3 className="rudra-bundle__label">{bundle.label}</h3> : null}
|
|
116
|
+
{block.title ? <p className="rudra-bundle__title">{block.title}</p> : null}
|
|
117
|
+
{block.body ? <p className="rudra-bundle__body">{block.body}</p> : null}
|
|
118
|
+
|
|
119
|
+
<ul className="rudra-bundle__items">
|
|
120
|
+
{products.map((product) => (
|
|
121
|
+
<li key={product.sku} className="rudra-bundle__item" data-rudra-sku={product.sku}>
|
|
122
|
+
<a className="rudra-bundle__link" href={context.hrefForSku(product.sku)}>
|
|
123
|
+
{product.title}
|
|
124
|
+
</a>
|
|
125
|
+
</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
|
|
129
|
+
<p className="rudra-bundle__price">{context.formatBundlePrice(bundle)}</p>
|
|
130
|
+
{block.ctaLabel ? <span className="rudra-bundle__cta">{block.ctaLabel}</span> : null}
|
|
131
|
+
</section>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { ProductReference } from '@rudra-js/core';
|
|
2
2
|
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* One product.
|
|
6
|
-
*
|
|
7
|
-
* Every fact here — the title, the price, the image, the link — is read from
|
|
8
|
-
* the catalog, not from the specification. The specification contributes only
|
|
9
|
-
* `reason`, `badge` and `emphasis`, and React escapes all three on the way
|
|
10
|
-
* into the markup.
|
|
11
|
-
*/
|
|
12
4
|
export function ProductCard({
|
|
13
5
|
reference,
|
|
14
6
|
context,
|
|
@@ -17,8 +9,6 @@ export function ProductCard({
|
|
|
17
9
|
context: BlockRenderContext;
|
|
18
10
|
}) {
|
|
19
11
|
const product = sellableProduct(context.products, reference.sku);
|
|
20
|
-
// Reconciliation drops a SKU the catalog does not have, so this should be
|
|
21
|
-
// unreachable. Rendering nothing beats rendering a card with holes in it.
|
|
22
12
|
if (!product) return null;
|
|
23
13
|
|
|
24
14
|
const isFeatured = reference.emphasis === 'featured';
|
|
@@ -35,9 +25,9 @@ export function ProductCard({
|
|
|
35
25
|
) : null}
|
|
36
26
|
|
|
37
27
|
<span className="rudra-card__body">
|
|
38
|
-
{reference.badge ? <span className="rudra-card__badge">{reference.badge}</span> : null}
|
|
39
|
-
<span className="rudra-card__title">{product.title}</span>
|
|
40
|
-
<span className="rudra-card__price">{context.formatPrice(product)}</span>
|
|
28
|
+
{reference.badge ? <span className="rudra-card__badge">{reference.badge}</span> : null}{' '}
|
|
29
|
+
<span className="rudra-card__title">{product.title}</span>{' '}
|
|
30
|
+
<span className="rudra-card__price">{context.formatPrice(product)}</span>{' '}
|
|
41
31
|
{reference.reason ? <span className="rudra-card__reason">{reference.reason}</span> : null}
|
|
42
32
|
</span>
|
|
43
33
|
</a>
|
package/src/registry.ts
CHANGED
|
@@ -3,12 +3,12 @@ import type { Block, BlockKind } from '@rudra-js/core';
|
|
|
3
3
|
import type { BlockRenderContext } from './render-context.js';
|
|
4
4
|
import {
|
|
5
5
|
BannerRenderer,
|
|
6
|
+
BundleRenderer,
|
|
6
7
|
CarouselRenderer,
|
|
7
8
|
CopyRenderer,
|
|
8
9
|
GridRenderer,
|
|
9
10
|
HeroRenderer,
|
|
10
11
|
} from './blocks/block-renderers.js';
|
|
11
|
-
import { BundleRenderer } from './blocks/bundle-block.js';
|
|
12
12
|
|
|
13
13
|
export type BlockRenderer<Kind extends BlockKind> = (props: {
|
|
14
14
|
block: Extract<Block, { kind: Kind }>;
|
package/src/render-context.ts
CHANGED
|
@@ -2,9 +2,7 @@ import type { Bundle, Product } from '@rudra-js/core';
|
|
|
2
2
|
|
|
3
3
|
export interface BlockRenderContext {
|
|
4
4
|
readonly products: ReadonlyMap<string, Product>;
|
|
5
|
-
|
|
6
5
|
readonly bundles: ReadonlyMap<string, Bundle>;
|
|
7
|
-
|
|
8
6
|
readonly hrefForSku: (sku: string) => string;
|
|
9
7
|
readonly formatPrice: (product: Product) => string;
|
|
10
8
|
readonly formatBundlePrice: (bundle: Bundle) => string;
|
|
@@ -22,6 +20,16 @@ export function sellableProduct(
|
|
|
22
20
|
return product?.isInStock === false ? undefined : product;
|
|
23
21
|
}
|
|
24
22
|
|
|
23
|
+
function formatMoney(price: number, currency: string, locale?: string): string {
|
|
24
|
+
let formatter: Intl.NumberFormat;
|
|
25
|
+
try {
|
|
26
|
+
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency });
|
|
27
|
+
} catch {
|
|
28
|
+
return `${currency} ${price}`;
|
|
29
|
+
}
|
|
30
|
+
return formatter.format(price);
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
export function defaultFormatPrice(product: Product, locale?: string): string {
|
|
26
34
|
if (!Number.isFinite(product.price)) {
|
|
27
35
|
throw new TypeError(
|
|
@@ -29,15 +37,7 @@ export function defaultFormatPrice(product: Product, locale?: string): string {
|
|
|
29
37
|
'catalog objects must satisfy productSchema from @rudra-js/core',
|
|
30
38
|
);
|
|
31
39
|
}
|
|
32
|
-
|
|
33
|
-
let formatter: Intl.NumberFormat;
|
|
34
|
-
try {
|
|
35
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: product.currency });
|
|
36
|
-
} catch {
|
|
37
|
-
return `${product.currency} ${product.price}`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return formatter.format(product.price);
|
|
40
|
+
return formatMoney(product.price, product.currency, locale);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function defaultFormatBundlePrice(bundle: Bundle, locale?: string): string {
|
|
@@ -47,13 +47,5 @@ export function defaultFormatBundlePrice(bundle: Bundle, locale?: string): strin
|
|
|
47
47
|
'bundle objects must satisfy bundleSchema from @rudra-js/core',
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
let formatter: Intl.NumberFormat;
|
|
52
|
-
try {
|
|
53
|
-
formatter = new Intl.NumberFormat(locale, { style: 'currency', currency: bundle.currency });
|
|
54
|
-
} catch {
|
|
55
|
-
return `${bundle.currency} ${bundle.price}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return formatter.format(bundle.price);
|
|
50
|
+
return formatMoney(bundle.price, bundle.currency, locale);
|
|
59
51
|
}
|
package/src/rudra-component.tsx
CHANGED
|
@@ -10,55 +10,19 @@ import { defaultRegistry, type BlockRegistry } from './registry.js';
|
|
|
10
10
|
|
|
11
11
|
export interface RudraComponentProps {
|
|
12
12
|
spec: ComponentSpec;
|
|
13
|
-
/**
|
|
14
|
-
* The host catalog. Every product fact on the page comes from here rather
|
|
15
|
-
* than from the specification.
|
|
16
|
-
*
|
|
17
|
-
* A list of products, or anything keyed by SKU — a `Map`, or your own view
|
|
18
|
-
* over a catalog too large to hold in one. The renderers only ever call
|
|
19
|
-
* `get(sku)` and `has(sku)`, so a view needs nothing else to be fast.
|
|
20
|
-
*
|
|
21
|
-
* Validate them with `productSchema` from `@rudra-js/core` — the same schema
|
|
22
|
-
* your candidates already passed — not with `parseTrackingInput`, which
|
|
23
|
-
* parses a whole tracking payload and will reject a bare catalog.
|
|
24
|
-
*
|
|
25
|
-
* This is a second door into the framework. `imageUrl` lands in an
|
|
26
|
-
* `<img src>`, and `productSchema` is the only thing that rejects a
|
|
27
|
-
* protocol-relative `//evil.example/pixel.png` or a `data:` URL — React
|
|
28
|
-
* neutralises `javascript:` on its own, but not those. A price that is not a
|
|
29
|
-
* finite number throws rather than rendering as free.
|
|
30
|
-
*/
|
|
31
13
|
products: ProductCatalog;
|
|
32
|
-
/** Sets the shop sells together. Only needed if a spec can carry a bundle block. */
|
|
33
14
|
bundles?: readonly Bundle[];
|
|
34
15
|
registry?: BlockRegistry;
|
|
35
16
|
hrefForSku?: (sku: string) => string;
|
|
36
17
|
formatPrice?: (product: Product) => string;
|
|
37
|
-
/** Same as `formatPrice`, but for a bundle — the shop's price, not a sum of the parts. */
|
|
38
18
|
formatBundlePrice?: (bundle: Bundle) => string;
|
|
39
|
-
/**
|
|
40
|
-
* The shopper's locale, used to punctuate prices. Defaults to the server's,
|
|
41
|
-
* which is almost never the shopper's — pass it if the shop serves more than
|
|
42
|
-
* one. Ignored when `formatPrice` and `formatBundlePrice` are supplied.
|
|
43
|
-
*/
|
|
44
19
|
locale?: string;
|
|
45
|
-
/**
|
|
46
|
-
* Adds the model's own reasoning, the provider and the model name to the
|
|
47
|
-
* markup. Useful while developing and while benchmarking; it publishes which
|
|
48
|
-
* vendor a shop uses and whether the component is currently degraded, so it
|
|
49
|
-
* is off unless asked for.
|
|
50
|
-
*/
|
|
51
20
|
hasDiagnostics?: boolean;
|
|
52
21
|
className?: string;
|
|
53
22
|
}
|
|
54
23
|
|
|
55
|
-
/** A list of products, or anything keyed by SKU that answers `get` and `has`. */
|
|
56
24
|
export type ProductCatalog = readonly Product[] | ReadonlyMap<string, Product>;
|
|
57
25
|
|
|
58
|
-
// Asks what the renderers call rather than which class the host built, since
|
|
59
|
-
// `instanceof Map` is per-realm. Checked before the list branch because a
|
|
60
|
-
// collection can answer both: an Immutable.js map has `map`, and converting
|
|
61
|
-
// through it yields `[sku, product]` pairs rather than products.
|
|
62
26
|
function isKeyedBySku(catalog: ProductCatalog): catalog is ReadonlyMap<string, Product> {
|
|
63
27
|
const candidate = catalog as { get?: unknown; has?: unknown };
|
|
64
28
|
return typeof candidate.get === 'function' && typeof candidate.has === 'function';
|
|
@@ -75,32 +39,26 @@ function toProductMap(catalog: ProductCatalog): ReadonlyMap<string, Product> {
|
|
|
75
39
|
return new Map(catalog.map((product) => [product.sku, product]));
|
|
76
40
|
}
|
|
77
41
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// the products that did, a bundle loses itself if any one member did.
|
|
82
|
-
function hasContent(
|
|
83
|
-
block: Block,
|
|
84
|
-
products: ReadonlyMap<string, Product>,
|
|
85
|
-
bundles: ReadonlyMap<string, Bundle>,
|
|
86
|
-
): boolean {
|
|
42
|
+
function hasContent(block: Block, context: BlockRenderContext): boolean {
|
|
43
|
+
const isSellable = (sku: string) => sellableProduct(context.products, sku) !== undefined;
|
|
44
|
+
|
|
87
45
|
switch (block.kind) {
|
|
88
46
|
case 'grid':
|
|
89
47
|
case 'carousel':
|
|
90
|
-
return block.items.some(
|
|
91
|
-
(reference) => sellableProduct(products, reference.sku) !== undefined,
|
|
92
|
-
);
|
|
48
|
+
return block.items.some((reference) => isSellable(reference.sku));
|
|
93
49
|
case 'hero':
|
|
50
|
+
return (
|
|
51
|
+
block.headline.length > 0 ||
|
|
52
|
+
(block.body !== null && block.body.length > 0) ||
|
|
53
|
+
(block.sku !== null && isSellable(block.sku))
|
|
54
|
+
);
|
|
94
55
|
case 'banner':
|
|
95
56
|
case 'copy':
|
|
96
57
|
return true;
|
|
97
58
|
case 'bundle': {
|
|
98
59
|
if (block.bundleId === null) return false;
|
|
99
|
-
const bundle = bundles.get(block.bundleId);
|
|
100
|
-
return (
|
|
101
|
-
bundle !== undefined &&
|
|
102
|
-
bundle.skus.every((sku) => sellableProduct(products, sku) !== undefined)
|
|
103
|
-
);
|
|
60
|
+
const bundle = context.bundles.get(block.bundleId);
|
|
61
|
+
return bundle !== undefined && bundle.skus.every((sku) => isSellable(sku));
|
|
104
62
|
}
|
|
105
63
|
default:
|
|
106
64
|
block satisfies never;
|
|
@@ -128,23 +86,11 @@ function renderBlock(
|
|
|
128
86
|
case 'bundle':
|
|
129
87
|
return <registry.bundle key={index} block={block} context={context} />;
|
|
130
88
|
default:
|
|
131
|
-
// A block kind this renderer predates loses that block, not the page.
|
|
132
89
|
block satisfies never;
|
|
133
90
|
return null;
|
|
134
91
|
}
|
|
135
92
|
}
|
|
136
93
|
|
|
137
|
-
/**
|
|
138
|
-
* Renders a component specification.
|
|
139
|
-
*
|
|
140
|
-
* A Server Component: no hooks, no state, no effects, and therefore no client
|
|
141
|
-
* bundle and no hydration for the recommendation area. The whole component
|
|
142
|
-
* arrives in the initial HTML response, which is what removes the pop-in a
|
|
143
|
-
* client-fetched recommendation rail has — and what makes the content visible
|
|
144
|
-
* to a crawler that does not run JavaScript.
|
|
145
|
-
*
|
|
146
|
-
* Renders nothing at all when no block produced markup.
|
|
147
|
-
*/
|
|
148
94
|
export function RudraComponent({
|
|
149
95
|
spec,
|
|
150
96
|
products,
|
|
@@ -157,24 +103,17 @@ export function RudraComponent({
|
|
|
157
103
|
hasDiagnostics = false,
|
|
158
104
|
className,
|
|
159
105
|
}: RudraComponentProps) {
|
|
160
|
-
const productMap = toProductMap(products);
|
|
161
|
-
const bundlesById = new Map((bundles ?? []).map((bundle) => [bundle.id, bundle]));
|
|
162
|
-
|
|
163
106
|
const context: BlockRenderContext = {
|
|
164
|
-
products:
|
|
165
|
-
bundles:
|
|
107
|
+
products: toProductMap(products),
|
|
108
|
+
bundles: new Map((bundles ?? []).map((bundle) => [bundle.id, bundle])),
|
|
166
109
|
hrefForSku,
|
|
167
110
|
formatPrice: formatPrice ?? ((product) => defaultFormatPrice(product, locale)),
|
|
168
111
|
formatBundlePrice: formatBundlePrice ?? ((bundle) => defaultFormatBundlePrice(bundle, locale)),
|
|
169
112
|
};
|
|
170
113
|
|
|
171
|
-
const visible = spec.blocks.filter((block) =>
|
|
172
|
-
hasContent(block, context.products, context.bundles),
|
|
173
|
-
);
|
|
114
|
+
const visible = spec.blocks.filter((block) => hasContent(block, context));
|
|
174
115
|
if (visible.length === 0) return null;
|
|
175
116
|
|
|
176
|
-
// React drops a data-* attribute whose value is undefined, so degradedReason
|
|
177
|
-
// needs no branch of its own.
|
|
178
117
|
const diagnosticAttributes = hasDiagnostics
|
|
179
118
|
? {
|
|
180
119
|
'data-rudra-provider': spec.provider ?? 'none',
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { BundleBlock } from '@rudra-js/core';
|
|
2
|
-
import { type BlockRenderContext } from '../render-context.js';
|
|
3
|
-
export declare function BundleRenderer({ block, context, }: {
|
|
4
|
-
block: BundleBlock;
|
|
5
|
-
context: BlockRenderContext;
|
|
6
|
-
}): import("react").JSX.Element | null;
|
|
7
|
-
//# sourceMappingURL=bundle-block.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-block.d.ts","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAW,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAmB,KAAK,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAEhF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,kBAAkB,CAAC;CAC7B,sCAiCA"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { sellableProduct } from '../render-context.js';
|
|
3
|
-
export function BundleRenderer({ block, context, }) {
|
|
4
|
-
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
5
|
-
if (!bundle)
|
|
6
|
-
return null;
|
|
7
|
-
const products = [];
|
|
8
|
-
for (const sku of bundle.skus) {
|
|
9
|
-
const product = sellableProduct(context.products, sku);
|
|
10
|
-
// A set missing one of its parts is not that set.
|
|
11
|
-
if (!product)
|
|
12
|
-
return null;
|
|
13
|
-
products.push(product);
|
|
14
|
-
}
|
|
15
|
-
return (_jsxs("section", { className: "rudra-bundle", children: [bundle.label ? _jsx("h3", { className: "rudra-bundle__label", children: bundle.label }) : null, block.title ? _jsx("p", { className: "rudra-bundle__title", children: block.title }) : null, block.body ? _jsx("p", { className: "rudra-bundle__body", children: block.body }) : null, _jsx("ul", { className: "rudra-bundle__items", children: products.map((product) => (_jsx("li", { className: "rudra-bundle__item", "data-rudra-sku": product.sku, children: _jsx("a", { className: "rudra-bundle__link", href: context.hrefForSku(product.sku), children: product.title }) }, product.sku))) }), _jsx("p", { className: "rudra-bundle__price", children: context.formatBundlePrice(bundle) }), block.ctaLabel ? _jsx("span", { className: "rudra-bundle__cta", children: block.ctaLabel }) : null] }));
|
|
16
|
-
}
|
|
17
|
-
//# sourceMappingURL=bundle-block.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-block.js","sourceRoot":"","sources":["../../src/blocks/bundle-block.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAA2B,MAAM,sBAAsB,CAAC;AAEhF,MAAM,UAAU,cAAc,CAAC,EAC7B,KAAK,EACL,OAAO,GAIR;IACC,MAAM,MAAM,GAAG,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACzF,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QACvD,kDAAkD;QAClD,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAC;QAC1B,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,CACL,mBAAS,SAAS,EAAC,cAAc,aAE9B,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,aAAI,SAAS,EAAC,qBAAqB,YAAE,MAAM,CAAC,KAAK,GAAM,CAAC,CAAC,CAAC,IAAI,EAC7E,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,qBAAqB,YAAE,KAAK,CAAC,KAAK,GAAK,CAAC,CAAC,CAAC,IAAI,EACzE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,oBAAoB,YAAE,KAAK,CAAC,IAAI,GAAK,CAAC,CAAC,CAAC,IAAI,EAEvE,aAAI,SAAS,EAAC,qBAAqB,YAChC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CACzB,aAAsB,SAAS,EAAC,oBAAoB,oBAAiB,OAAO,CAAC,GAAG,YAC9E,YAAG,SAAS,EAAC,oBAAoB,EAAC,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,YACpE,OAAO,CAAC,KAAK,GACZ,IAHG,OAAO,CAAC,GAAG,CAIf,CACN,CAAC,GACC,EAEL,YAAG,SAAS,EAAC,qBAAqB,YAAE,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,GAAK,EACzE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAM,SAAS,EAAC,mBAAmB,YAAE,KAAK,CAAC,QAAQ,GAAQ,CAAC,CAAC,CAAC,IAAI,IAC5E,CACX,CAAC;AACJ,CAAC"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { BundleBlock, Product } from '@rudra-js/core';
|
|
2
|
-
import { sellableProduct, type BlockRenderContext } from '../render-context.js';
|
|
3
|
-
|
|
4
|
-
export function BundleRenderer({
|
|
5
|
-
block,
|
|
6
|
-
context,
|
|
7
|
-
}: {
|
|
8
|
-
block: BundleBlock;
|
|
9
|
-
context: BlockRenderContext;
|
|
10
|
-
}) {
|
|
11
|
-
const bundle = block.bundleId === null ? undefined : context.bundles.get(block.bundleId);
|
|
12
|
-
if (!bundle) return null;
|
|
13
|
-
|
|
14
|
-
const products: Product[] = [];
|
|
15
|
-
for (const sku of bundle.skus) {
|
|
16
|
-
const product = sellableProduct(context.products, sku);
|
|
17
|
-
// A set missing one of its parts is not that set.
|
|
18
|
-
if (!product) return null;
|
|
19
|
-
products.push(product);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<section className="rudra-bundle">
|
|
24
|
-
{/* The shop's label is the one name here anyone can check, so it leads. */}
|
|
25
|
-
{bundle.label ? <h3 className="rudra-bundle__label">{bundle.label}</h3> : null}
|
|
26
|
-
{block.title ? <p className="rudra-bundle__title">{block.title}</p> : null}
|
|
27
|
-
{block.body ? <p className="rudra-bundle__body">{block.body}</p> : null}
|
|
28
|
-
|
|
29
|
-
<ul className="rudra-bundle__items">
|
|
30
|
-
{products.map((product) => (
|
|
31
|
-
<li key={product.sku} className="rudra-bundle__item" data-rudra-sku={product.sku}>
|
|
32
|
-
<a className="rudra-bundle__link" href={context.hrefForSku(product.sku)}>
|
|
33
|
-
{product.title}
|
|
34
|
-
</a>
|
|
35
|
-
</li>
|
|
36
|
-
))}
|
|
37
|
-
</ul>
|
|
38
|
-
|
|
39
|
-
<p className="rudra-bundle__price">{context.formatBundlePrice(bundle)}</p>
|
|
40
|
-
{block.ctaLabel ? <span className="rudra-bundle__cta">{block.ctaLabel}</span> : null}
|
|
41
|
-
</section>
|
|
42
|
-
);
|
|
43
|
-
}
|