From a7c5ca61fd7d4b154e3ba72f4dec35e03d7a2cab Mon Sep 17 00:00:00 2001 From: DrMint Date: Sun, 22 May 2022 14:43:36 +0200 Subject: [PATCH] Added basic search --- README.md | 6 ++- package-lock.json | 17 +++++++ package.json | 1 + src/components/AppLayout.tsx | 65 +++++++++++++++++++++++++ src/components/Panels/MainPanel.tsx | 20 +++----- src/contexts/AppLayoutContext.tsx | 10 ++++ src/helpers/search.ts | 74 +++++++++++++++++++++++++++++ 7 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 src/helpers/search.ts diff --git a/README.md b/README.md index 1d182aa..b554898 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,11 @@ REVALIDATION_TOKEN=abcdef0123456789 SMTP_HOST=email.provider.com SMTP_USER=email@example.com SMTP_PASSWORD=mypassword123 -NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com/ -NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com/ +NEXT_PUBLIC_URL_CMS=https://url-to.strapi-accords-library.com +NEXT_PUBLIC_URL_IMG=https://url-to.img-accords-library.com +NEXT_PUBLIC_URL_WATCH=https://url-to.watch-accords-library.com NEXT_PUBLIC_URL_SELF=https://url-to-front-accords-library.com +NEXT_PUBLIC_URL_SEARCH=https://url-to.search-accords-library.com ``` Run in dev mode: diff --git a/package-lock.json b/package-lock.json index f476b15..d98bd72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "autoprefixer": "^10.4.7", "graphql-request": "^4.2.0", "markdown-to-jsx": "^7.1.7", + "meilisearch": "^0.25.1", "next": "^12.1.6", "nodemailer": "^6.7.5", "react": "18.1.0", @@ -6638,6 +6639,14 @@ "react": ">= 0.14.0" } }, + "node_modules/meilisearch": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.25.1.tgz", + "integrity": "sha512-20jO0pK9BhghxHSkOLbdoYn58h/Z0PNL3JQcRq7ipNIeqrxkAetCZZ6ttJC3uxcz0jVglmiFoSXu3Z/lEOLOLQ==", + "dependencies": { + "cross-fetch": "^3.1.5" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -14050,6 +14059,14 @@ "integrity": "sha512-VI3TyyHlGkO8uFle0IOibzpO1c1iJDcXcS/zBrQrXQQvJ2tpdwVzVZ7XdKsyRz1NdRmre4dqQkMZzUHaKIG/1w==", "requires": {} }, + "meilisearch": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.25.1.tgz", + "integrity": "sha512-20jO0pK9BhghxHSkOLbdoYn58h/Z0PNL3JQcRq7ipNIeqrxkAetCZZ6ttJC3uxcz0jVglmiFoSXu3Z/lEOLOLQ==", + "requires": { + "cross-fetch": "^3.1.5" + } + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", diff --git a/package.json b/package.json index c2347d7..664d7b1 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "autoprefixer": "^10.4.7", "graphql-request": "^4.2.0", "markdown-to-jsx": "^7.1.7", + "meilisearch": "^0.25.1", "next": "^12.1.6", "nodemailer": "^6.7.5", "react": "18.1.0", diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 1dcaee8..315e5f2 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -4,6 +4,13 @@ import { UploadImageFragment } from "graphql/generated"; import { AppStaticProps } from "graphql/getAppStaticProps"; import { prettyLanguage, prettySlug } from "helpers/formatters"; import { getOgImage, ImageQuality, OgImage } from "helpers/img"; +import { + getClient, + getIndexes, + Indexes, + search, + SearchResult, +} from "helpers/search"; import { Immutable } from "helpers/types"; import { useMediaMobile } from "hooks/useMediaQuery"; import { AnchorIds } from "hooks/useScrollTopOnChange"; @@ -15,6 +22,7 @@ import { OrderableList } from "./Inputs/OrderableList"; import { Select } from "./Inputs/Select"; import { MainPanel } from "./Panels/MainPanel"; import { Popup } from "./Popup"; +import { PreviewCard } from "./PreviewCard"; interface Props extends AppStaticProps { subPanel?: React.ReactNode; @@ -43,6 +51,9 @@ export function AppLayout(props: Immutable): JSX.Element { const isMobile = useMediaMobile(); const appLayout = useAppLayout(); + const [searchQuery, setSearchQuery] = useState(""); + const [searchResult, setSearchResult] = useState(); + const sensibilitySwipe = 1.1; useMemo(() => { @@ -81,6 +92,20 @@ export function AppLayout(props: Immutable): JSX.Element { }, }); + const client = getClient(); + + useEffect(() => { + if (searchQuery.length > 1) { + search(client, Indexes.Post, searchQuery).then((result) => { + setSearchResult(result); + }); + } else { + setSearchResult(undefined); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + const turnSubIntoContent = subPanel && !contentPanel; const titlePrefix = "Accord’s Library"; @@ -496,6 +521,46 @@ export function AppLayout(props: Immutable): JSX.Element { + + +
+ {/* TODO: add to langui */} +

