Skip to content

Commit 7a4b137

Browse files
authored
feat(recommend): add Trending Facets widget (#6957)
* feat(recommend): add Trending Facets widget Add the `trendingFacets` Recommend model widget across instantsearch.js and React flavors. Unlike other Recommend widgets that return product hits, Trending Facets returns facet values (e.g., trending brands or categories) as a pure-display, read-only list. New packages/exports: - `connectTrendingFacets` connector (instantsearch.js) - `trendingFacets` widget (instantsearch.js) - `createTrendingFacetsComponent` UI component (instantsearch-ui-components) - `useTrendingFacets` React hook (react-instantsearch-core) - `<TrendingFacets>` React component (react-instantsearch) - `TrendingFacetItem` type with `{ facetName, facetValue, _score }` Key design decisions: - Standalone UI component (not based on shared Recommend base) - Strict `TrendingFacetItem` type, not wrapped in `Hit<>` - No `sendEvent`/analytics in v1 - Supports `queryParameters`, `fallbackParameters`, `escapeHTML`, `transformItems`, and full template customization (item/header/empty) * fix: address Copilot review feedback - Add exports to UMD entrypoints (connectors/index.umd.ts, widgets/index.umd.ts) - Replace custom escapeFacetValue() with shared escape() from escape-html.ts - Add runtime validation for required facetName option * fix: add TrendingFacets to UMD exports and update test fixtures - Add TrendingFacets to react-instantsearch UMD widget index - Update all-widgets.test.tsx inline snapshot with TrendingFacets entry - Add trendingFacets case to widgets index.test.ts (required facetName) * fix: handle v4 TrendingFacetsQuery type gap for queryParameters/fallbackParameters The v4 algoliasearch TrendingFacetsQuery type doesn't include queryParameters or fallbackParameters (v5 does). Use `as any` at the addTrendingFacets call boundary with a comment documenting the gap. * update bundlesize * test: add TrendingFacets to common test suite - Add createTrendingFacetsSearchClient mock with facet-shaped fixture data - Add common widget tests (options, links) for TrendingFacets - Add common connector tests (options, state) for TrendingFacets - Register in IS.js, React, and Vue (skipped) common test entry points * fix: add TrendingFacets connector stub to Vue common tests
1 parent 5289c69 commit 7a4b137

File tree

37 files changed

+2391
-2
lines changed

37 files changed

+2391
-2
lines changed

bundlesize.config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
},
1111
{
1212
"path": "./packages/instantsearch.js/dist/instantsearch.production.min.js",
13-
"maxSize": "121.25 kB"
13+
"maxSize": "121.8 kB"
1414
},
1515
{
1616
"path": "./packages/instantsearch.js/dist/instantsearch.development.js",
17-
"maxSize": "253 kB"
17+
"maxSize": "253.4 kB"
1818
},
1919
{
2020
"path": "packages/react-instantsearch-core/dist/umd/ReactInstantSearchCore.min.js",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/** @jsx createElement */
2+
3+
import { cx } from '../lib';
4+
5+
import type { ComponentProps, Renderer } from '../types';
6+
7+
export type TrendingFacetItem = {
8+
facetName: string;
9+
facetValue: string;
10+
_score: number;
11+
};
12+
13+
export type TrendingFacetsClassNames = {
14+
root: string;
15+
emptyRoot: string;
16+
title: string;
17+
container: string;
18+
list: string;
19+
item: string;
20+
};
21+
22+
export type TrendingFacetsTranslations = {
23+
title: string;
24+
};
25+
26+
export type TrendingFacetsProps = ComponentProps<'div'> & {
27+
items: TrendingFacetItem[];
28+
classNames?: Partial<TrendingFacetsClassNames>;
29+
itemComponent?: (props: { item: TrendingFacetItem }) => JSX.Element;
30+
headerComponent?: (props: {
31+
classNames: Partial<TrendingFacetsClassNames>;
32+
items: TrendingFacetItem[];
33+
translations: Required<TrendingFacetsTranslations>;
34+
}) => JSX.Element;
35+
emptyComponent?: () => JSX.Element;
36+
status: 'idle' | 'loading' | 'stalled' | 'error';
37+
translations?: Partial<TrendingFacetsTranslations>;
38+
};
39+
40+
export function createTrendingFacetsComponent({
41+
createElement,
42+
Fragment,
43+
}: Renderer) {
44+
function DefaultHeader({
45+
classNames = {},
46+
items,
47+
translations,
48+
}: {
49+
classNames: Partial<TrendingFacetsClassNames>;
50+
items: TrendingFacetItem[];
51+
translations: Required<TrendingFacetsTranslations>;
52+
}) {
53+
if (!items || items.length < 1) {
54+
return null;
55+
}
56+
57+
if (!translations.title) {
58+
return null;
59+
}
60+
61+
return <h3 className={classNames.title}>{translations.title}</h3>;
62+
}
63+
64+
function DefaultItem({ item }: { item: TrendingFacetItem }) {
65+
return <Fragment>{item.facetValue}</Fragment>;
66+
}
67+
68+
function DefaultEmpty() {
69+
return <Fragment>No results</Fragment>;
70+
}
71+
72+
return function TrendingFacets(userProps: TrendingFacetsProps) {
73+
const {
74+
classNames = {},
75+
emptyComponent: EmptyComponent = DefaultEmpty,
76+
headerComponent: HeaderComponent = DefaultHeader,
77+
itemComponent: ItemComponent = DefaultItem,
78+
items,
79+
status,
80+
translations: userTranslations,
81+
...props
82+
} = userProps;
83+
84+
const translations: Required<TrendingFacetsTranslations> = {
85+
title: 'Trending',
86+
...userTranslations,
87+
};
88+
89+
const cssClasses: TrendingFacetsClassNames = {
90+
root: cx('ais-TrendingFacets', classNames.root),
91+
emptyRoot: cx(
92+
'ais-TrendingFacets',
93+
classNames.root,
94+
'ais-TrendingFacets--empty',
95+
classNames.emptyRoot,
96+
props.className
97+
),
98+
title: cx('ais-TrendingFacets-title', classNames.title),
99+
container: cx('ais-TrendingFacets-container', classNames.container),
100+
list: cx('ais-TrendingFacets-list', classNames.list),
101+
item: cx('ais-TrendingFacets-item', classNames.item),
102+
};
103+
104+
if (items.length === 0 && status === 'idle') {
105+
return (
106+
<section {...props} className={cssClasses.emptyRoot}>
107+
<EmptyComponent />
108+
</section>
109+
);
110+
}
111+
112+
return (
113+
<section {...props} className={cssClasses.root}>
114+
<HeaderComponent
115+
classNames={cssClasses}
116+
items={items}
117+
translations={translations}
118+
/>
119+
120+
<div className={cssClasses.container}>
121+
<ol className={cssClasses.list}>
122+
{items.map((item, index) => (
123+
<li
124+
key={`${item.facetName}:${item.facetValue}:${index}`}
125+
className={cssClasses.item}
126+
>
127+
<ItemComponent item={item} />
128+
</li>
129+
))}
130+
</ol>
131+
</div>
132+
</section>
133+
);
134+
};
135+
}

0 commit comments

Comments
 (0)