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(
|
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
|
||||||
|
|
|
@ -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) => (
|
<>
|
||||||
<LibraryItemsPreview key={item.id} item={item.attributes} />
|
{items.length > 0 && (
|
||||||
))}
|
<>
|
||||||
</div>
|
<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>
|
</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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue