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_HOST=email.provider.com
SMTP_USER=email@example.com SMTP_USER=email@example.com
SMTP_PASSWORD=mypassword123 SMTP_PASSWORD=mypassword123
NEXT_PUBLIC_URL_CMS=https://url-to.strapi-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_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_SELF=https://url-to-front-accords-library.com
NEXT_PUBLIC_URL_SEARCH=https://url-to.search-accords-library.com
``` ```
Run in dev mode: Run in dev mode:

17
package-lock.json generated
View File

@ -14,6 +14,7 @@
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"graphql-request": "^4.2.0", "graphql-request": "^4.2.0",
"markdown-to-jsx": "^7.1.7", "markdown-to-jsx": "^7.1.7",
"meilisearch": "^0.25.1",
"next": "^12.1.6", "next": "^12.1.6",
"nodemailer": "^6.7.5", "nodemailer": "^6.7.5",
"react": "18.1.0", "react": "18.1.0",
@ -6638,6 +6639,14 @@
"react": ">= 0.14.0" "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": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@ -14050,6 +14059,14 @@
"integrity": "sha512-VI3TyyHlGkO8uFle0IOibzpO1c1iJDcXcS/zBrQrXQQvJ2tpdwVzVZ7XdKsyRz1NdRmre4dqQkMZzUHaKIG/1w==", "integrity": "sha512-VI3TyyHlGkO8uFle0IOibzpO1c1iJDcXcS/zBrQrXQQvJ2tpdwVzVZ7XdKsyRz1NdRmre4dqQkMZzUHaKIG/1w==",
"requires": {} "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": { "merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",

View File

@ -21,6 +21,7 @@
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"graphql-request": "^4.2.0", "graphql-request": "^4.2.0",
"markdown-to-jsx": "^7.1.7", "markdown-to-jsx": "^7.1.7",
"meilisearch": "^0.25.1",
"next": "^12.1.6", "next": "^12.1.6",
"nodemailer": "^6.7.5", "nodemailer": "^6.7.5",
"react": "18.1.0", "react": "18.1.0",

View File

@ -4,6 +4,13 @@ import { UploadImageFragment } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyLanguage, prettySlug } from "helpers/formatters"; import { prettyLanguage, prettySlug } from "helpers/formatters";
import { getOgImage, ImageQuality, OgImage } from "helpers/img"; import { getOgImage, ImageQuality, OgImage } from "helpers/img";
import {
getClient,
getIndexes,
Indexes,
search,
SearchResult,
} from "helpers/search";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import { AnchorIds } from "hooks/useScrollTopOnChange"; import { AnchorIds } from "hooks/useScrollTopOnChange";
@ -15,6 +22,7 @@ import { OrderableList } from "./Inputs/OrderableList";
import { Select } from "./Inputs/Select"; import { Select } from "./Inputs/Select";
import { MainPanel } from "./Panels/MainPanel"; import { MainPanel } from "./Panels/MainPanel";
import { Popup } from "./Popup"; import { Popup } from "./Popup";
import { PreviewCard } from "./PreviewCard";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
subPanel?: React.ReactNode; subPanel?: React.ReactNode;
@ -43,6 +51,9 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
const isMobile = useMediaMobile(); const isMobile = useMediaMobile();
const appLayout = useAppLayout(); const appLayout = useAppLayout();
const [searchQuery, setSearchQuery] = useState("");
const [searchResult, setSearchResult] = useState<SearchResult>();
const sensibilitySwipe = 1.1; const sensibilitySwipe = 1.1;
useMemo(() => { 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 turnSubIntoContent = subPanel && !contentPanel;
const titlePrefix = "Accords Library"; const titlePrefix = "Accords Library";
@ -496,6 +521,46 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
</div> </div>
</div> </div>
</Popup> </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>
</div> </div>
); );

View File

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

View File

@ -6,6 +6,7 @@ import React, { ReactNode, useContext, useState } from "react";
interface AppLayoutState { interface AppLayoutState {
subPanelOpen: boolean | undefined; subPanelOpen: boolean | undefined;
configPanelOpen: boolean | undefined; configPanelOpen: boolean | undefined;
searchPanelOpen: boolean | undefined;
mainPanelReduced: boolean | undefined; mainPanelReduced: boolean | undefined;
mainPanelOpen: boolean | undefined; mainPanelOpen: boolean | undefined;
darkMode: boolean | undefined; darkMode: boolean | undefined;
@ -18,6 +19,7 @@ interface AppLayoutState {
menuGestures: boolean; menuGestures: boolean;
setSubPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>; setSubPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>; setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setSearchPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setMainPanelReduced: React.Dispatch< setMainPanelReduced: React.Dispatch<
React.SetStateAction<boolean | undefined> React.SetStateAction<boolean | undefined>
>; >;
@ -40,6 +42,7 @@ interface AppLayoutState {
const initialState: AppLayoutState = { const initialState: AppLayoutState = {
subPanelOpen: false, subPanelOpen: false,
configPanelOpen: false, configPanelOpen: false,
searchPanelOpen: false,
mainPanelReduced: false, mainPanelReduced: false,
mainPanelOpen: false, mainPanelOpen: false,
darkMode: false, darkMode: false,
@ -56,6 +59,7 @@ const initialState: AppLayoutState = {
setDarkMode: () => {}, setDarkMode: () => {},
setSelectedThemeMode: () => {}, setSelectedThemeMode: () => {},
setConfigPanelOpen: () => {}, setConfigPanelOpen: () => {},
setSearchPanelOpen: () => {},
setFontSize: () => {}, setFontSize: () => {},
setDyslexic: () => {}, setDyslexic: () => {},
setCurrency: () => {}, setCurrency: () => {},
@ -122,11 +126,16 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
const [menuGestures, setMenuGestures] = useState(false); const [menuGestures, setMenuGestures] = useState(false);
const [searchPanelOpen, setSearchPanelOpen] = useStateWithLocalStorage<
boolean | undefined
>("mainPanelOpen", initialState.mainPanelOpen);
return ( return (
<AppContext.Provider <AppContext.Provider
value={{ value={{
subPanelOpen, subPanelOpen,
configPanelOpen, configPanelOpen,
searchPanelOpen,
mainPanelReduced, mainPanelReduced,
mainPanelOpen, mainPanelOpen,
darkMode, darkMode,
@ -139,6 +148,7 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
menuGestures, menuGestures,
setSubPanelOpen, setSubPanelOpen,
setConfigPanelOpen, setConfigPanelOpen,
setSearchPanelOpen,
setMainPanelReduced, setMainPanelReduced,
setMainPanelOpen, setMainPanelOpen,
setDarkMode, 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,
};
}