487 lines
20 KiB
TypeScript
487 lines
20 KiB
TypeScript
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
|
import { Fragment, useCallback } from "react";
|
|
import naturalCompare from "string-natural-compare";
|
|
import { AppLayout, AppLayoutRequired } from "components/AppLayout";
|
|
import { Chip } from "components/Chip";
|
|
import { HorizontalLine } from "components/HorizontalLine";
|
|
import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs";
|
|
import { Markdawn, TableOfContents } from "components/Markdown/Markdawn";
|
|
import { TranslatedReturnButton } from "components/PanelComponents/ReturnButton";
|
|
import { ContentPanel } from "components/Containers/ContentPanel";
|
|
import { SubPanel } from "components/Containers/SubPanel";
|
|
import { PreviewCard } from "components/PreviewCard";
|
|
import { RecorderChip } from "components/RecorderChip";
|
|
import { ThumbnailHeader } from "components/ThumbnailHeader";
|
|
import { ToolTip } from "components/ToolTip";
|
|
import { getReadySdk } from "graphql/sdk";
|
|
import {
|
|
prettyInlineTitle,
|
|
prettyLanguage,
|
|
prettyItemSubType,
|
|
prettySlug,
|
|
} from "helpers/formatters";
|
|
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
|
import { filterHasAttributes, isDefinedAndNotEmpty } from "helpers/asserts";
|
|
import { ContentWithTranslations } from "types/types";
|
|
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
|
import { getOpenGraph } from "helpers/openGraph";
|
|
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
|
|
import { getDescription } from "helpers/description";
|
|
import { TranslatedPreviewLine } from "components/PreviewLine";
|
|
import { cIf } from "helpers/className";
|
|
import { Ids } from "types/ids";
|
|
import { atoms } from "contexts/atoms";
|
|
import { useAtomGetter } from "helpers/atoms";
|
|
import { useFormat } from "hooks/useFormat";
|
|
import { getFormat } from "helpers/i18n";
|
|
|
|
/*
|
|
* ╭────────╮
|
|
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
|
*/
|
|
|
|
interface Props extends AppLayoutRequired {
|
|
content: ContentWithTranslations;
|
|
}
|
|
|
|
const Content = ({ content, ...otherProps }: Props): JSX.Element => {
|
|
const isContentPanelAtLeast2xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast2xl);
|
|
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
|
|
|
|
const { format, formatStatusDescription } = useFormat();
|
|
const languages = useAtomGetter(atoms.localData.languages);
|
|
|
|
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
|
|
items: content.translations,
|
|
languageExtractor: useCallback(
|
|
(item: NonNullable<Props["content"]["translations"][number]>) =>
|
|
item.language?.data?.attributes?.code,
|
|
[]
|
|
),
|
|
});
|
|
|
|
useScrollTopOnChange(Ids.ContentPanel, [selectedTranslation]);
|
|
|
|
const previousContent =
|
|
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
|
|
? getPreviousContent(content.folder.data.attributes.contents.data, content.slug)
|
|
: undefined;
|
|
const nextContent =
|
|
content.folder?.data?.attributes?.contents && content.folder.data.attributes.sequence
|
|
? getNextContent(content.folder.data.attributes.contents.data, content.slug)
|
|
: undefined;
|
|
|
|
const returnButtonProps = {
|
|
href: content.folder?.data?.attributes
|
|
? `/contents/folder/${content.folder.data.attributes.slug}`
|
|
: "/contents",
|
|
|
|
translations: filterHasAttributes(content.folder?.data?.attributes?.titles, [
|
|
"language.data.attributes.code",
|
|
] as const).map((title) => ({
|
|
language: title.language.data.attributes.code,
|
|
title: title.title,
|
|
})),
|
|
fallback: {
|
|
title: content.folder?.data?.attributes
|
|
? prettySlug(content.folder.data.attributes.slug)
|
|
: format("contents"),
|
|
},
|
|
};
|
|
|
|
const subPanel = (
|
|
<SubPanel>
|
|
<TranslatedReturnButton {...returnButtonProps} displayOnlyOn="3ColumnsLayout" />
|
|
|
|
{selectedTranslation?.text_set?.source_language?.data?.attributes?.code !== undefined && (
|
|
<>
|
|
<HorizontalLine />
|
|
<div className="grid gap-5">
|
|
<h2 className="text-xl">
|
|
{selectedTranslation.text_set.source_language.data.attributes.code ===
|
|
selectedTranslation.language?.data?.attributes?.code
|
|
? format("transcript_notice")
|
|
: format("translation_notice")}
|
|
</h2>
|
|
|
|
{selectedTranslation.text_set.source_language.data.attributes.code !==
|
|
selectedTranslation.language?.data?.attributes?.code && (
|
|
<div className="grid place-items-center gap-2">
|
|
<p className="font-headers font-bold">{format("source_language")}:</p>
|
|
<Chip
|
|
text={prettyLanguage(
|
|
selectedTranslation.text_set.source_language.data.attributes.code,
|
|
languages
|
|
)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
|
<p className="font-headers font-bold">{format("status")}:</p>
|
|
|
|
<ToolTip
|
|
content={formatStatusDescription(selectedTranslation.text_set.status)}
|
|
maxWidth={"20rem"}>
|
|
<Chip text={selectedTranslation.text_set.status} />
|
|
</ToolTip>
|
|
</div>
|
|
|
|
{selectedTranslation.text_set.transcribers &&
|
|
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
|
<div>
|
|
<p className="font-headers font-bold">{format("transcribers")}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
{filterHasAttributes(selectedTranslation.text_set.transcribers.data, [
|
|
"attributes",
|
|
"id",
|
|
] as const).map((recorder) => (
|
|
<Fragment key={recorder.id}>
|
|
<RecorderChip recorder={recorder.attributes} />
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedTranslation.text_set.translators &&
|
|
selectedTranslation.text_set.translators.data.length > 0 && (
|
|
<div>
|
|
<p className="font-headers font-bold">{format("translators")}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
{filterHasAttributes(selectedTranslation.text_set.translators.data, [
|
|
"attributes",
|
|
"id",
|
|
] as const).map((recorder) => (
|
|
<Fragment key={recorder.id}>
|
|
<RecorderChip recorder={recorder.attributes} />
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedTranslation.text_set.proofreaders &&
|
|
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
|
<div>
|
|
<p className="font-headers font-bold">{format("proofreaders")}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
{filterHasAttributes(selectedTranslation.text_set.proofreaders.data, [
|
|
"attributes",
|
|
"id",
|
|
] as const).map((recorder) => (
|
|
<Fragment key={recorder.id}>
|
|
<RecorderChip recorder={recorder.attributes} />
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
|
<div>
|
|
<p className="font-headers font-bold">{format("notes")}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
<Markdawn text={selectedTranslation.text_set.notes} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{selectedTranslation?.text_set?.text && (
|
|
<>
|
|
<TableOfContents
|
|
text={selectedTranslation.text_set.text}
|
|
title={prettyInlineTitle(
|
|
selectedTranslation.pre_title,
|
|
selectedTranslation.title,
|
|
selectedTranslation.subtitle
|
|
)}
|
|
horizontalLine
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{content.ranged_contents?.data && content.ranged_contents.data.length > 0 && (
|
|
<>
|
|
<HorizontalLine />
|
|
<div>
|
|
<p className="font-headers text-2xl font-bold">{format("source")}</p>
|
|
<div className="mt-6 grid place-items-center gap-6">
|
|
{filterHasAttributes(content.ranged_contents.data, [
|
|
"attributes.library_item.data.attributes",
|
|
"attributes.library_item.data.id",
|
|
] as const).map((rangedContent) => {
|
|
const libraryItem = rangedContent.attributes.library_item.data;
|
|
return (
|
|
<div key={libraryItem.attributes.slug} className={cIf(is1ColumnLayout, "w-3/4")}>
|
|
<PreviewCard
|
|
href={`/library/${libraryItem.attributes.slug}`}
|
|
title={libraryItem.attributes.title}
|
|
subtitle={libraryItem.attributes.subtitle}
|
|
thumbnail={libraryItem.attributes.thumbnail?.data?.attributes}
|
|
thumbnailAspectRatio="21/29.7"
|
|
thumbnailRounded={false}
|
|
topChips={
|
|
libraryItem.attributes.metadata &&
|
|
libraryItem.attributes.metadata.length > 0 &&
|
|
libraryItem.attributes.metadata[0]
|
|
? [prettyItemSubType(libraryItem.attributes.metadata[0])]
|
|
: []
|
|
}
|
|
bottomChips={filterHasAttributes(libraryItem.attributes.categories?.data, [
|
|
"attributes",
|
|
] as const).map((category) => category.attributes.short)}
|
|
metadata={{
|
|
releaseDate: libraryItem.attributes.release_date,
|
|
price: libraryItem.attributes.price,
|
|
position: "Bottom",
|
|
}}
|
|
infoAppend={
|
|
!isUntangibleGroupItem(libraryItem.attributes.metadata?.[0]) && (
|
|
<PreviewCardCTAs id={libraryItem.id} />
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</SubPanel>
|
|
);
|
|
|
|
const contentPanel = (
|
|
<ContentPanel>
|
|
<TranslatedReturnButton
|
|
{...returnButtonProps}
|
|
displayOnlyOn="1ColumnLayout"
|
|
className="mb-10"
|
|
/>
|
|
|
|
<div className="grid place-items-center">
|
|
<ThumbnailHeader
|
|
thumbnail={content.thumbnail?.data?.attributes}
|
|
pre_title={selectedTranslation?.pre_title}
|
|
title={selectedTranslation?.title}
|
|
subtitle={selectedTranslation?.subtitle}
|
|
description={selectedTranslation?.description}
|
|
type={content.type}
|
|
categories={content.categories}
|
|
languageSwitcher={
|
|
languageSwitcherProps.locales.size > 1 ? (
|
|
<LanguageSwitcher {...languageSwitcherProps} />
|
|
) : undefined
|
|
}
|
|
/>
|
|
|
|
{previousContent?.attributes && (
|
|
<div className="mt-12 mb-8 w-full">
|
|
<h2 className="mb-4 text-center text-2xl">{format("previous_content")}</h2>
|
|
<TranslatedPreviewLine
|
|
href={`/contents/${previousContent.attributes.slug}`}
|
|
translations={filterHasAttributes(previousContent.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(previousContent.attributes.slug),
|
|
}}
|
|
thumbnail={previousContent.attributes.thumbnail?.data?.attributes}
|
|
topChips={
|
|
isContentPanelAtLeast2xl && previousContent.attributes.type?.data?.attributes
|
|
? [
|
|
previousContent.attributes.type.data.attributes.titles?.[0]
|
|
? previousContent.attributes.type.data.attributes.titles[0]?.title
|
|
: prettySlug(previousContent.attributes.type.data.attributes.slug),
|
|
]
|
|
: undefined
|
|
}
|
|
bottomChips={
|
|
isContentPanelAtLeast2xl
|
|
? previousContent.attributes.categories?.data.map(
|
|
(category) => category.attributes?.short ?? ""
|
|
)
|
|
: undefined
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{selectedTranslation?.text_set?.text && (
|
|
<>
|
|
<HorizontalLine />
|
|
<Markdawn text={selectedTranslation.text_set.text} />
|
|
</>
|
|
)}
|
|
|
|
{nextContent?.attributes && (
|
|
<>
|
|
<HorizontalLine />
|
|
<h2 className="mb-4 text-center text-2xl">{format("followup_content")}</h2>
|
|
<TranslatedPreviewLine
|
|
href={`/contents/${nextContent.attributes.slug}`}
|
|
translations={filterHasAttributes(nextContent.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: nextContent.attributes.slug }}
|
|
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
|
|
topChips={
|
|
isContentPanelAtLeast2xl && nextContent.attributes.type?.data?.attributes
|
|
? [
|
|
nextContent.attributes.type.data.attributes.titles?.[0]
|
|
? nextContent.attributes.type.data.attributes.titles[0]?.title
|
|
: prettySlug(nextContent.attributes.type.data.attributes.slug),
|
|
]
|
|
: undefined
|
|
}
|
|
bottomChips={
|
|
isContentPanelAtLeast2xl
|
|
? nextContent.attributes.categories?.data.map(
|
|
(category) => category.attributes?.short ?? ""
|
|
)
|
|
: undefined
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</ContentPanel>
|
|
);
|
|
|
|
return <AppLayout contentPanel={contentPanel} subPanel={subPanel} {...otherProps} />;
|
|
};
|
|
export default Content;
|
|
|
|
/*
|
|
* ╭──────────────────────╮
|
|
* ───────────────────────────────────╯ 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 content = await sdk.getContentText({
|
|
slug: slug,
|
|
language_code: context.locale ?? "en",
|
|
});
|
|
|
|
if (!content.contents?.data[0]?.attributes?.translations) {
|
|
return { notFound: true };
|
|
}
|
|
|
|
const { title, description } = (() => {
|
|
if (context.locale && context.locales) {
|
|
const selectedTranslation = staticSmartLanguage({
|
|
items: content.contents.data[0].attributes.translations,
|
|
languageExtractor: (item) => item.language?.data?.attributes?.code,
|
|
preferredLanguages: getDefaultPreferredLanguages(context.locale, context.locales),
|
|
});
|
|
if (selectedTranslation) {
|
|
return {
|
|
title: prettyInlineTitle(
|
|
selectedTranslation.pre_title,
|
|
selectedTranslation.title,
|
|
selectedTranslation.subtitle
|
|
),
|
|
description: getDescription(selectedTranslation.description, {
|
|
[format("type", { count: Infinity })]: [
|
|
content.contents.data[0].attributes.type?.data?.attributes?.titles?.[0]?.title,
|
|
],
|
|
[format("category", { count: Infinity })]: filterHasAttributes(
|
|
content.contents.data[0].attributes.categories?.data,
|
|
["attributes"] as const
|
|
).map((category) => category.attributes.short),
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
title: prettySlug(content.contents.data[0].attributes.slug),
|
|
description: undefined,
|
|
};
|
|
})();
|
|
|
|
const thumbnail = content.contents.data[0].attributes.thumbnail?.data?.attributes;
|
|
|
|
content.contents.data[0].attributes.folder?.data?.attributes?.contents?.data.sort((a, b) =>
|
|
a.attributes && b.attributes ? naturalCompare(a.attributes.slug, b.attributes.slug) : 0
|
|
);
|
|
|
|
const props: Props = {
|
|
content: content.contents.data[0].attributes as ContentWithTranslations,
|
|
openGraph: getOpenGraph(format, title, description, thumbnail),
|
|
};
|
|
return {
|
|
props: props,
|
|
};
|
|
};
|
|
|
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
|
|
export const getStaticPaths: GetStaticPaths = async (context) => {
|
|
const sdk = getReadySdk();
|
|
const contents = await sdk.getContentsSlugs();
|
|
const paths: GetStaticPathsResult["paths"] = [];
|
|
filterHasAttributes(contents.contents?.data, ["attributes"] as const).map((item) => {
|
|
context.locales?.map((local) => {
|
|
paths.push({
|
|
params: { slug: item.attributes.slug },
|
|
locale: local,
|
|
});
|
|
});
|
|
});
|
|
return {
|
|
paths,
|
|
fallback: "blocking",
|
|
};
|
|
};
|
|
|
|
/*
|
|
* ╭───────────────────╮
|
|
* ─────────────────────────────────────╯ PRIVATE METHODS ╰───────────────────────────────────────
|
|
*/
|
|
|
|
type FolderContents = NonNullable<
|
|
NonNullable<
|
|
NonNullable<NonNullable<ContentWithTranslations["folder"]>["data"]>["attributes"]
|
|
>["contents"]
|
|
>["data"];
|
|
|
|
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 contents[index - 1];
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
|
|
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;
|
|
};
|