Extracted language selector logic into a custom hook

This commit is contained in:
DrMint 2022-05-07 11:17:58 +02:00
parent 6f69aaf236
commit 85106d1735
7 changed files with 321 additions and 232 deletions

View File

@ -1,15 +1,17 @@
import Chip from "components/Chip";
import Img, { getAssetURL, ImageQuality } from "components/Img";
import Img, {
getAssetFilename,
getAssetURL,
ImageQuality,
} from "components/Img";
import Button from "components/Inputs/Button";
import LanguageSwitcher from "components/Inputs/LanguageSwitcher";
import RecorderChip from "components/RecorderChip";
import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetLibraryItemScansQuery } from "graphql/generated";
import { useRouter } from "next/router";
import useSmartLanguage from "hooks/useSmartLanguage";
import { AppStaticProps } from "queries/getAppStaticProps";
import { getPreferredLanguage, getStatusDescription } from "queries/helpers";
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { getStatusDescription, isInteger } from "queries/helpers";
import { Dispatch, SetStateAction } from "react";
interface Props {
setLightboxOpen: Dispatch<SetStateAction<boolean>>;
@ -62,52 +64,28 @@ export default function ScanSet(props: Props): JSX.Element {
langui,
content,
} = props;
const appLayout = useAppLayout();
const router = useRouter();
const [selectedScan, setSelectedScan] = useState<Props["scanSet"][number]>();
const scanLocales: Map<string, number> = new Map();
const [selectedScanIndex, setSelectedScanIndex] = useState<
number | undefined
>();
scanSet.map((scan, index) => {
if (scan?.language?.data?.attributes?.code) {
scanLocales.set(scan.language.data.attributes.code, index);
}
});
useMemo(() => {
setSelectedScanIndex(
getPreferredLanguage(
appLayout.preferredLanguages ?? [router.locale],
scanLocales
)
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appLayout.preferredLanguages]);
useEffect(() => {
if (selectedScanIndex !== undefined) {
const selectedScanSet = scanSet[selectedScanIndex];
selectedScanSet?.pages?.data.sort((a, b) => {
function isInteger(value: string): boolean {
// eslint-disable-next-line require-unicode-regexp
return /^\d+$/.test(value);
}
function getFileName(path: string): string {
let result = path.split("/");
result = result[result.length - 1].split(".");
result = result
.splice(0, result.length - 1)
.join(".")
.split("_");
return result[0];
}
const [selectedScan, LanguageSwitcher] = useSmartLanguage({
items: scanSet,
languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code,
transform: (item) => {
item?.pages?.data.sort((a, b) => {
if (a.attributes?.url && b.attributes?.url) {
const aName = getFileName(a.attributes.url);
const bName = getFileName(b.attributes.url);
let aName = getAssetFilename(a.attributes.url);
let bName = getAssetFilename(b.attributes.url);
/*
* If the number is a succession of 0s, make the number
* incrementally smaller than 0 (i.e: 00 becomes -1)
*/
if (aName.replaceAll("0", "").length === 0) {
aName = (1 - aName.length).toString(10);
}
if (bName.replaceAll("0", "").length === 0) {
bName = (1 - bName.length).toString(10);
}
if (isInteger(aName) && isInteger(bName)) {
return parseInt(aName, 10) - parseInt(bName, 10);
}
@ -115,10 +93,9 @@ export default function ScanSet(props: Props): JSX.Element {
}
return 0;
});
setSelectedScan(selectedScanSet);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedScanIndex]);
return item;
},
});
return (
<>
@ -144,12 +121,7 @@ export default function ScanSet(props: Props): JSX.Element {
</Button>
)}
<LanguageSwitcher
languages={languages}
locales={scanLocales}
localesIndex={selectedScanIndex}
setLocalesIndex={setSelectedScanIndex}
/>
<LanguageSwitcher />
<div className="grid place-items-center place-content-center">
<p className="font-headers">{langui.status}:</p>

View File

@ -1,17 +1,15 @@
import Chip from "components/Chip";
import Img, { getAssetURL, ImageQuality } from "components/Img";
import LanguageSwitcher from "components/Inputs/LanguageSwitcher";
import RecorderChip from "components/RecorderChip";
import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import {
GetLibraryItemScansQuery,
UploadImageFragment,
} from "graphql/generated";
import { useRouter } from "next/router";
import useSmartLanguage from "hooks/useSmartLanguage";
import { AppStaticProps } from "queries/getAppStaticProps";
import { getPreferredLanguage, getStatusDescription } from "queries/helpers";
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from "react";
import { getStatusDescription } from "queries/helpers";
import { Dispatch, SetStateAction } from "react";
interface Props {
setLightboxOpen: Dispatch<SetStateAction<boolean>>;
@ -40,38 +38,13 @@ export default function ScanSetCover(props: Props): JSX.Element {
languages,
langui,
} = props;
const appLayout = useAppLayout();
const router = useRouter();
const [selectedScan, setSelectedScan] = useState<Props["images"][number]>();
const scanLocales: Map<string, number> = new Map();
const [selectedScanIndex, setSelectedScanIndex] = useState<
number | undefined
>();
images.map((scan, index) => {
if (scan?.language?.data?.attributes?.code) {
scanLocales.set(scan.language.data.attributes.code, index);
}
const [selectedScan, LanguageSwitcher] = useSmartLanguage({
items: images,
languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code,
});
useMemo(() => {
setSelectedScanIndex(
getPreferredLanguage(
appLayout.preferredLanguages ?? [router.locale],
scanLocales
)
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appLayout.preferredLanguages]);
useEffect(() => {
if (selectedScanIndex !== undefined)
setSelectedScan(images[selectedScanIndex]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedScanIndex]);
const coverImages: UploadImageFragment[] = [];
if (selectedScan?.obi_belt?.full?.data?.attributes)
coverImages.push(selectedScan.obi_belt.full.data.attributes);
@ -105,12 +78,7 @@ export default function ScanSetCover(props: Props): JSX.Element {
</div>
<div className="flex flex-row flex-wrap gap-4 pb-6 place-items-center">
<LanguageSwitcher
languages={languages}
locales={scanLocales}
localesIndex={selectedScanIndex}
setLocalesIndex={setSelectedScanIndex}
/>
<LanguageSwitcher />
<div className="grid place-items-center place-content-center">
<p className="font-headers">{langui.status}:</p>

View File

@ -1,17 +1,10 @@
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetPostQuery } from "graphql/generated";
import { useRouter } from "next/router";
import useSmartLanguage from "hooks/useSmartLanguage";
import { AppStaticProps } from "queries/getAppStaticProps";
import {
getPreferredLanguage,
getStatusDescription,
prettySlug,
} from "queries/helpers";
import { useEffect, useMemo, useState } from "react";
import { getStatusDescription, prettySlug } from "queries/helpers";
import AppLayout from "./AppLayout";
import Chip from "./Chip";
import HorizontalLine from "./HorizontalLine";
import LanguageSwitcher from "./Inputs/LanguageSwitcher";
import Markdawn from "./Markdown/Markdawn";
import TOC from "./Markdown/TOC";
import ReturnButton, { ReturnButtonType } from "./PanelComponents/ReturnButton";
@ -23,9 +16,12 @@ import ToolTip from "./ToolTip";
interface Props {
post: Exclude<
GetPostQuery["posts"],
Exclude<
GetPostQuery["posts"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["data"][number]["attributes"];
>;
langui: AppStaticProps["langui"];
languages: AppStaticProps["languages"];
currencies: AppStaticProps["currencies"];
@ -56,54 +52,18 @@ export default function Post(props: Props): JSX.Element {
} = 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
)
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appLayout.preferredLanguages]);
useEffect(() => {
if (selectedTranslationIndex !== undefined)
setSelectedTranslation(post?.translations?.[selectedTranslationIndex]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTranslationIndex]);
const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({
items: post.translations,
languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code,
});
const thumbnail =
selectedTranslation?.thumbnail?.data?.attributes ??
post?.thumbnail?.data?.attributes;
post.thumbnail?.data?.attributes;
const body = selectedTranslation?.body ?? "";
const title = selectedTranslation?.title ?? prettySlug(post?.slug);
const title = selectedTranslation?.title ?? prettySlug(post.slug);
const except = selectedTranslation?.excerpt ?? "";
const subPanel =
@ -137,7 +97,7 @@ export default function Post(props: Props): JSX.Element {
</div>
)}
{post?.authors && post.authors.data.length > 0 && (
{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">
@ -183,15 +143,8 @@ export default function Post(props: Props): JSX.Element {
title={title}
description={except}
langui={langui}
categories={post?.categories}
languageSwitcher={
<LanguageSwitcher
languages={languages}
locales={translationLocales}
localesIndex={selectedTranslationIndex}
setLocalesIndex={setSelectedTranslationIndex}
/>
}
categories={post.categories}
languageSwitcher={<LanguageSwitcher />}
/>
<HorizontalLine />
@ -200,12 +153,7 @@ export default function Post(props: Props): JSX.Element {
<>
{displayLanguageSwitcher && (
<div className="grid place-content-end place-items-start">
<LanguageSwitcher
languages={languages}
locales={translationLocales}
localesIndex={selectedTranslationIndex}
setLocalesIndex={setSelectedTranslationIndex}
/>
<LanguageSwitcher />
</div>
)}
{displayTitle && (

184
src/components/PostPage.tsx Normal file
View File

@ -0,0 +1,184 @@
import { GetPostQuery } from "graphql/generated";
import useSmartLanguage from "hooks/useSmartLanguage";
import { AppStaticProps } from "queries/getAppStaticProps";
import { getStatusDescription, prettySlug } from "queries/helpers";
import AppLayout from "./AppLayout";
import Chip from "./Chip";
import HorizontalLine from "./HorizontalLine";
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 ThumbnailHeader from "./ThumbnailHeader";
import ToolTip from "./ToolTip";
export type Post = Exclude<
Exclude<
GetPostQuery["posts"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>;
interface Props {
post: Post;
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 PostPage(props: Props): JSX.Element {
const {
post,
langui,
languages,
returnHref,
returnTitle,
displayCredits,
displayToc,
displayThumbnailHeader,
displayLanguageSwitcher,
appendBody,
prependBody,
} = props;
const displayTitle = props.displayTitle ?? true;
const [selectedTranslation, LanguageSwitcher] = useSmartLanguage({
items: post.translations,
languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code,
});
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>
{returnHref && returnTitle && (
<ReturnButton
href={returnHref}
title={returnTitle}
langui={langui}
displayOn={ReturnButtonType.mobile}
horizontalLine
/>
)}
{displayThumbnailHeader ? (
<>
<ThumbnailHeader
thumbnail={thumbnail}
title={title}
description={except}
langui={langui}
categories={post.categories}
languageSwitcher={<LanguageSwitcher />}
/>
<HorizontalLine />
</>
) : (
<>
{displayLanguageSwitcher && (
<div className="grid place-content-end place-items-start">
<LanguageSwitcher />
</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

@ -0,0 +1,65 @@
import LanguageSwitcher from "components/Inputs/LanguageSwitcher";
import { useAppLayout } from "contexts/AppLayoutContext";
import { useRouter } from "next/router";
import { AppStaticProps } from "queries/getAppStaticProps";
import { getPreferredLanguage } from "queries/helpers";
import { useEffect, useMemo, useState } from "react";
interface Props<T> {
items: T[];
languages: AppStaticProps["languages"];
languageExtractor: (item: T) => string | undefined;
transform?: (item: T) => T;
}
export default function useSmartLanguage<T>(
props: Props<T>
): [T | undefined, () => JSX.Element] {
const {
items,
languageExtractor,
languages,
transform = (item) => item,
} = props;
const appLayout = useAppLayout();
const router = useRouter();
const availableLocales: Map<string, number> = useMemo(() => new Map(), []);
const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<
number | undefined
>();
const [selectedTranslation, setSelectedTranslation] = useState<T>();
useEffect(() => {
items.map((elem, index) => {
const result = languageExtractor(elem);
if (result !== undefined) availableLocales.set(result, index);
});
}, [availableLocales, items, languageExtractor]);
useEffect(() => {
setSelectedTranslationIndex(
getPreferredLanguage(
appLayout.preferredLanguages ?? [router.locale],
availableLocales
)
);
}, [appLayout.preferredLanguages, availableLocales, router.locale]);
useEffect(() => {
if (selectedTranslationIndex !== undefined)
setSelectedTranslation(transform(items[selectedTranslationIndex]));
}, [items, selectedTranslationIndex, transform]);
return [
selectedTranslation,
() => (
<LanguageSwitcher
languages={languages}
locales={availableLocales}
localesIndex={selectedTranslationIndex}
setLocalesIndex={setSelectedTranslationIndex}
/>
),
];
}

View File

@ -1,7 +1,6 @@
import AppLayout from "components/AppLayout";
import Chip from "components/Chip";
import HorizontalLine from "components/HorizontalLine";
import LanguageSwitcher from "components/Inputs/LanguageSwitcher";
import Markdawn from "components/Markdown/Markdawn";
import TOC from "components/Markdown/TOC";
import ReturnButton, {
@ -13,25 +12,22 @@ import PreviewLine from "components/PreviewLine";
import RecorderChip from "components/RecorderChip";
import ThumbnailHeader from "components/ThumbnailHeader";
import ToolTip from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetContentTextQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { useMediaMobile } from "hooks/useMediaQuery";
import useSmartLanguage from "hooks/useSmartLanguage";
import {
GetStaticPathsContext,
GetStaticPathsResult,
GetStaticPropsContext,
} from "next";
import { useRouter } from "next/router";
import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
import {
getPreferredLanguage,
getStatusDescription,
prettyinlineTitle,
prettyLanguage,
prettySlug,
} from "queries/helpers";
import { useEffect, useMemo, useState } from "react";
interface Props extends AppStaticProps {
content: Exclude<
@ -46,53 +42,15 @@ interface Props extends AppStaticProps {
export default function Content(props: Props): JSX.Element {
const { langui, content, languages } = props;
const router = useRouter();
const appLayout = useAppLayout();
const isMobile = useMediaMobile();
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 [selectedTextSet, LanguageSwitcher] = useSmartLanguage({
items: content?.text_set,
languages: languages,
languageExtractor: (item) => item?.language?.data?.attributes?.code,
});
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
)
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appLayout.preferredLanguages]);
useEffect(() => {
if (selectedTextSetIndex !== undefined) {
setSelectedTextSet(content?.text_set?.[selectedTextSetIndex]);
setSelectedTitle(content?.titles?.[selectedTextSetIndex]);
}
}, [content?.text_set, content?.titles, selectedTextSetIndex]);
const selectedTitle = content?.titles?.[0];
const subPanel = (
<SubPanel>
@ -252,16 +210,7 @@ export default function Content(props: Props): JSX.Element {
type={content.type}
categories={content.categories}
langui={langui}
languageSwitcher={
selectedTextSet ? (
<LanguageSwitcher
locales={textSetLocales}
languages={props.languages}
localesIndex={selectedTextSetIndex}
setLocalesIndex={setSelectedTextSetIndex}
/>
) : undefined
}
languageSwitcher={<LanguageSwitcher />}
/>
{content.previous_recommended?.data?.attributes && (

View File

@ -10,9 +10,12 @@ import { AppStaticProps, getAppStaticProps } from "queries/getAppStaticProps";
interface Props extends AppStaticProps {
post: Exclude<
GetPostQuery["posts"],
Exclude<
GetPostQuery["posts"],
null | undefined
>["data"][number]["attributes"],
null | undefined
>["data"][number]["attributes"];
>;
postId: Exclude<
GetPostQuery["posts"],
null | undefined
@ -45,7 +48,7 @@ export async function getStaticProps(
slug: slug,
language_code: context.locale ?? "en",
});
if (!post.posts?.data[0]) return { notFound: true };
if (!post.posts?.data[0].attributes) return { notFound: true };
const props: Props = {
...(await getAppStaticProps(context)),
post: post.posts.data[0].attributes,