on 595fcbc help sign in
393 lines 15 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.patpSearch = this.patpSearch.bind(this);
18 this.devSearch = this.devSearch.bind(this);
19 this.opsSearch = this.opsSearch.bind(this);
20 }
22 searchEndpoint(query) {
23 return `/api/search?q=${encodeURIComponent(query)}`;
24 }
26 devSearch(query) {
27 return `/api/dev-search?q=${encodeURIComponent(query)}`;
28 }
30 opsSearch(query) {
31 return `/api/ops-search?q=${encodeURIComponent(query)}`;
32 }
34 patpSearch(query) {
35 return (
36 (ob.isValidPatp(`~${deSig(query.toLowerCase())}`) &&
37 `~${deSig(query.toLowerCase())}`.length < 15) ||
38 (!isNaN(query) && query <= 4294967295)
39 );
40 }
42 onSelect(item) {
43 this.setState({
44 query: "",
45 results: [],
46 });
48 if (item.url) {
49 this.props.router.push(`${item.url}${item?.slug ? item.slug : ""}`);
50 return this.props.closeSearch();
51 }
52 if (item.slug) {
53 this.props.router.push(item.slug);
54 return this.props.closeSearch();
55 }
56 }
58 onInputValueChange = debounce(async (query) => {
59 if (query.length) {
60 const search = fetch(this.searchEndpoint(query))
61 .then((res) => res.json())
62 .then(async (res) => {
63 return [...res.glossary.map((item) => ({
64 type: "GLOSSARY_RESULT",
65 content: item
66 })), ...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 devSearch = fetch(this.devSearch(query))
91 .then((res) => res.json())
92 .then((res) => {
93 return [...res.glossary.map((item) => ({
94 type: "GLOSSARY_RESULT",
95 content: item
96 })), ...res.results.map((item) => ({
97 type: "DEV_RESULT",
98 content: item,
99 }))];
100 });
102 const opsSearch = fetch(this.opsSearch(query))
103 .then((res) => res.json())
104 .then((res) => {
105 return [...res.glossary.map((item) => ({
106 type: "GLOSSARY_RESULT",
107 content: item
108 })), ...res.results.map((item) => ({
109 type: "OPS_RESULT",
110 content: item,
111 }))];
112 });
114 const [results, devResults, opsResults] =
115 await Promise.all([search, devSearch, opsSearch]);
117 const list = [
118 ...patpResult,
119 ...results,
120 ...devResults,
121 ...opsResults,
122 ]
123 .sort((a, b) => {
124 const aNum = a?.type === "GLOSSARY_RESULT" ? 1 : -1;
125 const bNum = b?.type === "GLOSSARY_RESULT" ? 1 : -1;
126 return bNum - aNum;
127 });;
129 this.setState({ results: list });
130 } else {
131 this.setState({ results: [] });
132 }
133 }, 250);
135 render() {
136 const { state, props } = this;
138 if (props.showSearch) {
139 return (
140 <Downshift
141 onSelect={(selection) => this.onSelect(selection)}
142 onInputValueChange={(event) => this.onInputValueChange(event)}
143 itemToString={(item) => (item ? item.slug : "")}
144 defaultHighlightedIndex={0}
145 >
146 {({
147 getInputProps,
148 getItemProps,
149 getLabelProps,
150 getMenuProps,
151 isOpen,
152 inputValue,
153 highlightedIndex,
154 selectedItem,
155 getRootProps,
156 }) => (
157 <div className="fixed w-screen h-screen z-50 flex flex-col items-center p-4">
158 <div
159 onClick={(event) => props.closeSearch(event)}
160 className="top-0 left-0 fixed w-screen h-screen bg-washedWall"
161 />
162 <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">
163 <div
164 style={{ display: "inline-block" }}
165 {...getRootProps({}, { suppressRefError: true })}
166 >
167 <input
168 autoFocus
169 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"
170 placeholder="Search..."
171 type="text"
172 onClick={(e) => e.stopPropagation()}
173 {...getInputProps({
174 onKeyDown: (event) => {
175 if (event.key === "Escape") {
176 // Prevent Downshift's default 'Escape' behavior.
177 event.nativeEvent.preventDownshiftDefault = true;
178 this.props.closeSearch(event);
179 }
180 },
181 })}
182 />
183 </div>
184 <ul {...getMenuProps()} className="overflow-y-scroll">
185 {isOpen
186 ? state.results.map((item, index) => {
187 const selected = highlightedIndex === index;
188 if (item.type === "PATP") {
189 return (
190 <li
191 className={`cursor-pointer p-2 flex space-x-2 items-center text-left w-full ${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 ${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 ${selected ? "text-white" : "text-wall-600"
226 }`}
227 >
228 {item.content.symbol.length > 0 && (
229 <code
230 className={`mr-1 rounded px-1 py-0.5 ${selected
231 ? "bg-washedWhite"
232 : "bg-wall-100"
233 }`}
234 >
235 {item.content.symbol}
236 </code>
237 )}
238 {item.content.name}
239 </p>
240 <p
241 className={`font-normal text-base mt-1 ${selected ? "text-white" : "text-wall-600"
242 }`}
243 dangerouslySetInnerHTML={{
244 __html: item.content.desc,
245 }}
246 ></p>
247 </div>
248 </li>
249 );
250 }
251 if (item.type === "RESULT") {
252 return (
253 <li
254 className={`cursor-pointer flex text-left w-full ${selected ? "bg-green-400" : ""
255 }`}
256 {...getItemProps({
257 key: item.content.link + "-" + index,
258 index,
259 item: item.content,
260 selected,
261 })}
262 >
263 <div className="p-3">
264 <p
265 className={`font-medium text-base ${selected ? "text-white" : "text-wall-600"
266 }`}
267 >
268 {item.content.parent !== "Content"
269 ? `${item.content.parent} /`
270 : ""}{" "}
271 {item.content.title}
272 </p>
273 <p
274 className={`text-base font-regular text-small ${selected ? "text-midWhite" : "text-wall-500"
275 }`}
276 >
277 {item.content.content}
278 {item?.content?.foundOnPage && (
279 <span className="italic block">
280 Found in page content
281 </span>
282 )}
283 </p>
284 </div>
285 </li>
286 );
287 }
288 if (item.type === "DEV_RESULT") {
289 const devItem = Object.assign({}, item.content);
290 devItem[
291 "slug"
292 ] = `https://developers.urbit.org${item.content.slug}`;
293 return (
294 <li
295 className={`cursor-pointer flex text-left w-full ${selected ? "bg-green-400" : ""
296 }`}
297 {...getItemProps({
298 key: item.content.link + "-" + index,
299 index,
300 item: devItem,
301 selected,
302 })}
303 >
304 <div className="p-3">
305 <p
306 className={`font-medium text-base ${selected ? "text-white" : "text-wall-600"
307 }`}
308 >
309 <span className="text-wall-400">
310 {item.content.parent !== "Content"
311 ? `developers.urbit.org / ${item.content.parent} /`
312 : "developers.urbit.org /"}{" "}
313 </span>
314 {item.content.title}
315 </p>
316 <p
317 className={`text-base font-regular text-small ${selected ? "text-midWhite" : "text-wall-500"
318 }`}
319 >
320 {item.content.content}
321 {item?.content?.foundOnPage && (
322 <span className="italic block">
323 Found in page content
324 </span>
325 )}
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 ${selected ? "bg-green-400" : ""
339 }`}
340 {...getItemProps({
341 key: item.content.link + "-" + index,
342 index,
343 item: opsItem,
344 selected,
345 })}
346 >
347 <div className="p-3">
348 <p
349 className={`font-medium text-base ${selected ? "text-white" : "text-wall-600"
350 }`}
351 >
352 <span className="text-wall-400">
353 {item.content.parent !== "Content"
354 ? `operators.urbit.org / ${item.content.parent} /`
355 : "operators.urbit.org /"}{" "}
356 </span>
357 {item.content.title}
358 </p>
359 <p
360 className={`text-base font-regular text-small ${selected ? "text-midWhite" : "text-wall-500"
361 }`}
362 >
363 {item.content.content}
364 {item?.content?.foundOnPage && (
365 <span className="italic block">
366 Found in page content
367 </span>
368 )}
369 </p>
370 </div>
371 </li>
372 );
373 }
374 return null;
375 })
376 : null}
377 </ul>
378 </div>
379 </div>
380 )}
381 </Downshift>
382 );
383 } else {
384 return null;
385 }
386 }
389function deSig(string) {
390 return string.startsWith("~") ? string.substring(1) : string;
393export default withRouter(Search);