Added smart language switch for posts ad hoc page and content

This commit is contained in:
DrMint 2022-04-12 12:25:40 +02:00
parent 049a2e2044
commit a84560d86e
16 changed files with 612 additions and 629 deletions

View File

@ -171,7 +171,7 @@ module.exports = {
"@typescript-eslint/no-require-imports": "error", "@typescript-eslint/no-require-imports": "error",
// "@typescript-eslint/no-type-alias": "warn", // "@typescript-eslint/no-type-alias": "warn",
"@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn", "@typescript-eslint/no-unnecessary-boolean-literal-compare": "warn",
// "@typescript-eslint/no-unnecessary-condition": "warn", "@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "warn", "@typescript-eslint/no-unnecessary-qualifier": "warn",
"@typescript-eslint/no-unnecessary-type-arguments": "warn", "@typescript-eslint/no-unnecessary-type-arguments": "warn",
"@typescript-eslint/prefer-enum-initializers": "error", "@typescript-eslint/prefer-enum-initializers": "error",

View File

@ -11,42 +11,61 @@ interface Props {
target?: "_blank"; target?: "_blank";
onClick?: MouseEventHandler<HTMLDivElement>; onClick?: MouseEventHandler<HTMLDivElement>;
draggable?: boolean; draggable?: boolean;
badgeNumber?: number;
} }
export default function Button(props: Props): JSX.Element { export default function Button(props: Props): JSX.Element {
const {
draggable,
id,
onClick,
active,
className,
children,
target,
href,
locale,
badgeNumber,
} = props;
const router = useRouter(); const router = useRouter();
const button = ( const button = (
<div <div
draggable={props.draggable} draggable={draggable}
id={props.id} id={id}
onClick={props.onClick} onClick={onClick}
className={`grid place-content-center place-items-center border-[1px] border-dark text-dark rounded-full px-4 pt-[0.4rem] pb-[0.5rem] transition-all select-none ${ className={`grid place-content-center place-items-center border-[1px]
props.className border-dark text-dark rounded-full px-4 pt-[0.4rem] pb-[0.5rem]
} ${ transition-all select-none hover:[--opacityBadge:0] --opacityBadge:100 ${className} ${
props.active active
? "text-light bg-black drop-shadow-black-lg !border-black cursor-not-allowed" ? "text-light bg-black drop-shadow-black-lg !border-black cursor-not-allowed"
: "cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:text-light active:drop-shadow-black-lg active:border-black" : "cursor-pointer hover:text-light hover:bg-dark hover:drop-shadow-shade-lg active:bg-black active:text-light active:drop-shadow-black-lg active:border-black"
}`} }`}
> >
{props.children} {badgeNumber && (
<div className="opacity-[var(--opacityBadge)] transition-opacity grid place-items-center absolute -top-3 -right-2 bg-dark w-8 h-8 text-light font-bold rounded-full">
{badgeNumber}
</div>
)}
{children}
</div> </div>
); );
if (props.target) { if (target) {
return ( return (
<a href={props.href} target={props.target} rel="noreferrer"> <a href={href} target={target} rel="noreferrer">
<div>{button}</div> <div className="relative">{button}</div>
</a> </a>
); );
} }
return ( return (
<div <div
className="relative"
onClick={() => { onClick={() => {
if (props.href || props.locale) if (href || locale)
router.push(props.href ?? router.asPath, props.href, { router.push(href ?? router.asPath, href, {
locale: props.locale, locale: locale,
}); });
}} }}
> >

View File

@ -26,6 +26,7 @@ interface Props {
>["categories"]; >["categories"];
thumbnail?: UploadImageFragment | null | undefined; thumbnail?: UploadImageFragment | null | undefined;
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
languageSwitcher?: JSX.Element;
} }
export default function ThumbnailHeader(props: Props): JSX.Element { export default function ThumbnailHeader(props: Props): JSX.Element {
@ -38,6 +39,7 @@ export default function ThumbnailHeader(props: Props): JSX.Element {
type, type,
categories, categories,
description, description,
languageSwitcher,
} = props; } = props;
return ( return (
@ -67,7 +69,7 @@ export default function ThumbnailHeader(props: Props): JSX.Element {
</div> </div>
</div> </div>
<div className="grid grid-flow-col gap-8"> <div className="flex place-content-center flex-row flew-wrap gap-8">
{type?.data?.attributes && ( {type?.data?.attributes && (
<div className="flex flex-col place-items-center gap-2"> <div className="flex flex-col place-items-center gap-2">
<h3 className="text-xl">{langui.type}</h3> <h3 className="text-xl">{langui.type}</h3>
@ -92,6 +94,7 @@ export default function ThumbnailHeader(props: Props): JSX.Element {
</div> </div>
</div> </div>
)} )}
{languageSwitcher}
</div> </div>
{description && <InsetBox className="mt-8">{description}</InsetBox>} {description && <InsetBox className="mt-8">{description}</InsetBox>}
</> </>

View File

@ -1,33 +1,31 @@
import { useRouter } from "next/router";
import { AppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps } from "queries/getAppStaticProps";
import { prettyLanguage } from "queries/helpers"; import { prettyLanguage } from "queries/helpers";
import { Dispatch, SetStateAction } from "react";
import Button from "./Button"; import Button from "./Button";
import ToolTip from "./ToolTip";
interface Props { interface Props {
className?: string; className?: string;
locales: (string | undefined)[];
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
langui: AppStaticProps["langui"]; locales: Map<string, number>;
href?: string; localesIndex: number | undefined;
setLocalesIndex: Dispatch<SetStateAction<number | undefined>>;
} }
export default function LanguageSwitcher(props: Props): JSX.Element { export default function LanguageSwitcher(props: Props): JSX.Element {
const { locales, langui, href } = props; const { locales, localesIndex, setLocalesIndex } = props;
const router = useRouter();
return ( return (
<div className="w-full grid place-content-center"> <ToolTip
<div className="flex flex-col place-items-center text-center gap-4 my-12 border-2 border-mid rounded-xl p-8 max-w-lg"> content={
<p>{langui.language_switch_message}</p> <div className="flex flex-col gap-2">
<div className="flex flex-wrap flex-row gap-2"> {[...locales].map(([locale, value], index) => (
{locales.map((locale, index) => (
<> <>
{locale && ( {locale && (
<Button <Button
key={index} key={index}
active={locale === router.locale} active={value === localesIndex}
href={href} onClick={() => setLocalesIndex(value)}
locale={locale}
> >
{prettyLanguage(locale, props.languages)} {prettyLanguage(locale, props.languages)}
</Button> </Button>
@ -35,7 +33,11 @@ export default function LanguageSwitcher(props: Props): JSX.Element {
</> </>
))} ))}
</div> </div>
</div> }
</div> >
<Button badgeNumber={locales.size}>
<span className="material-icons">translate</span>
</Button>
</ToolTip>
); );
} }

230
src/components/Post.tsx Normal file
View File

@ -0,0 +1,230 @@
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetPostQuery } from "graphql/generated";
import { useRouter } from "next/router";
import { AppStaticProps } from "queries/getAppStaticProps";
import {
getPreferredLanguage,
getStatusDescription,
prettySlug,
} from "queries/helpers";
import { useEffect, useMemo, useState } from "react";
import AppLayout from "./AppLayout";
import Chip from "./Chip";
import ThumbnailHeader from "./Content/ThumbnailHeader";
import HorizontalLine from "./HorizontalLine";
import LanguageSwitcher from "./LanguageSwitcher";
import Markdawn from "./Markdown/Markdawn";
import TOC from "./Markdown/TOC";
import ReturnButton, { ReturnButtonType } from "./PanelComponents/ReturnButton";
import ContentPanel from "./Panels/ContentPanel";
import SubPanel from "./Panels/SubPanel";
import RecorderChip from "./RecorderChip";
import ToolTip from "./ToolTip";
interface Props {
post: Exclude<
GetPostQuery["posts"],
null | undefined
>["data"][number]["attributes"];
langui: AppStaticProps["langui"];
languages: AppStaticProps["languages"];
currencies: AppStaticProps["currencies"];
returnHref?: string;
returnTitle?: string | null | undefined;
displayCredits?: boolean;
displayToc?: boolean;
displayThumbnailHeader?: boolean;
displayTitle?: boolean;
displayLanguageSwitcher?: boolean;
prependBody?: JSX.Element;
appendBody?: JSX.Element;
}
export default function Post(props: Props): JSX.Element {
const {
post,
langui,
languages,
returnHref,
returnTitle,
displayCredits,
displayToc,
displayThumbnailHeader,
displayLanguageSwitcher,
appendBody,
prependBody,
} = props;
const displayTitle = props.displayTitle ?? true;
const appLayout = useAppLayout();
const router = useRouter();
const [selectedTranslation, setSelectedTranslation] = useState<
| Exclude<
Exclude<Props["post"], null | undefined>["translations"],
null | undefined
>[number]
>();
const translationLocales: Map<string, number> = new Map();
const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<
number | undefined
>();
if (post?.translations) {
post.translations.map((translation, index) => {
if (translation?.language?.data?.attributes?.code) {
translationLocales.set(
translation.language.data.attributes.code,
index
);
}
});
}
useMemo(() => {
setSelectedTranslationIndex(
getPreferredLanguage(
appLayout.preferredLanguages ?? [router.locale],
translationLocales
)
);
}, [appLayout.preferredLanguages]);
useEffect(() => {
if (selectedTranslationIndex !== undefined)
setSelectedTranslation(post?.translations?.[selectedTranslationIndex]);
}, [selectedTranslationIndex]);
const thumbnail =
selectedTranslation?.thumbnail?.data?.attributes ??
post?.thumbnail?.data?.attributes;
const body = selectedTranslation?.body ?? "";
const title = selectedTranslation?.title ?? prettySlug(post?.slug);
const except = selectedTranslation?.excerpt ?? "";
const subPanel =
returnHref || returnTitle || displayCredits || displayToc ? (
<SubPanel>
{returnHref && returnTitle && (
<ReturnButton
href={returnHref}
title={returnTitle}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
)}
{displayCredits && (
<>
{selectedTranslation && (
<div className="grid grid-flow-col place-items-center place-content-center gap-2">
<p className="font-headers">{langui.status}:</p>
<ToolTip
content={getStatusDescription(
selectedTranslation.status,
langui
)}
maxWidth={"20rem"}
>
<Chip>{selectedTranslation.status}</Chip>
</ToolTip>
</div>
)}
{post?.authors && post.authors.data.length > 0 && (
<div>
<p className="font-headers">{"Authors"}:</p>
<div className="grid place-items-center place-content-center gap-2">
{post.authors.data.map((author) => (
<>
{author.attributes && (
<RecorderChip
key={author.id}
langui={langui}
recorder={author.attributes}
/>
)}
</>
))}
</div>
</div>
)}
<HorizontalLine />
</>
)}
{displayToc && <TOC text={body} title={title} />}
</SubPanel>
) : undefined;
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/news"
title={langui.news}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
{displayThumbnailHeader ? (
<>
<ThumbnailHeader
thumbnail={thumbnail}
title={title}
description={except}
langui={langui}
categories={post?.categories}
languageSwitcher={
<LanguageSwitcher
languages={languages}
locales={translationLocales}
localesIndex={selectedTranslationIndex}
setLocalesIndex={setSelectedTranslationIndex}
/>
}
/>
<HorizontalLine />
</>
) : (
<>
{displayLanguageSwitcher && (
<div className="grid place-content-end place-items-start">
<LanguageSwitcher
languages={languages}
locales={translationLocales}
localesIndex={selectedTranslationIndex}
setLocalesIndex={setSelectedTranslationIndex}
/>
</div>
)}
{displayTitle && (
<h1 className="text-center flex gap-3 justify-center text-4xl my-16">
{title}
</h1>
)}
</>
)}
{prependBody}
<Markdawn text={body} />
{appendBody}
</ContentPanel>
);
return (
<AppLayout
navTitle={title}
contentPanel={contentPanel}
subPanel={subPanel}
thumbnail={thumbnail ?? undefined}
{...props}
/>
);
}

View File

@ -4,7 +4,7 @@ query getContentText($slug: String, $language_code: String) {
id id
attributes { attributes {
slug slug
titles(filters: { language: { code: { eq: $language_code } } }) { titles {
pre_title pre_title
title title
subtitle subtitle
@ -56,7 +56,9 @@ query getContentText($slug: String, $language_code: String) {
} }
} }
} }
text_set_languages: text_set { text_set {
status
text
language { language {
data { data {
attributes { attributes {
@ -64,10 +66,6 @@ query getContentText($slug: String, $language_code: String) {
} }
} }
} }
}
text_set(filters: { language: { code: { eq: $language_code } } }) {
status
text
source_language { source_language {
data { data {
attributes { attributes {

View File

@ -29,7 +29,8 @@ query getLibraryItemScans($slug: String, $language_code: String) {
ending_time ending_time
} }
} }
scan_set_languages: scan_set { scan_set {
status
language { language {
data { data {
attributes { attributes {
@ -37,16 +38,6 @@ query getLibraryItemScans($slug: String, $language_code: String) {
} }
} }
} }
}
scan_set(
filters: {
or: [
{ language: { code: { eq: "xx" } } }
{ language: { code: { eq: $language_code } } }
]
}
) {
status
source_language { source_language {
data { data {
attributes { attributes {

View File

@ -33,7 +33,7 @@ query getPost($slug: String, $language_code: String) {
} }
} }
} }
translations_languages: translations { translations {
language { language {
data { data {
attributes { attributes {
@ -41,8 +41,6 @@ query getPost($slug: String, $language_code: String) {
} }
} }
} }
}
translations(filters: { language: { code: { eq: $language_code } } }) {
status status
title title
excerpt excerpt

View File

@ -1,18 +1,8 @@
import AppLayout from "components/AppLayout"; import Post from "components/Post";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { GetPostQuery } from "graphql/generated"; import { GetPostQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
post: Exclude< post: Exclude<
@ -22,53 +12,17 @@ interface Props extends AppStaticProps {
} }
export default function AccordsHandbook(props: Props): JSX.Element { export default function AccordsHandbook(props: Props): JSX.Element {
const { langui, post } = props; const { post, langui, languages, currencies } = props;
const router = useRouter();
const locales = getLocalesFromLanguages(post?.translations_languages);
const body = post?.translations?.[0]?.body ?? "";
const title = post?.translations?.[0]?.title ?? prettySlug(post?.slug);
const subPanel = (
<SubPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.desktop}
langui={langui}
title={langui.about_us}
horizontalLine
/>
<TOC text={body} title={title} />
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return ( return (
<AppLayout <Post
navTitle={title} currencies={currencies}
subPanel={subPanel} languages={languages}
contentPanel={contentPanel} langui={langui}
{...props} post={post}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
displayLanguageSwitcher
/> />
); );
} }

View File

@ -1,24 +1,12 @@
import AppLayout from "components/AppLayout";
import InsetBox from "components/InsetBox"; import InsetBox from "components/InsetBox";
import LanguageSwitcher from "components/LanguageSwitcher"; import Post from "components/Post";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { GetPostQuery } from "graphql/generated"; import { GetPostQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { RequestMailProps, ResponseMailProps } from "pages/api/mail"; import { RequestMailProps, ResponseMailProps } from "pages/api/mail";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { import { randomInt } from "queries/helpers";
getLocalesFromLanguages,
prettySlug,
randomInt,
} from "queries/helpers";
import { useState } from "react"; import { useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
@ -29,200 +17,170 @@ interface Props extends AppStaticProps {
} }
export default function AboutUs(props: Props): JSX.Element { export default function AboutUs(props: Props): JSX.Element {
const { langui, post } = props; const { post, langui, languages, currencies } = props;
const router = useRouter(); const router = useRouter();
const [formResponse, setFormResponse] = useState(""); const [formResponse, setFormResponse] = useState("");
const [formState, setFormState] = useState<"completed" | "ongoing" | "stale">( const [formState, setFormState] = useState<"completed" | "ongoing" | "stale">(
"stale" "stale"
); );
const locales = getLocalesFromLanguages(post?.translations_languages);
const [randomNumber1, setRandomNumber1] = useState(randomInt(0, 10)); const [randomNumber1, setRandomNumber1] = useState(randomInt(0, 10));
const [randomNumber2, setRandomNumber2] = useState(randomInt(0, 10)); const [randomNumber2, setRandomNumber2] = useState(randomInt(0, 10));
const body = post?.translations?.[0]?.body ?? ""; const contactForm = (
const title = post?.translations?.[0]?.title ?? prettySlug(post?.slug); <div className="flex flex-col gap-8 text-center">
<form
className={`gap-8 grid ${
formState !== "stale" &&
"opacity-60 cursor-not-allowed touch-none pointer-events-none"
}`}
onSubmit={(event) => {
event.preventDefault();
const subPanel = ( const fields = event.target as unknown as {
<SubPanel> verif: HTMLInputElement;
<ReturnButton name: HTMLInputElement;
href="/about-us" email: HTMLInputElement;
displayOn={ReturnButtonType.desktop} message: HTMLInputElement;
langui={langui} };
title={langui.about_us}
horizontalLine
/>
<TOC text={body} title={title} />
</SubPanel>
);
const contentPanel = ( setFormState("ongoing");
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
<div className="flex flex-col gap-8 text-center"> if (
<form parseInt(fields.verif.value, 10) ===
className={`gap-8 grid ${ randomNumber1 + randomNumber2 &&
formState !== "stale" && formState !== "completed"
"opacity-60 cursor-not-allowed touch-none pointer-events-none" ) {
}`} const content: RequestMailProps = {
onSubmit={(event) => { name: fields.name.value,
event.preventDefault(); email: fields.email.value,
message: fields.message.value,
const fields = event.target as unknown as { formName: "Contact Form",
verif: HTMLInputElement;
name: HTMLInputElement;
email: HTMLInputElement;
message: HTMLInputElement;
}; };
fetch("/api/mail", {
method: "POST",
body: JSON.stringify(content),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
})
.then(async (responseJson) => responseJson.json())
.then((response: ResponseMailProps) => {
switch (response.code) {
case "OKAY":
setFormResponse(langui.response_email_success ?? "");
setFormState("completed");
setFormState("ongoing"); break;
if ( case "EENVELOPE":
parseInt(fields.verif.value, 10) === setFormResponse(langui.response_invalid_email ?? "");
randomNumber1 + randomNumber2 && setFormState("stale");
formState !== "completed" break;
) {
const content: RequestMailProps = {
name: fields.name.value,
email: fields.email.value,
message: fields.message.value,
formName: "Contact Form",
};
fetch("/api/mail", {
method: "POST",
body: JSON.stringify(content),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
})
.then(async (responseJson) => responseJson.json())
.then((response: ResponseMailProps) => {
switch (response.code) {
case "OKAY":
setFormResponse(langui.response_email_success ?? "");
setFormState("completed");
break; default:
setFormResponse(response.message ?? "");
setFormState("stale");
break;
}
});
} else {
setFormResponse(langui.response_invalid_code ?? "");
setFormState("stale");
setRandomNumber1(randomInt(0, 10));
setRandomNumber2(randomInt(0, 10));
}
case "EENVELOPE": router.replace("#send-response");
setFormResponse(langui.response_invalid_email ?? ""); fields.verif.value = "";
setFormState("stale"); }}
break; >
<div className="flex flex-col place-items-center gap-1">
default: <label htmlFor="name">{langui.name}:</label>
setFormResponse(response.message ?? ""); <input
setFormState("stale"); type="text"
break; className="mobile:w-full"
} name="name"
}); id="name"
} else { required
setFormResponse(langui.response_invalid_code ?? ""); disabled={formState !== "stale"}
setFormState("stale"); />
setRandomNumber1(randomInt(0, 10));
setRandomNumber2(randomInt(0, 10));
}
router.replace("#send-response");
fields.verif.value = "";
}}
>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="name">{langui.name}:</label>
<input
type="text"
className="mobile:w-full"
name="name"
id="name"
required
disabled={formState !== "stale"}
/>
</div>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="email">{langui.email}:</label>
<input
type="email"
className="mobile:w-full"
name="email"
id="email"
required
disabled={formState !== "stale"}
/>
<p className="text-sm text-dark italic opacity-70">
{langui.email_gdpr_notice}
</p>
</div>
<div className="flex flex-col place-items-center gap-1 w-full">
<label htmlFor="message">{langui.message}:</label>
<textarea
name="message"
id="message"
className="w-full"
rows={8}
required
disabled={formState !== "stale"}
/>
</div>
<div className="grid grid-cols-2 place-items-center">
<div className="flex flex-row place-items-center gap-2">
<label
className="flex-shrink-0"
htmlFor="verif"
>{`${randomNumber1} + ${randomNumber2} =`}</label>
<input
className="w-24"
type="number"
name="verif"
id="verif"
required
disabled={formState !== "stale"}
/>
</div>
<input
type="submit"
value={langui.send ?? "Send"}
className="w-min !px-6"
disabled={formState !== "stale"}
/>
</div>
</form>
<div id="send-response">
{formResponse && (
<InsetBox>
<p>{formResponse}</p>
</InsetBox>
)}
</div> </div>
<div className="flex flex-col place-items-center gap-1">
<label htmlFor="email">{langui.email}:</label>
<input
type="email"
className="mobile:w-full"
name="email"
id="email"
required
disabled={formState !== "stale"}
/>
<p className="text-sm text-dark italic opacity-70">
{langui.email_gdpr_notice}
</p>
</div>
<div className="flex flex-col place-items-center gap-1 w-full">
<label htmlFor="message">{langui.message}:</label>
<textarea
name="message"
id="message"
className="w-full"
rows={8}
required
disabled={formState !== "stale"}
/>
</div>
<div className="grid grid-cols-2 place-items-center">
<div className="flex flex-row place-items-center gap-2">
<label
className="flex-shrink-0"
htmlFor="verif"
>{`${randomNumber1} + ${randomNumber2} =`}</label>
<input
className="w-24"
type="number"
name="verif"
id="verif"
required
disabled={formState !== "stale"}
/>
</div>
<input
type="submit"
value={langui.send ?? "Send"}
className="w-min !px-6"
disabled={formState !== "stale"}
/>
</div>
</form>
<div id="send-response">
{formResponse && (
<InsetBox>
<p>{formResponse}</p>
</InsetBox>
)}
</div> </div>
</ContentPanel> </div>
); );
return ( return (
<AppLayout <Post
navTitle={"Contact"} currencies={currencies}
subPanel={subPanel} languages={languages}
contentPanel={contentPanel} langui={langui}
{...props} post={post}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
appendBody={contactForm}
displayLanguageSwitcher
/> />
); );
} }

View File

@ -1,18 +1,8 @@
import AppLayout from "components/AppLayout"; import Post from "components/Post";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { GetPostQuery } from "graphql/generated"; import { GetPostQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
post: Exclude< post: Exclude<
@ -22,53 +12,17 @@ interface Props extends AppStaticProps {
} }
export default function SiteInformation(props: Props): JSX.Element { export default function SiteInformation(props: Props): JSX.Element {
const { langui, post } = props; const { post, langui, languages, currencies } = props;
const router = useRouter();
const locales = getLocalesFromLanguages(post?.translations_languages);
const body = post?.translations?.[0]?.body ?? "";
const title = post?.translations?.[0]?.title ?? prettySlug(post?.slug);
const subPanel = (
<SubPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.desktop}
langui={langui}
title={langui.about_us}
horizontalLine
/>
<TOC text={body} title={title} />
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return ( return (
<AppLayout <Post
navTitle={title} currencies={currencies}
subPanel={subPanel} languages={languages}
contentPanel={contentPanel} langui={langui}
{...props} post={post}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
displayLanguageSwitcher
/> />
); );
} }

View File

@ -1,18 +1,8 @@
import AppLayout from "components/AppLayout"; import Post from "components/Post";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import { GetPostQuery } from "graphql/generated"; import { GetPostQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
post: Exclude< post: Exclude<
@ -21,53 +11,17 @@ interface Props extends AppStaticProps {
>["data"][number]["attributes"]; >["data"][number]["attributes"];
} }
export default function SharingPolicy(props: Props): JSX.Element { export default function SharingPolicy(props: Props): JSX.Element {
const { langui, post } = props; const { post, langui, languages, currencies } = props;
const locales = getLocalesFromLanguages(post?.translations_languages);
const router = useRouter();
const body = post?.translations?.[0]?.body ?? "";
const title = post?.translations?.[0]?.title ?? prettySlug(post?.slug);
const subPanel = (
<SubPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.desktop}
langui={langui}
title={langui.about_us}
horizontalLine
/>
<TOC text={body} title={title} />
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/about-us"
displayOn={ReturnButtonType.mobile}
langui={langui}
title={langui.about_us}
className="mb-10"
/>
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return ( return (
<AppLayout <Post
navTitle={title} currencies={currencies}
subPanel={subPanel} languages={languages}
contentPanel={contentPanel} langui={langui}
{...props} post={post}
returnHref="/about-us/"
returnTitle={langui.about_us}
displayToc
displayLanguageSwitcher
/> />
); );
} }

View File

@ -13,6 +13,7 @@ import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel"; import SubPanel from "components/Panels/SubPanel";
import RecorderChip from "components/RecorderChip"; import RecorderChip from "components/RecorderChip";
import ToolTip from "components/ToolTip"; import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetContentTextQuery } from "graphql/generated"; import { GetContentTextQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { import {
@ -23,7 +24,7 @@ import {
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { import {
getLocalesFromLanguages, getPreferredLanguage,
getStatusDescription, getStatusDescription,
prettyinlineTitle, prettyinlineTitle,
prettyLanguage, prettyLanguage,
@ -31,6 +32,7 @@ import {
prettyTestError, prettyTestError,
prettyTestWarning, prettyTestWarning,
} from "queries/helpers"; } from "queries/helpers";
import { useEffect, useMemo, useState } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
content: Exclude< content: Exclude<
@ -47,7 +49,49 @@ export default function Content(props: Props): JSX.Element {
useTesting(props); useTesting(props);
const { langui, content, languages } = props; const { langui, content, languages } = props;
const router = useRouter(); const router = useRouter();
const locales = getLocalesFromLanguages(content?.text_set_languages); const appLayout = useAppLayout();
const [selectedTextSet, setSelectedTextSet] = useState<
| Exclude<
Exclude<Props["content"], null | undefined>["text_set"],
null | undefined
>[number]
>();
const [selectedTitle, setSelectedTitle] = useState<
| Exclude<
Exclude<Props["content"], null | undefined>["titles"],
null | undefined
>[number]
>();
const textSetLocales: Map<string, number> = new Map();
const [selectedTextSetIndex, setSelectedTextSetIndex] = useState<
number | undefined
>();
if (content?.text_set) {
content.text_set.map((textSet, index) => {
if (textSet?.language?.data?.attributes?.code && textSet.text) {
textSetLocales.set(textSet.language.data.attributes.code, index);
}
});
}
useMemo(() => {
setSelectedTextSetIndex(
getPreferredLanguage(
appLayout.preferredLanguages ?? [router.locale],
textSetLocales
)
);
}, [appLayout.preferredLanguages]);
useEffect(() => {
if (selectedTextSetIndex !== undefined)
setSelectedTextSet(content?.text_set?.[selectedTextSetIndex]);
if (selectedTextSetIndex !== undefined)
setSelectedTitle(content?.titles?.[selectedTextSetIndex]);
}, [selectedTextSetIndex]);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
@ -59,27 +103,25 @@ export default function Content(props: Props): JSX.Element {
horizontalLine horizontalLine
/> />
{content?.text_set?.[0]?.source_language?.data?.attributes && ( {selectedTextSet?.source_language?.data?.attributes && (
<div className="grid gap-5"> <div className="grid gap-5">
<h2 className="text-xl"> <h2 className="text-xl">
{content.text_set[0].source_language.data.attributes.code === {selectedTextSet.source_language.data.attributes.code ===
router.locale selectedTextSet.language?.data?.attributes?.code
? langui.transcript_notice ? langui.transcript_notice
: langui.translation_notice} : langui.translation_notice}
</h2> </h2>
{content.text_set[0].source_language.data.attributes.code !== {selectedTextSet.source_language.data.attributes.code !==
router.locale && ( selectedTextSet.language?.data?.attributes?.code && (
<div className="grid place-items-center gap-2"> <div className="grid place-items-center gap-2">
<p className="font-headers">{langui.source_language}:</p> <p className="font-headers">{langui.source_language}:</p>
<Button <Button
href={router.asPath} href={router.asPath}
locale={ locale={selectedTextSet.source_language.data.attributes.code}
content.text_set[0].source_language.data.attributes.code
}
> >
{prettyLanguage( {prettyLanguage(
content.text_set[0].source_language.data.attributes.code, selectedTextSet.source_language.data.attributes.code,
languages languages
)} )}
</Button> </Button>
@ -90,19 +132,19 @@ export default function Content(props: Props): JSX.Element {
<p className="font-headers">{langui.status}:</p> <p className="font-headers">{langui.status}:</p>
<ToolTip <ToolTip
content={getStatusDescription(content.text_set[0].status, langui)} content={getStatusDescription(selectedTextSet.status, langui)}
maxWidth={"20rem"} maxWidth={"20rem"}
> >
<Chip>{content.text_set[0].status}</Chip> <Chip>{selectedTextSet.status}</Chip>
</ToolTip> </ToolTip>
</div> </div>
{content.text_set[0].transcribers && {selectedTextSet.transcribers &&
content.text_set[0].transcribers.data.length > 0 && ( selectedTextSet.transcribers.data.length > 0 && (
<div> <div>
<p className="font-headers">{langui.transcribers}:</p> <p className="font-headers">{langui.transcribers}:</p>
<div className="grid place-items-center place-content-center gap-2"> <div className="grid place-items-center place-content-center gap-2">
{content.text_set[0].transcribers.data.map((recorder) => ( {selectedTextSet.transcribers.data.map((recorder) => (
<> <>
{recorder.attributes && ( {recorder.attributes && (
<RecorderChip <RecorderChip
@ -117,12 +159,12 @@ export default function Content(props: Props): JSX.Element {
</div> </div>
)} )}
{content.text_set[0].translators && {selectedTextSet.translators &&
content.text_set[0].translators.data.length > 0 && ( selectedTextSet.translators.data.length > 0 && (
<div> <div>
<p className="font-headers">{langui.translators}:</p> <p className="font-headers">{langui.translators}:</p>
<div className="grid place-items-center place-content-center gap-2"> <div className="grid place-items-center place-content-center gap-2">
{content.text_set[0].translators.data.map((recorder) => ( {selectedTextSet.translators.data.map((recorder) => (
<> <>
{recorder.attributes && ( {recorder.attributes && (
<RecorderChip <RecorderChip
@ -137,12 +179,12 @@ export default function Content(props: Props): JSX.Element {
</div> </div>
)} )}
{content.text_set[0].proofreaders && {selectedTextSet.proofreaders &&
content.text_set[0].proofreaders.data.length > 0 && ( selectedTextSet.proofreaders.data.length > 0 && (
<div> <div>
<p className="font-headers">{langui.proofreaders}:</p> <p className="font-headers">{langui.proofreaders}:</p>
<div className="grid place-items-center place-content-center gap-2"> <div className="grid place-items-center place-content-center gap-2">
{content.text_set[0].proofreaders.data.map((recorder) => ( {selectedTextSet.proofreaders.data.map((recorder) => (
<> <>
{recorder.attributes && ( {recorder.attributes && (
<RecorderChip <RecorderChip
@ -159,25 +201,23 @@ export default function Content(props: Props): JSX.Element {
</div> </div>
)} )}
{content?.text_set && {selectedTextSet && content?.text_set && selectedTextSet.text && (
content.text_set.length > 0 && <>
content.text_set[0]?.text && ( <HorizontalLine />
<> <TOC
<HorizontalLine /> text={selectedTextSet.text}
<TOC title={
text={content.text_set[0].text} content.titles && content.titles.length > 0 && selectedTitle
title={ ? prettyinlineTitle(
content.titles && content.titles.length > 0 && content.titles[0] selectedTitle.pre_title,
? prettyinlineTitle( selectedTitle.title,
content.titles[0].pre_title, selectedTitle.subtitle
content.titles[0].title, )
content.titles[0].subtitle : prettySlug(content.slug)
) }
: prettySlug(content.slug) />
} </>
/> )}
</>
)}
</SubPanel> </SubPanel>
); );
const contentPanel = ( const contentPanel = (
@ -189,46 +229,37 @@ export default function Content(props: Props): JSX.Element {
displayOn={ReturnButtonType.mobile} displayOn={ReturnButtonType.mobile}
className="mb-10" className="mb-10"
/> />
{content && ( {content && (
<div className="grid place-items-center"> <div className="grid place-items-center">
<ThumbnailHeader <ThumbnailHeader
thumbnail={content.thumbnail?.data?.attributes} thumbnail={content.thumbnail?.data?.attributes}
pre_title={ pre_title={
content.titles && content.titles.length > 0 selectedTitle?.pre_title ?? content.titles?.[0]?.pre_title
? content.titles[0]?.pre_title
: undefined
}
title={
content.titles && content.titles.length > 0
? content.titles[0]?.title
: prettySlug(content.slug)
}
subtitle={
content.titles && content.titles.length > 0
? content.titles[0]?.subtitle
: undefined
} }
title={selectedTitle?.title ?? content.titles?.[0]?.title}
subtitle={selectedTitle?.subtitle ?? content.titles?.[0]?.subtitle}
description={ description={
content.titles && content.titles.length > 0 selectedTitle?.description ?? content.titles?.[0]?.description
? content.titles[0]?.description
: undefined
} }
type={content.type} type={content.type}
categories={content.categories} categories={content.categories}
langui={langui} langui={langui}
languageSwitcher={
selectedTextSet ? (
<LanguageSwitcher
locales={textSetLocales}
languages={props.languages}
localesIndex={selectedTextSetIndex}
setLocalesIndex={setSelectedTextSetIndex}
/>
) : undefined
}
/> />
<HorizontalLine /> <HorizontalLine />
{locales.includes(router.locale ?? "en") ? ( <Markdawn text={selectedTextSet?.text ?? ""} />
<Markdawn text={content.text_set?.[0]?.text ?? ""} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</div> </div>
)} )}
</ContentPanel> </ContentPanel>
@ -276,7 +307,7 @@ export async function getStaticProps(
context: GetStaticPropsContext context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> { ): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk(); const sdk = getReadySdk();
const slug = context.params?.slug?.toString() ?? ""; const slug = context.params?.slug.toString() ?? "";
const content = await sdk.getContentText({ const content = await sdk.getContentText({
slug: slug, slug: slug,
language_code: context.locale ?? "en", language_code: context.locale ?? "en",

View File

@ -1,13 +1,8 @@
import AppLayout from "components/AppLayout"; import Post from "components/Post";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import ContentPanel from "components/Panels/ContentPanel";
import { GetPostQuery } from "graphql/generated"; import { GetPostQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import { getLocalesFromLanguages, prettySlug } from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
post: Exclude< post: Exclude<
@ -17,35 +12,26 @@ interface Props extends AppStaticProps {
} }
export default function Home(props: Props): JSX.Element { export default function Home(props: Props): JSX.Element {
const { post } = props; const { post, langui, languages, currencies } = props;
const locales = getLocalesFromLanguages(post?.translations_languages); return (
const router = useRouter(); <Post
currencies={currencies}
const body = post?.translations?.[0]?.body ?? ""; languages={languages}
const title = post?.translations?.[0]?.title ?? prettySlug(post?.slug); langui={langui}
post={post}
const contentPanel = ( prependBody={
<ContentPanel> <div className="grid place-items-center place-content-center w-full gap-5 text-center">
<div className="grid place-items-center place-content-center w-full gap-5 text-center"> <div className="[mask:url('/icons/accords.svg')] [mask-size:contain] [mask-repeat:no-repeat] [mask-position:center] w-32 aspect-square mobile:w-[50vw] bg-black" />
<div className="[mask:url('/icons/accords.svg')] [mask-size:contain] [mask-repeat:no-repeat] [mask-position:center] w-32 aspect-square mobile:w-[50vw] bg-black" /> <h1 className="text-5xl mb-0">Accord&rsquo;s Library</h1>
<h1 className="text-5xl mb-0">Accord&rsquo;s Library</h1> <h2 className="text-xl -mt-5">
<h2 className="text-xl -mt-5"> Discover Analyze Translate Archive
Discover Analyze Translate Archive </h2>
</h2> </div>
</div> }
{locales.includes(router.locale ?? "en") ? ( displayTitle={false}
<Markdawn text={body} /> displayLanguageSwitcher
) : ( />
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
); );
return <AppLayout navTitle={title} contentPanel={contentPanel} {...props} />;
} }
export async function getStaticProps( export async function getStaticProps(

View File

@ -1,17 +1,4 @@
import AppLayout from "components/AppLayout"; import Post from "components/Post";
import Chip from "components/Chip";
import ThumbnailHeader from "components/Content/ThumbnailHeader";
import HorizontalLine from "components/HorizontalLine";
import LanguageSwitcher from "components/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
ReturnButtonType,
} from "components/PanelComponents/ReturnButton";
import ContentPanel from "components/Panels/ContentPanel";
import SubPanel from "components/Panels/SubPanel";
import RecorderChip from "components/RecorderChip";
import ToolTip from "components/ToolTip";
import { GetPostQuery } from "graphql/generated"; import { GetPostQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { import {
@ -19,13 +6,7 @@ import {
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
getLocalesFromLanguages,
getStatusDescription,
prettySlug,
} from "queries/helpers";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
post: Exclude< post: Exclude<
@ -39,106 +20,18 @@ interface Props extends AppStaticProps {
} }
export default function LibrarySlug(props: Props): JSX.Element { export default function LibrarySlug(props: Props): JSX.Element {
const { post, langui } = props; const { post, langui, languages, currencies } = props;
const locales = getLocalesFromLanguages(post?.translations_languages);
const router = useRouter();
const thumbnail = post?.translations?.[0]?.thumbnail?.data
? post.translations[0].thumbnail.data.attributes
: post?.thumbnail?.data
? post.thumbnail.data.attributes
: undefined;
const body = post?.translations?.[0]?.body ?? "";
const title = post?.translations?.[0]?.title ?? prettySlug(post?.slug);
const except = post?.translations?.[0]?.excerpt ?? "";
const subPanel = (
<SubPanel>
<ReturnButton
href="/news"
title={langui.news}
langui={langui}
displayOn={ReturnButtonType.desktop}
horizontalLine
/>
{post?.translations?.[0] && (
<div className="grid grid-flow-col place-items-center place-content-center gap-2">
<p className="font-headers">{langui.status}:</p>
<ToolTip
content={getStatusDescription(post.translations[0].status, langui)}
maxWidth={"20rem"}
>
<Chip>{post.translations[0].status}</Chip>
</ToolTip>
</div>
)}
{post?.authors && post.authors.data.length > 0 && (
<div>
<p className="font-headers">{"Authors"}:</p>
<div className="grid place-items-center place-content-center gap-2">
{post.authors.data.map((author) => (
<>
{author.attributes && (
<RecorderChip
key={author.id}
langui={langui}
recorder={author.attributes}
/>
)}
</>
))}
</div>
</div>
)}
<HorizontalLine />
<TOC text={body} title={title} />
</SubPanel>
);
const contentPanel = (
<ContentPanel>
<ReturnButton
href="/news"
title={langui.news}
langui={langui}
displayOn={ReturnButtonType.mobile}
className="mb-10"
/>
<ThumbnailHeader
thumbnail={thumbnail}
title={title}
description={except}
langui={langui}
categories={post?.categories}
/>
<HorizontalLine />
{locales.includes(router.locale ?? "en") ? (
<Markdawn text={body} />
) : (
<LanguageSwitcher
locales={locales}
languages={props.languages}
langui={props.langui}
/>
)}
</ContentPanel>
);
return ( return (
<AppLayout <Post
navTitle={title} currencies={currencies}
contentPanel={contentPanel} languages={languages}
subPanel={subPanel} langui={langui}
thumbnail={thumbnail ?? undefined} post={post}
{...props} returnHref="/news"
returnTitle={langui.news}
displayCredits
displayThumbnailHeader
displayToc
/> />
); );
} }

View File

@ -480,3 +480,15 @@ export function arrayMove<T>(arr: T[], old_index: number, new_index: number) {
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]); arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr; return arr;
} }
export function getPreferredLanguage(
preferredLanguages: (string | undefined)[],
availableLanguages: Map<string, number>
): number | undefined {
for (const locale of preferredLanguages) {
if (locale && availableLanguages.has(locale)) {
return availableLanguages.get(locale);
}
}
return undefined;
}