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 { GetContentsFolderQuery } from "graphql/generated";
|
||||||
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
||||||
import { prettySlug } from "helpers/formatters";
|
import { prettySlug } from "helpers/formatters";
|
||||||
import { SmartList } from "components/SmartList";
|
|
||||||
import { Ico } from "components/Ico";
|
import { Ico } from "components/Ico";
|
||||||
import { Button, TranslatedButton } from "components/Inputs/Button";
|
import { Button, TranslatedButton } from "components/Inputs/Button";
|
||||||
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
import { PanelHeader } from "components/PanelComponents/PanelHeader";
|
||||||
|
@ -21,6 +20,7 @@ import { useAtomGetter, useAtomSetter } from "helpers/atoms";
|
||||||
import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
|
import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
|
||||||
import { useFormat } from "hooks/useFormat";
|
import { useFormat } from "hooks/useFormat";
|
||||||
import { getFormat } from "helpers/i18n";
|
import { getFormat } from "helpers/i18n";
|
||||||
|
import { Chip } from "components/Chip";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ╭────────╮
|
* ╭────────╮
|
||||||
|
@ -100,38 +100,59 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SmartList
|
{folder.subfolders?.data && folder.subfolders.data.length > 0 && (
|
||||||
items={filterHasAttributes(folder.subfolders?.data, ["id", "attributes"] as const)}
|
<div className="mb-8">
|
||||||
getItemId={(item) => item.id}
|
<h2 className="flex flex-row place-items-center gap-2 pb-2 text-2xl">
|
||||||
renderItem={({ item }) => (
|
{format("folders")}
|
||||||
<TranslatedPreviewFolder
|
<Chip text={format("x_results", { x: folder.subfolders.data.length })} />
|
||||||
href={`/contents/folder/${item.attributes.slug}`}
|
</h2>
|
||||||
translations={filterHasAttributes(item.attributes.titles, [
|
<div
|
||||||
"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(
|
className={cJoin(
|
||||||
"items-end",
|
"grid items-start gap-8 pb-12",
|
||||||
cIf(
|
cIf(
|
||||||
isContentPanelAtLeast4xl,
|
isContentPanelAtLeast4xl,
|
||||||
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
"grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8",
|
||||||
"grid-cols-2 gap-4"
|
"grid-cols-2 gap-4"
|
||||||
)
|
)
|
||||||
)}
|
)}>
|
||||||
renderWhenEmpty={() => <></>}
|
{filterHasAttributes(folder.subfolders.data, ["id", "attributes"] as const).map(
|
||||||
groupingFunction={() => [format("folders")]}
|
(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) }}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<SmartList
|
{folder.contents?.data && folder.contents.data.length > 0 && (
|
||||||
items={filterHasAttributes(folder.contents?.data, ["id", "attributes"] as const)}
|
<div className="mb-8">
|
||||||
getItemId={(item) => item.id}
|
<h2 className="flex flex-row place-items-center gap-2 pb-2 text-2xl">
|
||||||
renderItem={({ item }) => (
|
{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
|
<TranslatedPreviewCard
|
||||||
|
key={item.id}
|
||||||
href={`/contents/${item.attributes.slug}`}
|
href={`/contents/${item.attributes.slug}`}
|
||||||
translations={filterHasAttributes(item.attributes.translations, [
|
translations={filterHasAttributes(item.attributes.translations, [
|
||||||
"language.data.attributes.code",
|
"language.data.attributes.code",
|
||||||
|
@ -159,15 +180,11 @@ const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Elemen
|
||||||
)}
|
)}
|
||||||
keepInfoVisible
|
keepInfoVisible
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
className={cIf(
|
</div>
|
||||||
isContentPanelAtLeast4xl,
|
</div>
|
||||||
"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")]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
|
{folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (
|
||||||
<NoContentNorFolderMessage />
|
<NoContentNorFolderMessage />
|
||||||
|
|
Loading…
Reference in New Issue