Added ability to mark library item as 'Want' or 'have'

This commit is contained in:
DrMint 2022-05-28 19:33:10 +02:00
parent 8b6abd6379
commit 59283fa465
7 changed files with 511 additions and 281 deletions

View File

@ -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 && (
<div
className={`flex flex-row place-content-center place-items-center ${
expand ? "gap-4" : "gap-2"
}`}
>
{/* TODO: Add to langui */}
<ToolTip content="I want it!">
<Button
icon={Icon.Favorite}
text={expand ? "I want it!" : undefined}
active={
appLayout.libraryItemUserStatus?.[id] ===
LibraryItemUserStatus.Want
}
onClick={(event) => {
event.preventDefault();
appLayout.setLibraryItemUserStatus((current) => {
const newLibraryItemUserStatus = current
? { ...current }
: {};
newLibraryItemUserStatus[id] =
newLibraryItemUserStatus[id] === LibraryItemUserStatus.Want
? LibraryItemUserStatus.None
: LibraryItemUserStatus.Want;
return newLibraryItemUserStatus;
});
}}
/>
</ToolTip>
<ToolTip content="I have it!">
<Button
icon={Icon.BackHand}
text={expand ? "I have it!" : undefined}
active={
appLayout.libraryItemUserStatus?.[id] ===
LibraryItemUserStatus.Have
}
onClick={(event) => {
event.preventDefault();
appLayout.setLibraryItemUserStatus((current) => {
const newLibraryItemUserStatus = current
? { ...current }
: {};
newLibraryItemUserStatus[id] =
newLibraryItemUserStatus[id] === LibraryItemUserStatus.Have
? LibraryItemUserStatus.None
: LibraryItemUserStatus.Have;
return newLibraryItemUserStatus;
});
}}
/>
</ToolTip>
</div>
)}
</>
);
}

View File

@ -38,6 +38,7 @@ interface Props {
author?: string; author?: string;
position: "Bottom" | "Top"; position: "Bottom" | "Top";
}; };
infoAppend?: React.ReactNode;
hoverlay?: hoverlay?:
| { | {
__typename: "Video"; __typename: "Video";
@ -61,6 +62,7 @@ export function PreviewCard(props: Immutable<Props>): JSX.Element {
thumbnailAspectRatio, thumbnailAspectRatio,
metadata, metadata,
hoverlay, hoverlay,
infoAppend,
} = props; } = props;
const appLayout = useAppLayout(); const appLayout = useAppLayout();
@ -251,6 +253,8 @@ export function PreviewCard(props: Immutable<Props>): JSX.Element {
)} )}
{metadata?.position === "Bottom" && metadataJSX} {metadata?.position === "Bottom" && metadataJSX}
{infoAppend}
</div> </div>
</div> </div>
</Link> </Link>

View File

