From da1b8cef39ee7bc53fb1578375247c3c6b8e6b8b Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Thu, 20 Apr 2023 17:22:21 +0800 Subject: [PATCH 01/18] Implement new search UI --- src/components/Modal.tsx | 1 + src/components/Nav.tsx | 7 +- src/components/Search.tsx | 144 ---------------------- src/components/Search/Modal.tsx | 123 ++++++++++++++++++ src/components/Search/OpenModalButton.tsx | 28 +++++ src/components/Search/Results.tsx | 90 ++++++++++++++ src/components/Search/index.tsx | 4 + src/components/Sidebar.tsx | 4 +- src/hooks/useSearchIndex.ts | 26 ++++ src/layouts/Page.tsx | 8 +- src/{store.ts => store/index.ts} | 0 src/types.ts | 23 ++++ 12 files changed, 305 insertions(+), 153 deletions(-) delete mode 100644 src/components/Search.tsx create mode 100644 src/components/Search/Modal.tsx create mode 100644 src/components/Search/OpenModalButton.tsx create mode 100644 src/components/Search/Results.tsx create mode 100644 src/components/Search/index.tsx create mode 100644 src/hooks/useSearchIndex.ts rename src/{store.ts => store/index.ts} (100%) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 32fbef9d2..442a67a4b 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -47,6 +47,7 @@ const ModalDialog: React.FC> = ({ css={[ tw`fixed top-0 right-0 bottom-0 left-0 select-none z-50`, tw`bg-black bg-opacity-50`, + tw`overflow-scroll`, ]} > diff --git a/src/components/Nav.tsx b/src/components/Nav.tsx index 60bc04905..767f3e703 100644 --- a/src/components/Nav.tsx +++ b/src/components/Nav.tsx @@ -4,6 +4,7 @@ import { Home, Menu, X } from "react-feather"; import tw from "twin.macro"; import { Link } from "./Link"; import { Logo } from "./Logo"; +import { OpenSearchModalButton } from "@/components/Search"; import { MobileSidebar } from "./Sidebar"; import { ThemeSwitcher } from "./ThemeSwitcher"; @@ -55,9 +56,9 @@ export const MobileNav: React.FC = () => { - {/*
- -
*/} +
+ +
- - ); -}; - -const MAX_NUM_RESULTS = 5; - -export const SearchModal: React.FC<{ - fuse: Fuse; - closeModal: () => void; -}> = ({ fuse, closeModal }) => { - const [query, setQuery] = useState(""); - const [results, setResults] = useState([]); - const [selected, setSelected] = useState(0); - const router = useRouter(); - - const onPressUp = useCallback(() => { - setSelected(Math.max(0, selected - 1)); - }, [selected, results, setSelected]); - - const onPressDown = useCallback(() => { - setSelected(Math.min(results.length - 1, selected + 1)); - }, [selected, results, setSelected]); - - const handleEnter = useCallback(() => { - const item = results[selected]; - if (item != null) { - closeModal(); - router.push(item.slug); - } - }, [selected, results]); - - useEffect(() => { - const preventDefault = (fn: () => void) => (e: KeyboardEvent) => { - e.preventDefault(); - fn(); - }; - - const unsubscribe = tinykeys(window, { - Enter: () => handleEnter(), - - ArrowUp: () => onPressUp(), - "Control+k": () => onPressUp(), - "Control+p": preventDefault(() => onPressUp()), - - ArrowDown: () => onPressDown(), - "Control+j": preventDefault(() => onPressDown()), - "Control+n": preventDefault(() => onPressDown()), - }); - - return () => unsubscribe(); - }, [handleEnter, onPressUp, onPressDown]); - - useEffect(() => { - const results = fuse.search(query); - const pages = results.map(r => r.item).slice(0, MAX_NUM_RESULTS); - setResults(pages); - setSelected(Math.max(0, Math.min(pages.length - 1, selected))); - }, [query]); - - return ( -
{ - closeModal(); - }} - > -
e.stopPropagation()} - > - setQuery(e.target.value)} - autoFocus - tw="px-4 py-4 w-full bg-transparent focus:outline-none" - /> - -
-
    - {results.map((item, index) => ( -
  • setSelected(index)}> - { - closeModal(); - }} - css={[ - tw`flex items-center justify-between px-3 h-16 relative`, - index === selected - ? tw`text-pink-900 bg-pink-100` - : tw`text-gray-500`, - ]} - > -
    - {item.category != null && ( - - {item.category} - - )} - {item.title} -
    - - - -
  • - ))} -
