on 2a577be help sign in
391 lines 15 kB history raw
1import { Component, createRef } from "react";
2import { withRouter } from "next/router";
3import debounce from "lodash.debounce";
4import Downshift from "downshift";
5import ob from "urbit-ob";
6import Sigil from "../components/Sigil";
7import levenSort from "leven-sort";
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 this.devSearch = this.devSearch.bind(this);
21 this.opsSearch = this.opsSearch.bind(this);
22 }
24 searchEndpoint(query) {
25 return `/api/search?q=${query}`;
26 }
28 devSearch(query) {
29 return `/api/dev-search?q=${query}`;
30 }
32 glossarySearch(query) {
33 return `/api/glossary?q=${encodeURIComponent(query)}`;
34 }
36 opsSearch(query) {
37 return `/api/ops-search?q=${query}`;
38 }
40 patpSearch(query) {
41 return (
42 (ob.isValidPatp(`~${deSig(query.toLowerCase())}`) &&
43 `~${deSig(query.toLowerCase())}`.length < 15) ||
44 (!isNaN(query) && query <= 4294967295)
45 );
46 }
48 onSelect(item) {
49 if (item.slug) {
50 this.props.router.push(item.slug);
51 }
53 this.setState({
54 query: "",
55 results: [],
56 });
58 this.props.closeSearch();
59 }
61 onInputValueChange = debounce(async (query) => {
62 if (query.length) {
63 const search = fetch(this.searchEndpoint(query))
64 .then((res) => res.json())
65 .then(async (res) => {
66 return res.results.map((item) => ({
67 type: "RESULT",
68 content: item,
69 }));
70 });
72 const patp = this.patpSearch(query)
73 ? !isNaN(query)
74 ? ob.patp(query)
75 : ob.patp(ob.patp2dec(`~${deSig(query)}`))
76 : null;
78 const patpResult = this.patpSearch(query)
79 ? [
80 {
81 type: "PATP",
82 content: {
83 patp: patp,
84 slug: `/ids/${patp}`,
85 },
86 },
87 ]
88 : [];
90 const glossarySearch = fetch(this.glossarySearch(query))
91 .then((res) => res.json())
92 .then((res) => {
93 return res.results.map((item) => ({
94 type: "GLOSSARY_RESULT",
95 content: item,
96 }));
97 });
99 const devSearch = fetch(this.devSearch(query))
100 .then((res) => res.json())
101 .then((res) => {
102 return res.results.map((item) => ({
103 type: "DEV_RESULT",
104 content: item,
105 }));
106 });
108 const opsSearch = fetch(this.opsSearch(query))
109 .then((res) => res.json())
110 .then((res) => {
111 return res.results.map((item) => ({
112 type: "OPS_RESULT",
113 content: item,
114 }));
115 });
117 const [results, glossaryResults, devResults, opsResults] =
118 await Promise.all([search, glossarySearch, devSearch, opsSearch]);
120 const list = [
121 ...glossaryResults,
122 ...patpResult,
123 ...results,
124 ...devResults,
125 ...opsResults,
126 ];
128 this.setState({ results: list });
129 } else {
130 this.setState({ results: [] });
131 }
132 }, 250);
134 render() {
135 const { state, props } = this;
137 if (props.showSearch) {
138 return (
139 <Downshift
140 onSelect={(selection) => this.onSelect(selection)}
141 onInputValueChange={(event) => this.onInputValueChange(event)}
142 itemToString={(item) => (item ? item.slug : "")}
143 defaultHighlightedIndex={0}
144 >
145 {({
146 getInputProps,
147 getItemProps,
148 getLabelProps,
149 getMenuProps,
150 isOpen,
151 inputValue,
152 highlightedIndex,
153 selectedItem,
154 getRootProps,
155 }) => (
156 <div className="fixed w-screen h-screen z-50 flex flex-col items-center p-4">
157 <div
158 onClick={(event) => props.closeSearch(event)}
159 className="top-0 left-0 fixed w-screen h-screen bg-washedWall"
160 />
161 <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">
162 <div
163 style={{ display: "inline-block" }}
164 {...getRootProps({}, { suppressRefError: true })}
165 >
166 <input
167 autoFocus
168 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"
169 placeholder="Search..."
170 type="text"
171 onClick={(e) => e.stopPropagation()}
172 {...getInputProps({
173 onKeyDown: (event) => {
174 if (event.key === "Escape") {
175 // Prevent Downshift's default 'Escape' behavior.
176 event.nativeEvent.preventDownshiftDefault = true;
177 this.props.closeSearch(event);
178 }
179 },
180 })}
181 />
182 </div>
183 <ul {...getMenuProps()} className="overflow-y-scroll">
184 {isOpen
185 ? state.results.map((item, index) => {
186 const selected = highlightedIndex === index;
187 if (item.type === "PATP") {
188 return (
189 <li
190 className={`cursor-pointer p-2 flex space-x-2 items-center text-left w-full ${
191 selected ? "bg-green-400" : ""
192 }`}
193 {...getItemProps({
194 key: item.content.slug + "-" + index,
195 index,
196 item: item.content,
197 selected: highlightedIndex === index,
198 })}
199 >
200 <div className="rounded-md overflow-hidden">
201 <Sigil
202 patp={item.content.patp}
203 size={25}
204 icon
205 />
206 </div>
207 <p className="font-mono">{item.content.patp}</p>
208 </li>
209 );
210 }
211 if (item.type === "GLOSSARY_RESULT") {
212 return (
213 <li
214 className={`cursor-pointer flex text-left w-full ${
215 selected ? "bg-green-400" : ""
216 }`}
217 {...getItemProps({
218 key: item.content.slug + "-" + index,
219 index,
220 item: item.content,
221 selected: highlightedIndex === index,
222 })}
223 >
224 <div className="font-semibold p-3">
225 <p
226 className={`text-base ${
227 selected ? "text-white" : "text-wall-600"
228 }`}
229 >
230 {item.content.symbol.length > 0 && (
231 <code
232 className={`mr-1 rounded px-1 py-0.5 ${
233 selected
234 ? "bg-washedWhite"
235 : "bg-wall-100"
236 }`}
237 >
238 {item.content.symbol}
239 </code>
240 )}
241 {item.content.name}
242 </p>
243 <p
244 className={`font-normal text-base mt-1 ${
245 selected ? "text-white" : "text-wall-600"
246 }`}
247 dangerouslySetInnerHTML={{
248 __html: item.content.desc,
249 }}
250 ></p>
251 </div>
252 </li>
253 );
254 }
255 if (item.type === "RESULT") {
256 return (
257 <li
258 className={`cursor-pointer flex text-left w-full ${
259 selected ? "bg-green-400" : ""
260 }`}
261 {...getItemProps({
262 key: item.content.link + "-" + index,
263 index,
264 item: item.content,
265 selected,
266 })}
267 >
268 <div className="p-3">
269 <p
270 className={`font-medium text-base ${
271 selected ? "text-white" : "text-wall-600"
272 }`}
273 >
274 {item.content.parent !== "Content"
275 ? `${item.content.parent} /`
276 : ""}{" "}
277 {item.content.title}
278 </p>
279 <p
280 className={`text-base font-regular text-small ${
281 selected ? "text-midWhite" : "text-wall-500"
282 }`}
283 >
284 {item.content.content}
285 </p>
286 </div>
287 </li>
288 );
289 }
290 if (item.type === "DEV_RESULT") {
291 const devItem = Object.assign({}, item.content);
292 devItem[
293 "slug"
294 ] = `https://developers.urbit.org${item.content.slug}`;
295 return (
296 <li
297 className={`cursor-pointer flex text-left w-full ${
298 selected ? "bg-green-400" : ""
299 }`}
300 {...getItemProps({
301 key: item.content.link + "-" + index,
302 index,
303 item: devItem,
304 selected,
305 })}
306 >
307 <div className="p-3">
308 <p
309 className={`font-medium text-base ${
310 selected ? "text-white" : "text-wall-600"
311 }`}
312 >
313 <span className="text-wall-400">
314 {item.content.parent !== "Content"
315 ? `developers.urbit.org / ${item.content.parent} /`
316 : "developers.urbit.org /"}{" "}
317 </span>
318 {item.content.title}
319 </p>
320 <p
321 className={`text-base font-regular text-small ${
322 selected ? "text-midWhite" : "text-wall-500"
323 }`}
324 >
325 {item.content.content}
326 </p>
327 </div>
328 </li>
329 );
330 }
331 if (item.type === "OPS_RESULT") {
332 const opsItem = Object.assign({}, item.content);
333 opsItem[
334 "slug"
335 ] = `https://operators.urbit.org${item.content.slug}`;
336 return (
337 <li
338 className={`cursor-pointer flex text-left w-full ${
339 selected ? "bg-green-400" : ""
340 }`}
341 {...getItemProps({
342 key: item.content.link + "-" + index,
343 index,
344 item: opsItem,
345 selected,
346 })}
347 >
348 <div className="p-3">
349 <p
350 className={`font-medium text-base ${
351 selected ? "text-white" : "text-wall-600"
352 }`}
353 >
354 <span className="text-wall-400">
355 {item.content.parent !== "Content"
356 ? `operators.urbit.org / ${item.content.parent} /`
357 : "operators.urbit.org /"}{" "}
358 </span>
359 {item.content.title}
360 </p>
361 <p
362 className={`text-base font-regular text-small ${
363 selected ? "text-midWhite" : "text-wall-500"
364 }`}
365 >
366 {item.content.content}
367 </p>
368 </div>
369 </li>
370 );
371 }
372 return null;
373 })
374 : null}
375 </ul>
376 </div>
377 </div>
378 )}
379 </Downshift>
380 );
381 } else {
382 return null;
383 }
384 }
387function deSig(string) {
388 return string.startsWith("~") ? string.substring(1) : string;
391export default withRouter(Search);