on master help sign in
Merge pull request #1901 from urbit/mp/search-component

pages, components: pull Search into design system

commitea8466328bc6589dbb4c5af176ae6c7ea59efea5 authormatildepark <m@park.computer> dateMon 21 Nov 2022, 14:31 (4 years ago) parent869ba653914c61
9 files changed+104−444
components/Search.js−393 components/Sigil.js−36 components/gateway/Image.js+1−2 package-lock.json+44−9 package.json+1−1 pages/_app.js+4−2 pages/api/roadmap-search.js+8 pages/api/urbit-org-search.js+45 pages/ids/submit.js+1−1
components/Search.jsdeleted−393
@@ -1,393 +0,0 @@
1−import { Component } from "react";
2−import { withRouter } from "next/router";
3−import debounce from "lodash.debounce";
4−import Downshift from "downshift";
5−import ob from "urbit-ob";
6−import Sigil from "../components/Sigil";
7−
8−class 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− }
21−
22− searchEndpoint(query) {
23− return `/api/search?q=${encodeURIComponent(query)}`;
24− }
25−
26− devSearch(query) {
27− return `/api/dev-search?q=${encodeURIComponent(query)}`;
28− }
29−
30− opsSearch(query) {
31− return `/api/ops-search?q=${encodeURIComponent(query)}`;
32− }
33−
34− patpSearch(query) {
35− return (
36− (ob.isValidPatp(`~${deSig(query.toLowerCase())}`) &&
37− `~${deSig(query.toLowerCase())}`.length < 15) ||
38− (!isNaN(query) && query <= 4294967295)
39− );
40− }
41−
42− onSelect(item) {
43− this.setState({
44− query: "",
45− results: [],
46− });
47−
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− }
57−
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− });
71−
72− const patp = this.patpSearch(query)
73− ? !isNaN(query)
74− ? ob.patp(query)
75− : ob.patp(ob.patp2dec(`~${deSig(query)}`))
76− : null;
77−
78− const patpResult = this.patpSearch(query)
79− ? [
80− {
81− type: "PATP",
82− content: {
83− patp: patp,
84− slug: `/ids/${patp}`,
85− },
86− },
87− ]
88− : [];
89−
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− });
101−
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− });
113−
114− const [results, devResults, opsResults] =
115− await Promise.all([search, devSearch, opsSearch]);
116−
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− });;
128−
129− this.setState({ results: list });
130− } else {
131− this.setState({ results: [] });
132− }
133− }, 250);
134−
135− render() {
136− const { state, props } = this;
137−
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− }
387−}
388−
389−function deSig(string) {
390− return string.startsWith("~") ? string.substring(1) : string;
391−}
392−
393−export default withRouter(Search);
components/Sigil.jsdeleted−36
@@ -1,36 +0,0 @@
1−import { sigil, reactRenderer } from "@tlon/sigil-js";
2−
3−export const foregroundFromBackground = (background) => {
4− const rgb = {
5− r: parseInt(background.slice(1, 3), 16),
6− g: parseInt(background.slice(3, 5), 16),
7− b: parseInt(background.slice(5, 7), 16),
8− };
9− const brightness = (299 * rgb.r + 587 * rgb.g + 114 * rgb.b) / 1000;
10− const whiteBrightness = 255;
11−
12− return whiteBrightness - brightness < 50 ? "black" : "white";
13−};
14−
15−export const Sigil = ({ patp, size, color = "#24201E", icon }) => {
16− if (patp.length > 14) {
17− return <div />;
18− }
19− const foreground = foregroundFromBackground(color);
20− return (
21− <div
22− className={icon ? "p-1" : ""}
23− style={{ backgroundColor: icon ? color || "black" : "transparent" }}
24− >
25− {sigil({
26− patp: patp,
27− renderer: reactRenderer,
28− size: icon ? size / 2 : size,
29− colors: [color, foreground],
30− icon: icon || false,
31− })}
32− </div>
33− );
34−};
35−
36−export default Sigil;
components/gateway/Image.js+1−2
@@ -1,5 +1,4 @@
1−import Sigil from "../Sigil";
2−
1+import { Sigil } from "@urbit/foundation-design-system";
32const GatewayImage = ({
43 error = false,
54 patp = false,
package-lock.json+44−9
@@ -12,7 +12,7 @@
1212 "@socialgouv/matomo-next": "^1.3.0",
1313 "@tailwindcss/aspect-ratio": "^0.4.0",
1414 "@tlon/sigil-js": "^1.4.5",
15− "@urbit/foundation-design-system": "0.6.4",
15+ "@urbit/foundation-design-system": "0.7.1",
1616 "@urbit/markdoc": "^0.1.4",
1717 "axios": "^0.26.1",
1818 "buffer": "^6.0.3",
@@ -358,17 +358,21 @@
358358 "dev": true
359359 },
360360 "node_modules/@urbit/foundation-design-system": {
361− "version": "0.6.4",
362− "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.6.4.tgz",
363− "integrity": "sha512-gg2Vx9BHwoV7Z3JheUB8D+HEMMdJGtOwD7YZSGZeIe2Scy3HL9MO5sT774gfhA6J6aI3pRgyxv2R209ShgUChg==",
361+ "version": "0.7.1",
362+ "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.7.1.tgz",
363+ "integrity": "sha512-yQl+bf2TLv+ytlvo6Qq68D39OM18nzRfGTPTz8XukwtfoeThVJy35t5ZFAr4hyrADb4309mNIDCd6Pd7TQGImw==",
364364 "dependencies": {
365365 "@iarna/toml": "^2.2.5",
366+ "@tlon/sigil-js": "^1.4.5",
366367 "core-js": "^3.23.1",
368+ "downshift": "^7.0.1",
367369 "gray-matter": "^4.0.3",
368370 "html-react-parser": "^1.4.14",
371+ "lodash.debounce": "^4.0.8",
369372 "luxon": "^3.0.1",
370373 "prism-react-renderer": "^1.3.3",
371− "react-swipeable": "^6.2.1"
374+ "react-swipeable": "^6.2.1",
375+ "urbit-ob": "^5.0.1"
372376 },
373377 "peerDependencies": {
374378 "@tailwindcss/aspect-ratio": "^0.4.0",
@@ -384,6 +388,21 @@
384388 "tailwindcss-theme-swapper": "^0.6.2"
385389 }
386390 },
391+ "node_modules/@urbit/foundation-design-system/node_modules/downshift": {
392+ "version": "7.0.1",
393+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.0.1.tgz",
394+ "integrity": "sha512-AdFU8qJjUg9WZaLKzh7xAPfEwAB0dSqDGb7/Gnx/teldUXcNlaZrxhymOWedzDV+62xphMVustttAngmG33pXw==",
395+ "dependencies": {
396+ "@babel/runtime": "^7.14.8",
397+ "compute-scroll-into-view": "^1.0.17",
398+ "prop-types": "^15.7.2",
399+ "react-is": "^17.0.2",
400+ "tslib": "^2.3.0"
401+ },
402+ "peerDependencies": {
403+ "react": ">=16.12.0"
404+ }
405+ },
387406 "node_modules/@urbit/foundation-design-system/node_modules/luxon": {
388407 "version": "3.1.0",
389408 "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.0.tgz",
@@ -3323,19 +3342,35 @@
33233342 "dev": true
33243343 },
33253344 "@urbit/foundation-design-system": {
3326− "version": "0.6.4",
3327− "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.6.4.tgz",
3328− "integrity": "sha512-gg2Vx9BHwoV7Z3JheUB8D+HEMMdJGtOwD7YZSGZeIe2Scy3HL9MO5sT774gfhA6J6aI3pRgyxv2R209ShgUChg==",
3345+ "version": "0.7.1",
3346+ "resolved": "https://registry.npmjs.org/@urbit/foundation-design-system/-/foundation-design-system-0.7.1.tgz",
3347+ "integrity": "sha512-yQl+bf2TLv+ytlvo6Qq68D39OM18nzRfGTPTz8XukwtfoeThVJy35t5ZFAr4hyrADb4309mNIDCd6Pd7TQGImw==",
33293348 "requires": {
33303349 "@iarna/toml": "^2.2.5",
3350+ "@tlon/sigil-js": "^1.4.5",
33313351 "core-js": "^3.23.1",
3352+ "downshift": "^7.0.1",
33323353 "gray-matter": "^4.0.3",
33333354 "html-react-parser": "^1.4.14",
3355+ "lodash.debounce": "^4.0.8",
33343356 "luxon": "^3.0.1",
33353357 "prism-react-renderer": "^1.3.3",
3336− "react-swipeable": "^6.2.1"
3358+ "react-swipeable": "^6.2.1",
3359+ "urbit-ob": "^5.0.1"
33373360 },
33383361 "dependencies": {
3362+ "downshift": {
3363+ "version": "7.0.1",
3364+ "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.0.1.tgz",
3365+ "integrity": "sha512-AdFU8qJjUg9WZaLKzh7xAPfEwAB0dSqDGb7/Gnx/teldUXcNlaZrxhymOWedzDV+62xphMVustttAngmG33pXw==",
3366+ "requires": {
3367+ "@babel/runtime": "^7.14.8",
3368+ "compute-scroll-into-view": "^1.0.17",
3369+ "prop-types": "^15.7.2",
3370+ "react-is": "^17.0.2",
3371+ "tslib": "^2.3.0"
3372+ }
3373+ },
33393374 "luxon": {
33403375 "version": "3.1.0",
33413376 "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.1.0.tgz",
package.json+1−1
@@ -21,7 +21,7 @@
2121 "@socialgouv/matomo-next": "^1.3.0",
2222 "@tailwindcss/aspect-ratio": "^0.4.0",
2323 "@tlon/sigil-js": "^1.4.5",
24− "@urbit/foundation-design-system": "0.6.4",
24+ "@urbit/foundation-design-system": "0.7.1",
2525 "@urbit/markdoc": "^0.1.4",
2626 "axios": "^0.26.1",
2727 "buffer": "^6.0.3",
pages/_app.js+4−2
@@ -1,7 +1,7 @@
11import { useState, useEffect } from "react";
22import { configure, GlobalHotKeys } from "react-hotkeys";
33import { init } from "@socialgouv/matomo-next";
4−import Search from "../components/Search";
4+import { Search } from "@urbit/foundation-design-system";
55import "@urbit/foundation-design-system/styles/globals.css";
66import "@urbit/foundation-design-system/styles/markdown.css";
77import "@urbit/foundation-design-system/styles/prism.css";
@@ -54,7 +54,7 @@ function MyApp({ Component, pageProps }) {
5454 configure({
5555 // ignoreTags: [],
5656 ignoreTags: ["input", "select", "textarea"],
57− ignoreEventsCondition: function () {},
57+ ignoreEventsCondition: function () { },
5858 });
5959
6060 return (
@@ -65,6 +65,8 @@ function MyApp({ Component, pageProps }) {
6565 toggleSearch={toggleSearch}
6666 closeSearch={closeSearch}
6767 openSearch={openSearch}
68+ order={["org", "dev", "ops", "roadmap"]}
69+ ourSite="https://urbit.org"
6870 />
6971 <Component
7072 {...pageProps}
pages/api/roadmap-search.jsnew file+8
@@ -0,0 +1,8 @@
1+export default async (req, res) => {
2+ const results = await fetch(
3+ `https://roadmap.urbit.org/api/search?q=${req.query.q}`
4+ ).then((res) => res.json());
5+ res.statusCode = 200;
6+ res.setHeader("Content-Type", "application/json");
7+ res.end(JSON.stringify(results));
8+};
pages/api/urbit-org-search.jsnew file+45
@@ -0,0 +1,45 @@
1+import { index } from "../../cache/data";
2+import { glossary } from "../../cache/glossary";
3+const levenSort = require("leven-sort");
4+
5+export default (req, res) => {
6+ const results = index.filter(
7+ (e) =>
8+ e.title?.toLowerCase().includes(req.query.q.toLowerCase()) ||
9+ e?.slug.includes(req.query.q.toLowerCase()) ||
10+ e?.parent.toLowerCase().includes(req.query.q.toLowerCase())
11+ );
12+
13+ const entries = glossary.filter((entry) => {
14+ return (
15+ entry.name.includes(req.query.q.toLowerCase()) ||
16+ entry.symbol.includes(req.query.q)
17+ );
18+ });
19+
20+ results.push(
21+ ...index
22+ .filter((e) => !results.includes(e))
23+ .filter((e) => {
24+ let found = false;
25+ e.headings.forEach((heading) => {
26+ if (heading.toLowerCase().includes(req.query.q.toLowerCase())) {
27+ found = true;
28+ }
29+ });
30+ return found;
31+ })
32+ .map((e) => ({ ...e, foundOnPage: true }))
33+ );
34+
35+ const sorted = levenSort(results, req.q, [
36+ "title",
37+ "slug",
38+ "parent",
39+ "headings",
40+ ]);
41+ const sortedEntries = levenSort(entries, req.query.q, ["symbol"]);
42+ res.statusCode = 200;
43+ res.setHeader("Content-Type", "application/json");
44+ res.end(JSON.stringify({ results: sorted, glossary: sortedEntries }));
45+};
pages/ids/submit.js+1−1
@@ -3,10 +3,10 @@ import {
33 Container,
44 SingleColumn,
55 Section,
6+ Sigil
67} from "@urbit/foundation-design-system";
78import { useInputChange } from "../../lib/hooks";
89import ob from "urbit-ob";
9−import Sigil from "../../components/Sigil";
1010import Head from "next/head";
1111import Meta from "../../components/Meta";
1212