import { GetStaticProps } from "next"; import { useState, useMemo, useCallback } from "react"; import { useBoolean } from "usehooks-ts"; import naturalCompare from "string-natural-compare"; 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/Panels/ContentPanel"; import { SubPanel } from "components/Panels/SubPanel"; import { GetLibraryItemsPreviewQuery } from "graphql/generated"; import { getReadySdk } from "graphql/sdk"; import { prettyInlineTitle, prettyItemSubType } from "helpers/formatters"; import { LibraryItemUserStatus } from "types/types"; import { Icon } from "components/Ico"; import { WithLabel } from "components/Inputs/WithLabel"; import { TextInput } from "components/Inputs/TextInput"; import { Button } from "components/Inputs/Button"; import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; import { isUntangibleGroupItem } from "helpers/libraryItem"; import { PreviewCard } from "components/PreviewCard"; import { useDeviceSupportsHover } from "hooks/useMediaQuery"; import { ButtonGroup } from "components/Inputs/ButtonGroup"; import { filterHasAttributes, isDefined, isDefinedAndNotEmpty, isUndefined } from "helpers/others"; import { convertPrice } from "helpers/numbers"; import { SmartList } from "components/SmartList"; import { SelectiveNonNullable } from "types/SelectiveNonNullable"; import { getOpenGraph } from "helpers/openGraph"; import { compareDate } from "helpers/date"; import { HorizontalLine } from "components/HorizontalLine"; import { cIf, cJoin } from "helpers/className"; import { getLangui } from "graphql/fetchLocalData"; import { sendAnalytics } from "helpers/analytics"; import { useLocalData } from "contexts/LocalDataContext"; import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus"; import { useContainerQueries } from "contexts/ContainerQueriesContext"; /* * ╭─────────────╮ * ────────────────────────────────────────╯ CONSTANTS ╰────────────────────────────────────────── */ const DEFAULT_FILTERS_STATE = { searchName: "", showSubitems: false, showPrimaryItems: true, showSecondaryItems: false, sortingMethod: 0, groupingMethod: -1, keepInfoVisible: false, filterUserStatus: undefined, }; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ interface Props extends AppLayoutRequired { items: NonNullable["data"]; } const Library = ({ items, ...otherProps }: Props): JSX.Element => { const hoverable = useDeviceSupportsHover(); const { langui, currencies } = useLocalData(); const { libraryItemUserStatus } = useLibraryItemUserStatus(); const { isContentPanelAtLeast4xl } = useContainerQueries(); const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName); const { value: showSubitems, toggle: toggleShowSubitems, setValue: setShowSubitems, } = useBoolean(DEFAULT_FILTERS_STATE.showSubitems); const { value: showPrimaryItems, toggle: toggleShowPrimaryItems, setValue: setShowPrimaryItems, } = useBoolean(DEFAULT_FILTERS_STATE.showPrimaryItems); const { value: showSecondaryItems, toggle: toggleShowSecondaryItems, setValue: setShowSecondaryItems, } = useBoolean(DEFAULT_FILTERS_STATE.showSecondaryItems); const { value: keepInfoVisible, toggle: toggleKeepInfoVisible, setValue: setKeepInfoVisible, } = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible); const [sortingMethod, setSortingMethod] = useState(DEFAULT_FILTERS_STATE.sortingMethod); const [groupingMethod, setGroupingMethod] = useState( DEFAULT_FILTERS_STATE.groupingMethod ); const [filterUserStatus, setFilterUserStatus] = useState( DEFAULT_FILTERS_STATE.filterUserStatus ); const filteringFunction = useCallback( (item: SelectiveNonNullable) => { if (!showSubitems && !item.attributes.root_item) return false; if (showSubitems && isUntangibleGroupItem(item.attributes.metadata?.[0])) { return false; } if (item.attributes.primary && !showPrimaryItems) return false; if (!item.attributes.primary && !showSecondaryItems) return false; if (isDefined(filterUserStatus) && item.id) { if (isUntangibleGroupItem(item.attributes.metadata?.[0])) { return false; } if (filterUserStatus === LibraryItemUserStatus.None) { if (libraryItemUserStatus[item.id]) { return false; } } else if (filterUserStatus !== libraryItemUserStatus[item.id]) { return false; } } return true; }, [libraryItemUserStatus, filterUserStatus, showPrimaryItems, showSecondaryItems, showSubitems] ); const sortingFunction = useCallback( ( a: SelectiveNonNullable, b: SelectiveNonNullable ) => { switch (sortingMethod) { case 0: { const titleA = prettyInlineTitle("", a.attributes.title, a.attributes.subtitle); const titleB = prettyInlineTitle("", b.attributes.title, b.attributes.subtitle); return naturalCompare(titleA, titleB); } case 1: { const priceA = a.attributes.price ? convertPrice(a.attributes.price, currencies[0]) : Infinity; const priceB = b.attributes.price ? convertPrice(b.attributes.price, currencies[0]) : Infinity; return priceA - priceB; } case 2: { return compareDate(a.attributes.release_date, b.attributes.release_date); } default: return 0; } }, [currencies, sortingMethod] ); const groupingFunction = useCallback( (item: SelectiveNonNullable): string[] => { switch (groupingMethod) { case 0: { const categories = filterHasAttributes(item.attributes.categories?.data, [ "attributes", ] as const); if (categories.length > 0) { return categories.map((category) => category.attributes.name); } return [langui.no_category ?? "No category"]; } case 1: { if (item.attributes.metadata && item.attributes.metadata.length > 0) { switch (item.attributes.metadata[0]?.__typename) { case "ComponentMetadataAudio": return [langui.audio ?? "Audio"]; case "ComponentMetadataGame": return [langui.game ?? "Game"]; case "ComponentMetadataBooks": return [langui.textual ?? "Textual"]; case "ComponentMetadataVideo": return [langui.video ?? "Video"]; case "ComponentMetadataOther": return [langui.other ?? "Other"]; case "ComponentMetadataGroup": { switch (item.attributes.metadata[0]?.subitems_type?.data?.attributes?.slug) { case "audio": return [langui.audio ?? "Audio"]; case "video": return [langui.video ?? "Video"]; case "game": return [langui.game ?? "Game"]; case "textual": return [langui.textual ?? "Textual"]; case "mixed": return [langui.group ?? "Group"]; default: { return [langui.no_type ?? "No type"]; } } } default: return [langui.no_type ?? "No type"]; } } else { return [langui.no_type ?? "No type"]; } } case 2: { if (item.attributes.release_date?.year) { return [item.attributes.release_date.year.toString()]; } return [langui.no_year ?? "No year"]; } default: return [""]; } }, [groupingMethod, langui] ); const subPanel = useMemo( () => ( { setSearchName(name); if (isDefinedAndNotEmpty(name)) { sendAnalytics("Library", "Change search term"); } else { sendAnalytics("Library", "Clear search term"); } }} /> { setSortingMethod(value); sendAnalytics( "Library", `Change sorting method (${["name", "price", "release date"][value]})` ); }} /> { toggleShowSubitems(); sendAnalytics("Library", `${showSubitems ? "Hide" : "Show"} subitems`); }} /> { toggleShowPrimaryItems(); sendAnalytics("Library", `${showPrimaryItems ? "Hide" : "Show"} primary items`); }} /> { toggleShowSecondaryItems(); sendAnalytics("Library", `${showSecondaryItems ? "Hide" : "Show"} secondary items`); }} /> {hoverable && ( { toggleKeepInfoVisible(); sendAnalytics("Library", `Always ${keepInfoVisible ? "hide" : "show"} info`); }} /> )} { setFilterUserStatus(LibraryItemUserStatus.Want); sendAnalytics("Library", "Set filter status (I want)"); }, active: filterUserStatus === LibraryItemUserStatus.Want, }, { tooltip: langui.only_display_items_i_have, icon: Icon.BackHand, onClick: () => { setFilterUserStatus(LibraryItemUserStatus.Have); sendAnalytics("Library", "Set filter status (I have)"); }, active: filterUserStatus === LibraryItemUserStatus.Have, }, { tooltip: langui.only_display_unmarked_items, icon: Icon.RadioButtonUnchecked, onClick: () => { setFilterUserStatus(LibraryItemUserStatus.None); sendAnalytics("Library", "Set filter status (unmarked)"); }, active: filterUserStatus === LibraryItemUserStatus.None, }, { tooltip: langui.only_display_unmarked_items, text: langui.all, onClick: () => { setFilterUserStatus(undefined); sendAnalytics("Library", "Set filter status (all)"); }, active: isUndefined(filterUserStatus), }, ]} />