Convert content group to folders

This commit is contained in:
DrMint 2022-08-13 00:33:24 +02:00
parent 82c605086b
commit 6a3410d251
21 changed files with 1081 additions and 501 deletions

View File

@ -30,6 +30,11 @@ module.exports = {
destination: "https://gallery.accords-library.com/posts",
permanent: false,
},
{
source: "/contents/folder",
destination: "/contents",
permanent: false,
},
];
},
};

View File

@ -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)
: "Accords Library"}
</p>
{isDefined(subPanel) && !turnSubIntoContent && (
<Ico

View File

@ -21,6 +21,7 @@ interface Props {
onClick?: MouseEventHandler<HTMLDivElement>;
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 => (
<ConditionalWrapper
@ -49,6 +51,7 @@ export const Button = ({
draggable={draggable}
id={id}
onClick={onClick}
onFocus={(event) => 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
)}
>

View File

@ -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);

View File

@ -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 => (
<ButtonGroup
className={cJoin("flex flex-row place-content-center", className)}
buttonsProps={[
{ onClick: () => 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,
},
]}
/>
);

View File

@ -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<T> {
name: string;
items: T[];
totalCount: number;
}
const defaultGroupSortingFunction = <T,>(a: Group<T>, b: Group<T>) =>
a.name.localeCompare(b.name);
const defaultGroupCountingFunction = () => 1;
const defaultFilteringFunction = () => true;
const defaultSortingFunction = () => 0;
const defaultGroupingFunction = () => [""];
interface Props<T> {
// Items
items: T[];
@ -24,7 +37,7 @@ interface Props<T> {
searchingCaseInsensitive?: boolean;
// Grouping
groupingFunction?: (item: T) => string[];
groupSortingFunction?: (a: string, b: string) => number;
groupSortingFunction?: (a: Group<T>, b: Group<T>) => number;
groupCountingFunction?: (item: T) => number;
// Filtering
filteringFunction?: (item: T) => boolean;
@ -40,28 +53,28 @@ export const SmartList = <T,>({
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<T>): JSX.Element => {
const [page, setPage] = useState(0);
useScrollTopOnChange(AnchorIds.ContentPanel, [page], paginationScroolTop);
type Group = Map<string, T[]>;
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 = <T,>({
[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<T>[] = [];
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<T>[][] = [];
let currentPage: Group<T>[] = [];
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<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;
}, [groups, paginationItemPerPage]);
return (
<>
{pageCount > 1 && paginationSelectorTop && (
<PageSelector className="mb-12" page={page} onChange={changePage} />
{pages.length > 1 && paginationSelectorTop && (
<PageSelector
className="mb-12"
page={page}
pagesCount={pages.length}
onChange={setPage}
/>
)}
<div className="mb-8">
{groupedList.size > 0 ? (
iterateMap(
groupedList,
(name, groupItems) =>
groupItems.length > 0 && (
<Fragment key={name}>
{name.length > 0 && (
{pages[page]?.length > 0 ? (
pages[page]?.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"
first-of-type:pt-0"
>
{name}
{group.name}
<Chip
text={`${groupItems.reduce(
(acc, item) => 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 = <T,>({
className
)}
>
{groupItems.map((item) => (
{group.items.map((item) => (
<RenderItem item={item} key={getItemId(item)} />
))}
</div>
</Fragment>
),
([a], [b]) => groupSortingFunction(a, b)
)
)
) : isDefined(RenderWhenEmpty) ? (
<RenderWhenEmpty />
@ -194,8 +223,13 @@ export const SmartList = <T,>({
)}
</div>
{pageCount > 1 && paginationSelectorBottom && (
<PageSelector className="mb-12" page={page} onChange={changePage} />
{pages.length > 1 && paginationSelectorBottom && (
<PageSelector
className="mb-12"
page={page}
pagesCount={pages.length}
onChange={setPage}
/>
)}
</>
);
@ -217,7 +251,7 @@ const DefaultRenderWhenEmpty = ({ langui }: DefaultRenderWhenEmptyProps) => (
border-dark p-8 text-dark opacity-40"
>
<Ico icon={Icon.ChevronLeft} className="!text-[300%] mobile:hidden" />
<p className="max-w-xs text-2xl"> {langui.no_results_message} </p>
<p className="max-w-xs text-2xl">{langui.no_results_message}</p>
<Ico icon={Icon.ChevronRight} className="!text-[300%] desktop:hidden" />
</div>
</div>

View File

@ -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<P, K extends keyof P> = Omit<P, K> & {
translations: (Pick<P, K> & { language: string })[];
@ -158,3 +160,43 @@ export const TranslatedChroniclesList = ({
/>
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TranslatedButton = ({
translations,
fallback,
...otherProps
}: TranslatedProps<Parameters<typeof Button>[0], "text">): JSX.Element => {
const [selectedTranslation] = useSmartLanguage({
items: translations,
languageExtractor,
});
return (
<Button text={selectedTranslation?.text ?? fallback.text} {...otherProps} />
);
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const TranslatedPreviewFolder = ({
translations,
fallback,
...otherProps
}: TranslatedProps<
Parameters<typeof PreviewFolder>[0],
"title"
>): JSX.Element => {
const [selectedTranslation] = useSmartLanguage({
items: translations,
languageExtractor,
});
return (
<PreviewFolder
title={selectedTranslation?.title ?? fallback.title}
{...otherProps}
/>
);
};

View File

@ -214,9 +214,22 @@ query getContentText($slug: String, $language_code: String) {
}
}
}
group {
folders(pagination: { limit: -1 }) {
data {
id
attributes {
slug
titles(pagination: { limit: -1 }) {
language {
data {
attributes {
code
}
}
}
title
}
sequence
contents(pagination: { limit: -1 }) {
data {
attributes {

View File

@ -62,18 +62,6 @@ query getContents($language_code: String) {
}
}
}
group {
data {
attributes {
combine
contents(pagination: { limit: -1 }) {
data {
id
}
}
}
}
}
thumbnail {
data {
attributes {

View File

@ -0,0 +1,132 @@
query getContentsFolder($slug: String, $language_code: String) {
contentsFolders(filters: { slug: { eq: $slug } }) {
data {
attributes {
slug
titles(pagination: { limit: -1 }) {
id
language {
data {
attributes {
code
}
}
}
title
}
parent_folder {
data {
attributes {
slug
titles(pagination: { limit: -1 }) {
id
language {
data {
attributes {
code
}
}
}
title
}
}
}
}
contents(pagination: { limit: -1 }) {
data {
id
attributes {
slug
translations(pagination: { limit: -1 }) {
pre_title
title
subtitle
language {
data {
attributes {
code
}
}
}
}
categories(pagination: { limit: -1 }) {
data {
id
attributes {
name
short
}
}
}
type {
data {
attributes {
slug
titles(
filters: { language: { code: { eq: $language_code } } }
) {
title
}
}
}
}
ranged_contents(pagination: { limit: -1 }) {
data {
id
attributes {
slug
scan_set {
id
}
library_item {
data {
attributes {
slug
title
subtitle
thumbnail {
data {
attributes {
...uploadImage
}
}
}
}
}
}
}
}
}
thumbnail {
data {
attributes {
...uploadImage
}
}
}
}
}
}
subfolders(pagination: { limit: -1 }) {
data {
id
attributes {
slug
titles(pagination: { limit: -1 }) {
id
language {
data {
attributes {
code
}
}
}
title
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,9 @@
query getContentsFoldersSlugs {
contentsFolders(pagination: { limit: -1 }) {
data {
attributes {
slug
}
}
}
}

View File

@ -170,6 +170,8 @@ query getWebsiteInterface($language_code: String) {
no_source_warning
copy_anchor_link
anchor_link_copied
folders
empty_folder_message
}
}
}

View File

@ -15,7 +15,7 @@ const DEFAULT_OG_THUMBNAIL = {
alt: "Accord's Library Logo",
};
const TITLE_PREFIX = "Accords Library";
export const TITLE_PREFIX = "Accords Library - ";
export interface OpenGraph {
title: string;
@ -29,7 +29,7 @@ export const getOpenGraph = (
description?: string | null | undefined,
thumbnail?: UploadImageFragment | null | undefined
): OpenGraph => ({
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? ` - ${title}` : ""}`,
title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) ? `${title}` : ""}`,
description: isDefinedAndNotEmpty(description)
? description
: langui.default_description ?? "",

View File

@ -99,33 +99,31 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => {
items={filterHasAttributes(videos, ["id", "attributes"] as const)}
getItemId={(item) => item.id}
renderItem={({ item }) => (
<>
<PreviewCard
href={`/archives/videos/v/${item.attributes.uid}`}
title={item.attributes.title}
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
thumbnailAspectRatio="16/9"
thumbnailForceAspectRatio
keepInfoVisible={keepInfoVisible}
metadata={{
releaseDate: item.attributes.published_date,
views: item.attributes.views,
author: item.attributes.channel?.data?.attributes?.title,
position: "Top",
}}
hoverlay={{
__typename: "Video",
duration: item.attributes.duration,
}}
/>
</>
<PreviewCard
href={`/archives/videos/v/${item.attributes.uid}`}
title={item.attributes.title}
thumbnail={getVideoThumbnailURL(item.attributes.uid)}
thumbnailAspectRatio="16/9"
thumbnailForceAspectRatio
keepInfoVisible={keepInfoVisible}
metadata={{
releaseDate: item.attributes.published_date,
views: item.attributes.views,
author: item.attributes.channel?.data?.attributes?.title,
position: "Top",
}}
hoverlay={{
__typename: "Video",
duration: item.attributes.duration,
}}
/>
)}
langui={langui}
className="desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2
thin:grid-cols-1"
paginationItemPerPage={20}
paginationItemPerPage={25}
searchingTerm={searchName}
searchingBy={(item) => item.attributes.title}
langui={langui}
/>
</ContentPanel>
),

View File

@ -33,7 +33,10 @@ import { ContentWithTranslations } from "helpers/types";
import { useMediaMobile } from "hooks/useMediaQuery";
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { TranslatedPreviewLine } from "components/Translated";
import {
TranslatedPreviewFolder,
TranslatedPreviewLine,
} from "components/Translated";
import { getOpenGraph } from "helpers/openGraph";
import {
getDefaultPreferredLanguages,
@ -74,32 +77,51 @@ const Content = ({
const { previousContent, nextContent } = useMemo(
() => ({
previousContent: content.group?.data?.attributes?.contents
? getPreviousContent(
content.group.data.attributes.contents.data,
content.slug
)
: undefined,
nextContent: content.group?.data?.attributes?.contents
? getNextContent(
content.group.data.attributes.contents.data,
content.slug
)
: undefined,
previousContent:
content.folders?.data[0]?.attributes?.contents &&
content.folders.data[0].attributes.sequence
? getPreviousContent(
content.folders.data[0].attributes.contents.data,
content.slug
)
: undefined,
nextContent:
content.folders?.data[0]?.attributes?.contents &&
content.folders.data[0].attributes.sequence
? getNextContent(
content.folders.data[0].attributes.contents.data,
content.slug
)
: undefined,
}),
[content.group, content.slug]
[content.folders, content.slug]
);
const subPanel = useMemo(
() => (
<SubPanel>
<ReturnButton
href={`/contents`}
title={langui.contents}
langui={langui}
displayOn={ReturnButtonType.Desktop}
horizontalLine
/>
{content.folders?.data && content.folders.data.length > 0 && (
<>
<h2 className="mb-2 text-xl">{langui.folders}</h2>
{filterHasAttributes(content.folders.data, [
"attributes",
"id",
] as const).map((folder) => (
<TranslatedPreviewFolder
key={folder.id}
href={`/contents/folder/${folder.attributes.slug}`}
translations={filterHasAttributes(folder.attributes.titles, [
"language.data.attributes.code",
] as const).map((title) => ({
language: title.language.data.attributes.code,
title: title.title,
}))}
fallback={{ title: prettySlug(folder.attributes.slug) }}
/>
))}
<HorizontalLine />
</>
)}
{selectedTranslation?.text_set?.source_language?.data?.attributes
?.code !== undefined && (
@ -308,6 +330,7 @@ const Content = ({
</SubPanel>
),
[
content.folders?.data,
content.ranged_contents?.data,
currencies,
languages,
@ -320,7 +343,7 @@ const Content = ({
() => (
<ContentPanel>
<ReturnButton
href={`/contents`}
href={`/contents/folder`}
title={langui.contents}
langui={langui}
displayOn={ReturnButtonType.Mobile}
@ -579,19 +602,19 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
* PRIVATE METHODS
*/
type Group = NonNullable<
type FolderContents = NonNullable<
NonNullable<
NonNullable<
NonNullable<ContentWithTranslations["group"]>["data"]
NonNullable<ContentWithTranslations["folders"]>["data"][number]
>["attributes"]
>["contents"]
>["data"];
const getPreviousContent = (group: Group, currentSlug: string) => {
for (let index = 0; index < group.length; index++) {
const content = group[index];
const getPreviousContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < contents.length; index++) {
const content = contents[index];
if (content.attributes?.slug === currentSlug && index > 0) {
return group[index - 1];
return contents[index - 1];
}
}
return undefined;
@ -599,11 +622,14 @@ const getPreviousContent = (group: Group, currentSlug: string) => {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const getNextContent = (group: Group, currentSlug: string) => {
for (let index = 0; index < group.length; index++) {
const content = group[index];
if (content.attributes?.slug === currentSlug && index < group.length - 1) {
return group[index + 1];
const getNextContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < contents.length; index++) {
const content = contents[index];
if (
content.attributes?.slug === currentSlug &&
index < contents.length - 1
) {
return contents[index + 1];
}
}
return undefined;

315
src/pages/contents/all.tsx Normal file
View File

@ -0,0 +1,315 @@
import { GetStaticProps } from "next";
import { useState, useMemo, useCallback } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Select } from "components/Inputs/Select";
import { Switch } from "components/Inputs/Switch";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import {
ContentPanel,
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk";
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel";
import { Button } from "components/Inputs/Button";
import { useMediaHoverable } from "hooks/useMediaQuery";
import { Icon } from "components/Ico";
import { filterDefined, filterHasAttributes } from "helpers/others";
import { GetContentsQuery } from "graphql/generated";
import { SmartList } from "components/SmartList";
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
import { useBoolean } from "hooks/useBoolean";
import { TranslatedPreviewCard } from "components/Translated";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
groupingMethod: -1,
keepInfoVisible: false,
searchName: "",
};
/*
*
* PAGE
*/
interface Props extends AppStaticProps, AppLayoutRequired {
contents: NonNullable<GetContentsQuery["contents"]>["data"];
}
const Contents = ({
langui,
contents,
languages,
...otherProps
}: Props): JSX.Element => {
const hoverable = useMediaHoverable();
const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod
);
const {
state: keepInfoVisible,
toggleState: toggleKeepInfoVisible,
setState: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const [searchName, setSearchName] = useState(
DEFAULT_FILTERS_STATE.searchName
);
const groupingFunction = useCallback(
(
item: SelectiveNonNullable<
NonNullable<GetContentsQuery["contents"]>["data"][number],
"attributes" | "id"
>
): string[] => {
switch (groupingMethod) {
case 0: {
const categories = filterHasAttributes(
item.attributes.categories?.data,
["attributes"] as const
);
if (categories.length > 0) {
return categories.map((category) => category.attributes.name);
}
return [langui.no_category ?? "No category"];
}
case 1: {
return [
item.attributes.type?.data?.attributes?.titles?.[0]?.title ??
item.attributes.type?.data?.attributes?.slug
? prettySlug(item.attributes.type.data.attributes.slug)
: langui.no_type ?? "No type",
];
}
default: {
return [""];
}
}
},
[groupingMethod, langui]
);
const filteringFunction = useCallback(
(
item: SelectiveNonNullable<Props["contents"][number], "attributes" | "id">
) => {
if (searchName.length > 1) {
if (
filterDefined(item.attributes.translations).find((translation) =>
prettyInlineTitle(
translation.pre_title,
translation.title,
translation.subtitle
)
.toLowerCase()
.includes(searchName.toLowerCase())
)
) {
return true;
}
return false;
}
return true;
},
[searchName]
);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.Workspaces}
title={langui.contents}
description={langui.contents_description}
/>
<Button
href="/contents"
text={"Switch to folder view"}
icon={Icon.Folder}
/>
<HorizontalLine />
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
value={searchName}
onChange={setSearchName}
/>
<WithLabel
label={langui.group_by}
input={
<Select
className="w-full"
options={[langui.category ?? "Category", langui.type ?? "Type"]}
value={groupingMethod}
onChange={setGroupingMethod}
allowEmpty
/>
}
/>
{hoverable && (
<WithLabel
label={langui.always_show_info}
input={
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
}
/>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(DEFAULT_FILTERS_STATE.searchName);
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
}}
/>
</SubPanel>
),
[
groupingMethod,
hoverable,
keepInfoVisible,
langui.always_show_info,
langui.category,
langui.contents,
langui.contents_description,
langui.group_by,
langui.reset_all_filters,
langui.search_title,
langui.type,
searchName,
setKeepInfoVisible,
toggleKeepInfoVisible,
]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(contents, ["attributes", "id"] 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 ?? ""
)}
keepInfoVisible={keepInfoVisible}
/>
)}
className="grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
groupingFunction={groupingFunction}
filteringFunction={filteringFunction}
searchingTerm={searchName}
searchingBy={(item) =>
`
${item.attributes.slug}
${filterDefined(item.attributes.translations)
.map((translation) =>
prettyInlineTitle(
translation.pre_title,
translation.title,
translation.subtitle
)
)
.join(" ")}`
}
paginationItemPerPage={50}
langui={langui}
/>
</ContentPanel>
),
[
contents,
filteringFunction,
groupingFunction,
keepInfoVisible,
langui,
searchName,
]
);
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
subPanelIcon={Icon.Search}
languages={languages}
langui={langui}
{...otherProps}
/>
);
};
export default Contents;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const contents = await sdk.getContents({
language_code: context.locale ?? "en",
});
if (!contents.contents) return { notFound: true };
contents.contents.data.sort((a, b) => {
const titleA = a.attributes?.slug ?? "";
const titleB = b.attributes?.slug ?? "";
return titleA.localeCompare(titleB);
});
const appStaticProps = await getAppStaticProps(context);
const props: Props = {
...appStaticProps,
contents: contents.contents.data,
openGraph: getOpenGraph(
appStaticProps.langui,
appStaticProps.langui.contents ?? "Contents"
),
};
return {
props: props,
};
};

View File

@ -0,0 +1,322 @@
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
import { useMemo } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import {
ContentPanel,
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getOpenGraph } from "helpers/openGraph";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes } from "helpers/others";
import { GetContentsFolderQuery } from "graphql/generated";
import {
getDefaultPreferredLanguages,
staticSmartLanguage,
} from "helpers/locales";
import { prettySlug } from "helpers/formatters";
import { SmartList } from "components/SmartList";
import {
TranslatedButton,
TranslatedPreviewCard,
TranslatedPreviewFolder,
} from "components/Translated";
import { Ico, Icon } from "components/Ico";
import { Button } from "components/Inputs/Button";
import { Link } from "components/Inputs/Link";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import { SubPanel } from "components/Panels/SubPanel";
/*
*
* PAGE
*/
interface Props extends AppStaticProps, AppLayoutRequired {
folder: NonNullable<
NonNullable<
GetContentsFolderQuery["contentsFolders"]
>["data"][number]["attributes"]
>;
}
const ContentsFolder = ({
langui,
openGraph,
folder,
...otherProps
}: Props): JSX.Element => {
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.Workspaces}
title={langui.contents}
description={langui.contents_description}
/>
<Button
href="/contents/all"
text={"Switch to grid view"}
icon={Icon.Apps}
/>
</SubPanel>
),
[langui.contents, langui.contents_description]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<div className="mb-10 grid grid-flow-col place-items-center justify-start gap-x-2">
{folder.parent_folder?.data?.attributes && (
<>
{folder.parent_folder.data.attributes.slug === "root" ? (
<Button href="/contents" icon={Icon.Home} />
) : (
<TranslatedButton
href={`/contents/folder/${folder.parent_folder.data.attributes.slug}`}
translations={filterHasAttributes(
folder.parent_folder.data.attributes.titles,
["language.data.attributes.code"] as const
).map((title) => ({
language: title.language.data.attributes.code,
text: title.title,
}))}
fallback={{
text: prettySlug(folder.parent_folder.data.attributes.slug),
}}
/>
)}
<Ico icon={Icon.ChevronRight} />
</>
)}
{folder.slug === "root" ? (
<Button href="/contents" icon={Icon.Home} active />
) : (
<TranslatedButton
translations={filterHasAttributes(folder.titles, [
"language.data.attributes.code",
] as const).map((title) => ({
language: title.language.data.attributes.code,
text: title.title,
}))}
fallback={{
text: prettySlug(folder.slug),
}}
active
/>
)}
</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="grid-cols-2 items-stretch
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
renderWhenEmpty={() => <></>}
langui={langui}
groupingFunction={() => [langui.folders ?? "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 ?? ""
)}
keepInfoVisible
/>
)}
className="grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
renderWhenEmpty={() => <></>}
langui={langui}
groupingFunction={() => [langui.contents ?? "Contents"]}
/>
{folder.contents?.data.length === 0 &&
folder.subfolders?.data.length === 0 && (
<NoContentNorFolderMessage langui={langui} />
)}
</ContentPanel>
),
[
folder.contents,
folder.parent_folder?.data?.attributes,
folder.slug,
folder.subfolders,
folder.titles,
langui,
]
);
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
openGraph={openGraph}
langui={langui}
{...otherProps}
/>
);
};
export default ContentsFolder;
/*
*
* NEXT DATA FETCHING
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const slug = context.params?.slug ? context.params.slug.toString() : "";
const contentsFolder = await sdk.getContentsFolder({
slug: slug,
language_code: context.locale ?? "en",
});
if (!contentsFolder.contentsFolders?.data[0]?.attributes) {
return { notFound: true };
}
const folder = contentsFolder.contentsFolders.data[0].attributes;
const appStaticProps = await getAppStaticProps(context);
const title = (() => {
if (slug === "root") {
return appStaticProps.langui.contents ?? "Contents";
}
if (context.locale && context.locales) {
const selectedTranslation = staticSmartLanguage({
items: folder.titles,
languageExtractor: (item) => item.language?.data?.attributes?.code,
preferredLanguages: getDefaultPreferredLanguages(
context.locale,
context.locales
),
});
if (selectedTranslation) {
return selectedTranslation.title;
}
}
return prettySlug(folder.slug);
})();
const props: Props = {
...appStaticProps,
openGraph: getOpenGraph(appStaticProps.langui, title),
folder,
};
return {
props: props,
};
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const getStaticPaths: GetStaticPaths = async (context) => {
const sdk = getReadySdk();
const contents = await sdk.getContentsFoldersSlugs();
const paths: GetStaticPathsResult["paths"] = [];
filterHasAttributes(contents.contentsFolders?.data, [
"attributes",
] as const).map((item) => {
context.locales?.map((local) => {
paths.push({
params: { slug: item.attributes.slug },
locale: local,
});
});
});
return {
paths,
fallback: "blocking",
};
};
/*
*
* PRIVATE COMPONENTS
*/
interface PreviewFolderProps {
href: string;
title: string | null | undefined;
}
export const PreviewFolder = ({
href,
title,
}: PreviewFolderProps): JSX.Element => (
<Link
href={href}
className="flex w-full cursor-pointer flex-row place-content-center place-items-center gap-4
rounded-md bg-light p-6 px-8 transition-transform drop-shadow-shade-xl hover:scale-[1.02]
mobile:px-6 mobile:py-6"
>
{title && (
<p className="text-center font-headers text-lg font-bold leading-none">
{title}
</p>
)}
</Link>
);
interface NoContentNorFolderMessageProps {
langui: AppStaticProps["langui"];
}
const NoContentNorFolderMessage = ({
langui,
}: NoContentNorFolderMessageProps) => (
<div className="grid 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"
>
<p className="max-w-xs text-2xl">{langui.empty_folder_message}</p>
</div>
</div>
);

View File

@ -1,332 +1,16 @@
import { GetStaticProps } from "next";
import { useState, useMemo, useCallback } from "react";
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
import { Select } from "components/Inputs/Select";
import { Switch } from "components/Inputs/Switch";
import { PanelHeader } from "components/PanelComponents/PanelHeader";
import {
ContentPanel,
ContentPanelWidthSizes,
} from "components/Panels/ContentPanel";
import { SubPanel } from "components/Panels/SubPanel";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk";
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
import { TextInput } from "components/Inputs/TextInput";
import { WithLabel } from "components/Inputs/WithLabel";
import { Button } from "components/Inputs/Button";
import { useMediaHoverable } from "hooks/useMediaQuery";
import { Icon } from "components/Ico";
import { filterDefined, filterHasAttributes } from "helpers/others";
import { GetContentsQuery } from "graphql/generated";
import { SmartList } from "components/SmartList";
import { SelectiveNonNullable } from "helpers/types/SelectiveNonNullable";
import { useBoolean } from "hooks/useBoolean";
import { TranslatedPreviewCard } from "components/Translated";
import { getOpenGraph } from "helpers/openGraph";
/*
*
* CONSTANTS
*/
const DEFAULT_FILTERS_STATE = {
groupingMethod: -1,
keepInfoVisible: false,
combineRelatedContent: true,
searchName: "",
};
import ContentsFolder, {
getStaticProps as folderGetStaticProps,
} from "./folder/[slug]";
/*
*
* PAGE
*/
interface Props extends AppStaticProps, AppLayoutRequired {
contents: NonNullable<GetContentsQuery["contents"]>["data"];
}
const Contents = ({
langui,
contents,
languages,
...otherProps
}: Props): JSX.Element => {
const hoverable = useMediaHoverable();
const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod
);
const {
state: keepInfoVisible,
toggleState: toggleKeepInfoVisible,
setState: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const {
state: combineRelatedContent,
toggleState: toggleCombineRelatedContent,
setState: setCombineRelatedContent,
} = useBoolean(DEFAULT_FILTERS_STATE.combineRelatedContent);
const [searchName, setSearchName] = useState(
DEFAULT_FILTERS_STATE.searchName
);
const effectiveCombineRelatedContent = useMemo(
() => (searchName.length > 1 ? false : combineRelatedContent),
[combineRelatedContent, searchName.length]
);
const groupingFunction = useCallback(
(
item: SelectiveNonNullable<
NonNullable<GetContentsQuery["contents"]>["data"][number],
"attributes" | "id"
>
): string[] => {
switch (groupingMethod) {
case 0: {
const categories = filterHasAttributes(
item.attributes.categories?.data,
["attributes"] as const
);
if (categories.length > 0) {
return categories.map((category) => category.attributes.name);
}
return [langui.no_category ?? "No category"];
}
case 1: {
return [
item.attributes.type?.data?.attributes?.titles?.[0]?.title ??
item.attributes.type?.data?.attributes?.slug
? prettySlug(item.attributes.type.data.attributes.slug)
: langui.no_type ?? "No type",
];
}
default: {
return [""];
}
}
},
[groupingMethod, langui]
);
const filteringFunction = useCallback(
(
item: SelectiveNonNullable<Props["contents"][number], "attributes" | "id">
) => {
if (
effectiveCombineRelatedContent &&
item.attributes.group?.data?.attributes?.combine === true &&
item.attributes.group.data.attributes.contents?.data[0].id !== item.id
) {
return false;
}
if (searchName.length > 1) {
if (
filterDefined(item.attributes.translations).find((translation) =>
prettyInlineTitle(
translation.pre_title,
translation.title,
translation.subtitle
)
.toLowerCase()
.includes(searchName.toLowerCase())
)
) {
return true;
}
return false;
}
return true;
},
[effectiveCombineRelatedContent, searchName]
);
const groupCountingFunction = useCallback(
(
item: SelectiveNonNullable<Props["contents"][number], "attributes" | "id">
) =>
item.attributes.group?.data?.attributes?.combine &&
effectiveCombineRelatedContent
? item.attributes.group.data.attributes.contents?.data.length ?? 1
: 1,
[effectiveCombineRelatedContent]
);
const subPanel = useMemo(
() => (
<SubPanel>
<PanelHeader
icon={Icon.Workspaces}
title={langui.contents}
description={langui.contents_description}
/>
<TextInput
className="mb-6 w-full"
placeholder={langui.search_title ?? "Search..."}
value={searchName}
onChange={setSearchName}
/>
<WithLabel
label={langui.group_by}
input={
<Select
className="w-full"
options={[langui.category ?? "Category", langui.type ?? "Type"]}
value={groupingMethod}
onChange={setGroupingMethod}
allowEmpty
/>
}
/>
<WithLabel
label={langui.combine_related_contents}
disabled={searchName.length > 1}
input={
<Switch
value={effectiveCombineRelatedContent}
onClick={toggleCombineRelatedContent}
/>
}
/>
{hoverable && (
<WithLabel
label={langui.always_show_info}
input={
<Switch onClick={toggleKeepInfoVisible} value={keepInfoVisible} />
}
/>
)}
<Button
className="mt-8"
text={langui.reset_all_filters}
icon={Icon.Replay}
onClick={() => {
setSearchName(DEFAULT_FILTERS_STATE.searchName);
setGroupingMethod(DEFAULT_FILTERS_STATE.groupingMethod);
setKeepInfoVisible(DEFAULT_FILTERS_STATE.keepInfoVisible);
setCombineRelatedContent(
DEFAULT_FILTERS_STATE.combineRelatedContent
);
}}
/>
</SubPanel>
),
[
effectiveCombineRelatedContent,
groupingMethod,
hoverable,
keepInfoVisible,
langui.always_show_info,
langui.category,
langui.combine_related_contents,
langui.contents,
langui.contents_description,
langui.group_by,
langui.reset_all_filters,
langui.search_title,
langui.type,
searchName,
setCombineRelatedContent,
setKeepInfoVisible,
toggleCombineRelatedContent,
toggleKeepInfoVisible,
]
);
const contentPanel = useMemo(
() => (
<ContentPanel width={ContentPanelWidthSizes.Full}>
<SmartList
items={filterHasAttributes(contents, ["attributes", "id"] 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
stackNumber={
effectiveCombineRelatedContent &&
item.attributes.group?.data?.attributes?.combine === true
? item.attributes.group.data.attributes.contents?.data.length
: 0
}
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={keepInfoVisible}
/>
)}
className="grid-cols-2 desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))]"
groupingFunction={groupingFunction}
groupCountingFunction={groupCountingFunction}
filteringFunction={filteringFunction}
searchingTerm={searchName}
searchingBy={(item) =>
`
${item.attributes.slug}
${filterDefined(item.attributes.translations)
.map((translation) =>
prettyInlineTitle(
translation.pre_title,
translation.title,
translation.subtitle
)
)
.join(" ")}`
}
langui={langui}
/>
</ContentPanel>
),
[
contents,
effectiveCombineRelatedContent,
filteringFunction,
groupCountingFunction,
groupingFunction,
keepInfoVisible,
langui,
searchName,
]
);
return (
<AppLayout
subPanel={subPanel}
contentPanel={contentPanel}
subPanelIcon={Icon.Search}
languages={languages}
langui={langui}
{...otherProps}
/>
);
};
const Contents = (props: Parameters<typeof ContentsFolder>[0]): JSX.Element => (
<ContentsFolder {...props} />
);
export default Contents;
/*
@ -335,27 +19,6 @@ export default Contents;
*/
export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk();
const contents = await sdk.getContents({
language_code: context.locale ?? "en",
});
if (!contents.contents) return { notFound: true };
contents.contents.data.sort((a, b) => {
const titleA = a.attributes?.slug ?? "";
const titleB = b.attributes?.slug ?? "";
return titleA.localeCompare(titleB);
});
const appStaticProps = await getAppStaticProps(context);
const props: Props = {
...appStaticProps,
contents: contents.contents.data,
openGraph: getOpenGraph(
appStaticProps.langui,
appStaticProps.langui.contents ?? "Contents"
),
};
return {
props: props,
};
context.params = { slug: "root" };
return await folderGetStaticProps(context);
};

View File

@ -451,6 +451,7 @@ const Library = ({
)
}
filteringFunction={filteringFunction}
paginationItemPerPage={25}
langui={langui}
/>
</ContentPanel>

View File

@ -139,6 +139,7 @@ const News = ({ langui, posts, ...otherProps }: Props): JSX.Element => {
?.map((translation) => translation?.title)
.join(" ")}`
}
paginationItemPerPage={25}
/>
</ContentPanel>
),

View File

@ -212,6 +212,7 @@ const Wiki = ({ langui, pages, ...otherProps }: Props): JSX.Element => {
.join(" ")
}
groupingFunction={groupingFunction}
paginationItemPerPage={25}
/>
</ContentPanel>
),