566 lines
19 KiB
TypeScript
566 lines
19 KiB
TypeScript
import { Button } from "components/Inputs/Button";
|
||
import { useAppLayout } from "contexts/AppLayoutContext";
|
||
import { UploadImageFragment } from "graphql/generated";
|
||
import { AppStaticProps } from "graphql/getAppStaticProps";
|
||
import { cIf, cJoin } from "helpers/className";
|
||
import { prettyLanguage, prettySlug } from "helpers/formatters";
|
||
import { getOgImage, ImageQuality } from "helpers/img";
|
||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||
// import { getClient, Indexes, search, SearchResult } from "helpers/search";
|
||
import { Immutable } from "helpers/types";
|
||
import { useMediaMobile } from "hooks/useMediaQuery";
|
||
import { AnchorIds } from "hooks/useScrollTopOnChange";
|
||
import Head from "next/head";
|
||
import { useRouter } from "next/router";
|
||
import { useEffect, useLayoutEffect, useMemo, useState } from "react";
|
||
import { useSwipeable } from "react-swipeable";
|
||
import { Ico, Icon } from "./Ico";
|
||
import { ButtonGroup } from "./Inputs/ButtonGroup";
|
||
import { OrderableList } from "./Inputs/OrderableList";
|
||
import { Select } from "./Inputs/Select";
|
||
import { TextInput } from "./Inputs/TextInput";
|
||
import { MainPanel } from "./Panels/MainPanel";
|
||
import { Popup } from "./Popup";
|
||
|
||
interface Props extends AppStaticProps {
|
||
subPanel?: React.ReactNode;
|
||
subPanelIcon?: Icon;
|
||
contentPanel?: React.ReactNode;
|
||
title?: string;
|
||
navTitle: string | null | undefined;
|
||
thumbnail?: UploadImageFragment;
|
||
description?: string;
|
||
}
|
||
|
||
const SENSIBILITY_SWIPE = 1.1;
|
||
const TITLE_PREFIX = "Accord’s Library";
|
||
|
||
export function AppLayout(props: Immutable<Props>): JSX.Element {
|
||
const {
|
||
langui,
|
||
currencies,
|
||
languages,
|
||
subPanel,
|
||
contentPanel,
|
||
thumbnail,
|
||
title,
|
||
navTitle,
|
||
description,
|
||
subPanelIcon = Icon.Tune,
|
||
} = props;
|
||
|
||
const {
|
||
configPanelOpen,
|
||
currency,
|
||
darkMode,
|
||
dyslexic,
|
||
fontSize,
|
||
mainPanelOpen,
|
||
mainPanelReduced,
|
||
menuGestures,
|
||
playerName,
|
||
preferredLanguages,
|
||
selectedThemeMode,
|
||
subPanelOpen,
|
||
setConfigPanelOpen,
|
||
setCurrency,
|
||
setDarkMode,
|
||
setDyslexic,
|
||
setFontSize,
|
||
setMainPanelOpen,
|
||
setPlayerName,
|
||
setPreferredLanguages,
|
||
setSelectedThemeMode,
|
||
setSubPanelOpen,
|
||
toggleMainPanelOpen,
|
||
toggleSubPanelOpen,
|
||
} = useAppLayout();
|
||
|
||
const router = useRouter();
|
||
const isMobile = useMediaMobile();
|
||
|
||
useEffect(() => {
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
router.events?.on("routeChangeStart", () => {
|
||
setConfigPanelOpen(false);
|
||
setMainPanelOpen(false);
|
||
setSubPanelOpen(false);
|
||
});
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
router.events?.on("hashChangeStart", () => {
|
||
setSubPanelOpen(false);
|
||
});
|
||
}, [router.events, setConfigPanelOpen, setMainPanelOpen, setSubPanelOpen]);
|
||
|
||
const handlers = useSwipeable({
|
||
onSwipedLeft: (SwipeEventData) => {
|
||
if (menuGestures) {
|
||
if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
|
||
if (mainPanelOpen === true) {
|
||
setMainPanelOpen(false);
|
||
} else if (subPanel === true && contentPanel === true) {
|
||
setSubPanelOpen(true);
|
||
}
|
||
}
|
||
},
|
||
onSwipedRight: (SwipeEventData) => {
|
||
if (menuGestures) {
|
||
if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
|
||
if (subPanelOpen === true) {
|
||
setSubPanelOpen(false);
|
||
} else {
|
||
setMainPanelOpen(true);
|
||
}
|
||
}
|
||
},
|
||
});
|
||
|
||
const turnSubIntoContent = useMemo(
|
||
() => isDefined(subPanel) && isDefined(contentPanel),
|
||
[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
|
||
}%`;
|
||
}, [fontSize]);
|
||
|
||
const defaultPreferredLanguages = useMemo(() => {
|
||
let list: string[] = [];
|
||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||
if (router.locale === "en") {
|
||
list = [router.locale];
|
||
router.locales.map((locale) => {
|
||
if (locale !== router.locale) list.push(locale);
|
||
});
|
||
} else {
|
||
list = [router.locale, "en"];
|
||
router.locales.map((locale) => {
|
||
if (locale !== router.locale && locale !== "en") list.push(locale);
|
||
});
|
||
}
|
||
}
|
||
return list;
|
||
}, [router.locale, router.locales]);
|
||
|
||
const currencyOptions = useMemo(() => {
|
||
const list: string[] = [];
|
||
currencies.map((currentCurrency) => {
|
||
if (
|
||
currentCurrency.attributes &&
|
||
isDefinedAndNotEmpty(currentCurrency.attributes.code)
|
||
)
|
||
list.push(currentCurrency.attributes.code);
|
||
});
|
||
return list;
|
||
}, [currencies]);
|
||
|
||
const [currencySelect, setCurrencySelect] = useState<number>(-1);
|
||
|
||
useEffect(() => {
|
||
if (isDefined(currency))
|
||
setCurrencySelect(currencyOptions.indexOf(currency));
|
||
}, [currency, currencyOptions]);
|
||
|
||
useEffect(() => {
|
||
if (currencySelect >= 0) setCurrency(currencyOptions[currencySelect]);
|
||
}, [currencyOptions, currencySelect, setCurrency]);
|
||
|
||
const gridCol = useMemo(() => {
|
||
if (isDefined(subPanel)) {
|
||
if (mainPanelReduced === true) {
|
||
return "grid-cols-[6rem_20rem_1fr]";
|
||
}
|
||
return "grid-cols-[20rem_20rem_1fr]";
|
||
} else if (mainPanelReduced === true) {
|
||
return "grid-cols-[6rem_0px_1fr]";
|
||
}
|
||
return "grid-cols-[20rem_0px_1fr]";
|
||
}, [mainPanelReduced, subPanel]);
|
||
|
||
return (
|
||
<div
|
||
className={cJoin(
|
||
cIf(darkMode, "set-theme-dark", "set-theme-light"),
|
||
cIf(dyslexic, "set-theme-font-dyslexic", "set-theme-font-standard")
|
||
)}
|
||
>
|
||
<div
|
||
{...handlers}
|
||
className={cJoin(
|
||
`fixed inset-0 m-0 grid touch-pan-y bg-light p-0 text-black
|
||
[grid-template-areas:'main_sub_content'] mobile:grid-cols-[1fr]
|
||
mobile:grid-rows-[1fr_5rem] mobile:[grid-template-areas:'content''navbar']`,
|
||
gridCol
|
||
)}
|
||
>
|
||
<Head>
|
||
<title>{metaTitle}</title>
|
||
<meta name="description" content={metaDescription} />
|
||
|
||
<meta name="twitter:title" content={metaTitle}></meta>
|
||
<meta name="twitter:description" content={metaDescription}></meta>
|
||
<meta name="twitter:card" content="summary_large_image"></meta>
|
||
<meta name="twitter:image" content={metaImage.image}></meta>
|
||
|
||
<meta property="og:title" content={metaTitle} />
|
||
<meta property="og:description" content={metaDescription} />
|
||
<meta property="og:image" content={metaImage.image}></meta>
|
||
<meta property="og:image:secure_url" content={metaImage.image}></meta>
|
||
<meta
|
||
property="og:image:width"
|
||
content={metaImage.width.toString()}
|
||
></meta>
|
||
<meta
|
||
property="og:image:height"
|
||
content={metaImage.height.toString()}
|
||
></meta>
|
||
<meta property="og:image:alt" content={metaImage.alt}></meta>
|
||
<meta property="og:image:type" content="image/jpeg"></meta>
|
||
</Head>
|
||
|
||
{/* Background when navbar is opened */}
|
||
<div
|
||
className={cJoin(
|
||
`absolute inset-0 transition-[backdrop-filter] duration-500 [grid-area:content]
|
||
mobile:z-10`,
|
||
cIf(
|
||
(mainPanelOpen === true || subPanelOpen === true) && isMobile,
|
||
"[backdrop-filter:blur(2px)]",
|
||
"pointer-events-none touch-none"
|
||
)
|
||
)}
|
||
>
|
||
<div
|
||
className={cJoin(
|
||
"absolute inset-0 bg-shade transition-opacity duration-500",
|
||
cIf(
|
||
(mainPanelOpen === true || subPanelOpen === true) && isMobile,
|
||
"opacity-60",
|
||
"opacity-0"
|
||
)
|
||
)}
|
||
onClick={() => {
|
||
setMainPanelOpen(false);
|
||
setSubPanelOpen(false);
|
||
}}
|
||
></div>
|
||
</div>
|
||
|
||
{/* Content panel */}
|
||
<div
|
||
id={AnchorIds.ContentPanel}
|
||
className={`texture-paper-dots overflow-y-scroll bg-light [grid-area:content]`}
|
||
>
|
||
{isDefined(contentPanel) ? (
|
||
contentPanel
|
||
) : (
|
||
<div className="grid h-full place-content-center">
|
||
<div
|
||
className="grid grid-flow-col place-items-center gap-9 rounded-2xl
|
||
border-2 border-dotted border-dark p-8 text-dark opacity-40"
|
||
>
|
||
<p className="text-4xl">❮</p>
|
||
<p className="w-64 text-2xl">{langui.select_option_sidebar}</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Sub panel */}
|
||
{isDefined(subPanel) && (
|
||
<div
|
||
className={cJoin(
|
||
`texture-paper-dots overflow-y-scroll border-r-[1px] border-dotted
|
||
border-black bg-light transition-transform duration-300 [grid-area:sub]
|
||
[scrollbar-width:none] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%]
|
||
mobile:justify-self-end mobile:border-r-0 mobile:border-l-[1px]
|
||
mobile:[grid-area:content]`,
|
||
turnSubIntoContent
|
||
? "mobile:w-full mobile:border-l-0"
|
||
: subPanelOpen === true
|
||
? ""
|
||
: "mobile:translate-x-[100vw]"
|
||
)}
|
||
>
|
||
{subPanel}
|
||
</div>
|
||
)}
|
||
|
||
{/* Main panel */}
|
||
<div
|
||
className={cJoin(
|
||
`texture-paper-dots overflow-y-scroll border-r-[1px] border-dotted
|
||
border-black bg-light transition-transform duration-300 [grid-area:main]
|
||
[scrollbar-width:none] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%]
|
||
mobile:justify-self-start mobile:[grid-area:content]`,
|
||
cIf(mainPanelOpen === false, "mobile:-translate-x-full")
|
||
)}
|
||
>
|
||
<MainPanel langui={langui} />
|
||
</div>
|
||
|
||
{/* Navbar */}
|
||
<div
|
||
className="texture-paper-dots grid grid-cols-[5rem_1fr_5rem] place-items-center
|
||
border-t-[1px] border-dotted border-black bg-light [grid-area:navbar] desktop:hidden"
|
||
>
|
||
<Ico
|
||
icon={mainPanelOpen === true ? Icon.Close : Icon.Menu}
|
||
className="mt-[.1em] cursor-pointer !text-2xl"
|
||
onClick={() => {
|
||
toggleMainPanelOpen();
|
||
setSubPanelOpen(false);
|
||
}}
|
||
/>
|
||
<p
|
||
className={cJoin(
|
||
"overflow-hidden text-center font-headers font-black",
|
||
cIf(
|
||
ogTitle && ogTitle.length > 30,
|
||
"max-h-14 text-xl",
|
||
"max-h-16 text-2xl"
|
||
)
|
||
)}
|
||
>
|
||
{ogTitle}
|
||
</p>
|
||
{isDefined(subPanel) && !turnSubIntoContent && (
|
||
<Ico
|
||
icon={subPanelOpen === true ? Icon.Close : subPanelIcon}
|
||
className="mt-[.1em] cursor-pointer !text-2xl"
|
||
onClick={() => {
|
||
toggleSubPanelOpen();
|
||
setMainPanelOpen(false);
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<Popup state={configPanelOpen ?? false} setState={setConfigPanelOpen}>
|
||
<h2 className="text-2xl">{langui.settings}</h2>
|
||
|
||
<div
|
||
className="mt-4 grid justify-items-center gap-16
|
||
text-center desktop:grid-cols-[auto_auto]"
|
||
>
|
||
{router.locales && (
|
||
<div>
|
||
<h3 className="text-xl">{langui.languages}</h3>
|
||
{preferredLanguages && (
|
||
<OrderableList
|
||
items={
|
||
preferredLanguages.length > 0
|
||
? new Map(
|
||
preferredLanguages.map((locale) => [
|
||
locale,
|
||
prettyLanguage(locale, languages),
|
||
])
|
||
)
|
||
: new Map(
|
||
defaultPreferredLanguages.map((locale) => [
|
||
locale,
|
||
prettyLanguage(locale, languages),
|
||
])
|
||
)
|
||
}
|
||
insertLabels={
|
||
new Map([
|
||
[0, langui.primary_language],
|
||
[1, langui.secondary_language],
|
||
])
|
||
}
|
||
onChange={(items) => {
|
||
const newPreferredLanguages = [...items].map(
|
||
([code]) => code
|
||
);
|
||
setPreferredLanguages(newPreferredLanguages);
|
||
if (router.locale !== newPreferredLanguages[0]) {
|
||
router.push(router.asPath, router.asPath, {
|
||
locale: newPreferredLanguages[0],
|
||
});
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
<div className="grid place-items-center gap-8 text-center desktop:grid-cols-2">
|
||
<div>
|
||
<h3 className="text-xl">{langui.theme}</h3>
|
||
<ButtonGroup>
|
||
<Button
|
||
onClick={() => {
|
||
setDarkMode(false);
|
||
setSelectedThemeMode(true);
|
||
}}
|
||
active={selectedThemeMode === true && darkMode === false}
|
||
text={langui.light}
|
||
/>
|
||
<Button
|
||
onClick={() => {
|
||
setSelectedThemeMode(false);
|
||
}}
|
||
active={selectedThemeMode === false}
|
||
text={langui.auto}
|
||
/>
|
||
<Button
|
||
onClick={() => {
|
||
setDarkMode(true);
|
||
setSelectedThemeMode(true);
|
||
}}
|
||
active={selectedThemeMode === true && darkMode === true}
|
||
text={langui.dark}
|
||
/>
|
||
</ButtonGroup>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className="text-xl">{langui.currency}</h3>
|
||
<div>
|
||
<Select
|
||
options={currencyOptions}
|
||
state={currencySelect}
|
||
setState={setCurrencySelect}
|
||
className="w-28"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className="text-xl">{langui.font_size}</h3>
|
||
<ButtonGroup>
|
||
<Button
|
||
onClick={() => setFontSize((fontSize ?? 1) / 1.05)}
|
||
icon={Icon.TextDecrease}
|
||
/>
|
||
<Button
|
||
onClick={() => setFontSize(1)}
|
||
text={`${((fontSize ?? 1) * 100).toLocaleString(undefined, {
|
||
maximumFractionDigits: 0,
|
||
})}%`}
|
||
/>
|
||
<Button
|
||
onClick={() => setFontSize((fontSize ?? 1) * 1.05)}
|
||
icon={Icon.TextIncrease}
|
||
/>
|
||
</ButtonGroup>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className="text-xl">{langui.font}</h3>
|
||
<div className="grid gap-2">
|
||
<Button
|
||
active={dyslexic === false}
|
||
onClick={() => setDyslexic(false)}
|
||
className="font-zenMaruGothic"
|
||
text="Zen Maru Gothic"
|
||
/>
|
||
<Button
|
||
active={dyslexic === true}
|
||
onClick={() => setDyslexic(true)}
|
||
className="font-openDyslexic"
|
||
text="OpenDyslexic"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className="text-xl">{langui.player_name}</h3>
|
||
<TextInput
|
||
placeholder="<player>"
|
||
className="w-48"
|
||
state={playerName}
|
||
setState={setPlayerName}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Popup>
|
||
|
||
{/* <Popup
|
||
state={searchPanelOpen}
|
||
setState={setSearchPanelOpen}
|
||
>
|
||
<div className="grid place-items-center gap-2">
|
||
TODO: add to langui
|
||
<h2 className="text-2xl">{"Search"}</h2>
|
||
<TextInput
|
||
className="mb-6 w-full"
|
||
placeholder={"Search query..."}
|
||
state={searchQuery}
|
||
setState={setSearchQuery}
|
||
/>
|
||
</div>
|
||
TODO: add to langui
|
||
<div className="grid gap-4">
|
||
<p className="font-headers text-xl">In news:</p>
|
||
<div
|
||
className="grid grid-cols-2 items-end gap-8
|
||
desktop:grid-cols-[repeat(auto-fill,_minmax(15rem,1fr))] mobile:gap-4"
|
||
>
|
||
{searchResult?.hits.map((hit) => (
|
||
<PreviewCard
|
||
key={hit.id}
|
||
href={hit.href}
|
||
title={hit.title}
|
||
thumbnailAspectRatio={"3/2"}
|
||
thumbnail={hit.thumbnail}
|
||
keepInfoVisible
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Popup> */}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/*
|
||
* const [searchQuery, setSearchQuery] = useState("");
|
||
* const [searchResult, setSearchResult] = useState<SearchResult>();
|
||
*/
|
||
|
||
/*
|
||
* const client = getClient();
|
||
* useEffect(() => {
|
||
* if (searchQuery.length > 1) {
|
||
* search(client, Indexes.Post, searchQuery).then((result) => {
|
||
* setSearchResult(result);
|
||
* });
|
||
* } else {
|
||
* setSearchResult(undefined);
|
||
* }
|
||
* // eslint-disable-next-line react-hooks/exhaustive-deps
|
||
* }, [searchQuery]);
|
||
*/
|