import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel"; import { getOpenGraph } from "helpers/openGraph"; import { getReadySdk } from "graphql/sdk"; import { filterHasAttributes } from "helpers/asserts"; import { GetContentsFolderQuery, ParentFolderPreviewFragment } from "graphql/generated"; import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales"; import { prettySlug } from "helpers/formatters"; import { Button } from "components/Inputs/Button"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { SubPanel } from "components/Containers/SubPanel"; import { TranslatedPreviewCard } from "components/PreviewCard"; import { HorizontalLine } from "components/HorizontalLine"; import { cJoin, cIf } from "helpers/className"; import { atoms } from "contexts/atoms"; import { useAtomGetter, useAtomSetter } from "helpers/atoms"; import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder"; import { useFormat } from "hooks/useFormat"; import { getFormat } from "helpers/i18n"; import { Chip } from "components/Chip"; import { FolderPath } from "components/Contents/FolderPath"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ interface Props extends AppLayoutRequired { folder: NonNullable< NonNullable<GetContentsFolderQuery["contentsFolders"]>["data"][number]["attributes"] >; path: ParentFolderPreviewFragment[]; } const ContentsFolder = ({ openGraph, folder, path, ...otherProps }: Props): JSX.Element => { const { format, formatCategory, formatContentType } = useFormat(); const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened); const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl); const subPanel = ( <SubPanel> <PanelHeader icon="workspaces" title={format("contents")} description={format("contents_description")} /> <HorizontalLine /> <Button href="/contents/all" text={format("switch_to_grid_view")} icon="apps" onClick={() => setSubPanelOpened(false)} /> </SubPanel> ); const contentPanel = ( <ContentPanel width={ContentPanelWidthSizes.Full}> <FolderPath path={path} /> {folder.subfolders?.data && folder.subfolders.data.length > 0 && ( <div className="mb-8"> <div className="mb-2 flex place-items-center gap-2"> <h2 className="text-2xl">{format("folders")}</h2> <Chip text={format("x_results", { x: folder.subfolders.data.length })} /> </div> <div className={cJoin( "grid items-start pb-12", cIf( isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-2 gap-4" ) )}> {filterHasAttributes(folder.subfolders.data, ["id", "attributes"]).map((subfolder) => ( <TranslatedPreviewFolder key={subfolder.id} href={`/contents/folder/${subfolder.attributes.slug}`} translations={filterHasAttributes(subfolder.attributes.titles, [ "language.data.attributes.code", ]).map((title) => ({ title: title.title, language: title.language.data.attributes.code, }))} fallback={{ title: prettySlug(subfolder.attributes.slug) }} /> ))} </div> </div> )} {folder.contents?.data && folder.contents.data.length > 0 && ( <div className="mb-8"> <div className="mb-2 flex place-items-center gap-2"> <h2 className="text-2xl">{format("contents")}</h2> <Chip text={format("x_results", { x: folder.contents.data.length })} /> </div> <div className={cJoin( "grid items-start pb-12", cIf( isContentPanelAtLeast4xl, "grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] gap-x-6 gap-y-8", "grid-cols-2 gap-4" ) )}> {filterHasAttributes(folder.contents.data, ["id", "attributes"]).map((item) => ( <TranslatedPreviewCard key={item.id} href={`/contents/${item.attributes.slug}`} translations={filterHasAttributes(item.attributes.translations, [ "language.data.attributes.code", ]).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 ? [formatContentType(item.attributes.type.data.attributes.slug)] : undefined } bottomChips={filterHasAttributes(item.attributes.categories?.data, [ "attributes", ]).map((category) => formatCategory(category.attributes.slug))} keepInfoVisible /> ))} </div> </div> )} {folder.contents?.data.length === 0 && folder.subfolders?.data.length === 0 && ( <NoContentNorFolderMessage /> )} </ContentPanel> ); return ( <AppLayout subPanel={subPanel} contentPanel={contentPanel} openGraph={openGraph} {...otherProps} /> ); }; export default ContentsFolder; /* * ╭──────────────────────╮ * ───────────────────────────────────╯ NEXT DATA FETCHING ╰────────────────────────────────────── */ export const getStaticProps: GetStaticProps = async (context) => { const sdk = getReadySdk(); const { format } = getFormat(context.locale); const slug = context.params?.slug ? context.params.slug.toString() : ""; const contentsFolder = await sdk.getContentsFolder({ slug: slug }); if (!contentsFolder.contentsFolders?.data[0]?.attributes) { return { notFound: true }; } const folder = contentsFolder.contentsFolders.data[0].attributes; const title = (() => { if (slug === "root") { return format("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 = { openGraph: getOpenGraph(format, title), folder, path: getRecursiveParentFolderPreview(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"]).map((item) => { context.locales?.map((local) => { paths.push({ params: { slug: item.attributes.slug }, locale: local, }); }); }); return { paths, fallback: "blocking", }; }; /* * ╭──────────────────────╮ * ───────────────────────────────────╯ PRIVATE COMPONENTS ╰────────────────────────────────────── */ const NoContentNorFolderMessage = () => { const { format } = useFormat(); return ( <div className="grid place-content-center"> <div className="mt-12 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">{format("empty_folder_message")}</p> </div> </div> ); }; /* * ╭───────────────────╮ * ─────────────────────────────────────╯ PRIVATE METHODS ╰─────────────────────────────────────── */ type ParentFolderWithParentFolder = ParentFolderPreviewFragment & { parent_folder?: { data?: { attributes?: ParentFolderPreviewFragment | ParentFolderWithParentFolder | null; } | null; } | null; }; const getRecursiveParentFolderPreview = ( parentFolder: ParentFolderWithParentFolder ): ParentFolderPreviewFragment[] => [ ...(parentFolder.parent_folder?.data?.attributes ? getRecursiveParentFolderPreview(parentFolder.parent_folder.data.attributes) : []), { slug: parentFolder.slug, titles: parentFolder.titles }, ];