Skip to content

Commit

Permalink
feat(react-instantsearch): migrate recommend demo (#484)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhayab committed May 23, 2024
1 parent a886ef8 commit 31c2f26
Show file tree
Hide file tree
Showing 37 changed files with 3,459 additions and 0 deletions.
4 changes: 4 additions & 0 deletions react-instantsearch/recommend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules

/.parcel-cache
/dist
5 changes: 5 additions & 0 deletions react-instantsearch/recommend/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"proseWrap": "never",
"trailingComma": "es5"
}
19 changes: 19 additions & 0 deletions react-instantsearch/recommend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# recommend

## Get started

To run this project locally, install the dependencies and run the local server:

```sh
npm install
npm start
```

Alternatively, you may use [Yarn](https://http://yarnpkg.com/):

```sh
yarn
yarn start
```

Open http://localhost:1234 to see your app.
Binary file added react-instantsearch/recommend/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions react-instantsearch/recommend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<link rel="shortcut icon" href="favicon.png" type="image/x-icon" />

<!--
Do not use @7 in production, use a complete version like x.x.x, see website for latest version:
https://community.algolia.com/react-instantsearch/Getting_started.html#load-the-algolia-theme
-->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/instantsearch.css@8/themes/reset-min.css"
/>

<title>Algolia Recommend</title>
</head>

<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>

<script type="module" src="src/index.tsx"></script>
</body>
</html>
26 changes: 26 additions & 0 deletions react-instantsearch/recommend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "recommend",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "parcel index.html",
"build": "parcel build index.html"
},
"dependencies": {
"@algolia/autocomplete-js": "1.17.1",
"@algolia/autocomplete-theme-classic": "1.17.1",
"@algolia/ui-components-horizontal-slider-react": "1.2.2",
"@algolia/ui-components-horizontal-slider-theme": "1.2.2",
"algoliasearch": "4.23.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-instantsearch": "7.9.0",
"react-router-dom": "^6.23.1",
"search-insights": "2.14.0"
},
"devDependencies": {
"@types/react-dom": "18.3.0",
"parcel": "2.12.0",
"typescript": "5.1.3"
}
}
33 changes: 33 additions & 0 deletions react-instantsearch/recommend/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
* {
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 0.5rem;
}

h3 {
margin-bottom: 0.5rem;
margin-top: 1rem;
}

.container {
margin: 0 auto;
max-width: 1100px;
}

.product-image {
min-width: 150px;
min-height: 200px;
background-color: var(--auc-primary-color);
}

.title a {
text-decoration: none;
color: var(--auc-dark-color);
}
25 changes: 25 additions & 0 deletions react-instantsearch/recommend/src/Recommend.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
:root {
--auc-primary-color: #9698c3;
--auc-muted-color: #c4c4c4;
--auc-dark-color: #0f0f0f;
}

.auc-Recommend-list {
display: grid;
gap: 0.5rem;
list-style: none;
margin: 0;
outline-color: var(--auc-primary-color);
outline-offset: 0.5rem;
padding: 0;
}

.auc-Recommend-list {
grid-template-columns: repeat(2, 1fr);
}

