@@ -1,142 +1,54 @@
1−import { useCallback, useRef, useState, useEffect, createContext } from "react";
1+import { Component, createRef } from "react";
22import { glossary } from "../lib/glossary";
33import Link from "next/link";
4−import { useRouter } from "next/router";
4+import { withRouter } from "next/router";
55import { configure, HotKeys } from "react-hotkeys";
66import classnames from "classnames";
7+import Downshift from "downshift";
78
8−const SearchContext = createContext();
9−
10−const glossarySearch = (query) => {
11− return glossary.filter((entry) => {
12− return query.toLowerCase() === entry.name || entry.symbol === query;
13− });
14−};
15−
16−function GlossaryResult(props) {
17− const selectedClassnames = classnames({
18− "bg-green": props.selected,
19− });
20−
21− const codeBg = classnames({
22− "bg-wall": !props.selected,
23− "bg-washedWhite": props.selected,
24− });
25−
26− const text = classnames({
27− "text-black": !props.selected,
28− "text-white": props.selected,
29− });
30−
31− return (
32− <div
33− className={`cursor-pointer ${selectedClassnames}`}
34− onMouseEnter={() => props.setCursor(props.index)}
35− >
36− <div className="font-semibold p-3">
37− <p className={`text-base ${text}`}>
38− {props.symbol.length > 0 && (
39− <code className={`mr-1 rounded px-1 py-0.5 ${codeBg}`}>
40− {props.symbol}
41− </code>
42− )}
43− {props.name}
44− </p>
45− <p className={`font-normal text-base mt-1 ${text}`}>{props.desc}</p>
46− </div>
47− </div>
48− );
49−}
50−
51−function Result(props) {
52− const selectedClassnames = classnames({
53− "bg-green": props.selected,
54− });
55− const text = classnames({
56− "text-black": !props.selected,
57− "text-white": props.selected,
58− });
59− const description = classnames({
60− "text-gray": !props.selected,
61− "text-midWhite": props.selected,
62− });
63− return (
64− <div
65− className={`cursor-pointer ${selectedClassnames}`}
66− onMouseEnter={() => props.setCursor(props.index)}
67− >
68− <div className="p-3">
69− <p className={`font-medium text-base ${text}`}>
70− {props.parent !== "Content" ? `${props.parent} /` : ""} {props.title}
71− </p>
72− <p
73− className={`text-base font-regular text-small text-gray ${description}`}
74− >
75− {props.content}
76− </p>
77− </div>
78− </div>
79− );
80−}
81−
82−function Subheading(props) {
83− return (
84− <div className="sticky top-0 flex font-semibold text-sm p-3 py-1 w-full bg-ultraDeepWall text-white">
85− <p className="font-semibold text-sm">{props.title}</p>
86− </div>
87− );
88−}
9+class Search extends Component {
10+ constructor(props) {
11+ super(props);
12+ this.state = {
13+ results: [],
14+ };
15+ this.searchEndpoint = this.searchEndpoint.bind(this);
16+ this.onInputValueChange = this.onInputValueChange.bind(this);
17+ this.onChange = this.onChange.bind(this);
18+ this.glossarySearch = this.glossarySearch.bind(this);
19+ }
8920
90−export default function Search(props) {
91− const searchRef = useRef(null);
92− const [query, setQuery] = useState("");
93− const [active, setActive] = useState(false);
94− const [results, setResults] = useState([]);
95− const [cursor, setCursor] = useState(0);
96− const searchEndpoint = (query) => `/api/search?q=${query}`;
97− const router = useRouter();
21+ searchEndpoint(query) {
22+ return `/api/search?q=${query}`;
23+ }
9824
99− const keyMap = {
100− onNavUp: ["up"],
101− onNavDown: ["down"],
102− onSelect: ["enter"],
103− };
25+ glossarySearch(query) {
26+ console.log(query);
27+ return glossary.filter((entry) => {
28+ return query.toLowerCase() === entry.name || entry.symbol === query;
29+ });
30+ }
10431
105− const handlers = {
106− onNavUp: (event) => onNavUp(event),
107− onNavDown: (event) => onNavDown(event),
108− onSelect: (event) => onSelect(event, results[cursor]),
109− };
32+ onChange(item) {
33+ if (item.link) {
34+ window.location.href = item.link;
35+ }
11036
111− configure({
112− // ignoreTags: [],
113− ignoreTags: ["input", "select", "textarea"],
114− ignoreEventsCondition: function () {},
115− });
37+ if (item.slug) {
38+ this.props.router.push(item.slug);
39+ }
11640
117− {
118− /* useEffect(() => {
119− router.events.on("routeChangeComplete", handleRouteChange);
120− return () => {
121− router.events.off("routeChangeComplete", handleRouteChange);
122− };
123− }, [router.events]);
41+ this.setState({
42+ query: "",
43+ results: [],
44+ });
12445
125− const handleRouteChange = () => {
126− setQuery("");
127− setResults([]);
128− setActive(false);
129− props.closeSearch();
130− }; */
46+ this.props.closeSearch();
13147 }
13248
133− const onChange = useCallback((event) => {
134− const query = event.target.value;
135−
136− setQuery(query);
137−
49+ onInputValueChange(query) {
13850 if (query.length) {
139− fetch(searchEndpoint(query))
51+ fetch(this.searchEndpoint(query))
14052 .then((res) => res.json())
14153 .then((res) => {
14254 // Wrap results in an object which will tell React what component to use to render results.
@@ -145,154 +57,149 @@ export default function Search(props) {
14557 content: item,
14658 }));
14759
148− const glossaryResults = glossarySearch(query).map((item) => ({
60+ const glossaryResults = this.glossarySearch(query).map((item) => ({
14961 type: "GLOSSARY_RESULT",
15062 content: item,
15163 }));
15264
15365 const list = [...glossaryResults, ...results];
15466
155− setResults(list);
67+ this.setState({ results: list });
15668 });
15769 } else {
158− setResults([]);
159− }
160− }, []);
161−
162− const onFocus = useCallback(() => {
163− setActive(true);
164− // window.addEventListener("click", onClick);
165− }, []);
166−
167− const onClick = useCallback((event) => {
168− if (searchRef.current && !searchRef.current.contains(event.target)) {
169− setActive(false);
170− window.removeEventListener("click", onClick);
171− props.toggleSearch();
172− }
173− }, []);
174−
175− const onSelect = () => {
176− console.log(results, cursor);
177−
178− {
179− /* const cursorItem = results[cursor].content
180−
181−
182−
183− if (cursorItem.link) {
184− window.location.href = cursorItem.link
185− }
186−
187− if (cursorItem.slug) {
188− router.push(cursorItem.slug)
189− }
190−
191− props.toggleSearch();
192− setActive(false);
193− setQuery("");
194− setResults([])
195− props.closeSearch(); */
196− }
197− };
198−
199− const onNavUp = useCallback((event) => {
200− // prevents cursor from moving in search input
201− event.preventDefault();
202− setCursor((state) => state - 1);
203− });
204−
205− const onNavDown = useCallback((event) => {
206− // prevents cursor from moving in search input
207− event.preventDefault();
208− setCursor((state) => state + 1);
209− });
210−
211− const clear = (e) => {
212− e.stopPropagation();
213− setQuery("");
214− };
215−
216− // Locks document scrolling when search is open
217− if (typeof document !== "undefined") {
218− if (props.showSearch) {
219− document.body.style.overflow = "hidden";
220− } else {
221− document.body.style.overflow = "visible";
70+ this.setState({ results: [] });
22271 }
22372 }
22473
225− if (props.showSearch) {
226− return (
227− <SearchContext.Provider value={42}>
228− <HotKeys handlers={handlers} keyMap={keyMap}>
229− <div
230− onClick={props.closeSearch}
231− className="fixed w-screen h-screen bg-washedWall z-50 flex flex-col items-center p-4"
232− >
233− <div className="relative flex flex-col max-w-screen-lg md:my-32 w-full md:w-10/12 lg:w-8/12 xl:w-6/12 rounded-xl bg-white min-h-0">
234− <div className="relative">
235− <input
236− className="text-lg md:text-xl lg:text-2xl font-medium text-green bg-transparent py-2 px-4 outline-none relative w-full"
237− onChange={onChange}
238− onFocus={onFocus}
239− placeholder="Search..."
240− type="text"
241− value={query}
242− onClick={(e) => e.stopPropagation()}
243− autoFocus={true}
244− />
245−
246− <span
247− onClick={props.toggleSearch}
248− className="absolute cursor-pointer right-1.5 top-1.5 md:right-2 md:top-2 lg:right-2.5 lg:top-2.5"
74+ render() {
75+ const { state, props } = this;
76+
77+ if (props.isOpen) {
78+ return (
79+ <Downshift
80+ onChange={(selection) => this.onChange(selection)}
81+ onInputValueChange={(event) => this.onInputValueChange(event)}
82+ itemToString={(item) =>
83+ item ? item.content.slug || item.content.link : ""
84+ }
85+ defaultHighlightedIndex={0}
86+ >
87+ {({
88+ getInputProps,
89+ getItemProps,
90+ getLabelProps,
91+ getMenuProps,
92+ isOpen,
93+ inputValue,
94+ highlightedIndex,
95+ selectedItem,
96+ getRootProps,
97+ }) => (
98+ <div className="fixed w-screen h-screen bg-washedWall z-50 flex flex-col items-center p-4">
99+ <div className="relative flex flex-col max-w-screen-lg md:my-32 w-full md:w-10/12 lg:w-8/12 xl:w-6/12 rounded-xl bg-white min-h-0 overflow-hidden">
100+ <div
101+ style={{ display: "inline-block" }}
102+ {...getRootProps({}, { suppressRefError: true })}
249103 >
250− <p className="px-2 h-8 bg-wall flex items-center justify-center text-sm rounded">
251− ESC
252− </p>
253− </span>
254− </div>
255−
256− <div
257− className={
258− "overflow-y-scroll border-t-2 border-wall" +
259− (query.length > 0 ? "" : "hidden border-transparent")
260− }
261− >
262− {active && query !== ""
263− ? results.map((item, index) => {
264− if (item.type === "GLOSSARY_RESULT") {
265− return (
266− <GlossaryResult
267− key={item.link + "-" + index}
268− {...item.content}
269− index={index}
270− setCursor={setCursor}
271− selected={index === cursor}
272− />
273− );
274− }
275−
276− if (item.type === "RESULT") {
277− return (
278− <Result
279− key={item.link + "-" + index}
280− {...item.content}
281− index={index}
282− setCursor={setCursor}
283− selected={index === cursor}
284− />
285− );
286− }
287− return null;
288− })
289− : null}
104+ <input
105+ autoFocus
106+ className="text-lg md:text-xl lg:text-2xl font-medium text-green bg-transparent py-2 px-4 outline-none relative w-full"
107+ {...getInputProps()}
108+ />
109+ </div>
110+ <ul {...getMenuProps()} className="overflow-y-scroll">
111+ {isOpen
112+ ? state.results.map((item, index) => {
113+ const selected = highlightedIndex === index;
114+ if (item.type === "GLOSSARY_RESULT") {
115+ return (
116+ <li
117+ className={`cursor-pointer flex text-left w-full ${
118+ selected ? "bg-green" : ""
119+ }`}
120+ {...getItemProps({
121+ key: item.content.link + "-" + index,
122+ index,
123+ item: item.content,
124+ selected: highlightedIndex === index,
125+ })}
126+ >
127+ <div className="font-semibold p-3">
128+ <p
129+ className={`text-base ${
130+ selected ? "text-white" : "text-black"
131+ }`}
132+ >
133+ {item.content.symbol.length > 0 && (
134+ <code
135+ className={`mr-1 rounded px-1 py-0.5 ${
136+ selected ? "bg-washedWhite" : "bg-wall"
137+ }`}
138+ >
139+ {item.content.symbol}
140+ </code>
141+ )}
142+ {item.content.name}
143+ </p>
144+ <p
145+ className={`font-normal text-base mt-1 ${
146+ selected ? "text-white" : "text-black"
147+ }`}
148+ >
149+ {item.content.desc}
150+ </p>
151+ </div>
152+ </li>
153+ );
154+ }
155+ if (item.type === "RESULT") {
156+ return (
157+ <li
158+ className={`cursor-pointer flex text-left w-full ${
159+ selected ? "bg-green" : ""
160+ }`}
161+ {...getItemProps({
162+ key: item.content.link + "-" + index,
163+ index,
164+ item: item.content,
165+ selected,
166+ })}
167+ >
168+ <div className="p-3">
169+ <p
170+ className={`font-medium text-base ${
171+ selected ? "text-white" : "text-black"
172+ }`}
173+ >
174+ {item.content.parent !== "Content"
175+ ? `${item.content.parent} /`
176+ : ""}{" "}
177+ {item.content.title}
178+ </p>
179+ <p
180+ className={`text-base font-regular text-small ${
181+ selected ? "text-midWhite" : "text-gray"
182+ }`}
183+ >
184+ {item.content.content}
185+ </p>
186+ </div>
187+ </li>
188+ );
189+ }
190+ return null;
191+ })
192+ : null}
193+ </ul>
290194 </div>
291195 </div>
292− </div>
293− </HotKeys>
294− </SearchContext.Provider>
295− );
196+ )}
197+ </Downshift>
198+ );
199+ } else {
200+ return null;
201+ }
296202 }
297− return null;
298203}
204+
205+export default withRouter(Search);