Skip to content

Commit 5bfd850

Browse files
raed667Haroenv
authored andcommitted
feat(recommend): add Trending-Facets model
1 parent eeb0a61 commit 5bfd850

File tree

17 files changed

+814
-113
lines changed

17 files changed

+814
-113
lines changed

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

Lines changed: 0 additions & 79 deletions
This file was deleted.

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

Lines changed: 8 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,10 +61,15 @@ 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}
68+
itemComponent={({ item }) => (
69+
<div>
70+
{item.facetName}:{item.facetValue}
71+
</div>
72+
)}
6873
/>
6974
</div>
7075
</div>
@@ -96,17 +101,3 @@ function HitComponent({ hit }: { hit: HitType }) {
96101
</article>
97102
);
98103
}
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/algoliasearch-helper/types/algoliasearch.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,24 @@ export type RecommendResponses<T> = PickForClient<{
182182
v5: AlgoliaSearch.GetRecommendationsResponse;
183183
}>;
184184

185+
export type TrendingFacetHit = PickForClient<{
186+
v3: any;
187+
// @ts-ignore
188+
v4: {
189+
readonly _score: number;
190+
readonly facetName: string;
191+
readonly facetValue: string;
192+
} & {
193+
__position: number;
194+
__queryID?: string;
195+
};
196+
// @ts-ignore
197+
v5: RecommendClient.TrendingFacetHit & {
198+
__position: number;
199+
__queryID?: string;
200+
};
201+
}>;
202+
185203
// We remove `indexName` from the Recommend query types as the helper
186204
// will fill in this value before sending the queries
187205
type _OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

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

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/** @jsx createElement */
2+
23
import { cx } from '../lib';
34

45
import { createDefaultItemComponent } from './recommend-shared';
6+
import { isTrendingFacetHit } from './TrendingFacets';
57

68
import type {
79
ComponentProps,
@@ -10,6 +12,7 @@ import type {
1012
RecordWithObjectID,
1113
Renderer,
1214
SendEventForHits,
15+
TrendingFacetHit,
1316
} from '../types';
1417

1518
export type CarouselProps<
@@ -20,7 +23,7 @@ export type CarouselProps<
2023
nextButtonRef: MutableRef<HTMLButtonElement | null>;
2124
previousButtonRef: MutableRef<HTMLButtonElement | null>;
2225
carouselIdRef: MutableRef<string>;
23-
items: Array<RecordWithObjectID<TObject>>;
26+
items: Array<RecordWithObjectID<TObject> | TrendingFacetHit>;
2427
itemComponent?: (
2528
props: RecommendItemComponentProps<RecordWithObjectID<TObject>> &
2629
TComponentProps
@@ -232,22 +235,41 @@ export function createCarouselComponent({ createElement, Fragment }: Renderer) {
232235
}
233236
}}
234237
>
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-
))}
238+
{items.map((item, index) =>
239+
isTrendingFacetHit(item) ? (
240+
<li
241+
key={item.facetName + item.facetValue}
242+
className={cx(cssClasses.item)}
243+
aria-roledescription="slide"
244+
aria-label={`${index + 1} of ${items.length}`}
245+
>
246+
<ItemComponent
247+
item={
248+
{
249+
...item,
250+
objectID: item.facetName + item.facetValue,
251+
} as RecordWithObjectID<any>
252+
}
253+
sendEvent={sendEvent}
254+
/>
255+
</li>
256+
) : (
257+
<li
258+
key={item.objectID}
259+
className={cx(cssClasses.item)}
260+
aria-roledescription="slide"
261+
aria-label={`${index + 1} of ${items.length}`}
262+
onClick={() => {
263+
sendEvent('click:internal', item, 'Item Clicked');
264+
}}
265+
onAuxClick={() => {
266+
sendEvent('click:internal', item, 'Item Clicked');
267+
}}
268+
>
269+
<ItemComponent item={item} sendEvent={sendEvent} />
270+
</li>
271+
)
272+
)}
251273
</ol>
252274

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

0 commit comments

Comments
 (0)