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_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:
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 = "Accord’s Library";
|
const titlePrefix = "Accord’s 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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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