Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat(InfiniteHits): add an infinite hits widgets with load more (#1483)
fixes #1344
  • Loading branch information
bobylito authored and vvo committed Nov 14, 2016
1 parent a947404 commit b446cb2
Show file tree
Hide file tree
Showing 17 changed files with 1,283 additions and 3 deletions.
299 changes: 299 additions & 0 deletions docgen/src/examples/e-commerce-infinite/App.js
@@ -0,0 +1,299 @@
/* eslint react/prop-types: 0 */

import React from 'react';
import {createConnector} from 'react-instantsearch';
import {
InstantSearch,
HierarchicalMenu,
RefinementList,
SortBy,
Stats,
ClearAll,
RangeRatings,
RangeInput,
} from 'react-instantsearch/dom';
import {
connectSearchBox,
connectRefinementList,
connectInfiniteHits,
connectMultiRange,
} from 'react-instantsearch/connectors';

export default function App() {
return (
<InstantSearch
className="container-fluid"
appId="latency"
apiKey="6be0576ff61c053d5f9a3225e2a90f76"
indexName="ikea"
>
<div>
<Header />
<div className="content-wrapper">
<Facets />
<CustomResults />
</div>
</div>
</InstantSearch>
);
}

const Header = () =>
<header className="content-wrapper">
<a href="https://community.algolia.com/instantsearch.js/" className="is-logo"><img
src="https://res.cloudinary.com/hilnmyskv/image/upload/w_100,h_100,dpr_2.0//v1461180087/logo-instantsearchjs-avatar.png"
width={40}/></a>
<a href="./" className="logo">amazing</a>
<ConnectedSearchBox/>
</header>;

const Facets = () =>
<aside>

<ClearAll
translations={{
reset: 'Clear all filters',
}}
/>

<SideBarSection
title="Show results for"
items={[
<HierarchicalMenu
id="categories"
key="categories"
attributes={[
'category',
'sub_category',
'sub_sub_category',
]}
/>,
]}
/>

<SideBarSection
title="Refine by"
items={[
<RefinementListWithTitle
title="Materials"
key="Materials"
item={<RefinementList attributeName="materials" operator="or" limitMin={10}/>}
/>,
<RefinementListWithTitle
title="Color"
key="Color"
item={<ConnectedColorRefinementList attributeName="colors" operator="or"/>}
/>,
<RefinementListWithTitle
title="Rating"
key="rating"
item={<RangeRatings attributeName="rating" max={5}/>}
/>,
<RefinementListWithTitle
title="Price"
key="Price"
item={ <CustomPriceRanges
attributeName="price"
id="price_ranges"
items={[
{end: 10},
{start: 10, end: 20},
{start: 20, end: 50},
{start: 50, end: 100},
{start: 100, end: 300},
{start: 300, end: 500},
{start: 500},
]}
/>}
/>,
<RangeInput key="price_input" attributeName="price" id="price_input"/>,
]}
/>
<div className="thank-you">Data courtesy of <a href="http://www.ikea.com/">ikea.com</a></div>
</aside>
;

const SideBarSection = ({title, items}) =>
<section className="facet-wrapper">
<div className="facet-category-title facet">{title}</div>
{items.map(i => i)}
</section>;

const RefinementListWithTitle = ({title, item}) =>
<div>
<div className="facet-title">{title}</div>
{item}
</div>;

const CustomCheckbox = ({query, refine}) =>
<div className="input-group">
<input type="text"
value={query}
onChange={e => refine(e.target.value)}
className="form-control"
id="q"/>
<span className="input-group-btn">
<button className="btn btn-default"><i className="fa fa-search"></i></button>
</span>
</div>
;

const ColorItem = ({item, createURL, refine}) => {
const active = item.isRefined ? 'checked' : '';
return (
<a
className={`${active} facet-color`}
href={createURL(item.value)}
onClick={e => {
e.preventDefault();
refine(item.value);
}}
data-facet-value={item.label}
>
</a>
);
};

const CustomColorRefinementList = ({items, refine, createURL}) =>
<div>
{items.map(item =>
<ColorItem
key={item.label}
item={item}
refine={refine}
createURL={createURL}
/>
)}
</div>
;

function CustomHits({hits, refine, hasMore}) {
const loadMoreButton = hasMore ?
<button onClick={refine} className="btn btn-primary btn-block">Load more</button> :
<button disabled className="btn btn-primary btn-block">Load more</button>;
return (
<main id="hits">
{hits.map((hit, idx) =>
<Hit item={hit} key={idx}/>
)}
{loadMoreButton}
</main>
);
}

const Hit = ({item}) => {
const icons = [];
for (let i = 0; i < 5; i++) {
const suffix = i >= item.rating ? '_empty' : '';
icons.push(<label key={i} label className={`ais-RangeRatings__ratingIcon${suffix}`}></label>);
}
return (
<article className="hit">
<div className="product-picture-wrapper">
<div className="product-picture"><img src={`${item.image}`}/></div>
</div>
<div className="product-desc-wrapper">
<div className="product-name" dangerouslySetInnerHTML={{__html: item._highlightResult.name.value}}/>
<div className="product-type" dangerouslySetInnerHTML={{__html: item._highlightResult.type.value}}/>
<div className="ais-RangeRatings__ratingLink">
{icons}
<div className="product-price">${item.price}</div>
</div>
</div>
</article>);
};

const CustomResults = createConnector({
displayName: 'CustomResults',

getProps(props, state, search) {
const noResults = search.results ? search.results.nbHits === 0 : false;
return {query: state.q, noResults};
},
})(({noResults, query}) => {
if (noResults) {
return (
<div className="results-wrapper">
<div className="no-results">
No results found matching <span className="query">{query}</span>
</div>
</div>
);
} else {
return (
<div className="results-wrapper">
<section id="results-topbar">
<div className="sort-by">
<label>Sort by</label>
<SortBy
items={[
{value: 'ikea', label: 'Featured'},
{value: 'ikea_price_asc', label: 'Price asc.'},
{value: 'ikea_price_desc', label: 'Price desc.'},
]}
defaultRefinement="ikea"
/>
</div>
<Stats />
</section>
<ConnectedHits hitsPerPage={16}/>
</div>
);
}
});

const CustomPriceRanges = connectMultiRange(React.createClass({
checkIfNeedReset(value) {
const selectedItem = this.props.selectedItem === value ? '' : value;
this.props.refine(selectedItem);
},

filteredItems(items) {
let filteredItems = items;
if (!(this.props.selectedItem === '')) {
filteredItems = items.filter(i => this.props.selectedItem === i.value);
}
return filteredItems;
},

render() {
const {items, refine} = this.props;
const ranges = this.filteredItems(items).map(item => {
const min = parseFloat(item.value.split(':')[0]);
const max = parseFloat(item.value.split(':')[1]);

let label;
if (Number.isNaN(min)) {
label = `≤$${max}`;
} else if (Number.isNaN(max)) {
label = `≥$${min}`;
} else {
label = `$${min} - $${max}`;
}

return <PriceRange label={label} key={item.value} value={item.value}
refine={refine} onClick={this.checkIfNeedReset}/>;
});

return (
<ul className="ais-price-ranges--list">
{ranges}
</ul>
);
},
}));

const PriceRange = ({label, value, onClick}) =>
<li className="ais-price-ranges--item">
<a
key={value}
onClick={onClick.bind(null, value)}
className="ais-price-ranges--link"
>
{label}
</a>
</li>;

const ConnectedSearchBox = connectSearchBox(CustomCheckbox);
const ConnectedColorRefinementList = connectRefinementList(CustomColorRefinementList);
const ConnectedHits = connectInfiniteHits(CustomHits);
12 changes: 12 additions & 0 deletions docgen/src/examples/e-commerce-infinite/index.html
@@ -0,0 +1,12 @@
---
title: E-commerce with infinite hits
layout: example.pug
name: e-commerce-infinite
category: examples
source: https://github.com/algolia/instantsearch.js/tree/v2/docgen/src/examples/e-commerce-infinite
navWeight: 1
stylesheets:
- https://cdn.jsdelivr.net/bootstrap/3.3.5/css/bootstrap.min.css
- https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css
---
<div id="app"></div>
23 changes: 23 additions & 0 deletions docgen/src/examples/e-commerce-infinite/index.js
@@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {AppContainer} from 'react-hot-loader';

import App from './App.js';

ReactDOM.render(<AppContainer><App/></AppContainer>, document.querySelector('#app'));

// Hot Module Replacement API
// this and AppContainer are react-hot-loader 3 API needs
// https://github.com/gaearon/react-hot-loader/blob/123d940ff34c3178549fec9d57b9378ff48b4841/docs/README.md
if (module.hot) {
module.hot.accept('./App.js', () => {
const NextApp = require('./App.js').default;
ReactDOM.render(
<AppContainer>
<NextApp/>
</AppContainer>
,
document.querySelector('#app')
);
});
}

0 comments on commit b446cb2

Please sign in to comment.