Added ability to mark library item as 'Want' or 'have'
This commit is contained in:
		
							parent
							
								
									8b6abd6379
								
							
						
					
					
						commit
						59283fa465
					
				
							
								
								
									
										76
									
								
								src/components/Library/PreviewCardCTAs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/components/Library/PreviewCardCTAs.tsx
									
									
									
									
									
										Normal 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> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @ -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} | ||||
|  | ||||
							
								
								
									
										251
									
								
								src/helpers/libraryItem.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/helpers/libraryItem.ts
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 DrMint
						DrMint