@@ -1,7 +1,11 @@
1−import { useCallback, useRef, useState, useEffect } from "react";
1+import { useCallback, useRef, useState, useEffect, createContext } from "react";
2import { glossary } from "../lib/glossary";
2import { glossary } from "../lib/glossary";
3import Link from "next/link";
3import Link from "next/link";
4import { useRouter } from "next/router";
4import { useRouter } from "next/router";
5+import { configure, HotKeys } from "react-hotkeys";
6+import classnames from "classnames";
8+const SearchContext = createContext();
6const glossarySearch = (query) => {
10const glossarySearch = (query) => {
7 return glossary.filter((entry) => {
11 return glossary.filter((entry) => {
@@ -9,16 +13,109 @@ const glossarySearch = (query) => {
16+function GlossaryResult(props) {
17+ const selectedClassnames = classnames({
18+ "bg-green": props.selected,
21+ const codeBg = classnames({
22+ "bg-wall": !props.selected,
23+ "bg-washedWhite": props.selected,
26+ const text = classnames({
27+ "text-black": !props.selected,
28+ "text-white": props.selected,
33+ className={`cursor-pointer ${selectedClassnames}`}
34+ onMouseEnter={() => props.setCursor(props.index)}
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}`}>
45+ <p className={`font-normal text-base mt-1 ${text}`}>{props.desc}</p>
51+function Result(props) {
52+ const selectedClassnames = classnames({
53+ "bg-green": props.selected,
55+ const text = classnames({
56+ "text-black": !props.selected,
57+ "text-white": props.selected,
59+ const description = classnames({
60+ "text-gray": !props.selected,
61+ "text-midWhite": props.selected,
65+ className={`cursor-pointer ${selectedClassnames}`}
66+ onMouseEnter={() => props.setCursor(props.index)}
68+ <div className="p-3">
69+ <p className={`font-medium text-base ${text}`}>
70+ {props.parent !== "Content" ? `${props.parent} /` : ""} {props.title}
73+ className={`text-base font-regular text-small text-gray ${description}`}
82+function Subheading(props) {
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>
12export default function Search(props) {
90export default function Search(props) {
13 const searchRef = useRef(null);
91 const searchRef = useRef(null);
14 const [query, setQuery] = useState("");
92 const [query, setQuery] = useState("");
15 const [active, setActive] = useState(false);
93 const [active, setActive] = useState(false);
16 const [results, setResults] = useState([]);
94 const [results, setResults] = useState([]);
95+ const [cursor, setCursor] = useState(0);
17 const searchEndpoint = (query) => `/api/search?q=${query}`;
96 const searchEndpoint = (query) => `/api/search?q=${query}`;
19 const router = useRouter();
97 const router = useRouter();
101+ onNavDown: ["down"],
102+ onSelect: ["enter"],
106+ onNavUp: (event) => onNavUp(event),
107+ onNavDown: (event) => onNavDown(event),
108+ onSelect: (event) => onSelect(event, results[cursor]),
113+ ignoreTags: ["input", "select", "textarea"],
114+ ignoreEventsCondition: function () {},
21− useEffect(() => {
118+ /* useEffect(() => {
22 router.events.on("routeChangeComplete", handleRouteChange);
119 router.events.on("routeChangeComplete", handleRouteChange);
23 return () => {
120 return () => {
24 router.events.off("routeChangeComplete", handleRouteChange);
121 router.events.off("routeChangeComplete", handleRouteChange);
@@ -30,17 +127,32 @@ export default function Search(props) {
30 setResults([]);
127 setResults([]);
31 setActive(false);
128 setActive(false);
32 props.closeSearch();
129 props.closeSearch();
35 const onChange = useCallback((event) => {
133 const onChange = useCallback((event) => {
36 const query = event.target.value;
134 const query = event.target.value;
38 setQuery(query);
136 setQuery(query);
39 if (query.length) {
138 if (query.length) {
40 fetch(searchEndpoint(query))
139 fetch(searchEndpoint(query))
41 .then((res) => res.json())
140 .then((res) => res.json())
42 .then((res) => {
141 .then((res) => {
142+ // Wrap results in an object which will tell React what component to use to render results.
43− setResults(res.results);
143+ const results = res.results.map((item) => ({
148+ const glossaryResults = glossarySearch(query).map((item) => ({
149+ type: "GLOSSARY_RESULT",
153+ const list = [...glossaryResults, ...results];
46 setResults([]);
158 setResults([]);
@@ -49,7 +161,7 @@ export default function Search(props) {
50 const onFocus = useCallback(() => {
162 const onFocus = useCallback(() => {
51 setActive(true);
163 setActive(true);
52− window.addEventListener("click", onClick);
164+ // window.addEventListener("click", onClick);
55 const onClick = useCallback((event) => {
167 const onClick = useCallback((event) => {
@@ -60,15 +172,46 @@ export default function Search(props) {
63− let glossaryResults = [];
175+ const onSelect = () => {
64− if (query.length) {
176+ console.log(results, cursor);
65− glossaryResults = glossarySearch(query);
177+
179+ /* const cursorItem = results[cursor].content
183+ if (cursorItem.link) {
184+ window.location.href = cursorItem.link
187+ if (cursorItem.slug) {
188+ router.push(cursorItem.slug)
191+ props.toggleSearch();
195+ props.closeSearch(); */
68− const clear = useCallback((e) => {
199+ const onNavUp = useCallback((event) => {
200+ // prevents cursor from moving in search input
201+ event.preventDefault();
202+ setCursor((state) => state - 1);
205+ const onNavDown = useCallback((event) => {
206+ // prevents cursor from moving in search input
207+ event.preventDefault();
208+ setCursor((state) => state + 1);
211+ const clear = (e) => {
69 e.stopPropagation();
212 e.stopPropagation();
70 setQuery("");
213 setQuery("");
73 // Locks document scrolling when search is open
216 // Locks document scrolling when search is open
74 if (typeof document !== "undefined") {
217 if (typeof document !== "undefined") {
@@ -81,110 +224,74 @@ export default function Search(props) {
82 if (props.showSearch) {
225 if (props.showSearch) {
84− <div
227+ <SearchContext.Provider value={42}>
85− onClick={props.closeSearch}
228+ <HotKeys handlers={handlers} keyMap={keyMap}>
86− className="fixed w-screen h-screen bg-washedWall z-50 flex flex-col items-center p-4"
89− 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"
92− <div className="relative">
94− className="text-lg md:text-xl lg:text-2xl font-medium text-green bg-transparent py-2 px-4 outline-none relative w-full"
97− placeholder="Search..."
100− onClick={(e) => e.stopPropagation()}
105− onClick={props.toggleSearch}
106− className="absolute cursor-pointer right-1.5 top-1.5 md:right-2 md:top-2 lg:right-2.5 lg:top-2.5"
108− <p className="px-2 h-8 bg-wall flex items-center justify-center text-sm rounded">
115− className={"overflow-y-scroll" + (query.length > 0 ? "" : "hidden")}
230+ onClick={props.closeSearch}
231+ className="fixed w-screen h-screen bg-washedWall z-50 flex flex-col items-center p-4"
117− <div>
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">
118− {active &&
234+ <div className="relative">
119− results.length === 0 &&
235+ <input
120− glossaryResults.length === 0 &&
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"
121− query !== "" ? (
237+ onChange={onChange}
122− <div className="flex font-semibold text-sm p-3 py-8 w-full">
238+ onFocus={onFocus}
123− <p className="mr-2 text-gray">No Results found for</p>
239+ placeholder="Search..."
124− <p className="font-semibold">"{query}"</p>
240+ type="text"
125− </div>
241+ value={query}
126− ) : null}
242+ onClick={(e) => e.stopPropagation()}
127− {active && glossaryResults.length > 0 ? (
243+ autoFocus={true}
129− <div className="sticky top-0 flex font-semibold text-sm p-3 py-1 w-full bg-ultraDeepWall text-white">
245+
130− <p className="font-semibold text-sm">Glossary</p>
246+ <span
131− </div>
247+ onClick={props.toggleSearch}
132−
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"
133− {glossaryResults.map(({ name, link, desc, symbol }) => {
135− <Link href={link} as={link} key={link}>
136− <div className="cursor-pointer hover:bg-wall">
137− <div className="font-semibold p-3">
138− {symbol.length > 0 && (
139− <code className="mr-1 bg-wall rounded px-1 py-0.5">
144− <p className="font-normal text-base mt-1">{desc}</p>
152− {active && results.length > 0 && (
154− <div className="sticky top-0 flex font-semibold text-sm p-3 py-1 w-full bg-ultraDeepWall text-white">
155− <p className="font-semibold text-sm">
156− {results.length} results for "{query}"
161− .filter((e) => e.parent !== "Glossary")
162− .map(({ title, slug, content, parent }) => {
166− className="cursor-pointer hover:bg-wall p-3"
168− <Link href={slug} as={slug}>
170− <p className="font-medium text-base">
171− {parent !== "Content" ? `${parent} /` : ""}{" "}
174− <p className="text-base font-regular text-small text-gray">
250+ <p className="px-2 h-8 bg-wall flex items-center justify-center text-sm rounded">
258+ "overflow-y-scroll border-t-2 border-wall" +
259+ (query.length > 0 ? "" : "hidden border-transparent")
262+ {active && query !== ""
263+ ? results.map((item, index) => {
264+ if (item.type === "GLOSSARY_RESULT") {
267+ key={item.link + "-" + index}
270+ setCursor={setCursor}
271+ selected={index === cursor}
276+ if (item.type === "RESULT") {
279+ key={item.link + "-" + index}
282+ setCursor={setCursor}
283+ selected={index === cursor}
182− </>
287+ return null;
186− </div>
293+ </HotKeys>
187− </div>
294+ </SearchContext.Provider>
190 return null;
297 return null;