Added support for filtering Library items
This commit is contained in:
parent
67aa30c2f7
commit
8684640ef4
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -128,7 +128,6 @@ query getLibraryItemsPreview($language_code: String) {
|
|||
libraryItems(
|
||||
filters: { root_item: { eq: true } }
|
||||
pagination: { limit: -1 }
|
||||
sort: ["title:asc", "subtitle:asc"]
|
||||
) {
|
||||
data {
|
||||
id
|
||||
|
|
|
@ -14,14 +14,43 @@ import {
|
|||
import PanelHeader from "components/PanelComponents/PanelHeader";
|
||||
import AppLayout from "components/AppLayout";
|
||||
import LibraryItemsPreview from "components/Library/LibraryItemsPreview";
|
||||
import Select from "components/Select";
|
||||
import { useEffect, useState } from "react";
|
||||
import { prettyDate, prettyinlineTitle } from "queries/helpers";
|
||||
|
||||
type LibraryProps = {
|
||||
libraryItems: GetLibraryItemsPreviewQuery;
|
||||
langui: GetWebsiteInterfaceQuery;
|
||||
};
|
||||
|
||||
type GroupLibraryItems = Map<
|
||||
string,
|
||||
GetLibraryItemsPreviewQuery["libraryItems"]["data"]
|
||||
>;
|
||||
|
||||
export default function Library(props: LibraryProps): JSX.Element {
|
||||
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 = (
|
||||
<SubPanel>
|
||||
<PanelHeader
|
||||
|
@ -29,15 +58,54 @@ export default function Library(props: LibraryProps): JSX.Element {
|
|||
title={langui.main_library}
|
||||
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>
|
||||
);
|
||||
const contentPanel = (
|
||||
<ContentPanel width={ContentPanelWidthSizes.large}>
|
||||
<div className="grid gap-8 items-end mobile:grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(13rem,1fr))]">
|
||||
{props.libraryItems.libraryItems.data.map((item) => (
|
||||
<LibraryItemsPreview key={item.id} item={item.attributes} />
|
||||
))}
|
||||
</div>
|
||||
{[...groups].map(([name, items]) => (
|
||||
<>
|
||||
{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} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</ContentPanel>
|
||||
);
|
||||
return (
|
||||
|
@ -67,3 +135,118 @@ export const getStaticProps: GetStaticProps = async (context) => {
|
|||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue