Updated meilisearch

This commit is contained in:
DrMint 2023-04-09 09:59:43 +02:00
parent 0f735c62cc
commit 5be25c656f
16 changed files with 282 additions and 274 deletions

View File

@ -46,7 +46,7 @@ module.exports = {
"func-style": ["warn", "expression"], "func-style": ["warn", "expression"],
"grouped-accessor-pairs": "warn", "grouped-accessor-pairs": "warn",
"guard-for-in": "warn", "guard-for-in": "warn",
"id-denylist": ["error", "data", "err", "e", "cb", "callback", "i"], "id-denylist": ["error", "err", "e", "cb", "callback", "i"],
// "id-length": "warn", // "id-length": "warn",
"id-match": "warn", "id-match": "warn",
"max-classes-per-file": ["error", 1], "max-classes-per-file": ["error", 1],

28
package-lock.json generated
View File

@ -20,7 +20,7 @@
"markdown-to-jsx": "^7.2.0", "markdown-to-jsx": "^7.2.0",
"marked": "^4.3.0", "marked": "^4.3.0",
"material-symbols": "^0.5.5", "material-symbols": "^0.5.5",
"meilisearch": "^0.31.1", "meilisearch": "^0.32.3",
"next": "^13.3.0", "next": "^13.3.0",
"nodemailer": "^6.9.1", "nodemailer": "^6.9.1",
"rc-slider": "^10.1.1", "rc-slider": "^10.1.1",
@ -60,7 +60,7 @@
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"graphql-request": "5.1.0", "graphql-request": "5.1.0",
"next-sitemap": "^4.0.6", "next-sitemap": "^4.0.7",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.7", "prettier-plugin-tailwindcss": "^0.2.7",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",
@ -8021,9 +8021,9 @@
"integrity": "sha512-NFUsjEVBNZvcRRqslY0RWnmlGgjhJkpDQkQs42o52gT2AmIbaP6V7wTRgyTkLAoD5VtpgpIx9eoOAXcH2ynwkg==" "integrity": "sha512-NFUsjEVBNZvcRRqslY0RWnmlGgjhJkpDQkQs42o52gT2AmIbaP6V7wTRgyTkLAoD5VtpgpIx9eoOAXcH2ynwkg=="
}, },
"node_modules/meilisearch": { "node_modules/meilisearch": {
"version": "0.31.1", "version": "0.32.3",
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.31.1.tgz", "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.32.3.tgz",
"integrity": "sha512-ajMieU0e25lLkT+05J0snX0Ycow1UofxIy5sag03flERUbjXq8ouVwkrJkW27JsKftIeDeffRRRr89LasU9+0w==", "integrity": "sha512-EOgfBuRE5SiIPIpEDYe2HO0D7a4z5bexIgaAdJFma/dH5hx1kwO+u/qb2g3qKyjG+iA3l8MlmTj/Xd72uahaAw==",
"dependencies": { "dependencies": {
"cross-fetch": "^3.1.5" "cross-fetch": "^3.1.5"
} }
@ -8208,9 +8208,9 @@
} }
}, },
"node_modules/next-sitemap": { "node_modules/next-sitemap": {
"version": "4.0.6", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.0.6.tgz", "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.0.7.tgz",
"integrity": "sha512-pZ9tynYe6mRR189qZqcOlWVgM1Gxo07BJQW0AjerKmLwQOt+6FQMdaDgifgCt6jDT3Y3EG/+NUDDZRcd0gbPkA==", "integrity": "sha512-S2g5IwJeO0+ecmFq981fb+Mw9YWmntOuN/qTCxclSkUibOJ8qKIOye0vn6NEJ1S4tKhbY+MTYKgJpNdFZYxLoA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -16972,9 +16972,9 @@
"integrity": "sha512-NFUsjEVBNZvcRRqslY0RWnmlGgjhJkpDQkQs42o52gT2AmIbaP6V7wTRgyTkLAoD5VtpgpIx9eoOAXcH2ynwkg==" "integrity": "sha512-NFUsjEVBNZvcRRqslY0RWnmlGgjhJkpDQkQs42o52gT2AmIbaP6V7wTRgyTkLAoD5VtpgpIx9eoOAXcH2ynwkg=="
}, },
"meilisearch": { "meilisearch": {
"version": "0.31.1", "version": "0.32.3",
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.31.1.tgz", "resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.32.3.tgz",
"integrity": "sha512-ajMieU0e25lLkT+05J0snX0Ycow1UofxIy5sag03flERUbjXq8ouVwkrJkW27JsKftIeDeffRRRr89LasU9+0w==", "integrity": "sha512-EOgfBuRE5SiIPIpEDYe2HO0D7a4z5bexIgaAdJFma/dH5hx1kwO+u/qb2g3qKyjG+iA3l8MlmTj/Xd72uahaAw==",
"requires": { "requires": {
"cross-fetch": "^3.1.5" "cross-fetch": "^3.1.5"
} }
@ -17092,9 +17092,9 @@
} }
}, },
"next-sitemap": { "next-sitemap": {
"version": "4.0.6", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.0.6.tgz", "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.0.7.tgz",
"integrity": "sha512-pZ9tynYe6mRR189qZqcOlWVgM1Gxo07BJQW0AjerKmLwQOt+6FQMdaDgifgCt6jDT3Y3EG/+NUDDZRcd0gbPkA==", "integrity": "sha512-S2g5IwJeO0+ecmFq981fb+Mw9YWmntOuN/qTCxclSkUibOJ8qKIOye0vn6NEJ1S4tKhbY+MTYKgJpNdFZYxLoA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@corex/deepmerge": "^4.0.37", "@corex/deepmerge": "^4.0.37",

View File

@ -16,7 +16,7 @@
"generate": "graphql-codegen --config graphql-codegen.config.js", "generate": "graphql-codegen --config graphql-codegen.config.js",
"tsc": "tsc", "tsc": "tsc",
"prettier": "prettier --end-of-line auto --write .", "prettier": "prettier --end-of-line auto --write .",
"update": "ncu --interactive --format group" "upgrade": "ncu --interactive --format group"
}, },
"dependencies": { "dependencies": {
"@fontsource/opendyslexic": "^4.5.4", "@fontsource/opendyslexic": "^4.5.4",
@ -33,7 +33,7 @@
"markdown-to-jsx": "^7.2.0", "markdown-to-jsx": "^7.2.0",
"marked": "^4.3.0", "marked": "^4.3.0",
"material-symbols": "^0.5.5", "material-symbols": "^0.5.5",
"meilisearch": "^0.31.1", "meilisearch": "^0.32.3",
"next": "^13.3.0", "next": "^13.3.0",
"nodemailer": "^6.9.1", "nodemailer": "^6.9.1",
"rc-slider": "^10.1.1", "rc-slider": "^10.1.1",
@ -73,7 +73,7 @@
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"graphql-request": "5.1.0", "graphql-request": "5.1.0",
"next-sitemap": "^4.0.6", "next-sitemap": "^4.0.7",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"prettier-plugin-tailwindcss": "^0.2.7", "prettier-plugin-tailwindcss": "^0.2.7",
"tailwindcss": "^3.3.1", "tailwindcss": "^3.3.1",

View File

@ -1,5 +1,4 @@
// eslint-disable-next-line import/named import type { Placement } from "tippy.js";
import { Placement } from "tippy.js";
import { Button } from "./Button"; import { Button } from "./Button";
import { ToolTip } from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";

View File

@ -5,9 +5,14 @@ import { sendAnalytics } from "helpers/analytics";
import { atoms } from "contexts/atoms"; import { atoms } from "contexts/atoms";
import { useAtomPair, useAtomSetter } from "helpers/atoms"; import { useAtomPair, useAtomSetter } from "helpers/atoms";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; import {
containsHighlight,
CustomSearchResponse,
filterHitsWithHighlight,
meiliMultiSearch,
} from "helpers/search";
import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard"; import { PreviewCard, TranslatedPreviewCard } from "components/PreviewCard";
import { filterDefined, filterHasAttributes, isDefined } from "helpers/asserts"; import { filterHasAttributes, isDefined } from "helpers/asserts";
import { import {
MeiliContent, MeiliContent,
MeiliIndices, MeiliIndices,
@ -35,160 +40,154 @@ const SEARCH_LIMIT = 8;
* COMPONENT * COMPONENT
*/ */
interface MultiResult {
libraryItems?: CustomSearchResponse<MeiliLibraryItem>;
contents?: CustomSearchResponse<MeiliContent>;
videos?: CustomSearchResponse<MeiliVideo>;
posts?: CustomSearchResponse<MeiliPost>;
wikiPages?: CustomSearchResponse<MeiliWikiPage>;
weapons?: CustomSearchResponse<MeiliWeapon>;
}
export const SearchPopup = (): JSX.Element => { export const SearchPopup = (): JSX.Element => {
const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened); const [isSearchOpened, setSearchOpened] = useAtomPair(atoms.layout.searchOpened);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const { format } = useFormat(); const { format } = useFormat();
const [libraryItems, setLibraryItems] = useState<CustomSearchResponse<MeiliLibraryItem>>(); const [multiResult, setMultiResult] = useState<MultiResult>({});
const [contents, setContents] = useState<CustomSearchResponse<MeiliContent>>();
const [videos, setVideos] = useState<CustomSearchResponse<MeiliVideo>>();
const [posts, setPosts] = useState<CustomSearchResponse<MeiliPost>>();
const [wikiPages, setWikiPages] = useState<CustomSearchResponse<MeiliWikiPage>>();
const [weapons, setWeapons] = useState<CustomSearchResponse<MeiliWeapon>>();
useEffect(() => { useEffect(() => {
const fetchLibraryItems = async () => { const fetchMultiResult = async () => {
const searchResult = await meiliSearch(MeiliIndices.LIBRARY_ITEM, query, { const searchResults = (
limit: SEARCH_LIMIT, await meiliMultiSearch([
attributesToRetrieve: [ {
"title", indexUid: MeiliIndices.LIBRARY_ITEM,
"subtitle", q: query,
"descriptions", limit: SEARCH_LIMIT,
"id", attributesToRetrieve: [
"slug", "title",
"thumbnail", "subtitle",
"release_date", "descriptions",
"price", "id",
"categories", "slug",
"metadata", "thumbnail",
], "release_date",
attributesToHighlight: ["title", "subtitle", "descriptions"], "price",
attributesToCrop: ["descriptions"], "categories",
}); "metadata",
searchResult.hits = searchResult.hits.map((item) => { ],
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("descriptions"))) { attributesToHighlight: ["title", "subtitle", "descriptions"],
item._formatted.descriptions = filterDefined(item._formatted.descriptions).filter( attributesToCrop: ["descriptions"],
(description) => containsHighlight(JSON.stringify(description)) },
); {
} indexUid: MeiliIndices.CONTENT,
return item; q: query,
}); limit: SEARCH_LIMIT,
setLibraryItems(searchResult); attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
}; attributesToHighlight: ["translations"],
attributesToCrop: ["translations.displayable_description"],
},
{
indexUid: MeiliIndices.VIDEOS,
q: query,
limit: SEARCH_LIMIT,
attributesToRetrieve: [
"title",
"channel",
"uid",
"published_date",
"views",
"duration",
"description",
],
attributesToHighlight: ["title", "channel", "description"],
attributesToCrop: ["description"],
},
{
indexUid: MeiliIndices.POST,
q: query,
limit: SEARCH_LIMIT,
attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"],
attributesToHighlight: [
"translations.title",
"translations.excerpt",
"translations.body",
],
attributesToCrop: ["translations.body"],
filter: ["hidden = false"],
},
{
indexUid: MeiliIndices.WEAPON,
q: query,
limit: SEARCH_LIMIT,
attributesToHighlight: ["translations.description", "translations.names"],
attributesToCrop: ["translations.description"],
sort: ["slug:asc"],
},
{
indexUid: MeiliIndices.WIKI_PAGE,
q: query,
limit: SEARCH_LIMIT,
attributesToHighlight: [
"translations.title",
"translations.aliases",
"translations.summary",
"translations.displayable_description",
],
attributesToCrop: ["translations.displayable_description"],
},
])
).results;
const fetchContents = async () => { const result: MultiResult = {};
const searchResult = await meiliSearch(MeiliIndices.CONTENT, query, {
limit: SEARCH_LIMIT,
attributesToRetrieve: ["translations", "id", "slug", "categories", "type", "thumbnail"],
attributesToHighlight: ["translations"],
attributesToCrop: ["translations.displayable_description"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => containsHighlight(JSON.stringify(translation))
);
}
return item;
});
setContents(searchResult);
};
const fetchVideos = async () => { searchResults.map((searchResult) => {
const searchResult = await meiliSearch(MeiliIndices.VIDEOS, query, { switch (searchResult.indexUid) {
limit: SEARCH_LIMIT, case MeiliIndices.LIBRARY_ITEM: {
attributesToRetrieve: [ result.libraryItems = filterHitsWithHighlight<MeiliLibraryItem>(
"title", searchResult,
"channel", "descriptions"
"uid", );
"published_date", break;
"views", }
"duration",
"description",
],
attributesToHighlight: ["title", "channel", "description"],
attributesToCrop: ["description"],
});
setVideos(searchResult);
};
const fetchPosts = async () => { case MeiliIndices.CONTENT: {
const searchResult = await meiliSearch(MeiliIndices.POST, query, { result.contents = filterHitsWithHighlight<MeiliContent>(searchResult, "translations");
limit: SEARCH_LIMIT, break;
attributesToRetrieve: ["translations", "thumbnail", "slug", "date", "categories"], }
attributesToHighlight: ["translations.title", "translations.excerpt", "translations.body"],
attributesToCrop: ["translations.body"],
filter: ["hidden = false"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
setPosts(searchResult);
};
const fetchWeapons = async () => { case MeiliIndices.VIDEOS: {
const searchResult = await meiliSearch(MeiliIndices.WEAPON, query, { result.videos = filterHitsWithHighlight<MeiliVideo>(searchResult);
limit: SEARCH_LIMIT, break;
attributesToRetrieve: ["*"], }
attributesToHighlight: ["translations.description", "translations.names"],
attributesToCrop: ["translations.description"],
sort: ["slug:asc"],
});
searchResult.hits = searchResult.hits.map((item) => {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
setWeapons(searchResult);
};
const fetchWikiPages = async () => { case MeiliIndices.POST: {
const searchResult = await meiliSearch(MeiliIndices.WIKI_PAGE, query, { result.posts = filterHitsWithHighlight<MeiliPost>(searchResult, "translations");
limit: SEARCH_LIMIT, break;
attributesToHighlight: [ }
"translations.title",
"translations.aliases", case MeiliIndices.WEAPON: {
"translations.summary", result.weapons = filterHitsWithHighlight<MeiliWeapon>(searchResult, "translations");
"translations.displayable_description", break;
], }
attributesToCrop: ["translations.displayable_description"],
}); case MeiliIndices.WIKI_PAGE: {
searchResult.hits = searchResult.hits.map((item) => { result.wikiPages = filterHitsWithHighlight<MeiliWikiPage>(searchResult, "translations");
if ( break;
Object.keys(item._matchesPosition).filter((match) => match.startsWith("translations")) }
.length > 0
) { default: {
item._formatted.translations = filterDefined(item._formatted.translations).filter( console.log("What the fuck?");
(translation) => JSON.stringify(translation).includes("</mark>") }
);
} }
return item;
}); });
setWikiPages(searchResult);
setMultiResult(result);
}; };
if (query === "") { if (query === "") {
setWikiPages(undefined); setMultiResult({});
setLibraryItems(undefined);
setContents(undefined);
setVideos(undefined);
setPosts(undefined);
setWeapons(undefined);
} else { } else {
fetchWikiPages(); fetchMultiResult();
fetchLibraryItems();
fetchContents();
fetchVideos();
fetchPosts();
fetchWeapons();
} }
}, [query]); }, [query]);
@ -207,15 +206,15 @@ export const SearchPopup = (): JSX.Element => {
<TextInput onChange={setQuery} value={query} placeholder={format("search_title")} /> <TextInput onChange={setQuery} value={query} placeholder={format("search_title")} />
<div className="flex w-full flex-wrap gap-12 gap-x-16"> <div className="flex w-full flex-wrap gap-12 gap-x-16">
{isDefined(libraryItems) && ( {isDefined(multiResult.libraryItems) && (
<SearchResultSection <SearchResultSection
title={format("library")} title={format("library")}
icon="auto_stories" icon="auto_stories"
href={`/library?page=1&query=${query}\ href={`/library?page=1&query=${query}\
&sort=0&primary=true&secondary=true&subitems=true&status=all`} &sort=0&primary=true&secondary=true&subitems=true&status=all`}
totalHits={libraryItems.estimatedTotalHits}> totalHits={multiResult.libraryItems.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8"> <div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{libraryItems.hits.map((item) => ( {multiResult.libraryItems.hits.map((item) => (
<TranslatedPreviewCard <TranslatedPreviewCard
key={item.id} key={item.id}
className="w-56" className="w-56"
@ -255,14 +254,14 @@ export const SearchPopup = (): JSX.Element => {
</SearchResultSection> </SearchResultSection>
)} )}
{isDefined(contents) && ( {isDefined(multiResult.contents) && (
<SearchResultSection <SearchResultSection
title={format("contents")} title={format("contents")}
icon="workspaces" icon="workspaces"
href={`/contents/all?page=1&query=${query}&sort=0`} href={`/contents/all?page=1&query=${query}&sort=0`}
totalHits={contents.estimatedTotalHits}> totalHits={multiResult.contents.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8"> <div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{contents.hits.map((item) => ( {multiResult.contents.hits.map((item) => (
<TranslatedPreviewCard <TranslatedPreviewCard
key={item.id} key={item.id}
className="w-56" className="w-56"
@ -300,14 +299,14 @@ export const SearchPopup = (): JSX.Element => {
</SearchResultSection> </SearchResultSection>
)} )}
{isDefined(wikiPages) && ( {isDefined(multiResult.wikiPages) && (
<SearchResultSection <SearchResultSection
title={format("wiki")} title={format("wiki")}
icon="travel_explore" icon="travel_explore"
href={`/wiki?page=1&query=${query}`} href={`/wiki?page=1&query=${query}`}
totalHits={wikiPages.estimatedTotalHits}> totalHits={multiResult.wikiPages.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8"> <div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{wikiPages.hits.map((item) => ( {multiResult.wikiPages.hits.map((item) => (
<TranslatedPreviewCard <TranslatedPreviewCard
key={item.id} key={item.id}
className="w-56" className="w-56"
@ -352,14 +351,14 @@ export const SearchPopup = (): JSX.Element => {
</SearchResultSection> </SearchResultSection>
)} )}
{isDefined(posts) && ( {isDefined(multiResult.posts) && (
<SearchResultSection <SearchResultSection
title={format("news")} title={format("news")}
icon="newspaper" icon="newspaper"
href={`/news?page=1&query=${query}`} href={`/news?page=1&query=${query}`}
totalHits={posts.estimatedTotalHits}> totalHits={multiResult.posts.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8"> <div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{posts.hits.map((item) => ( {multiResult.posts.hits.map((item) => (
<TranslatedPreviewCard <TranslatedPreviewCard
className="w-56" className="w-56"
key={item.id} key={item.id}
@ -395,14 +394,14 @@ export const SearchPopup = (): JSX.Element => {
</SearchResultSection> </SearchResultSection>
)} )}
{isDefined(videos) && ( {isDefined(multiResult.videos) && (
<SearchResultSection <SearchResultSection
title={format("videos")} title={format("videos")}
icon="movie" icon="movie"
href={`/archives/videos?page=1&query=${query}&sort=1&gone=`} href={`/archives/videos?page=1&query=${query}&sort=1&gone=`}
totalHits={videos.estimatedTotalHits}> totalHits={multiResult.videos.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8"> <div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{videos.hits.map((item) => ( {multiResult.videos.hits.map((item) => (
<PreviewCard <PreviewCard
className="w-56" className="w-56"
key={item.uid} key={item.uid}
@ -435,14 +434,14 @@ export const SearchPopup = (): JSX.Element => {
</SearchResultSection> </SearchResultSection>
)} )}
{isDefined(weapons) && ( {isDefined(multiResult.weapons) && (
<SearchResultSection <SearchResultSection
title={format("weapon", { count: Infinity })} title={format("weapon", { count: Infinity })}
icon="shield" icon="shield"
href={`/wiki/weapons?page=1&query=${query}`} href={`/wiki/weapons?page=1&query=${query}`}
totalHits={weapons.estimatedTotalHits}> totalHits={multiResult.weapons.estimatedTotalHits}>
<div className="flex flex-wrap items-start gap-x-6 gap-y-8"> <div className="flex flex-wrap items-start gap-x-6 gap-y-8">
{weapons.hits.map((item) => ( {multiResult.weapons.hits.map((item) => (
<TranslatedPreviewCard <TranslatedPreviewCard
key={item.id} key={item.id}
className="w-56" className="w-56"

View File

@ -1,5 +1,5 @@
// eslint-disable-next-line import/named import Tippy from "@tippyjs/react";
import Tippy, { TippyProps } from "@tippyjs/react"; import type { TippyProps } from "@tippyjs/react";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import "tippy.js/animations/scale-subtle.css"; import "tippy.js/animations/scale-subtle.css";

View File

@ -1,7 +1,6 @@
import { atom, PrimitiveAtom, Atom, WritableAtom, useAtom } from "jotai"; import { atom, PrimitiveAtom, Atom, WritableAtom, useAtom } from "jotai";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments
type AtomPair<T> = [Atom<T>, WritableAtom<null, [newText: T], void>]; type AtomPair<T> = [Atom<T>, WritableAtom<null, [newText: T], void>];
export const atomPairing = <T>(anAtom: PrimitiveAtom<T>): AtomPair<T> => { export const atomPairing = <T>(anAtom: PrimitiveAtom<T>): AtomPair<T> => {

View File

@ -58,7 +58,6 @@ export const prettyInlineTitle = (
return result; return result;
}; };
/* eslint-disable id-denylist */
export const prettyItemSubType = ( export const prettyItemSubType = (
metadata: metadata:
| { | {

View File

@ -4,7 +4,6 @@ export const isUntangibleGroupItem = (
metadata: metadata:
| { | {
__typename: string; __typename: string;
// eslint-disable-next-line id-denylist
subtype?: { data?: { attributes?: { slug: string } | null } | null } | null; subtype?: { data?: { attributes?: { slug: string } | null } | null } | null;
} }
| null | null

View File

@ -2,28 +2,30 @@ type LoggerMode = "both" | "client" | "server";
const isServer = typeof window === "undefined"; const isServer = typeof window === "undefined";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types type Logger = {
export const getLogger = (prefix: string, mode: LoggerMode = "client") => { error: (message?: unknown, ...optionalParams: unknown[]) => void;
warn: (message?: unknown, ...optionalParams: unknown[]) => void;
log: (message?: unknown, ...optionalParams: unknown[]) => void;
info: (message?: unknown, ...optionalParams: unknown[]) => void;
debug: (message?: unknown, ...optionalParams: unknown[]) => void;
};
export const getLogger = (prefix: string, mode: LoggerMode = "client"): Logger => {
if ((mode === "client" && isServer) || (mode === "server" && !isServer)) { if ((mode === "client" && isServer) || (mode === "server" && !isServer)) {
return { return {
error: () => null, error: () => undefined,
warn: () => null, warn: () => undefined,
log: () => null, log: () => undefined,
info: () => null, info: () => undefined,
debug: () => null, debug: () => undefined,
}; };
} }
return { return {
error: (message?: unknown, ...optionalParams: unknown[]) => error: (message, ...optionalParams) => console.error(prefix, message, ...optionalParams),
console.error(prefix, message, ...optionalParams), warn: (message, ...optionalParams) => console.warn(prefix, message, ...optionalParams),
warn: (message?: unknown, ...optionalParams: unknown[]) => log: (message, ...optionalParams) => console.log(prefix, message, ...optionalParams),
console.warn(prefix, message, ...optionalParams), info: (message, ...optionalParams) => console.info(prefix, message, ...optionalParams),
log: (message?: unknown, ...optionalParams: unknown[]) => debug: (message, ...optionalParams) => console.debug(prefix, message, ...optionalParams),
console.log(prefix, message, ...optionalParams),
info: (message?: unknown, ...optionalParams: unknown[]) =>
console.info(prefix, message, ...optionalParams),
debug: (message?: unknown, ...optionalParams: unknown[]) =>
console.debug(prefix, message, ...optionalParams),
}; };
}; };

View File

@ -1,7 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import { MeiliSearch } from "meilisearch";
// eslint-disable-next-line import/named import type {
import { MatchesPosition, MeiliSearch, SearchParams, SearchResponse } from "meilisearch"; SearchParams,
import { isDefined } from "./asserts"; MatchesPosition,
SearchResponse,
MultiSearchQuery,
MultiSearchResponse,
MultiSearchResult,
} from "meilisearch";
import { filterDefined, isDefined } from "./asserts";
import { MeiliDocumentsType } from "shared/meilisearch-graphql-typings/meiliTypes"; import { MeiliDocumentsType } from "shared/meilisearch-graphql-typings/meiliTypes";
const meili = new MeiliSearch({ const meili = new MeiliSearch({
@ -12,20 +18,61 @@ const meili = new MeiliSearch({
interface CustomSearchParams interface CustomSearchParams
extends Omit< extends Omit<
SearchParams, SearchParams,
"cropMarker" | "highlightPostTag" | "highlightPreTag" | "q" | "showMatchesPosition" | "cropLength"
| "cropMarker"
| "cropMarker"
| "highlightPostTag"
| "highlightPreTag"
| "q"
| "showMatchesPosition"
> {} > {}
type CustomHit<T = Record<string, any>> = T & { type CustomHit<T = Record<string, unknown>> = T & {
_formatted: Partial<T>; _formatted: Partial<T>;
_matchesPosition: MatchesPosition<T>; _matchesPosition: MatchesPosition<T>;
}; };
type CustomHits<T = Record<string, any>> = CustomHit<T>[]; type CustomHits<T = Record<string, unknown>> = CustomHit<T>[];
export interface CustomSearchResponse<T> extends Omit<SearchResponse<T>, "hits"> { export interface CustomSearchResponse<T> extends Omit<SearchResponse<T>, "hits"> {
hits: CustomHits<T>; hits: CustomHits<T>;
} }
export const meiliMultiSearch = async (queries: MultiSearchQuery[]): Promise<MultiSearchResponse> =>
await meili.multiSearch({
queries: queries.map((query) => ({
attributesToHighlight: ["*"],
...query,
highlightPreTag: "<mark>",
highlightPostTag: "</mark>",
showMatchesPosition: true,
cropLength: 20,
cropMarker: "...",
})),
});
export const filterHitsWithHighlight = <T extends MeiliDocumentsType["documents"]>(
searchResult: CustomSearchResponse<T> | MultiSearchResult<Record<string, unknown>>,
keyToFilter?: keyof T
): CustomSearchResponse<T> => {
const result = searchResult as unknown as CustomSearchResponse<T>;
if (isDefined(keyToFilter)) {
result.hits = result.hits.map((item) => {
if (
Object.keys(item._matchesPosition).some((match) => match.startsWith(keyToFilter as string))
) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
item._formatted[keyToFilter] = filterDefined(item._formatted[keyToFilter]).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
}
return result;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const meiliSearch = async <I extends MeiliDocumentsType["index"]>( export const meiliSearch = async <I extends MeiliDocumentsType["index"]>(
indexName: I, indexName: I,

View File

@ -12,16 +12,16 @@ import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph"; import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { sendAnalytics } from "helpers/analytics"; import { sendAnalytics } from "helpers/analytics";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; import {
containsHighlight,
CustomSearchResponse,
filterHitsWithHighlight,
meiliSearch,
} from "helpers/search";
import { MeiliContent, MeiliIndices } from "shared/meilisearch-graphql-typings/meiliTypes"; import { MeiliContent, MeiliIndices } from "shared/meilisearch-graphql-typings/meiliTypes";
import { useTypedRouter } from "hooks/useTypedRouter"; import { useTypedRouter } from "hooks/useTypedRouter";
import { TranslatedPreviewCard } from "components/PreviewCard"; import { TranslatedPreviewCard } from "components/PreviewCard";
@ -97,15 +97,7 @@ const Contents = (props: Props): JSX.Element => {
page, page,
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined, sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
}); });
searchResult.hits = searchResult.hits.map((item) => { setContents(filterHitsWithHighlight<MeiliContent>(searchResult, "translations"));
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => containsHighlight(JSON.stringify(translation))
);
}
return item;
});
setContents(searchResult);
}; };
fetchPosts(); fetchPosts();
}, [query, page, sortingMethod, sortingMethods]); }, [query, page, sortingMethod, sortingMethods]);

View File

@ -1,4 +1,3 @@
/* eslint-disable id-denylist */
import { GetStaticProps } from "next"; import { GetStaticProps } from "next";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import Slider from "rc-slider"; import Slider from "rc-slider";

View File

@ -14,17 +14,16 @@ import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { ButtonGroup } from "components/Inputs/ButtonGroup"; import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { import { filterHasAttributes, isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/asserts";
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
isUndefined,
} from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph"; import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { sendAnalytics } from "helpers/analytics"; import { sendAnalytics } from "helpers/analytics";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; import {
containsHighlight,
CustomSearchResponse,
filterHitsWithHighlight,
meiliSearch,
} from "helpers/search";
import { MeiliIndices, MeiliLibraryItem } from "shared/meilisearch-graphql-typings/meiliTypes"; import { MeiliIndices, MeiliLibraryItem } from "shared/meilisearch-graphql-typings/meiliTypes";
import { useTypedRouter } from "hooks/useTypedRouter"; import { useTypedRouter } from "hooks/useTypedRouter";
import { TranslatedPreviewCard } from "components/PreviewCard"; import { TranslatedPreviewCard } from "components/PreviewCard";
@ -178,15 +177,7 @@ const Library = (props: Props): JSX.Element => {
sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined, sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined,
filter, filter,
}); });
searchResult.hits = searchResult.hits.map((item) => { setLibraryItems(filterHitsWithHighlight<MeiliLibraryItem>(searchResult, "descriptions"));
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("descriptions"))) {
item._formatted.descriptions = filterDefined(item._formatted.descriptions).filter(
(description) => containsHighlight(JSON.stringify(description))
);
}
return item;
});
setLibraryItems(searchResult);
}; };
fetchLibraryItems(); fetchLibraryItems();
}, [ }, [

View File

@ -11,12 +11,7 @@ import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/asserts";
import { getOpenGraph } from "helpers/openGraph"; import { getOpenGraph } from "helpers/openGraph";
import { TranslatedPreviewCard } from "components/PreviewCard"; import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
@ -24,7 +19,12 @@ import { sendAnalytics } from "helpers/analytics";
import { Terminal } from "components/Cli/Terminal"; import { Terminal } from "components/Cli/Terminal";
import { atoms } from "contexts/atoms"; import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms"; import { useAtomGetter } from "helpers/atoms";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; import {
containsHighlight,
CustomSearchResponse,
filterHitsWithHighlight,
meiliSearch,
} from "helpers/search";
import { MeiliIndices, MeiliPost } from "shared/meilisearch-graphql-typings/meiliTypes"; import { MeiliIndices, MeiliPost } from "shared/meilisearch-graphql-typings/meiliTypes";
import { useTypedRouter } from "hooks/useTypedRouter"; import { useTypedRouter } from "hooks/useTypedRouter";
import { prettySlug } from "helpers/formatters"; import { prettySlug } from "helpers/formatters";
@ -84,15 +84,7 @@ const News = ({ ...otherProps }: Props): JSX.Element => {
sort: ["sortable_date:desc"], sort: ["sortable_date:desc"],
filter: ["hidden = false"], filter: ["hidden = false"],
}); });
searchResult.hits = searchResult.hits.map((item) => { setPosts(filterHitsWithHighlight<MeiliPost>(searchResult, "translations"));
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
setPosts(searchResult);
}; };
fetchPosts(); fetchPosts();
}, [query, page]); }, [query, page]);

View File

@ -10,16 +10,16 @@ import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
import { useTypedRouter } from "hooks/useTypedRouter"; import { useTypedRouter } from "hooks/useTypedRouter";
import { useFormat } from "hooks/useFormat"; import { useFormat } from "hooks/useFormat";
import { import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
filterDefined,
filterHasAttributes,
isDefined,
isDefinedAndNotEmpty,
} from "helpers/asserts";
import { sendAnalytics } from "helpers/analytics"; import { sendAnalytics } from "helpers/analytics";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; import {
containsHighlight,
CustomSearchResponse,
filterHitsWithHighlight,
meiliSearch,
} from "helpers/search";
import { MeiliIndices, MeiliWeapon } from "shared/meilisearch-graphql-typings/meiliTypes"; import { MeiliIndices, MeiliWeapon } from "shared/meilisearch-graphql-typings/meiliTypes";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel"; import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { Paginator } from "components/Containers/Paginator"; import { Paginator } from "components/Containers/Paginator";
@ -79,17 +79,7 @@ const Weapons = (props: Props): JSX.Element => {
attributesToCrop: ["translations.description"], attributesToCrop: ["translations.description"],
sort: ["slug:asc"], sort: ["slug:asc"],
}); });
setWeapons(filterHitsWithHighlight<MeiliWeapon>(searchResult, "translations"));
searchResult.hits = searchResult.hits.map((item) => {
if (Object.keys(item._matchesPosition).some((match) => match.startsWith("translations"))) {
item._formatted.translations = filterDefined(item._formatted.translations).filter(
(translation) => JSON.stringify(translation).includes("</mark>")
);
}
return item;
});
setWeapons(searchResult);
}; };
fetchPosts(); fetchPosts();
}, [query, page]); }, [query, page]);