Added ability to mark library item as 'Want' or 'have'
This commit is contained in:
parent
8b6abd6379
commit
59283fa465
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -24,3 +24,9 @@ export type Immutable<T> = {
|
|||
? T[K]
|
||||
: Immutable<T[K]>;
|
||||
};
|
||||
|
||||
export enum LibraryItemUserStatus {
|
||||
None = 0,
|
||||
Want = 1,
|
||||
Have = 2,
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 “I want”">
|
||||
<Button
|
||||
className="rounded-r-none"
|
||||
icon={Icon.Favorite}
|
||||
onClick={() => setFilterUserStatus(LibraryItemUserStatus.Want)}
|
||||
active={filterUserStatus === LibraryItemUserStatus.Want}
|
||||
/>
|
||||
</ToolTip>
|
||||
<ToolTip content="Only display items marked as “I have”">
|
||||
<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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue