Added basic search

This commit is contained in:
DrMint 2022-05-22 14:43:36 +02:00
parent 88b60077df
commit a7c5ca61fd
7 changed files with 177 additions and 16 deletions

View File

@ -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:

17
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<Props>): JSX.Element {
const isMobile = useMediaMobile();
const appLayout = useAppLayout();
const [searchQuery, setSearchQuery] = useState("");
const [searchResult, setSearchResult] = useState<SearchResult>();
const sensibilitySwipe = 1.1;
useMemo(() => {
@ -81,6 +92,20 @@ export function AppLayout(props: Immutable<Props>): 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 = "Accords Library";
@ -496,6 +521,46 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
</div>
</div>
</Popup>
<Popup
state={appLayout.searchPanelOpen}
setState={appLayout.setSearchPanelOpen}
>
<div className="grid place-items-center gap-2">
{/* TODO: add to langui */}
<h2 className="text-2xl">{"Search"}</h2>
<input
className="mb-6 w-full"
type="text"
name="name"
id="name"
placeholder={"Search query..."}
onChange={(event) => {
const input = event.target as HTMLInputElement;
setSearchQuery(input.value);
}}
/>
</div>
{/* TODO: add to langui */}
<div className="grid gap-4">
<p className="font-headers text-xl">In news:</p>
<div
className="grid grid-cols-2 items-end gap-8
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4"
>
{searchResult?.hits.map((hit) => (
<PreviewCard
key={hit.id}
href={hit.href}
title={hit.title}
thumbnailAspectRatio={"3/2"}
thumbnail={hit.thumbnail}
keepInfoVisible
/>
))}
</div>
</div>
</Popup>
</div>
</div>
);

View File

@ -79,28 +79,20 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
</Button>
</ToolTip>
{/* <ToolTip
<ToolTip
content={<h3 className="text-2xl">{langui.open_search}</h3>}
placement="right"
className="text-left"
disabled={!appLayout.mainPanelReduced}
>
<Button
className={
appLayout.mainPanelReduced && isDesktop
? ""
: "!py-0.5 !px-2.5"
}
onClick={() => {
appLayout.setSearchPanelOpen(true);
}}
>
<span
className={`material-icons ${
!(appLayout.mainPanelReduced && isDesktop) && "!text-sm"
} `}
>
search
</span>
<span className={"material-icons"}>search</span>
</Button>
</ToolTip> */}
</ToolTip>
</div>
</div>
</div>

View File

@ -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<React.SetStateAction<boolean | undefined>>;
setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setSearchPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setMainPanelReduced: React.Dispatch<
React.SetStateAction<boolean | undefined>
>;
@ -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<Props>): JSX.Element {
const [menuGestures, setMenuGestures] = useState(false);
const [searchPanelOpen, setSearchPanelOpen] = useStateWithLocalStorage<
boolean | undefined
>("mainPanelOpen", initialState.mainPanelOpen);
return (
<AppContext.Provider
value={{
subPanelOpen,
configPanelOpen,
searchPanelOpen,
mainPanelReduced,
mainPanelOpen,
darkMode,
@ -139,6 +148,7 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
menuGestures,
setSubPanelOpen,
setConfigPanelOpen,
setSearchPanelOpen,
setMainPanelReduced,
setMainPanelOpen,
setDarkMode,

74
src/helpers/search.ts Normal file
View File

@ -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<Record<string, any>>,
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,
};
}