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