@ -1,41 +1,57 @@
import { Immutable } from "helpers/types"; import { Immutable, LibraryItemUserStatus } from "helpers/types";
import { useDarkMode } from "hooks/useDarkMode"; import { useDarkMode } from "hooks/useDarkMode";
import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage"; import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage";
import React, { ReactNode, useContext, useState } from "react"; import React, { ReactNode, useContext, useState } from "react";
interface AppLayoutState { export interface AppLayoutState {
subPanelOpen: boolean | undefined; subPanelOpen: boolean | undefined;
setSubPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["subPanelOpen"]>
>;
configPanelOpen: boolean | undefined; configPanelOpen: boolean | undefined;
setConfigPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["configPanelOpen"]>
>;
searchPanelOpen: boolean | undefined; searchPanelOpen: boolean | undefined;
setSearchPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["searchPanelOpen"]>
>;
mainPanelReduced: boolean | undefined; 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<React.SetStateAction<boolean | undefined>>;
setConfigPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setSearchPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setMainPanelReduced: React.Dispatch< setMainPanelReduced: React.Dispatch<
React.SetStateAction<boolean | undefined> React.SetStateAction<AppLayoutState["mainPanelReduced"]>
>; >;
setMainPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>; mainPanelOpen: boolean | undefined;
setDarkMode: React.Dispatch<React.SetStateAction<boolean | undefined>>; setMainPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["mainPanelOpen"]>
>;
darkMode: boolean | undefined;
setDarkMode: React.Dispatch<React.SetStateAction<AppLayoutState["darkMode"]>>;
selectedThemeMode: boolean | undefined;
setSelectedThemeMode: React.Dispatch< setSelectedThemeMode: React.Dispatch<
React.SetStateAction<boolean | undefined> React.SetStateAction<AppLayoutState["selectedThemeMode"]>
>; >;
setFontSize: React.Dispatch<React.SetStateAction<number | undefined>>; fontSize: number | undefined;
setDyslexic: React.Dispatch<React.SetStateAction<boolean | undefined>>; setFontSize: React.Dispatch<React.SetStateAction<AppLayoutState["fontSize"]>>;
setCurrency: React.Dispatch<React.SetStateAction<string | undefined>>; dyslexic: boolean | undefined;
setPlayerName: React.Dispatch<React.SetStateAction<string | undefined>>; setDyslexic: React.Dispatch<React.SetStateAction<AppLayoutState["dyslexic"]>>;
currency: string | undefined;
setCurrency: React.Dispatch<React.SetStateAction<AppLayoutState["currency"]>>;
playerName: string | undefined;
setPlayerName: React.Dispatch<
React.SetStateAction<AppLayoutState["playerName"]>
>;
preferredLanguages: string[] | undefined;
setPreferredLanguages: React.Dispatch< setPreferredLanguages: React.Dispatch<
React.SetStateAction<string[] | undefined> React.SetStateAction<AppLayoutState["preferredLanguages"]>
>;
menuGestures: boolean;
setMenuGestures: React.Dispatch<
React.SetStateAction<AppLayoutState["menuGestures"]>
>;
libraryItemUserStatus: Record<string, LibraryItemUserStatus> | undefined;
setLibraryItemUserStatus: React.Dispatch<
React.SetStateAction<AppLayoutState["libraryItemUserStatus"]>
>; >;
setMenuGestures: React.Dispatch<React.SetStateAction<boolean>>;
} }
/* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-empty-function */
@ -53,6 +69,7 @@ const initialState: AppLayoutState = {
playerName: "", playerName: "",
preferredLanguages: [], preferredLanguages: [],
menuGestures: true, menuGestures: true,
libraryItemUserStatus: {},
setSubPanelOpen: () => {}, setSubPanelOpen: () => {},
setMainPanelReduced: () => {}, setMainPanelReduced: () => {},
setMainPanelOpen: () => {}, setMainPanelOpen: () => {},
@ -66,6 +83,7 @@ const initialState: AppLayoutState = {
setPlayerName: () => {}, setPlayerName: () => {},
setPreferredLanguages: () => {}, setPreferredLanguages: () => {},
setMenuGestures: () => {}, setMenuGestures: () => {},
setLibraryItemUserStatus: () => {},
}; };
/* eslint-enable @typescript-eslint/no-empty-function */ /* eslint-enable @typescript-eslint/no-empty-function */
@ -82,53 +100,66 @@ interface Props {
} }
export function AppContextProvider(props: Immutable<Props>): JSX.Element { export function AppContextProvider(props: Immutable<Props>): JSX.Element {
const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage< const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage(
boolean | undefined "subPanelOpen",
>("subPanelOpen", initialState.subPanelOpen); initialState.subPanelOpen
);
const [configPanelOpen, setConfigPanelOpen] = useStateWithLocalStorage< const [configPanelOpen, setConfigPanelOpen] = useStateWithLocalStorage(
boolean | undefined "configPanelOpen",
>("configPanelOpen", initialState.configPanelOpen); initialState.configPanelOpen
);
const [mainPanelReduced, setMainPanelReduced] = useStateWithLocalStorage< const [mainPanelReduced, setMainPanelReduced] = useStateWithLocalStorage(
boolean | undefined "mainPanelReduced",
>("mainPanelReduced", initialState.mainPanelReduced); initialState.mainPanelReduced
);
const [mainPanelOpen, setMainPanelOpen] = useStateWithLocalStorage< const [mainPanelOpen, setMainPanelOpen] = useStateWithLocalStorage(
boolean | undefined "mainPanelOpen",
>("mainPanelOpen", initialState.mainPanelOpen); initialState.mainPanelOpen
);
const [darkMode, selectedThemeMode, setDarkMode, setSelectedThemeMode] = const [darkMode, selectedThemeMode, setDarkMode, setSelectedThemeMode] =
useDarkMode("darkMode", initialState.darkMode); useDarkMode("darkMode", initialState.darkMode);
const [fontSize, setFontSize] = useStateWithLocalStorage<number | undefined>( const [fontSize, setFontSize] = useStateWithLocalStorage(
"fontSize", "fontSize",
initialState.fontSize initialState.fontSize
); );
const [dyslexic, setDyslexic] = useStateWithLocalStorage<boolean | undefined>( const [dyslexic, setDyslexic] = useStateWithLocalStorage(
"dyslexic", "dyslexic",
initialState.dyslexic initialState.dyslexic
); );
const [currency, setCurrency] = useStateWithLocalStorage<string | undefined>( const [currency, setCurrency] = useStateWithLocalStorage(
"currency", "currency",
initialState.currency initialState.currency
); );
const [playerName, setPlayerName] = useStateWithLocalStorage< const [playerName, setPlayerName] = useStateWithLocalStorage(
string | undefined "playerName",
>("playerName", initialState.playerName); initialState.playerName
);
const [preferredLanguages, setPreferredLanguages] = useStateWithLocalStorage< const [preferredLanguages, setPreferredLanguages] = useStateWithLocalStorage(
string[] | undefined "preferredLanguages",
>("preferredLanguages", initialState.preferredLanguages); initialState.preferredLanguages
);
const [menuGestures, setMenuGestures] = useState(false); const [menuGestures, setMenuGestures] = useState(false);
const [searchPanelOpen, setSearchPanelOpen] = useStateWithLocalStorage< const [searchPanelOpen, setSearchPanelOpen] = useStateWithLocalStorage(
boolean | undefined "searchPanelOpen",
>("searchPanelOpen", initialState.searchPanelOpen); initialState.searchPanelOpen
);
const [libraryItemUserStatus, setLibraryItemUserStatus] =
useStateWithLocalStorage(
"libraryItemUserStatus",
initialState.libraryItemUserStatus
);
return ( return (
<AppContext.Provider <AppContext.Provider
@ -146,6 +177,7 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
playerName, playerName,
preferredLanguages, preferredLanguages,
menuGestures, menuGestures,
libraryItemUserStatus,
setSubPanelOpen, setSubPanelOpen,
setConfigPanelOpen, setConfigPanelOpen,
setSearchPanelOpen, setSearchPanelOpen,
@ -159,6 +191,7 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
setPlayerName, setPlayerName,
setPreferredLanguages, setPreferredLanguages,
setMenuGestures, setMenuGestures,
setLibraryItemUserStatus,
}} }}
> >
{props.children} {props.children}

251
src/helpers/libraryItem.ts Normal file
View File

@ -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<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
type GroupLibraryItems = Map<string, Immutable<Items>>;
export function getGroups(
langui: AppStaticProps["langui"],
groupByType: number,
items: Immutable<Items>
): 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<Items>,
searchName: string,
showSubitems: boolean,
showPrimaryItems: boolean,
showSecondaryItems: boolean,
filterUserStatus: LibraryItemUserStatus | undefined
): Immutable<Items> {
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<NonNullable<NonNullable<Items[number]["attributes"]>["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<Items>,
currencies: AppStaticProps["currencies"]
): Immutable<Items> {
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;
}
}

View File

@ -24,3 +24,9 @@ export type Immutable<T> = {
? T[K] ? T[K]
: Immutable<T[K]>; : Immutable<T[K]>;
}; };
export enum LibraryItemUserStatus {
None = 0,
Want = 1,
Have = 2,
}

View File

@ -5,6 +5,7 @@ import { Button } from "components/Inputs/Button";
import { Switch } from "components/Inputs/Switch"; import { Switch } from "components/Inputs/Switch";
import { InsetBox } from "components/InsetBox"; import { InsetBox } from "components/InsetBox";
import { ContentLine } from "components/Library/ContentLine"; import { ContentLine } from "components/Library/ContentLine";
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
import { NavOption } from "components/PanelComponents/NavOption"; import { NavOption } from "components/PanelComponents/NavOption";
import { import {
ReturnButton, ReturnButton,
@ -44,6 +45,7 @@ import {
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { Fragment, useState } from "react"; import { Fragment, useState } from "react";
import { isUntangibleGroupItem } from "helpers/libraryItem";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
item: NonNullable< item: NonNullable<
@ -55,7 +57,7 @@ interface Props extends AppStaticProps {
} }
export default function LibrarySlug(props: Immutable<Props>): JSX.Element { export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
const { item, langui, currencies } = props; const { item, itemId, langui, currencies } = props;
const appLayout = useAppLayout(); const appLayout = useAppLayout();
useScrollTopOnChange(AnchorIds.ContentPanel, [item]); useScrollTopOnChange(AnchorIds.ContentPanel, [item]);
@ -169,6 +171,12 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
<h1 className="text-3xl">{item?.title}</h1> <h1 className="text-3xl">{item?.title}</h1>
{item?.subtitle && <h2 className="text-2xl">{item.subtitle}</h2>} {item?.subtitle && <h2 className="text-2xl">{item.subtitle}</h2>}
</div> </div>
<PreviewCardCTAs
id={itemId}
displayCTAs={!isUntangibleGroupItem(item?.metadata?.[0])}
expand
/>
{item?.descriptions?.[0] && ( {item?.descriptions?.[0] && (
<p className="text-justify">{item.descriptions[0].description}</p> <p className="text-justify">{item.descriptions[0].description}</p>
)} )}
@ -402,7 +410,7 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
> >
{item.subitems.data.map((subitem) => ( {item.subitems.data.map((subitem) => (
<Fragment key={subitem.id}> <Fragment key={subitem.id}>
{subitem.attributes && ( {subitem.attributes && subitem.id && (
<PreviewCard <PreviewCard
href={`/library/${subitem.attributes.slug}`} href={`/library/${subitem.attributes.slug}`}
title={subitem.attributes.title} title={subitem.attributes.title}
@ -426,6 +434,16 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
price: subitem.attributes.price, price: subitem.attributes.price,
position: "Bottom", position: "Bottom",
}} }}
infoAppend={
<PreviewCardCTAs
id={subitem.id}
displayCTAs={
!isUntangibleGroupItem(
subitem.attributes.metadata?.[0]
)
}
/>
}
/> />
)} )}
</Fragment> </Fragment>

View File

@ -8,30 +8,32 @@ import {
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import { SubPanel } from "components/Panels/SubPanel"; import { SubPanel } from "components/Panels/SubPanel";
import { PreviewCard } from "components/PreviewCard";
import { GetLibraryItemsPreviewQuery } from "graphql/generated"; import { GetLibraryItemsPreviewQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { import { prettyItemSubType } from "helpers/formatters";
prettyDate, import { Immutable, LibraryItemUserStatus } from "helpers/types";
prettyinlineTitle,
prettyItemSubType,
} from "helpers/formatters";
import { convertPrice } from "helpers/numbers";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel"; 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 { 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 { interface Props extends AppStaticProps {
items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"]; items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
} }
type GroupLibraryItems = Map<string, Immutable<Props["items"]>>;
const defaultFiltersState = { const defaultFiltersState = {
searchName: "", searchName: "",
showSubitems: false, showSubitems: false,
@ -40,10 +42,12 @@ const defaultFiltersState = {
sortingMethod: 0, sortingMethod: 0,
groupingMethod: -1, groupingMethod: -1,
keepInfoVisible: false, keepInfoVisible: false,
filterUserStatus: undefined,
}; };
export default function Library(props: Immutable<Props>): JSX.Element { export default function Library(props: Immutable<Props>): JSX.Element {
const { langui, items: libraryItems, currencies } = props; const { langui, items: libraryItems, currencies } = props;
const appLayout = useAppLayout();
const [searchName, setSearchName] = useState(defaultFiltersState.searchName); const [searchName, setSearchName] = useState(defaultFiltersState.searchName);
const [showSubitems, setShowSubitems] = useState<boolean>( const [showSubitems, setShowSubitems] = useState<boolean>(
@ -64,14 +68,19 @@ export default function Library(props: Immutable<Props>): JSX.Element {
const [keepInfoVisible, setKeepInfoVisible] = useState( const [keepInfoVisible, setKeepInfoVisible] = useState(
defaultFiltersState.keepInfoVisible defaultFiltersState.keepInfoVisible
); );
const [filterUserStatus, setFilterUserStatus] = useState<
LibraryItemUserStatus | undefined
>(defaultFiltersState.filterUserStatus);
const [filteredItems, setFilteredItems] = useState( const [filteredItems, setFilteredItems] = useState(
filterItems( filterItems(
appLayout,
libraryItems, libraryItems,
searchName, searchName,
showSubitems, showSubitems,
showPrimaryItems, showPrimaryItems,
showSecondaryItems showSecondaryItems,
filterUserStatus
) )
); );
@ -86,11 +95,13 @@ export default function Library(props: Immutable<Props>): JSX.Element {
useEffect(() => { useEffect(() => {
setFilteredItems( setFilteredItems(
filterItems( filterItems(
appLayout,
libraryItems, libraryItems,
searchName, searchName,
showSubitems, showSubitems,
showPrimaryItems, showPrimaryItems,
showSecondaryItems showSecondaryItems,
filterUserStatus
) )
); );
}, [ }, [
@ -99,6 +110,8 @@ export default function Library(props: Immutable<Props>): JSX.Element {
showPrimaryItems, showPrimaryItems,
showSecondaryItems, showSecondaryItems,
searchName, searchName,
filterUserStatus,
appLayout,
]); ]);
useEffect(() => { useEffect(() => {
@ -181,6 +194,42 @@ export default function Library(props: Immutable<Props>): JSX.Element {
input={<Switch state={keepInfoVisible} setState={setKeepInfoVisible} />} input={<Switch state={keepInfoVisible} setState={setKeepInfoVisible} />}
/> />
<div className="mt-4 grid grid-flow-col">
{/* TODO: Add to Langui */}
<ToolTip content="Only display items marked as &ldquo;I want&rdquo;">
<Button
className="rounded-r-none"
icon={Icon.Favorite}
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)}
active={filterUserStatus === LibraryItemUserStatus.Want}
/>
</ToolTip>
<ToolTip content="Only display items marked as &ldquo;I have&rdquo;">
<Button
className="rounded-none border-l-0"
icon={Icon.BackHand}
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Have)}
active={filterUserStatus === LibraryItemUserStatus.Have}
/>
</ToolTip>
<ToolTip content="Only display unmarked items">
<Button
className="rounded-none border-l-0"
icon={Icon.RadioButtonUnchecked}
onClick={() => setFilterUserStatus(LibraryItemUserStatus.None)}
active={filterUserStatus === LibraryItemUserStatus.None}
/>
</ToolTip>
<ToolTip content="Display all items">
<Button
className="rounded-l-none border-l-0"
text={"All"}
onClick={() => setFilterUserStatus(undefined)}
active={filterUserStatus === undefined}
/>
</ToolTip>
</div>
{/* TODO: Add to Langui */} {/* TODO: Add to Langui */}
<Button <Button
className="mt-8" className="mt-8"
@ -194,6 +243,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
setSortingMethod(defaultFiltersState.sortingMethod); setSortingMethod(defaultFiltersState.sortingMethod);
setGroupingMethod(defaultFiltersState.groupingMethod); setGroupingMethod(defaultFiltersState.groupingMethod);
setKeepInfoVisible(defaultFiltersState.keepInfoVisible); setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
setFilterUserStatus(defaultFiltersState.filterUserStatus);
}} }}
/> />
</SubPanel> </SubPanel>
@ -224,7 +274,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
> >
{items.map((item) => ( {items.map((item) => (
<Fragment key={item.id}> <Fragment key={item.id}>
{item.attributes && ( {item.id && item.attributes && (
<PreviewCard <PreviewCard
href={`/library/${item.attributes.slug}`} href={`/library/${item.attributes.slug}`}
title={item.attributes.title} title={item.attributes.title}
@ -248,6 +298,16 @@ export default function Library(props: Immutable<Props>): JSX.Element {
price: item.attributes.price, price: item.attributes.price,
position: "Bottom", position: "Bottom",
}} }}
infoAppend={
<PreviewCardCTAs
id={item.id}
displayCTAs={
!isUntangibleGroupItem(
item.attributes.metadata?.[0]
)
}
/>
}
/> />
)} )}
</Fragment> </Fragment>
@ -286,221 +346,3 @@ export async function getStaticProps(
props: props, props: props,
}; };
} }
function getGroups(
langui: AppStaticProps["langui"],
groupByType: number,
items: Immutable<Props["items"]>
): 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;
}
}
}
function filterItems(
items: Immutable<Props["items"]>,
searchName: string,
showSubitems: boolean,
showPrimaryItems: boolean,
showSecondaryItems: boolean
): Immutable<Props["items"]> {
return [...items].filter((item) => {
if (!showSubitems && !item.attributes?.root_item) return false;
if (
showSubitems &&
item.attributes?.metadata?.[0]?.__typename === "ComponentMetadataGroup" &&
(item.attributes.metadata[0].subtype?.data?.attributes?.slug ===
"variant-set" ||
item.attributes.metadata[0].subtype?.data?.attributes?.slug ===
"relation-set")
) {
return false;
}
if (item.attributes?.primary && !showPrimaryItems) return false;
if (!item.attributes?.primary && !showSecondaryItems) return false;
if (searchName.length > 1) {
if (
prettyinlineTitle("", item.attributes?.title, item.attributes?.subtitle)
.toLowerCase()
.includes(searchName.toLowerCase())
) {
return true;
}
return false;
}
return true;
});
}
function sortBy(
orderByType: number,
items: Immutable<Props["items"]>,
currencies: AppStaticProps["currencies"]
): Immutable<Props["items"]> {
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;
}
}