From 6a3410d251d6604e96bd57f8fadd271075839563 Mon Sep 17 00:00:00 2001 From: DrMint Date: Sat, 13 Aug 2022 00:33:24 +0200 Subject: [PATCH] Convert content group to folders --- next.config.js | 5 + src/components/AppLayout.tsx | 8 +- src/components/Inputs/Button.tsx | 4 + src/components/Inputs/Link.tsx | 2 +- src/components/Inputs/PageSelector.tsx | 26 +- src/components/SmartList.tsx | 194 ++++++---- src/components/Translated.tsx | 42 +++ src/graphql/operations/getContentText.graphql | 15 +- src/graphql/operations/getContents.graphql | 12 - .../operations/getContentsFolder.graphql | 132 +++++++ .../getContentsFoldersSlugs.graphql | 9 + .../operations/getWebsiteInterface.graphql | 2 + src/helpers/openGraph.ts | 4 +- src/pages/archives/videos/index.tsx | 42 +-- .../contents/{[slug]/index.tsx => [slug].tsx} | 92 +++-- src/pages/contents/all.tsx | 315 ++++++++++++++++ src/pages/contents/folder/[slug].tsx | 322 ++++++++++++++++ src/pages/contents/index.tsx | 353 +----------------- src/pages/library/index.tsx | 1 + src/pages/news/index.tsx | 1 + src/pages/wiki/index.tsx | 1 + 21 files changed, 1081 insertions(+), 501 deletions(-) create mode 100644 src/graphql/operations/getContentsFolder.graphql create mode 100644 src/graphql/operations/getContentsFoldersSlugs.graphql rename src/pages/contents/{[slug]/index.tsx => [slug].tsx} (87%) create mode 100644 src/pages/contents/all.tsx create mode 100644 src/pages/contents/folder/[slug].tsx diff --git a/next.config.js b/next.config.js index 4646531..69ca6d1 100644 --- a/next.config.js +++ b/next.config.js @@ -30,6 +30,11 @@ module.exports = { destination: "https://gallery.accords-library.com/posts", permanent: false, }, + { + source: "/contents/folder", + destination: "/contents", + permanent: false, + }, ]; }, }; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 3084dad..65a3d5b 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -25,7 +25,7 @@ import { cIf, cJoin } from "helpers/className"; import { AppStaticProps } from "graphql/getAppStaticProps"; import { useAppLayout } from "contexts/AppLayoutContext"; import { Button } from "components/Inputs/Button"; -import { OpenGraph } from "helpers/openGraph"; +import { OpenGraph, TITLE_PREFIX } from "helpers/openGraph"; import { getDefaultPreferredLanguages } from "helpers/locales"; import useIsClient from "hooks/useIsClient"; import { useBoolean } from "hooks/useBoolean"; @@ -359,7 +359,11 @@ export const AppLayout = ({ ) )} > - {openGraph.title} + {isDefinedAndNotEmpty( + openGraph.title.substring(TITLE_PREFIX.length) + ) + ? openGraph.title.substring(TITLE_PREFIX.length) + : "Accord’s Library"}

