Fixed the hooks problems plus other things
This commit is contained in:
parent
c076ec06ad
commit
24a8b43701
|
@ -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",
|
||||
|
|
|
@ -4,14 +4,15 @@ import { UploadImageFragment } from "graphql/generated";
|
|||
import { AppStaticProps } from "graphql/getAppStaticProps";
|
||||
import { cIf, cJoin } from "helpers/className";
|
||||
import { prettyLanguage, prettySlug } from "helpers/formatters";
|
||||
import { getOgImage, ImageQuality, OgImage } from "helpers/img";
|
||||
import { getOgImage, ImageQuality } from "helpers/img";
|
||||
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
|
||||
// import { getClient, Indexes, search, SearchResult } from "helpers/search";
|
||||
import { Immutable } from "helpers/types";
|
||||
import { useMediaMobile } from "hooks/useMediaQuery";
|
||||
import { AnchorIds } from "hooks/useScrollTopOnChange";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useLayoutEffect, useMemo, useState } from "react";
|
||||
import { useSwipeable } from "react-swipeable";
|
||||
import { Ico, Icon } from "./Ico";
|
||||
import { ButtonGroup } from "./Inputs/ButtonGroup";
|
||||
|
@ -31,6 +32,9 @@ interface Props extends AppStaticProps {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
const SENSIBILITY_SWIPE = 1.1;
|
||||
const TITLE_PREFIX = "Accord’s Library";
|
||||
|
||||
export function AppLayout(props: Immutable<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 = "Accord’s Library";
|
||||
const metaImage: OgImage = thumbnail
|
||||
? getOgImage(ImageQuality.Og, thumbnail)
|
||||
: {
|
||||
image: "/default_og.jpg",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: "Accord's Library Logo",
|
||||
};
|
||||
const ogTitle =
|
||||
title ?? navTitle ?? prettySlug(router.asPath.split("/").pop());
|
||||
const { ogTitle, metaTitle } = useMemo(() => {
|
||||
const resultTitle =
|
||||
title ?? navTitle ?? prettySlug(router.asPath.split("/").pop());
|
||||
return {
|
||||
ogTitle: resultTitle,
|
||||
metaTitle: `${TITLE_PREFIX} - ${resultTitle}`,
|
||||
};
|
||||
}, [navTitle, router.asPath, title]);
|
||||
|
||||
const metaTitle = `${titlePrefix} - ${ogTitle}`;
|
||||
const metaDescription = useMemo(
|
||||
() => description ?? langui.default_description ?? "",
|
||||
[description, langui.default_description]
|
||||
);
|
||||
|
||||
const metaDescription = description
|
||||
? description
|
||||
: langui.default_description ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
document.getElementsByTagName("html")[0].style.fontSize = `${
|
||||
(appLayout.fontSize ?? 1) * 100
|
||||
(fontSize ?? 1) * 100
|
||||
}%`;
|
||||
}, [appLayout.fontSize]);
|
||||
}, [fontSize]);
|
||||
|
||||
const defaultPreferredLanguages = useMemo(() => {
|
||||
let list: string[] = [];
|
||||
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
|
||||
if (router.locale === "en") {
|
||||
list = [router.locale];
|
||||
router.locales.map((locale) => {
|
||||
if (locale !== router.locale) list.push(locale);
|
||||
});
|
||||
} else {
|
||||
list = [router.locale, "en"];
|
||||
router.locales.map((locale) => {
|
||||
if (locale !== router.locale && locale !== "en") list.push(locale);
|
||||
});
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}, [router.locale, router.locales]);
|
||||
|
||||
const currencyOptions = useMemo(() => {
|
||||
const list: string[] = [];
|
||||
currencies.map((currentCurrency) => {
|
||||
if (
|
||||
currentCurrency.attributes &&
|
||||
isDefinedAndNotEmpty(currentCurrency.attributes.code)
|
||||
)
|
||||
list.push(currentCurrency.attributes.code);
|
||||
});
|
||||
return list;
|
||||
}, [currencies]);
|
||||
|
||||
const currencyOptions: string[] = [];
|
||||
currencies.map((currency) => {
|
||||
if (currency.attributes?.code)
|
||||
currencyOptions.push(currency.attributes.code);
|
||||
});
|
||||
const [currencySelect, setCurrencySelect] = useState<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]);
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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) ? (
|
||||
<>
|
||||
“{compProps.children}”
|
||||
<cite>— {compProps.cite}</cite>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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’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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}</>
|
||||
);
|
||||
}
|
|
@ -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(" ");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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>[];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
),
|
||||
];
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Dispatch, SetStateAction, useCallback } from "react";
|
||||
|
||||
export function useToggle(setState: Dispatch<SetStateAction<boolean>>) {
|
||||
return useCallback(() => {
|
||||
setState((current) => !current);
|
||||
}, []);
|
||||
}
|
|
@ -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 = {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -115,7 +115,6 @@ type ReportLine = {
|
|||
frontendUrl: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function testingLibraryItem(
|
||||
libraryItems: Immutable<Props["libraryItems"]>
|
||||
): Report {
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue