From 59283fa4652cb743b8a1e711f09f07af165488c4 Mon Sep 17 00:00:00 2001 From: DrMint Date: Sat, 28 May 2022 19:33:10 +0200 Subject: [PATCH] Added ability to mark library item as 'Want' or 'have' --- src/components/Library/PreviewCardCTAs.tsx | 76 ++++++ src/components/PreviewCard.tsx | 4 + src/contexts/AppLayoutContext.tsx | 129 +++++---- src/helpers/libraryItem.ts | 251 +++++++++++++++++ src/helpers/types.ts | 6 + src/pages/library/[slug]/index.tsx | 22 +- src/pages/library/index.tsx | 304 +++++---------------- 7 files changed, 511 insertions(+), 281 deletions(-) create mode 100644 src/components/Library/PreviewCardCTAs.tsx create mode 100644 src/helpers/libraryItem.ts diff --git a/src/components/Library/PreviewCardCTAs.tsx b/src/components/Library/PreviewCardCTAs.tsx new file mode 100644 index 0000000..3983260 --- /dev/null +++ b/src/components/Library/PreviewCardCTAs.tsx @@ -0,0 +1,76 @@ +import { Icon } from "components/Ico"; +import { Button } from "components/Inputs/Button"; +import { ToolTip } from "components/ToolTip"; +import { useAppLayout } from "contexts/AppLayoutContext"; +import { LibraryItemUserStatus } from "helpers/types"; + +interface Props { + id: string | null | undefined; + displayCTAs: boolean; + expand?: boolean; +} + +export function PreviewCardCTAs(props: Props): JSX.Element { + const { id, displayCTAs, expand = false } = props; + const appLayout = useAppLayout(); + + return ( + <> + {displayCTAs && id && ( +
+ {/* TODO: Add to langui */} + +
+ )} + + ); +} diff --git a/src/components/PreviewCard.tsx b/src/components/PreviewCard.tsx index f612568..51f66db 100644 --- a/src/components/PreviewCard.tsx +++ b/src/components/PreviewCard.tsx @@ -38,6 +38,7 @@ interface Props { author?: string; position: "Bottom" | "Top"; }; + infoAppend?: React.ReactNode; hoverlay?: | { __typename: "Video"; @@ -61,6 +62,7 @@ export function PreviewCard(props: Immutable): JSX.Element { thumbnailAspectRatio, metadata, hoverlay, + infoAppend, } = props; const appLayout = useAppLayout(); @@ -251,6 +253,8 @@ export function PreviewCard(props: Immutable): JSX.Element { )} {metadata?.position === "Bottom" && metadataJSX} + + {infoAppend} diff --git a/src/contexts/AppLayoutContext.tsx b/src/contexts/AppLayoutContext.tsx index e34b728..294630a 100644 --- a/src/contexts/AppLayoutContext.tsx +++ b/src/contexts/AppLayoutContext.tsx @@ -1,41 +1,57 @@ -import { Immutable } from "helpers/types"; +import { Immutable, LibraryItemUserStatus } from "helpers/types"; import { useDarkMode } from "hooks/useDarkMode"; import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage"; import React, { ReactNode, useContext, useState } from "react"; -interface AppLayoutState { +export interface AppLayoutState { subPanelOpen: boolean | undefined; + setSubPanelOpen: React.Dispatch< + React.SetStateAction + >; configPanelOpen: boolean | undefined; + setConfigPanelOpen: React.Dispatch< + React.SetStateAction + >; searchPanelOpen: boolean | undefined; + setSearchPanelOpen: React.Dispatch< + React.SetStateAction + >; mainPanelReduced: boolean | undefined; - mainPanelOpen: boolean | undefined; - darkMode: boolean | undefined; - selectedThemeMode: boolean | undefined; - fontSize: number | undefined; - dyslexic: boolean | undefined; - currency: string | undefined; - playerName: string | undefined; - preferredLanguages: string[] | undefined; - menuGestures: boolean; - setSubPanelOpen: React.Dispatch>; - setConfigPanelOpen: React.Dispatch>; - setSearchPanelOpen: React.Dispatch>; setMainPanelReduced: React.Dispatch< - React.SetStateAction + React.SetStateAction >; - setMainPanelOpen: React.Dispatch>; - setDarkMode: React.Dispatch>; + mainPanelOpen: boolean | undefined; + setMainPanelOpen: React.Dispatch< + React.SetStateAction + >; + darkMode: boolean | undefined; + setDarkMode: React.Dispatch>; + selectedThemeMode: boolean | undefined; setSelectedThemeMode: React.Dispatch< - React.SetStateAction + React.SetStateAction >; - setFontSize: React.Dispatch>; - setDyslexic: React.Dispatch>; - setCurrency: React.Dispatch>; - setPlayerName: React.Dispatch>; + fontSize: number | undefined; + setFontSize: React.Dispatch>; + dyslexic: boolean | undefined; + setDyslexic: React.Dispatch>; + currency: string | undefined; + setCurrency: React.Dispatch>; + playerName: string | undefined; + setPlayerName: React.Dispatch< + React.SetStateAction + >; + preferredLanguages: string[] | undefined; setPreferredLanguages: React.Dispatch< - React.SetStateAction + React.SetStateAction + >; + menuGestures: boolean; + setMenuGestures: React.Dispatch< + React.SetStateAction + >; + libraryItemUserStatus: Record | undefined; + setLibraryItemUserStatus: React.Dispatch< + React.SetStateAction >; - setMenuGestures: React.Dispatch>; } /* eslint-disable @typescript-eslint/no-empty-function */ @@ -53,6 +69,7 @@ const initialState: AppLayoutState = { playerName: "", preferredLanguages: [], menuGestures: true, + libraryItemUserStatus: {}, setSubPanelOpen: () => {}, setMainPanelReduced: () => {}, setMainPanelOpen: () => {}, @@ -66,6 +83,7 @@ const initialState: AppLayoutState = { setPlayerName: () => {}, setPreferredLanguages: () => {}, setMenuGestures: () => {}, + setLibraryItemUserStatus: () => {}, }; /* eslint-enable @typescript-eslint/no-empty-function */ @@ -82,53 +100,66 @@ interface Props { } export function AppContextProvider(props: Immutable): JSX.Element { - const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage< - boolean | undefined - >("subPanelOpen", initialState.subPanelOpen); + const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage( + "subPanelOpen", + initialState.subPanelOpen + ); - const [configPanelOpen, setConfigPanelOpen] = useStateWithLocalStorage< - boolean | undefined - >("configPanelOpen", initialState.configPanelOpen); + const [configPanelOpen, setConfigPanelOpen] = useStateWithLocalStorage( + "configPanelOpen", + initialState.configPanelOpen + ); - const [mainPanelReduced, setMainPanelReduced] = useStateWithLocalStorage< - boolean | undefined - >("mainPanelReduced", initialState.mainPanelReduced); + const [mainPanelReduced, setMainPanelReduced] = useStateWithLocalStorage( + "mainPanelReduced", + initialState.mainPanelReduced + ); - const [mainPanelOpen, setMainPanelOpen] = useStateWithLocalStorage< - boolean | undefined - >("mainPanelOpen", initialState.mainPanelOpen); + const [mainPanelOpen, setMainPanelOpen] = useStateWithLocalStorage( + "mainPanelOpen", + initialState.mainPanelOpen + ); const [darkMode, selectedThemeMode, setDarkMode, setSelectedThemeMode] = useDarkMode("darkMode", initialState.darkMode); - const [fontSize, setFontSize] = useStateWithLocalStorage( + const [fontSize, setFontSize] = useStateWithLocalStorage( "fontSize", initialState.fontSize ); - const [dyslexic, setDyslexic] = useStateWithLocalStorage( + const [dyslexic, setDyslexic] = useStateWithLocalStorage( "dyslexic", initialState.dyslexic ); - const [currency, setCurrency] = useStateWithLocalStorage( + const [currency, setCurrency] = useStateWithLocalStorage( "currency", initialState.currency ); - const [playerName, setPlayerName] = useStateWithLocalStorage< - string | undefined - >("playerName", initialState.playerName); + const [playerName, setPlayerName] = useStateWithLocalStorage( + "playerName", + initialState.playerName + ); - const [preferredLanguages, setPreferredLanguages] = useStateWithLocalStorage< - string[] | undefined - >("preferredLanguages", initialState.preferredLanguages); + const [preferredLanguages, setPreferredLanguages] = useStateWithLocalStorage( + "preferredLanguages", + initialState.preferredLanguages + ); const [menuGestures, setMenuGestures] = useState(false); - const [searchPanelOpen, setSearchPanelOpen] = useStateWithLocalStorage< - boolean | undefined - >("searchPanelOpen", initialState.searchPanelOpen); + const [searchPanelOpen, setSearchPanelOpen] = useStateWithLocalStorage( + "searchPanelOpen", + initialState.searchPanelOpen + ); + + const [libraryItemUserStatus, setLibraryItemUserStatus] = + useStateWithLocalStorage( + "libraryItemUserStatus", + initialState.libraryItemUserStatus + ); return ( ): JSX.Element { playerName, preferredLanguages, menuGestures, + libraryItemUserStatus, setSubPanelOpen, setConfigPanelOpen, setSearchPanelOpen, @@ -159,6 +191,7 @@ export function AppContextProvider(props: Immutable): JSX.Element { setPlayerName, setPreferredLanguages, setMenuGestures, + setLibraryItemUserStatus, }} > {props.children} diff --git a/src/helpers/libraryItem.ts b/src/helpers/libraryItem.ts new file mode 100644 index 0000000..b1c4900 --- /dev/null +++ b/src/helpers/libraryItem.ts @@ -0,0 +1,251 @@ +import { AppLayoutState } from "contexts/AppLayoutContext"; +import { GetLibraryItemsPreviewQuery } from "graphql/generated"; +import { AppStaticProps } from "graphql/getAppStaticProps"; +import { prettyinlineTitle, prettyDate } from "./formatters"; +import { convertPrice } from "./numbers"; +import { Immutable, LibraryItemUserStatus } from "./types"; +type Items = NonNullable["data"]; +type GroupLibraryItems = Map>; + +export function getGroups( + langui: AppStaticProps["langui"], + groupByType: number, + items: Immutable +): GroupLibraryItems { + switch (groupByType) { + case 0: { + const typeGroup = new Map(); + typeGroup.set("Drakengard 1", []); + typeGroup.set("Drakengard 1.3", []); + typeGroup.set("Drakengard 2", []); + typeGroup.set("Drakengard 3", []); + typeGroup.set("Drakengard 4", []); + typeGroup.set("NieR Gestalt", []); + typeGroup.set("NieR Replicant", []); + typeGroup.set("NieR Replicant ver.1.22474487139...", []); + typeGroup.set("NieR:Automata", []); + typeGroup.set("NieR Re[in]carnation", []); + typeGroup.set("SINoALICE", []); + typeGroup.set("Voice of Cards", []); + typeGroup.set("Final Fantasy XIV", []); + typeGroup.set("Thou Shalt Not Die", []); + typeGroup.set("Bakuken", []); + typeGroup.set("YoRHa", []); + typeGroup.set("YoRHa Boys", []); + typeGroup.set(langui.no_category, []); + + items.map((item) => { + if (item.attributes?.categories?.data.length === 0) { + typeGroup.get(langui.no_category)?.push(item); + } else { + item.attributes?.categories?.data.map((category) => { + typeGroup.get(category.attributes?.name)?.push(item); + }); + } + }); + + return typeGroup; + } + + case 1: { + const group = new Map(); + group.set(langui.audio ?? "Audio", []); + group.set(langui.game ?? "Game", []); + group.set(langui.textual ?? "Textual", []); + group.set(langui.video ?? "Video", []); + group.set(langui.other ?? "Other", []); + group.set(langui.group ?? "Group", []); + group.set(langui.no_type ?? "No type", []); + items.map((item) => { + if (item.attributes?.metadata && item.attributes.metadata.length > 0) { + switch (item.attributes.metadata[0]?.__typename) { + case "ComponentMetadataAudio": + group.get(langui.audio ?? "Audio")?.push(item); + break; + case "ComponentMetadataGame": + group.get(langui.game ?? "Game")?.push(item); + break; + case "ComponentMetadataBooks": + group.get(langui.textual ?? "Textual")?.push(item); + break; + case "ComponentMetadataVideo": + group.get(langui.video ?? "Video")?.push(item); + break; + case "ComponentMetadataOther": + group.get(langui.other ?? "Other")?.push(item); + break; + case "ComponentMetadataGroup": + switch ( + item.attributes.metadata[0]?.subitems_type?.data?.attributes + ?.slug + ) { + case "audio": + group.get(langui.audio ?? "Audio")?.push(item); + break; + case "video": + group.get(langui.video ?? "Video")?.push(item); + break; + case "game": + group.get(langui.game ?? "Game")?.push(item); + break; + case "textual": + group.get(langui.textual ?? "Textual")?.push(item); + break; + case "mixed": + group.get(langui.group ?? "Group")?.push(item); + break; + default: { + throw new Error( + "An unexpected subtype of group-metadata was given" + ); + } + } + break; + default: { + throw new Error("An unexpected type of metadata was given"); + } + } + } else { + group.get(langui.no_type ?? "No type")?.push(item); + } + }); + return group; + } + + case 2: { + const years: number[] = []; + items.map((item) => { + if (item.attributes?.release_date?.year) { + if (!years.includes(item.attributes.release_date.year)) + years.push(item.attributes.release_date.year); + } + }); + const group = new Map(); + years.sort((a, b) => a - b); + years.map((year) => { + group.set(year.toString(), []); + }); + group.set(langui.no_year ?? "No year", []); + items.map((item) => { + if (item.attributes?.release_date?.year) { + group.get(item.attributes.release_date.year.toString())?.push(item); + } else { + group.get(langui.no_year ?? "No year")?.push(item); + } + }); + + return group; + } + + default: { + const group = new Map(); + group.set("", items); + return group; + } + } +} + +export function filterItems( + appLayout: AppLayoutState, + items: Immutable, + searchName: string, + showSubitems: boolean, + showPrimaryItems: boolean, + showSecondaryItems: boolean, + filterUserStatus: LibraryItemUserStatus | undefined +): Immutable { + return [...items].filter((item) => { + 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 ( + searchName.length > 1 && + !prettyinlineTitle("", item.attributes?.title, item.attributes?.subtitle) + .toLowerCase() + .includes(searchName.toLowerCase()) + ) { + return false; + } + + if ( + filterUserStatus !== undefined && + item.id && + appLayout.libraryItemUserStatus + ) { + if (isUntangibleGroupItem(item.attributes?.metadata?.[0])) { + return false; + } + if (filterUserStatus === LibraryItemUserStatus.None) { + if (appLayout.libraryItemUserStatus[item.id]) { + return false; + } + } else if ( + filterUserStatus !== appLayout.libraryItemUserStatus[item.id] + ) { + return false; + } + } + + return true; + }); +} + +// TODO: Properly type this shit +// Best attempt was Immutable["metadata"]>[number]> +export function isUntangibleGroupItem(metadata: any) { + return ( + metadata && + metadata.__typename === "ComponentMetadataGroup" && + (metadata.subtype?.data?.attributes?.slug === "variant-set" || + metadata.subtype?.data?.attributes?.slug === "relation-set") + ); +} + +export function sortBy( + orderByType: number, + items: Immutable, + currencies: AppStaticProps["currencies"] +): Immutable { + switch (orderByType) { + case 0: + return [...items].sort((a, b) => { + const titleA = prettyinlineTitle( + "", + a.attributes?.title, + a.attributes?.subtitle + ); + const titleB = prettyinlineTitle( + "", + b.attributes?.title, + b.attributes?.subtitle + ); + return titleA.localeCompare(titleB); + }); + case 1: + return [...items].sort((a, b) => { + const priceA = a.attributes?.price + ? convertPrice(a.attributes.price, currencies[0]) + : 99999; + const priceB = b.attributes?.price + ? convertPrice(b.attributes.price, currencies[0]) + : 99999; + return priceA - priceB; + }); + case 2: + return [...items].sort((a, b) => { + const dateA = a.attributes?.release_date + ? prettyDate(a.attributes.release_date) + : "9999"; + const dateB = b.attributes?.release_date + ? prettyDate(b.attributes.release_date) + : "9999"; + return dateA.localeCompare(dateB); + }); + default: + return items; + } +} diff --git a/src/helpers/types.ts b/src/helpers/types.ts index eb6fe98..3646a52 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -24,3 +24,9 @@ export type Immutable = { ? T[K] : Immutable; }; + +export enum LibraryItemUserStatus { + None = 0, + Want = 1, + Have = 2, +} diff --git a/src/pages/library/[slug]/index.tsx b/src/pages/library/[slug]/index.tsx index 1501773..319bc06 100644 --- a/src/pages/library/[slug]/index.tsx +++ b/src/pages/library/[slug]/index.tsx @@ -5,6 +5,7 @@ import { Button } from "components/Inputs/Button"; import { Switch } from "components/Inputs/Switch"; import { InsetBox } from "components/InsetBox"; import { ContentLine } from "components/Library/ContentLine"; +import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; import { NavOption } from "components/PanelComponents/NavOption"; import { ReturnButton, @@ -44,6 +45,7 @@ import { GetStaticPropsContext, } from "next"; import { Fragment, useState } from "react"; +import { isUntangibleGroupItem } from "helpers/libraryItem"; interface Props extends AppStaticProps { item: NonNullable< @@ -55,7 +57,7 @@ interface Props extends AppStaticProps { } export default function LibrarySlug(props: Immutable): JSX.Element { - const { item, langui, currencies } = props; + const { item, itemId, langui, currencies } = props; const appLayout = useAppLayout(); useScrollTopOnChange(AnchorIds.ContentPanel, [item]); @@ -169,6 +171,12 @@ export default function LibrarySlug(props: Immutable): JSX.Element {

