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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { slugify } from "helpers/formatters";
import { Immutable } from "helpers/types";
import { useRouter } from "next/router";
import { Fragment } from "react";
import { Fragment, useMemo } from "react";
import { preprocessMarkDawn } from "./Markdawn";
interface Props {
@ -11,11 +11,15 @@ interface Props {
export function TOC(props: Immutable<Props>): JSX.Element {
const { text, title } = props;
const toc = getTocFromMarkdawn(preprocessMarkDawn(text), title);
const router = useRouter();
const toc = useMemo(
() => getTocFromMarkdawn(preprocessMarkDawn(text), title),
[text, title]
);
return (
<>
{/* TODO: add to LANGUI */}
<h3 className="text-xl">Table of content</h3>
<div className="max-w-[14.5rem] 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 { ToolTip } from "components/ToolTip";
import { cJoin, cIf } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { Immutable } from "helpers/types";
import { useRouter } from "next/router";
import { MouseEventHandler } from "react";
import { MouseEventHandler, useMemo } from "react";
interface Props {
url: string;
@ -16,29 +17,35 @@ interface Props {
}
export function NavOption(props: Immutable<Props>): JSX.Element {
const { url, icon, title, subtitle, border, reduced, onClick } = props;
const router = useRouter();
const isActive = router.asPath.startsWith(props.url);
const isActive = useMemo(
() => router.asPath.startsWith(url),
[url, router.asPath]
);
return (
<ToolTip
content={
<div>
<h3 className="text-2xl">{props.title}</h3>
{props.subtitle && <p className="col-start-2">{props.subtitle}</p>}
<h3 className="text-2xl">{title}</h3>
{isDefinedAndNotEmpty(subtitle) && (
<p className="col-start-2">{subtitle}</p>
)}
</div>
}
placement="right"
className="text-left"
disabled={!props.reduced}
disabled={reduced === false}
>
<div
onClick={(event) => {
if (props.onClick) props.onClick(event);
if (props.url) {
if (props.url.startsWith("#")) {
router.replace(props.url);
if (onClick) onClick(event);
if (url) {
if (url.startsWith("#")) {
router.replace(url);
} 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]
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`,
cIf(props.icon, "text-left", "text-center"),
cIf(icon, "text-left", "text-center"),
cIf(
props.border,
border,
"outline outline-2 outline-offset-[-2px] outline-mid hover:outline-[transparent]"
),
cIf(isActive, "bg-mid shadow-inner-sm shadow-shade")
)}
>
{props.icon && (
<Ico icon={props.icon} className="mt-[-.1em] !text-2xl" />
)}
{icon && <Ico icon={icon} className="mt-[-.1em] !text-2xl" />}
{!props.reduced && (
{reduced === false && (
<div>
<h3 className="text-2xl">{props.title}</h3>
{props.subtitle && <p className="col-start-2">{props.subtitle}</p>}
<h3 className="text-2xl">{title}</h3>
{isDefinedAndNotEmpty(subtitle) && <p className="col-start-2">{subtitle}</p>}
</div>
)}
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,55 +19,6 @@ interface Props {
export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element {
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) {
return (
<div
@ -168,3 +119,52 @@ export function ChronologyItemComponent(props: Immutable<Props>): JSX.Element {
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 { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage";
import React, { ReactNode, useContext, useState } from "react";
export interface AppLayoutState {
subPanelOpen: boolean | undefined;
toggleSubPanelOpen: () => void;
setSubPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["subPanelOpen"]>
>;
configPanelOpen: boolean | undefined;
toggleConfigPanelOpen: () => void;
setConfigPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["configPanelOpen"]>
>;
searchPanelOpen: boolean | undefined;
toggleSearchPanelOpen: () => void;
setSearchPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["searchPanelOpen"]>
>;
mainPanelReduced: boolean | undefined;
toggleMainPanelReduced: () => void;
setMainPanelReduced: React.Dispatch<
React.SetStateAction<AppLayoutState["mainPanelReduced"]>
>;
mainPanelOpen: boolean | undefined;
toggleMainPanelOpen: () => void;
setMainPanelOpen: React.Dispatch<
React.SetStateAction<AppLayoutState["mainPanelOpen"]>
>;
darkMode: boolean | undefined;
toggleDarkMode: () => void;
setDarkMode: React.Dispatch<React.SetStateAction<AppLayoutState["darkMode"]>>;
selectedThemeMode: boolean | undefined;
toggleSelectedThemeMode: () => void;
setSelectedThemeMode: React.Dispatch<
React.SetStateAction<AppLayoutState["selectedThemeMode"]>
>;
fontSize: number | undefined;
setFontSize: React.Dispatch<React.SetStateAction<AppLayoutState["fontSize"]>>;
dyslexic: boolean | undefined;
toggleDyslexic: () => void;
setDyslexic: React.Dispatch<React.SetStateAction<AppLayoutState["dyslexic"]>>;
currency: string | undefined;
setCurrency: React.Dispatch<React.SetStateAction<AppLayoutState["currency"]>>;
@ -45,6 +58,7 @@ export interface AppLayoutState {
React.SetStateAction<AppLayoutState["preferredLanguages"]>
>;
menuGestures: boolean;
toggleMenuGestures: () => void;
setMenuGestures: React.Dispatch<
React.SetStateAction<AppLayoutState["menuGestures"]>
>;
@ -54,38 +68,45 @@ export interface AppLayoutState {
>;
}
/* eslint-disable @typescript-eslint/no-empty-function */
const initialState: AppLayoutState = {
const initialState: RequiredNonNullable<AppLayoutState> = {
subPanelOpen: false,
toggleSubPanelOpen: () => null,
setSubPanelOpen: () => null,
configPanelOpen: false,
setConfigPanelOpen: () => null,
toggleConfigPanelOpen: () => null,
searchPanelOpen: false,
setSearchPanelOpen: () => null,
toggleSearchPanelOpen: () => null,
mainPanelReduced: false,
setMainPanelReduced: () => null,
toggleMainPanelReduced: () => null,
mainPanelOpen: false,
toggleMainPanelOpen: () => null,
setMainPanelOpen: () => null,
darkMode: false,
toggleDarkMode: () => null,
setDarkMode: () => null,
selectedThemeMode: false,
toggleSelectedThemeMode: () => null,
setSelectedThemeMode: () => null,
fontSize: 1,
setFontSize: () => null,
dyslexic: false,
toggleDyslexic: () => null,
setDyslexic: () => null,
currency: "USD",
setCurrency: () => null,
playerName: "",
setPlayerName: () => null,
preferredLanguages: [],
setPreferredLanguages: () => null,
menuGestures: true,
toggleMenuGestures: () => null,
setMenuGestures: () => null,
libraryItemUserStatus: {},
setSubPanelOpen: () => {},
setMainPanelReduced: () => {},
setMainPanelOpen: () => {},
setDarkMode: () => {},
setSelectedThemeMode: () => {},
setConfigPanelOpen: () => {},
setSearchPanelOpen: () => {},
setFontSize: () => {},
setDyslexic: () => {},
setCurrency: () => {},
setPlayerName: () => {},
setPreferredLanguages: () => {},
setMenuGestures: () => {},
setLibraryItemUserStatus: () => {},
setLibraryItemUserStatus: () => null,
};
/* eslint-enable @typescript-eslint/no-empty-function */
const AppContext = React.createContext<AppLayoutState>(initialState);
@ -161,6 +182,44 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
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 (
<AppContext.Provider
value={{
@ -192,6 +251,15 @@ export function AppContextProvider(props: Immutable<Props>): JSX.Element {
setPreferredLanguages,
setMenuGestures,
setLibraryItemUserStatus,
toggleSubPanelOpen,
toggleConfigPanelOpen,
toggleSearchPanelOpen,
toggleMainPanelReduced,
toggleMainPanelOpen,
toggleDarkMode,
toggleMenuGestures,
toggleSelectedThemeMode,
toggleDyslexic,
}}
>
{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 { prettySlug } from "./formatters";
import { isDefined } from "./others";
import { Content, Immutable } from "./types";
interface Description {
@ -51,7 +52,7 @@ function prettyMarkdown(markdown: string): string {
function prettyChip(items: (string | undefined)[]): string {
return items
.filter((item) => item !== undefined)
.filter((item) => isDefined(item))
.map((item) => `(${item})`)
.join(" ");
}

View File

@ -3,6 +3,7 @@ import { GetLibraryItemsPreviewQuery } from "graphql/generated";
import { AppStaticProps } from "graphql/getAppStaticProps";
import { prettyinlineTitle, prettyDate } from "./formatters";
import { convertPrice } from "./numbers";
import { isDefined } from "./others";
import { Immutable, LibraryItemUserStatus } from "./types";
type Items = NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
type GroupLibraryItems = Map<string, Immutable<Items>>;
@ -172,7 +173,7 @@ export function filterItems(
}
if (
filterUserStatus !== undefined &&
isDefined(filterUserStatus) &&
item.id &&
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]);
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]>;
};
export type RequiredNonNullable<T> = Required<{
[P in keyof T]: NonNullable<T[P]>;
}>;
export enum LibraryItemUserStatus {
None = 0,
Want = 1,

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { isDefined } from "helpers/others";
import { useEffect, useState } from "react";
export function useStateWithLocalStorage<T>(
@ -10,7 +11,7 @@ export function useStateWithLocalStorage<T>(
useEffect(() => {
try {
const item = localStorage.getItem(key);
if (item !== "undefined" && item !== null) {
if (isDefined(item)) {
setValue(JSON.parse(item) as T);
} else {
setValue(initialValue);
@ -23,7 +24,7 @@ export function useStateWithLocalStorage<T>(
}, [initialValue, key]);
useEffect(() => {
if (value !== undefined) localStorage.setItem(key, JSON.stringify(value));
if (isDefined(value)) localStorage.setItem(key, JSON.stringify(value));
}, [value, key]);
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 { useMediaHoverable } from "hooks/useMediaQuery";
import { WithLabel } from "components/Inputs/WithLabel";
import { isDefined } from "helpers/others";
interface Props extends AppStaticProps {
channel: NonNullable<
@ -114,7 +115,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
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 };
const props: Props = {

View File

@ -18,6 +18,7 @@ import { GetVideoQuery } from "graphql/generated";
import { AppStaticProps, getAppStaticProps } from "graphql/getAppStaticProps";
import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
import { isDefined } from "helpers/others";
import { getVideoFile } from "helpers/videos";
import { useMediaMobile } from "hooks/useMediaQuery";
import {
@ -190,7 +191,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
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 };
const props: Props = {

View File

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

View File

@ -15,7 +15,7 @@ import { getReadySdk } from "graphql/sdk";
import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { Immutable } from "helpers/types";
import { GetStaticPropsContext } from "next";
import { Fragment, useEffect, useState } from "react";
import { Fragment, useState, useMemo } from "react";
import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel";
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 [effectiveCombineRelatedContent, setEffectiveCombineRelatedContent] =
useState(true);
const [filteredItems, setFilteredItems] = useState(
filterContents(contents, combineRelatedContent, searchName)
const filteredItems = useMemo(
() => filterContents(contents, combineRelatedContent, searchName),
[combineRelatedContent, contents, searchName]
);
const [groups, setGroups] = useState<GroupContentItems>(
getGroups(langui, groupingMethod, filteredItems)
const groups = useMemo(
() => getGroups(langui, groupingMethod, filteredItems),
[langui, groupingMethod, filteredItems]
);
useEffect(() => {
if (searchName.length > 1) {
setEffectiveCombineRelatedContent(false);
} else {
setEffectiveCombineRelatedContent(combineRelatedContent);
}
setFilteredItems(
filterContents(contents, effectiveCombineRelatedContent, searchName)
);
}, [
effectiveCombineRelatedContent,
contents,
searchName,
combineRelatedContent,
]);
useEffect(() => {
setGroups(getGroups(langui, groupingMethod, filteredItems));
}, [langui, groupingMethod, filteredItems]);
const effectiveCombineRelatedContent = useMemo(
() => (searchName.length > 1 ? false : combineRelatedContent),
[combineRelatedContent, searchName.length]
);
const subPanel = (
<SubPanel>

View File

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

View File

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

View File

@ -35,7 +35,7 @@ import {
} from "helpers/formatters";
import { getAssetURL, ImageQuality } from "helpers/img";
import { convertMmToInch } from "helpers/numbers";
import { sortContent } from "helpers/others";
import { isDefined, sortContent } from "helpers/others";
import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox";
import { AnchorIds, useScrollTopOnChange } from "hooks/useScrollTopOnChange";
@ -196,17 +196,19 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
{item?.urls && item.urls.length ? (
<div className="flex flex-row place-items-center gap-3">
<p>{langui.available_at}</p>
{item.urls.map((url, index) => (
<Fragment key={index}>
{url?.url && (
<Button
href={url.url}
target={"_blank"}
text={prettyURL(url.url)}
/>
)}
</Fragment>
))}
{item.urls
.filter((url) => url)
.map((url, index) => (
<Fragment key={index}>
{url?.url && (
<Button
href={url.url}
target={"_blank"}
text={prettyURL(url.url)}
/>
)}
</Fragment>
))}
</div>
) : (
<p>{langui.item_not_available}</p>
@ -342,7 +344,7 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
<p>{convertMmToInch(item.size.height)} in</p>
</div>
</div>
{item.size.thickness && (
{isDefined(item.size.thickness) && (
<div
className="grid place-items-center gap-x-4 desktop:grid-flow-col
desktop:place-items-start"
@ -523,7 +525,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
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",
});
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 { getReadySdk } from "graphql/sdk";
import { prettyinlineTitle, prettySlug } from "helpers/formatters";
import { sortContent } from "helpers/others";
import { isDefined, sortContent } from "helpers/others";
import { Immutable } from "helpers/types";
import { useLightBox } from "hooks/useLightBox";
import {
@ -123,7 +123,10 @@ export async function getStaticProps(
): Promise<{ notFound: boolean } | { props: Props }> {
const sdk = getReadySdk();
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",
});
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 { Immutable, LibraryItemUserStatus } from "helpers/types";
import { GetStaticPropsContext } from "next";
import { Fragment, useEffect, useState } from "react";
import { Fragment, useState, useMemo } from "react";
import { Icon } from "components/Ico";
import { WithLabel } from "components/Inputs/WithLabel";
import { TextInput } from "components/Inputs/TextInput";
@ -31,6 +31,7 @@ import {
import { PreviewCard } from "components/PreviewCard";
import { useMediaHoverable } from "hooks/useMediaQuery";
import { ButtonGroup } from "components/Inputs/ButtonGroup";
import { isDefinedAndNotEmpty, isUndefined } from "helpers/others";
interface Props extends AppStaticProps {
items: NonNullable<GetLibraryItemsPreviewQuery["libraryItems"]>["data"];
@ -75,28 +76,8 @@ export default function Library(props: Immutable<Props>): JSX.Element {
LibraryItemUserStatus | undefined
>(defaultFiltersState.filterUserStatus);
const [filteredItems, setFilteredItems] = useState(
filterItems(
appLayout,
libraryItems,
searchName,
showSubitems,
showPrimaryItems,
showSecondaryItems,
filterUserStatus
)
);
const [sortedItems, setSortedItem] = useState(
sortBy(groupingMethod, filteredItems, currencies)
);
const [groups, setGroups] = useState(
getGroups(langui, groupingMethod, sortedItems)
);
useEffect(() => {
setFilteredItems(
const filteredItems = useMemo(
() =>
filterItems(
appLayout,
libraryItems,
@ -105,25 +86,27 @@ export default function Library(props: Immutable<Props>): JSX.Element {
showPrimaryItems,
showSecondaryItems,
filterUserStatus
)
);
}, [
showSubitems,
libraryItems,
showPrimaryItems,
showSecondaryItems,
searchName,
filterUserStatus,
appLayout,
]);
),
[
appLayout,
filterUserStatus,
libraryItems,
searchName,
showPrimaryItems,
showSecondaryItems,
showSubitems,
]
);
useEffect(() => {
setSortedItem(sortBy(sortingMethod, filteredItems, currencies));
}, [currencies, filteredItems, sortingMethod]);
const sortedItems = useMemo(
() => sortBy(sortingMethod, filteredItems, currencies),
[currencies, filteredItems, sortingMethod]
);
useEffect(() => {
setGroups(getGroups(langui, groupingMethod, sortedItems));
}, [langui, groupingMethod, sortedItems]);
const groups = useMemo(
() => getGroups(langui, groupingMethod, sortedItems),
[langui, groupingMethod, sortedItems]
);
const subPanel = (
<SubPanel>
@ -227,7 +210,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
<Button
text={"All"}
onClick={() => setFilterUserStatus(undefined)}
active={filterUserStatus === undefined}
active={isUndefined(filterUserStatus)}
/>
</ToolTip>
</ButtonGroup>
@ -275,7 +258,7 @@ export default function Library(props: Immutable<Props>): JSX.Element {
>
{items.map((item) => (
<Fragment key={item.id}>
{item.id && item.attributes && (
{isDefinedAndNotEmpty(item.id) && item.attributes && (
<PreviewCard
href={`/library/${item.attributes.slug}`}
title={item.attributes.title}

View File

@ -5,6 +5,7 @@ import {
PostStaticProps,
} from "graphql/getPostStaticProps";
import { getReadySdk } from "graphql/sdk";
import { isDefined } from "helpers/others";
import { Immutable } from "helpers/types";
import {
GetStaticPathsContext,
@ -34,7 +35,10 @@ export default function LibrarySlug(props: Immutable<Props>): JSX.Element {
export async function getStaticProps(
context: GetStaticPropsContext
): 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);
}

View File

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

View File

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

View File

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

View File

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