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, Icon } from "./Ico"; import { cJoin } from "helpers/className"; import { isDefined, isDefinedAndNotEmpty } from "helpers/others"; import { useScrollTopOnChange } from "hooks/useScrollTopOnChange"; import { Ids } from "types/ids"; import { atoms } from "contexts/atoms"; import { useAtomGetter } from "helpers/atoms"; interface Group { name: string; items: T[]; totalCount: number; } const defaultGroupSortingFunction = (a: Group, b: Group) => naturalCompare(a.name, b.name); const defaultGroupCountingFunction = () => 1; const defaultFilteringFunction = () => true; const defaultSortingFunction = () => 0; const defaultGroupingFunction = () => [""]; interface Props { // 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, b: Group) => number; groupCountingFunction?: (item: T) => number; // Filtering filteringFunction?: (item: T) => boolean; // Sorting sortingFunction?: (a: T, b: T) => number; // Other className?: string; } export const SmartList = ({ 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): JSX.Element => { const [page, setPage] = useState(0); const langui = useAtomGetter(atoms.localData.langui); useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop); useEffect(() => setPage(0), [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[] = []; sortedItem.forEach((item) => { groupingFunction(item).forEach((category) => { const index = memo.findIndex((group) => group.name === category); if (index === -1) { memo.push({ name: category, items: [item], totalCount: groupCountingFunction(item), }); } else { memo[index].items.push(item); memo[index].totalCount += groupCountingFunction(item); } }); }); return memo.sort(groupSortingFunction); })(); const pages = (() => { const memo: Group[][] = []; let currentPage: Group[] = []; 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 = { 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 > 0 }); useHotkeys("right", () => setPage((current) => current + 1), { enabled: page < pages.length - 1, }); return ( <> {pages.length > 1 && paginationSelectorTop && ( )}
{pages[page]?.length > 0 ? ( pages[page]?.map( (group) => group.items.length > 0 && ( {group.name.length > 0 && (

{group.name}

)}
{group.items.map((item) => ( ))}
) ) ) : isDefined(RenderWhenEmpty) ? ( ) : ( )}
{pages.length > 1 && paginationSelectorBottom && ( )} ); }; /* * ╭──────────────────────╮ * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰────────────────────────────────────── */ const DefaultRenderWhenEmpty = () => { const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout); const langui = useAtomGetter(atoms.localData.langui); return (
{is3ColumnsLayout && }

{langui.no_results_message}

{!is3ColumnsLayout && }
); };