548 lines
20 KiB
TypeScript
548 lines
20 KiB
TypeScript
import { AppLayout } 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 {
|
|
ReturnButton,
|
|
ReturnButtonType,
|
|
} from "components/PanelComponents/ReturnButton";
|
|
import { ContentPanel } from "components/Panels/ContentPanel";
|
|
import { SubPanel } from "components/Panels/SubPanel";
|
|
import { PreviewCard } from "components/PreviewCard";
|
|
import { TranslatedPreviewLine } from "components/PreviewLine";
|
|
import { RecorderChip } from "components/RecorderChip";
|
|
import { ThumbnailHeader } from "components/ThumbnailHeader";
|
|
import { ToolTip } from "components/ToolTip";
|
|
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
|
|
import { getReadySdk } from "graphql/sdk";
|
|
import { getDescription } from "helpers/description";
|
|
import {
|
|
prettyinlineTitle,
|
|
prettyLanguage,
|
|
prettyItemSubType,
|
|
prettySlug,
|
|
} from "helpers/formatters";
|
|
import { isUntangibleGroupItem } from "helpers/libraryItem";
|
|
import {
|
|
filterDefined,
|
|
filterHasAttributes,
|
|
getStatusDescription,
|
|
isDefinedAndNotEmpty,
|
|
} from "helpers/others";
|
|
import { ContentWithTranslations } from "helpers/types";
|
|
import { useMediaMobile } from "hooks/useMediaQuery";
|
|
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
|
|
import { useSmartLanguage } from "hooks/useSmartLanguage";
|
|
import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next";
|
|
import { Fragment, useCallback, useMemo } from "react";
|
|
|
|
/*
|
|
* ╭────────╮
|
|
* ──────────────────────────────────────────╯ PAGE ╰─────────────────────────────────────────────
|
|
*/
|
|
|
|
interface Props extends AppStaticProps {
|
|
content: ContentWithTranslations;
|
|
}
|
|
|
|
const Content = ({
|
|
langui,
|
|
content,
|
|
languages,
|
|
currencies,
|
|
...otherProps
|
|
}: Props): JSX.Element => {
|
|
const isMobile = useMediaMobile();
|
|
|
|
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] =
|
|
useSmartLanguage({
|
|
items: content.translations,
|
|
languages: languages,
|
|
languageExtractor: useCallback(
|
|
(item: NonNullable<Props["content"]["translations"][number]>) =>
|
|
item.language?.data?.attributes?.code,
|
|
[]
|
|
),
|
|
});
|
|
|
|
useScrollTopOnChange(AnchorIds.ContentPanel, [selectedTranslation]);
|
|
|
|
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,
|
|
}),
|
|
[content.group, content.slug]
|
|
);
|
|
|
|
const subPanel = useMemo(
|
|
() => (
|
|
<SubPanel>
|
|
<ReturnButton
|
|
href={`/contents`}
|
|
title={langui.contents}
|
|
langui={langui}
|
|
displayOn={ReturnButtonType.Desktop}
|
|
horizontalLine
|
|
/>
|
|
|
|
{selectedTranslation?.text_set?.source_language?.data?.attributes
|
|
?.code !== undefined && (
|
|
<div className="grid gap-5">
|
|
<h2 className="text-xl">
|
|
{selectedTranslation.text_set.source_language.data.attributes
|
|
.code === selectedTranslation.language?.data?.attributes?.code
|
|
? langui.transcript_notice
|
|
: langui.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">{langui.source_language}:</p>
|
|
<Chip>
|
|
{prettyLanguage(
|
|
selectedTranslation.text_set.source_language.data.attributes
|
|
.code,
|
|
languages
|
|
)}
|
|
</Chip>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-flow-col place-content-center place-items-center gap-2">
|
|
<p className="font-headers">{langui.status}:</p>
|
|
|
|
<ToolTip
|
|
content={getStatusDescription(
|
|
selectedTranslation.text_set.status,
|
|
langui
|
|
)}
|
|
maxWidth={"20rem"}
|
|
>
|
|
<Chip>{selectedTranslation.text_set.status}</Chip>
|
|
</ToolTip>
|
|
</div>
|
|
|
|
{selectedTranslation.text_set.transcribers &&
|
|
selectedTranslation.text_set.transcribers.data.length > 0 && (
|
|
<div>
|
|
<p className="font-headers">{langui.transcribers}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
{filterHasAttributes(
|
|
selectedTranslation.text_set.transcribers.data
|
|
).map((recorder) => (
|
|
<Fragment key={recorder.id}>
|
|
<RecorderChip
|
|
langui={langui}
|
|
recorder={recorder.attributes}
|
|
/>
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedTranslation.text_set.translators &&
|
|
selectedTranslation.text_set.translators.data.length > 0 && (
|
|
<div>
|
|
<p className="font-headers">{langui.translators}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
{filterHasAttributes(
|
|
selectedTranslation.text_set.translators.data
|
|
).map((recorder) => (
|
|
<Fragment key={recorder.id}>
|
|
<RecorderChip
|
|
langui={langui}
|
|
recorder={recorder.attributes}
|
|
/>
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedTranslation.text_set.proofreaders &&
|
|
selectedTranslation.text_set.proofreaders.data.length > 0 && (
|
|
<div>
|
|
<p className="font-headers">{langui.proofreaders}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
{filterHasAttributes(
|
|
selectedTranslation.text_set.proofreaders.data
|
|
).map((recorder) => (
|
|
<Fragment key={recorder.id}>
|
|
<RecorderChip
|
|
langui={langui}
|
|
recorder={recorder.attributes}
|
|
/>
|
|
</Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isDefinedAndNotEmpty(selectedTranslation.text_set.notes) && (
|
|
<div>
|
|
<p className="font-headers">{"Notes"}:</p>
|
|
<div className="grid place-content-center place-items-center gap-2">
|
|
<Markdawn text={selectedTranslation.text_set.notes} />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{content.ranged_contents?.data &&
|
|
content.ranged_contents.data.length > 0 && (
|
|
<>
|
|
<HorizontalLine />
|
|
<div>
|
|
<p className="font-headers text-2xl">{langui.source}</p>
|
|
<div className="mt-6 grid place-items-center gap-6 text-left">
|
|
{content.ranged_contents.data.map((rangedContent) => {
|
|
const libraryItem =
|
|
rangedContent.attributes?.library_item?.data;
|
|
if (libraryItem?.attributes && libraryItem.id) {
|
|
return (
|
|
<div
|
|
key={libraryItem.attributes.slug}
|
|
className="mobile:w-[80%]"
|
|
>
|
|
<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={libraryItem.attributes.categories?.data.map(
|
|
(category) => category.attributes?.short ?? ""
|
|
)}
|
|
metadata={{
|
|
currencies: currencies,
|
|
release_date: libraryItem.attributes.release_date,
|
|
price: libraryItem.attributes.price,
|
|
position: "Bottom",
|
|
}}
|
|
infoAppend={
|
|
!isUntangibleGroupItem(
|
|
libraryItem.attributes.metadata?.[0]
|
|
) && (
|
|
<PreviewCardCTAs
|
|
id={libraryItem.id}
|
|
langui={langui}
|
|
/>
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
return <></>;
|
|
})}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{selectedTranslation?.text_set?.text && (
|
|
<>
|
|
<HorizontalLine />
|
|
<TableOfContents
|
|
text={selectedTranslation.text_set.text}
|
|
title={prettyinlineTitle(
|
|
selectedTranslation.pre_title,
|
|
selectedTranslation.title,
|
|
selectedTranslation.subtitle
|
|
)}
|
|
langui={langui}
|
|
/>
|
|
</>
|
|
)}
|
|
</SubPanel>
|
|
),
|
|
[
|
|
content.ranged_contents?.data,
|
|
currencies,
|
|
languages,
|
|
langui,
|
|
selectedTranslation,
|
|
]
|
|
);
|
|
|
|
const contentPanel = useMemo(
|
|
() => (
|
|
<ContentPanel>
|
|
<ReturnButton
|
|
href={`/contents`}
|
|
title={langui.contents}
|
|
langui={langui}
|
|
displayOn={ReturnButtonType.Mobile}
|
|
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}
|
|
langui={langui}
|
|
languageSwitcher={<LanguageSwitcher {...languageSwitcherProps} />}
|
|
/>
|
|
|
|
{previousContent?.attributes && (
|
|
<div className="mt-12 mb-8 w-full">
|
|
<h2 className="mb-4 text-center text-2xl">
|
|
{langui.previous_content}
|
|
</h2>
|
|
<TranslatedPreviewLine
|
|
href={`/contents/${previousContent.attributes.slug}`}
|
|
translations={filterDefined(
|
|
previousContent.attributes.translations
|
|
).map((translation) => ({
|
|
pre_title: translation.pre_title,
|
|
title: translation.title,
|
|
subtitle: translation.subtitle,
|
|
language: translation.language?.data?.attributes?.code,
|
|
}))}
|
|
slug={previousContent.attributes.slug}
|
|
languages={languages}
|
|
thumbnail={
|
|
previousContent.attributes.thumbnail?.data?.attributes
|
|
}
|
|
thumbnailAspectRatio="3/2"
|
|
topChips={
|
|
isMobile
|
|
? undefined
|
|
: 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={
|
|
isMobile
|
|
? undefined
|
|
: previousContent.attributes.categories?.data.map(
|
|
(category) => category.attributes?.short ?? ""
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<HorizontalLine />
|
|
|
|
<Markdawn text={selectedTranslation?.text_set?.text ?? ""} />
|
|
|
|
{nextContent?.attributes && (
|
|
<>
|
|
<HorizontalLine />
|
|
<h2 className="mb-4 text-center text-2xl">
|
|
{langui.followup_content}
|
|
</h2>
|
|
<TranslatedPreviewLine
|
|
href={`/contents/${nextContent.attributes.slug}`}
|
|
translations={filterDefined(
|
|
nextContent.attributes.translations
|
|
).map((translation) => ({
|
|
pre_title: translation.pre_title,
|
|
title: translation.title,
|
|
subtitle: translation.subtitle,
|
|
language: translation.language?.data?.attributes?.code,
|
|
}))}
|
|
slug={nextContent.attributes.slug}
|
|
languages={languages}
|
|
thumbnail={nextContent.attributes.thumbnail?.data?.attributes}
|
|
thumbnailAspectRatio="3/2"
|
|
topChips={
|
|
isMobile
|
|
? undefined
|
|
: 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={
|
|
isMobile
|
|
? undefined
|
|
: nextContent.attributes.categories?.data.map(
|
|
(category) => category.attributes?.short ?? ""
|
|
)
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</ContentPanel>
|
|
),
|
|
[
|
|
LanguageSwitcher,
|
|
content.categories,
|
|
content.thumbnail?.data?.attributes,
|
|
content.type,
|
|
isMobile,
|
|
languageSwitcherProps,
|
|
languages,
|
|
langui,
|
|
nextContent?.attributes,
|
|
previousContent?.attributes,
|
|
selectedTranslation?.description,
|
|
selectedTranslation?.pre_title,
|
|
selectedTranslation?.subtitle,
|
|
selectedTranslation?.text_set?.text,
|
|
selectedTranslation?.title,
|
|
]
|
|
);
|
|
|
|
return (
|
|
<AppLayout
|
|
navTitle={
|
|
selectedTranslation
|
|
? prettyinlineTitle(
|
|
selectedTranslation.pre_title,
|
|
selectedTranslation.title,
|
|
selectedTranslation.subtitle
|
|
)
|
|
: prettySlug(content.slug)
|
|
}
|
|
description={getDescription({
|
|
langui: langui,
|
|
description: selectedTranslation?.description,
|
|
type: content.type,
|
|
categories: content.categories,
|
|
})}
|
|
thumbnail={content.thumbnail?.data?.attributes ?? undefined}
|
|
contentPanel={contentPanel}
|
|
subPanel={subPanel}
|
|
currencies={currencies}
|
|
languages={languages}
|
|
langui={langui}
|
|
{...otherProps}
|
|
/>
|
|
);
|
|
};
|
|
export default Content;
|
|
|
|
/*
|
|
* ╭──────────────────────╮
|
|
* ───────────────────────────────────╯ NEXT DATA FETCHING ╰──────────────────────────────────────
|
|
*/
|
|
|
|
export const getStaticProps: GetStaticProps = async (context) => {
|
|
const sdk = getReadySdk();
|
|
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 props: Props = {
|
|
...(await getAppStaticProps(context)),
|
|
content: content.contents.data[0].attributes as ContentWithTranslations,
|
|
};
|
|
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).map((item) => {
|
|
context.locales?.map((local) => {
|
|
paths.push({
|
|
params: { slug: item.attributes.slug },
|
|
locale: local,
|
|
});
|
|
});
|
|
});
|
|
return {
|
|
paths,
|
|
fallback: "blocking",
|
|
};
|
|
};
|
|
|
|
/*
|
|
* ╭───────────────────╮
|
|
* ─────────────────────────────────────╯ PRIVATE METHODS ╰───────────────────────────────────────
|
|
*/
|
|
|
|
type Group = NonNullable<
|
|
NonNullable<
|
|
NonNullable<
|
|
NonNullable<ContentWithTranslations["group"]>["data"]
|
|
>["attributes"]
|
|
>["contents"]
|
|
>["data"];
|
|
|
|
const getPreviousContent = (group: Group, currentSlug: string) => {
|
|
for (let index = 0; index < group.length; index += 1) {
|
|
const content = group[index];
|
|
if (content.attributes?.slug === currentSlug && index > 0) {
|
|
return group[index - 1];
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
|
|
const getNextContent = (group: Group, currentSlug: string) => {
|
|
for (let index = 0; index < group.length; index += 1) {
|
|
const content = group[index];
|
|
if (content.attributes?.slug === currentSlug && index < group.length - 1) {
|
|
return group[index + 1];
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|