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", destination: "https://gallery.accords-library.com/posts",
permanent: false, 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 { AppStaticProps } from "graphql/getAppStaticProps";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
import { OpenGraph } from "helpers/openGraph"; import { OpenGraph, TITLE_PREFIX } from "helpers/openGraph";
import { getDefaultPreferredLanguages } from "helpers/locales"; import { getDefaultPreferredLanguages } from "helpers/locales";
import useIsClient from "hooks/useIsClient"; import useIsClient from "hooks/useIsClient";
import { useBoolean } from "hooks/useBoolean"; 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> </p>
{isDefined(subPanel) && !turnSubIntoContent && ( {isDefined(subPanel) && !turnSubIntoContent && (
<Ico <Ico

View File

@ -21,6 +21,7 @@ interface Props {
onClick?: MouseEventHandler<HTMLDivElement>; onClick?: MouseEventHandler<HTMLDivElement>;
draggable?: boolean; draggable?: boolean;
badgeNumber?: number; badgeNumber?: number;
disabled?: boolean;
size?: "normal" | "small"; size?: "normal" | "small";
} }
@ -37,6 +38,7 @@ export const Button = ({
href, href,
alwaysNewTab = false, alwaysNewTab = false,
badgeNumber, badgeNumber,
disabled,
size = "normal", size = "normal",
}: Props): JSX.Element => ( }: Props): JSX.Element => (
<ConditionalWrapper <ConditionalWrapper
@ -49,6 +51,7 @@ export const Button = ({
draggable={draggable} draggable={draggable}
id={id} id={id}
onClick={onClick} onClick={onClick}
onFocus={(event) => event.target.blur()}
className={cJoin( className={cJoin(
`group grid cursor-pointer select-none grid-flow-col place-content-center `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 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` active:hover:bg-black active:hover:!text-light active:hover:drop-shadow-black-lg`
), ),
cIf(size === "small", "px-3 py-1 text-xs"), cIf(size === "small", "px-3 py-1 text-xs"),
cIf(disabled, "cursor-not-allowed"),
className className
)} )}
> >

View File

@ -36,7 +36,7 @@ export const Link = ({
} else if (isValidClick && href) { } else if (isValidClick && href) {
if (event.button !== MouseButton.Right) { if (event.button !== MouseButton.Right) {
if (alwaysNewTab) { if (alwaysNewTab) {
window.open(href, "_blank"); window.open(href, "_blank", "noopener");
} else if (event.button === MouseButton.Left) { } else if (event.button === MouseButton.Left) {
if (href.startsWith("#")) { if (href.startsWith("#")) {
router.replace(href); router.replace(href);

View File

@ -10,6 +10,7 @@ import { cJoin } from "helpers/className";
interface Props { interface Props {
className?: string; className?: string;
page: number; page: number;
pagesCount: number;
onChange: (value: number) => void; onChange: (value: number) => void;
} }
@ -18,14 +19,33 @@ interface Props {
export const PageSelector = ({ export const PageSelector = ({
page, page,
className, className,
pagesCount,
onChange, onChange,
}: Props): JSX.Element => ( }: Props): JSX.Element => (
<ButtonGroup <ButtonGroup
className={cJoin("flex flex-row place-content-center", className)} className={cJoin("flex flex-row place-content-center", className)}
buttonsProps={[ buttonsProps={[
{ onClick: () => onChange(page - 1), icon: Icon.NavigateBefore }, {
{ text: (page + 1).toString() }, onClick: () => onChange(0),
{ onClick: () => onChange(page + 1), icon: Icon.NavigateNext }, 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 { Ico, Icon } from "./Ico";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { isDefined, isDefinedAndNotEmpty, iterateMap } from "helpers/others"; import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; 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> { interface Props<T> {
// Items // Items
items: T[]; items: T[];
@ -24,7 +37,7 @@ interface Props<T> {
searchingCaseInsensitive?: boolean; searchingCaseInsensitive?: boolean;
// Grouping // Grouping
groupingFunction?: (item: T) => string[]; groupingFunction?: (item: T) => string[];
groupSortingFunction?: (a: string, b: string) => number; groupSortingFunction?: (a: Group<T>, b: Group<T>) => number;
groupCountingFunction?: (item: T) => number; groupCountingFunction?: (item: T) => number;
// Filtering // Filtering
filteringFunction?: (item: T) => boolean; filteringFunction?: (item: T) => boolean;
@ -40,28 +53,28 @@ export const SmartList = <T,>({
getItemId, getItemId,
renderItem: RenderItem, renderItem: RenderItem,
renderWhenEmpty: RenderWhenEmpty, renderWhenEmpty: RenderWhenEmpty,
paginationItemPerPage = 0, paginationItemPerPage = Infinity,
paginationSelectorTop = true, paginationSelectorTop = true,
paginationSelectorBottom = true, paginationSelectorBottom = true,
paginationScroolTop = true, paginationScroolTop = true,
searchingTerm, searchingTerm,
searchingBy, searchingBy,
searchingCaseInsensitive = true, searchingCaseInsensitive = true,
groupingFunction = () => [""], groupingFunction = defaultGroupingFunction,
groupSortingFunction = (a, b) => a.localeCompare(b), groupSortingFunction = defaultGroupSortingFunction,
groupCountingFunction = () => 1, groupCountingFunction = defaultGroupCountingFunction,
filteringFunction = () => true, filteringFunction = defaultFilteringFunction,
sortingFunction = () => 0, sortingFunction = defaultSortingFunction,
className, className,
langui, langui,
}: Props<T>): JSX.Element => { }: Props<T>): JSX.Element => {
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
useScrollTopOnChange(AnchorIds.ContentPanel, [page], paginationScroolTop); useScrollTopOnChange(AnchorIds.ContentPanel, [page], paginationScroolTop);
useEffect(
type Group = Map<string, T[]>; () => setPage(0),
[searchingTerm, groupingFunction, groupSortingFunction, items]
useEffect(() => setPage(0), [searchingTerm]); );
const searchFilter = useCallback(() => { const searchFilter = useCallback(() => {
if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) { if (isDefinedAndNotEmpty(searchingTerm) && isDefined(searchingBy)) {
@ -85,87 +98,104 @@ export const SmartList = <T,>({
[filteredItems, sortingFunction] [filteredItems, sortingFunction]
); );
const paginatedItems = useMemo(() => { const groups = useMemo(() => {
if (paginationItemPerPage > 0) { const memo: Group<T>[] = [];
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 groupedList = useMemo(() => { sortedItem.forEach((item) => {
const groups: Group = new Map();
paginatedItems[page]?.forEach((item) => {
groupingFunction(item).forEach((category) => { groupingFunction(item).forEach((category) => {
if (groups.has(category)) { const index = memo.findIndex((group) => group.name === category);
groups.get(category)?.push(item); if (index === -1) {
memo.push({
name: category,
items: [item],
totalCount: groupCountingFunction(item),
});
} else { } else {
groups.set(category, [item]); memo[index].items.push(item);
memo[index].totalCount += groupCountingFunction(item);
} }
}); });
}); });
return groups; return memo.sort(groupSortingFunction);
}, [groupingFunction, page, paginatedItems]); }, [
groupCountingFunction,
groupSortingFunction,
groupingFunction,
sortedItem,
]);
const pageCount = useMemo( const pages = useMemo(() => {
() => const memo: Group<T>[][] = [];
paginationItemPerPage > 0 let currentPage: Group<T>[] = [];
? Math.floor(filteredItems.length / paginationItemPerPage) let remainingSlots = paginationItemPerPage;
: 1, let loopSafeguard = 1000;
[paginationItemPerPage, filteredItems.length]
);
const changePage = useCallback( const newPage = () => {
(newPage: number) => memo.push(currentPage);
setPage(() => { currentPage = [];
if (newPage <= 0) { remainingSlots = paginationItemPerPage;
return 0; };
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; 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);
} }
return newPage; }
}),
[pageCount] if (currentPage.length > 0) {
); newPage();
}
return memo;
}, [groups, paginationItemPerPage]);
return ( return (
<> <>
{pageCount > 1 && paginationSelectorTop && ( {pages.length > 1 && paginationSelectorTop && (
<PageSelector className="mb-12" page={page} onChange={changePage} /> <PageSelector
className="mb-12"
page={page}
pagesCount={pages.length}
onChange={setPage}
/>
)} )}
<div className="mb-8"> <div className="mb-8">
{groupedList.size > 0 ? ( {pages[page]?.length > 0 ? (
iterateMap( pages[page]?.map(
groupedList, (group) =>
(name, groupItems) => group.items.length > 0 && (
groupItems.length > 0 && ( <Fragment key={group.name}>
<Fragment key={name}> {group.name.length > 0 && (
{name.length > 0 && (
<h2 <h2
className="flex flex-row place-items-center gap-2 pb-2 pt-10 text-2xl 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 <Chip
text={`${groupItems.reduce( text={`${group.totalCount} ${
(acc, item) => acc + groupCountingFunction(item), group.items.length <= 1
0
)} ${
groupItems.length <= 1
? langui.result?.toLowerCase() ?? "" ? langui.result?.toLowerCase() ?? ""
: langui.results?.toLowerCase() ?? "" : langui.results?.toLowerCase() ?? ""
}`} }`}
@ -179,13 +209,12 @@ export const SmartList = <T,>({
className className
)} )}
> >
{groupItems.map((item) => ( {group.items.map((item) => (
<RenderItem item={item} key={getItemId(item)} /> <RenderItem item={item} key={getItemId(item)} />
))} ))}
</div> </div>
</Fragment> </Fragment>
), )
([a], [b]) => groupSortingFunction(a, b)
) )
) : isDefined(RenderWhenEmpty) ? ( ) : isDefined(RenderWhenEmpty) ? (
<RenderWhenEmpty /> <RenderWhenEmpty />
@ -194,8 +223,13 @@ export const SmartList = <T,>({
)} )}
</div> </div>
{pageCount > 1 && paginationSelectorBottom && ( {pages.length > 1 && paginationSelectorBottom && (
<PageSelector className="mb-12" page={page} onChange={changePage} /> <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" border-dark p-8 text-dark opacity-40"
> >
<Ico icon={Icon.ChevronLeft} className="!text-[300%] mobile:hidden" /> <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" /> <Ico icon={Icon.ChevronRight} className="!text-[300%] desktop:hidden" />
</div> </div>
</div> </div>

View File

@ -4,7 +4,9 @@ import { ScanSet } from "./Library/ScanSet";
import { NavOption } from "./PanelComponents/NavOption"; import { NavOption } from "./PanelComponents/NavOption";
import { ChroniclePreview } from "./Chronicles/ChroniclePreview"; import { ChroniclePreview } from "./Chronicles/ChroniclePreview";
import { ChroniclesList } from "./Chronicles/ChroniclesList"; import { ChroniclesList } from "./Chronicles/ChroniclesList";
import { Button } from "./Inputs/Button";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { PreviewFolder } from "pages/contents/folder/[slug]";
export type TranslatedProps<P, K extends keyof P> = Omit<P, K> & { export type TranslatedProps<P, K extends keyof P> = Omit<P, K> & {
translations: (Pick<P, K> & { language: string })[]; 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 { data {
attributes { attributes {
code
}
}
}
title
}
sequence
contents(pagination: { limit: -1 }) { contents(pagination: { limit: -1 }) {
data { data {
attributes { attributes {

View File

@ -62,18 +62,6 @@ query getContents($language_code: String) {
} }
} }
} }
group {
data {
attributes {
combine
contents(pagination: { limit: -1 }) {
data {
id
}
}
}
}
}
thumbnail { thumbnail {
data { data {
attributes { 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 no_source_warning
copy_anchor_link copy_anchor_link
anchor_link_copied anchor_link_copied
folders
empty_folder_message
} }
} }
} }

View File

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

View File

@ -99,7 +99,6 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => {
items={filterHasAttributes(videos, ["id", "attributes"] as const)} items={filterHasAttributes(videos, ["id", "attributes"] as const)}
getItemId={(item) => item.id} getItemId={(item) => item.id}
renderItem={({ item }) => ( renderItem={({ item }) => (
<>
<PreviewCard <PreviewCard
href={`/archives/videos/v/${item.attributes.uid}`} href={`/archives/videos/v/${item.attributes.uid}`}
title={item.attributes.title} title={item.attributes.title}
@ -118,14 +117,13 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => {
duration: item.attributes.duration, duration: item.attributes.duration,
}} }}
/> />
</>
)} )}
langui={langui}
className="desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2 className="desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:grid-cols-2
thin:grid-cols-1" thin:grid-cols-1"
paginationItemPerPage={20} paginationItemPerPage={25}
searchingTerm={searchName} searchingTerm={searchName}
searchingBy={(item) => item.attributes.title} searchingBy={(item) => item.attributes.title}
langui={langui}
/> />
</ContentPanel> </ContentPanel>
), ),

View File

@ -33,7 +33,10 @@ import { ContentWithTranslations } from "helpers/types";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { TranslatedPreviewLine } from "components/Translated"; import {
TranslatedPreviewFolder,
TranslatedPreviewLine,
} from "components/Translated";
import { getOpenGraph } from "helpers/openGraph"; import { getOpenGraph } from "helpers/openGraph";
import { import {
getDefaultPreferredLanguages, getDefaultPreferredLanguages,
@ -74,32 +77,51 @@ const Content = ({
const { previousContent, nextContent } = useMemo( const { previousContent, nextContent } = useMemo(
() => ({ () => ({
previousContent: content.group?.data?.attributes?.contents previousContent:
content.folders?.data[0]?.attributes?.contents &&
content.folders.data[0].attributes.sequence
? getPreviousContent( ? getPreviousContent(
content.group.data.attributes.contents.data, content.folders.data[0].attributes.contents.data,
content.slug content.slug
) )
: undefined, : undefined,
nextContent: content.group?.data?.attributes?.contents nextContent:
content.folders?.data[0]?.attributes?.contents &&
content.folders.data[0].attributes.sequence
? getNextContent( ? getNextContent(
content.group.data.attributes.contents.data, content.folders.data[0].attributes.contents.data,
content.slug content.slug
) )
: undefined, : undefined,
}), }),
[content.group, content.slug] [content.folders, content.slug]
); );
const subPanel = useMemo( const subPanel = useMemo(
() => ( () => (
<SubPanel> <SubPanel>
<ReturnButton {content.folders?.data && content.folders.data.length > 0 && (
href={`/contents`} <>
title={langui.contents} <h2 className="mb-2 text-xl">{langui.folders}</h2>
langui={langui} {filterHasAttributes(content.folders.data, [
displayOn={ReturnButtonType.Desktop} "attributes",
horizontalLine "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 {selectedTranslation?.text_set?.source_language?.data?.attributes
?.code !== undefined && ( ?.code !== undefined && (
@ -308,6 +330,7 @@ const Content = ({
</SubPanel> </SubPanel>
), ),
[ [
content.folders?.data,
content.ranged_contents?.data, content.ranged_contents?.data,
currencies, currencies,
languages, languages,
@ -320,7 +343,7 @@ const Content = ({
() => ( () => (
<ContentPanel> <ContentPanel>
<ReturnButton <ReturnButton
href={`/contents`} href={`/contents/folder`}
title={langui.contents} title={langui.contents}
langui={langui} langui={langui}
displayOn={ReturnButtonType.Mobile} displayOn={ReturnButtonType.Mobile}
@ -579,19 +602,19 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
* PRIVATE METHODS * PRIVATE METHODS
*/ */
type Group = NonNullable< type FolderContents = NonNullable<
NonNullable< NonNullable<
NonNullable< NonNullable<
NonNullable<ContentWithTranslations["group"]>["data"] NonNullable<ContentWithTranslations["folders"]>["data"][number]
>["attributes"] >["attributes"]
>["contents"] >["contents"]
>["data"]; >["data"];
const getPreviousContent = (group: Group, currentSlug: string) => { const getPreviousContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < group.length; index++) { for (let index = 0; index < contents.length; index++) {
const content = group[index]; const content = contents[index];
if (content.attributes?.slug === currentSlug && index > 0) { if (content.attributes?.slug === currentSlug && index > 0) {
return group[index - 1]; return contents[index - 1];
} }
} }
return undefined; return undefined;
@ -599,11 +622,14 @@ const getPreviousContent = (group: Group, currentSlug: string) => {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const getNextContent = (group: Group, currentSlug: string) => { const getNextContent = (contents: FolderContents, currentSlug: string) => {
for (let index = 0; index < group.length; index++) { for (let index = 0; index < contents.length; index++) {
const content = group[index]; const content = contents[index];
if (content.attributes?.slug === currentSlug && index < group.length - 1) { if (
return group[index + 1]; content.attributes?.slug === currentSlug &&
index < contents.length - 1
) {
return contents[index + 1];
} }
} }
return undefined; 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 { GetStaticProps } from "next";
import { useState, useMemo, useCallback } from "react"; import ContentsFolder, {
import { AppLayout, AppLayoutRequired } from "components/AppLayout"; getStaticProps as folderGetStaticProps,
import { Select } from "components/Inputs/Select"; } from "./folder/[slug]";
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: "",
};
/* /*
* *
* PAGE * PAGE
*/ */
interface Props extends AppStaticProps, AppLayoutRequired { const Contents = (props: Parameters<typeof ContentsFolder>[0]): JSX.Element => (
contents: NonNullable<GetContentsQuery["contents"]>["data"]; <ContentsFolder {...props} />
} );
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}
/>
);
};
export default Contents; export default Contents;
/* /*
@ -335,27 +19,6 @@ export default Contents;
*/ */
export const getStaticProps: GetStaticProps = async (context) => { export const getStaticProps: GetStaticProps = async (context) => {
const sdk = getReadySdk(); context.params = { slug: "root" };
const contents = await sdk.getContents({ return await folderGetStaticProps(context);
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

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

View File

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

View File

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