diff --git a/.eslintrc.js b/.eslintrc.js index b3d7308..bfa1395 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -60,7 +60,7 @@ module.exports = { "no-alert": "warn", "no-bitwise": "warn", "no-caller": "warn", - "no-confusing-arrow": "warn", + // "no-confusing-arrow": "warn", "no-continue": "warn", "no-else-return": "warn", "no-eq-null": "warn", @@ -190,7 +190,10 @@ module.exports = { "@typescript-eslint/promise-function-async": "error", "@typescript-eslint/require-array-sort-compare": "error", "@typescript-eslint/sort-type-union-intersection-members": "warn", - // "@typescript-eslint/strict-boolean-expressions": "error", + "@typescript-eslint/strict-boolean-expressions": [ + "error", + { allowAny: true }, + ], "@typescript-eslint/switch-exhaustiveness-check": "error", "@typescript-eslint/typedef": "error", "@typescript-eslint/unified-signatures": "error", diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 7c70c2a..02bf6b2 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -4,14 +4,15 @@ 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, OgImage } from "helpers/img"; +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, useMemo, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useState } from "react"; import { useSwipeable } from "react-swipeable"; import { Ico, Icon } from "./Ico"; import { ButtonGroup } from "./Inputs/ButtonGroup"; @@ -31,6 +32,9 @@ interface Props extends AppStaticProps { description?: string; } +const SENSIBILITY_SWIPE = 1.1; +const TITLE_PREFIX = "Accord’s Library"; + export function AppLayout(props: Immutable): JSX.Element { const { langui, @@ -44,151 +48,169 @@ export function AppLayout(props: Immutable): JSX.Element { 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(); - const appLayout = useAppLayout(); - /* - * const [searchQuery, setSearchQuery] = useState(""); - * const [searchResult, setSearchResult] = useState(); - */ - - const sensibilitySwipe = 1.1; - - useMemo(() => { + useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition router.events?.on("routeChangeStart", () => { - appLayout.setConfigPanelOpen(false); - appLayout.setMainPanelOpen(false); - appLayout.setSubPanelOpen(false); + setConfigPanelOpen(false); + setMainPanelOpen(false); + setSubPanelOpen(false); }); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition router.events?.on("hashChangeStart", () => { - appLayout.setSubPanelOpen(false); + setSubPanelOpen(false); }); - }, [appLayout, router.events]); + }, [router.events, setConfigPanelOpen, setMainPanelOpen, setSubPanelOpen]); 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); + if (menuGestures) { + if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return; + if (mainPanelOpen === true) { + setMainPanelOpen(false); + } else if (subPanel === true && contentPanel === true) { + setSubPanelOpen(true); } } }, onSwipedRight: (SwipeEventData) => { - if (appLayout.menuGestures) { - if (SwipeEventData.velocity < sensibilitySwipe) return; - if (appLayout.subPanelOpen) { - appLayout.setSubPanelOpen(false); + if (menuGestures) { + if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return; + if (subPanelOpen === true) { + setSubPanelOpen(false); } else { - appLayout.setMainPanelOpen(true); + 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 = useMemo( + () => isDefined(subPanel) && isDefined(contentPanel), + [contentPanel, subPanel] + ); - const turnSubIntoContent = subPanel && !contentPanel; + const metaImage = useMemo( + () => + thumbnail + ? getOgImage(ImageQuality.Og, thumbnail) + : { + image: "/default_og.jpg", + width: 1200, + height: 630, + alt: "Accord's Library Logo", + }, + [thumbnail] + ); - 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 { ogTitle, metaTitle } = useMemo(() => { + const resultTitle = + title ?? navTitle ?? prettySlug(router.asPath.split("/").pop()); + return { + ogTitle: resultTitle, + metaTitle: `${TITLE_PREFIX} - ${resultTitle}`, + }; + }, [navTitle, router.asPath, title]); - const metaTitle = `${titlePrefix} - ${ogTitle}`; + const metaDescription = useMemo( + () => description ?? langui.default_description ?? "", + [description, langui.default_description] + ); - const metaDescription = description - ? description - : langui.default_description ?? ""; - - useEffect(() => { + useLayoutEffect(() => { document.getElementsByTagName("html")[0].style.fontSize = `${ - (appLayout.fontSize ?? 1) * 100 + (fontSize ?? 1) * 100 }%`; - }, [appLayout.fontSize]); + }, [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 currencyOptions: string[] = []; - currencies.map((currency) => { - if (currency.attributes?.code) - currencyOptions.push(currency.attributes.code); - }); const [currencySelect, setCurrencySelect] = useState(-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 (isDefined(currency)) + setCurrencySelect(currencyOptions.indexOf(currency)); + }, [currency, currencyOptions]); useEffect(() => { - if (appLayout.currency) - setCurrencySelect(currencyOptions.indexOf(appLayout.currency)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [appLayout.currency]); + if (currencySelect >= 0) setCurrency(currencyOptions[currencySelect]); + }, [currencyOptions, currencySelect, setCurrency]); - 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]"; + 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]"; } - } else if (appLayout.mainPanelReduced) { - gridCol = "grid-cols-[6rem_0px_1fr]"; - } else { - gridCol = "grid-cols-[20rem_0px_1fr]"; - } + return "grid-cols-[20rem_0px_1fr]"; + }, [mainPanelReduced, subPanel]); return (
): JSX.Element { `absolute inset-0 transition-[backdrop-filter] duration-500 [grid-area:content] mobile:z-10`, cIf( - (appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile, + (mainPanelOpen === true || subPanelOpen === true) && isMobile, "[backdrop-filter:blur(2px)]", "pointer-events-none touch-none" ) @@ -241,14 +263,14 @@ export function AppLayout(props: Immutable): JSX.Element { className={cJoin( "absolute inset-0 bg-shade transition-opacity duration-500", cIf( - (appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile, + (mainPanelOpen === true || subPanelOpen === true) && isMobile, "opacity-60", "opacity-0" ) )} onClick={() => { - appLayout.setMainPanelOpen(false); - appLayout.setSubPanelOpen(false); + setMainPanelOpen(false); + setSubPanelOpen(false); }} >
@@ -258,7 +280,7 @@ export function AppLayout(props: Immutable): JSX.Element { id={AnchorIds.ContentPanel} className={`texture-paper-dots overflow-y-scroll bg-light [grid-area:content]`} > - {contentPanel ? ( + {isDefined(contentPanel) ? ( contentPanel ) : (
@@ -274,7 +296,7 @@ export function AppLayout(props: Immutable): JSX.Element {
{/* Sub panel */} - {subPanel && ( + {isDefined(subPanel) && (
): JSX.Element { mobile:[grid-area:content]`, turnSubIntoContent ? "mobile:w-full mobile:border-l-0" - : appLayout.subPanelOpen + : subPanelOpen === true ? "" : "mobile:translate-x-[100vw]" )} @@ -300,7 +322,7 @@ export function AppLayout(props: Immutable): JSX.Element { 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(!appLayout.mainPanelOpen, "mobile:-translate-x-full") + cIf(mainPanelOpen === false, "mobile:-translate-x-full") )} > @@ -312,11 +334,11 @@ export function AppLayout(props: Immutable): JSX.Element { border-t-[1px] border-dotted border-black bg-light [grid-area:navbar] desktop:hidden" > { - appLayout.setMainPanelOpen(!appLayout.mainPanelOpen); - appLayout.setSubPanelOpen(false); + toggleMainPanelOpen(); + setSubPanelOpen(false); }} />

): JSX.Element { > {ogTitle}

- {subPanel && !turnSubIntoContent && ( + {isDefined(subPanel) && !turnSubIntoContent && ( { - appLayout.setSubPanelOpen(!appLayout.subPanelOpen); - appLayout.setMainPanelOpen(false); + toggleSubPanelOpen(); + setMainPanelOpen(false); }} /> )}
- +

{langui.settings}

): JSX.Element { {router.locales && (

{langui.languages}

- {appLayout.preferredLanguages && ( + {preferredLanguages && ( 0 + preferredLanguages.length > 0 ? new Map( - appLayout.preferredLanguages.map((locale) => [ + preferredLanguages.map((locale) => [ locale, prettyLanguage(locale, languages), ]) @@ -380,13 +399,13 @@ export function AppLayout(props: Immutable): JSX.Element { ]) } onChange={(items) => { - const preferredLanguages = [...items].map( + const newPreferredLanguages = [...items].map( ([code]) => code ); - appLayout.setPreferredLanguages(preferredLanguages); - if (router.locale !== preferredLanguages[0]) { + setPreferredLanguages(newPreferredLanguages); + if (router.locale !== newPreferredLanguages[0]) { router.push(router.asPath, router.asPath, { - locale: preferredLanguages[0], + locale: newPreferredLanguages[0], }); } }} @@ -400,31 +419,25 @@ export function AppLayout(props: Immutable): JSX.Element {
@@ -509,8 +507,8 @@ export function AppLayout(props: Immutable): JSX.Element {
{/*
TODO: add to langui @@ -546,3 +544,22 @@ export function AppLayout(props: Immutable): JSX.Element {
); } + +/* + * const [searchQuery, setSearchQuery] = useState(""); + * const [searchResult, setSearchResult] = useState(); + */ + +/* + * 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]); + */ diff --git a/src/components/Inputs/Button.tsx b/src/components/Inputs/Button.tsx index b9e4e33..c578b09 100644 --- a/src/components/Inputs/Button.tsx +++ b/src/components/Inputs/Button.tsx @@ -1,8 +1,10 @@ import { Ico, Icon } from "components/Ico"; import { cIf, cJoin } from "helpers/className"; +import { ConditionalWrapper, Wrapper } from "helpers/component"; import { Immutable } from "helpers/types"; +import { isDefined, isDefinedAndNotEmpty } from "helpers/others"; import { useRouter } from "next/router"; -import { MouseEventHandler } from "react"; +import React, { MouseEventHandler } from "react"; interface Props { id?: string; @@ -32,60 +34,71 @@ export function Button(props: Immutable): JSX.Element { locale, badgeNumber, } = props; + const router = useRouter(); - const button = ( -
- {badgeNumber && ( -
-

{badgeNumber}

-
- )} - {icon && ( - - )} - {text &&

{text}

} -
- ); - - if (target) { - return ( - -
{button}
-
- ); - } - return ( -
{ - if (href || locale) - router.push(href ?? router.asPath, href, { - locale: locale, - }); - }} + - {button} -
+
{ + if (isDefined(href) || isDefined(locale)) { + router.push(href ?? router.asPath, href, { + locale: locale, + }); + } + }} + > +
+ {isDefined(badgeNumber) && ( +
+

{badgeNumber}

+
+ )} + {isDefinedAndNotEmpty(icon) && ( + + )} + {isDefinedAndNotEmpty(text) && ( +

{text}

+ )} +
+
+ + ); +} + +interface LinkWrapperProps { + href?: string; +} + +function LinkWrapper(props: LinkWrapperProps & Wrapper) { + const { children, href } = props; + return ( + + {children} + ); } diff --git a/src/components/Inputs/ButtonGroup.tsx b/src/components/Inputs/ButtonGroup.tsx index d721788..e619fac 100644 --- a/src/components/Inputs/ButtonGroup.tsx +++ b/src/components/Inputs/ButtonGroup.tsx @@ -1,6 +1,6 @@ import { cJoin } from "helpers/className"; import { Immutable } from "helpers/types"; -import { useEffect, useRef } from "react"; +import { useLayoutEffect, useRef } from "react"; interface Props { children: React.ReactNode; @@ -11,7 +11,7 @@ export function ButtonGroup(props: Immutable): JSX.Element { const { children, className } = props; const ref = useRef(null); - useEffect(() => { + useLayoutEffect(() => { if (ref.current) { const buttons = ref.current.querySelectorAll(".component-button"); buttons.forEach((button, index) => { diff --git a/src/components/Inputs/LanguageSwitcher.tsx b/src/components/Inputs/LanguageSwitcher.tsx index d14a49a..647aa9f 100644 --- a/src/components/Inputs/LanguageSwitcher.tsx +++ b/src/components/Inputs/LanguageSwitcher.tsx @@ -3,7 +3,7 @@ import { AppStaticProps } from "graphql/getAppStaticProps"; import { cJoin } from "helpers/className"; import { prettyLanguage } from "helpers/formatters"; import { Immutable } from "helpers/types"; -import { Dispatch, Fragment, SetStateAction } from "react"; +import { Fragment } from "react"; import { ToolTip } from "../ToolTip"; import { Button } from "./Button"; @@ -12,11 +12,11 @@ interface Props { languages: AppStaticProps["languages"]; locales: Map; localesIndex: number | undefined; - setLocalesIndex: Dispatch>; + onLanguageChanged: (index: number) => void; } export function LanguageSwitcher(props: Immutable): JSX.Element { - const { locales, className, localesIndex, setLocalesIndex } = props; + const { locales, className, localesIndex, onLanguageChanged } = props; return ( ): JSX.Element { {locale && (