@media (min-width: 999px) {
.auc-Recommend-list {
grid-template-columns: repeat(5, 1fr);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
autocomplete,
AutocompleteOptions,
getAlgoliaResults,
} from '@algolia/autocomplete-js';
import React, { createElement, Fragment, useEffect, useRef } from 'react';
import { render } from 'react-dom';

import { ProductHit } from '../../types';

export function Autocomplete(props: Partial<AutocompleteOptions<ProductHit>>) {
const containerRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
if (!containerRef.current) {
return undefined;
}

const search = autocomplete({
container: containerRef.current,
renderer: { createElement, Fragment, render: () => {} },
render({ children }, root) {
render(children, root);
},
...props,
});

return () => {
search.destroy();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return <div ref={containerRef} />;
}

export { getAlgoliaResults };
38 changes: 38 additions & 0 deletions react-instantsearch/recommend/src/components/BundleItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from 'react';
import { InsightsClient } from 'search-insights';

import { indexName } from '../../config';
import { ProductHit } from '../../types';

type BundleItemProps<TObject> = {
item: TObject;
onSelect(item: TObject): void;
insights: InsightsClient;
};

export const BundleItem: React.FC<BundleItemProps<ProductHit>> = ({
item,
onSelect,
insights,
}) => {
return (
<a
className="Hit Hit-link"
href={item.url}
onClick={(event) => {
event.preventDefault();

onSelect(item);
insights('clickedObjectIDs', {
objectIDs: [item.objectID],
eventName: 'Product Clicked',
index: indexName,
});
}}
>
<div className="Hit-Image">
<img src={item.image_urls[0]} alt={item.name} />
</div>
</a>
);
};
154 changes: 154 additions & 0 deletions react-instantsearch/recommend/src/components/BundleView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React, { useEffect, useMemo, useState } from 'react';

import { BaseObject, BundleViewTranslations } from '../../types';

import './style.css';

function getAmountDefault<TObject extends BaseObject>(items: TObject[]) {
return items.reduce((sum, current) => sum + current.price.value, 0);
}

function formatPriceDefault(price: number) {
return `$${price}`;
}

type BundleViewProps<TObject> = {
currentItem: TObject;
formatPrice?: (price: number) => string;
getAmount?(items: TObject[]): number;
itemComponent({ item }): JSX.Element;
items: TObject[];
translations?: BundleViewTranslations;
};

export function BundleView<TObject extends BaseObject>(
props: BundleViewProps<TObject>
): JSX.Element {
const items = Array.from(new Set([props.currentItem, ...props.items]));
const formatPrice = props.formatPrice || formatPriceDefault;
const getAmount = props.getAmount || getAmountDefault;

const [selectedItems, setSelectedItems] = useState(() => items);
const [price, setPrice] = useState(() => getAmount(selectedItems));
const translations = useMemo(
() => ({
totalPrice: 'Total price',
thisArticle: 'This article',
addToCart: (count: number) => {
if (count === 1) {
return `Add ${count} product to cart`;
}

return `Add ${count} products to cart`;
},
...props.translations,
}),
[props.translations]
);

useEffect(() => {
setPrice(getAmount(selectedItems));
}, [selectedItems, getAmount]);

return (
<div className="uic-BundleView-container">
<div className="uic-BundleView-items">
<ol className="uic-BundleView-list">
{items.map((item, index) => {
const isSelected = Boolean(
selectedItems.find((x) => x.objectID === item.objectID)
);

function onChange(event: React.ChangeEvent<HTMLInputElement>) {
setSelectedItems((items) =>
event.target.checked
? [...items, item]
: items.filter((x) => x.objectID !== item.objectID)
);
}

return (
<li
key={item.objectID}
className={[
'uic-BundleView-item',
isSelected && 'uic-BundleView-item--selected',
]
.filter(Boolean)
.join(' ')}
>
<div className="uic-BundleView-itemContainer">
<div className="uic-BundleView-itemContainer-hit">
<props.itemComponent item={item} />
</div>
{index < items.length - 1 && (
<div className="uic-BundleView-plus">
<svg viewBox="0 0 24 24" fill="none">
<rect
width="24"
height="24"
rx="12"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 5.54169C12.3452 5.54169 12.625 5.82151 12.625 6.16669V17.8334C12.625 18.1785 12.3452 18.4584 12 18.4584C11.6548 18.4584 11.375 18.1785 11.375 17.8334V6.16669C11.375 5.82151 11.6548 5.54169 12 5.54169Z"
fill="#fff"
stroke="#fff"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.54166 12C5.54166 11.6548 5.82148 11.375 6.16666 11.375H17.8333C18.1785 11.375 18.4583 11.6548 18.4583 12C18.4583 12.3452 18.1785 12.625 17.8333 12.625H6.16666C5.82148 12.625 5.54166 12.3452 5.54166 12Z"
fill="#fff"
stroke="#fff"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
)}
</div>

<label className="uic-BundleView-label">
<input
className="uic-BundleView-checkbox"
type="checkbox"
defaultChecked={isSelected}
onChange={onChange}
/>
{item.objectID === props.currentItem.objectID && (
<span className="uic-BundleView-label-currentArticle">
{translations.thisArticle}:
</span>
)}
<span className="uic-BundleView-label-name">{item.name}</span>
<span className="uic-BundleView-label-price">
{formatPrice(item.price.value)}
</span>
</label>
</li>
);
})}
</ol>
</div>

{selectedItems.length > 0 && (
<div className="uic-BundleView-cart">
<div>
{translations.totalPrice}:{' '}
<span className="uic-BundleView-label-price">
{formatPrice(price)}
</span>
</div>
<button className="uic-BundleView-addToCart">
{translations.addToCart(selectedItems.length)}
</button>
</div>
)}
</div>
);
}
Loading

0 comments on commit 31c2f26

Please sign in to comment.