diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index edba18d..7a9dc5d 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -19,13 +19,13 @@ import { isUndefined, iterateMap, } from "helpers/others"; -import { getOgImage, ImageQuality } from "helpers/img"; -import { prettyLanguage, prettySlug } from "helpers/formatters"; +import { prettyLanguage } from "helpers/formatters"; import { cIf, cJoin } from "helpers/className"; import { AppStaticProps } from "graphql/getAppStaticProps"; -import { UploadImageFragment } from "graphql/generated"; import { useAppLayout } from "contexts/AppLayoutContext"; import { Button } from "components/Inputs/Button"; +import { OpenGraph } from "helpers/openGraph"; +import { getDefaultPreferredLanguages } from "helpers/locales"; /* * ╭─────────────╮ @@ -33,21 +33,20 @@ import { Button } from "components/Inputs/Button"; */ const SENSIBILITY_SWIPE = 1.1; -const TITLE_PREFIX = "Accord’s Library"; /* * ╭─────────────╮ * ───────────────────────────────────────╯ COMPONENT ╰─────────────────────────────────────────── */ -interface Props extends AppStaticProps { +export interface AppLayoutRequired { + openGraph: OpenGraph; +} + +interface Props extends AppStaticProps, AppLayoutRequired { subPanel?: React.ReactNode; subPanelIcon?: Icon; contentPanel?: React.ReactNode; - title?: string; - navTitle: string | null | undefined; - thumbnail?: UploadImageFragment; - description?: string; contentPanelScroolbar?: boolean; } @@ -59,10 +58,7 @@ export const AppLayout = ({ languages, subPanel, contentPanel, - thumbnail, - title, - navTitle, - description, + openGraph, subPanelIcon = Icon.Tune, contentPanelScroolbar = true, }: Props): JSX.Element => { @@ -136,33 +132,6 @@ export const AppLayout = ({ [contentPanel, subPanel] ); - const metaImage = useMemo( - () => - thumbnail - ? getOgImage(ImageQuality.Og, thumbnail) - : { - image: "/default_og.jpg", - width: 1200, - height: 630, - alt: "Accord's Library Logo", - }, - [thumbnail] - ); - - const { ogTitle, metaTitle } = useMemo(() => { - const resultTitle = - title ?? navTitle ?? prettySlug(router.asPath.split("/").pop()); - return { - ogTitle: resultTitle, - metaTitle: `${TITLE_PREFIX} - ${resultTitle}`, - }; - }, [navTitle, router.asPath, title]); - - const metaDescription = useMemo( - () => description ?? langui.default_description ?? "", - [description, langui.default_description] - ); - useLayoutEffect(() => { document.getElementsByTagName("html")[0].style.fontSize = `${ (fontSize ?? 1) * 100 @@ -191,25 +160,13 @@ export const AppLayout = ({ useEffect(() => { if (preferredLanguages) { if (preferredLanguages.length === 0) { - let defaultPreferredLanguages: string[] = []; if (isDefinedAndNotEmpty(router.locale) && router.locales) { - if (router.locale === "en") { - defaultPreferredLanguages = [router.locale]; - router.locales.map((locale) => { - if (locale !== router.locale) - defaultPreferredLanguages.push(locale); - }); - } else { - defaultPreferredLanguages = [router.locale, "en"]; - router.locales.map((locale) => { - if (locale !== router.locale && locale !== "en") - defaultPreferredLanguages.push(locale); - }); - } + setPreferredLanguages( + getDefaultPreferredLanguages(router.locale, router.locales) + ); } - setPreferredLanguages(defaultPreferredLanguages); } else if (router.locale !== preferredLanguages[0]) { - router.push(router.asPath, router.asPath, { + router.replace(router.asPath, router.asPath, { locale: preferredLanguages[0], }); } @@ -251,27 +208,36 @@ export const AppLayout = ({ )} > - {metaTitle} - + {openGraph.title} + - - + + - + - - - - + + + + + - @@ -370,13 +336,13 @@ export const AppLayout = ({ className={cJoin( "overflow-hidden text-center font-headers font-black", cIf( - ogTitle && ogTitle.length > 30, + openGraph.title.length > 30, "max-h-14 text-xl", "max-h-16 text-2xl" ) )} > - {ogTitle} + {openGraph.title}

{isDefined(subPanel) && !turnSubIntoContent && ( { const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({ @@ -221,16 +220,9 @@ export const PostPage = ({ return ( diff --git a/src/components/PreviewCard.tsx b/src/components/PreviewCard.tsx index 501f95e..5a5a593 100644 --- a/src/components/PreviewCard.tsx +++ b/src/components/PreviewCard.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { useMemo } from "react"; +import { useRouter } from "next/router"; import { Chip } from "./Chip"; import { Ico, Icon } from "./Ico"; import { Img } from "./Img"; @@ -41,7 +42,8 @@ interface Props { stackNumber?: number; metadata?: { currencies?: AppStaticProps["currencies"]; - release_date?: DatePickerFragment | null; + releaseDate?: DatePickerFragment | null; + releaseDateFormat?: Intl.DateTimeFormatOptions["dateStyle"]; price?: PricePickerFragment | null; views?: number; author?: string; @@ -78,19 +80,20 @@ export const PreviewCard = ({ }: Props): JSX.Element => { const { currency } = useAppLayout(); const isHoverable = useMediaHoverable(); + const router = useRouter(); const metadataJSX = useMemo( () => ( <> - {metadata && (metadata.release_date || metadata.price) && ( + {metadata && (metadata.releaseDate || metadata.price) && (
- {metadata.release_date && ( + {metadata.releaseDate && (

- {prettyDate(metadata.release_date)} + {prettyDate(metadata.releaseDate, router.locale)}

)} {metadata.price && metadata.currencies && ( @@ -124,7 +127,7 @@ export const PreviewCard = ({ )} ), - [currency, metadata] + [currency, metadata, router.locale] ); return ( @@ -278,7 +281,7 @@ export const PreviewCard = ({ {bottomChips && bottomChips.length > 0 && (
{bottomChips.map((text, index) => ( diff --git a/src/graphql/getPostStaticProps.ts b/src/graphql/getPostStaticProps.ts index 2e86c2d..c7b981a 100644 --- a/src/graphql/getPostStaticProps.ts +++ b/src/graphql/getPostStaticProps.ts @@ -2,9 +2,18 @@ import { GetStaticProps } from "next"; import { AppStaticProps, getAppStaticProps } from "./getAppStaticProps"; import { getReadySdk } from "./sdk"; import { PostWithTranslations } from "helpers/types"; +import { OpenGraph, getOpenGraph } from "helpers/openGraph"; +import { prettyDate, prettySlug } from "helpers/formatters"; +import { + getDefaultPreferredLanguages, + staticSmartLanguage, +} from "helpers/locales"; +import { filterHasAttributes, isDefined } from "helpers/others"; +import { getDescription } from "helpers/description"; export interface PostStaticProps extends AppStaticProps { post: PostWithTranslations; + openGraph: OpenGraph; } export const getPostStaticProps = @@ -15,10 +24,48 @@ export const getPostStaticProps = slug: slug, language_code: context.locale ?? "en", }); - if (post.posts?.data[0]?.attributes?.translations) { + if ( + post.posts?.data && + post.posts.data.length > 0 && + post.posts.data[0].attributes?.translations && + isDefined(context.locale) && + isDefined(context.locales) + ) { + const appStaticProps = await getAppStaticProps(context); + const selectedTranslation = staticSmartLanguage({ + items: post.posts.data[0].attributes.translations, + languageExtractor: (item) => item.language?.data?.attributes?.code, + preferredLanguages: getDefaultPreferredLanguages( + context.locale, + context.locales + ), + }); + + const title = selectedTranslation?.title ?? prettySlug(slug); + + const description = getDescription(selectedTranslation?.excerpt, { + [appStaticProps.langui.release_date ?? "Release date"]: [ + prettyDate(post.posts.data[0].attributes.date, context.locale), + ], + [appStaticProps.langui.categories ?? "Categories"]: filterHasAttributes( + post.posts.data[0].attributes.categories?.data, + ["attributes"] as const + ).map((category) => category.attributes.short), + }); + + const thumbnail = + selectedTranslation?.thumbnail?.data?.attributes ?? + post.posts.data[0].attributes.thumbnail?.data?.attributes; + const props: PostStaticProps = { - ...(await getAppStaticProps(context)), + ...appStaticProps, post: post.posts.data[0].attributes as PostWithTranslations, + openGraph: getOpenGraph( + appStaticProps.langui, + title, + description, + thumbnail + ), }; return { props: props, diff --git a/src/graphql/operations/getWikiPage.graphql b/src/graphql/operations/getWikiPage.graphql index ebb945b..0507da1 100644 --- a/src/graphql/operations/getWikiPage.graphql +++ b/src/graphql/operations/getWikiPage.graphql @@ -20,19 +20,12 @@ query getWikiPage($slug: String, $language_code: String) { } } } - tags(pagination: { limit: -1 }) { + tags { data { id attributes { slug titles(filters: { language: { code: { eq: $language_code } } }) { - language { - data { - attributes { - code - } - } - } title } } diff --git a/src/helpers/date.ts b/src/helpers/date.ts index b6fd86d..7d77f4c 100644 --- a/src/helpers/date.ts +++ b/src/helpers/date.ts @@ -1,9 +1,13 @@ +import { isUndefined } from "./others"; import { DatePickerFragment } from "graphql/generated"; export const compareDate = ( - a: DatePickerFragment, - b: DatePickerFragment + a: DatePickerFragment | null | undefined, + b: DatePickerFragment | null | undefined ): number => { + if (isUndefined(a) || isUndefined(b)) { + return 0; + } const dateA = (a.year ?? 99999) * 365 + (a.month ?? 12) * 31 + (a.day ?? 31); const dateB = (b.year ?? 99999) * 365 + (b.month ?? 12) * 31 + (b.day ?? 31); return dateA - dateB; diff --git a/src/helpers/description.ts b/src/helpers/description.ts index d713c2d..90f889d 100644 --- a/src/helpers/description.ts +++ b/src/helpers/description.ts @@ -1,52 +1,28 @@ -import { prettySlug } from "./formatters"; -import { isDefined } from "./others"; -import { Content } from "./types"; -import { AppStaticProps } from "graphql/getAppStaticProps"; +import { isDefined, isDefinedAndNotEmpty } from "./others"; -interface Description { - langui: AppStaticProps["langui"]; - description?: string | null | undefined; - type?: Content["type"]; - categories?: Content["categories"]; -} +export const getDescription = ( + description: string | null | undefined, + chipsGroups?: Record +): string => { + let result = ""; -export const getDescription = ({ - langui, - description: text, - type, - categories, -}: Description): string => { - let description = ""; - - // TEXT - if (text) { - description += prettyMarkdown(text); - description += "\n\n"; + if (isDefinedAndNotEmpty(description)) { + result += prettyMarkdown(description); + if (isDefined(chipsGroups)) { + result += "\n\n"; + } } - // TYPE - if (type?.data) { - description += `${langui.type}: `; - - description += `(${ - type.data.attributes?.titles?.[0]?.title ?? - prettySlug(type.data.attributes?.slug) - })`; - - description += "\n"; + for (const key in chipsGroups) { + if (Object.hasOwn(chipsGroups, key)) { + const chipsGroup = chipsGroups[key]; + if (chipsGroup.length > 0) { + result += `${key}: ${prettyChip(chipsGroup)}\n`; + } + } } - // CATEGORIES - if (categories?.data && categories.data.length > 0) { - description += `${langui.categories}: `; - description += prettyChip( - categories.data.map((category) => category.attributes?.short) - ); - - description += "\n"; - } - - return description; + return result; }; const prettyMarkdown = (markdown: string): string => diff --git a/src/helpers/formatters.ts b/src/helpers/formatters.ts index a47063b..7345426 100644 --- a/src/helpers/formatters.ts +++ b/src/helpers/formatters.ts @@ -1,17 +1,18 @@ import { AppStaticProps } from "../graphql/getAppStaticProps"; import { convertPrice } from "./numbers"; -import { isDefinedAndNotEmpty } from "./others"; +import { isDefinedAndNotEmpty, isUndefined } from "./others"; import { DatePickerFragment, PricePickerFragment } from "graphql/generated"; -export const prettyDate = (datePicker: DatePickerFragment): string => { - let result = ""; - if (datePicker.year) result += datePicker.year.toString(); - if (datePicker.month) - result += `/${datePicker.month.toString().padStart(2, "0")}`; - if (datePicker.day) - result += `/${datePicker.day.toString().padStart(2, "0")}`; - return result; -}; +export const prettyDate = ( + datePicker: DatePickerFragment, + locale = "en", + dateStyle: Intl.DateTimeFormatOptions["dateStyle"] = "medium" +): string => + new Date( + datePicker.year ?? 0, + datePicker.month ?? 0, + datePicker.day ?? 1 + ).toLocaleString(locale, { dateStyle }); export const prettyPrice = ( pricePicker: PricePickerFragment, @@ -19,19 +20,23 @@ export const prettyPrice = ( targetCurrencyCode?: string ): string => { if (!targetCurrencyCode) return ""; - let result = ""; - currencies.map((currency) => { - if (currency.attributes?.code === targetCurrencyCode) { - const amountInTargetCurrency = convertPrice(pricePicker, currency); - result = - currency.attributes.symbol + - amountInTargetCurrency.toLocaleString(undefined, { - minimumFractionDigits: currency.attributes.display_decimals ? 2 : 0, - maximumFractionDigits: currency.attributes.display_decimals ? 2 : 0, - }); - } + if (isUndefined(pricePicker.amount)) return ""; + + const targetCurrency = currencies.find( + (currency) => currency.attributes?.code === targetCurrencyCode + ); + + if (targetCurrency?.attributes) { + const amountInTargetCurrency = convertPrice(pricePicker, targetCurrency); + return amountInTargetCurrency.toLocaleString("en", { + style: "currency", + currency: targetCurrency.attributes.code, + }); + } + return pricePicker.amount.toLocaleString("en", { + style: "currency", + currency: pricePicker.currency?.data?.attributes?.code, }); - return result; }; export const prettySlug = (slug?: string, parentSlug?: string): string => { diff --git a/src/helpers/img.ts b/src/helpers/img.ts index 0519ae4..a1d2b64 100644 --- a/src/helpers/img.ts +++ b/src/helpers/img.ts @@ -1,5 +1,3 @@ -import { UploadImageFragment } from "graphql/generated"; - export enum ImageQuality { Small = "small", Medium = "medium", @@ -7,7 +5,7 @@ export enum ImageQuality { Og = "og", } -interface OgImage { +export interface OgImage { image: string; width: number; height: number; @@ -65,20 +63,3 @@ export const getImgSizesByQuality = ( return { width: 0, height: 0 }; } }; - -export const getOgImage = ( - quality: ImageQuality, - image: UploadImageFragment -): OgImage => { - const imgSize = getImgSizesByQuality( - image.width ?? 0, - image.height ?? 0, - quality - ); - return { - image: getAssetURL(image.url, quality), - width: imgSize.width, - height: imgSize.height, - alt: image.alternativeText ?? "", - }; -}; diff --git a/src/helpers/locales.ts b/src/helpers/locales.ts new file mode 100644 index 0000000..8a4d09d --- /dev/null +++ b/src/helpers/locales.ts @@ -0,0 +1,54 @@ +import { isDefined } from "./others"; + +export const getDefaultPreferredLanguages = ( + routerLocal: string, + locales: string[] +): string[] => { + let defaultPreferredLanguages: string[] = []; + if (routerLocal === "en") { + defaultPreferredLanguages = [routerLocal]; + locales.map((locale) => { + if (locale !== routerLocal) defaultPreferredLanguages.push(locale); + }); + } else { + defaultPreferredLanguages = [routerLocal, "en"]; + locales.map((locale) => { + if (locale !== routerLocal && locale !== "en") + defaultPreferredLanguages.push(locale); + }); + } + return defaultPreferredLanguages; +}; + +export const getPreferredLanguage = ( + preferredLanguages: (string | undefined)[], + availableLanguages: Map +): number | undefined => { + for (const locale of preferredLanguages) { + if (isDefined(locale) && availableLanguages.has(locale)) { + return availableLanguages.get(locale); + } + } + return undefined; +}; + +interface StaticSmartLanguageProps { + items: T[]; + preferredLanguages: string[]; + languageExtractor: (item: NonNullable) => string | undefined; +} + +export const staticSmartLanguage = ({ + languageExtractor, + preferredLanguages, + items, +}: StaticSmartLanguageProps): T | undefined => { + for (const language of preferredLanguages) { + for (const item of items) { + if (isDefined(item) && languageExtractor(item) === language) { + return item; + } + } + } + return undefined; +}; diff --git a/src/helpers/openGraph.ts b/src/helpers/openGraph.ts new file mode 100644 index 0000000..3687f86 --- /dev/null +++ b/src/helpers/openGraph.ts @@ -0,0 +1,51 @@ +import { + OgImage, + getImgSizesByQuality, + ImageQuality, + getAssetURL, +} from "./img"; +import { isDefinedAndNotEmpty } from "./others"; +import { UploadImageFragment } from "graphql/generated"; +import { AppStaticProps } from "graphql/getAppStaticProps"; + +const DEFAULT_OG_THUMBNAIL = { + image: `${process.env.NEXT_PUBLIC_URL_SELF}/default_og.jpg`, + width: 1200, + height: 630, + alt: "Accord's Library Logo", +}; + +const TITLE_PREFIX = "Accord’s Library"; + +export interface OpenGraph { + title: string; + description: string; + thumbnail: OgImage; +} + +export const getOpenGraph = ( + langui: AppStaticProps["langui"], + title: string, + description?: string | null | undefined, + thumbnail?: UploadImageFragment | null | undefined +): OpenGraph => ({ + title: `${TITLE_PREFIX}${isDefinedAndNotEmpty(title) && ` - ${title}`}`, + description: isDefinedAndNotEmpty(description) + ? description + : langui.default_description ?? "", + thumbnail: thumbnail ? getOgImage(thumbnail) : DEFAULT_OG_THUMBNAIL, +}); + +const getOgImage = (image: UploadImageFragment): OgImage => { + const imgSize = getImgSizesByQuality( + image.width ?? 0, + image.height ?? 0, + ImageQuality.Og + ); + return { + image: getAssetURL(image.url, ImageQuality.Og), + width: imgSize.width, + height: imgSize.height, + alt: image.alternativeText ?? "", + }; +}; diff --git a/src/helpers/types.ts b/src/helpers/types.ts index 47b2ce3..cde3e26 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -17,7 +17,7 @@ export interface PostWithTranslations extends Omit { // ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -export type Content = NonNullable< +type Content = NonNullable< NonNullable["data"][number]["attributes"] >; diff --git a/src/hooks/useSmartLanguage.ts b/src/hooks/useSmartLanguage.ts index 9984440..8ed5393 100644 --- a/src/hooks/useSmartLanguage.ts +++ b/src/hooks/useSmartLanguage.ts @@ -4,6 +4,7 @@ import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher"; import { useAppLayout } from "contexts/AppLayoutContext"; import { AppStaticProps } from "graphql/getAppStaticProps"; import { filterDefined, isDefined } from "helpers/others"; +import { getPreferredLanguage } from "helpers/locales"; interface Props { items: T[]; @@ -12,18 +13,6 @@ interface Props { transform?: (item: NonNullable) => NonNullable; } -const getPreferredLanguage = ( - preferredLanguages: (string | undefined)[], - availableLanguages: Map -): number | undefined => { - for (const locale of preferredLanguages) { - if (isDefined(locale) && availableLanguages.has(locale)) { - return availableLanguages.get(locale); - } - } - return undefined; -}; - export const useSmartLanguage = ({ items, languageExtractor, diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 34bcfe7..068d2ba 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,25 +1,29 @@ import { GetStaticProps } from "next"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { ReturnButton, ReturnButtonType, } from "components/PanelComponents/ReturnButton"; import { ContentPanel } from "components/Panels/ContentPanel"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; +import { getOpenGraph } from "helpers/openGraph"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps {} +interface Props extends AppStaticProps, AppLayoutRequired {} -const FourOhFour = ({ langui, ...otherProps }: Props): JSX.Element => ( +const FourOhFour = ({ + langui, + openGraph, + ...otherProps +}: Props): JSX.Element => ( -

404 - {langui.page_not_found}

+

{openGraph.title}

( /> } + openGraph={openGraph} langui={langui} {...otherProps} /> @@ -40,8 +45,13 @@ export default FourOhFour; */ export const getStaticProps: GetStaticProps = async (context) => { + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, + openGraph: getOpenGraph( + appStaticProps.langui, + `404 - ${appStaticProps.langui.page_not_found}` + ), }; return { props: props, diff --git a/src/pages/500.tsx b/src/pages/500.tsx index 1ee938a..3898699 100644 --- a/src/pages/500.tsx +++ b/src/pages/500.tsx @@ -1,25 +1,29 @@ import { GetStaticProps } from "next"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { ReturnButton, ReturnButtonType, } from "components/PanelComponents/ReturnButton"; import { ContentPanel } from "components/Panels/ContentPanel"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; +import { getOpenGraph } from "helpers/openGraph"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps {} +interface Props extends AppStaticProps, AppLayoutRequired {} -const FiveHundred = ({ langui, ...otherProps }: Props): JSX.Element => ( +const FiveHundred = ({ + langui, + openGraph, + ...otherProps +}: Props): JSX.Element => ( -

500 - Internal Server Error

+

{openGraph.title}

( /> } + openGraph={openGraph} langui={langui} {...otherProps} /> @@ -40,8 +45,13 @@ export default FiveHundred; */ export const getStaticProps: GetStaticProps = async (context) => { + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, + openGraph: getOpenGraph( + appStaticProps.langui, + "500 - Internal Server Error" + ), }; return { props: props, diff --git a/src/pages/about-us/accords-handbook.tsx b/src/pages/about-us/accords-handbook.tsx index 78dce0b..ba9c8cd 100644 --- a/src/pages/about-us/accords-handbook.tsx +++ b/src/pages/about-us/accords-handbook.tsx @@ -9,19 +9,11 @@ import { * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -const AccordsHandbook = ({ - post, - langui, - languages, - currencies, -}: PostStaticProps): JSX.Element => ( +const AccordsHandbook = (props: PostStaticProps): JSX.Element => ( diff --git a/src/pages/about-us/contact.tsx b/src/pages/about-us/contact.tsx index 136763a..d0196f5 100644 --- a/src/pages/about-us/contact.tsx +++ b/src/pages/about-us/contact.tsx @@ -15,12 +15,7 @@ import { RequestMailProps, ResponseMailProps } from "pages/api/mail"; * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -const AboutUs = ({ - post, - langui, - languages, - currencies, -}: PostStaticProps): JSX.Element => { +const AboutUs = ({ langui, ...otherProps }: PostStaticProps): JSX.Element => { const router = useRouter(); const [formResponse, setFormResponse] = useState(""); const [formState, setFormState] = useState<"completed" | "ongoing" | "stale">( @@ -177,10 +172,8 @@ const AboutUs = ({ return ( ( { + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, + openGraph: getOpenGraph( + appStaticProps.langui, + appStaticProps.langui.about_us ?? "About us" + ), }; return { props: props, diff --git a/src/pages/about-us/legality.tsx b/src/pages/about-us/legality.tsx index 43eb96c..1e8d8b2 100644 --- a/src/pages/about-us/legality.tsx +++ b/src/pages/about-us/legality.tsx @@ -9,19 +9,11 @@ import { * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -const Legality = ({ - post, - langui, - languages, - currencies, -}: PostStaticProps): JSX.Element => ( +const Legality = (props: PostStaticProps): JSX.Element => ( diff --git a/src/pages/about-us/sharing-policy.tsx b/src/pages/about-us/sharing-policy.tsx index ab3bcf2..29e22ef 100644 --- a/src/pages/about-us/sharing-policy.tsx +++ b/src/pages/about-us/sharing-policy.tsx @@ -9,19 +9,11 @@ import { * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -const SharingPolicy = ({ - post, - langui, - languages, - currencies, -}: PostStaticProps): JSX.Element => ( +const SharingPolicy = (props: PostStaticProps): JSX.Element => ( diff --git a/src/pages/archives/index.tsx b/src/pages/archives/index.tsx index 283fc34..c02173f 100644 --- a/src/pages/archives/index.tsx +++ b/src/pages/archives/index.tsx @@ -1,18 +1,19 @@ import { GetStaticProps } from "next"; import { useMemo } from "react"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { NavOption } from "components/PanelComponents/NavOption"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { SubPanel } from "components/Panels/SubPanel"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { Icon } from "components/Ico"; +import { getOpenGraph } from "helpers/openGraph"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps {} +interface Props extends AppStaticProps, AppLayoutRequired {} const Archives = ({ langui, ...otherProps }: Props): JSX.Element => { const subPanel = useMemo( @@ -28,14 +29,7 @@ const Archives = ({ langui, ...otherProps }: Props): JSX.Element => { ), [langui] ); - return ( - - ); + return ; }; export default Archives; @@ -45,8 +39,13 @@ export default Archives; */ export const getStaticProps: GetStaticProps = async (context) => { + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, + openGraph: getOpenGraph( + appStaticProps.langui, + appStaticProps.langui.archives ?? "Archives" + ), }; return { props: props, diff --git a/src/pages/archives/videos/c/[uid].tsx b/src/pages/archives/videos/c/[uid].tsx index 9a39cc2..cdd7605 100644 --- a/src/pages/archives/videos/c/[uid].tsx +++ b/src/pages/archives/videos/c/[uid].tsx @@ -1,6 +1,6 @@ import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { Fragment, useMemo } from "react"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { Switch } from "components/Inputs/Switch"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { @@ -22,13 +22,14 @@ import { useMediaHoverable } from "hooks/useMediaQuery"; import { WithLabel } from "components/Inputs/WithLabel"; import { filterHasAttributes, isDefined } from "helpers/others"; import { useBoolean } from "hooks/useBoolean"; +import { getOpenGraph } from "helpers/openGraph"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps { +interface Props extends AppStaticProps, AppLayoutRequired { channel: NonNullable< GetVideoChannelQuery["videoChannels"] >["data"][number]["attributes"]; @@ -91,7 +92,7 @@ const Channel = ({ langui, channel, ...otherProps }: Props): JSX.Element => { thumbnailAspectRatio="16/9" keepInfoVisible={keepInfoVisible} metadata={{ - release_date: video.attributes.published_date, + releaseDate: video.attributes.published_date, views: video.attributes.views, author: channel?.title, position: "Top", @@ -116,7 +117,6 @@ const Channel = ({ langui, channel, ...otherProps }: Props): JSX.Element => { return ( { : "", }); if (!channel.videoChannels?.data[0].attributes) return { notFound: true }; + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, channel: channel.videoChannels.data[0].attributes, + openGraph: getOpenGraph( + appStaticProps.langui, + channel.videoChannels.data[0].attributes.title + ), }; return { props: props, diff --git a/src/pages/archives/videos/index.tsx b/src/pages/archives/videos/index.tsx index d959e56..d008d65 100644 --- a/src/pages/archives/videos/index.tsx +++ b/src/pages/archives/videos/index.tsx @@ -1,6 +1,6 @@ import { GetStaticProps } from "next"; import { useMemo, useState } from "react"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { SmartList } from "components/SmartList"; import { Icon } from "components/Ico"; import { Switch } from "components/Inputs/Switch"; @@ -20,11 +20,12 @@ import { PreviewCard } from "components/PreviewCard"; import { GetVideosPreviewQuery } from "graphql/generated"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { getReadySdk } from "graphql/sdk"; -import { prettyDate } from "helpers/formatters"; import { filterHasAttributes } from "helpers/others"; import { getVideoThumbnailURL } from "helpers/videos"; import { useMediaHoverable } from "hooks/useMediaQuery"; import { useBoolean } from "hooks/useBoolean"; +import { getOpenGraph } from "helpers/openGraph"; +import { compareDate } from "helpers/date"; /* * ╭─────────────╮ @@ -40,7 +41,7 @@ const DEFAULT_FILTERS_STATE = { * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps { +interface Props extends AppStaticProps, AppLayoutRequired { videos: NonNullable["data"]; } @@ -73,7 +74,7 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => { @@ -107,7 +108,7 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => { thumbnailForceAspectRatio keepInfoVisible={keepInfoVisible} metadata={{ - release_date: item.attributes.published_date, + releaseDate: item.attributes.published_date, views: item.attributes.views, author: item.attributes.channel?.data?.attributes?.title, position: "Top", @@ -132,7 +133,6 @@ const Videos = ({ langui, videos, ...otherProps }: Props): JSX.Element => { ); return ( { const videos = await sdk.getVideosPreview(); if (!videos.videos) return { notFound: true }; videos.videos.data - .sort((a, b) => { - const dateA = a.attributes?.published_date - ? prettyDate(a.attributes.published_date) - : "9999"; - const dateB = b.attributes?.published_date - ? prettyDate(b.attributes.published_date) - : "9999"; - return dateA.localeCompare(dateB); - }) + .sort((a, b) => + compareDate(a.attributes?.published_date, b.attributes?.published_date) + ) .reverse(); + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, videos: videos.videos.data, + openGraph: getOpenGraph( + appStaticProps.langui, + appStaticProps.langui.videos ?? "Videos" + ), }; return { props: props, diff --git a/src/pages/archives/videos/v/[uid].tsx b/src/pages/archives/videos/v/[uid].tsx index dff055b..d166277 100644 --- a/src/pages/archives/videos/v/[uid].tsx +++ b/src/pages/archives/videos/v/[uid].tsx @@ -1,6 +1,7 @@ import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { useMemo } from "react"; -import { AppLayout } from "components/AppLayout"; +import { useRouter } from "next/router"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { HorizontalLine } from "components/HorizontalLine"; import { Ico, Icon } from "components/Ico"; import { Button } from "components/Inputs/Button"; @@ -23,13 +24,14 @@ import { prettyDate, prettyShortenNumber } from "helpers/formatters"; import { filterHasAttributes, isDefined } from "helpers/others"; import { getVideoFile } from "helpers/videos"; import { useMediaMobile } from "hooks/useMediaQuery"; +import { getOpenGraph } from "helpers/openGraph"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps { +interface Props extends AppStaticProps, AppLayoutRequired { video: NonNullable< NonNullable["data"][number]["attributes"] >; @@ -38,6 +40,8 @@ interface Props extends AppStaticProps { const Video = ({ langui, video, ...otherProps }: Props): JSX.Element => { const isMobile = useMediaMobile(); const { setSubPanelOpen } = useAppLayout(); + const router = useRouter(); + const subPanel = useMemo( () => ( @@ -118,7 +122,7 @@ const Video = ({ langui, video, ...otherProps }: Props): JSX.Element => { icon={Icon.Event} className="mr-1 translate-y-[.15em] !text-base" /> - {prettyDate(video.published_date)} + {prettyDate(video.published_date, router.locale)}

{ [ isMobile, langui, + router.locale, video.channel?.data?.attributes, video.description, video.gone, @@ -198,7 +203,6 @@ const Video = ({ langui, video, ...otherProps }: Props): JSX.Element => { return ( { : "", }); if (!videos.videos?.data[0]?.attributes) return { notFound: true }; + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, video: videos.videos.data[0].attributes, + openGraph: getOpenGraph( + appStaticProps.langui, + videos.videos.data[0].attributes.title + ), }; return { props: props, diff --git a/src/pages/chronicles/[slug]/index.tsx b/src/pages/chronicles/[slug]/index.tsx index ed2fa36..b79a3f9 100644 --- a/src/pages/chronicles/[slug]/index.tsx +++ b/src/pages/chronicles/[slug]/index.tsx @@ -4,7 +4,7 @@ import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { getReadySdk } from "graphql/sdk"; import { isDefined, filterHasAttributes } from "helpers/others"; import { ChronicleWithTranslations } from "helpers/types"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { useSmartLanguage } from "hooks/useSmartLanguage"; import { ContentPanel } from "components/Panels/ContentPanel"; import { Markdawn } from "components/Markdown/Markdawn"; @@ -12,20 +12,26 @@ import { SubPanel } from "components/Panels/SubPanel"; import { ThumbnailHeader } from "components/ThumbnailHeader"; import { HorizontalLine } from "components/HorizontalLine"; import { GetChroniclesChaptersQuery } from "graphql/generated"; -import { prettySlug } from "helpers/formatters"; +import { prettyInlineTitle, prettySlug } from "helpers/formatters"; import { ReturnButton, ReturnButtonType, } from "components/PanelComponents/ReturnButton"; import { TranslatedChroniclesList } from "components/Translated"; import { Icon } from "components/Ico"; +import { getOpenGraph } from "helpers/openGraph"; +import { + getDefaultPreferredLanguages, + staticSmartLanguage, +} from "helpers/locales"; +import { getDescription } from "helpers/description"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps { +interface Props extends AppStaticProps, AppLayoutRequired { chronicle: ChronicleWithTranslations; chapters: NonNullable< GetChroniclesChaptersQuery["chroniclesChapters"] @@ -191,7 +197,6 @@ const Chronicle = ({ return ( { }); const chronicles = await sdk.getChroniclesChapters(); if ( - !chronicle.chronicles?.data[0].attributes?.translations || + !chronicle.chronicles?.data[0]?.attributes?.translations || !chronicles.chroniclesChapters?.data ) return { notFound: true }; + const appStaticProps = await getAppStaticProps(context); + + const { title, description } = (() => { + if (context.locale && context.locales) { + if ( + chronicle.chronicles.data[0].attributes.contents?.data[0]?.attributes + ?.translations + ) { + const selectedContentTranslation = staticSmartLanguage({ + items: + chronicle.chronicles.data[0].attributes.contents.data[0].attributes + .translations, + languageExtractor: (item) => item.language?.data?.attributes?.code, + preferredLanguages: getDefaultPreferredLanguages( + context.locale, + context.locales + ), + }); + if (selectedContentTranslation) { + return { + title: prettyInlineTitle( + selectedContentTranslation.pre_title, + selectedContentTranslation.title, + selectedContentTranslation.subtitle + ), + description: getDescription( + selectedContentTranslation.description, + { + [appStaticProps.langui.type ?? "Type"]: [ + chronicle.chronicles.data[0].attributes.contents.data[0] + .attributes.type?.data?.attributes?.titles?.[0]?.title, + ], + [appStaticProps.langui.categories ?? "Categories"]: + filterHasAttributes( + chronicle.chronicles.data[0].attributes.contents.data[0] + .attributes.categories?.data, + ["attributes"] as const + ).map((category) => category.attributes.short), + } + ), + }; + } + } else { + const selectedTranslation = staticSmartLanguage({ + items: chronicle.chronicles.data[0].attributes.translations, + languageExtractor: (item) => item.language?.data?.attributes?.code, + preferredLanguages: getDefaultPreferredLanguages( + context.locale, + context.locales + ), + }); + if (selectedTranslation) { + return { + title: selectedTranslation.title, + description: selectedTranslation.summary, + }; + } + } + } + return { + title: prettySlug(chronicle.chronicles.data[0].attributes.slug), + description: undefined, + }; + })(); + + const thumbnail = + chronicle.chronicles.data[0].attributes.translations.length === 0 + ? chronicle.chronicles.data[0].attributes.contents?.data[0]?.attributes + ?.thumbnail?.data?.attributes + : undefined; + const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, chronicle: chronicle.chronicles.data[0] .attributes as ChronicleWithTranslations, chapters: chronicles.chroniclesChapters.data, + openGraph: getOpenGraph( + appStaticProps.langui, + title, + description, + thumbnail + ), }; return { props: props, diff --git a/src/pages/chronicles/index.tsx b/src/pages/chronicles/index.tsx index afcc5f2..e3e3f3f 100644 --- a/src/pages/chronicles/index.tsx +++ b/src/pages/chronicles/index.tsx @@ -1,6 +1,6 @@ import { GetStaticProps } from "next"; import { useMemo } from "react"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; import { SubPanel } from "components/Panels/SubPanel"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; @@ -10,13 +10,14 @@ import { GetChroniclesChaptersQuery } from "graphql/generated"; import { filterHasAttributes } from "helpers/others"; import { prettySlug } from "helpers/formatters"; import { TranslatedChroniclesList } from "components/Translated"; +import { getOpenGraph } from "helpers/openGraph"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps { +interface Props extends AppStaticProps, AppLayoutRequired { chapters: NonNullable< GetChroniclesChaptersQuery["chroniclesChapters"] >["data"]; @@ -58,14 +59,7 @@ const Chronicles = ({ [chapters, langui] ); - return ( - - ); + return ; }; export default Chronicles; @@ -78,9 +72,14 @@ export const getStaticProps: GetStaticProps = async (context) => { const sdk = getReadySdk(); const chronicles = await sdk.getChroniclesChapters(); if (!chronicles.chroniclesChapters?.data) return { notFound: true }; + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, chapters: chronicles.chroniclesChapters.data, + openGraph: getOpenGraph( + appStaticProps.langui, + appStaticProps.langui.chronicles ?? "Chronicles" + ), }; return { props: props, diff --git a/src/pages/contents/[slug]/index.tsx b/src/pages/contents/[slug]/index.tsx index f453511..9125275 100644 --- a/src/pages/contents/[slug]/index.tsx +++ b/src/pages/contents/[slug]/index.tsx @@ -1,6 +1,6 @@ import { GetStaticPaths, GetStaticPathsResult, GetStaticProps } from "next"; import { Fragment, useCallback, useMemo } from "react"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { Chip } from "components/Chip"; import { HorizontalLine } from "components/HorizontalLine"; import { PreviewCardCTAs } from "components/Library/PreviewCardCTAs"; @@ -17,7 +17,6 @@ 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, @@ -35,13 +34,19 @@ import { useMediaMobile } from "hooks/useMediaQuery"; import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; import { useSmartLanguage } from "hooks/useSmartLanguage"; import { TranslatedPreviewLine } from "components/Translated"; +import { getOpenGraph } from "helpers/openGraph"; +import { + getDefaultPreferredLanguages, + staticSmartLanguage, +} from "helpers/locales"; +import { getDescription } from "helpers/description"; /* * ╭────────╮ * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps { +interface Props extends AppStaticProps, AppLayoutRequired { content: ContentWithTranslations; } @@ -260,7 +265,7 @@ const Content = ({ ).map((category) => category.attributes.short)} metadata={{ currencies: currencies, - release_date: libraryItem.attributes.release_date, + releaseDate: libraryItem.attributes.release_date, price: libraryItem.attributes.price, position: "Bottom", }} @@ -457,22 +462,6 @@ const Content = ({ return ( { if (!content.contents?.data[0]?.attributes?.translations) { return { notFound: true }; } + const appStaticProps = await getAppStaticProps(context); + + 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, { + [appStaticProps.langui.type ?? "Type"]: [ + content.contents.data[0].attributes.type?.data?.attributes + ?.titles?.[0]?.title, + ], + [appStaticProps.langui.categories ?? "Categories"]: + 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; + const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, content: content.contents.data[0].attributes as ContentWithTranslations, + openGraph: getOpenGraph( + appStaticProps.langui, + title, + description, + thumbnail + ), }; return { props: props, diff --git a/src/pages/contents/index.tsx b/src/pages/contents/index.tsx index fda27c7..9b4639d 100644 --- a/src/pages/contents/index.tsx +++ b/src/pages/contents/index.tsx @@ -1,6 +1,6 @@ import { GetStaticProps } from "next"; import { useState, useMemo, useCallback } from "react"; -import { AppLayout } from "components/AppLayout"; +import { AppLayout, AppLayoutRequired } from "components/AppLayout"; import { Select } from "components/Inputs/Select"; import { Switch } from "components/Inputs/Switch"; import { PanelHeader } from "components/PanelComponents/PanelHeader"; @@ -23,6 +23,7 @@ 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"; /* * ╭─────────────╮ @@ -41,7 +42,7 @@ const DEFAULT_FILTERS_STATE = { * ──────────────────────────────────────────╯ PAGE ╰───────────────────────────────────────────── */ -interface Props extends AppStaticProps { +interface Props extends AppStaticProps, AppLayoutRequired { contents: NonNullable["data"]; } @@ -164,7 +165,7 @@ const Contents = ({ @@ -174,7 +175,7 @@ const Contents = ({ input={ { return ( { language_code: context.locale ?? "en", }); if (!pages.wikiPages?.data) return { notFound: true }; + const appStaticProps = await getAppStaticProps(context); const props: Props = { - ...(await getAppStaticProps(context)), + ...appStaticProps, pages: sortPages(pages.wikiPages.data), + openGraph: getOpenGraph( + appStaticProps.langui, + appStaticProps.langui.wiki ?? "Wiki" + ), }; return { props: props,