import { GetStaticProps } from "next"; import { useEffect, useMemo, useState } from "react"; import { useBoolean } from "usehooks-ts"; import { z } from "zod"; import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { Select } from "components/Inputs/Select"; import { Switch } from "components/Inputs/Switch"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel"; import { SubPanel } from "components/Containers/SubPanel"; import { LibraryItemUserStatus } from "types/types"; import { WithLabel } from "components/Inputs/WithLabel"; import { TextInput } from "components/Inputs/TextInput"; import { Button } from "components/Inputs/Button"; import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { ButtonGroup } from "components/Inputs/ButtonGroup"; import { filterDefined, filterHasAttributes, isDefined, isDefinedAndNotEmpty, isUndefined, } from "helpers/asserts"; import { getOpenGraph } from "helpers/openGraph"; import { HorizontalLine } from "components/HorizontalLine"; import { sendAnalytics } from "helpers/analytics"; import { containsHighlight, CustomSearchResponse, meiliSearch } from "helpers/search"; import { MeiliIndices, MeiliLibraryItem } from "shared/meilisearch-graphql-typings/meiliTypes"; import { useTypedRouter } from "hooks/useTypedRouter"; import { TranslatedPreviewCard } from "components/PreviewCard"; import { prettyItemSubType } from "helpers/formatters"; import { isUntangibleGroupItem } from "helpers/libraryItem"; import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus"; import { Paginator } from "components/Containers/Paginator"; import { useFormat } from "hooks/useFormat"; import { getFormat } from "helpers/i18n"; /* * ╭─────────────╮ * ────────────────────────────────────────╯ CONSTANTS ╰────────────────────────────────────────── */ const DEFAULT_FILTERS_STATE = { query: "", showSubitems: false, showPrimaryItems: true, showSecondaryItems: false, page: 1, sortingMethod: 0, keepInfoVisible: false, filterUserStatus: undefined, }; const queryParamSchema = z.object({ query: z.coerce.string().optional(), page: z.coerce.number().positive().optional(), sort: z.coerce.number().min(0).max(5).optional(), subitems: z.coerce.boolean().optional(), primary: z.coerce.boolean().optional(), secondary: z.coerce.boolean().optional(), status: z.coerce.string().optional(), }); /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ interface Props extends AppLayoutRequired {} const Library = (props: Props): JSX.Element => { const hoverable = useDeviceSupportsHover(); const { format } = useFormat(); const { libraryItemUserStatus } = useLibraryItemUserStatus(); const sortingMethods = useMemo( () => [ { meiliAttribute: "sortable_name:asc", displayedName: format("name") }, { meiliAttribute: "sortable_date:asc", displayedName: format("release_date") }, { meiliAttribute: "sortable_price:asc", displayedName: format("price") }, ], [format] ); const router = useTypedRouter(queryParamSchema); const [page, setPage] = useState(router.query.page ?? DEFAULT_FILTERS_STATE.page); const [libraryItems, setLibraryItems] = useState>(); const [query, setQuery] = useState(router.query.query ?? DEFAULT_FILTERS_STATE.query); const { value: showSubitems, toggle: toggleShowSubitems, setValue: setShowSubitems, } = useBoolean(router.query.subitems ?? DEFAULT_FILTERS_STATE.showSubitems); const { value: showPrimaryItems, toggle: toggleShowPrimaryItems, setValue: setShowPrimaryItems, } = useBoolean(router.query.primary ?? DEFAULT_FILTERS_STATE.showPrimaryItems); const { value: showSecondaryItems, toggle: toggleShowSecondaryItems, setValue: setShowSecondaryItems, } = useBoolean(router.query.secondary ?? DEFAULT_FILTERS_STATE.showSecondaryItems); const { value: keepInfoVisible, toggle: toggleKeepInfoVisible, setValue: setKeepInfoVisible, } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); const [sortingMethod, setSortingMethod] = useState( router.query.sort ?? DEFAULT_FILTERS_STATE.sortingMethod ); const [filterUserStatus, setFilterUserStatus] = useState( fromStringToLibraryItemUserStatus(router.query.status) ?? DEFAULT_FILTERS_STATE.filterUserStatus ); useEffect(() => { const fetchLibraryItems = async () => { const currentSortingMethod = sortingMethods[sortingMethod]; const filter: string[] = []; if (!showPrimaryItems && !showSecondaryItems) { filter.push("primary NOT EXISTS"); } else if (showPrimaryItems && !showSecondaryItems) { filter.push("primary = true"); } else if (!showPrimaryItems && showSecondaryItems) { filter.push("primary = false"); } if (showSubitems) { filter.push("untangible_group_item = false"); } else { filter.push("root_item = true"); } if (isDefined(filterUserStatus)) { filter.push("untangible_group_item = false"); if (filterUserStatus === LibraryItemUserStatus.None) { filter.push( `id NOT IN [${Object.entries(libraryItemUserStatus) .filter(([, value]) => value !== filterUserStatus) .map(([id]) => id) .join(", ")}]` ); } else { filter.push( `id IN [${Object.entries(libraryItemUserStatus) .filter(([, value]) => value === filterUserStatus) .map(([id]) => id) .join(", ")}]` ); } } const searchResult = await meiliSearch(MeiliIndices.LIBRARY_ITEM, query, { hitsPerPage: 25, page, attributesToRetrieve: [ "title", "subtitle", "descriptions", "id", "slug", "thumbnail", "release_date", "price", "categories", "metadata", ], attributesToHighlight: ["title", "subtitle", "descriptions"], attributesToCrop: ["descriptions"], sort: isDefined(currentSortingMethod) ? [currentSortingMethod.meiliAttribute] : undefined, filter, }); searchResult.hits = searchResult.hits.map((item) => { 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(); }, [ filterUserStatus, libraryItemUserStatus, page, query, showPrimaryItems, showSecondaryItems, showSubitems, sortingMethod, sortingMethods, ]); useEffect(() => { if (router.isReady) { router.updateQuery({ page, query, sort: sortingMethod, primary: showPrimaryItems, secondary: showSecondaryItems, subitems: showSubitems, status: fromLibraryItemUserStatusToString(filterUserStatus), }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ page, query, sortingMethod, router.isReady, showPrimaryItems, showSecondaryItems, showSubitems, filterUserStatus, ]); useEffect(() => { if (router.isReady) { if (isDefined(router.query.page)) setPage(router.query.page); if (isDefined(router.query.query)) setQuery(router.query.query); if (isDefined(router.query.sort)) setSortingMethod(router.query.sort); if (isDefined(router.query.primary)) setShowPrimaryItems(router.query.primary); if (isDefined(router.query.secondary)) setShowSecondaryItems(router.query.secondary); if (isDefined(router.query.subitems)) setShowSubitems(router.query.subitems); if (isDefined(router.query.status)) setFilterUserStatus(fromStringToLibraryItemUserStatus(router.query.status)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.isReady]); useEffect(() => { const totalPages = libraryItems?.totalPages; if (isDefined(totalPages) && totalPages < page && totalPages >= 1) setPage(totalPages); }, [libraryItems?.totalPages, page]); const subPanel = ( { setPage(1); setQuery(name); if (isDefinedAndNotEmpty(name)) { sendAnalytics("Library", "Change search term"); } else { sendAnalytics("Library", "Clear search term"); } }} />