Added basic search
This commit is contained in:
parent
88b60077df
commit
a7c5ca61fd
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 = "Accord’s 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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue