Removed now unused SmartList component
This commit is contained in:
parent
5677fb180f
commit
75de7c5f2a
|
@ -1,233 +0,0 @@
|
|||
import { Fragment, useCallback, useEffect, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import naturalCompare from "string-natural-compare";
|
||||
import { Chip } from "./Chip";
|
||||
import { PageSelector } from "./Inputs/PageSelector";
|
||||
import { Ico } from "./Ico";
|
||||
import { cJoin } from "helpers/className";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/asserts";
|
||||
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
||||
import { Ids } from "types/ids";
|
||||
import { atoms } from "contexts/atoms";
|
||||
import { useAtomGetter } from "helpers/atoms";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
|
||||
interface Group<T> {
|
||||
name: string;
|
||||
items: T[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
const defaultGroupSortingFunction = <T,>(a: Group<T>, b: Group<T>) =>
|
||||
naturalCompare(a.name, b.name);
|
||||
const defaultGroupCountingFunction = () => 1;
|
||||
const defaultFilteringFunction = () => true;
|
||||
const defaultSortingFunction = () => 0;
|
||||
const defaultGroupingFunction = () => [""];
|
||||
|
||||
interface Props<T> {
|
||||
// Items
|
||||
items: T[];
|
||||
getItemId: (item: T) => string;
|
||||
renderItem: (props: { item: T }) => JSX.Element;
|
||||
renderWhenEmpty?: () => JSX.Element;
|
||||
// Pagination
|
||||
paginationItemPerPage?: number;
|
||||
paginationSelectorTop?: boolean;
|
||||
paginationSelectorBottom?: boolean;
|
||||
paginationScroolTop?: boolean;
|
||||
// Searching
|
||||
searchingTerm?: string;
|
||||
searchingBy?: (item: T) => string;
|
||||
searchingCaseInsensitive?: boolean;
|
||||
// Grouping
|
||||
groupingFunction?: (item: T) => string[];
|
||||
groupSortingFunction?: (a: Group<T>, b: Group<T>) => number;
|
||||
groupCountingFunction?: (item: T) => number;
|
||||
// Filtering
|
||||
filteringFunction?: (item: T) => boolean;
|
||||
// Sorting
|
||||
sortingFunction?: (a: T, b: T) => number;
|
||||
// Other
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SmartList = <T,>({
|
||||
items,
|
||||
getItemId,
|
||||
renderItem: RenderItem,
|
||||
renderWhenEmpty: RenderWhenEmpty,
|
||||
paginationItemPerPage = Infinity,
|
||||
paginationSelectorTop = true,
|
||||
paginationSelectorBottom = true,
|
||||
paginationScroolTop = true,
|
||||
searchingTerm,
|
||||
searchingBy,
|
||||
searchingCaseInsensitive = true,
|
||||
groupingFunction = defaultGroupingFunction,
|
||||
groupSortingFunction = defaultGroupSortingFunction,
|
||||
groupCountingFunction = defaultGroupCountingFunction,
|
||||
filteringFunction = defaultFilteringFunction,
|
||||
sortingFunction = defaultSortingFunction,
|
||||
className,
|
||||
}: Props<T>): JSX.Element => {
|
||||
const [page, setPage] = useState(1);
|
||||
const { format } = useFormat();
|
||||
useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
|
||||
useEffect(() => setPage(1), [searchingTerm, groupingFunction, groupSortingFunction]);
|
||||
|
||||
const searchFilter = useCallback(() => {
|
||||
if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) {
|
||||
if (searchingCaseInsensitive) {
|
||||
return items.filter((item) =>
|
||||
searchingBy(item).toLowerCase().includes(searchingTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
return items.filter((item) => searchingBy(item).includes(searchingTerm));
|
||||
}
|
||||
return items;
|
||||
}, [items, searchingBy, searchingCaseInsensitive, searchingTerm]);
|
||||
|
||||
const filteredItems = searchFilter().filter(filteringFunction);
|
||||
|
||||
const sortedItem = filteredItems.sort(sortingFunction);
|
||||
|
||||
const groups = (() => {
|
||||
const memo: Group<T>[] = [];
|
||||
|
||||
sortedItem.forEach((item) => {
|
||||
groupingFunction(item).forEach((groupName) => {
|
||||
const group = memo.find((elem) => elem.name === groupName);
|
||||
if (isDefined(group)) {
|
||||
group.items.push(item);
|
||||
group.totalCount += groupCountingFunction(item);
|
||||
} else {
|
||||
memo.push({
|
||||
name: groupName,
|
||||
items: [item],
|
||||
totalCount: groupCountingFunction(item),
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return memo.sort(groupSortingFunction);
|
||||
})();
|
||||
|
||||
const pages = (() => {
|
||||
const memo: Group<T>[][] = [];
|
||||
let currentPage: Group<T>[] = [];
|
||||
let remainingSlots = paginationItemPerPage;
|
||||
let loopSafeguard = 1000;
|
||||
|
||||
const newPage = () => {
|
||||
memo.push(currentPage);
|
||||
currentPage = [];
|
||||
remainingSlots = paginationItemPerPage;
|
||||
};
|
||||
|
||||
for (const group of groups) {
|
||||
let remainingItems = group.items.length;
|
||||
while (remainingItems > 0 && loopSafeguard >= 0) {
|
||||
loopSafeguard--;
|
||||
const currentIndex = group.items.length - remainingItems;
|
||||
|
||||
if (
|
||||
remainingSlots <= 0 ||
|
||||
(currentIndex === 0 &&
|
||||
remainingItems > remainingSlots &&
|
||||
remainingItems <= paginationItemPerPage)
|
||||
) {
|
||||
newPage();
|
||||
}
|
||||
|
||||
const slicedGroup: Group<T> = {
|
||||
name: group.name,
|
||||
items: group.items.slice(currentIndex, currentIndex + remainingSlots),
|
||||
totalCount: group.totalCount,
|
||||
};
|
||||
|
||||
remainingItems -= slicedGroup.items.length;
|
||||
remainingSlots -= slicedGroup.items.length;
|
||||
currentPage.push(slicedGroup);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPage.length > 0) {
|
||||
newPage();
|
||||
}
|
||||
|
||||
return memo;
|
||||
})();
|
||||
|
||||
useHotkeys("left", () => setPage((current) => current - 1), { enabled: page > 1 });
|
||||
useHotkeys("right", () => setPage((current) => current + 1), {
|
||||
enabled: page < pages.length,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{pages.length > 1 && paginationSelectorTop && (
|
||||
<PageSelector className="mb-12" page={page} pagesCount={pages.length} onChange={setPage} />
|
||||
)}
|
||||
|
||||
<div className="mb-8">
|
||||
{(pages[page - 1]?.length ?? 0) > 0 ? (
|
||||
pages[page - 1]?.map(
|
||||
(group) =>
|
||||
group.items.length > 0 && (
|
||||
<Fragment key={group.name}>
|
||||
{group.name.length > 0 && (
|
||||
<h2
|
||||
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl
|
||||
first-of-type:pt-0">
|
||||
{group.name}
|
||||
<Chip text={format("x_results", { x: group.totalCount })} />
|
||||
</h2>
|
||||
)}
|
||||
<div
|
||||
className={cJoin(
|
||||
`grid items-start gap-8 border-b-2 border-dotted pb-12
|
||||
last-of-type:border-0`,
|
||||
className
|
||||
)}>
|
||||
{group.items.map((item) => (
|
||||
<RenderItem item={item} key={getItemId(item)} />
|
||||
))}
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
)
|
||||
) : isDefined(RenderWhenEmpty) ? (
|
||||
<RenderWhenEmpty />
|
||||
) : (
|
||||
<DefaultRenderWhenEmpty />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{pages.length > 1 && paginationSelectorBottom && (
|
||||
<PageSelector className="mb-12" page={page} pagesCount={pages.length} onChange={setPage} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* ╭──────────────────────╮
|
||||
* ───────────────────────────────────╯ PRIVATE COMPONENTS ╰──────────────────────────────────────
|
||||
*/
|
||||
|
||||
const DefaultRenderWhenEmpty = () => {
|
||||
const { format } = useFormat();
|
||||
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
|
||||
return (
|
||||
<div className="grid h-full place-content-center">
|
||||
<div
|
||||
className="grid grid-flow-col place-items-center gap-9 rounded-2xl border-2 border-dotted
|
||||
border-dark p-8 text-dark opacity-40">
|
||||
{is3ColumnsLayout && <Ico icon="chevron_left" className="!text-[300%]" />}
|
||||
<p className="max-w-xs text-2xl">{format("no_results_message")}</p>
|
||||
{!is3ColumnsLayout && <Ico icon="chevron_right" className="!text-[300%]" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -8,7 +8,6 @@ import { filterHasAttributes } from "helpers/asserts";
|
|||
import { GetContentsFolderQuery } from "graphql/generated";
|
||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||
import { prettySlug } from "helpers/formatters";
|
||||
import { SmartList } from "components/SmartList";
|
||||
import { Ico } from "components/Ico";
|
||||
import { Button, TranslatedButton } from "components/Inputs/Button";
|
||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||
|
@ -21,6 +20,7 @@ import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
|||
import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
|
||||
import { useFormat } from "hooks/useFormat";
|
||||
import { getFormat } from "helpers/i18n";
|
||||
import { Chip } from "components/Chip";
|
||||
|
||||
/*
|
||||
* ╭────────╮
|
||||
|
@ -100,74 +100,91 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen
|
|||
)}
|
||||
</div>
|
||||
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.subfolders?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewFolder
|
||||
href={`/contents/folder/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
title: title.title,
|
||||
language: title.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
/>
|
||||
)}
|
||||
className={cJoin(
|
||||
"items-end",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-4"
|
||||
)
|
||||
)}
|
||||
renderWhenEmpty={() => <></>}
|
||||
groupingFunction={() => [format("folders")]}
|
||||
/>
|
||||
|
||||
<SmartList
|
||||
items={filterHasAttributes(folder.contents?.data, ["id", "attributes"] as const)}
|
||||
getItemId={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TranslatedPreviewCard
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
{folder.subfolders?.data && folder.subfolders.data.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="flex flex-row place-items-center gap-2 pb-2 text-2xl">
|
||||
{format("folders")}
|
||||
<Chip text={format("x_results", { x: folder.subfolders.data.length })} />
|
||||
</h2>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid items-start gap-8 pb-12",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-4"
|
||||
)
|
||||
)}>
|
||||
{filterHasAttributes(folder.subfolders.data, ["id", "attributes"] as const).map(
|
||||
(subfolder) => (
|
||||
<TranslatedPreviewFolder
|
||||
key={subfolder.id}
|
||||
href={`/contents/folder/${subfolder.attributes.slug}`}
|
||||
translations={filterHasAttributes(subfolder.attributes.titles, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((title) => ({
|
||||
title: title.title,
|
||||
language: title.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(subfolder.attributes.slug) }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
)}
|
||||
className={cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-x-3 gap-y-5"
|
||||
)}
|
||||
renderWhenEmpty={() => <></>}
|
||||
groupingFunction={() => [format("contents")]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{folder.contents?.data && folder.contents.data.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="flex flex-row place-items-center gap-2 pb-2 text-2xl">
|
||||
{format("contents")}
|
||||
<Chip text={format("x_results", { x: folder.contents.data.length })} />
|
||||
</h2>
|
||||
<div
|
||||
className={cJoin(
|
||||
"grid items-start gap-8 pb-12",
|
||||
cIf(
|
||||
isContentPanelAtLeast4xl,
|
||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||
"grid-cols-2 gap-4"
|
||||
)
|
||||
)}>
|
||||
{filterHasAttributes(folder.contents.data, ["id", "attributes"] as const).map(
|
||||
(item) => (
|
||||
<TranslatedPreviewCard
|
||||
key={item.id}
|
||||
href={`/contents/${item.attributes.slug}`}
|
||||
translations={filterHasAttributes(item.attributes.translations, [
|
||||
"language.data.attributes.code",
|
||||
] as const).map((translation) => ({
|
||||
pre_title: translation.pre_title,
|
||||
title: translation.title,
|
||||
subtitle: translation.subtitle,
|
||||
language: translation.language.data.attributes.code,
|
||||
}))}
|
||||
fallback={{ title: prettySlug(item.attributes.slug) }}
|
||||
thumbnail={item.attributes.thumbnail?.data?.attributes}
|
||||
thumbnailAspectRatio="3/2"
|
||||
thumbnailForceAspectRatio
|
||||
topChips={
|
||||
item.attributes.type?.data?.attributes
|
||||
? [
|
||||
item.attributes.type.data.attributes.titles?.[0]
|
||||
? item.attributes.type.data.attributes.titles[0]?.title
|
||||
: prettySlug(item.attributes.type.data.attributes.slug),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
bottomChips={item.attributes.categories?.data.map(
|
||||
(category) => category.attributes?.short ?? ""
|
||||
)}
|
||||
keepInfoVisible
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
|
||||
<NoContentNorFolderMessage />
|
||||
|
|
Loading…
Reference in New Issue