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

View File

@ -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<AppLayoutState["subPanelOpen"]>
>;
configPanelOpen: boolean | undefined;
setConfigPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["configPanelOpen"]>
>;
searchPanelOpen: boolean | undefined;
setSearchPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["searchPanelOpen"]>
>;
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<
React.SetStateAction<boolean | undefined>
React.SetStateAction<AppLayoutState["mainPanelReduced"]>
>;
setMainPanelOpen: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setDarkMode: React.Dispatch<React.SetStateAction<boolean | undefined>>;
mainPanelOpen: 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<
React.SetStateAction<boolean | undefined>
React.SetStateAction<AppLayoutState["selectedThemeMode"]>
>;
setFontSize: React.Dispatch<React.SetStateAction<number | undefined>>;
setDyslexic: React.Dispatch<React.SetStateAction<boolean | undefined>>;
setCurrency: React.Dispatch<React.SetStateAction<string | undefined>>;
setPlayerName: React.Dispatch<React.SetStateAction<string | undefined>>;
fontSize: number | undefined;
setFontSize: React.Dispatch<React.SetStateAction<AppLayoutState["fontSize"]>>;
dyslexic: boolean | 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<
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 */
@ -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<Props>): 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<number | undefined>(
const [fontSize, setFontSize] = useStateWithLocalStorage(
"fontSize",
initialState.fontSize
);
const [dyslexic, setDyslexic] = useStateWithLocalStorage<boolean | undefined>(
const [dyslexic, setDyslexic] = useStateWithLocalStorage(
"dyslexic",
initialState.dyslexic
);
const [currency, setCurrency] = useStateWithLocalStorage<string | undefined>(
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 (
<AppContext.Provider
@ -146,6 +177,7 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
playerName,
preferredLanguages,
menuGestures,
libraryItemUserStatus,
setSubPanelOpen,
setConfigPanelOpen,
setSearchPanelOpen,
@ -159,6 +191,7 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
setPlayerName,
setPreferredLanguages,
setMenuGestures,
setLibraryItemUserStatus,
}}
>
{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]
: 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 { 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<Props>): 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<Props>): JSX.Element {
<h1 className="text-3xl">{item?.title}</h1>
{item?.subtitle && <h2 className="text-2xl">{item.subtitle}</h2>}
</div>
<PreviewCardCTAs
id={itemId}
displayCTAs={!isUntangibleGroupItem(item?.metadata?.[0])}
expand
/>
{item?.descriptions?.[0] && (
<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) => (
<Fragment key={subitem.id}>
{subitem.attributes && (
{subitem.attributes && subitem.id && (
<PreviewCard
href={`/library/${subitem.attributes.slug}`}
title={subitem.attributes.title}
@ -426,6 +434,16 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
price: subitem.attributes.price,
position: "Bottom",
}}
infoAppend={
<PreviewCardCTAs
id={subitem.id}
displayCTAs={
!isUntangibleGroupItem(
subitem.attributes.metadata?.[0]
)
}
/>
}
/>
)}
</Fragment>

View File

@ -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<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
}
type GroupLibraryItems = Map<string, Immutable<Props["items"]>>;
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<Props>): JSX.Element {
const { langui, items: libraryItems, currencies } = props;
const appLayout = useAppLayout();
const [searchName, setSearchName] = useState(defaultFiltersState.searchName);
const [showSubitems, setShowSubitems] = useState<boolean>(
@ -64,14 +68,19 @@ export default function Library(props: Immutable<Props>): 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<Props>): JSX.Element {
useEffect(() => {
setFilteredItems(
filterItems(
appLayout,
libraryItems,
searchName,
showSubitems,
showPrimaryItems,
showSecondaryItems
showSecondaryItems,
filterUserStatus
)
);
}, [
@ -99,6 +110,8 @@ export default function Library(props: Immutable<Props>): JSX.Element {
showPrimaryItems,
showSecondaryItems,
searchName,
filterUserStatus,
appLayout,
]);
useEffect(() => {
@ -181,6 +194,42 @@ export default function Library(props: Immutable<Props>): JSX.Element {
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 */}
<Button
className="mt-8"
@ -194,6 +243,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
setSortingMethod(defaultFiltersState.sortingMethod);
setGroupingMethod(defaultFiltersState.groupingMethod);
setKeepInfoVisible(defaultFiltersState.keepInfoVisible);
setFilterUserStatus(defaultFiltersState.filterUserStatus);
}}
/>
</SubPanel>
@ -224,7 +274,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
>
{items.map((item) => (
<Fragment key={item.id}>
{item.attributes && (
{item.id && item.attributes && (
<PreviewCard
href={`/library/${item.attributes.slug}`}
title={item.attributes.title}
@ -248,6 +298,16 @@ export default function Library(props: Immutable<Props>): JSX.Element {
price: item.attributes.price,
position: "Bottom",
}}
infoAppend={
<PreviewCardCTAs
id={item.id}
displayCTAs={
!isUntangibleGroupItem(
item.attributes.metadata?.[0]
)
}
/>
}
/>
)}
</Fragment>
@ -286,221 +346,3 @@ export async function getStaticProps(
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;
}
}