on 4fdc19d help sign in
270 lines 10 kB history raw
1import { Component, createRef } from "react";
2import { glossary } from "../lib/glossary";
3import { withRouter } from "next/router";
4import debounce from "lodash.debounce";
5import Downshift from "downshift";
6import ob from "urbit-ob";
7import Sigil from "../components/Sigil";
9class 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.onSelect = this.onSelect.bind(this);
18 this.glossarySearch = this.glossarySearch.bind(this);
19 this.patpSearch = this.patpSearch.bind(this);
20 }
22 searchEndpoint(query) {
23 return `/api/search?q=${query}`;
24 }
26 glossarySearch(query) {
27 return glossary.filter((entry) => {
28 return query.toLowerCase() === entry.name || entry.symbol === query;
29 });
30 }
32 patpSearch(query) {
33 return (
34 (ob.isValidPatp(`~${deSig(query.toLowerCase())}`) &&
35 `~${deSig(query.toLowerCase())}`.length < 15) ||
36 (!isNaN(query) && query <= 4294967295)
37 );
38 }
40 onSelect(item) {
41 if (item.slug) {
42 this.props.router.push(item.slug);
43 }
45 this.setState({
46 query: "",
47 results: [],
48 });
50 this.props.closeSearch();
51 }
53 onInputValueChange = debounce((query) => {
54 if (query.length) {
55 fetch(this.searchEndpoint(query))
56 .then((res) => res.json())
57 .then((res) => {
58 // Wrap results in an object which will tell React what component to use to render results.
59 const results = res.results.map((item) => ({
60 type: "RESULT",
61 content: item,
62 }));
64 const patp = this.patpSearch(query)
65 ? !isNaN(query)
66 ? ob.patp(query)
67 : ob.patp(ob.patp2dec(`~${deSig(query)}`))
68 : null;
70 const patpResult = this.patpSearch(query)
71 ? [
72 {
73 type: "PATP",
74 content: {
75 patp: patp,
76 slug: `/ids/${patp}`,
77 },
78 },
79 ]
80 : [];
82 const glossaryResults = this.glossarySearch(query).map((item) => ({
83 type: "GLOSSARY_RESULT",
84 content: item,
85 }));
87 const list = [...patpResult, ...glossaryResults, ...results];
89 this.setState({ results: list });
90 });
91 } else {
92 this.setState({ results: [] });
93 }
94 }, 250);
96 render() {
97 const { state, props } = this;
99 if (props.showSearch) {
100 return (
101 <Downshift
102 onSelect={(selection) => this.onSelect(selection)}
103 onInputValueChange={(event) => this.onInputValueChange(event)}
104 itemToString={(item) => (item ? item.slug : "")}
105 defaultHighlightedIndex={0}
106 >
107 {({
108 getInputProps,
109 getItemProps,
110 getLabelProps,
111 getMenuProps,
112 isOpen,
113 inputValue,
114 highlightedIndex,
115 selectedItem,
116 getRootProps,
117 }) => (
118 <div className="fixed w-screen h-screen z-50 flex flex-col items-center p-4">
119 <div
120 onClick={(event) => props.closeSearch(event)}
121 className="top-0 left-0 fixed w-screen h-screen bg-washedWall"
122 />
123 <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">
124 <div
125 style={{ display: "inline-block" }}
126 {...getRootProps({}, { suppressRefError: true })}
127 >
128 <input
129 autoFocus
130 className="text-lg md:text-xl lg:text-2xl font-medium text-green-400 bg-transparent py-2 px-4 outline-none relative w-full"
131 placeholder="Search..."
132 type="text"
133 onClick={(e) => e.stopPropagation()}
134 {...getInputProps({
135 onKeyDown: (event) => {
136 if (event.key === "Escape") {
137 // Prevent Downshift's default 'Escape' behavior.
138 event.nativeEvent.preventDownshiftDefault = true;
139 this.props.closeSearch(event);
140 }
141 },
142 })}
143 />
144 </div>
145 <ul {...getMenuProps()} className="overflow-y-scroll">
146 {isOpen
147 ? state.results.map((item, index) => {
148 const selected = highlightedIndex === index;
149 if (item.type === "PATP") {
150 return (
151 <li
152 className={`cursor-pointer p-2 flex space-x-2 items-center text-left w-full ${
153 selected ? "bg-green-400" : ""
154 }`}
155 {...getItemProps({
156 key: item.content.slug + "-" + index,
157 index,
158 item: item.content,
159 selected: highlightedIndex === index,
160 })}
161 >
162 <div className="rounded-md overflow-hidden">
163 <Sigil
164 patp={item.content.patp}
165 size={25}
166 icon
167 />
168 </div>
169 <p className="font-mono">{item.content.patp}</p>
170 </li>
171 );
172 }
173 if (item.type === "GLOSSARY_RESULT") {
174 return (
175 <li
176 className={`cursor-pointer flex text-left w-full ${
177 selected ? "bg-green-400" : ""
178 }`}
179 {...getItemProps({
180 key: item.content.slug + "-" + index,
181 index,
182 item: item.content,
183 selected: highlightedIndex === index,
184 })}
185 >
186 <div className="font-semibold p-3">
187 <p
188 className={`text-base ${
189 selected ? "text-white" : "text-wall-600"
190 }`}
191 >
192 {item.content.symbol.length > 0 && (
193 <code
194 className={`mr-1 rounded px-1 py-0.5 ${
195 selected
196 ? "bg-washedWhite"
197 : "bg-wall-100"
198 }`}
199 >
200 {item.content.symbol}
201 </code>
202 )}
203 {item.content.name}
204 </p>
205 <p
206 className={`font-normal text-base mt-1 ${
207 selected ? "text-white" : "text-wall-600"
208 }`}
209 >
210 {item.content.desc}
211 </p>
212 </div>
213 </li>
214 );
215 }
216 if (item.type === "RESULT") {
217 return (
218 <li
219 className={`cursor-pointer flex text-left w-full ${
220 selected ? "bg-green-400" : ""
221 }`}
222 {...getItemProps({
223 key: item.content.link + "-" + index,
224 index,
225 item: item.content,
226 selected,
227 })}
228 >
229 <div className="p-3">
230 <p
231 className={`font-medium text-base ${
232 selected ? "text-white" : "text-wall-600"
233 }`}
234 >
235 {item.content.parent !== "Content"
236 ? `${item.content.parent} /`
237 : ""}{" "}
238 {item.content.title}
239 </p>
240 <p
241 className={`text-base font-regular text-small ${
242 selected ? "text-midWhite" : "text-wall-500"
243 }`}
244 >
245 {item.content.content}
246 </p>
247 </div>
248 </li>
249 );
250 }
251 return null;
252 })
253 : null}
254 </ul>
255 </div>
256 </div>
257 )}
258 </Downshift>
259 );
260 } else {
261 return null;
262 }
263 }
266function deSig(string) {
267 return string.startsWith("~") ? string.substring(1) : string;
270export default withRouter(Search);