Skip to content

Commit b55881a

Browse files
committed
feat(recommend): add Trending-Facets model
1 parent 7bba206 commit b55881a

File tree

14 files changed

+790
-43
lines changed

14 files changed

+790
-43
lines changed

examples/js/getting-started/src/app.js

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
panel,
99
refinementList,
1010
searchBox,
11-
trendingItems,
11+
trendingFacets,
1212
} from 'instantsearch.js/es/widgets';
1313

1414
import 'instantsearch.css/themes/satellite.css';
@@ -56,19 +56,14 @@ search.addWidgets([
5656
pagination({
5757
container: '#pagination',
5858
}),
59-
trendingItems({
59+
trendingFacets({
6060
container: '#trending',
6161
limit: 6,
62+
facetName: 'brand',
6263
templates: {
6364
item: (item, { html }) => html`
6465
<div>
65-
<article>
66-
<div>
67-
<img src="${item.image}" />
68-
<h2>${item.name}</h2>
69-
</div>
70-
<a href="/products.html?pid=${item.objectID}">See product</a>
71-
</article>
66+
<h2>${item.facetName}: ${item.facetValue}</h2>
7267
</div>
7368
`,
7469
layout: carousel(),

examples/react/getting-started/src/App.tsx

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Pagination,
1010
RefinementList,
1111
SearchBox,
12-
TrendingItems,
12+
TrendingFacets,
1313
Carousel,
1414
} from 'react-instantsearch';
1515

@@ -61,8 +61,8 @@ export function App() {
6161
<Pagination />
6262
</div>
6363
<div>
64-
<TrendingItems
65-
itemComponent={ItemComponent}
64+
<TrendingFacets
65+
facetName="brand"
6666
limit={6}
6767
layoutComponent={Carousel}
6868
/>
@@ -96,17 +96,3 @@ function HitComponent({ hit }: { hit: HitType }) {
9696
</article>
9797
);
9898
}
99-
100-
function ItemComponent({ item }: { item: Hit }) {
101-
return (
102-
<div>
103-
<article>
104-
<div>
105-
<img src={item.image} />
106-
<h2>{item.name}</h2>
107-
</div>
108-
<a href={`/products.html?pid=${item.objectID}`}>See product</a>
109-
</article>
110-
</div>
111-
);
112-
}

packages/instantsearch-ui-components/src/components/Carousel.tsx

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { cx } from '../lib';
33

44
import { createDefaultItemComponent } from './recommend-shared';
5+
import { isTrendingFacetHit } from './TrendingFacets';
56

67
import type {
78
ComponentProps,
@@ -11,6 +12,7 @@ import type {
1112
Renderer,
1213
SendEventForHits,
1314
} from '../types';
15+
import type { TrendingFacetHit } from './TrendingFacets';
1416

1517
export type CarouselProps<
1618
TObject,
@@ -20,7 +22,7 @@ export type CarouselProps<
2022
nextButtonRef: MutableRef<HTMLButtonElement | null>;
2123
previousButtonRef: MutableRef<HTMLButtonElement | null>;
2224
carouselIdRef: MutableRef<string>;
23-
items: Array<RecordWithObjectID<TObject>>;
25+
items: Array<RecordWithObjectID<TObject> | TrendingFacetHit>;
2426
itemComponent?: (
2527
props: RecommendItemComponentProps<RecordWithObjectID<TObject>> &
2628
TComponentProps
@@ -232,22 +234,41 @@ export function createCarouselComponent({ createElement, Fragment }: Renderer) {
232234
}
233235
}}
234236
>
235-
{items.map((item, index) => (
236-
<li
237-
key={item.objectID}
238-
className={cx(cssClasses.item)}
239-
aria-roledescription="slide"
240-
aria-label={`${index + 1} of ${items.length}`}
241-
onClick={() => {
242-
sendEvent('click:internal', item, 'Item Clicked');
243-
}}
244-
onAuxClick={() => {
245-
sendEvent('click:internal', item, 'Item Clicked');
246-
}}
247-
>
248-
<ItemComponent item={item} sendEvent={sendEvent} />
249-
</li>
250-
))}
237+
{items.map((item, index) =>
238+
isTrendingFacetHit(item) ? (
239+
<li
240+
key={item.facetName + item.facetValue}
241+
className={cx(cssClasses.item)}
242+
aria-roledescription="slide"
243+
aria-label={`${index + 1} of ${items.length}`}
244+
>
245+
<ItemComponent
246+
item={
247+
{
248+
...item,
249+
objectID: item.facetName + item.facetValue,
250+
} as RecordWithObjectID<any>
251+
}
252+
sendEvent={sendEvent}
253+
/>
254+
</li>
255+
) : (
256+
<li
257+
key={item.objectID}
258+
className={cx(cssClasses.item)}
259+
aria-roledescription="slide"
260+
aria-label={`${index + 1} of ${items.length}`}
261+
onClick={() => {
262+
sendEvent('click:internal', item, 'Item Clicked');
263+
}}
264+
onAuxClick={() => {
265+
sendEvent('click:internal', item, 'Item Clicked');
266+
}}
267+
>
268+
<ItemComponent item={item} sendEvent={sendEvent} />
269+
</li>
270+
)
271+
)}
251272
</ol>
252273

253274
<button
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/** @jsx createElement */
2+
3+
import { cx } from '../lib';
4+
5+
import {
6+
createDefaultEmptyComponent,
7+
createDefaultHeaderComponent,
8+
createDefaultItemComponent,
9+
} from './recommend-shared';
10+
11+
import type {
12+
ComponentProps,
13+
RecommendClassNames,
14+
RecommendInnerComponentProps,
15+
RecommendItemComponentProps,
16+
RecommendStatus,
17+
RecommendTranslations,
18+
RecordWithObjectID,
19+
Renderer,
20+
SendEventForHits,
21+
} from '../types';
22+
import type { TrendingFacetHit as Base } from 'algoliasearch';
23+
24+
export type TrendingFacetHit = Base & {
25+
__position: number;
26+
__queryID?: string;
27+
};
28+
29+
type TrendingFacetLayoutProps<TClassNames extends Record<string, string>> = {
30+
classNames: TClassNames;
31+
itemComponent: (
32+
props: RecommendItemComponentProps<TrendingFacetHit>
33+
) => JSX.Element;
34+
items: TrendingFacetHit[];
35+
sendEvent: SendEventForHits;
36+
};
37+
38+
export type TrendingFacetsComponentProps<
39+
TComponentProps extends Record<string, unknown> = Record<string, unknown>
40+
> = {
41+
itemComponent?: (
42+
props: RecommendItemComponentProps<TrendingFacetHit> & TComponentProps
43+
) => JSX.Element;
44+
items: TrendingFacetHit[];
45+
sendEvent: SendEventForHits;
46+
classNames?: Partial<RecommendClassNames>;
47+
emptyComponent?: (props: TComponentProps) => JSX.Element;
48+
headerComponent?: (
49+
props: RecommendInnerComponentProps<TrendingFacetHit> & TComponentProps
50+
) => JSX.Element;
51+
status: RecommendStatus;
52+
translations?: Partial<RecommendTranslations>;
53+
layout?: (
54+
props: TrendingFacetLayoutProps<Record<string, string>> & TComponentProps
55+
) => JSX.Element;
56+
};
57+
58+
export type TrendingFacetsProps<
59+
TComponentProps extends Record<string, unknown> = Record<string, unknown>
60+
> = ComponentProps<'div'> & TrendingFacetsComponentProps<TComponentProps>;
61+
62+
export function createTrendingFacetsComponent({
63+
createElement,
64+
Fragment,
65+
}: Renderer) {
66+
return function TrendingFacets(userProps: TrendingFacetsProps) {
67+
const {
68+
classNames = {},
69+
emptyComponent: EmptyComponent = createDefaultEmptyComponent({
70+
createElement,
71+
Fragment,
72+
}),
73+
headerComponent: HeaderComponent = createDefaultHeaderComponent({
74+
createElement,
75+
Fragment,
76+
}),
77+
itemComponent: ItemComponent = createDefaultItemComponent({
78+
createElement,
79+
Fragment,
80+
}),
81+
layout: Layout = createListComponent({ createElement, Fragment }),
82+
items,
83+
status,
84+
translations: userTranslations,
85+
sendEvent,
86+
...props
87+
} = userProps;
88+
89+
const translations: Required<RecommendTranslations> = {
90+
title: 'Trending facets',
91+
sliderLabel: 'Trending facets',
92+
...userTranslations,
93+
};
94+
95+
const cssClasses: RecommendClassNames = {
96+
root: cx('ais-TrendingFacets', classNames.root),
97+
emptyRoot: cx(
98+
'ais-TrendingFacets',
99+
classNames.root,
100+
'ais-TrendingFacets--empty',
101+
classNames.emptyRoot,
102+
props.className
103+
),
104+
title: cx('ais-TrendingFacets-title', classNames.title),
105+
container: cx('ais-TrendingFacets-container', classNames.container),
106+
list: cx('ais-TrendingFacets-list', classNames.list),
107+
item: cx('ais-TrendingFacets-item', classNames.item),
108+
};
109+
110+
if (items.length === 0 && status === 'idle') {
111+
return (
112+
<section {...props} className={cssClasses.emptyRoot}>
113+
<EmptyComponent />
114+
</section>
115+
);
116+
}
117+
118+
return (
119+
<section {...props} className={cssClasses.root}>
120+
<HeaderComponent
121+
classNames={cssClasses}
122+
items={items}
123+
translations={translations}
124+
/>
125+
126+
<Layout
127+
classNames={cssClasses}
128+
itemComponent={ItemComponent}
129+
items={items}
130+
sendEvent={sendEvent}
131+
/>
132+
</section>
133+
);
134+
};
135+
}
136+
137+
export function createListComponent({ createElement }: Renderer) {
138+
return function List(
139+
userProps: TrendingFacetLayoutProps<Partial<RecommendClassNames>>
140+
) {
141+
const {
142+
classNames = {},
143+
itemComponent: ItemComponent,
144+
items,
145+
sendEvent,
146+
} = userProps;
147+
148+
return (
149+
<div className={classNames.container}>
150+
<ol className={classNames.list}>
151+
{items.map((item) => (
152+
<li
153+
key={item.facetName + item.facetValue}
154+
className={classNames.item}
155+
>
156+
<ItemComponent item={item} sendEvent={sendEvent} />
157+
</li>
158+
))}
159+
</ol>
160+
</div>
161+
);
162+
};
163+
}
164+
165+
export const isTrendingFacetHit = (
166+
item: RecordWithObjectID<any> | TrendingFacetHit
167+
): item is TrendingFacetHit => !item.objectID && 'facetValue' in item;

packages/instantsearch-ui-components/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * from './Hits';
55
export * from './LookingSimilar';
66
export * from './RelatedProducts';
77
export * from './TrendingItems';
8+
export * from './TrendingFacets';

packages/instantsearch.js/src/connectors/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,5 @@ export { default as connectVoiceSearch } from './voice-search/connectVoiceSearch
5454
export { default as connectRelevantSort } from './relevant-sort/connectRelevantSort';
5555
export { default as connectFrequentlyBoughtTogether } from './frequently-bought-together/connectFrequentlyBoughtTogether';
5656
export { default as connectLookingSimilar } from './looking-similar/connectLookingSimilar';
57+
export { default as connectTrendingFacets } from './trending-facets/connectTrendingFacets';
58+
export type { TrendingFacetHit } from './trending-facets/connectTrendingFacets';

0 commit comments

Comments
 (0)