Fixed the hooks problems plus other things

This commit is contained in:
DrMint 2022-06-18 04:02:20 +02:00
parent c076ec06ad
commit 24a8b43701
47 changed files with 780 additions and 605 deletions

View File

@ -60,7 +60,7 @@ module.exports = {
"no-alert": "warn", "no-alert": "warn",
"no-bitwise": "warn", "no-bitwise": "warn",
"no-caller": "warn", "no-caller": "warn",
"no-confusing-arrow": "warn", // "no-confusing-arrow": "warn",
"no-continue": "warn", "no-continue": "warn",
"no-else-return": "warn", "no-else-return": "warn",
"no-eq-null": "warn", "no-eq-null": "warn",
@ -190,7 +190,10 @@ module.exports = {
"@typescript-eslint/promise-function-async": "error", "@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error", "@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/sort-type-union-intersection-members": "warn", "@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/switch-exhaustiveness-check": "error",
"@typescript-eslint/typedef": "error", "@typescript-eslint/typedef": "error",
"@typescript-eslint/unified-signatures": "error", "@typescript-eslint/unified-signatures": "error",

View File

@ -4,14 +4,15 @@ import { UploadImageFragment } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { prettyLanguage, prettySlug } from "helpers/formatters"; 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 { getClient, Indexes, search, SearchResult } from "helpers/search";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import { AnchorIds } from "hooks/useScrollTopOnChange"; import { AnchorIds } from "hooks/useScrollTopOnChange";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useLayoutEffect, useMemo, useState } from "react";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { Ico, Icon } from "./Ico"; import { Ico, Icon } from "./Ico";
import { ButtonGroup } from "./Inputs/ButtonGroup"; import { ButtonGroup } from "./Inputs/ButtonGroup";
@ -31,6 +32,9 @@ interface Props extends AppStaticProps {
description?: string; description?: string;
} }
const SENSIBILITY_SWIPE = 1.1;
const TITLE_PREFIX = "Accords Library";
export function AppLayout(props: Immutable<Props>): JSX.Element { export function AppLayout(props: Immutable<Props>): JSX.Element {
const { const {
langui, langui,
@ -44,151 +48,169 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
description, description,
subPanelIcon = Icon.Tune, subPanelIcon = Icon.Tune,
} = props; } = 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 router = useRouter();
const isMobile = useMediaMobile(); const isMobile = useMediaMobile();
const appLayout = useAppLayout();
/* useEffect(() => {
* const [searchQuery, setSearchQuery] = useState("");
* const [searchResult, setSearchResult] = useState<SearchResult>();
*/
const sensibilitySwipe = 1.1;
useMemo(() => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
router.events?.on("routeChangeStart", () => { router.events?.on("routeChangeStart", () => {
appLayout.setConfigPanelOpen(false); setConfigPanelOpen(false);
appLayout.setMainPanelOpen(false); setMainPanelOpen(false);
appLayout.setSubPanelOpen(false); setSubPanelOpen(false);
}); });
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
router.events?.on("hashChangeStart", () => { router.events?.on("hashChangeStart", () => {
appLayout.setSubPanelOpen(false); setSubPanelOpen(false);
}); });
}, [appLayout, router.events]); }, [router.events, setConfigPanelOpen, setMainPanelOpen, setSubPanelOpen]);
const handlers = useSwipeable({ const handlers = useSwipeable({
onSwipedLeft: (SwipeEventData) => { onSwipedLeft: (SwipeEventData) => {
if (appLayout.menuGestures) { if (menuGestures) {
if (SwipeEventData.velocity < sensibilitySwipe) return; if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
if (appLayout.mainPanelOpen) { if (mainPanelOpen === true) {
appLayout.setMainPanelOpen(false); setMainPanelOpen(false);
} else if (subPanel && contentPanel) { } else if (subPanel === true && contentPanel === true) {
appLayout.setSubPanelOpen(true); setSubPanelOpen(true);
} }
} }
}, },
onSwipedRight: (SwipeEventData) => { onSwipedRight: (SwipeEventData) => {
if (appLayout.menuGestures) { if (menuGestures) {
if (SwipeEventData.velocity < sensibilitySwipe) return; if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
if (appLayout.subPanelOpen) { if (subPanelOpen === true) {
appLayout.setSubPanelOpen(false); setSubPanelOpen(false);
} else { } else {
appLayout.setMainPanelOpen(true); setMainPanelOpen(true);
} }
} }
}, },
}); });
/* const turnSubIntoContent = useMemo(
* const client = getClient(); () => isDefined(subPanel) && isDefined(contentPanel),
* useEffect(() => { [contentPanel, subPanel]
* 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 metaImage = useMemo(
() =>
const titlePrefix = "Accords Library"; thumbnail
const metaImage: OgImage = thumbnail
? getOgImage(ImageQuality.Og, thumbnail) ? getOgImage(ImageQuality.Og, thumbnail)
: { : {
image: "/default_og.jpg", image: "/default_og.jpg",
width: 1200, width: 1200,
height: 630, height: 630,
alt: "Accord's Library Logo", alt: "Accord's Library Logo",
}; },
const ogTitle = [thumbnail]
);
const { ogTitle, metaTitle } = useMemo(() => {
const resultTitle =
title ?? navTitle ?? prettySlug(router.asPath.split("/").pop()); 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 useLayoutEffect(() => {
? description
: langui.default_description ?? "";
useEffect(() => {
document.getElementsByTagName("html")[0].style.fontSize = `${ document.getElementsByTagName("html")[0].style.fontSize = `${
(appLayout.fontSize ?? 1) * 100 (fontSize ?? 1) * 100
}%`; }%`;
}, [appLayout.fontSize]); }, [fontSize]);
const currencyOptions: string[] = []; const defaultPreferredLanguages = useMemo(() => {
currencies.map((currency) => { let list: string[] = [];
if (currency.attributes?.code) if (isDefinedAndNotEmpty(router.locale) && router.locales) {
currencyOptions.push(currency.attributes.code); 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); const [currencySelect, setCurrencySelect] = useState<number>(-1);
let defaultPreferredLanguages: string[] = []; useEffect(() => {
if (isDefined(currency))
if (router.locale && router.locales) { setCurrencySelect(currencyOptions.indexOf(currency));
if (router.locale === "en") { }, [currency, currencyOptions]);
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(() => { useEffect(() => {
if (appLayout.currency) if (currencySelect >= 0) setCurrency(currencyOptions[currencySelect]);
setCurrencySelect(currencyOptions.indexOf(appLayout.currency)); }, [currencyOptions, currencySelect, setCurrency]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appLayout.currency]);
useEffect(() => { const gridCol = useMemo(() => {
if (currencySelect >= 0) if (isDefined(subPanel)) {
appLayout.setCurrency(currencyOptions[currencySelect]); if (mainPanelReduced === true) {
// eslint-disable-next-line react-hooks/exhaustive-deps return "grid-cols-[6rem_20rem_1fr]";
}, [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) { return "grid-cols-[20rem_20rem_1fr]";
gridCol = "grid-cols-[6rem_0px_1fr]"; } else if (mainPanelReduced === true) {
} else { return "grid-cols-[6rem_0px_1fr]";
gridCol = "grid-cols-[20rem_0px_1fr]";
} }
return "grid-cols-[20rem_0px_1fr]";
}, [mainPanelReduced, subPanel]);
return ( return (
<div <div
className={cJoin( className={cJoin(
cIf(appLayout.darkMode, "set-theme-dark", "set-theme-light"), cIf(darkMode, "set-theme-dark", "set-theme-light"),
cIf( cIf(dyslexic, "set-theme-font-dyslexic", "set-theme-font-standard")
appLayout.dyslexic,
"set-theme-font-dyslexic",
"set-theme-font-standard"
)
)} )}
> >
<div <div
@ -231,7 +253,7 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
`absolute inset-0 transition-[backdrop-filter] duration-500 [grid-area:content] `absolute inset-0 transition-[backdrop-filter] duration-500 [grid-area:content]
mobile:z-10`, mobile:z-10`,
cIf( cIf(
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile, (mainPanelOpen === true || subPanelOpen === true) && isMobile,
"[backdrop-filter:blur(2px)]", "[backdrop-filter:blur(2px)]",
"pointer-events-none touch-none" "pointer-events-none touch-none"
) )
@ -241,14 +263,14 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
className={cJoin( className={cJoin(
"absolute inset-0 bg-shade transition-opacity duration-500", "absolute inset-0 bg-shade transition-opacity duration-500",
cIf( cIf(
(appLayout.mainPanelOpen || appLayout.subPanelOpen) && isMobile, (mainPanelOpen === true || subPanelOpen === true) && isMobile,
"opacity-60", "opacity-60",
"opacity-0" "opacity-0"
) )
)} )}
onClick={() => { onClick={() => {
appLayout.setMainPanelOpen(false); setMainPanelOpen(false);
appLayout.setSubPanelOpen(false); setSubPanelOpen(false);
}} }}
></div> ></div>
</div> </div>
@ -258,7 +280,7 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
id={AnchorIds.ContentPanel} id={AnchorIds.ContentPanel}
className={`texture-paper-dots overflow-y-scroll bg-light [grid-area:content]`} className={`texture-paper-dots overflow-y-scroll bg-light [grid-area:content]`}
> >
{contentPanel ? ( {isDefined(contentPanel) ? (
contentPanel contentPanel
) : ( ) : (
<div className="grid h-full place-content-center"> <div className="grid h-full place-content-center">
@ -274,7 +296,7 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
</div> </div>
{/* Sub panel */} {/* Sub panel */}
{subPanel && ( {isDefined(subPanel) && (
<div <div
className={cJoin( className={cJoin(
`texture-paper-dots overflow-y-scroll border-r-[1px] border-dotted `texture-paper-dots overflow-y-scroll border-r-[1px] border-dotted
@ -284,7 +306,7 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
mobile:[grid-area:content]`, mobile:[grid-area:content]`,
turnSubIntoContent turnSubIntoContent
? "mobile:w-full mobile:border-l-0" ? "mobile:w-full mobile:border-l-0"
: appLayout.subPanelOpen : subPanelOpen === true
? "" ? ""
: "mobile:translate-x-[100vw]" : "mobile:translate-x-[100vw]"
)} )}
@ -300,7 +322,7 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
border-black bg-light transition-transform duration-300 [grid-area:main] border-black bg-light transition-transform duration-300 [grid-area:main]
[scrollbar-width:none] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%] [scrollbar-width:none] webkit-scrollbar:w-0 mobile:z-10 mobile:w-[90%]
mobile:justify-self-start mobile:[grid-area:content]`, mobile:justify-self-start mobile:[grid-area:content]`,
cIf(!appLayout.mainPanelOpen, "mobile:-translate-x-full") cIf(mainPanelOpen === false, "mobile:-translate-x-full")
)} )}
> >
<MainPanel langui={langui} /> <MainPanel langui={langui} />
@ -312,11 +334,11 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
border-t-[1px] border-dotted border-black bg-light [grid-area:navbar] desktop:hidden" border-t-[1px] border-dotted border-black bg-light [grid-area:navbar] desktop:hidden"
> >
<Ico <Ico
icon={appLayout.mainPanelOpen ? Icon.Close : Icon.Menu} icon={mainPanelOpen === true ? Icon.Close : Icon.Menu}
className="mt-[.1em] cursor-pointer !text-2xl" className="mt-[.1em] cursor-pointer !text-2xl"
onClick={() => { onClick={() => {
appLayout.setMainPanelOpen(!appLayout.mainPanelOpen); toggleMainPanelOpen();
appLayout.setSubPanelOpen(false); setSubPanelOpen(false);
}} }}
/> />
<p <p
@ -331,22 +353,19 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
> >
{ogTitle} {ogTitle}
</p> </p>
{subPanel && !turnSubIntoContent && ( {isDefined(subPanel) && !turnSubIntoContent && (
<Ico <Ico
icon={appLayout.subPanelOpen ? Icon.Close : subPanelIcon} icon={subPanelOpen === true ? Icon.Close : subPanelIcon}
className="mt-[.1em] cursor-pointer !text-2xl" className="mt-[.1em] cursor-pointer !text-2xl"
onClick={() => { onClick={() => {
appLayout.setSubPanelOpen(!appLayout.subPanelOpen); toggleSubPanelOpen();
appLayout.setMainPanelOpen(false); setMainPanelOpen(false);
}} }}
/> />
)} )}
</div> </div>
<Popup <Popup state={configPanelOpen ?? false} setState={setConfigPanelOpen}>
state={appLayout.configPanelOpen}
setState={appLayout.setConfigPanelOpen}
>
<h2 className="text-2xl">{langui.settings}</h2> <h2 className="text-2xl">{langui.settings}</h2>
<div <div
@ -356,12 +375,12 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
{router.locales && ( {router.locales && (
<div> <div>
<h3 className="text-xl">{langui.languages}</h3> <h3 className="text-xl">{langui.languages}</h3>
{appLayout.preferredLanguages && ( {preferredLanguages && (
<OrderableList <OrderableList
items={ items={
appLayout.preferredLanguages.length > 0 preferredLanguages.length > 0
? new Map( ? new Map(
appLayout.preferredLanguages.map((locale) => [ preferredLanguages.map((locale) => [
locale, locale,
prettyLanguage(locale, languages), prettyLanguage(locale, languages),
]) ])
@ -380,13 +399,13 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
]) ])
} }
onChange={(items) => { onChange={(items) => {
const preferredLanguages = [...items].map( const newPreferredLanguages = [...items].map(
([code]) => code ([code]) => code
); );
appLayout.setPreferredLanguages(preferredLanguages); setPreferredLanguages(newPreferredLanguages);
if (router.locale !== preferredLanguages[0]) { if (router.locale !== newPreferredLanguages[0]) {
router.push(router.asPath, router.asPath, { router.push(router.asPath, router.asPath, {
locale: preferredLanguages[0], locale: newPreferredLanguages[0],
}); });
} }
}} }}
@ -400,31 +419,25 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
<ButtonGroup> <ButtonGroup>
<Button <Button
onClick={() => { onClick={() => {
appLayout.setDarkMode(false); setDarkMode(false);
appLayout.setSelectedThemeMode(true); setSelectedThemeMode(true);
}} }}
active={ active={selectedThemeMode === true && darkMode === false}
appLayout.selectedThemeMode === true &&
appLayout.darkMode === false
}
text={langui.light} text={langui.light}
/> />
<Button <Button
onClick={() => { onClick={() => {
appLayout.setSelectedThemeMode(false); setSelectedThemeMode(false);
}} }}
active={appLayout.selectedThemeMode === false} active={selectedThemeMode === false}
text={langui.auto} text={langui.auto}
/> />
<Button <Button
onClick={() => { onClick={() => {
appLayout.setDarkMode(true); setDarkMode(true);
appLayout.setSelectedThemeMode(true); setSelectedThemeMode(true);
}} }}
active={ active={selectedThemeMode === true && darkMode === true}
appLayout.selectedThemeMode === true &&
appLayout.darkMode === true
}
text={langui.dark} text={langui.dark}
/> />
</ButtonGroup> </ButtonGroup>
@ -446,32 +459,17 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
<h3 className="text-xl">{langui.font_size}</h3> <h3 className="text-xl">{langui.font_size}</h3>
<ButtonGroup> <ButtonGroup>
<Button <Button
onClick={() => onClick={() => setFontSize((fontSize ?? 1) / 1.05)}
appLayout.setFontSize(
appLayout.fontSize
? appLayout.fontSize / 1.05
: 1 / 1.05
)
}
icon={Icon.TextDecrease} icon={Icon.TextDecrease}
/> />
<Button <Button
onClick={() => appLayout.setFontSize(1)} onClick={() => setFontSize(1)}
text={`${((appLayout.fontSize ?? 1) * 100).toLocaleString( text={`${((fontSize ?? 1) * 100).toLocaleString(undefined, {
undefined,
{
maximumFractionDigits: 0, maximumFractionDigits: 0,
} })}%`}
)}%`}
/> />
<Button <Button
onClick={() => onClick={() => setFontSize((fontSize ?? 1) * 1.05)}
appLayout.setFontSize(
appLayout.fontSize
? appLayout.fontSize * 1.05
: 1 * 1.05
)
}
icon={Icon.TextIncrease} icon={Icon.TextIncrease}
/> />
</ButtonGroup> </ButtonGroup>
@ -481,14 +479,14 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
<h3 className="text-xl">{langui.font}</h3> <h3 className="text-xl">{langui.font}</h3>
<div className="grid gap-2"> <div className="grid gap-2">
<Button <Button
active={appLayout.dyslexic === false} active={dyslexic === false}
onClick={() => appLayout.setDyslexic(false)} onClick={() => setDyslexic(false)}
className="font-zenMaruGothic" className="font-zenMaruGothic"
text="Zen Maru Gothic" text="Zen Maru Gothic"
/> />
<Button <Button
active={appLayout.dyslexic === true} active={dyslexic === true}
onClick={() => appLayout.setDyslexic(true)} onClick={() => setDyslexic(true)}
className="font-openDyslexic" className="font-openDyslexic"
text="OpenDyslexic" text="OpenDyslexic"
/> />
@ -500,8 +498,8 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
<TextInput <TextInput
placeholder="<player>" placeholder="<player>"
className="w-48" className="w-48"
state={appLayout.playerName} state={playerName}
setState={appLayout.setPlayerName} setState={setPlayerName}
/> />
</div> </div>
</div> </div>
@ -509,8 +507,8 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
</Popup> </Popup>
{/* <Popup {/* <Popup
state={appLayout.searchPanelOpen} state={searchPanelOpen}
setState={appLayout.setSearchPanelOpen} setState={setSearchPanelOpen}
> >
<div className="grid place-items-center gap-2"> <div className="grid place-items-center gap-2">
TODO: add to langui TODO: add to langui
@ -546,3 +544,22 @@ export function AppLayout(props: Immutable<Props>): JSX.Element {
</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]);
*/

View File

@ -1,8 +1,10 @@
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { ConditionalWrapper, Wrapper } from "helpers/component";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { MouseEventHandler } from "react"; import React, { MouseEventHandler } from "react";
interface Props { interface Props {
id?: string; id?: string;
@ -32,9 +34,25 @@ export function Button(props: Immutable<Props>): JSX.Element {
locale, locale,
badgeNumber, badgeNumber,
} = props; } = props;
const router = useRouter(); const router = useRouter();
const button = ( return (
<ConditionalWrapper
isWrapping={isDefined(target)}
wrapperProps={{ href: href }}
wrapper={LinkWrapper}
>
<div
className="relative"
onClick={() => {
if (isDefined(href) || isDefined(locale)) {
router.push(href ?? router.asPath, href, {
locale: locale,
});
}
}}
>
<div <div
draggable={draggable} draggable={draggable}
id={id} id={id}
@ -52,40 +70,35 @@ export function Button(props: Immutable<Props>): JSX.Element {
className className
)} )}
> >
{badgeNumber && ( {isDefined(badgeNumber) && (
<div <div
className="absolute -top-3 -right-2 grid h-8 w-8 place-items-center rounded-full bg-dark className="absolute -top-3 -right-2 grid h-8 w-8 place-items-center rounded-full
font-bold text-light transition-opacity group-hover:opacity-0" bg-dark font-bold text-light transition-opacity group-hover:opacity-0"
> >
<p className="-translate-y-[0.05em]">{badgeNumber}</p> <p className="-translate-y-[0.05em]">{badgeNumber}</p>
</div> </div>
)} )}
{icon && ( {isDefinedAndNotEmpty(icon) && (
<Ico className="[font-size:150%] [line-height:0.66]" icon={icon} /> <Ico className="[font-size:150%] [line-height:0.66]" icon={icon} />
)} )}
{text && <p className="-translate-y-[0.05em] text-center">{text}</p>} {isDefinedAndNotEmpty(text) && (
<p className="-translate-y-[0.05em] text-center">{text}</p>
)}
</div> </div>
</div>
</ConditionalWrapper>
); );
}
if (target) { interface LinkWrapperProps {
href?: string;
}
function LinkWrapper(props: LinkWrapperProps & Wrapper) {
const { children, href } = props;
return ( return (
<a href={href} target={target} rel="noreferrer"> <a href={href} target="_blank" rel="noreferrer">
<div className="relative">{button}</div> {children}
</a> </a>
); );
} }
return (
<div
className="relative"
onClick={() => {
if (href || locale)
router.push(href ?? router.asPath, href, {
locale: locale,
});
}}
>
{button}
</div>
);
}

View File

@ -1,6 +1,6 @@
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useEffect, useRef } from "react"; import { useLayoutEffect, useRef } from "react";
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
@ -11,7 +11,7 @@ export function ButtonGroup(props: Immutable<Props>): JSX.Element {
const { children, className } = props; const { children, className } = props;
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useEffect(() => { useLayoutEffect(() => {
if (ref.current) { if (ref.current) {
const buttons = ref.current.querySelectorAll(".component-button"); const buttons = ref.current.querySelectorAll(".component-button");
buttons.forEach((button, index) => { buttons.forEach((button, index) => {

View File

@ -3,7 +3,7 @@ import { AppStaticProps } from "graphql/getAppStaticProps";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { prettyLanguage } from "helpers/formatters"; import { prettyLanguage } from "helpers/formatters";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { Dispatch, Fragment, SetStateAction } from "react"; import { Fragment } from "react";
import { ToolTip } from "../ToolTip"; import { ToolTip } from "../ToolTip";
import { Button } from "./Button"; import { Button } from "./Button";
@ -12,11 +12,11 @@ interface Props {
languages: AppStaticProps["languages"]; languages: AppStaticProps["languages"];
locales: Map<string, number>; locales: Map<string, number>;
localesIndex: number | undefined; localesIndex: number | undefined;
setLocalesIndex: Dispatch<SetStateAction<number | undefined>>; onLanguageChanged: (index: number) => void;
} }
export function LanguageSwitcher(props: Immutable<Props>): JSX.Element { export function LanguageSwitcher(props: Immutable<Props>): JSX.Element {
const { locales, className, localesIndex, setLocalesIndex } = props; const { locales, className, localesIndex, onLanguageChanged } = props;
return ( return (
<ToolTip <ToolTip
@ -27,7 +27,7 @@ export function LanguageSwitcher(props: Immutable<Props>): JSX.Element {
{locale && ( {locale && (
<Button <Button
active={value === localesIndex} active={value === localesIndex}
onClick={() => setLocalesIndex(value)} onClick={() => onLanguageChanged(value)}
text={prettyLanguage(locale, props.languages)} text={prettyLanguage(locale, props.languages)}
/> />
)} )}

View File

@ -1,7 +1,7 @@
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { arrayMove } from "helpers/others"; import { arrayMove, isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useCallback, useState } from "react";
interface Props { interface Props {
className?: string; className?: string;
@ -11,23 +11,25 @@ interface Props {
} }
export function OrderableList(props: Immutable<Props>): JSX.Element { export function OrderableList(props: Immutable<Props>): JSX.Element {
const { onChange } = props;
const [items, setItems] = useState<Map<string, string>>(props.items); const [items, setItems] = useState<Map<string, string>>(props.items);
useEffect(() => { const updateOrder = useCallback(
props.onChange?.(items); (sourceIndex: number, targetIndex: number) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items]);
function updateOrder(sourceIndex: number, targetIndex: number) {
const newItems = arrayMove([...items], sourceIndex, targetIndex); const newItems = arrayMove([...items], sourceIndex, targetIndex);
setItems(new Map(newItems)); const map = new Map(newItems);
} setItems(map);
onChange?.(map);
},
[items, onChange]
);
return ( return (
<div className="grid gap-2"> <div className="grid gap-2">
{[...items].map(([key, value], index) => ( {[...items].map(([key, value], index) => (
<Fragment key={key}> <Fragment key={key}>
{props.insertLabels?.get(index) && ( {props.insertLabels &&
isDefinedAndNotEmpty(props.insertLabels.get(index)) && (
<p>{props.insertLabels.get(index)}</p> <p>{props.insertLabels.get(index)}</p>
)} )}
<div <div

View File

@ -17,9 +17,7 @@ export function PageSelector(props: Immutable<Props>): JSX.Element {
return ( return (
<div className={cJoin("flex flex-row place-content-center", className)}> <div className={cJoin("flex flex-row place-content-center", className)}>
<Button <Button
onClick={() => { onClick={() => setPage((current) => (page > 0 ? current - 1 : current))}
if (page > 0) setPage(page - 1);
}}
className="rounded-r-none" className="rounded-r-none"
icon={Icon.NavigateBefore} icon={Icon.NavigateBefore}
/> />
@ -28,9 +26,9 @@ export function PageSelector(props: Immutable<Props>): JSX.Element {
text={(page + 1).toString()} text={(page + 1).toString()}
/> />
<Button <Button
onClick={() => { onClick={() =>
if (page < maxPage) setPage(page + 1); setPage((current) => (page < maxPage ? page + 1 : current))
}} }
className="rounded-l-none" className="rounded-l-none"
icon={Icon.NavigateNext} icon={Icon.NavigateNext}
/> />

View File

@ -1,6 +1,7 @@
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useToggle } from "hooks/useToggle";
import { Dispatch, Fragment, SetStateAction, useState } from "react"; import { Dispatch, Fragment, SetStateAction, useState } from "react";
interface Props { interface Props {
@ -15,6 +16,7 @@ interface Props {
export function Select(props: Immutable<Props>): JSX.Element { export function Select(props: Immutable<Props>): JSX.Element {
const { className, state, options, allowEmpty, setState } = props; const { className, state, options, allowEmpty, setState } = props;
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const toggleOpened = useToggle(setOpened);
return ( return (
<div <div
@ -32,18 +34,21 @@ export function Select(props: Immutable<Props>): JSX.Element {
cIf(opened, "rounded-b-none bg-highlight outline-[transparent]") cIf(opened, "rounded-b-none bg-highlight outline-[transparent]")
)} )}
> >
<p onClick={() => setOpened(!opened)} className="w-full"> <p onClick={toggleOpened} className="w-full">
{state === -1 ? "—" : options[state]} {state === -1 ? "—" : options[state]}
</p> </p>
{state >= 0 && allowEmpty && ( {state >= 0 && allowEmpty && (
<Ico <Ico
icon={Icon.Close} icon={Icon.Close}
className="!text-xs" className="!text-xs"
onClick={() => setState(-1)} onClick={() => {
setState(-1);
toggleOpened();
}}
/> />
)} )}
<Ico <Ico
onClick={() => setOpened(!opened)} onClick={toggleOpened}
icon={opened ? Icon.ArrowDropUp : Icon.ArrowDropDown} icon={opened ? Icon.ArrowDropUp : Icon.ArrowDropDown}
/> />
</div> </div>

View File

@ -1,5 +1,6 @@
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useToggle } from "hooks/useToggle";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
interface Props { interface Props {
@ -11,6 +12,7 @@ interface Props {
export function Switch(props: Immutable<Props>): JSX.Element { export function Switch(props: Immutable<Props>): JSX.Element {
const { state, setState, className, disabled } = props; const { state, setState, className, disabled } = props;
const toggleState = useToggle(setState);
return ( return (
<div <div
className={cJoin( className={cJoin(
@ -24,7 +26,7 @@ export function Switch(props: Immutable<Props>): JSX.Element {
className className
)} )}
onClick={() => { onClick={() => {
if (!disabled) setState(!state); if (disabled === false) toggleState();
}} }}
> >
<div <div

View File

@ -1,5 +1,6 @@
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction } from "react";
@ -28,8 +29,8 @@ export function TextInput(props: Immutable<Props>): JSX.Element {
setState(event.target.value); setState(event.target.value);
}} }}
/> />
{isDefinedAndNotEmpty(state) && (
<div className="absolute right-4 top-0 bottom-0 grid place-items-center"> <div className="absolute right-4 top-0 bottom-0 grid place-items-center">
{state && (
<Ico <Ico
className="cursor-pointer !text-xs" className="cursor-pointer !text-xs"
icon={Icon.Close} icon={Icon.Close}
@ -37,8 +38,8 @@ export function TextInput(props: Immutable<Props>): JSX.Element {
setState(""); setState("");
}} }}
/> />
)}
</div> </div>
)}
</div> </div>
); );
} }

View File

@ -1,4 +1,5 @@
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
interface Props { interface Props {
@ -16,7 +17,7 @@ export function WithLabel(props: Immutable<Props>): JSX.Element {
cIf(disabled, "text-dark brightness-150 contrast-75 grayscale") cIf(disabled, "text-dark brightness-150 contrast-75 grayscale")
)} )}
> >
{label && ( {isDefinedAndNotEmpty(label) && (
<p <p
className={cJoin( className={cJoin(
"text-left", "text-left",

View File

@ -5,6 +5,7 @@ import { GetLibraryItemQuery } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyinlineTitle, prettySlug } from "helpers/formatters"; import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useToggle } from "hooks/useToggle";
import { useState } from "react"; import { useState } from "react";
interface Props { interface Props {
@ -23,6 +24,7 @@ export function ContentLine(props: Immutable<Props>): JSX.Element {
const { content, langui, parentSlug } = props; const { content, langui, parentSlug } = props;
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const toggleOpened = useToggle(setOpened);
if (content.attributes) { if (content.attributes) {
return ( return (
@ -36,7 +38,7 @@ export function ContentLine(props: Immutable<Props>): JSX.Element {
gap-4 thin:grid-cols-[auto_auto_1fr_auto]" gap-4 thin:grid-cols-[auto_auto_1fr_auto]"
> >
<a> <a>
<h3 className="cursor-pointer" onClick={() => setOpened(!opened)}> <h3 className="cursor-pointer" onClick={toggleOpened}>
{content.attributes.content?.data?.attributes?.translations?.[0] {content.attributes.content?.data?.attributes?.translations?.[0]
? prettyinlineTitle( ? prettyinlineTitle(
content.attributes.content.data.attributes.translations[0] content.attributes.content.data.attributes.translations[0]

View File

@ -7,7 +7,7 @@ import { GetLibraryItemScansQuery } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img"; import { getAssetFilename, getAssetURL, ImageQuality } from "helpers/img";
import { isInteger } from "helpers/numbers"; import { isInteger } from "helpers/numbers";
import { getStatusDescription } from "helpers/others"; import { getStatusDescription, isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Fragment } from "react"; import { Fragment } from "react";
@ -51,7 +51,12 @@ export function ScanSet(props: Immutable<Props>): JSX.Element {
transform: (item) => { transform: (item) => {
(item as NonNullable<Props["scanSet"][number]>).pages?.data.sort( (item as NonNullable<Props["scanSet"][number]>).pages?.data.sort(
(a, b) => { (a, b) => {
if (a.attributes?.url && b.attributes?.url) { if (
a.attributes &&
b.attributes &&
isDefinedAndNotEmpty(a.attributes.url) &&
isDefinedAndNotEmpty(b.attributes.url)
) {
let aName = getAssetFilename(a.attributes.url); let aName = getAssetFilename(a.attributes.url);
let bName = getAssetFilename(b.attributes.url); let bName = getAssetFilename(b.attributes.url);
@ -99,7 +104,8 @@ export function ScanSet(props: Immutable<Props>): JSX.Element {
</div> </div>
<div className="flex flex-row flex-wrap place-items-center gap-4 pb-6"> <div className="flex flex-row flex-wrap place-items-center gap-4 pb-6">
{content?.data?.attributes?.slug && ( {content?.data?.attributes &&
isDefinedAndNotEmpty(content.data.attributes.slug) && (
<Button <Button
href={`/contents/${content.data.attributes.slug}`} href={`/contents/${content.data.attributes.slug}`}
text={langui.open_content} text={langui.open_content}
@ -173,7 +179,7 @@ export function ScanSet(props: Immutable<Props>): JSX.Element {
</div> </div>
)} )}
{selectedScan.notes && ( {isDefinedAndNotEmpty(selectedScan.notes) && (
<ToolTip content={selectedScan.notes}> <ToolTip content={selectedScan.notes}>
<Chip>{"Notes"}</Chip> <Chip>{"Notes"}</Chip>
</ToolTip> </ToolTip>
@ -192,7 +198,10 @@ export function ScanSet(props: Immutable<Props>): JSX.Element {
onClick={() => { onClick={() => {
const images: string[] = []; const images: string[] = [];
selectedScan.pages?.data.map((image) => { selectedScan.pages?.data.map((image) => {
if (image.attributes?.url) if (
image.attributes &&
isDefinedAndNotEmpty(image.attributes.url)
)
images.push( images.push(
getAssetURL(image.attributes.url, ImageQuality.Large) getAssetURL(image.attributes.url, ImageQuality.Large)
); );

View File

@ -1,5 +1,5 @@
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction, useCallback } from "react";
import Hotkeys from "react-hot-keys"; import Hotkeys from "react-hot-keys";
import { useSwipeable } from "react-swipeable"; import { useSwipeable } from "react-swipeable";
import { Img } from "./Img"; import { Img } from "./Img";
@ -17,26 +17,26 @@ interface Props {
setIndex: Dispatch<SetStateAction<number>>; setIndex: Dispatch<SetStateAction<number>>;
} }
const SENSIBILITY_SWIPE = 0.5;
export function LightBox(props: Immutable<Props>): JSX.Element { export function LightBox(props: Immutable<Props>): JSX.Element {
const { state, setState, images, index, setIndex } = props; const { state, setState, images, index, setIndex } = props;
function handlePrevious() { const handlePrevious = useCallback(() => {
if (index > 0) setIndex(index - 1); if (index > 0) setIndex(index - 1);
} }, [index, setIndex]);
function handleNext() { const handleNext = useCallback(() => {
if (index < images.length - 1) setIndex(index + 1); if (index < images.length - 1) setIndex(index + 1);
} }, [images.length, index, setIndex]);
const sensibilitySwipe = 0.5;
const handlers = useSwipeable({ const handlers = useSwipeable({
onSwipedLeft: (SwipeEventData) => { onSwipedLeft: (SwipeEventData) => {
if (SwipeEventData.velocity < sensibilitySwipe) return; if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
handleNext(); handleNext();
}, },
onSwipedRight: (SwipeEventData) => { onSwipedRight: (SwipeEventData) => {
if (SwipeEventData.velocity < sensibilitySwipe) return; if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
handlePrevious(); handlePrevious();
}, },
}); });

View File

@ -7,11 +7,12 @@ import { useAppLayout } from "contexts/AppLayoutContext";
import { cJoin } from "helpers/className"; import { cJoin } from "helpers/className";
import { slugify } from "helpers/formatters"; import { slugify } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img"; import { getAssetURL, ImageQuality } from "helpers/img";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import Markdown from "markdown-to-jsx"; import Markdown from "markdown-to-jsx";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React from "react"; import React, { useMemo } from "react";
import ReactDOMServer from "react-dom/server"; import ReactDOMServer from "react-dom/server";
interface Props { interface Props {
@ -20,20 +21,20 @@ interface Props {
} }
export function Markdawn(props: Immutable<Props>): JSX.Element { export function Markdawn(props: Immutable<Props>): JSX.Element {
const { className, text: rawText } = props;
const appLayout = useAppLayout(); const appLayout = useAppLayout();
// eslint-disable-next-line no-irregular-whitespace
const text = `${preprocessMarkDawn(props.text)}`;
const router = useRouter(); const router = useRouter();
const [openLightBox, LightBox] = useLightBox(); const [openLightBox, LightBox] = useLightBox();
// eslint-disable-next-line no-irregular-whitespace
const text = useMemo(() => `${preprocessMarkDawn(rawText)}`, [rawText]);
if (text) { if (text) {
return ( return (
<> <>
<LightBox /> <LightBox />
<Markdown <Markdown
className={cJoin("formatted", props.className)} className={cJoin("formatted", className)}
options={{ options={{
slugify: slugify, slugify: slugify,
overrides: { overrides: {
@ -155,15 +156,13 @@ export function Markdawn(props: Immutable<Props>): JSX.Element {
target?: string; target?: string;
page?: string; page?: string;
}) => { }) => {
const slug = compProps.target const slug = isDefinedAndNotEmpty(compProps.target)
? slugify(compProps.target) ? slugify(compProps.target)
: slugify(compProps.children?.toString()); : slugify(compProps.children?.toString());
return ( return (
<a <a
onClick={async () => onClick={async () =>
router.replace( router.replace(`${compProps.page ?? ""}#${slug}`)
`${compProps.page ? compProps.page : ""}#${slug}`
)
} }
> >
{compProps.children} {compProps.children}
@ -175,7 +174,7 @@ export function Markdawn(props: Immutable<Props>): JSX.Element {
player: { player: {
component: () => ( component: () => (
<span className="text-dark opacity-70"> <span className="text-dark opacity-70">
{appLayout.playerName ? appLayout.playerName : "<player>"} {appLayout.playerName ?? "<player>"}
</span> </span>
), ),
}, },
@ -209,7 +208,7 @@ export function Markdawn(props: Immutable<Props>): JSX.Element {
component: (compProps: { children: React.ReactNode }) => ( component: (compProps: { children: React.ReactNode }) => (
<li <li
className={ className={
compProps.children && isDefined(compProps.children) &&
ReactDOMServer.renderToStaticMarkup( ReactDOMServer.renderToStaticMarkup(
<>{compProps.children}</> <>{compProps.children}</>
).length > 100 ).length > 100
@ -243,7 +242,7 @@ export function Markdawn(props: Immutable<Props>): JSX.Element {
cite?: string; cite?: string;
}) => ( }) => (
<blockquote> <blockquote>
{compProps.cite ? ( {isDefinedAndNotEmpty(compProps.cite) ? (
<> <>
&ldquo;{compProps.children}&rdquo; &ldquo;{compProps.children}&rdquo;
<cite> {compProps.cite}</cite> <cite> {compProps.cite}</cite>

View File

@ -1,7 +1,7 @@
import { slugify } from "helpers/formatters"; import { slugify } from "helpers/formatters";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Fragment } from "react"; import { Fragment, useMemo } from "react";
import { preprocessMarkDawn } from "./Markdawn"; import { preprocessMarkDawn } from "./Markdawn";
interface Props { interface Props {
@ -11,11 +11,15 @@ interface Props {
export function TOC(props: Immutable<Props>): JSX.Element { export function TOC(props: Immutable<Props>): JSX.Element {
const { text, title } = props; const { text, title } = props;
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
const router = useRouter(); const router = useRouter();
const toc = useMemo(
() => getTocFromMarkdawn(preprocessMarkDawn(text), title),
[text, title]
);
return ( return (
<> <>
{/* TODO: add to LANGUI */}
<h3 className="text-xl">Table of content</h3> <h3 className="text-xl">Table of content</h3>
<div className="max-w-[14.5rem] text-left"> <div className="max-w-[14.5rem] text-left">
<p className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap text-left"> <p className="relative my-2 overflow-x-hidden text-ellipsis whitespace-nowrap text-left">

View File

@ -1,9 +1,10 @@
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { ToolTip } from "components/ToolTip"; import { ToolTip } from "components/ToolTip";
import { cJoin, cIf } from "helpers/className"; import { cJoin, cIf } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { MouseEventHandler } from "react"; import { MouseEventHandler, useMemo } from "react";
interface Props { interface Props {
url: string; url: string;
@ -16,29 +17,35 @@ interface Props {
} }
export function NavOption(props: Immutable<Props>): JSX.Element { export function NavOption(props: Immutable<Props>): JSX.Element {
const { url, icon, title, subtitle, border, reduced, onClick } = props;
const router = useRouter(); const router = useRouter();
const isActive = router.asPath.startsWith(props.url); const isActive = useMemo(
() => router.asPath.startsWith(url),
[url, router.asPath]
);
return ( return (
<ToolTip <ToolTip
content={ content={
<div> <div>
<h3 className="text-2xl">{props.title}</h3> <h3 className="text-2xl">{title}</h3>
{props.subtitle && <p className="col-start-2">{props.subtitle}</p>} {isDefinedAndNotEmpty(subtitle) && (
<p className="col-start-2">{subtitle}</p>
)}
</div> </div>
} }
placement="right" placement="right"
className="text-left" className="text-left"
disabled={!props.reduced} disabled={reduced === false}
> >
<div <div
onClick={(event) => { onClick={(event) => {
if (props.onClick) props.onClick(event); if (onClick) onClick(event);
if (props.url) { if (url) {
if (props.url.startsWith("#")) { if (url.startsWith("#")) {
router.replace(props.url); router.replace(url);
} else { } else {
router.push(props.url); router.push(url);
} }
} }
}} }}
@ -46,22 +53,20 @@ export function NavOption(props: Immutable<Props>): JSX.Element {
`relative grid w-full cursor-pointer auto-cols-fr grid-flow-col grid-cols-[auto] `relative grid w-full cursor-pointer auto-cols-fr grid-flow-col grid-cols-[auto]
justify-center gap-x-5 rounded-2xl p-4 transition-all hover:bg-mid hover:shadow-inner-sm justify-center gap-x-5 rounded-2xl p-4 transition-all hover:bg-mid hover:shadow-inner-sm
hover:shadow-shade hover:active:shadow-inner hover:active:shadow-shade`, hover:shadow-shade hover:active:shadow-inner hover:active:shadow-shade`,
cIf(props.icon, "text-left", "text-center"), cIf(icon, "text-left", "text-center"),
cIf( cIf(
props.border, border,
"outline outline-2 outline-offset-[-2px] outline-mid hover:outline-[transparent]" "outline outline-2 outline-offset-[-2px] outline-mid hover:outline-[transparent]"
), ),
cIf(isActive, "bg-mid shadow-inner-sm shadow-shade") cIf(isActive, "bg-mid shadow-inner-sm shadow-shade")
)} )}
> >
{props.icon && ( {icon && <Ico icon={icon} className="mt-[-.1em] !text-2xl" />}
<Ico icon={props.icon} className="mt-[-.1em] !text-2xl" />
)}
{!props.reduced && ( {reduced === false && (
<div> <div>
<h3 className="text-2xl">{props.title}</h3> <h3 className="text-2xl">{title}</h3>
{props.subtitle && <p className="col-start-2">{props.subtitle}</p>} {isDefinedAndNotEmpty(subtitle) && <p className="col-start-2">{subtitle}</p>}
</div> </div>
)} )}
</div> </div>

View File

@ -1,5 +1,6 @@
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { Ico, Icon } from "components/Ico"; import { Ico, Icon } from "components/Ico";
import { isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
interface Props { interface Props {
@ -9,12 +10,13 @@ interface Props {
} }
export function PanelHeader(props: Immutable<Props>): JSX.Element { export function PanelHeader(props: Immutable<Props>): JSX.Element {
const { icon, description, title } = props;
return ( return (
<> <>
<div className="grid w-full place-items-center"> <div className="grid w-full place-items-center">
{props.icon && <Ico icon={props.icon} className="mb-3 !text-4xl" />} {icon && <Ico icon={icon} className="mb-3 !text-4xl" />}
<h2 className="text-2xl">{props.title}</h2> <h2 className="text-2xl">{title}</h2>
{props.description ? <p>{props.description}</p> : ""} {isDefinedAndNotEmpty(description) && <p>{description}</p>}
</div> </div>
<HorizontalLine /> <HorizontalLine />
</> </>

View File

@ -22,26 +22,27 @@ export enum ReturnButtonType {
} }
export function ReturnButton(props: Immutable<Props>): JSX.Element { export function ReturnButton(props: Immutable<Props>): JSX.Element {
const { href, title, langui, displayOn, horizontalLine, className } = props;
const appLayout = useAppLayout(); const appLayout = useAppLayout();
return ( return (
<div <div
className={cJoin( className={cJoin(
props.displayOn === ReturnButtonType.Mobile displayOn === ReturnButtonType.Mobile
? "desktop:hidden" ? "desktop:hidden"
: props.displayOn === ReturnButtonType.Desktop : displayOn === ReturnButtonType.Desktop
? "mobile:hidden" ? "mobile:hidden"
: "", : "",
props.className className
)} )}
> >
<Button <Button
onClick={() => appLayout.setSubPanelOpen(false)} onClick={() => appLayout.setSubPanelOpen(false)}
href={props.href} href={href}
text={`${props.langui.return_to} ${props.title}`} text={`${langui.return_to} ${title}`}
icon={Icon.NavigateBefore} icon={Icon.NavigateBefore}
/> />
{props.horizontalLine && <HorizontalLine />} {horizontalLine === true && <HorizontalLine />}
</div> </div>
); );
} }

View File

@ -10,6 +10,7 @@ import Markdown from "markdown-to-jsx";
import Link from "next/link"; import Link from "next/link";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className"; import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
interface Props { interface Props {
langui: AppStaticProps["langui"]; langui: AppStaticProps["langui"];
@ -18,30 +19,30 @@ interface Props {
export function MainPanel(props: Immutable<Props>): JSX.Element { export function MainPanel(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
const isDesktop = useMediaDesktop(); const isDesktop = useMediaDesktop();
const appLayout = useAppLayout(); const {
mainPanelReduced = false,
toggleMainPanelReduced,
setConfigPanelOpen,
} = useAppLayout();
return ( return (
<div <div
className={cJoin( className={cJoin(
"grid content-start justify-center gap-y-2 p-8 text-center", "grid content-start justify-center gap-y-2 p-8 text-center",
cIf(appLayout.mainPanelReduced && isDesktop, "px-4") cIf(mainPanelReduced && isDesktop, "px-4")
)} )}
> >
{/* Reduce/expand main menu */} {/* Reduce/expand main menu */}
<div <div
className={cJoin( className={cJoin(
"fixed top-1/2 mobile:hidden", "fixed top-1/2 mobile:hidden",
cIf(appLayout.mainPanelReduced, "left-[4.65rem]", "left-[18.65rem]") cIf(mainPanelReduced, "left-[4.65rem]", "left-[18.65rem]")
)} )}
onClick={() => onClick={toggleMainPanelReduced}
appLayout.setMainPanelReduced(!appLayout.mainPanelReduced)
}
> >
<Button <Button
className="bg-light !px-2" className="bg-light !px-2"
icon={ icon={mainPanelReduced ? Icon.ChevronRight : Icon.ChevronLeft}
appLayout.mainPanelReduced ? Icon.ChevronRight : Icon.ChevronLeft
}
/> />
</div> </div>
@ -53,34 +54,30 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
`mb-4 aspect-square cursor-pointer bg-black transition-colors `mb-4 aspect-square cursor-pointer bg-black transition-colors
[mask:url('/icons/accords.svg')] ![mask-size:contain] ![mask-repeat:no-repeat] [mask:url('/icons/accords.svg')] ![mask-size:contain] ![mask-repeat:no-repeat]
![mask-position:center] hover:bg-dark`, ![mask-position:center] hover:bg-dark`,
cIf(appLayout.mainPanelReduced && isDesktop, "w-12", "w-1/2") cIf(mainPanelReduced && isDesktop, "w-12", "w-1/2")
)} )}
></div> ></div>
</Link> </Link>
{(!appLayout.mainPanelReduced || !isDesktop) && ( {(!mainPanelReduced || !isDesktop) && (
<h2 className="text-3xl">Accord&rsquo;s Library</h2> <h2 className="text-3xl">Accord&rsquo;s Library</h2>
)} )}
<div <div
className={cJoin( className={cJoin(
"flex flex-wrap gap-2", "flex flex-wrap gap-2",
cIf( cIf(mainPanelReduced && isDesktop, "flex-col gap-3", "flex-row")
appLayout.mainPanelReduced && isDesktop,
"flex-col gap-3",
"flex-row"
)
)} )}
> >
<ToolTip <ToolTip
content={<h3 className="text-2xl">{langui.open_settings}</h3>} content={<h3 className="text-2xl">{langui.open_settings}</h3>}
placement="right" placement="right"
className="text-left" className="text-left"
disabled={!appLayout.mainPanelReduced} disabled={!mainPanelReduced}
> >
<Button <Button
onClick={() => { onClick={() => {
appLayout.setConfigPanelOpen(true); setConfigPanelOpen(true);
}} }}
icon={Icon.Settings} icon={Icon.Settings}
/> />
@ -90,11 +87,11 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
content={<h3 className="text-2xl">{langui.open_search}</h3>} content={<h3 className="text-2xl">{langui.open_search}</h3>}
placement="right" placement="right"
className="text-left" className="text-left"
disabled={!appLayout.mainPanelReduced} disabled={!mainPanelReduced}
> >
<Button <Button
onClick={() => { onClick={() => {
appLayout.setSearchPanelOpen(true); setSearchPanelOpen(true);
}} }}
icon={Icon.Search} icon={Icon.Search}
/> />
@ -110,7 +107,7 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
icon={Icon.LibraryBooks} icon={Icon.LibraryBooks}
title={langui.library} title={langui.library}
subtitle={langui.library_short_description} subtitle={langui.library_short_description}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
<NavOption <NavOption
@ -118,7 +115,7 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
icon={Icon.Workspaces} icon={Icon.Workspaces}
title={langui.contents} title={langui.contents}
subtitle={langui.contents_short_description} subtitle={langui.contents_short_description}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
<NavOption <NavOption
@ -126,7 +123,7 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
icon={Icon.TravelExplore} icon={Icon.TravelExplore}
title={langui.wiki} title={langui.wiki}
subtitle={langui.wiki_short_description} subtitle={langui.wiki_short_description}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
{/* {/*
@ -135,7 +132,7 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
icon={Icon.WatchLater} icon={Icon.WatchLater}
title={langui.chronicles} title={langui.chronicles}
subtitle={langui.chronicles_short_description} subtitle={langui.chronicles_short_description}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
*/} */}
@ -145,7 +142,7 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
url="/news" url="/news"
icon={Icon.Feed} icon={Icon.Feed}
title={langui.news} title={langui.news}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
{/* {/*
@ -153,7 +150,7 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
url="/merch" url="/merch"
icon={Icon.Store} icon={Icon.Store}
title={langui.merch} title={langui.merch}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
*/} */}
@ -161,36 +158,36 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
url="https://gallery.accords-library.com/" url="https://gallery.accords-library.com/"
icon={Icon.Collections} icon={Icon.Collections}
title={langui.gallery} title={langui.gallery}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
<NavOption <NavOption
url="/archives" url="/archives"
icon={Icon.Inventory} icon={Icon.Inventory}
title={langui.archives} title={langui.archives}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
<NavOption <NavOption
url="/about-us" url="/about-us"
icon={Icon.Info} icon={Icon.Info}
title={langui.about_us} title={langui.about_us}
reduced={appLayout.mainPanelReduced && isDesktop} reduced={mainPanelReduced && isDesktop}
/> />
{appLayout.mainPanelReduced && isDesktop ? "" : <HorizontalLine />} {mainPanelReduced && isDesktop ? "" : <HorizontalLine />}
<div <div
className={cJoin( className={cJoin(
"text-center", "text-center",
cIf(appLayout.mainPanelReduced && isDesktop, "hidden") cIf(mainPanelReduced && isDesktop, "hidden")
)} )}
> >
{isDefinedAndNotEmpty(langui.licensing_notice) && (
<p> <p>
{langui.licensing_notice && (
<Markdown>{langui.licensing_notice}</Markdown> <Markdown>{langui.licensing_notice}</Markdown>
)}
</p> </p>
)}
<div className="mt-4 mb-8 grid place-content-center"> <div className="mt-4 mb-8 grid place-content-center">
<a <a
aria-label="Read more about the license we use for this website" aria-label="Read more about the license we use for this website"
@ -214,11 +211,11 @@ export function MainPanel(props: Immutable<Props>): JSX.Element {
/> />
</a> </a>
</div> </div>
{isDefinedAndNotEmpty(langui.copyright_notice) && (
<p> <p>
{langui.copyright_notice && (
<Markdown>{langui.copyright_notice}</Markdown> <Markdown>{langui.copyright_notice}</Markdown>
)}
</p> </p>
)}
<div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8"> <div className="mt-12 mb-4 grid h-4 grid-flow-col place-content-center gap-8">
<a <a
aria-label="Browse our GitHub repository, which include this website source code" aria-label="Browse our GitHub repository, which include this website source code"

View File

@ -8,7 +8,7 @@ interface Props {
setState: setState:
| Dispatch<SetStateAction<boolean | undefined>> | Dispatch<SetStateAction<boolean | undefined>>
| Dispatch<SetStateAction<boolean>>; | Dispatch<SetStateAction<boolean>>;
state?: boolean; state: boolean;
children: React.ReactNode; children: React.ReactNode;
fillViewport?: boolean; fillViewport?: boolean;
hideBackground?: boolean; hideBackground?: boolean;
@ -21,16 +21,15 @@ export function Popup(props: Immutable<Props>): JSX.Element {
state, state,
children, children,
fillViewport, fillViewport,
hideBackground, hideBackground = false,
padding = true, padding = true,
} = props; } = props;
const appLayout = useAppLayout(); const { setMenuGestures } = useAppLayout();
useEffect(() => { useEffect(() => {
appLayout.setMenuGestures(!state); setMenuGestures(state);
// eslint-disable-next-line react-hooks/exhaustive-deps }, [setMenuGestures, state]);
}, [state]);
return ( return (
<Hotkeys <Hotkeys

View File

@ -4,7 +4,7 @@ import { prettySlug } from "helpers/formatters";
import { getStatusDescription } from "helpers/others"; import { getStatusDescription } from "helpers/others";
import { Immutable, PostWithTranslations } from "helpers/types"; import { Immutable, PostWithTranslations } from "helpers/types";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Fragment } from "react"; import { Fragment, useMemo } from "react";
import { AppLayout } from "./AppLayout"; import { AppLayout } from "./AppLayout";
import { Chip } from "./Chip"; import { Chip } from "./Chip";
import { HorizontalLine } from "./HorizontalLine"; import { HorizontalLine } from "./HorizontalLine";
@ -55,13 +55,17 @@ export function PostPage(props: Immutable<Props>): JSX.Element {
languageExtractor: (item) => item.language?.data?.attributes?.code, languageExtractor: (item) => item.language?.data?.attributes?.code,
}); });
const thumbnail = const { thumbnail, body, title, excerpt } = useMemo(
() => ({
thumbnail:
selectedTranslation?.thumbnail?.data?.attributes ?? selectedTranslation?.thumbnail?.data?.attributes ??
post.thumbnail?.data?.attributes; post.thumbnail?.data?.attributes,
body: selectedTranslation?.body ?? "",
const body = selectedTranslation?.body ?? ""; title: selectedTranslation?.title ?? prettySlug(post.slug),
const title = selectedTranslation?.title ?? prettySlug(post.slug); excerpt: selectedTranslation?.excerpt ?? "",
const except = selectedTranslation?.excerpt ?? ""; }),
[post.slug, post.thumbnail, selectedTranslation]
);
const subPanel = const subPanel =
returnHref || returnTitle || displayCredits || displayToc ? ( returnHref || returnTitle || displayCredits || displayToc ? (
@ -137,7 +141,7 @@ export function PostPage(props: Immutable<Props>): JSX.Element {
<ThumbnailHeader <ThumbnailHeader
thumbnail={thumbnail} thumbnail={thumbnail}
title={title} title={title}
description={except} description={excerpt}
langui={langui} langui={langui}
categories={post.categories} categories={post.categories}
languageSwitcher={<LanguageSwitcher />} languageSwitcher={<LanguageSwitcher />}

View File

@ -19,55 +19,6 @@ interface Props {
export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element { export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element {
const { langui } = props; const { langui } = props;
function generateAnchor(
year: number | undefined,
month: number | null | undefined,
day: number | null | undefined
): string {
let result = "";
if (year) result += year;
if (month) result += `- ${month.toString().padStart(2, "0")}`;
if (day) result += `- ${day.toString().padStart(2, "0")}`;
return result;
}
function generateYear(
displayed_date: string | null | undefined,
year: number | undefined
): string {
return displayed_date ?? year?.toString() ?? "";
}
function generateDate(
month: number | null | undefined,
day: number | null | undefined
): string {
const lut = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
let result = "";
if (month && month >= 1 && month <= 12) {
result += lut[month - 1];
if (day) {
result += ` ${day}`;
}
}
return result;
}
if (props.item.attributes) { if (props.item.attributes) {
return ( return (
<div <div
@ -168,3 +119,52 @@ export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element {
return <></>; return <></>;
} }
function generateAnchor(
year: number | undefined,
month: number | null | undefined,
day: number | null | undefined
): string {
let result = "";
if (year) result += year;
if (month) result += `- ${month.toString().padStart(2, "0")}`;
if (day) result += `- ${day.toString().padStart(2, "0")}`;
return result;
}
function generateYear(
displayed_date: string | null | undefined,
year: number | undefined
): string {
return displayed_date ?? year?.toString() ?? "";
}
function generateDate(
month: number | null | undefined,
day: number | null | undefined
): string {
const lut = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
];
let result = "";
if (month && month >= 1 && month <= 12) {
result += lut[month - 1];
if (day) {
result += ` ${day}`;
}
}
return result;
}

View File

@ -1,38 +1,51 @@
import { Immutable, LibraryItemUserStatus } from "helpers/types"; import { isDefined } from "helpers/others";
import {
Immutable,
LibraryItemUserStatus,
RequiredNonNullable,
} from "helpers/types";
import { useDarkMode } from "hooks/useDarkMode"; import { useDarkMode } from "hooks/useDarkMode";
import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage"; import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage";
import React, { ReactNode, useContext, useState } from "react"; import React, { ReactNode, useContext, useState } from "react";
export interface AppLayoutState { export interface AppLayoutState {
subPanelOpen: boolean | undefined; subPanelOpen: boolean | undefined;
toggleSubPanelOpen: () => void;
setSubPanelOpen: React.Dispatch< setSubPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["subPanelOpen"]> React.SetStateAction<AppLayoutState["subPanelOpen"]>
>; >;
configPanelOpen: boolean | undefined; configPanelOpen: boolean | undefined;
toggleConfigPanelOpen: () => void;
setConfigPanelOpen: React.Dispatch< setConfigPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["configPanelOpen"]> React.SetStateAction<AppLayoutState["configPanelOpen"]>
>; >;
searchPanelOpen: boolean | undefined; searchPanelOpen: boolean | undefined;
toggleSearchPanelOpen: () => void;
setSearchPanelOpen: React.Dispatch< setSearchPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["searchPanelOpen"]> React.SetStateAction<AppLayoutState["searchPanelOpen"]>
>; >;
mainPanelReduced: boolean | undefined; mainPanelReduced: boolean | undefined;
toggleMainPanelReduced: () => void;
setMainPanelReduced: React.Dispatch< setMainPanelReduced: React.Dispatch<
React.SetStateAction<AppLayoutState["mainPanelReduced"]> React.SetStateAction<AppLayoutState["mainPanelReduced"]>
>; >;
mainPanelOpen: boolean | undefined; mainPanelOpen: boolean | undefined;
toggleMainPanelOpen: () => void;
setMainPanelOpen: React.Dispatch< setMainPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["mainPanelOpen"]> React.SetStateAction<AppLayoutState["mainPanelOpen"]>
>; >;
darkMode: boolean | undefined; darkMode: boolean | undefined;
toggleDarkMode: () => void;
setDarkMode: React.Dispatch<React.SetStateAction<AppLayoutState["darkMode"]>>; setDarkMode: React.Dispatch<React.SetStateAction<AppLayoutState["darkMode"]>>;
selectedThemeMode: boolean | undefined; selectedThemeMode: boolean | undefined;
toggleSelectedThemeMode: () => void;
setSelectedThemeMode: React.Dispatch< setSelectedThemeMode: React.Dispatch<
React.SetStateAction<AppLayoutState["selectedThemeMode"]> React.SetStateAction<AppLayoutState["selectedThemeMode"]>
>; >;
fontSize: number | undefined; fontSize: number | undefined;
setFontSize: React.Dispatch<React.SetStateAction<AppLayoutState["fontSize"]>>; setFontSize: React.Dispatch<React.SetStateAction<AppLayoutState["fontSize"]>>;
dyslexic: boolean | undefined; dyslexic: boolean | undefined;
toggleDyslexic: () => void;
setDyslexic: React.Dispatch<React.SetStateAction<AppLayoutState["dyslexic"]>>; setDyslexic: React.Dispatch<React.SetStateAction<AppLayoutState["dyslexic"]>>;
currency: string | undefined; currency: string | undefined;
setCurrency: React.Dispatch<React.SetStateAction<AppLayoutState["currency"]>>; setCurrency: React.Dispatch<React.SetStateAction<AppLayoutState["currency"]>>;
@ -45,6 +58,7 @@ export interface AppLayoutState {
React.SetStateAction<AppLayoutState["preferredLanguages"]> React.SetStateAction<AppLayoutState["preferredLanguages"]>
>; >;
menuGestures: boolean; menuGestures: boolean;
toggleMenuGestures: () => void;
setMenuGestures: React.Dispatch< setMenuGestures: React.Dispatch<
React.SetStateAction<AppLayoutState["menuGestures"]> React.SetStateAction<AppLayoutState["menuGestures"]>
>; >;
@ -54,38 +68,45 @@ export interface AppLayoutState {
>; >;
} }
/* eslint-disable @typescript-eslint/no-empty-function */ const initialState: RequiredNonNullable<AppLayoutState> = {
const initialState: AppLayoutState = {
subPanelOpen: false, subPanelOpen: false,
toggleSubPanelOpen: () => null,
setSubPanelOpen: () => null,
configPanelOpen: false, configPanelOpen: false,
setConfigPanelOpen: () => null,
toggleConfigPanelOpen: () => null,
searchPanelOpen: false, searchPanelOpen: false,
setSearchPanelOpen: () => null,
toggleSearchPanelOpen: () => null,
mainPanelReduced: false, mainPanelReduced: false,
setMainPanelReduced: () => null,
toggleMainPanelReduced: () => null,
mainPanelOpen: false, mainPanelOpen: false,
toggleMainPanelOpen: () => null,
setMainPanelOpen: () => null,
darkMode: false, darkMode: false,
toggleDarkMode: () => null,
setDarkMode: () => null,
selectedThemeMode: false, selectedThemeMode: false,
toggleSelectedThemeMode: () => null,
setSelectedThemeMode: () => null,
fontSize: 1, fontSize: 1,
setFontSize: () => null,
dyslexic: false, dyslexic: false,
toggleDyslexic: () => null,
setDyslexic: () => null,
currency: "USD", currency: "USD",
setCurrency: () => null,
playerName: "", playerName: "",
setPlayerName: () => null,
preferredLanguages: [], preferredLanguages: [],
setPreferredLanguages: () => null,
menuGestures: true, menuGestures: true,
toggleMenuGestures: () => null,
setMenuGestures: () => null,
libraryItemUserStatus: {}, libraryItemUserStatus: {},
setSubPanelOpen: () => {}, setLibraryItemUserStatus: () => null,
setMainPanelReduced: () => {},
setMainPanelOpen: () => {},
setDarkMode: () => {},
setSelectedThemeMode: () => {},
setConfigPanelOpen: () => {},
setSearchPanelOpen: () => {},
setFontSize: () => {},
setDyslexic: () => {},
setCurrency: () => {},
setPlayerName: () => {},
setPreferredLanguages: () => {},
setMenuGestures: () => {},
setLibraryItemUserStatus: () => {},
}; };
/* eslint-enable @typescript-eslint/no-empty-function */
const AppContext = React.createContext<AppLayoutState>(initialState); const AppContext = React.createContext<AppLayoutState>(initialState);
@ -161,6 +182,44 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
initialState.libraryItemUserStatus initialState.libraryItemUserStatus
); );
function toggleSubPanelOpen() {
setSubPanelOpen((current) => (isDefined(current) ? !current : current));
}
function toggleConfigPanelOpen() {
setConfigPanelOpen((current) => (isDefined(current) ? !current : current));
}
function toggleSearchPanelOpen() {
setSearchPanelOpen((current) => (isDefined(current) ? !current : current));
}
function toggleMainPanelReduced() {
setMainPanelReduced((current) => (isDefined(current) ? !current : current));
}
function toggleMainPanelOpen() {
setMainPanelOpen((current) => (isDefined(current) ? !current : current));
}
function toggleDarkMode() {
setDarkMode((current) => (isDefined(current) ? !current : current));
}
function toggleMenuGestures() {
setMenuGestures((current) => !current);
}
function toggleSelectedThemeMode() {
setSelectedThemeMode((current) =>
isDefined(current) ? !current : current
);
}
function toggleDyslexic() {
setDyslexic((current) => (isDefined(current) ? !current : current));
}
return ( return (
<AppContext.Provider <AppContext.Provider
value={{ value={{
@ -192,6 +251,15 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
setPreferredLanguages, setPreferredLanguages,
setMenuGestures, setMenuGestures,
setLibraryItemUserStatus, setLibraryItemUserStatus,
toggleSubPanelOpen,
toggleConfigPanelOpen,
toggleSearchPanelOpen,
toggleMainPanelReduced,
toggleMainPanelOpen,
toggleDarkMode,
toggleMenuGestures,
toggleSelectedThemeMode,
toggleDyslexic,
}} }}
> >
{props.children} {props.children}

17
src/helpers/component.tsx Normal file
View File

@ -0,0 +1,17 @@
export interface Wrapper {
children: React.ReactNode;
}
export function ConditionalWrapper<T>(props: {
isWrapping: boolean;
children: React.ReactNode;
wrapper: (wrapperProps: T & Wrapper) => JSX.Element;
wrapperProps: T;
}): JSX.Element {
const { isWrapping, children, wrapper: Wrapper, wrapperProps } = props;
return isWrapping ? (
<Wrapper {...wrapperProps}>{children}</Wrapper>
) : (
<>{children}</>
);
}

View File

@ -1,5 +1,6 @@
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettySlug } from "./formatters"; import { prettySlug } from "./formatters";
import { isDefined } from "./others";
import { Content, Immutable } from "./types"; import { Content, Immutable } from "./types";
interface Description { interface Description {
@ -51,7 +52,7 @@ function prettyMarkdown(markdown: string): string {
function prettyChip(items: (string | undefined)[]): string { function prettyChip(items: (string | undefined)[]): string {
return items return items
.filter((item) => item !== undefined) .filter((item) => isDefined(item))
.map((item) => `(${item})`) .map((item) => `(${item})`)
.join(" "); .join(" ");
} }

View File

@ -3,6 +3,7 @@ import { GetLibraryItemsPreviewQuery } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyinlineTitle, prettyDate } from "./formatters"; import { prettyinlineTitle, prettyDate } from "./formatters";
import { convertPrice } from "./numbers"; import { convertPrice } from "./numbers";
import { isDefined } from "./others";
import { Immutable, LibraryItemUserStatus } from "./types"; import { Immutable, LibraryItemUserStatus } from "./types";
type Items = NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"]; type Items = NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
type GroupLibraryItems = Map<string, Immutable<Items>>; type GroupLibraryItems = Map<string, Immutable<Items>>;
@ -172,7 +173,7 @@ export function filterItems(
} }
if ( if (
filterUserStatus !== undefined && isDefined(filterUserStatus) &&
item.id && item.id &&
appLayout.libraryItemUserStatus appLayout.libraryItemUserStatus
) { ) {

View File

@ -64,3 +64,21 @@ 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 isDefined<T>(t: T): t is NonNullable<T> {
return t !== null && t !== undefined;
}
export function isUndefined<T>(t: T | undefined | null): t is undefined | null {
return t === null || t === undefined;
}
export function isDefinedAndNotEmpty(
string: string | undefined | null
): string is string {
return isDefined(string) && string.length > 0;
}
export function filterDefined<T>(t: T[]): NonNullable<T>[] {
return t.filter((item) => isDefined(item)) as NonNullable<T>[];
}

View File

@ -38,6 +38,10 @@ export type Immutable<T> = {
: Immutable<T[K]>; : Immutable<T[K]>;
}; };
export type RequiredNonNullable<T> = Required<{
[P in keyof T]: NonNullable<T[P]>;
}>;
export enum LibraryItemUserStatus { export enum LibraryItemUserStatus {
None = 0, None = 0,
Want = 1, Want = 1,

View File

@ -12,11 +12,11 @@ export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState<boolean>(getMatches(query)); const [matches, setMatches] = useState<boolean>(getMatches(query));
useEffect(() => {
function handleChange() { function handleChange() {
setMatches(getMatches(query)); setMatches(getMatches(query));
} }
useEffect(() => {
const matchMedia = window.matchMedia(query); const matchMedia = window.matchMedia(query);
// Triggered at the first client-side load and if query changes // Triggered at the first client-side load and if query changes
@ -28,7 +28,6 @@ export function useMediaQuery(query: string): boolean {
return () => { return () => {
matchMedia.removeEventListener("change", handleChange); matchMedia.removeEventListener("change", handleChange);
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]); }, [query]);
return matches; return matches;

View File

@ -1,6 +1,7 @@
import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher"; import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher";
import { useAppLayout } from "contexts/AppLayoutContext"; import { useAppLayout } from "contexts/AppLayoutContext";
import { AppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps } from "graphql/getAppStaticProps";
import { isDefined } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@ -17,7 +18,7 @@ function getPreferredLanguage(
availableLanguages: Map<string, number> availableLanguages: Map<string, number>
): number | undefined { ): number | undefined {
for (const locale of preferredLanguages) { for (const locale of preferredLanguages) {
if (locale && availableLanguages.has(locale)) { if (isDefined(locale) && availableLanguages.has(locale)) {
return availableLanguages.get(locale); return availableLanguages.get(locale);
} }
} }
@ -33,39 +34,42 @@ export function useSmartLanguage<T>(
languages, languages,
transform = (item) => item, transform = (item) => item,
} = props; } = props;
const appLayout = useAppLayout(); const { preferredLanguages } = useAppLayout();
const router = useRouter(); const router = useRouter();
const availableLocales: Map<string, number> = useMemo(() => new Map(), []); const availableLocales = useMemo(() => {
const map = new Map<string, number>();
items.map((elem, index) => {
if (isDefined(elem)) {
const result = languageExtractor(elem);
if (isDefined(result)) map.set(result, index);
}
});
return map;
}, [items, languageExtractor]);
const [selectedTranslationIndex, setSelectedTranslationIndex] = useState< const [selectedTranslationIndex, setSelectedTranslationIndex] = useState<
number | undefined number | undefined
>(); >();
const [selectedTranslation, setSelectedTranslation] =
useState<Immutable<T>>();
useEffect(() => {
items.map((elem, index) => {
if (elem !== null && elem !== undefined) {
const result = languageExtractor(elem);
if (result !== undefined) availableLocales.set(result, index);
}
});
}, [availableLocales, items, languageExtractor]);
useEffect(() => { useEffect(() => {
setSelectedTranslationIndex( setSelectedTranslationIndex(
(current) =>
current ??
getPreferredLanguage( getPreferredLanguage(
appLayout.preferredLanguages ?? [router.locale], preferredLanguages ?? [router.locale],
availableLocales availableLocales
) )
); );
}, [appLayout.preferredLanguages, availableLocales, router.locale]); }, [preferredLanguages, availableLocales, router.locale]);
useEffect(() => { const selectedTranslation = useMemo(
if (selectedTranslationIndex !== undefined) { () =>
setSelectedTranslation(transform(items[selectedTranslationIndex])); isDefined(selectedTranslationIndex)
} ? transform(items[selectedTranslationIndex])
}, [items, selectedTranslationIndex, transform]); : undefined,
[items, selectedTranslationIndex, transform]
);
return [ return [
selectedTranslation, selectedTranslation,
@ -74,7 +78,7 @@ export function useSmartLanguage<T>(
languages={languages} languages={languages}
locales={availableLocales} locales={availableLocales}
localesIndex={selectedTranslationIndex} localesIndex={selectedTranslationIndex}
setLocalesIndex={setSelectedTranslationIndex} onLanguageChanged={setSelectedTranslationIndex}
/> />
), ),
]; ];

View File

@ -1,3 +1,4 @@
import { isDefined } from "helpers/others";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useStateWithLocalStorage<T>( export function useStateWithLocalStorage<T>(
@ -10,7 +11,7 @@ export function useStateWithLocalStorage<T>(
useEffect(() => { useEffect(() => {
try { try {
const item = localStorage.getItem(key); const item = localStorage.getItem(key);
if (item !== "undefined" && item !== null) { if (isDefined(item)) {
setValue(JSON.parse(item) as T); setValue(JSON.parse(item) as T);
} else { } else {
setValue(initialValue); setValue(initialValue);
@ -23,7 +24,7 @@ export function useStateWithLocalStorage<T>(
}, [initialValue, key]); }, [initialValue, key]);
useEffect(() => { useEffect(() => {
if (value !== undefined) localStorage.setItem(key, JSON.stringify(value)); if (isDefined(value)) localStorage.setItem(key, JSON.stringify(value));
}, [value, key]); }, [value, key]);
return [value, setValue]; return [value, setValue];

7
src/hooks/useToggle.ts Normal file
View File

@ -0,0 +1,7 @@
import { Dispatch, SetStateAction, useCallback } from "react";
export function useToggle(setState: Dispatch<SetStateAction<boolean>>) {
return useCallback(() => {
setState((current) => !current);
}, []);
}

View File

@ -24,6 +24,7 @@ import { Fragment, useState } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
import { isDefined } from "helpers/others";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
channel: NonNullable< channel: NonNullable<
@ -114,7 +115,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> { ): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk(); const sdk = getReadySdk();
const channel = await sdk.getVideoChannel({ const channel = await sdk.getVideoChannel({
channel: context.params?.uid ? context.params.uid.toString() : "", channel:
context.params && isDefined(context.params.uid)
? context.params.uid.toString()
: "",
}); });
if (!channel.videoChannels?.data[0].attributes) return { notFound: true }; if (!channel.videoChannels?.data[0].attributes) return { notFound: true };
const props: Props = { const props: Props = {

View File

@ -18,6 +18,7 @@ import { GetVideoQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettyShortenNumber } from "helpers/formatters"; import { prettyDate, prettyShortenNumber } from "helpers/formatters";
import { isDefined } from "helpers/others";
import { getVideoFile } from "helpers/videos"; import { getVideoFile } from "helpers/videos";
import { useMediaMobile } from "hooks/useMediaQuery"; import { useMediaMobile } from "hooks/useMediaQuery";
import { import {
@ -190,7 +191,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> { ): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk(); const sdk = getReadySdk();
const videos = await sdk.getVideo({ const videos = await sdk.getVideo({
uid: context.params?.uid ? context.params.uid.toString() : "", uid:
context.params && isDefined(context.params.uid)
? context.params.uid.toString()
: "",
}); });
if (!videos.videos?.data[0]?.attributes) return { notFound: true }; if (!videos.videos?.data[0]?.attributes) return { notFound: true };
const props: Props = { const props: Props = {

View File

@ -36,7 +36,7 @@ import {
GetStaticPathsResult, GetStaticPathsResult,
GetStaticPropsContext, GetStaticPropsContext,
} from "next"; } from "next";
import { Fragment } from "react"; import { Fragment, useMemo } from "react";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
content: ContentWithTranslations; content: ContentWithTranslations;
@ -54,16 +54,23 @@ export default function Content(props: Immutable<Props>): JSX.Element {
useScrollTopOnChange(AnchorIds.ContentPanel, [selectedTranslation]); useScrollTopOnChange(AnchorIds.ContentPanel, [selectedTranslation]);
const previousContent = content.group?.data?.attributes?.contents const { previousContent, nextContent } = useMemo(
() => ({
previousContent: content.group?.data?.attributes?.contents
? getPreviousContent( ? getPreviousContent(
content.group.data.attributes.contents.data, content.group.data.attributes.contents.data,
content.slug content.slug
) )
: undefined; : undefined,
nextContent: content.group?.data?.attributes?.contents
const nextContent = content.group?.data?.attributes?.contents ? getNextContent(
? getNextContent(content.group.data.attributes.contents.data, content.slug) content.group.data.attributes.contents.data,
: undefined; content.slug
)
: undefined,
}),
[content.group, content.slug]
);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>

View File

@ -15,7 +15,7 @@ import { getReadySdk } from "graphql/sdk";
import { prettyinlineTitle, prettySlug } from "helpers/formatters"; import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useState, useMemo } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
@ -50,36 +50,20 @@ export default function Contents(props: Immutable<Props>): JSX.Element {
); );
const [searchName, setSearchName] = useState(defaultFiltersState.searchName); const [searchName, setSearchName] = useState(defaultFiltersState.searchName);
const [effectiveCombineRelatedContent, setEffectiveCombineRelatedContent] = const filteredItems = useMemo(
useState(true); () => filterContents(contents, combineRelatedContent, searchName),
[combineRelatedContent, contents, searchName]
const [filteredItems, setFilteredItems] = useState(
filterContents(contents, combineRelatedContent, searchName)
); );
const [groups, setGroups] = useState<GroupContentItems>( const groups = useMemo(
getGroups(langui, groupingMethod, filteredItems) () => getGroups(langui, groupingMethod, filteredItems),
[langui, groupingMethod, filteredItems]
); );
useEffect(() => { const effectiveCombineRelatedContent = useMemo(
if (searchName.length > 1) { () => (searchName.length > 1 ? false : combineRelatedContent),
setEffectiveCombineRelatedContent(false); [combineRelatedContent, searchName.length]
} else {
setEffectiveCombineRelatedContent(combineRelatedContent);
}
setFilteredItems(
filterContents(contents, effectiveCombineRelatedContent, searchName)
); );
}, [
effectiveCombineRelatedContent,
contents,
searchName,
combineRelatedContent,
]);
useEffect(() => {
setGroups(getGroups(langui, groupingMethod, filteredItems));
}, [langui, groupingMethod, filteredItems]);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>

View File

@ -115,7 +115,6 @@ type ReportLine = {
frontendUrl: string; frontendUrl: string;
}; };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function testingLibraryItem( function testingLibraryItem(
libraryItems: Immutable<Props["libraryItems"]> libraryItems: Immutable<Props["libraryItems"]>
): Report { ): Report {

View File

@ -42,7 +42,7 @@ export default function Editor(props: Immutable<Props>): JSX.Element {
append = `</${wrapper}>`; append = `</${wrapper}>`;
} }
if (addInnerNewLines) { if (addInnerNewLines === true) {
prepend = `${prepend}\n`; prepend = `${prepend}\n`;
append = `\n${append}`; append = `\n${append}`;
} }

View File

@ -35,7 +35,7 @@ import {
} from "helpers/formatters"; } from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img"; import { getAssetURL, ImageQuality } from "helpers/img";
import { convertMmToInch } from "helpers/numbers"; import { convertMmToInch } from "helpers/numbers";
import { sortContent } from "helpers/others"; import { isDefined, sortContent } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange"; import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
@ -196,7 +196,9 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
{item?.urls && item.urls.length ? ( {item?.urls && item.urls.length ? (
<div className="flex flex-row place-items-center gap-3"> <div className="flex flex-row place-items-center gap-3">
<p>{langui.available_at}</p> <p>{langui.available_at}</p>
{item.urls.map((url, index) => ( {item.urls
.filter((url) => url)
.map((url, index) => (
<Fragment key={index}> <Fragment key={index}>
{url?.url && ( {url?.url && (
<Button <Button
@ -342,7 +344,7 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
<p>{convertMmToInch(item.size.height)} in</p> <p>{convertMmToInch(item.size.height)} in</p>
</div> </div>
</div> </div>
{item.size.thickness && ( {isDefined(item.size.thickness) && (
<div <div
className="grid place-items-center gap-x-4 desktop:grid-flow-col className="grid place-items-center gap-x-4 desktop:grid-flow-col
desktop:place-items-start" desktop:place-items-start"
@ -523,7 +525,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> { ): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk(); const sdk = getReadySdk();
const item = await sdk.getLibraryItem({ const item = await sdk.getLibraryItem({
slug: context.params?.slug ? context.params.slug.toString() : "", slug:
context.params && isDefined(context.params.slug)
? context.params.slug.toString()
: "",
language_code: context.locale ?? "en", language_code: context.locale ?? "en",
}); });
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true }; if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };

View File

@ -15,7 +15,7 @@ import { GetLibraryItemScansQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettyinlineTitle, prettySlug } from "helpers/formatters"; import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { sortContent } from "helpers/others"; import { isDefined, sortContent } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox"; import { useLightBox } from "hooks/useLightBox";
import { import {
@ -123,7 +123,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> { ): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk(); const sdk = getReadySdk();
const item = await sdk.getLibraryItemScans({ const item = await sdk.getLibraryItemScans({
slug: context.params?.slug ? context.params.slug.toString() : "", slug:
context.params && isDefined(context.params.slug)
? context.params.slug.toString()
: "",
language_code: context.locale ?? "en", language_code: context.locale ?? "en",
}); });
if (!item.libraryItems?.data[0]?.attributes) return { notFound: true }; if (!item.libraryItems?.data[0]?.attributes) return { notFound: true };

View File

@ -14,7 +14,7 @@ import { getReadySdk } from "graphql/sdk";
import { prettyItemSubType } from "helpers/formatters"; import { prettyItemSubType } from "helpers/formatters";
import { Immutable, LibraryItemUserStatus } from "helpers/types"; import { Immutable, LibraryItemUserStatus } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useState, useMemo } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
@ -31,6 +31,7 @@ import {
import { PreviewCard } from "components/PreviewCard"; import { PreviewCard } from "components/PreviewCard";
import { useMediaHoverable } from "hooks/useMediaQuery"; import { useMediaHoverable } from "hooks/useMediaQuery";
import { ButtonGroup } from "components/Inputs/ButtonGroup"; import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { isDefinedAndNotEmpty, isUndefined } from "helpers/others";
interface Props extends AppStaticProps { interface Props extends AppStaticProps {
items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"]; items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
@ -75,7 +76,8 @@ export default function Library(props: Immutable<Props>): JSX.Element {
LibraryItemUserStatus | undefined LibraryItemUserStatus | undefined
>(defaultFiltersState.filterUserStatus); >(defaultFiltersState.filterUserStatus);
const [filteredItems, setFilteredItems] = useState( const filteredItems = useMemo(
() =>
filterItems( filterItems(
appLayout, appLayout,
libraryItems, libraryItems,
@ -84,46 +86,27 @@ export default function Library(props: Immutable<Props>): JSX.Element {
showPrimaryItems, showPrimaryItems,
showSecondaryItems, showSecondaryItems,
filterUserStatus filterUserStatus
) ),
); [
const [sortedItems, setSortedItem] = useState(
sortBy(groupingMethod, filteredItems, currencies)
);
const [groups, setGroups] = useState(
getGroups(langui, groupingMethod, sortedItems)
);
useEffect(() => {
setFilteredItems(
filterItems(
appLayout, appLayout,
libraryItems,
searchName,
showSubitems,
showPrimaryItems,
showSecondaryItems,
filterUserStatus
)
);
}, [
showSubitems,
libraryItems,
showPrimaryItems,
showSecondaryItems,
searchName,
filterUserStatus, filterUserStatus,
appLayout, libraryItems,
]); searchName,
showPrimaryItems,
showSecondaryItems,
showSubitems,
]
);
useEffect(() => { const sortedItems = useMemo(
setSortedItem(sortBy(sortingMethod, filteredItems, currencies)); () => sortBy(sortingMethod, filteredItems, currencies),
}, [currencies, filteredItems, sortingMethod]); [currencies, filteredItems, sortingMethod]
);
useEffect(() => { const groups = useMemo(
setGroups(getGroups(langui, groupingMethod, sortedItems)); () => getGroups(langui, groupingMethod, sortedItems),
}, [langui, groupingMethod, sortedItems]); [langui, groupingMethod, sortedItems]
);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
@ -227,7 +210,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
<Button <Button
text={"All"} text={"All"}
onClick={() => setFilterUserStatus(undefined)} onClick={() => setFilterUserStatus(undefined)}
active={filterUserStatus === undefined} active={isUndefined(filterUserStatus)}
/> />
</ToolTip> </ToolTip>
</ButtonGroup> </ButtonGroup>
@ -275,7 +258,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
> >
{items.map((item) => ( {items.map((item) => (
<Fragment key={item.id}> <Fragment key={item.id}>
{item.id && item.attributes && ( {isDefinedAndNotEmpty(item.id) && item.attributes && (
<PreviewCard <PreviewCard
href={`/library/${item.attributes.slug}`} href={`/library/${item.attributes.slug}`}
title={item.attributes.title} title={item.attributes.title}

View File

@ -5,6 +5,7 @@ import {
PostStaticProps, PostStaticProps,
} from "graphql/getPostStaticProps"; } from "graphql/getPostStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { isDefined } from "helpers/others";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { import {
GetStaticPathsContext, GetStaticPathsContext,
@ -34,7 +35,10 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
export async function getStaticProps( export async function getStaticProps(
context: GetStaticPropsContext context: GetStaticPropsContext
): Promise<{ notFound: boolean } | { props: Props }> { ): Promise<{ notFound: boolean } | { props: Props }> {
const slug = context.params?.slug ? context.params.slug.toString() : ""; const slug =
context.params && isDefined(context.params.slug)
? context.params.slug.toString()
: "";
return await getPostStaticProps(slug)(context); return await getPostStaticProps(slug)(context);
} }

View File

@ -13,7 +13,7 @@ import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettySlug } from "helpers/formatters"; import { prettyDate, prettySlug } from "helpers/formatters";
import { Immutable } from "helpers/types"; import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import { Icon } from "components/Ico"; import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel"; import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput"; import { TextInput } from "components/Inputs/TextInput";
@ -35,19 +35,16 @@ export default function News(props: Immutable<Props>): JSX.Element {
const hoverable = useMediaHoverable(); const hoverable = useMediaHoverable();
const [searchName, setSearchName] = useState(defaultFiltersState.searchName); const [searchName, setSearchName] = useState(defaultFiltersState.searchName);
const [keepInfoVisible, setKeepInfoVisible] = useState( const [keepInfoVisible, setKeepInfoVisible] = useState(
defaultFiltersState.keepInfoVisible defaultFiltersState.keepInfoVisible
); );
const [filteredItems, setFilteredItems] = useState( const filteredItems = useMemo(
filterItems(posts, searchName) () => filterItems(posts, searchName),
[posts, searchName]
); );
useEffect(() => {
setFilteredItems(filterItems(posts, searchName));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchName]);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
<PanelHeader <PanelHeader
@ -167,7 +164,7 @@ function filterItems(posts: Immutable<Props["posts"]>, searchName: string) {
if ( if (
post.attributes?.translations?.[0]?.title post.attributes?.translations?.[0]?.title
.toLowerCase() .toLowerCase()
.includes(searchName.toLowerCase()) .includes(searchName.toLowerCase()) === true
) { ) {
return true; return true;
} }

View File

@ -14,6 +14,7 @@ import { SubPanel } from "components/Panels/SubPanel";
import DefinitionCard from "components/Wiki/DefinitionCard"; import DefinitionCard from "components/Wiki/DefinitionCard";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { Immutable, WikiPageWithTranslations } from "helpers/types"; import { Immutable, WikiPageWithTranslations } from "helpers/types";
import { useSmartLanguage } from "hooks/useSmartLanguage"; import { useSmartLanguage } from "hooks/useSmartLanguage";
import { import {
@ -81,7 +82,7 @@ export default function WikiPage(props: Immutable<Props>): JSX.Element {
</div> </div>
</div> </div>
</div> </div>
{selectedTranslation.summary && ( {isDefinedAndNotEmpty(selectedTranslation.summary) && (
<div className="mb-6"> <div className="mb-6">
<p className="font-headers text-lg">{langui.summary}</p> <p className="font-headers text-lg">{langui.summary}</p>
<p>{selectedTranslation.summary}</p> <p>{selectedTranslation.summary}</p>
@ -121,7 +122,10 @@ 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 ? context.params.slug.toString() : ""; const slug =
context.params && isDefined(context.params.slug)
? context.params.slug.toString()
: "";
const page = await sdk.getWikiPage({ const page = await sdk.getWikiPage({
language_code: context.locale ?? "en", language_code: context.locale ?? "en",
slug: slug, slug: slug,

View File

@ -12,6 +12,7 @@ import { GetChronologyItemsQuery, GetErasQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps"; import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk"; import { getReadySdk } from "graphql/sdk";
import { prettySlug } from "helpers/formatters"; import { prettySlug } from "helpers/formatters";
import { isDefined } from "helpers/others";
import { GetStaticPropsContext } from "next"; import { GetStaticPropsContext } from "next";
import { Fragment } from "react"; import { Fragment } from "react";
@ -119,7 +120,7 @@ export default function Chronology(props: Props): JSX.Element {
</InsetBox> </InsetBox>
{era.map((items, index) => ( {era.map((items, index) => (
<Fragment key={index}> <Fragment key={index}>
{items[0].attributes?.year && ( {items[0].attributes && isDefined(items[0].attributes.year) && (
<ChronologyYearComponent <ChronologyYearComponent
year={items[0].attributes.year} year={items[0].attributes.year}
items={items} items={items}

View File

@ -12,7 +12,7 @@ import {
ContentPanel, ContentPanel,
ContentPanelWidthSizes, ContentPanelWidthSizes,
} from "components/Panels/ContentPanel"; } from "components/Panels/ContentPanel";
import { Fragment, useEffect, useState } from "react"; import { Fragment, useMemo, useState } from "react";
import { TranslatedPreviewCard } from "components/PreviewCard"; import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine"; import { HorizontalLine } from "components/HorizontalLine";
import { Button } from "components/Inputs/Button"; import { Button } from "components/Inputs/Button";
@ -40,15 +40,11 @@ export default function Wiki(props: Immutable<Props>): JSX.Element {
defaultFiltersState.keepInfoVisible defaultFiltersState.keepInfoVisible
); );
const [filteredPages, setFilteredPages] = useState( const filteredPages = useMemo(
filterPages(pages, searchName) () => filterPages(pages, searchName),
[pages, searchName]
); );
useEffect(() => {
setFilteredPages(filterPages(pages, searchName));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchName]);
const subPanel = ( const subPanel = (
<SubPanel> <SubPanel>
<PanelHeader <PanelHeader
@ -171,7 +167,7 @@ function filterPages(posts: Immutable<Props["pages"]>, searchName: string) {
if ( if (
post.attributes?.translations?.[0]?.title post.attributes?.translations?.[0]?.title
.toLowerCase() .toLowerCase()
.includes(searchName.toLowerCase()) .includes(searchName.toLowerCase()) === true
) { ) {
return true; return true;
} }