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 */}