{isDefined(subPanel) && !turnSubIntoContent && ( ; draggable?: boolean; badgeNumber?: number; + disabled?: boolean; size?: "normal" | "small"; } @@ -37,6 +38,7 @@ export const Button = ({ href, alwaysNewTab = false, badgeNumber, + disabled, size = "normal", }: Props): JSX.Element => ( event.target.blur()} className={cJoin( `group grid cursor-pointer select-none grid-flow-col place-content-center place-items-center gap-2 rounded-full border-[1px] border-dark py-3 px-4 @@ -60,6 +63,7 @@ export const Button = ({ active:hover:bg-black active:hover:!text-light active:hover:drop-shadow-black-lg` ), cIf(size === "small", "px-3 py-1 text-xs"), + cIf(disabled, "cursor-not-allowed"), className )} > diff --git a/src/components/Inputs/Link.tsx b/src/components/Inputs/Link.tsx index aec004a..9bf661c 100644 --- a/src/components/Inputs/Link.tsx +++ b/src/components/Inputs/Link.tsx @@ -36,7 +36,7 @@ export const Link = ({ } else if (isValidClick && href) { if (event.button !== MouseButton.Right) { if (alwaysNewTab) { - window.open(href, "_blank"); + window.open(href, "_blank", "noopener"); } else if (event.button === MouseButton.Left) { if (href.startsWith("#")) { router.replace(href); diff --git a/src/components/Inputs/PageSelector.tsx b/src/components/Inputs/PageSelector.tsx index 0bc2dce..910b9bf 100644 --- a/src/components/Inputs/PageSelector.tsx +++ b/src/components/Inputs/PageSelector.tsx @@ -10,6 +10,7 @@ import { cJoin } from "helpers/className"; interface Props { className?: string; page: number; + pagesCount: number; onChange: (value: number) => void; } @@ -18,14 +19,33 @@ interface Props { export const PageSelector = ({ page, className, + pagesCount, onChange, }: Props): JSX.Element => ( onChange(page - 1), icon: Icon.NavigateBefore }, - { text: (page + 1).toString() }, - { onClick: () => onChange(page + 1), icon: Icon.NavigateNext }, + { + onClick: () => onChange(0), + disabled: page === 0, + icon: Icon.FirstPage, + }, + { + onClick: () => page > 0 && onChange(page - 1), + disabled: page === 0, + icon: Icon.NavigateBefore, + }, + { text: `${page + 1} / ${pagesCount}` }, + { + onClick: () => page < pagesCount - 1 && onChange(page + 1), + disabled: page === pagesCount - 1, + icon: Icon.NavigateNext, + }, + { + onClick: () => onChange(pagesCount - 1), + disabled: page === pagesCount - 1, + icon: Icon.LastPage, + }, ]} /> ); diff --git a/src/components/SmartList.tsx b/src/components/SmartList.tsx index 1dccbbf..ac5ed40 100644 --- a/src/components/SmartList.tsx +++ b/src/components/SmartList.tsx @@ -4,9 +4,22 @@ import { PageSelector } from "./Inputs/PageSelector"; import { Ico, Icon } from "./Ico"; import { AppStaticProps } from "graphql/getAppStaticProps"; import { cJoin } from "helpers/className"; -import { isDefined, isDefinedAndNotEmpty, iterateMap } from "helpers/others"; +import { isDefined, isDefinedAndNotEmpty } from "helpers/others"; import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; +interface Group { + name: string; + items: T[]; + totalCount: number; +} + +const defaultGroupSortingFunction = (a: Group, b: Group) => + a.name.localeCompare(b.name); +const defaultGroupCountingFunction = () => 1; +const defaultFilteringFunction = () => true; +const defaultSortingFunction = () => 0; +const defaultGroupingFunction = () => [""]; + interface Props { // Items items: T[]; @@ -24,7 +37,7 @@ interface Props { searchingCaseInsensitive?: boolean; // Grouping groupingFunction?: (item: T) => string[]; - groupSortingFunction?: (a: string, b: string) => number; + groupSortingFunction?: (a: Group, b: Group) => number; groupCountingFunction?: (item: T) => number; // Filtering filteringFunction?: (item: T) => boolean; @@ -40,28 +53,28 @@ export const SmartList = ({ getItemId, renderItem: RenderItem, renderWhenEmpty: RenderWhenEmpty, - paginationItemPerPage = 0, + paginationItemPerPage = Infinity, paginationSelectorTop = true, paginationSelectorBottom = true, paginationScroolTop = true, searchingTerm, searchingBy, searchingCaseInsensitive = true, - groupingFunction = () => [""], - groupSortingFunction = (a, b) => a.localeCompare(b), - groupCountingFunction = () => 1, - filteringFunction = () => true, - sortingFunction = () => 0, + groupingFunction = defaultGroupingFunction, + groupSortingFunction = defaultGroupSortingFunction, + groupCountingFunction = defaultGroupCountingFunction, + filteringFunction = defaultFilteringFunction, + sortingFunction = defaultSortingFunction, className, langui, }: Props): JSX.Element => { const [page, setPage] = useState(0); useScrollTopOnChange(AnchorIds.ContentPanel, [page], paginationScroolTop); - - type Group = Map; - - useEffect(() => setPage(0), [searchingTerm]); + useEffect( + () => setPage(0), + [searchingTerm, groupingFunction, groupSortingFunction, items] + ); const searchFilter = useCallback(() => { if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) { @@ -85,87 +98,104 @@ export const SmartList = ({ [filteredItems, sortingFunction] ); - const paginatedItems = useMemo(() => { - if (paginationItemPerPage > 0) { - const memo = []; - for ( - let index = 0; - paginationItemPerPage * index < sortedItem.length; - index++ - ) { - memo.push( - sortedItem.slice( - index * paginationItemPerPage, - (index + 1) * paginationItemPerPage - ) - ); - } - return memo; - } - return [sortedItem]; - }, [paginationItemPerPage, sortedItem]); + const groups = useMemo(() => { + const memo: Group[] = []; - const groupedList = useMemo(() => { - const groups: Group = new Map(); - paginatedItems[page]?.forEach((item) => { + sortedItem.forEach((item) => { groupingFunction(item).forEach((category) => { - if (groups.has(category)) { - groups.get(category)?.push(item); + const index = memo.findIndex((group) => group.name === category); + if (index === -1) { + memo.push({ + name: category, + items: [item], + totalCount: groupCountingFunction(item), + }); } else { - groups.set(category, [item]); + memo[index].items.push(item); + memo[index].totalCount += groupCountingFunction(item); } }); }); - return groups; - }, [groupingFunction, page, paginatedItems]); + return memo.sort(groupSortingFunction); + }, [ + groupCountingFunction, + groupSortingFunction, + groupingFunction, + sortedItem, + ]); - const pageCount = useMemo( - () => - paginationItemPerPage > 0 - ? Math.floor(filteredItems.length / paginationItemPerPage) - : 1, - [paginationItemPerPage, filteredItems.length] - ); + const pages = useMemo(() => { + const memo: Group[][] = []; + let currentPage: Group[] = []; + let remainingSlots = paginationItemPerPage; + let loopSafeguard = 1000; - const changePage = useCallback( - (newPage: number) => - setPage(() => { - if (newPage <= 0) { - return 0; + 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(); } - if (newPage >= pageCount) { - return pageCount; - } - return newPage; - }), - [pageCount] - ); + + 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; + }, [groups, paginationItemPerPage]); return ( <> - {pageCount > 1 && paginationSelectorTop && ( - + {pages.length > 1 && paginationSelectorTop && ( + )}
- {groupedList.size > 0 ? ( - iterateMap( - groupedList, - (name, groupItems) => - groupItems.length > 0 && ( - - {name.length > 0 && ( + {pages[page]?.length > 0 ? ( + pages[page]?.map( + (group) => + group.items.length > 0 && ( + + {group.name.length > 0 && (

- {name} + {group.name} acc + groupCountingFunction(item), - 0 - )} ${ - groupItems.length <= 1 + text={`${group.totalCount} ${ + group.items.length <= 1 ? langui.result?.toLowerCase() ?? "" : langui.results?.toLowerCase() ?? "" }`} @@ -179,13 +209,12 @@ export const SmartList = ({ className )} > - {groupItems.map((item) => ( + {group.items.map((item) => ( ))}

- ), - ([a], [b]) => groupSortingFunction(a, b) + ) ) ) : isDefined(RenderWhenEmpty) ? ( @@ -194,8 +223,13 @@ export const SmartList = ({ )} - {pageCount > 1 && paginationSelectorBottom && ( - + {pages.length > 1 && paginationSelectorBottom && ( + )} ); @@ -217,7 +251,7 @@ const DefaultRenderWhenEmpty = ({ langui }: DefaultRenderWhenEmptyProps) => ( border-dark p-8 text-dark opacity-40" > -

{langui.no_results_message}

+

{langui.no_results_message}

diff --git a/src/components/Translated.tsx b/src/components/Translated.tsx index 5d448c7..7a674d4 100644 --- a/src/components/Translated.tsx +++ b/src/components/Translated.tsx @@ -4,7 +4,9 @@ import { ScanSet } from "./Library/ScanSet"; import { NavOption } from "./PanelComponents/NavOption"; import { ChroniclePreview } from "./Chronicles/ChroniclePreview"; import { ChroniclesList } from "./Chronicles/ChroniclesList"; +import { Button } from "./Inputs/Button"; import { useSmartLanguage } from "hooks/useSmartLanguage"; +import { PreviewFolder } from "pages/contents/folder/[slug]"; export type TranslatedProps = Omit & { translations: (Pick & { language: string })[]; @@ -158,3 +160,43 @@ export const TranslatedChroniclesList = ({ /> ); }; + +// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ + +export const TranslatedButton = ({ + translations, + fallback, + ...otherProps +}: TranslatedProps[0], "text">): JSX.Element => { + const [selectedTranslation] = useSmartLanguage({ + items: translations, + languageExtractor, + }); + + return ( +