diff --git a/src/helpers/search.ts b/src/helpers/search.ts index 337d41c..62c3157 100644 --- a/src/helpers/search.ts +++ b/src/helpers/search.ts @@ -91,5 +91,24 @@ export const meiliSearch = async ( })) as unknown as CustomSearchResponse["documents"]>; }; +export type MeiliFacetResult = { name: string; count: number }[]; + +export const meiliFacet = async ( + indexName: I, + facet: string +): Promise => { + const index = meili.index(indexName); + const searchResult = await index.search["documents"]>( + "", + { + hitsPerPage: 0, + facets: [facet], + } + ); + return [...Object.entries(searchResult.facetDistribution?.[facet] ?? {})].map( + ([name, count]) => ({ name, count }) + ); +}; + export const containsHighlight = (text: string | null | undefined): boolean => isDefined(text) && text.includes(""); diff --git a/src/pages/contents/all.tsx b/src/pages/contents/all.tsx index f2bfbb7..8727330 100644 --- a/src/pages/contents/all.tsx +++ b/src/pages/contents/all.tsx @@ -2,6 +2,7 @@ import { GetStaticProps } from "next"; import { useEffect, useMemo, useState } from "react"; import { useBoolean } from "usehooks-ts"; import { z } from "zod"; +import Collapsible from "react-collapsible"; import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { Select } from "components/Inputs/Select"; import { Switch } from "components/Inputs/Switch"; @@ -20,6 +21,8 @@ import { containsHighlight, CustomSearchResponse, filterHitsWithHighlight, + meiliFacet, + MeiliFacetResult, meiliSearch, } from "helpers/search"; import { MeiliContent, MeiliIndices } from "shared/meilisearch-graphql-typings/meiliTypes"; @@ -31,6 +34,7 @@ import { useFormat } from "hooks/useFormat"; import { getFormat } from "helpers/i18n"; import { useAtomGetter, useAtomSetter } from "helpers/atoms"; import { atoms } from "contexts/atoms"; +import { Ico } from "components/Ico"; /* * ╭─────────────╮ @@ -75,17 +79,6 @@ const Contents = (props: Props): JSX.Element => { [format] ); - const languageOptions = useMemo(() => { - const memo = - router.locales?.map((language) => ({ - meiliAttribute: language, - displayedName: formatLanguage(language), - })) ?? []; - - memo.unshift({ meiliAttribute: "", displayedName: format("all") }); - return memo; - }, [router.locales, formatLanguage, format]); - const [sortingMethod, setSortingMethod] = useState( router.query.sort ?? DEFAULT_FILTERS_STATE.sortingMethod ); @@ -103,14 +96,15 @@ const Contents = (props: Props): JSX.Element => { router.query.lang ?? DEFAULT_FILTERS_STATE.lang ); + const [selectedLocales, setSelectedLocales] = useState([]); + useEffect(() => { const fetchPosts = async () => { const currentSortingMethod = sortingMethods[sortingMethod]?.meiliAttribute; - const currentLanguageOption = languageOptions[languageOption]?.meiliAttribute; const filter: string[] = []; - if (languageOption !== 0) { - filter.push(`filterable_languages = ${currentLanguageOption}`); + if (selectedLocales.length !== 0) { + filter.push(`filterable_languages IN [${selectedLocales.join()}]`); } const searchResult = await meiliSearch(MeiliIndices.CONTENT, query, { @@ -123,14 +117,10 @@ const Contents = (props: Props): JSX.Element => { sort: isDefined(currentSortingMethod) ? [currentSortingMethod] : undefined, }); - setContents( - languageOption === 0 - ? filterHitsWithHighlight(searchResult, "translations") - : searchResult - ); + setContents(filterHitsWithHighlight(searchResult, "translations")); }; fetchPosts(); - }, [query, page, sortingMethod, sortingMethods, languageOption, languageOptions]); + }, [query, page, sortingMethod, sortingMethods, selectedLocales]); useEffect(() => { if (router.isReady) @@ -153,6 +143,14 @@ const Contents = (props: Props): JSX.Element => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.isReady]); + const [countForLanguages, setCountForLanguages] = useState([]); + const [countForCategories, setCountForCategories] = useState([]); + + useEffect(() => { + meiliFacet(MeiliIndices.CONTENT, "filterable_languages").then(setCountForLanguages); + meiliFacet(MeiliIndices.CONTENT, "filterable_categories").then(setCountForCategories); + }, []); + const searchInput = ( { {!is1ColumnLayout &&
{searchInput}
} - + + + + item.displayedName)} - value={languageOption} - onChange={(newLanguageOption) => { - setPage(1); - setLanguageOption(newLanguageOption); - sendAnalytics( - "Contents/All", - `Change language filter (${ - languageOptions.map((item) => item.meiliAttribute)[newLanguageOption] - })` - ); - }} - /> - - {hoverable && ( { })) .filter( ({ language }) => - languageOption === 0 || - language === languageOptions[languageOption]?.meiliAttribute + selectedLocales.length === 0 || + query !== "" || + selectedLocales.includes(language) )} fallback={{ title: prettySlug(item.slug) }} thumbnail={item.thumbnail?.data?.attributes} @@ -322,3 +318,63 @@ export const getStaticProps: GetStaticProps = (context) => { props: props, }; }; + +/* + * ╭──────────────────────╮ + * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰────────────────────────────────────── + */ + +interface CollapsibleFiltersProps { + title: string; + facetResult: MeiliFacetResult; + selectedValues: string[]; + onValueChanged: (setStateFn: (current: string[]) => string[]) => void; + format: (name: string) => string; +} + +const CollapsibleFilters = ({ + title, + facetResult, + selectedValues, + onValueChanged, + format, +}: CollapsibleFiltersProps): JSX.Element => { + const [isOpened, setOpened] = useState(false); + + if (facetResult.length === 0) return <>; + return ( + setOpened(false)} + onOpening={() => setOpened(true)} + trigger={ +
+

{title}

+ +
+ } + easing="ease-in-out" + transitionTime={400} + contentInnerClassName="flex flex-wrap gap-1 py-3" + overflowWhenOpen="visible"> + {facetResult + .filter(({ count }) => count > 0) + .map(({ name, count }) => ( +