{"Search"}

+ { + const input = event.target as HTMLInputElement; + setSearchQuery(input.value); + }} + /> +
+ {/* TODO: add to langui */} +
+

In news:

+
+ {searchResult?.hits.map((hit) => ( + + ))} +
+
+
); diff --git a/src/components/Panels/MainPanel.tsx b/src/components/Panels/MainPanel.tsx index 250037e..1c76265 100644 --- a/src/components/Panels/MainPanel.tsx +++ b/src/components/Panels/MainPanel.tsx @@ -79,28 +79,20 @@ export function MainPanel(props: Immutable): JSX.Element { - {/* {langui.open_search}} placement="right" className="text-left" disabled={!appLayout.mainPanelReduced} > - */} + diff --git a/src/contexts/AppLayoutContext.tsx b/src/contexts/AppLayoutContext.tsx index f9bc26e..e46a84c 100644 --- a/src/contexts/AppLayoutContext.tsx +++ b/src/contexts/AppLayoutContext.tsx @@ -6,6 +6,7 @@ import React, { ReactNode, useContext, useState } from "react"; interface AppLayoutState { subPanelOpen: boolean | undefined; configPanelOpen: boolean | undefined; + searchPanelOpen: boolean | undefined; mainPanelReduced: boolean | undefined; mainPanelOpen: boolean | undefined; darkMode: boolean | undefined; @@ -18,6 +19,7 @@ interface AppLayoutState { menuGestures: boolean; setSubPanelOpen: React.Dispatch>; setConfigPanelOpen: React.Dispatch>; + setSearchPanelOpen: React.Dispatch>; setMainPanelReduced: React.Dispatch< React.SetStateAction >; @@ -40,6 +42,7 @@ interface AppLayoutState { const initialState: AppLayoutState = { subPanelOpen: false, configPanelOpen: false, + searchPanelOpen: false, mainPanelReduced: false, mainPanelOpen: false, darkMode: false, @@ -56,6 +59,7 @@ const initialState: AppLayoutState = { setDarkMode: () => {}, setSelectedThemeMode: () => {}, setConfigPanelOpen: () => {}, + setSearchPanelOpen: () => {}, setFontSize: () => {}, setDyslexic: () => {}, setCurrency: () => {}, @@ -122,11 +126,16 @@ export function AppContextProvider(props: Immutable): JSX.Element { const [menuGestures, setMenuGestures] = useState(false); + const [searchPanelOpen, setSearchPanelOpen] = useStateWithLocalStorage< + boolean | undefined + >("mainPanelOpen", initialState.mainPanelOpen); + return ( ): JSX.Element { menuGestures, setSubPanelOpen, setConfigPanelOpen, + setSearchPanelOpen, setMainPanelReduced, setMainPanelOpen, setDarkMode, diff --git a/src/helpers/search.ts b/src/helpers/search.ts new file mode 100644 index 0000000..40f6a05 --- /dev/null +++ b/src/helpers/search.ts @@ -0,0 +1,74 @@ +import { UploadImageFragment } from "graphql/generated"; +import { MeiliSearch, SearchResponse } from "meilisearch"; +import { prettySlug } from "./formatters"; + +export enum Indexes { + ChronologyEra = "chronology-era", + ChronologyItem = "chronology-item", + Content = "content", + GlossaryItem = "glossary-item", + LibraryItem = "library-item", + MerchItem = "merch-item", + Post = "post", + Video = "video", + VideoChannel = "video-channel", + WeaponStory = "weapon-story", +} + +export function getClient() { + return new MeiliSearch({ + host: process.env.NEXT_PUBLIC_URL_SEARCH ?? "", + apiKey: "", + }); +} + +export async function getIndexes(client: MeiliSearch) { + return await client.getIndexes(); +} + +export async function search( + client: MeiliSearch, + indexName: Indexes, + query: string +) { + const index = await client.getIndex(indexName); + const results = await index.search(query); + return processSearchResults(results, indexName); +} + +export type SearchResult = { + hits: { + id: string; + href: string; + title: string; + thumbnail?: UploadImageFragment; + }[]; + indexName: string; +}; + +export function processSearchResults( + result: SearchResponse>, + indexName: Indexes +): SearchResult { + return { + hits: result.hits.map((hit) => { + switch (indexName) { + case Indexes.Post: { + return { + id: hit.id, + title: + hit.translations.length > 0 + ? hit.translations[0].title + : prettySlug(hit.slug), + href: `/news/${hit.slug}`, + thumbnail: hit.thumbnail, + }; + } + default: { + return { id: hit.id, title: prettySlug(hit.slug), href: "error" }; + } + } + }), + indexName: indexName, + }; +}