Skip to content

Commit 34c3d83

Browse files
long-wan-epShaun Maharaj
andauthored
Added coveo integration and search provider switching (#100)
* Added coveo integration and search switching * Fixed config names * - Update README, add Coveo env vars to config * Moved unsupported search message to locales Co-authored-by: Shaun Maharaj <Shaun.Maharaj@elasticpath.com>
1 parent bc3cb71 commit 34c3d83

16 files changed

+901
-180
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
4747
<td align="center"><a href="https://github.com/skarpyak"><img src="https://avatars3.githubusercontent.com/u/8594755?v=4" width="100px;" alt=""/><br /><sub><b>Sergii Karpiak</b></sub></a><br /><a href="https://github.com/elasticpath/epcc-react-pwa-reference-storefront/commits?author=skarpyak" title="Code">💻</a></td>
4848
<td align="center"><a href="https://github.com/BonnieEP"><img src="https://avatars3.githubusercontent.com/u/49495842?v=4" width="100px;" alt=""/><br /><sub><b>Bonnie Bishop</b></sub></a><br /><a href="https://ui-components.elasticpath.com" title="Design">🎨</td>
4949
<td align="center"><a href="https://github.com/JenSmith-EP"><img src="https://avatars3.githubusercontent.com/u/58435007?v=4" width="100px;" alt=""/><br /><sub><b>Jen Smith</b></sub></a><br /><a href="https://documentation.elasticpath.com/storefront-react" title="Documentation">📖</a></td>
50+
<td align="center"><a href="https://github.com/maennis-ep"><img src="https://avatars3.githubusercontent.com/u/25517396?v=4" width="100px;" alt=""/><br /><sub><b>Mark Ennis</b></sub></a><br /><a href="https://github.com/elasticpath/epcc-react-pwa-reference-storefront/commits?author=maennis-ep" title="Code">💻</a></td>
51+
<td align="center"><a href="https://github.com/plundaahl-ep"><img src="https://avatars3.githubusercontent.com/u/54957521?v=4" width="100px;" alt=""/><br /><sub><b>plundaahl-ep</b></sub></a><br /><a href="https://github.com/elasticpath/epcc-react-pwa-reference-storefront/commits?author=plundaahl-ep" title="Code">💻</a></td>
52+
</tr>
53+
<tr>
5054
<td align="center"><a href="https://github.com/mwan-ep"><img src="https://avatars3.githubusercontent.com/u/54115904?v=4" width="100px;" alt=""/><br /><sub><b>Michelle Wan</b></sub></a><br /><a href="https://github.com/elasticpath/epcc-react-pwa-reference-storefront/commits?author=mwan-ep" title="Code">💻</a></td>
55+
<td align="center"><a href="https://github.com/long-wan-ep"><img src="https://avatars3.githubusercontent.com/u/50891790?v=4" width="100px;" alt=""/><br /><sub><b>long-wan-ep</b></sub></a><br /><a href="https://github.com/elasticpath/epcc-react-pwa-reference-storefront/commits?author=long-wan-ep" title="Code">💻</a></td>
5156
</tr>
5257
</table>
5358

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"algoliasearch": "^4.3.0",
1414
"app-localizer": "^1.2.2",
1515
"constate": "^3.1.0",
16+
"coveo-search-ui": "^2.10081.2",
1617
"css-reset-and-normalize": "^2.1.0",
1718
"eslint-plugin-jsx-a11y": "^6.3.1",
1819
"formik": "^2.1.4",
File renamed without changes.

src/AlgoliaSearch.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React from 'react';
2+
import { Hits, SortBy, Pagination } from 'react-instantsearch-dom'
3+
import { ProductHit } from './ProductHit';
4+
import { CustomRefinementList } from './CustomRefinementList';
5+
import { useTranslation } from './app-state';
6+
import { config } from './config';
7+
8+
import './AlgoliaSearch.scss';
9+
10+
interface SearchParams {
11+
}
12+
13+
export const AlgoliaSearch: React.FC<SearchParams> = () => {
14+
const { t } = useTranslation();
15+
16+
const Hit = ({ hit }: any) => (
17+
<div className="search__product">
18+
<ProductHit hit={hit} />
19+
</div>
20+
);
21+
22+
const Facets = () => (
23+
<div className="search__facets">
24+
<div>
25+
<h2 className="search__facetstitle">
26+
{t('filter-by')}
27+
</h2>
28+
<SortBy
29+
key="facets-SortBy"
30+
defaultRefinement={config.algoliaIndexName}
31+
items={[
32+
{ value: config.algoliaIndexName, label: t('featured') },
33+
{ value: `${config.algoliaIndexName}_price_asc`, label: t('price-asc') },
34+
{ value: `${config.algoliaIndexName}_price_desc`, label: t('price-desc') }
35+
]}
36+
/>
37+
</div>
38+
<CustomRefinementList key="facets-list-1" title={t('category')} attribute="categories" />
39+
<CustomRefinementList key="facets-list-2" title={t('collection')} attribute="collections" />
40+
<CustomRefinementList key="facets-list-3" title={t('brand')} attribute="brands" />
41+
</div>
42+
);
43+
44+
return (
45+
<div className="search">
46+
<h1 className="search__title">
47+
{t('search')}
48+
</h1>
49+
<input type="checkbox" id="checkbox" className="search__facetstoggleinput"/>
50+
<label htmlFor="checkbox" className="search__facetstoggle epbtn --bordered">
51+
{t('filter')}
52+
</label>
53+
<Facets key="search-facets" />
54+
<div className="search__productlist">
55+
<Hits hitComponent={Hit} />
56+
</div>
57+
<div className="search__pagination">
58+
<Pagination showFirst={false} />
59+
</div>
60+
</div>
61+
);
62+
};

src/AlgoliaSearchBar.tsx

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React, { useState } from 'react';
2+
import { Link, useHistory } from 'react-router-dom';
3+
import useOnclickOutside from 'react-cool-onclickoutside';
4+
import { SearchBox, Hits, PoweredBy, VoiceSearch } from 'react-instantsearch-dom';
5+
import { useTranslation } from './app-state';
6+
import { createSearchUrl } from './routes';
7+
8+
import { ReactComponent as MagnifyingGlassIcon } from './images/icons/magnifying_glass.svg';
9+
import { ReactComponent as ClearIcon } from './images/icons/ic_clear.svg';
10+
11+
import './AlgoliaSearchBar.scss';
12+
13+
interface SearchBoxProps {
14+
}
15+
16+
const SearchButtonMic = (props: any) => (
17+
props.islistening==="true" ?
18+
<span className="VoiceSearchButtonBreathing"
19+
{...props} /> :
20+
<span className="VoiceSearchButton"
21+
{...props} />
22+
);
23+
24+
export const AlgoliaSearchBar: React.FC<SearchBoxProps> = () => {
25+
const { t } = useTranslation();
26+
const [ inputVisible, setInputVisible] = useState(false);
27+
const [ hitsVisible, setHitsVisible ] = useState(false);
28+
const [ searchValue, setsSearchValue ] = useState(false);
29+
const history = useHistory();
30+
31+
const searchBarRef = useOnclickOutside(() => {
32+
setHitsVisible(false);
33+
});
34+
35+
const handleChange = (event: any) => {
36+
setsSearchValue(event.target.value)
37+
};
38+
39+
const handleSubmit = (event: any) => {
40+
event.preventDefault();
41+
setHitsVisible(false);
42+
const searchUrl = createSearchUrl();
43+
if(searchUrl !== history.location.pathname) {
44+
history.push(searchUrl);
45+
}
46+
};
47+
48+
const handleFocus = () => {
49+
setHitsVisible(true);
50+
setInputVisible(true);
51+
};
52+
53+
const handleLinkClick = () => {
54+
setHitsVisible(false);
55+
};
56+
57+
const handleInputToggle = () => {
58+
setInputVisible(!inputVisible);
59+
};
60+
61+
const onCancel = () => {
62+
setInputVisible(false);
63+
};
64+
65+
const VoiceSearchButtonText = ({
66+
isListening,
67+
isBrowserSupported
68+
}: {isListening:any, isBrowserSupported:any}) => (
69+
isBrowserSupported ? (
70+
isListening ?
71+
<SearchButtonMic
72+
islistening={isListening.toString()}
73+
/> :
74+
<SearchButtonMic
75+
islistening={isListening.toString()}
76+
onClick={() => handleFocus()}
77+
/>
78+
) : null
79+
);
80+
81+
const Hit = ({ hit }: any) => {
82+
return (
83+
<Link className="searchbar__hint" to={`/product/${hit.slug}`} onClick={handleLinkClick}>
84+
<img
85+
className="searchbar__image"
86+
src={hit.imgUrl}
87+
alt=""
88+
/>
89+
<p className="searchbar__hint-text">{hit.name}</p>
90+
</Link>
91+
);
92+
};
93+
94+
const translations = {
95+
placeholder: t('search-here'),
96+
};
97+
98+
return (
99+
<div ref={searchBarRef} className="searchbar">
100+
<button
101+
className="searchbar__open"
102+
onClick={handleInputToggle}
103+
aria-label={t('search')}
104+
>
105+
<MagnifyingGlassIcon />
106+
</button>
107+
<div className={`searchbar__input ${inputVisible ? '--show' : ''}`}>
108+
<SearchBox
109+
onFocus={handleFocus}
110+
onChange={handleChange}
111+
onReset={handleChange}
112+
searchAsYouType
113+
onSubmit={handleSubmit}
114+
submit={<MagnifyingGlassIcon />}
115+
reset={<ClearIcon />}
116+
translations={translations}
117+
/>
118+
<VoiceSearch
119+
searchAsYouSpeak={true}
120+
buttonTextComponent={VoiceSearchButtonText}
121+
/>
122+
<button
123+
className={`searchbar__close ${searchValue && '--show'}`}
124+
onClick={onCancel}>
125+
{t('cancel')}
126+
</button>
127+
{ hitsVisible &&
128+
<div className="searchbar__hints">
129+
<Hits
130+
hitComponent={Hit}
131+
/>
132+
<PoweredBy />
133+
</div>
134+
}
135+
</div>
136+
</div>
137+
);
138+
};

src/CoveoSearch.scss

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
@import './theme/common';
2+
3+
.CoveoSearchButton {
4+
color: $mainNavigationColor !important;
5+
border: 1px solid $secondComplimentTextColor !important;
6+
border-left: none !important;
7+
text-decoration: none !important;
8+
text-align: center !important;
9+
vertical-align: middle !important;
10+
overflow: hidden !important;
11+
height: 50px !important;
12+
width: 60px !important;
13+
cursor: pointer !important;
14+
line-height: 0 !important;
15+
}
16+
17+
.CoveoImageFieldValue {
18+
img {
19+
object-fit: contain;
20+
}
21+
}
22+
23+
.coveo-result-list-container {
24+
justify-content: space-between;
25+
}
26+
27+
.coveo-card-layout.CoveoResult {
28+
min-width: 30%;
29+
max-width: 30%;
30+
}
31+
32+
.coveo-dynamic-facet-header-title, .coveo-dynamic-facet-collapse-toggle-svg,
33+
.CoveoResultLink, a.CoveoResultLink, .CoveoResult a.CoveoResultLink, .coveo-results-per-page-list-item,
34+
.coveo-search-button-svg {
35+
color: $firstComplimentColor;
36+
}
37+
38+
.CoveoSort.coveo-selected, .CoveoSort.coveo-selected:hover {
39+
border-bottom-color: $firstComplimentColor;
40+
}
41+
42+
.coveo-results-per-page-list-item.coveo-active, .coveo-results-per-page-list-item:hover {
43+
background-color: $firstComplimentColor;
44+
}
45+
46+
@media (max-width: $mobileWidth - 1px) {
47+
.coveo-card-layout.CoveoResult {
48+
min-width: 100%;
49+
max-width: 100%;
50+
}
51+
}

src/CoveoSearch.tsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/* eslint-disable jsx-a11y/anchor-has-content */
2+
/* eslint-disable jsx-a11y/anchor-is-valid */
3+
import React, { useEffect, useRef } from 'react';
4+
import { useHistory } from 'react-router-dom';
5+
import * as Coveo from 'coveo-search-ui';
6+
import 'coveo-search-ui/bin/css/CoveoFullSearch.css';
7+
import 'coveo-search-ui/bin/js/templates/templates.js';
8+
9+
import './CoveoSearch.scss';
10+
import { config } from './config';
11+
12+
export const CoveoSearch: React.FC = () => {
13+
const searchRef = useRef<any>(null);
14+
const history = useHistory();
15+
16+
const getTemplateContent = () => {
17+
return `
18+
<div class="coveo-result-frame">
19+
<div class="coveo-result-cell" style="vertical-align: top;">
20+
<div class="coveo-result-row">
21+
<div class="coveo-result-cell" style="text-align:center">
22+
<a class='CoveoImageFieldValue' data-field="@imgUrl" data-height='200' data-width='200'></a>
23+
</div>
24+
</div>
25+
<div class="coveo-result-row" style="margin-top:0;">
26+
<div class="coveo-result-cell" style="font-size:16px; text-align:center" role="heading" aria-level="2">
27+
<a class="CoveoResultLink"></a>
28+
</div>
29+
</div>
30+
<div class="coveo-result-row" style="margin-top:10px;">
31+
<div class="coveo-result-cell" style="text-align:center">
32+
<span class="CoveoFieldValue" data-field="@price"></span>
33+
</div>
34+
</div>
35+
</div>
36+
</div>
37+
`
38+
}
39+
40+
useEffect(() => {
41+
Coveo.SearchEndpoint.configureCloudV2Endpoint(config.coveoOrg, config.coveoApiKey);
42+
Coveo.init(searchRef.current, {
43+
ResultLink: {
44+
onClick: (e: any, result: any) => {
45+
e.preventDefault();
46+
if (result.raw.sku) {
47+
history.push(`/product/${result.raw.slug}`)
48+
}
49+
}
50+
}
51+
});
52+
}, [searchRef, history])
53+
54+
return (
55+
<div id="search" className="CoveoSearchInterface" data-expression={`@source==${config.coveoSourceName}`} ref={searchRef} data-enable-history="true">
56+
<div className="CoveoFolding"></div>
57+
<div className="CoveoAnalytics"></div>
58+
<div className="coveo-search-section">
59+
<div className="CoveoSearchbox" data-enable-omnibox="true">
60+
<div className="CoveoFieldSuggestions" data-query-override={`@source==${config.coveoSourceName}`} data-field="@name"></div>
61+
</div>
62+
</div>
63+
<div className="coveo-main-section">
64+
<div className="coveo-facet-column">
65+
<div className="CoveoDynamicFacet" data-field="@collections" data-number-of-values="" data-title="Collection"></div>
66+
<div className="CoveoDynamicFacet" data-title="Categories" data-field="@categories"></div>
67+
<div className="CoveoDynamicFacet" data-title="Brands" data-field="@brands"></div>
68+
</div>
69+
<div className="coveo-results-column">
70+
<div className="CoveoShareQuery"></div>
71+
<div className="CoveoPreferencesPanel">
72+
<div className="CoveoResultsPreferences"></div>
73+
<div className="CoveoResultsFiltersPreferences"></div>
74+
</div>
75+
<div className="CoveoTriggers"></div>
76+
<div className="CoveoBreadcrumb"></div>
77+
<div className="CoveoDidYouMean"></div>
78+
<div className="coveo-results-header">
79+
<div className="coveo-summary-section">
80+
<span className="CoveoQuerySummary"><div className="coveo-show-if-no-results"></div></span>
81+
<span className="CoveoQueryDuration"></span>
82+
</div>
83+
<div className="coveo-result-layout-section">
84+
<span className="CoveoResultLayout"></span>
85+
</div>
86+
<div className="coveo-sort-section">
87+
<span className="CoveoSort" data-sort-criteria="relevancy" data-caption="Relevance"></span>
88+
<span className="CoveoSort" data-caption="Price" data-sort-criteria="@amount descending,@amount ascending"></span>
89+
<span className="CoveoSort" data-sort-criteria="date descending,date ascending" data-caption="Date"></span>
90+
</div>
91+
</div>
92+
<div className="CoveoHiddenQuery"></div>
93+
<div className="CoveoErrorReport" data-pop-up="false"></div>
94+
<div className="CoveoResultList" data-layout="card" data-wait-animation="fade" data-auto-select-fields-to-include="false">
95+
<script className="result-template" type="text/html" data-layout="card" dangerouslySetInnerHTML={{ __html: getTemplateContent() }}></script>
96+
</div>
97+
<div className="CoveoPager"></div>
98+
<div className="CoveoLogo"></div>
99+
<div className="CoveoResultsPerPage"></div>
100+
</div>
101+
</div>
102+
</div>
103+
);
104+
};

0 commit comments

Comments
 (0)