Added support for filtering Library items

This commit is contained in:
DrMint 2022-03-04 23:21:42 +01:00
parent 67aa30c2f7
commit 8684640ef4
3 changed files with 275 additions and 6 deletions

87
src/components/Select.tsx Normal file
View File

@ -0,0 +1,87 @@
import { useEffect, useState } from "react";
export type SelectProps = {
options: SelectOption[];
selected?: number;
allowEmpty?: boolean;
className?: string;
onChange?: Function;
};
export type SelectOption = {
name: string;
label: string;
};
export function selectOptionsIncludes(
options: SelectOption[],
newOption: SelectOption
) {
options.map((option) => {
if (option.label === newOption.label) return true;
});
return false;
}
export default function Select(props: SelectProps): JSX.Element {
const [selected, setSelected] = useState(
props.selected ? props.selected : props.allowEmpty ? -1 : 0
);
const [opened, setOpened] = useState(false);
return (
<div
className={`relative transition-[filter] ${
opened && "drop-shadow-shade-lg z-10"
} ${props.className}`}
>
<div
className={`outline outline-mid outline-2 outline-offset-[-2px] hover:outline-[transparent] bg-light rounded-[1em] p-1 grid grid-flow-col grid-cols-[1fr_auto_auto] place-items-center cursor-pointer hover:bg-mid transition-all ${
opened && "outline-[transparent] rounded-b-none"
}`}
>
<p onClick={() => setOpened(!opened)} className="w-full">
{selected === -1 ? "—" : props.options[selected].label}
</p>
{selected >= 0 && props.allowEmpty && (
<span
onClick={() => {
setSelected(-1);
props.onChange && props.onChange("");
}}
className="material-icons !text-xs"
>
close
</span>
)}
<span onClick={() => setOpened(!opened)} className="material-icons">
{opened ? "arrow_drop_up" : "arrow_drop_down"}
</span>
</div>
<div
className={`left-0 right-0 rounded-b-[1em] ${
opened ? "absolute" : "hidden"
}`}
>
{props.options.map((option, index) => (
<>
{index !== selected && (
<div
className="bg-light hover:bg-mid transition-colors cursor-pointer p-1 last-of-type:rounded-b-[1em]"
key={option.name}
id={option.name}
onClick={() => {
setOpened(false);
setSelected(index);
props.onChange && props.onChange(props.options[index].name);
}}
>
{option.label}
</div>
)}
</>
))}
</div>
</div>
);
}

View File

