diff --git a/src/components/SmartList.tsx b/src/components/SmartList.tsx deleted file mode 100644 index 279a184..0000000 --- a/src/components/SmartList.tsx +++ /dev/null @@ -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 { - 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(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[] = []; - - 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[][] = []; - 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 > 1 }); - useHotkeys("right", () => setPage((current) => current + 1), { - enabled: page < pages.length, - }); - - return ( - <> - {pages.length > 1 && paginationSelectorTop && ( - - )} - -
- {(pages[page - 1]?.length ?? 0) > 0 ? ( - pages[page - 1]?.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 { format } = useFormat(); - const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout); - return ( -
-
- {is3ColumnsLayout && } -

{format("no_results_message")}

- {!is3ColumnsLayout && } -
-
- ); -}; diff --git a/src/pages/contents/folder/[slug].tsx b/src/pages/contents/folder/[slug].tsx index 63e68d2..6853bf6 100644 --- a/src/pages/contents/folder/[slug].tsx +++ b/src/pages/contents/folder/[slug].tsx @@ -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 )} - item.id} - renderItem={({ item }) => ( - ({ - 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")]} - /> - - item.id} - renderItem={({ item }) => ( - ({ - 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 && ( +
+

+ {format("folders")} + +

+
+ {filterHasAttributes(folder.subfolders.data, ["id", "attributes"] as const).map( + (subfolder) => ( + ({ + 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")]} - /> +
+
+ )} + + {folder.contents?.data && folder.contents.data.length > 0 && ( +
+

+ {format("contents")} + +

+
+ {filterHasAttributes(folder.contents.data, ["id", "attributes"] as const).map( + (item) => ( + ({ + 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 + /> + ) + )} +
+
+ )} {folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && (