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;
};