-
-
-
- ); -}; diff --git a/src/components/Search/Modal.tsx b/src/components/Search/Modal.tsx new file mode 100644 index 000000000..30234b0ee --- /dev/null +++ b/src/components/Search/Modal.tsx @@ -0,0 +1,123 @@ +import { useSearchIndex } from "@/hooks/useSearchIndex"; +import { DiscordIcon } from "@/components/Icons"; +import { Search } from "@/types"; +import { Link } from "@/components/Link"; +import React, { useCallback, useEffect, useState } from "react"; +import { HelpCircle, Mail } from "react-feather"; +import tw from "twin.macro"; +import SearchResults from "./Results"; + +const SearchModal: React.FC<{ + closeModal: () => void; +}> = ({ closeModal }) => { + const [query, setQuery] = useState(""); + const [response, setResponse] = useState( + null, + ); + + const index = useSearchIndex( + process.env.NEXT_PUBLIC_MEILISEARCH_HOST ?? "", + process.env.NEXT_PUBLIC_MEILISEARCH_READ_API_KEY ?? "", + process.env.NEXT_PUBLIC_MEILISEARCH_INDEX_NAME ?? "", + ); + + const fetchResponse = useCallback(async () => { + if (query === "") { + return; + } + try { + const response = await index.search( + query, + { + limit: 10, + attributesToHighlight: ["*"], + highlightPreTag: "", + highlightPostTag: "", + }, + ); + setResponse(response.hits); + } catch (e) { + console.error(`Search for query "${query}" failed (${e})`); + } + }, [query]); + + useEffect(() => { + if (query === "") { + return; + } + fetchResponse(); + }, [query]); + + return ( +
{ + closeModal(); + }} + > +
e.stopPropagation()} + css={[tw`bg-background border rounded-lg w-full md:w-1/2 mx-auto`]} + > + setQuery(e.target.value)} + autoFocus + css={[ + tw`px-4 py-4 w-full bg-transparent focus:outline-none`, + tw`border-b-2 border-gray-200 border-dotted`, + ]} + /> +
+ {response && + (response.length === 0 ? ( +
+ +
+

+ We couldn't find what you're searching for. +

+
+

Reach out to us if you need help:

+
+ + + Discord + + + + Email + +
+
+
+
+ ) : ( + + ))} +
+
+
+ ); +}; + +export default SearchModal; diff --git a/src/components/Search/OpenModalButton.tsx b/src/components/Search/OpenModalButton.tsx new file mode 100644 index 000000000..ddf1133e1 --- /dev/null +++ b/src/components/Search/OpenModalButton.tsx @@ -0,0 +1,28 @@ +import { searchStore } from "@/store"; +import React from "react"; +import { Search as SearchIcon } from "react-feather"; +import tw from "twin.macro"; + +const OpenSearchModalButton: React.FC = () => { + return ( + <> + + + ); +}; + +export default OpenSearchModalButton; diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx new file mode 100644 index 000000000..fb3d6893d --- /dev/null +++ b/src/components/Search/Results.tsx @@ -0,0 +1,90 @@ +import { Link } from "@/components/Link"; +import { Search } from "@/types"; +import { Markup } from "interweave"; +import React from "react"; +import tw from "twin.macro"; + +const SearchResults: React.FC<{ + response: Search.MeilisearchResponse; +}> = ({ response }) => { + const chapters = Array.from(new Set(response.map(r => r.hierarchy_lvl0))); + + const results = chapters.reduce((acc, curr) => { + acc[curr] = [ + ...response + .filter(r => r.hierarchy_lvl0 === curr) + .map(hit => ({ + hierarchies: [ + // `hit.hierarchy_lvl0` is intentionally ignored here; we're + // grouping the rendered output by it so it's redundant. + // In practice, this means that we'll render: + // >> "Databases" + // >> "PostgreSQL" + // >> ... + // instead of: + // >> "Databases" + // >> "Databases -> PostgreSQL" + // >> ... + hit.hierarchy_lvl1, + hit.hierarchy_lvl2, + hit.hierarchy_lvl3, + hit.hierarchy_lvl4, + ].filter(h => h !== null), + slug: hit.url, + text: hit._formatted.content, + })), + ]; + return acc; + }, {} as Search.Result); + + const cleanSlug = (slug: string) => slug.replace("#__next", ""); + + return ( +
+ {Array.from(Object.entries(results)).map(([chapter, hits]) => { + return ( +
+

{chapter}

+
    + {hits.map(h => { + return ( +
  • + + {h.hierarchies.join(" -> ")} + + .rendered span]:bg-yellow-200`, + tw`[&>.rendered span]:p-1`, + tw`[&>.rendered span]:text-black`, + tw`[&>.rendered span]:dark:text-white`, + ]} + > + {h.text !== "" && ( + + )} + +
  • + ); + })} +
+
+ ); + })} +
+ ); +}; + +export default SearchResults; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx new file mode 100644 index 000000000..164188a26 --- /dev/null +++ b/src/components/Search/index.tsx @@ -0,0 +1,4 @@ +import SearchModal from "./Modal"; +import OpenSearchModalButton from "./OpenModalButton"; + +export { SearchModal, OpenSearchModalButton }; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 9cddb0afd..ab6e61ea9 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -7,7 +7,7 @@ import { Link } from "./Link"; import { Logo } from "./Logo"; import { ISidebarSection } from "@/types"; import { ScrollArea } from "./ScrollArea"; -import { Search } from "./Search"; +import { OpenSearchModalButton } from "@/components/Search"; import { ThemeSwitcher } from "./ThemeSwitcher"; export const Sidebar: React.FC = ({ ...props }) => { @@ -35,7 +35,7 @@ export const Sidebar: React.FC = ({ ...props }) => {
- +
diff --git a/src/hooks/useSearchIndex.ts b/src/hooks/useSearchIndex.ts new file mode 100644 index 000000000..d63ac3cce --- /dev/null +++ b/src/hooks/useSearchIndex.ts @@ -0,0 +1,26 @@ +import { Index, MeiliSearch } from "meilisearch"; +import { useMemo } from "react"; + +export const useSearchIndex = ( + host: string, + apiKey: string, + indexName: string, +): Index => { + if (host === "") { + console.error(`useSearchIndex.host not provided`); + } + if (apiKey === "") { + console.error(`useSearchIndex.apiKey not provided`); + } + if (indexName === "") { + console.error(`useSearchIndex.indexName not provided`); + } + return useMemo(() => { + const meilisearch = new MeiliSearch({ + host, + apiKey, + }); + const index = meilisearch.index(indexName); + return index; + }, [apiKey, host, indexName]); +}; diff --git a/src/layouts/Page.tsx b/src/layouts/Page.tsx index fe9c7a206..8e78532ee 100644 --- a/src/layouts/Page.tsx +++ b/src/layouts/Page.tsx @@ -3,14 +3,14 @@ import Fuse from "fuse.js"; import React, { PropsWithChildren, useEffect, useMemo } from "react"; import tinykeys from "tinykeys"; import "twin.macro"; -import { Modal } from "../components/Modal"; +import { Modal } from "@/components/Modal"; import { MobileNav, Nav } from "../components/Nav"; -import { SearchModal } from "../components/Search"; +import { SearchModal } from "@/components/Search"; import { Props as SEOProps, SEO } from "../components/SEO"; import { Sidebar } from "../components/Sidebar"; import { sidebarContent } from "../data/sidebar"; import { Background } from "../pages"; -import { searchStore } from "../store"; +import { searchStore } from "@/store"; export interface Props { seo?: SEOProps; @@ -65,7 +65,7 @@ export const Page: React.FC> = props => { isOpen={isSearchOpen} onClose={() => searchStore.set(false)} > - searchStore.set(false)} /> + searchStore.set(false)} /> ); diff --git a/src/store.ts b/src/store/index.ts similarity index 100% rename from src/store.ts rename to src/store/index.ts diff --git a/src/types.ts b/src/types.ts index 4fd7f004b..64c4f4ade 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,3 +15,26 @@ export interface ISidebarSection { title?: string; pages: IPage[]; } + +export namespace Search { + export interface ResultItem { + hierarchies: string[]; + slug: string; + text: string; + } + export type Result = Record; + export type MeilisearchResponse = MeilisearchResponseItem[]; + + interface _MeilisearchResponseItem { + hierarchy_lvl0: string; + hierarchy_lvl1: string; + hierarchy_lvl2: string; + hierarchy_lvl3: string; + hierarchy_lvl4: string; + url: string; + content: string; + } + export interface MeilisearchResponseItem extends _MeilisearchResponseItem { + _formatted: _MeilisearchResponseItem; + } +} From d07b7a7156b68f411398b4b60190b0ac9894b66e Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Mon, 24 Apr 2023 16:34:46 +0800 Subject: [PATCH 02/18] Move "no results" into its own component --- src/components/Search/Modal.tsx | 44 ++--------------------------- src/components/Search/NoResults.tsx | 44 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 42 deletions(-) create mode 100644 src/components/Search/NoResults.tsx diff --git a/src/components/Search/Modal.tsx b/src/components/Search/Modal.tsx index 30234b0ee..6f3b9fad3 100644 --- a/src/components/Search/Modal.tsx +++ b/src/components/Search/Modal.tsx @@ -1,10 +1,8 @@ import { useSearchIndex } from "@/hooks/useSearchIndex"; -import { DiscordIcon } from "@/components/Icons"; import { Search } from "@/types"; -import { Link } from "@/components/Link"; import React, { useCallback, useEffect, useState } from "react"; -import { HelpCircle, Mail } from "react-feather"; import tw from "twin.macro"; +import NoResults from "./NoResults"; import SearchResults from "./Results"; const SearchModal: React.FC<{ @@ -72,45 +70,7 @@ const SearchModal: React.FC<{
{response && (response.length === 0 ? ( -
- -
-

- We couldn't find what you're searching for. -

-
-

Reach out to us if you need help:

-
- - - Discord - - - - Email - -
-
-
-
+ ) : ( ))} diff --git a/src/components/Search/NoResults.tsx b/src/components/Search/NoResults.tsx new file mode 100644 index 000000000..b3b763572 --- /dev/null +++ b/src/components/Search/NoResults.tsx @@ -0,0 +1,44 @@ +import { DiscordIcon } from "@/components/Icons"; +import { Link } from "@/components/Link"; +import React from "react"; +import { HelpCircle, Mail } from "react-feather"; +import tw, { styled } from "twin.macro"; + +const ContactButton = styled(Link)` + ${[ + tw`flex flex-row items-center gap-2`, + tw`border border-solid rounded-lg`, + tw`hover:bg-pink-100`, + tw`p-2`, + tw`[&>svg]:w-8`, + tw`[&>svg]:h-8`, + ]} +`; + +const NoResults: React.FC = () => { + return ( +
+ +
+

+ We couldn't find what you're searching for. +

+
+

Reach out to us if you need help:

+
+ + + Discord + + + + Email + +
+
+
+
+ ); +}; + +export default NoResults; From 802072327d22070e5aa8080a685e30ead6b68cba Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Mon, 24 Apr 2023 18:04:59 +0800 Subject: [PATCH 03/18] re-lock dependencies --- package.json | 2 ++ yarn.lock | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/package.json b/package.json index 50a0e7d00..7f349d9b6 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "dayjs": "^1.10.4", "fathom-client": "^3.1.0", "fuse.js": "^6.4.6", + "interweave": "^13.1.0", + "meilisearch": "^0.32.3", "nanostores": "^0.7.4", "next": "13.2.1", "next-contentlayer": "^0.3.0", diff --git a/yarn.lock b/yarn.lock index ac9a4f470..f1ca2865b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2533,6 +2533,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -2694,6 +2701,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" @@ -3122,6 +3134,13 @@ inline-style-parser@0.1.1: resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== +interweave@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/interweave/-/interweave-13.1.0.tgz#4b7a0a87a7eb32001bef64525f68d95296dee03c" + integrity sha512-JIDq0+2NYg0cgL7AB26fBcV0yZdiJvPDBp+aF6k8gq6Cr1kH5Gd2/Xqn7j8z+TGb8jCWZn739jzalCz+nPYwcA== + dependencies: + escape-html "^1.0.3" + intl-messageformat@^10.1.0: version "10.3.1" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.1.tgz#99b280b706e4eeb232458dcb1b5758e80abf919c" @@ -3608,6 +3627,13 @@ mdx-bundler@^9.2.1: uuid "^8.3.2" vfile "^5.3.2" +meilisearch@^0.32.3: + version "0.32.3" + resolved "https://registry.yarnpkg.com/meilisearch/-/meilisearch-0.32.3.tgz#cbc224014c3dddc818090f4b501029993f117b6a" + integrity sha512-EOgfBuRE5SiIPIpEDYe2HO0D7a4z5bexIgaAdJFma/dH5hx1kwO+u/qb2g3qKyjG+iA3l8MlmTj/Xd72uahaAw== + dependencies: + cross-fetch "^3.1.5" + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -4112,6 +4138,13 @@ node-domexception@^1.0.0: resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== +node-fetch@2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-fetch@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.0.tgz#37e71db4ecc257057af828d523a7243d651d91e4" @@ -4973,6 +5006,11 @@ toml@^3.0.0: resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -5196,6 +5234,19 @@ web-streams-polyfill@^3.0.3: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" From bb09a5550b8f504738877112b2c88957f8023fa1 Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Mon, 24 Apr 2023 18:06:13 +0800 Subject: [PATCH 04/18] Refactor: Move search logic into reusable hooks --- src/components/Search/Modal.tsx | 47 ++++++++--------------------- src/components/Search/NoResults.tsx | 3 +- src/components/Search/Results.tsx | 11 ++++--- src/hooks/useDebouncedSearch.ts | 45 +++++++++++++++++++++++++++ src/hooks/useSearchIndex.ts | 7 ++--- src/types.ts | 9 +++--- 6 files changed, 72 insertions(+), 50 deletions(-) create mode 100644 src/hooks/useDebouncedSearch.ts diff --git a/src/components/Search/Modal.tsx b/src/components/Search/Modal.tsx index 6f3b9fad3..be2bcc7d9 100644 --- a/src/components/Search/Modal.tsx +++ b/src/components/Search/Modal.tsx @@ -1,6 +1,7 @@ +import { useDebouncedSearch } from "@/hooks/useDebouncedSearch"; import { useSearchIndex } from "@/hooks/useSearchIndex"; import { Search } from "@/types"; -import React, { useCallback, useEffect, useState } from "react"; +import React from "react"; import tw from "twin.macro"; import NoResults from "./NoResults"; import SearchResults from "./Results"; @@ -8,43 +9,21 @@ import SearchResults from "./Results"; const SearchModal: React.FC<{ closeModal: () => void; }> = ({ closeModal }) => { - const [query, setQuery] = useState(""); - const [response, setResponse] = useState( - null, - ); - - const index = useSearchIndex( + const index = useSearchIndex( process.env.NEXT_PUBLIC_MEILISEARCH_HOST ?? "", process.env.NEXT_PUBLIC_MEILISEARCH_READ_API_KEY ?? "", process.env.NEXT_PUBLIC_MEILISEARCH_INDEX_NAME ?? "", ); - const fetchResponse = useCallback(async () => { - if (query === "") { - return; - } - try { - const response = await index.search( - query, - { - limit: 10, - attributesToHighlight: ["*"], - highlightPreTag: "", - highlightPostTag: "", - }, - ); - setResponse(response.hits); - } catch (e) { - console.error(`Search for query "${query}" failed (${e})`); - } - }, [query]); - - useEffect(() => { - if (query === "") { - return; - } - fetchResponse(); - }, [query]); + const { query, setQuery, response } = useDebouncedSearch( + index, + { + limit: 10, + attributesToHighlight: ["*"], + highlightPreTag: "", + highlightPostTag: "", + }, + ); return (
{response && - (response.length === 0 ? ( + (response.hits.length === 0 ? ( ) : ( diff --git a/src/components/Search/NoResults.tsx b/src/components/Search/NoResults.tsx index b3b763572..f0dae9cb6 100644 --- a/src/components/Search/NoResults.tsx +++ b/src/components/Search/NoResults.tsx @@ -6,10 +6,9 @@ import tw, { styled } from "twin.macro"; const ContactButton = styled(Link)` ${[ - tw`flex flex-row items-center gap-2`, + tw`flex flex-row items-center gap-2 p-2`, tw`border border-solid rounded-lg`, tw`hover:bg-pink-100`, - tw`p-2`, tw`[&>svg]:w-8`, tw`[&>svg]:h-8`, ]} diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx index fb3d6893d..2dd228d1c 100644 --- a/src/components/Search/Results.tsx +++ b/src/components/Search/Results.tsx @@ -1,3 +1,4 @@ +import { SearchResponse } from "meilisearch"; import { Link } from "@/components/Link"; import { Search } from "@/types"; import { Markup } from "interweave"; @@ -5,13 +6,13 @@ import React from "react"; import tw from "twin.macro"; const SearchResults: React.FC<{ - response: Search.MeilisearchResponse; + response: SearchResponse; }> = ({ response }) => { - const chapters = Array.from(new Set(response.map(r => r.hierarchy_lvl0))); - + const { hits } = response; + const chapters = Array.from(new Set(hits.map(r => r.hierarchy_lvl0))); const results = chapters.reduce((acc, curr) => { acc[curr] = [ - ...response + ...hits .filter(r => r.hierarchy_lvl0 === curr) .map(hit => ({ hierarchies: [ @@ -31,7 +32,7 @@ const SearchResults: React.FC<{ hit.hierarchy_lvl4, ].filter(h => h !== null), slug: hit.url, - text: hit._formatted.content, + text: (hit._formatted && hit._formatted.content) ?? "", })), ]; return acc; diff --git a/src/hooks/useDebouncedSearch.ts b/src/hooks/useDebouncedSearch.ts new file mode 100644 index 000000000..fcdfbc3e5 --- /dev/null +++ b/src/hooks/useDebouncedSearch.ts @@ -0,0 +1,45 @@ +import { Index, SearchParams, SearchResponse } from "meilisearch"; +import { useCallback, useEffect, useState } from "react"; + +export const useDebouncedSearch = >( + index: Index, + params: SearchParams, + debounceMs: number = 500, +) => { + // Controlled input for rendering + const [rawInput, setRawInput] = useState(""); + // Actual query from input that gets sent in search requests + const [query, setQuery] = useState(""); + + const [response, setResponse] = useState | null>(null); + + useEffect(() => { + const debouncedFn = setTimeout(() => setQuery(rawInput), debounceMs); + return () => clearTimeout(debouncedFn); + }, [rawInput, debounceMs]); + + const search = useCallback(async () => { + if (query === "") { + return; + } + try { + const response = await index.search(query, params); + setResponse(response); + } catch (e) { + console.error(`Search for query "${query}" failed (${e})`); + } + }, [query]); + + useEffect(() => { + if (query === "") { + return; + } + search(); + }, [query, search]); + + return { + query: rawInput, + setQuery: setRawInput, + response, + }; +}; diff --git a/src/hooks/useSearchIndex.ts b/src/hooks/useSearchIndex.ts index d63ac3cce..7ae7f473d 100644 --- a/src/hooks/useSearchIndex.ts +++ b/src/hooks/useSearchIndex.ts @@ -1,11 +1,11 @@ import { Index, MeiliSearch } from "meilisearch"; import { useMemo } from "react"; -export const useSearchIndex = ( +export const useSearchIndex = >( host: string, apiKey: string, indexName: string, -): Index => { +): Index => { if (host === "") { console.error(`useSearchIndex.host not provided`); } @@ -20,7 +20,6 @@ export const useSearchIndex = ( host, apiKey, }); - const index = meilisearch.index(indexName); - return index; + return meilisearch.index(indexName); }, [apiKey, host, indexName]); }; diff --git a/src/types.ts b/src/types.ts index 64c4f4ade..a28fbb3f1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import { Hits, Index as MsIndex } from 'meilisearch' + export interface FrontMatter { title: string; } @@ -23,9 +25,9 @@ export namespace Search { text: string; } export type Result = Record; - export type MeilisearchResponse = MeilisearchResponseItem[]; + // export type MeilisearchResponse = Hits; - interface _MeilisearchResponseItem { + export interface Response { hierarchy_lvl0: string; hierarchy_lvl1: string; hierarchy_lvl2: string; @@ -34,7 +36,4 @@ export namespace Search { url: string; content: string; } - export interface MeilisearchResponseItem extends _MeilisearchResponseItem { - _formatted: _MeilisearchResponseItem; - } } From 0583ee8a848904c347a062b1ba276e6a1b5ef431 Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Mon, 24 Apr 2023 18:24:21 +0800 Subject: [PATCH 05/18] Refactor: clean up, combine useSearchIndex --- src/components/Search/Modal.tsx | 7 +---- src/components/Search/Results.tsx | 12 +++++++-- src/hooks/useDebouncedSearch.ts | 44 ++++++++++++++++++++++++------- src/hooks/useSearchIndex.ts | 25 ------------------ src/types.ts | 10 +------ 5 files changed, 46 insertions(+), 52 deletions(-) delete mode 100644 src/hooks/useSearchIndex.ts diff --git a/src/components/Search/Modal.tsx b/src/components/Search/Modal.tsx index be2bcc7d9..b68962116 100644 --- a/src/components/Search/Modal.tsx +++ b/src/components/Search/Modal.tsx @@ -1,5 +1,4 @@ import { useDebouncedSearch } from "@/hooks/useDebouncedSearch"; -import { useSearchIndex } from "@/hooks/useSearchIndex"; import { Search } from "@/types"; import React from "react"; import tw from "twin.macro"; @@ -9,14 +8,10 @@ import SearchResults from "./Results"; const SearchModal: React.FC<{ closeModal: () => void; }> = ({ closeModal }) => { - const index = useSearchIndex( + const { query, setQuery, response } = useDebouncedSearch( process.env.NEXT_PUBLIC_MEILISEARCH_HOST ?? "", process.env.NEXT_PUBLIC_MEILISEARCH_READ_API_KEY ?? "", process.env.NEXT_PUBLIC_MEILISEARCH_INDEX_NAME ?? "", - ); - - const { query, setQuery, response } = useDebouncedSearch( - index, { limit: 10, attributesToHighlight: ["*"], diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx index 2dd228d1c..9d4c4c7b6 100644 --- a/src/components/Search/Results.tsx +++ b/src/components/Search/Results.tsx @@ -5,8 +5,16 @@ import { Markup } from "interweave"; import React from "react"; import tw from "twin.macro"; +interface ResultItem { + hierarchies: string[]; + slug: string; + text: string; +} + +type Result = Record; + const SearchResults: React.FC<{ - response: SearchResponse; + response: SearchResponse; }> = ({ response }) => { const { hits } = response; const chapters = Array.from(new Set(hits.map(r => r.hierarchy_lvl0))); @@ -36,7 +44,7 @@ const SearchResults: React.FC<{ })), ]; return acc; - }, {} as Search.Result); + }, {} as Result); const cleanSlug = (slug: string) => slug.replace("#__next", ""); diff --git a/src/hooks/useDebouncedSearch.ts b/src/hooks/useDebouncedSearch.ts index fcdfbc3e5..e339dfb82 100644 --- a/src/hooks/useDebouncedSearch.ts +++ b/src/hooks/useDebouncedSearch.ts @@ -1,35 +1,53 @@ -import { Index, SearchParams, SearchResponse } from "meilisearch"; -import { useCallback, useEffect, useState } from "react"; +import { MeiliSearch, SearchParams, SearchResponse } from "meilisearch"; +import { useCallback, useEffect, useMemo, useState } from "react"; -export const useDebouncedSearch = >( - index: Index, +export const useDebouncedSearch = >( + host: string, + apiKey: string, + indexName: string, params: SearchParams, debounceMs: number = 500, ) => { + if (host === "") { + console.error(`useDebouncedSearch.host is missing`); + } + if (apiKey === "") { + console.error(`useDebouncedSearch.apiKey is missing`); + } + if (indexName === "") { + console.error(`useDebouncedSearch.indexName is missing`); + } + // Controlled input for rendering const [rawInput, setRawInput] = useState(""); // Actual query from input that gets sent in search requests const [query, setQuery] = useState(""); - const [response, setResponse] = useState | null>(null); + const [response, setResponse] = useState | null>(null); - useEffect(() => { - const debouncedFn = setTimeout(() => setQuery(rawInput), debounceMs); - return () => clearTimeout(debouncedFn); - }, [rawInput, debounceMs]); + // Get index + const index = useMemo(() => { + const meilisearch = new MeiliSearch({ + host, + apiKey, + }); + return meilisearch.index(indexName); + }, [host, apiKey, indexName]); + // Get search response const search = useCallback(async () => { if (query === "") { return; } try { - const response = await index.search(query, params); + const response = await index.search(query, params); setResponse(response); } catch (e) { console.error(`Search for query "${query}" failed (${e})`); } }, [query]); + // Perform search useEffect(() => { if (query === "") { return; @@ -37,6 +55,12 @@ export const useDebouncedSearch = >( search(); }, [query, search]); + // Debounce query input + useEffect(() => { + const debouncedFn = setTimeout(() => setQuery(rawInput), debounceMs); + return () => clearTimeout(debouncedFn); + }, [rawInput, debounceMs]); + return { query: rawInput, setQuery: setRawInput, diff --git a/src/hooks/useSearchIndex.ts b/src/hooks/useSearchIndex.ts deleted file mode 100644 index 7ae7f473d..000000000 --- a/src/hooks/useSearchIndex.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Index, MeiliSearch } from "meilisearch"; -import { useMemo } from "react"; - -export const useSearchIndex = >( - host: string, - apiKey: string, - indexName: string, -): Index => { - if (host === "") { - console.error(`useSearchIndex.host not provided`); - } - if (apiKey === "") { - console.error(`useSearchIndex.apiKey not provided`); - } - if (indexName === "") { - console.error(`useSearchIndex.indexName not provided`); - } - return useMemo(() => { - const meilisearch = new MeiliSearch({ - host, - apiKey, - }); - return meilisearch.index(indexName); - }, [apiKey, host, indexName]); -}; diff --git a/src/types.ts b/src/types.ts index a28fbb3f1..daf684407 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,15 +19,7 @@ export interface ISidebarSection { } export namespace Search { - export interface ResultItem { - hierarchies: string[]; - slug: string; - text: string; - } - export type Result = Record; - // export type MeilisearchResponse = Hits; - - export interface Response { + export interface Document { hierarchy_lvl0: string; hierarchy_lvl1: string; hierarchy_lvl2: string; From eec4afcb5348df99af46b232798f7311bfda652a Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Mon, 24 Apr 2023 18:44:20 +0800 Subject: [PATCH 06/18] Refactor: Separate QueryInput, add ability to clear results --- src/components/Search/Modal.tsx | 44 ++++++++++++++-------------- src/components/Search/QueryInput.tsx | 42 ++++++++++++++++++++++++++ src/hooks/useDebouncedSearch.ts | 5 ++++ 3 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 src/components/Search/QueryInput.tsx diff --git a/src/components/Search/Modal.tsx b/src/components/Search/Modal.tsx index b68962116..a8e018570 100644 --- a/src/components/Search/Modal.tsx +++ b/src/components/Search/Modal.tsx @@ -1,24 +1,27 @@ import { useDebouncedSearch } from "@/hooks/useDebouncedSearch"; +import { Search as SearchIcon } from "react-feather"; import { Search } from "@/types"; import React from "react"; import tw from "twin.macro"; import NoResults from "./NoResults"; import SearchResults from "./Results"; +import QueryInput from "./QueryInput"; const SearchModal: React.FC<{ closeModal: () => void; }> = ({ closeModal }) => { - const { query, setQuery, response } = useDebouncedSearch( - process.env.NEXT_PUBLIC_MEILISEARCH_HOST ?? "", - process.env.NEXT_PUBLIC_MEILISEARCH_READ_API_KEY ?? "", - process.env.NEXT_PUBLIC_MEILISEARCH_INDEX_NAME ?? "", - { - limit: 10, - attributesToHighlight: ["*"], - highlightPreTag: "", - highlightPostTag: "", - }, - ); + const { clearResponse, query, setQuery, response } = + useDebouncedSearch( + process.env.NEXT_PUBLIC_MEILISEARCH_HOST ?? "", + process.env.NEXT_PUBLIC_MEILISEARCH_READ_API_KEY ?? "", + process.env.NEXT_PUBLIC_MEILISEARCH_INDEX_NAME ?? "", + { + limit: 10, + attributesToHighlight: ["*"], + highlightPreTag: "", + highlightPostTag: "", + }, + ); return (
e.stopPropagation()} css={[tw`bg-background border rounded-lg w-full md:w-1/2 mx-auto`]} > - setQuery(e.target.value)} - autoFocus - css={[ - tw`px-4 py-4 w-full bg-transparent focus:outline-none`, - tw`border-b-2 border-gray-200 border-dotted`, - ]} - /> -
+
+ +
+
{response && (response.hits.length === 0 ? ( diff --git a/src/components/Search/QueryInput.tsx b/src/components/Search/QueryInput.tsx new file mode 100644 index 000000000..e3903b4e0 --- /dev/null +++ b/src/components/Search/QueryInput.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { Search as SearchIcon } from "react-feather"; +import tw from "twin.macro"; + +interface Props { + clearResponse: () => void; + query: string; + setQuery: (q: string) => void; +} + +const QueryInput: React.FC = ({ clearResponse, query, setQuery }) => { + return ( +
+
+ + + + setQuery(e.target.value)} + /> + + + +
+
+ ); +}; + +export default QueryInput; diff --git a/src/hooks/useDebouncedSearch.ts b/src/hooks/useDebouncedSearch.ts index e339dfb82..720753fd1 100644 --- a/src/hooks/useDebouncedSearch.ts +++ b/src/hooks/useDebouncedSearch.ts @@ -63,6 +63,11 @@ export const useDebouncedSearch = >( return { query: rawInput, + clearResponse: () => { + setQuery(""); + setRawInput(""); + setResponse(null); + }, setQuery: setRawInput, response, }; From 0eb551571572f3534cbc0ffe5e12b9a781b054d5 Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Mon, 24 Apr 2023 18:50:00 +0800 Subject: [PATCH 07/18] Cleanup --- src/components/Search/Modal.tsx | 9 ++++----- src/components/Search/OpenModalButton.tsx | 4 ++-- src/components/Search/Results.tsx | 6 ++++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/components/Search/Modal.tsx b/src/components/Search/Modal.tsx index a8e018570..752466637 100644 --- a/src/components/Search/Modal.tsx +++ b/src/components/Search/Modal.tsx @@ -1,13 +1,12 @@ import { useDebouncedSearch } from "@/hooks/useDebouncedSearch"; -import { Search as SearchIcon } from "react-feather"; import { Search } from "@/types"; import React from "react"; import tw from "twin.macro"; import NoResults from "./NoResults"; -import SearchResults from "./Results"; import QueryInput from "./QueryInput"; +import Results from "./Results"; -const SearchModal: React.FC<{ +const Modal: React.FC<{ closeModal: () => void; }> = ({ closeModal }) => { const { clearResponse, query, setQuery, response } = @@ -46,7 +45,7 @@ const SearchModal: React.FC<{ (response.hits.length === 0 ? ( ) : ( - + ))}
@@ -54,4 +53,4 @@ const SearchModal: React.FC<{ ); }; -export default SearchModal; +export default Modal; diff --git a/src/components/Search/OpenModalButton.tsx b/src/components/Search/OpenModalButton.tsx index ddf1133e1..15d661ae8 100644 --- a/src/components/Search/OpenModalButton.tsx +++ b/src/components/Search/OpenModalButton.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Search as SearchIcon } from "react-feather"; import tw from "twin.macro"; -const OpenSearchModalButton: React.FC = () => { +const OpenModalButton: React.FC = () => { return ( <>
diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx index 8101c50cb..0446aa302 100644 --- a/src/components/Search/Results.tsx +++ b/src/components/Search/Results.tsx @@ -1,53 +1,14 @@ -import { SearchResponse } from "meilisearch"; import { Link } from "@/components/Link"; import { Search } from "@/types"; import { Markup } from "interweave"; import React from "react"; import tw from "twin.macro"; -interface ResultItem { - hierarchies: string[]; - slug: string; - text: string; -} - -type Result = Record; - interface Props { - response: SearchResponse; + results: Search.Result; } -const Results: React.FC = ({ response }) => { - const { hits } = response; - const chapters = Array.from(new Set(hits.map(r => r.hierarchy_lvl0))); - const results = chapters.reduce((acc, curr) => { - acc[curr] = [ - ...hits - .filter(r => r.hierarchy_lvl0 === curr) - .map(hit => ({ - hierarchies: [ - // `hit.hierarchy_lvl0` is intentionally ignored here; we're - // grouping the rendered output by it so it's redundant. - // In practice, this means that we'll render: - // >> "Databases" - // >> "PostgreSQL" - // >> ... - // instead of: - // >> "Databases" - // >> "Databases -> PostgreSQL" - // >> ... - hit.hierarchy_lvl1, - hit.hierarchy_lvl2, - hit.hierarchy_lvl3, - hit.hierarchy_lvl4, - ].filter(h => h !== null), - slug: hit.url, - text: (hit._formatted && hit._formatted.content) ?? "", - })), - ]; - return acc; - }, {} as Result); - +const Results: React.FC = ({ results }) => { // @FIXME: Indexer is grabbing #__next from anchor hrefs. This should be // fixed upstream, but no harm in just hacking it in place for now. const cleanSlug = (slug: string) => slug.replace("#__next", ""); diff --git a/src/hooks/useDebouncedSearch.ts b/src/hooks/useDebouncedSearch.ts index 720753fd1..f69d3f8d3 100644 --- a/src/hooks/useDebouncedSearch.ts +++ b/src/hooks/useDebouncedSearch.ts @@ -1,12 +1,73 @@ +import { Search } from "@/types"; import { MeiliSearch, SearchParams, SearchResponse } from "meilisearch"; import { useCallback, useEffect, useMemo, useState } from "react"; -export const useDebouncedSearch = >( +type Transformer = ( + src: SearchResponse, +) => Result; + +/** + * Default transformer for the document structure created by + * https://github.com/meilisearch/docs-scraper. + */ +const defaultTransformResponse: Transformer< + Search.Document, + Search.Result +> = response => { + const { hits } = response; + const chapters = Array.from(new Set(hits.map(r => r.hierarchy_lvl0))); + return chapters.reduce((acc, curr) => { + acc[curr] = [ + ...hits + .filter(r => r.hierarchy_lvl0 === curr) + .map(hit => ({ + hierarchies: [ + // `hit.hierarchy_lvl0` is intentionally ignored here; we're + // grouping the rendered output by it so it's redundant. + // In practice, this means that we'll render: + // >> "Databases" + // >> "PostgreSQL" + // >> ... + // instead of: + // >> "Databases" + // >> "Databases -> PostgreSQL" + // >> ... + hit.hierarchy_lvl1, + hit.hierarchy_lvl2, + hit.hierarchy_lvl3, + hit.hierarchy_lvl4, + ].filter(h => h !== null), + slug: hit.url, + text: (hit._formatted && hit._formatted.content) ?? "", + })), + ]; + return acc; + }, {} as Search.Result); +}; + +/** + * This hook provides functionality for searching a MeiliSearch index. It + * exposes a `query` string and `setQuery` method for controlling an + * element in a debounced manner, a `results` object containing + * the search results, and a `clearResults` method for clearing search + * results. + * + * It can also take in a `transformResponse` method for transforming the + * raw response from Meilisearch into a structure you define. + */ +export const useDebouncedSearch = < + Response extends Record, + Result, +>( host: string, apiKey: string, indexName: string, params: SearchParams, debounceMs: number = 500, + transformResponse: Transformer< + Response, + Result + > = defaultTransformResponse as Transformer, ) => { if (host === "") { console.error(`useDebouncedSearch.host is missing`); @@ -23,7 +84,7 @@ export const useDebouncedSearch = >( // Actual query from input that gets sent in search requests const [query, setQuery] = useState(""); - const [response, setResponse] = useState | null>(null); + const [results, setResults] = useState(null); // Get index const index = useMemo(() => { @@ -31,7 +92,7 @@ export const useDebouncedSearch = >( host, apiKey, }); - return meilisearch.index(indexName); + return meilisearch.index(indexName); }, [host, apiKey, indexName]); // Get search response @@ -40,8 +101,8 @@ export const useDebouncedSearch = >( return; } try { - const response = await index.search(query, params); - setResponse(response); + const response = await index.search(query, params); + setResults(transformResponse(response)); } catch (e) { console.error(`Search for query "${query}" failed (${e})`); } @@ -66,9 +127,9 @@ export const useDebouncedSearch = >( clearResponse: () => { setQuery(""); setRawInput(""); - setResponse(null); + setResults(null); }, setQuery: setRawInput, - response, + results, }; }; diff --git a/src/types.ts b/src/types.ts index 051f34afa..7b3ea0486 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,12 @@ export interface ISidebarSection { } export namespace Search { + interface ResultItem { + hierarchies: string[]; + slug: string; + text: string; + } + export type Result = Record; export interface Document { hierarchy_lvl0: string; hierarchy_lvl1: string; From c016b1501b6f4b37def36fcff62921540ce93387 Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Tue, 25 Apr 2023 04:20:46 +0800 Subject: [PATCH 16/18] Remove redundant `Array.from` --- src/components/Search/Results.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx index 0446aa302..98b470ffc 100644 --- a/src/components/Search/Results.tsx +++ b/src/components/Search/Results.tsx @@ -15,7 +15,7 @@ const Results: React.FC = ({ results }) => { return (
- {Array.from(Object.entries(results)).map(([chapter, hits]) => { + {Object.entries(results).map(([chapter, hits]) => { return (
Date: Tue, 25 Apr 2023 04:25:35 +0800 Subject: [PATCH 17/18] Remove baseUri from slug --- src/components/Search/Results.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx index 98b470ffc..eeb2dd664 100644 --- a/src/components/Search/Results.tsx +++ b/src/components/Search/Results.tsx @@ -9,9 +9,16 @@ interface Props { } const Results: React.FC = ({ results }) => { + const withoutBaseUri = (slug: string) => { + const url = new URL(slug); + const { hash, pathname } = url; + return `${pathname}${hash}`; + }; + // @FIXME: Indexer is grabbing #__next from anchor hrefs. This should be // fixed upstream, but no harm in just hacking it in place for now. - const cleanSlug = (slug: string) => slug.replace("#__next", ""); + const cleanSlug = (slug: string) => + withoutBaseUri(slug.replace("#__next", "")); return (
From 1d7d8e8a90cf683a1d580a997a4a9f08ebf66bf7 Mon Sep 17 00:00:00 2001 From: Ray Chen Date: Tue, 25 Apr 2023 04:37:20 +0800 Subject: [PATCH 18/18] fix: close modal on result click --- src/components/Search/Modal.tsx | 2 +- src/components/Search/Results.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Search/Modal.tsx b/src/components/Search/Modal.tsx index dcc7997c5..582b662e1 100644 --- a/src/components/Search/Modal.tsx +++ b/src/components/Search/Modal.tsx @@ -51,7 +51,7 @@ const Modal: React.FC = ({ closeModal }) => { (Object.keys(results).length === 0 ? ( ) : ( - + ))}
diff --git a/src/components/Search/Results.tsx b/src/components/Search/Results.tsx index eeb2dd664..720154e81 100644 --- a/src/components/Search/Results.tsx +++ b/src/components/Search/Results.tsx @@ -5,10 +5,11 @@ import React from "react"; import tw from "twin.macro"; interface Props { + closeModal: () => void results: Search.Result; } -const Results: React.FC = ({ results }) => { +const Results: React.FC = ({ closeModal, results }) => { const withoutBaseUri = (slug: string) => { const url = new URL(slug); const { hash, pathname } = url; @@ -39,6 +40,7 @@ const Results: React.FC = ({ results }) => {
  • closeModal()} css={[tw`flex flex-col font-medium hover:text-pink-700`]} > {h.hierarchies.join(" -> ")}