{item?.title}

{item?.subtitle &&

{item.subtitle}

} + + {item?.descriptions?.[0] && (

{item.descriptions[0].description}

)} @@ -402,7 +410,7 @@ export default function LibrarySlug(props: Immutable): JSX.Element { > {item.subitems.data.map((subitem) => ( - {subitem.attributes && ( + {subitem.attributes && subitem.id && ( ): JSX.Element { price: subitem.attributes.price, position: "Bottom", }} + infoAppend={ + + } /> )} diff --git a/src/pages/library/index.tsx b/src/pages/library/index.tsx index 4781b65..3e1299a 100644 --- a/src/pages/library/index.tsx +++ b/src/pages/library/index.tsx @@ -8,30 +8,32 @@ import { ContentPanelWidthSizes, } from "components/Panels/ContentPanel"; import { SubPanel } from "components/Panels/SubPanel"; -import { PreviewCard } from "components/PreviewCard"; import { GetLibraryItemsPreviewQuery } from "graphql/generated"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { getReadySdk } from "graphql/sdk"; -import { - prettyDate, - prettyinlineTitle, - prettyItemSubType, -} from "helpers/formatters"; -import { convertPrice } from "helpers/numbers"; -import { Immutable } from "helpers/types"; +import { prettyItemSubType } from "helpers/formatters"; +import { Immutable, LibraryItemUserStatus } from "helpers/types"; import { GetStaticPropsContext } from "next"; import { Fragment, useEffect, useState } from "react"; 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 { useAppLayout } from "contexts/AppLayoutContext"; +import { ToolTip } from "components/ToolTip"; +import { + filterItems, + getGroups, + sortBy, + isUntangibleGroupItem, +} from "helpers/libraryItem"; +import { PreviewCard } from "components/PreviewCard"; interface Props extends AppStaticProps { items: NonNullable["data"]; } -type GroupLibraryItems = Map>; - const defaultFiltersState = { searchName: "", showSubitems: false, @@ -40,10 +42,12 @@ const defaultFiltersState = { sortingMethod: 0, groupingMethod: -1, keepInfoVisible: false, + filterUserStatus: undefined, }; export default function Library(props: Immutable): JSX.Element { const { langui, items: libraryItems, currencies } = props; + const appLayout = useAppLayout(); const [searchName, setSearchName] = useState(defaultFiltersState.searchName); const [showSubitems, setShowSubitems] = useState( @@ -64,14 +68,19 @@ export default function Library(props: Immutable): JSX.Element { const [keepInfoVisible, setKeepInfoVisible] = useState( defaultFiltersState.keepInfoVisible ); + const [filterUserStatus, setFilterUserStatus] = useState< + LibraryItemUserStatus | undefined + >(defaultFiltersState.filterUserStatus); const [filteredItems, setFilteredItems] = useState( filterItems( + appLayout, libraryItems, searchName, showSubitems, showPrimaryItems, - showSecondaryItems + showSecondaryItems, + filterUserStatus ) ); @@ -86,11 +95,13 @@ export default function Library(props: Immutable): JSX.Element { useEffect(() => { setFilteredItems( filterItems( + appLayout, libraryItems, searchName, showSubitems, showPrimaryItems, - showSecondaryItems + showSecondaryItems, + filterUserStatus ) ); }, [ @@ -99,6 +110,8 @@ export default function Library(props: Immutable): JSX.Element { showPrimaryItems, showSecondaryItems, searchName, + filterUserStatus, + appLayout, ]); useEffect(() => { @@ -181,6 +194,42 @@ export default function Library(props: Immutable): JSX.Element { input={} /> +
+ {/* TODO: Add to Langui */} + +
+ {/* TODO: Add to Langui */}