537 lines
19 KiB
TypeScript
537 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 { prettyLanguage, prettySlug } from "helpers/formatters";
|
||
import { getOgImage, ImageQuality, OgImage } from "helpers/img";
|
||
// 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, useMemo, useState } from "react";
|
||
import { useSwipeable } from "react-swipeable";
|
||
import { Ico, Icon } from "./Ico";
|
||
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;
|
||
}
|
||
|
||
export function AppLayout(props: Immutable<Props>): JSX.Element {
|
||
const {
|
||
langui,
|
||
currencies,
|
||
languages,
|
||
subPanel,
|
||
contentPanel,
|
||
thumbnail,
|
||
title,
|
||
navTitle,
|
||
description,
|
||
subPanelIcon = Icon.Tune,
|
||
} = props;
|
||
const router = useRouter();
|
||
const isMobile = useMediaMobile();
|
||
const appLayout = useAppLayout();
|
||
|
||
/*
|
||
* const [searchQuery, setSearchQuery] = useState("");
|
||
* const [searchResult, setSearchResult] = useState<SearchResult>();
|
||
*/
|
||
|
||
const sensibilitySwipe = 1.1;
|
||
|
||
useMemo(() => {
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
router.events?.on("routeChangeStart", () => {
|
||
appLayout.setConfigPanelOpen(false);
|
||
appLayout.setMainPanelOpen(false);
|
||
appLayout.setSubPanelOpen(false);
|
||
});
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
router.events?.on("hashChangeStart", () => {
|
||
appLayout.setSubPanelOpen(false);
|
||
});
|
||
}, [appLayout, router.events]);
|
||
|
||
const handlers = useSwipeable({
|
||
onSwipedLeft: (SwipeEventData) => {
|
||
if (appLayout.menuGestures) {
|
||
if (SwipeEventData.velocity < sensibilitySwipe) return;
|
||
if (appLayout.mainPanelOpen) {
|
||
appLayout.setMainPanelOpen(false);
|
||
} else if (subPanel && contentPanel) {
|
||
appLayout.setSubPanelOpen(true);
|
||
}
|
||
}
|
||
},
|
||
onSwipedRight: (SwipeEventData) => {
|
||
if (appLayout.menuGestures) {
|
||
if (SwipeEventData.velocity < sensibilitySwipe) return;
|
||
if (appLayout.subPanelOpen) {
|
||
appLayout.setSubPanelOpen(false);
|
||
} else {
|
||
appLayout.setMainPanelOpen(true);
|
||
}
|
||
}
|
||
},
|
||
});
|
||
|
||
/*
|
||
* 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]);
|
||
*/
|
||
|
||
const turnSubIntoContent = subPanel && !contentPanel;
|
||
|
||
const titlePrefix = "Accord’s Library";
|
||
const metaImage: OgImage = thumbnail
|
||
? getOgImage(ImageQuality.Og, thumbnail)
|
||
: {
|
||
image: "/default_og.jpg",
|
||
width: 1200,
|
||
height: 630,
|
||
alt: "Accord's Library Logo",
|
||
};
|
||
const ogTitle =
|
||
title ?? navTitle ?? prettySlug(router.asPath.split("/").pop());
|
||
|
||
const metaTitle = `${titlePrefix} - ${ogTitle}`;
|
||
|
||
const metaDescription = description ?? langui.default_description ?? "";
|
||
|
||
useEffect(() => {
|
||
document.getElementsByTagName("html")[0].style.fontSize = `${
|
||
(appLayout.fontSize ?? 1) * 100
|
||
}%`;
|
||
}, [appLayout.fontSize]);
|
||
|
||
const currencyOptions: string[] = [];
|
||
currencies.map((currency) => {
|
||
if (currency.attributes?.code)
|
||
currencyOptions.push(currency.attributes.code);
|
||
});
|
||
const [currencySelect, setCurrencySelect] = useState<number>(-1);
|
||
|
||
let defaultPreferredLanguages: string[] = [];
|
||
|
||
if (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);
|
||
});
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
if (appLayout.currency)
|
||
setCurrencySelect(currencyOptions.indexOf(appLayout.currency));
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [appLayout.currency]);
|
||
|
||
useEffect(() => {
|
||
if (currencySelect >= 0)
|
||
appLayout.setCurrency(currencyOptions[currencySelect]);
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [currencySelect]);
|
||
|
||
let gridCol = "";
|
||
if (subPanel) {
|
||
if (appLayout.mainPanelReduced) {
|
||
gridCol = "grid-cols-[6rem_20rem_1fr]";
|
||
} else {
|
||
gridCol = "grid-cols-[20rem_20rem_1fr]";
|
||
}
|
||
} else if (appLayout.mainPanelReduced) {
|
||
gridCol = "grid-cols-[6rem_0px_1fr]";
|
||
} else {
|
||
gridCol = "grid-cols-[20rem_0px_1fr]";
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={`${
|
||
appLayout.darkMode ? "set-theme-dark" : "set-theme-light"
|
||
} ${
|
||
appLayout.dyslexic
|
||
? "set-theme-font-dyslexic"
|
||
: "set-theme-font-standard"
|
||
}`}
|
||
>
|
||
<div
|
||
{...handlers}
|
||
className={`fixed inset-0 m-0 grid touch-pan-y bg-light p-0 text-black
|
||
[grid-template-areas:'main_sub_content'] ${gridCol} mobile:grid-cols-[1fr]
|
||
mobile:grid-rows-[1fr_5rem] mobile:[grid-template-areas:'content''navbar']`}
|
||
>
|
||
<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={`absolute inset-0 transition-[backdrop-filter]
|
||
duration-500 [grid-area:content] mobile:z-10 ${
|
||
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile
|
||
? "[backdrop-filter:blur(2px)]"
|
||
: "pointer-events-none touch-none "
|
||
}`}
|
||
>
|
||
<div
|
||
className={`absolute inset-0 bg-shade transition-opacity duration-500
|
||
${turnSubIntoContent ? "" : ""}
|
||
${
|
||
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile
|
||
? "opacity-60"
|
||
: "opacity-0"
|
||
}`}
|
||
onClick={() => {
|
||
appLayout.setMainPanelOpen(false);
|
||
appLayout.setSubPanelOpen(false);
|
||
}}
|
||
></div>
|
||
</div>
|
||
|
||
{/* Content panel */}
|
||
<div
|
||
id={AnchorIds.ContentPanel}
|
||
className={`texture-paper-dots overflow-y-scroll bg-light [grid-area:content]`}
|
||
>
|
||
{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 */}
|
||
{subPanel && (
|
||
<div
|
||
className={`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"
|
||
: !appLayout.subPanelOpen && "mobile:translate-x-[100vw]"
|
||
}`}
|
||
>
|
||
{subPanel}
|
||
</div>
|
||
)}
|
||
|
||
{/* Main panel */}
|
||
<div
|
||
className={`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] ${
|
||
appLayout.mainPanelOpen ? "" : "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={appLayout.mainPanelOpen ? Icon.Close : Icon.Menu}
|
||
className="mt-[.1em] cursor-pointer"
|
||
onClick={() => {
|
||
appLayout.setMainPanelOpen(!appLayout.mainPanelOpen);
|
||
appLayout.setSubPanelOpen(false);
|
||
}}
|
||
/>
|
||
<p
|
||
className={`overflow-hidden text-center font-headers font-black ${
|
||
ogTitle && ogTitle.length > 30
|
||
? "max-h-14 text-xl"
|
||
: "max-h-16 text-2xl"
|
||
}`}
|
||
>
|
||
{ogTitle}
|
||
</p>
|
||
{subPanel && !turnSubIntoContent && (
|
||
<Ico
|
||
icon={appLayout.subPanelOpen ? Icon.Close : subPanelIcon}
|
||
className="mt-[.1em] cursor-pointer"
|
||
onClick={() => {
|
||
appLayout.setSubPanelOpen(!appLayout.subPanelOpen);
|
||
appLayout.setMainPanelOpen(false);
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
<Popup
|
||
state={appLayout.configPanelOpen}
|
||
setState={appLayout.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>
|
||
{appLayout.preferredLanguages && (
|
||
<OrderableList
|
||
items={
|
||
appLayout.preferredLanguages.length > 0
|
||
? new Map(
|
||
appLayout.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 preferredLanguages = [...items].map(
|
||
([code]) => code
|
||
);
|
||
appLayout.setPreferredLanguages(preferredLanguages);
|
||
if (router.locale !== preferredLanguages[0]) {
|
||
router.push(router.asPath, router.asPath, {
|
||
locale: preferredLanguages[0],
|
||
});
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
)}
|
||
<div className="grid place-items-center gap-8 text-center desktop:grid-cols-2">
|
||
<div>
|
||
<h3 className="text-xl">{langui.theme}</h3>
|
||
<div className="flex flex-row">
|
||
<Button
|
||
onClick={() => {
|
||
appLayout.setDarkMode(false);
|
||
appLayout.setSelectedThemeMode(true);
|
||
}}
|
||
active={
|
||
appLayout.selectedThemeMode === true &&
|
||
appLayout.darkMode === false
|
||
}
|
||
className="rounded-r-none"
|
||
text={langui.light}
|
||
/>
|
||
<Button
|
||
onClick={() => {
|
||
appLayout.setSelectedThemeMode(false);
|
||
}}
|
||
active={appLayout.selectedThemeMode === false}
|
||
className="rounded-l-none rounded-r-none border-x-0"
|
||
text={langui.auto}
|
||
/>
|
||
<Button
|
||
onClick={() => {
|
||
appLayout.setDarkMode(true);
|
||
appLayout.setSelectedThemeMode(true);
|
||
}}
|
||
active={
|
||
appLayout.selectedThemeMode === true &&
|
||
appLayout.darkMode === true
|
||
}
|
||
className="rounded-l-none"
|
||
text={langui.dark}
|
||
/>
|
||
</div>
|
||
</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>
|
||
<div className="flex flex-row">
|
||
<Button
|
||
className="rounded-r-none"
|
||
onClick={() =>
|
||
appLayout.setFontSize(
|
||
appLayout.fontSize
|
||
? appLayout.fontSize / 1.05
|
||
: 1 / 1.05
|
||
)
|
||
}
|
||
icon={Icon.TextDecrease}
|
||
/>
|
||
<Button
|
||
className="rounded-l-none rounded-r-none border-x-0"
|
||
onClick={() => appLayout.setFontSize(1)}
|
||
text={`${((appLayout.fontSize ?? 1) * 100).toLocaleString(
|
||
undefined,
|
||
{
|
||
maximumFractionDigits: 0,
|
||
}
|
||
)}%`}
|
||
/>
|
||
<Button
|
||
className="rounded-l-none"
|
||
onClick={() =>
|
||
appLayout.setFontSize(
|
||
appLayout.fontSize
|
||
? appLayout.fontSize * 1.05
|
||
: 1 * 1.05
|
||
)
|
||
}
|
||
icon={Icon.TextIncrease}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h3 className="text-xl">{langui.font}</h3>
|
||
<div className="grid gap-2">
|
||
<Button
|
||
active={appLayout.dyslexic === false}
|
||
onClick={() => appLayout.setDyslexic(false)}
|
||
className="font-zenMaruGothic"
|
||
text="Zen Maru Gothic"
|
||
/>
|
||
<Button
|
||
active={appLayout.dyslexic === true}
|
||
onClick={() => appLayout.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={appLayout.playerName}
|
||
setState={appLayout.setPlayerName}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Popup>
|
||
|
||
{/* <Popup
|
||
state={appLayout.searchPanelOpen}
|
||
setState={appLayout.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>
|
||
);
|
||
}
|