@ -128,7 +128,6 @@ query getLibraryItemsPreview($language_code: String) {
libraryItems( libraryItems(
filters: { root_item: { eq: true } } filters: { root_item: { eq: true } }
pagination: { limit: -1 } pagination: { limit: -1 }
sort: ["title:asc", "subtitle:asc"]
) { ) {
data { data {
id id

View File

@ -14,14 +14,43 @@ import {
import PanelHeader from "components/PanelComponents/PanelHeader"; import PanelHeader from "components/PanelComponents/PanelHeader";
import AppLayout from "components/AppLayout"; import AppLayout from "components/AppLayout";
import LibraryItemsPreview from "components/Library/LibraryItemsPreview"; import LibraryItemsPreview from "components/Library/LibraryItemsPreview";
import Select from "components/Select";
import { useEffect, useState } from "react";
import { prettyDate, prettyinlineTitle } from "queries/helpers";
type LibraryProps = { type LibraryProps = {
libraryItems: GetLibraryItemsPreviewQuery; libraryItems: GetLibraryItemsPreviewQuery;
langui: GetWebsiteInterfaceQuery; langui: GetWebsiteInterfaceQuery;
}; };
type GroupLibraryItems = Map<
string,
GetLibraryItemsPreviewQuery["libraryItems"]["data"]
>;
export default function Library(props: LibraryProps): JSX.Element { export default function Library(props: LibraryProps): JSX.Element {
const langui = props.langui.websiteInterfaces.data[0].attributes; const langui = props.langui.websiteInterfaces.data[0].attributes;
const [sortedItems, setSortedItem] = useState<
LibraryProps["libraryItems"]["libraryItems"]["data"]
>(sortBy("title", props.libraryItems.libraryItems.data));
const [sortingMethod, setSortingMethod] = useState<string>("title");
const [groups, setGroups] = useState<GroupLibraryItems>(
getGroups("", sortedItems)
);
const [groupingMethod, setGroupingMethod] = useState<string>("");
useEffect(() => {
setSortedItem(sortBy(sortingMethod, props.libraryItems.libraryItems.data));
}, [props.libraryItems.libraryItems.data, sortingMethod]);
useEffect(() => {
setGroups(getGroups(groupingMethod, sortedItems));
}, [groupingMethod, sortedItems]);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
<PanelHeader <PanelHeader
@ -29,15 +58,54 @@ export default function Library(props: LibraryProps): JSX.Element {
title={langui.main_library} title={langui.main_library}
description={langui.library_description} description={langui.library_description}
/> />
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">Group by:</p>
<Select
className="w-full"
options={[
{ name: "category", label: "Category" },
{ name: "type", label: "Type" },
{ name: "releaseYear", label: "Release year" },
]}
onChange={setGroupingMethod}
allowEmpty
/>
</div>
<div className="flex flex-row gap-2 place-items-center">
<p className="flex-shrink-0">Order by:</p>
<Select
className="w-full"
options={[
{ name: "title", label: "Title" },
{ name: "releaseDate", label: "Release date" },
{ name: "price", label: "Price" },
]}
onChange={setSortingMethod}
/>
</div>
</SubPanel> </SubPanel>
); );
const contentPanel = ( const contentPanel = (
<ContentPanel width={ContentPanelWidthSizes.large}> <ContentPanel width={ContentPanelWidthSizes.large}>
<div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]"> {[...groups].map(([name, items]) => (
{props.libraryItems.libraryItems.data.map((item) => ( <>
{items.length > 0 && (
<>
<h2 className="text-2xl pb-2">{name}</h2>
<div
key={name}
className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))] pb-12"
>
{items.map((item) => (
<LibraryItemsPreview key={item.id} item={item.attributes} /> <LibraryItemsPreview key={item.id} item={item.attributes} />
))} ))}
</div> </div>
</>
)}
</>
))}
</ContentPanel> </ContentPanel>
); );
return ( return (
@ -67,3 +135,118 @@ export const getStaticProps: GetStaticProps = async (context) => {
return { props: {} }; return { props: {} };
} }
}; };
function getGroups(
groupByType: string,
items: LibraryProps["libraryItems"]["libraryItems"]["data"]
): GroupLibraryItems {
switch (groupByType) {
case "category":
return new Map();
case "type":
const groupType: GroupLibraryItems = new Map();
groupType.set("Audio", []);
groupType.set("Game", []);
groupType.set("Textual", []);
groupType.set("Video", []);
groupType.set("Other", []);
groupType.set("No type", []);
items.map((item) => {
if (item.attributes.metadata.length > 0) {
switch (item.attributes.metadata[0].__typename) {
case "ComponentMetadataAudio":
groupType.get("Audio")?.push(item);
break;
case "ComponentMetadataGame":
groupType.get("Game")?.push(item);
break;
case "ComponentMetadataBooks":
groupType.get("Textual")?.push(item);
break;
case "ComponentMetadataVideo":
groupType.get("Video")?.push(item);
break;
case "ComponentMetadataOther":
groupType.get("Other")?.push(item);
break;
}
} else {
groupType.get("No type")?.push(item);
}
});
return groupType;
case "releaseYear":
const years: number[] = [];
items.map((item) => {
if (item.attributes.release_date) {
if (!years.includes(item.attributes.release_date.year))
years.push(item.attributes.release_date.year);
}
});
const groupYear: GroupLibraryItems = new Map();
years.sort();
years.map((year) => {
groupYear.set(year.toString(), []);
});
groupYear.set("No year", []);
items.map((item) => {
if (item.attributes.release_date) {
groupYear
.get(item.attributes.release_date.year.toString())
?.push(item);
} else {
groupYear.get("No year")?.push(item);
}
});
return groupYear;
default:
const groupDefault: GroupLibraryItems = new Map();
groupDefault.set("", items);
return groupDefault;
}
}
function sortBy(
orderByType: string,
items: LibraryProps["libraryItems"]["libraryItems"]["data"]
): LibraryProps["libraryItems"]["libraryItems"]["data"] {
switch (orderByType) {
case "title":
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 "price":
return [...items].sort((a, b) => {
const priceA = a.attributes.price ? a.attributes.price.amount : 99999;
const priceB = b.attributes.price ? b.attributes.price.amount : 99999;
return priceA - priceB;
});
case "releaseDate":
return [...items].sort((a, b) => {
const dateA =
a.attributes.release_date !== null
? prettyDate(a.attributes.release_date)
: "9999";
const dateB =
b.attributes.release_date !== null
? prettyDate(b.attributes.release_date)
: "9999";
return dateA.localeCompare(dateB);
});
}
return items;
}