Skip to content
This repository was archived by the owner on Feb 23, 2023. It is now read-only.

Commit 9f0612e

Browse files
authored
Adding Algolia Autocomplete search bar to /docs (#658)
* Adding Algolia Autocomplete searchbar to /docs * Fixes on Autocomplete propTypes and css * Fixing HitItem filename Co-authored-by: Alessandra Karine Carneiro <alessandra.carneiro@ckl.io>
1 parent 4428657 commit 9f0612e

16 files changed

+17338
-15907
lines changed

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
}
2020
},
2121
"dependencies": {
22+
"@algolia/autocomplete-js": "^1.5.4",
23+
"@algolia/autocomplete-plugin-query-suggestions": "^1.5.4",
24+
"@algolia/autocomplete-theme-classic": "^1.5.4",
2225
"@babel/core": "^7.11.4",
2326
"@babel/plugin-proposal-class-properties": "^7.4.0",
2427
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
@@ -35,6 +38,7 @@
3538
"@stellar/prettier-config": "^1.0.1",
3639
"@typescript-eslint/eslint-plugin": "3.9.1",
3740
"@typescript-eslint/parser": "3.9.1",
41+
"algoliasearch": "^4.13.0",
3842
"babel-core": "^7.0.0-bridge.0",
3943
"babel-eslint": "10.x",
4044
"babel-jest": "^26.3.0",
+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { autocomplete } from "@algolia/autocomplete-js";
2+
import React, { createElement, Fragment, useEffect, useRef } from "react";
3+
import { render } from "react-dom";
4+
5+
import { ListItemPreview } from "./ListItemPreview";
6+
import { TableItemPreview } from "./TableItemPreview";
7+
import { QuoteItemPreview } from "./QuoteItemPreview";
8+
import { ContentPreview } from "./ContentPreview";
9+
import { CodeBlockPreview } from "./CodeBlockPreview";
10+
11+
export function Autocomplete(props) {
12+
const containerRef = useRef(null);
13+
14+
useEffect(() => {
15+
if (!containerRef.current) {
16+
return undefined;
17+
}
18+
19+
const search = autocomplete({
20+
container: containerRef.current,
21+
debug: true,
22+
renderer: { createElement, Fragment },
23+
renderNoResults({ state }, root) {
24+
render(
25+
<div className="aa-NoResults">
26+
<p>
27+
No results for <strong>"{state.query}"</strong>.
28+
</p>
29+
</div>,
30+
root,
31+
);
32+
},
33+
render({ children, state, components }, root) {
34+
const { preview } = state.context;
35+
render(
36+
<div className="aa-Grid">
37+
<div className="aa-Results aa-Column">{children}</div>
38+
<div className="aa-Preview aa-Column">
39+
{preview && children && (
40+
<div className="aa-PreviewSection">
41+
<div className="aa-PreviewSectionContent">
42+
<div className="aa-PreviewBreadcrumb">
43+
<span>
44+
{preview.urlSegments.level1}{" "}
45+
{preview.urlSegments.level2 &&
46+
`> ${preview.urlSegments.level2}`}
47+
</span>
48+
</div>
49+
<div className="aa-TitlePreview">
50+
<a href={preview.url}>
51+
<h1>
52+
<components.Highlight
53+
hit={preview}
54+
attribute="title"
55+
/>
56+
</h1>
57+
</a>
58+
</div>
59+
<div className="aa-ContentPreview">
60+
{preview.schema === "LIST_ITEM" && (
61+
<ListItemPreview
62+
preview={preview}
63+
components={components}
64+
/>
65+
)}
66+
{preview.schema === "TABLE_ITEM" &&
67+
preview.tableAttributes &&
68+
preview.tableAttributes.headers.length > 0 && (
69+
<TableItemPreview
70+
preview={preview}
71+
components={components}
72+
/>
73+
)}
74+
{preview.schema === "QUOTE" && (
75+
<QuoteItemPreview
76+
preview={preview}
77+
components={components}
78+
/>
79+
)}
80+
{preview.schema.startsWith("PARAGRAPH") && (
81+
<ContentPreview
82+
preview={preview}
83+
components={components}
84+
/>
85+
)}
86+
</div>
87+
{preview.schema === "PARAGRAPH_WITH_CODE" && (
88+
<CodeBlockPreview />
89+
)}
90+
{preview.pageOutlines.length > 0 && (
91+
<div className="aa-Outline">
92+
<h2 className="aa-OutlineTitle">On this page</h2>
93+
<ol className="aa-OutlineList">
94+
{preview.pageOutlines.map((pageOutline) => (
95+
<li
96+
key={pageOutline.url}
97+
className="aa-OutlineLink"
98+
>
99+
<a href={pageOutline.url}>{pageOutline.title}</a>
100+
</li>
101+
))}
102+
</ol>
103+
</div>
104+
)}
105+
</div>
106+
</div>
107+
)}
108+
</div>
109+
</div>,
110+
root,
111+
);
112+
},
113+
...props,
114+
});
115+
116+
return () => {
117+
search.destroy();
118+
};
119+
}, [props]);
120+
121+
return <div className="aa-AutocompleteSearchbar" ref={containerRef} />;
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from "react";
2+
3+
export function CodeBlockPreview() {
4+
return (
5+
<div className="aa-CodePreview">
6+
<p>{`</>`}</p>
7+
</div>
8+
);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
export function ContentPreview({ preview, components }) {
5+
return <components.Highlight hit={preview} attribute="content" />;
6+
}
7+
8+
ContentPreview.propTypes = {
9+
preview: PropTypes.object.isRequired,
10+
components: PropTypes.object.isRequired,
11+
};
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
export function HitItem({ item, components }) {
5+
return (
6+
<a href={item.url} className="aa-ItemLink">
7+
<div className="aa-ItemWrapper">
8+
<div className="aa-ItemContent">
9+
<div className="aa-ItemContentBody">
10+
<div className="aa-ItemContentTitle">
11+
<components.Highlight hit={item} attribute="title" />
12+
</div>
13+
<div className="aa-ItemContentDescription">
14+
{item.urlSegments.level1}{" "}
15+
{item.urlSegments?.level2 && `> ${item.urlSegments?.level2}`}
16+
</div>
17+
</div>
18+
</div>
19+
</div>
20+
</a>
21+
);
22+
}
23+
24+
HitItem.propTypes = {
25+
item: PropTypes.object.isRequired,
26+
components: PropTypes.object.isRequired,
27+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
export function ListItemPreview({ preview, components }) {
5+
return (
6+
<ul>
7+
<li>
8+
<components.Highlight hit={preview} attribute="content" />
9+
</li>
10+
<li>...</li>
11+
</ul>
12+
);
13+
}
14+
15+
ListItemPreview.propTypes = {
16+
preview: PropTypes.object.isRequired,
17+
components: PropTypes.object.isRequired,
18+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
export function QuoteItemPreview({ preview, components }) {
5+
return (
6+
<blockquote className="aa-QuotePreview">
7+
<components.Highlight hit={preview} attribute="content" />
8+
</blockquote>
9+
);
10+
}
11+
12+
QuoteItemPreview.propTypes = {
13+
preview: PropTypes.object.isRequired,
14+
components: PropTypes.object.isRequired,
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
export function SuggestionHeader({ items, Fragment }) {
5+
if (items.length === 0) {
6+
return null;
7+
}
8+
return (
9+
<Fragment>
10+
<div>
11+
<span className="aa-SourceHeaderTitle">
12+
Not finding what you‘re looking for? Try one of these queries instead:
13+
</span>
14+
</div>
15+
</Fragment>
16+
);
17+
}
18+
19+
SuggestionHeader.propTypes = {
20+
items: PropTypes.array.isRequired,
21+
Fragment: PropTypes.node.isRequired,
22+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
export function SuggestionItem({ item, components }) {
5+
return (
6+
<button className="aa-QuerySuggestion">
7+
<components.ReverseHighlight hit={item} attribute="query" cl />
8+
</button>
9+
);
10+
}
11+
12+
SuggestionItem.propTypes = {
13+
item: PropTypes.object.isRequired,
14+
components: PropTypes.object.isRequired,
15+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from "react";
2+
import PropTypes from "prop-types";
3+
4+
export function TableItemPreview({ preview, components }) {
5+
return (
6+
<table className="aa-TablePreview">
7+
<thead>
8+
<tr>
9+
{preview.tableAttributes.headers.map((header) => (
10+
<th>{header}</th>
11+
))}
12+
</tr>
13+
</thead>
14+
<tbody>
15+
<tr>
16+
{preview.tableAttributes.headers.map((header, index) => {
17+
if (index === preview.tableAttributes?.column)
18+
return (
19+
<td>
20+
<components.Highlight hit={preview} attribute="content" />
21+
</td>
22+
);
23+
return <td>...</td>;
24+
})}
25+
</tr>
26+
</tbody>
27+
</table>
28+
);
29+
}
30+
31+
TableItemPreview.propTypes = {
32+
preview: PropTypes.object.isRequired,
33+
components: PropTypes.object.isRequired,
34+
};

src/components/AutocompleteSearch.js

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from "react";
2+
import { getAlgoliaResults } from "@algolia/autocomplete-js";
3+
import algoliasearch from "algoliasearch/lite";
4+
import styled from "styled-components";
5+
import "@algolia/autocomplete-theme-classic";
6+
7+
import {
8+
AUTOCOMPLETE_APP_ID,
9+
AUTOCOMPLETE_API_KEY,
10+
AUTOCOMPLETE_INDEX_NAME,
11+
AUTOCOMPLETE_SUGGESTIONS_INDEX_NAME,
12+
} from "constants/config";
13+
import { MEDIA_QUERIES } from "constants/styles";
14+
15+
import { Autocomplete } from "./Autocomplete/Autocomplete";
16+
import { HitItem } from "./Autocomplete/HitItem";
17+
import { SuggestionHeader } from "./Autocomplete/SuggestionHeader";
18+
import { SuggestionItem } from "./Autocomplete/SuggestionItem";
19+
20+
const searchClient = algoliasearch(AUTOCOMPLETE_APP_ID, AUTOCOMPLETE_API_KEY);
21+
22+
const SearchContainerEl = styled.form`
23+
height: 5rem;
24+
display: flex;
25+
align-items: center;
26+
27+
@media (${MEDIA_QUERIES.ltLaptop}) {
28+
width: 100%;
29+
height: auto;
30+
margin-top: 6.5rem;
31+
border-bottom: none;
32+
}
33+
`;
34+
35+
export const AutocompleteSearch = () => (
36+
<SearchContainerEl>
37+
<Autocomplete
38+
defaultActiveItemId={0}
39+
placeholder="Search"
40+
detachedMediaQuery="none"
41+
getSources={() => [
42+
{
43+
sourceId: "hits",
44+
getItems({ query }) {
45+
return getAlgoliaResults({
46+
searchClient,
47+
queries: [
48+
{
49+
indexName: AUTOCOMPLETE_INDEX_NAME,
50+
query,
51+
params: {
52+
hitsPerPage: 15,
53+
},
54+
},
55+
],
56+
});
57+
},
58+
getItemUrl({ item }) {
59+
return item.url;
60+
},
61+
onActive({ item, setContext }) {
62+
setContext({ preview: item });
63+
},
64+
templates: {
65+
item: HitItem,
66+
},
67+
},
68+
{
69+
sourceId: "suggestions",
70+
getItems({ query }) {
71+
return getAlgoliaResults({
72+
searchClient,
73+
queries: [
74+
{
75+
indexName: AUTOCOMPLETE_SUGGESTIONS_INDEX_NAME,
76+
query,
77+
params: {
78+
hitsPerPage: 4,
79+
},
80+
},
81+
],
82+
});
83+
},
84+
onSelect({ item, setQuery, setIsOpen, refresh }) {
85+
setQuery(`${item.query} `);
86+
setIsOpen(true);
87+
refresh();
88+
},
89+
templates: {
90+
header: SuggestionHeader,
91+
item: SuggestionItem,
92+
},
93+
},
94+
]}
95+
/>
96+
</SearchContainerEl>
97+
);

0 commit comments

Comments
 (0)