Updating to Next.js 13 #64

Closed
DrMint wants to merge 6 commits from next13 into main
72 changed files with 1554 additions and 2381 deletions
Showing only changes of commit 2aea7fa040 - Show all commits

View File

@ -39,7 +39,8 @@
- Support for Container Queries (media queries at the element level)
- The website has a three-column layout, which turns into one-column + 2 toggleable side-menus if the screen is too narrow.
- Check out our [Design System Showcase](https://accords-library.com/dev/showcase/design-system)
- State Management: [React Context](https://reactjs.org/docs/context.html)
- State Management: [Jōtai](https://jotai.org/)
- Jōtai is a small-weighted library for atomic state management
- Persistent app state using LocalStorage and SessionStorage
- Accessibility
- Gestures using [react-swipeable](https://www.npmjs.com/package/react-swipeable)
@ -56,7 +57,7 @@
- The website is built before running in production
- Performances are great, and possibility to deploy the app on a CDN
- On-Demand ISR to continuously update the website when new content is added or existing content is modified/deleted
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every page.
- UI localizations are downloaded separetely into the `public/local-data` to avoid fetching the same static props for every page.
- SEO
- Good defaults for the metadata and OpenGraph properties
- Each page can provide a custom thumbnail, title, description to be used
@ -66,6 +67,7 @@
- Each warning/error comes with a front-end link to the incriminating element, as well as a link to the CMS to fix it
- Check for completeness, conformity, and integrity
- Code quality and style
- React Strict Mode
- [Eslint](https://www.npmjs.com/package/eslint) with [eslint-plugin-import](https://www.npmjs.com/package/eslint-plugin-import), [typescript-eslint](https://www.npmjs.com/package/@typescript-eslint/eslint-plugin)
- [Prettier](https://www.npmjs.com/package/prettier) with [prettier-plugin-tailwindcss](https://www.npmjs.com/package/prettier-plugin-tailwindcss)

1902
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@
"@tippyjs/react": "^4.2.6",
"autoprefixer": "^10.4.13",
"graphql-request": "^5.0.0",
"jotai": "^1.9.0",
"markdown-to-jsx": "^7.1.7",
"next": "^13.0.1",
"nodemailer": "^6.8.0",

View File

@ -1,7 +1,8 @@
import { Ico, Icon } from "./Ico";
import { ToolTip } from "./ToolTip";
import { cJoin } from "helpers/className";
import { useLocalData } from "contexts/LocalDataContext";
import { useAtomGetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";
/*
*
@ -16,7 +17,7 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const AnchorShare = ({ id, className }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<ToolTip content={langui.copy_anchor_link} trigger="mouseenter" className="text-sm">
<ToolTip content={langui.anchor_link_copied} trigger="click" className="text-sm">

View File

@ -7,11 +7,10 @@ import { MainPanel } from "./Panels/MainPanel";
import { SafariPopup } from "./Panels/SafariPopup";
import { isDefined, isUndefined } from "helpers/others";
import { cIf, cJoin } from "helpers/className";
import { useAppLayout } from "contexts/AppLayoutContext";
import { OpenGraph, TITLE_PREFIX, TITLE_SEPARATOR } from "helpers/openGraph";
import { Ids } from "types/ids";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter, useAtomPair } from "helpers/atoms";
/*
*
@ -45,39 +44,34 @@ export const AppLayout = ({
subPanelIcon = Icon.Tune,
contentPanelScroolbar = true,
}: Props): JSX.Element => {
const {
mainPanelOpen,
mainPanelReduced,
menuGestures,
subPanelOpen,
setMainPanelOpen,
setSubPanelOpen,
toggleMainPanelOpen,
toggleSubPanelOpen,
} = useAppLayout();
const isMainPanelReduced = useAtomGetter(atoms.layout.mainPanelReduced);
const [isSubPanelOpened, setSubPanelOpened] = useAtomPair(atoms.layout.subPanelOpened);
const [isMainPanelOpened, setMainPanelOpened] = useAtomPair(atoms.layout.mainPanelOpened);
const isMenuGesturesEnabled = useAtomGetter(atoms.layout.menuGesturesEnabled);
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const { is1ColumnLayout, isScreenAtLeastXs } = useContainerQueries();
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const isScreenAtLeastXs = useAtomGetter(atoms.containerQueries.isScreenAtLeastXs);
const handlers = useSwipeable({
onSwipedLeft: (SwipeEventData) => {
if (menuGestures) {
if (isMenuGesturesEnabled) {
if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
if (mainPanelOpen) {
setMainPanelOpen(false);
if (isMainPanelOpened) {
setMainPanelOpened(false);
} else if (isDefined(subPanel) && isDefined(contentPanel)) {
setSubPanelOpen(true);
setSubPanelOpened(true);
}
}
},
onSwipedRight: (SwipeEventData) => {
if (menuGestures) {
if (isMenuGesturesEnabled) {
if (SwipeEventData.velocity < SENSIBILITY_SWIPE) return;
if (subPanelOpen) {
setSubPanelOpen(false);
if (isSubPanelOpened) {
setSubPanelOpened(false);
} else {
setMainPanelOpen(true);
setMainPanelOpened(true);
}
}
},
@ -99,7 +93,7 @@ export const AppLayout = ({
style={{
gridTemplateColumns: is1ColumnLayout
? "1fr"
: `${mainPanelReduced ? layout.mainMenuReduced : layout.mainMenu}rem ${
: `${isMainPanelReduced ? layout.mainMenuReduced : layout.mainMenu}rem ${
isDefined(subPanel) ? layout.subMenu : 0
}rem 1fr`,
}}>
@ -128,7 +122,7 @@ export const AppLayout = ({
`absolute inset-0 transition-filter duration-500
[grid-area:content]`,
cIf(
(mainPanelOpen || subPanelOpen) && is1ColumnLayout,
(isMainPanelOpened || isSubPanelOpened) && is1ColumnLayout,
"z-10 backdrop-blur",
"pointer-events-none touch-none"
)
@ -136,11 +130,15 @@ export const AppLayout = ({
<div
className={cJoin(
"absolute inset-0 bg-shade transition-opacity duration-500",
cIf((mainPanelOpen || subPanelOpen) && is1ColumnLayout, "opacity-60", "opacity-0")
cIf(
(isMainPanelOpened || isSubPanelOpened) && is1ColumnLayout,
"opacity-60",
"opacity-0"
)
)}
onClick={() => {
setMainPanelOpen(false);
setSubPanelOpen(false);
setMainPanelOpened(false);
setSubPanelOpened(false);
}}
/>
</div>
@ -175,7 +173,7 @@ export const AppLayout = ({
"[grid-area:sub]"
),
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)] border-l"),
cIf(is1ColumnLayout && !subPanelOpen && !turnSubIntoContent, "translate-x-[100vw]"),
cIf(is1ColumnLayout && !isSubPanelOpened && !turnSubIntoContent, "translate-x-[100vw]"),
cIf(is1ColumnLayout && turnSubIntoContent, "w-full border-l-0")
)}>
{subPanel}
@ -189,7 +187,7 @@ export const AppLayout = ({
transition-transform duration-300 scrollbar-none texture-paper-dots`,
cIf(is1ColumnLayout, "justify-self-start [grid-area:content]", "[grid-area:main]"),
cIf(is1ColumnLayout && isScreenAtLeastXs, "w-[min(30rem,90%)]"),
cIf(!mainPanelOpen && is1ColumnLayout, "-translate-x-full")
cIf(!isMainPanelOpened && is1ColumnLayout, "-translate-x-full")
)}>
<MainPanel />
</div>
@ -202,11 +200,11 @@ export const AppLayout = ({
cIf(!is1ColumnLayout, "hidden")
)}>
<Ico
icon={mainPanelOpen ? Icon.Close : Icon.Menu}
icon={isMainPanelOpened ? Icon.Close : Icon.Menu}
className="cursor-pointer !text-2xl"
onClick={() => {
toggleMainPanelOpen();
setSubPanelOpen(false);
setMainPanelOpened((current) => !current);
setSubPanelOpened(false);
}}
/>
<p
@ -220,11 +218,11 @@ export const AppLayout = ({
</p>
{isDefined(subPanel) && !turnSubIntoContent && (
<Ico
icon={subPanelOpen ? Icon.Close : subPanelIcon}
icon={isSubPanelOpened ? Icon.Close : subPanelIcon}
className="cursor-pointer !text-2xl"
onClick={() => {
toggleSubPanelOpen();
setMainPanelOpen(false);
setSubPanelOpened((current) => !current);
setMainPanelOpened(false);
}}
/>
)}

View File

@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/router";
import { cJoin, cIf } from "helpers/className";
import { useTerminalContext } from "contexts/TerminalContext";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { useUserSettings } from "contexts/UserSettingsContext";
import { atoms } from "contexts/atoms";
import { useAtomSetter, useAtomPair } from "helpers/atoms";
/*
*
@ -31,9 +31,11 @@ export const Terminal = ({
content,
}: Props): JSX.Element => {
const [childrenPaths, setChildrenPaths] = useState(propsChildrenPaths);
const { darkMode, setPlayerName } = useUserSettings();
const { previousCommands, previousLines, setPreviousCommands, setPreviousLines } =
useTerminalContext();
const setPlayerName = useAtomSetter(atoms.settings.playerName);
const [previousCommands, setPreviousCommands] = useAtomPair(atoms.terminal.previousCommands);
const [previousLines, setPreviousLines] = useAtomPair(atoms.terminal.previousLines);
const [line, setLine] = useState("");
const [displayCurrentLine, setDisplayCurrentLine] = useState(true);
const [previousCommandIndex, setPreviousCommandIndex] = useState(0);
@ -232,11 +234,7 @@ export const Terminal = ({
}, [line]);
return (
<div
className={cJoin(
"h-screen overflow-hidden bg-light set-theme-font-standard",
cIf(darkMode, "set-theme-dark", "set-theme-light")
)}>
<div className={cJoin("h-screen overflow-hidden bg-light set-theme-font-standard")}>
<div
ref={terminalWindowRef}
className="h-full overflow-scroll scroll-auto p-6 scrollbar-none">

View File

@ -1,4 +1,5 @@
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { cIf, cJoin } from "helpers/className";
/*
@ -25,7 +26,7 @@ export const ContentPanel = ({
children,
className,
}: Props): JSX.Element => {
const { isContentPanelAtLeast3xl } = useContainerQueries();
const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl);
return (
<div className="grid h-full">
<main

View File

@ -1,7 +1,8 @@
import { useEffect } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useAppLayout } from "contexts/AppLayoutContext";
import { cIf, cJoin } from "helpers/className";
import { atoms } from "contexts/atoms";
import { useAtomSetter } from "helpers/atoms";
/*
*
@ -27,13 +28,13 @@ export const Popup = ({
hideBackground = false,
padding = true,
}: Props): JSX.Element => {
const { setMenuGestures } = useAppLayout();
const setMenuGesturesEnabled = useAtomSetter(atoms.layout.menuGesturesEnabled);
useHotkeys("escape", () => onCloseRequest?.(), {}, [onCloseRequest]);
useEffect(() => {
setMenuGestures(!isVisible);
}, [setMenuGestures, isVisible]);
setMenuGesturesEnabled(!isVisible);
}, [isVisible, setMenuGesturesEnabled]);
return (
<div

View File

@ -1,4 +1,5 @@
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { cIf, cJoin } from "helpers/className";
/*
@ -13,7 +14,7 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const SubPanel = ({ children }: Props): JSX.Element => {
const { isSubPanelAtLeastXs } = useContainerQueries();
const isSubPanelAtLeastXs = useAtomGetter(atoms.containerQueries.isSubPanelAtLeastXs);
return (
<div
className={cJoin(

View File

@ -23,8 +23,8 @@ export const UpPressable = ({
href={href}
onFocusChanged={setFocused}
className={cJoin(
`overflow-hidden rounded-md drop-shadow-lg transition-all duration-300 shadow-shade`,
cIf(!noBackground, "bg-highlight"),
`drop-shadow-lg transition-all duration-300 shadow-shade`,
cIf(!noBackground, "overflow-hidden rounded-md bg-highlight"),
cIf(
disabled,
"cursor-not-allowed opacity-50 grayscale",

View File

@ -6,7 +6,8 @@ import { cJoin } from "helpers/className";
import { prettyLanguage } from "helpers/formatters";
import { iterateMap } from "helpers/others";
import { sendAnalytics } from "helpers/analytics";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -32,7 +33,7 @@ export const LanguageSwitcher = ({
onLanguageChanged,
showBadge = true,
}: Props): JSX.Element => {
const { languages } = useLocalData();
const languages = useAtomGetter(atoms.localData.languages);
return (
<ToolTip
content={

View File

@ -1,5 +1,5 @@
import router from "next/router";
import { PointerEventHandler, useState } from "react";
import { MouseEventHandler, useState } from "react";
import { isDefined } from "helpers/others";
interface Props {
@ -8,7 +8,7 @@ interface Props {
allowNewTab?: boolean;
alwaysNewTab?: boolean;
children: React.ReactNode;
onClick?: PointerEventHandler<HTMLDivElement>;
onClick?: MouseEventHandler<HTMLDivElement>;
onFocusChanged?: (isFocused: boolean) => void;
disabled?: boolean;
}
@ -28,19 +28,19 @@ export const Link = ({
return (
<div
className={className}
onPointerLeave={() => {
onMouseLeave={() => {
setIsValidClick(false);
onFocusChanged?.(false);
}}
onContextMenu={(event) => event.preventDefault()}
onPointerDown={(event) => {
onMouseDown={(event) => {
if (!disabled) {
event.preventDefault();
onFocusChanged?.(true);
setIsValidClick(true);
}
}}
onPointerUp={(event) => {
onMouseUp={(event) => {
onFocusChanged?.(false);
if (!disabled) {
if (isDefined(onClick)) {

View File

@ -3,8 +3,9 @@ import { Button } from "components/Inputs/Button";
import { ToolTip } from "components/ToolTip";
import { LibraryItemUserStatus } from "types/types";
import { cIf, cJoin } from "helpers/className";
import { useLocalData } from "contexts/LocalDataContext";
import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -20,7 +21,7 @@ interface Props {
export const PreviewCardCTAs = ({ id, expand = false }: Props): JSX.Element => {
const { libraryItemUserStatus, setLibraryItemUserStatus } = useLibraryItemUserStatus();
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<div

View File

@ -10,7 +10,8 @@ import { Ids } from "types/ids";
import { UploadImageFragment } from "graphql/generated";
import { ImageQuality } from "helpers/img";
import { isDefined } from "helpers/others";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { useAtomGetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";
interface Props {
onCloseRequest: () => void;
@ -138,7 +139,7 @@ const ControlButtons = ({
onCloseRequest,
toggleFullscreen,
}: ControlButtonsProps): JSX.Element => {
const { is1ColumnLayout } = useContainerQueries();
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const PreviousButton = () => (
<Button

View File

@ -13,10 +13,8 @@ import { AnchorShare } from "components/AnchorShare";
import { useIntersectionList } from "hooks/useIntersectionList";
import { Ico, Icon } from "components/Ico";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { useUserSettings } from "contexts/UserSettingsContext";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { useLightBox } from "contexts/LightBoxContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -31,10 +29,10 @@ interface MarkdawnProps {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const Markdawn = ({ className, text: rawText }: MarkdawnProps): JSX.Element => {
const { playerName } = useUserSettings();
const playerName = useAtomGetter(atoms.settings.playerName);
const router = useRouter();
const { isContentPanelAtLeastLg } = useContainerQueries();
const { showLightBox } = useLightBox();
const isContentPanelAtLeastLg = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastLg);
const { showLightBox } = useAtomGetter(atoms.lightBox);
/* eslint-disable no-irregular-whitespace */
const text = useMemo(
@ -232,7 +230,7 @@ export const TableOfContents = ({
horizontalLine = false,
}: TableOfContentsProps): JSX.Element => {
const router = useRouter();
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const toc = useMemo(() => getTocFromMarkdawn(preprocessMarkDawn(text), title), [text, title]);
return (

View File

@ -1,5 +1,5 @@
import { useRouter } from "next/router";
import { MouseEventHandler, useCallback, useMemo, useState } from "react";
import { MouseEventHandler, useCallback, useMemo } from "react";
import { Ico, Icon } from "components/Ico";
import { ToolTip } from "components/ToolTip";
import { cIf, cJoin } from "helpers/className";
@ -43,7 +43,6 @@ export const NavOption = ({
() => active || router.asPath.startsWith(url),
[active, router.asPath, url]
);
const [isFocused, setFocused] = useState(false);
return (
<ToolTip
@ -65,11 +64,8 @@ export const NavOption = ({
border={border}
onClick={onClick}
active={isActive}
disabled={disabled}
onFocusChanged={setFocused}>
{icon && (
<Ico icon={icon} className="mt-[-.1em] !text-2xl" isFilled={isActive || isFocused} />
)}
disabled={disabled}>
{icon && <Ico icon={icon} className="mt-[-.1em] !text-2xl" isFilled={isActive} />}
{!reduced && (
<div>
<h3 className="text-2xl">{title}</h3>

View File

@ -1,12 +1,11 @@
import { useCallback } from "react";
import { Icon } from "components/Ico";
import { Button } from "components/Inputs/Button";
import { useAppLayout } from "contexts/AppLayoutContext";
import { TranslatedProps } from "types/TranslatedProps";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { isDefined } from "helpers/others";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -24,9 +23,8 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props): JSX.Element => {
const { setSubPanelOpen } = useAppLayout();
const { langui } = useLocalData();
const { is3ColumnsLayout } = useContainerQueries();
const langui = useAtomGetter(atoms.localData.langui);
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
return (
<>
@ -34,12 +32,7 @@ export const ReturnButton = ({ href, title, displayOnlyOn, className }: Props):
(!is3ColumnsLayout && displayOnlyOn === "1ColumnLayout") ||
!isDefined(displayOnlyOn)) && (
<div className={className}>
<Button
onClick={() => setSubPanelOpen(false)}
href={href}
text={`${langui.return_to} ${title}`}
icon={Icon.NavigateBefore}
/>
<Button href={href} text={`${langui.return_to} ${title}`} icon={Icon.NavigateBefore} />
</div>
)}
</>

View File

@ -3,15 +3,14 @@ import { HorizontalLine } from "components/HorizontalLine";
import { Button } from "components/Inputs/Button";
import { NavOption } from "components/PanelComponents/NavOption";
import { ToolTip } from "components/ToolTip";
import { useAppLayout } from "contexts/AppLayoutContext";
import { Icon } from "components/Ico";
import { cIf, cJoin } from "helpers/className";
import { isDefinedAndNotEmpty } from "helpers/others";
import { Link } from "components/Inputs/Link";
import { sendAnalytics } from "helpers/analytics";
import { useLocalData } from "contexts/LocalDataContext";
import { ColoredSvg } from "components/ColoredSvg";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter, useAtomPair, useAtomSetter } from "helpers/atoms";
/*
*
@ -19,34 +18,35 @@ import { useContainerQueries } from "contexts/ContainerQueriesContext";
*/
export const MainPanel = (): JSX.Element => {
const { is3ColumnsLayout } = useContainerQueries();
const { mainPanelReduced, toggleMainPanelReduced, setConfigPanelOpen } = useAppLayout();
const { langui } = useLocalData();
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const langui = useAtomGetter(atoms.localData.langui);
const [isMainPanelReduced, setMainPanelReduced] = useAtomPair(atoms.layout.mainPanelReduced);
const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened);
return (
<div
className={cJoin(
"grid content-start justify-center gap-y-2 p-8 text-center",
cIf(mainPanelReduced && is3ColumnsLayout, "px-4")
cIf(isMainPanelReduced && is3ColumnsLayout, "px-4")
)}>
{/* Reduce/expand main menu */}
{is3ColumnsLayout && (
<div
className={cJoin(
"fixed top-1/2",
cIf(mainPanelReduced, "left-[4.65rem]", "left-[18.65rem]")
cIf(isMainPanelReduced, "left-[4.65rem]", "left-[18.65rem]")
)}>
<Button
onClick={() => {
if (mainPanelReduced) {
if (isMainPanelReduced) {
sendAnalytics("MainPanel", "Expand");
} else {
sendAnalytics("MainPanel", "Reduce");
}
toggleMainPanelReduced();
setMainPanelReduced((current) => !current);
}}
className="z-50 bg-light !px-2"
icon={mainPanelReduced ? Icon.ChevronRight : Icon.ChevronLeft}
icon={isMainPanelReduced ? Icon.ChevronRight : Icon.ChevronLeft}
/>
</div>
)}
@ -57,28 +57,28 @@ export const MainPanel = (): JSX.Element => {
src="/icons/accords.svg"
className={cJoin(
"mb-4 aspect-square bg-black hover:bg-dark",
cIf(mainPanelReduced && is3ColumnsLayout, "w-12", "w-1/2")
cIf(isMainPanelReduced && is3ColumnsLayout, "w-12", "w-1/2")
)}
/>
</Link>
{(!mainPanelReduced || !is3ColumnsLayout) && (
{(!isMainPanelReduced || !is3ColumnsLayout) && (
<h2 className="mb-4 text-3xl">Accord&rsquo;s Library</h2>
)}
<div
className={cJoin(
"flex flex-wrap gap-2",
cIf(mainPanelReduced && is3ColumnsLayout, "flex-col gap-3", "flex-row")
cIf(isMainPanelReduced && is3ColumnsLayout, "flex-col gap-3", "flex-row")
)}>
<ToolTip
content={<h3 className="text-2xl">{langui.open_settings}</h3>}
placement="right"
className="text-left"
disabled={!mainPanelReduced}>
disabled={!isMainPanelReduced}>
<Button
onClick={() => {
setConfigPanelOpen(true);
setSettingsOpened(true);
sendAnalytics("Settings", "Open settings");
}}
icon={Icon.Settings}
@ -95,7 +95,7 @@ export const MainPanel = (): JSX.Element => {
icon={Icon.LibraryBooks}
title={langui.library}
subtitle={langui.library_short_description}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
@ -103,7 +103,7 @@ export const MainPanel = (): JSX.Element => {
icon={Icon.Workspaces}
title={langui.contents}
subtitle={langui.contents_short_description}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
@ -111,7 +111,7 @@ export const MainPanel = (): JSX.Element => {
icon={Icon.TravelExplore}
title={langui.wiki}
subtitle={langui.wiki_short_description}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
@ -119,7 +119,7 @@ export const MainPanel = (): JSX.Element => {
icon={Icon.WatchLater}
title={langui.chronicles}
subtitle={langui.chronicles_short_description}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<HorizontalLine />
@ -128,7 +128,7 @@ export const MainPanel = (): JSX.Element => {
url="/news"
icon={Icon.Feed}
title={langui.news}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
{/*
@ -136,7 +136,7 @@ export const MainPanel = (): JSX.Element => {
url="/merch"
icon={Icon.Store}
title={langui.merch}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
*/}
@ -144,26 +144,26 @@ export const MainPanel = (): JSX.Element => {
url="https://gallery.accords-library.com/posts/"
icon={Icon.Collections}
title={langui.gallery}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
url="/archives"
icon={Icon.Inventory2}
title={langui.archives}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
<NavOption
url="/about-us"
icon={Icon.Info}
title={langui.about_us}
reduced={mainPanelReduced && is3ColumnsLayout}
reduced={isMainPanelReduced && is3ColumnsLayout}
/>
{(!mainPanelReduced || !is3ColumnsLayout) && <HorizontalLine />}
{(!isMainPanelReduced || !is3ColumnsLayout) && <HorizontalLine />}
<div className={cJoin("text-center", cIf(mainPanelReduced && is3ColumnsLayout, "hidden"))}>
<div className={cJoin("text-center", cIf(isMainPanelReduced && is3ColumnsLayout, "hidden"))}>
{isDefinedAndNotEmpty(langui.licensing_notice) && (
<p>
<Markdown>{langui.licensing_notice}</Markdown>

View File

@ -7,37 +7,32 @@ import { OrderableList } from "components/Inputs/OrderableList";
import { Select } from "components/Inputs/Select";
import { TextInput } from "components/Inputs/TextInput";
import { Popup } from "components/Containers/Popup";
import { useAppLayout } from "contexts/AppLayoutContext";
import { useLocalData } from "contexts/LocalDataContext";
import { useUserSettings } from "contexts/UserSettingsContext";
import { sendAnalytics } from "helpers/analytics";
import { cJoin, cIf } from "helpers/className";
import { prettyLanguage } from "helpers/formatters";
import { filterHasAttributes, isDefined } from "helpers/others";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter, useAtomPair } from "helpers/atoms";
import { ThemeMode } from "contexts/settings";
export const SettingsPopup = (): JSX.Element => {
const {
currency,
darkMode,
dyslexic,
fontSize,
playerName,
preferredLanguages,
selectedThemeMode,
setCurrency,
setDarkMode,
setDyslexic,
setFontSize,
setPlayerName,
setPreferredLanguages,
setSelectedThemeMode,
} = useUserSettings();
const [preferredLanguages, setPreferredLanguages] = useAtomPair(
atoms.settings.preferredLanguages
);
const [isSettingsOpened, setSettingsOpened] = useAtomPair(atoms.layout.settingsOpened);
const [currency, setCurrency] = useAtomPair(atoms.settings.currency);
const [isDyslexic, setDyslexic] = useAtomPair(atoms.settings.dyslexic);
const [fontSize, setFontSize] = useAtomPair(atoms.settings.fontSize);
const [playerName, setPlayerName] = useAtomPair(atoms.settings.playerName);
const [themeMode, setThemeMode] = useAtomPair(atoms.settings.themeMode);
const languages = useAtomGetter(atoms.localData.languages);
const langui = useAtomGetter(atoms.localData.langui);
const currencies = useAtomGetter(atoms.localData.currencies);
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const { langui, currencies, languages } = useLocalData();
const { is1ColumnLayout } = useContainerQueries();
const router = useRouter();
const { configPanelOpen, setConfigPanelOpen } = useAppLayout();
const currencyOptions = useMemo(
() =>
@ -59,9 +54,9 @@ export const SettingsPopup = (): JSX.Element => {
return (
<Popup
isVisible={configPanelOpen}
isVisible={isSettingsOpened}
onCloseRequest={() => {
setConfigPanelOpen(false);
setSettingsOpened(false);
sendAnalytics("Settings", "Close settings");
}}>
<h2 className="text-2xl">{langui.settings}</h2>
@ -110,28 +105,26 @@ export const SettingsPopup = (): JSX.Element => {
buttonsProps={[
{
onClick: () => {
setDarkMode(false);
setSelectedThemeMode(true);
setThemeMode(ThemeMode.Light);
sendAnalytics("Settings", "Change theme (light)");
},
active: selectedThemeMode && !darkMode,
active: themeMode === ThemeMode.Light,
text: langui.light,
},
{
onClick: () => {
setSelectedThemeMode(false);
setThemeMode(ThemeMode.Auto);
sendAnalytics("Settings", "Change theme (auto)");
},
active: !selectedThemeMode,
active: themeMode === ThemeMode.Auto,
text: langui.auto,
},
{
onClick: () => {
setDarkMode(true);
setSelectedThemeMode(true);
setThemeMode(ThemeMode.Dark);
sendAnalytics("Settings", "Change theme (dark)");
},
active: selectedThemeMode && darkMode,
active: themeMode === ThemeMode.Dark,
text: langui.dark,
},
]}
@ -198,7 +191,7 @@ export const SettingsPopup = (): JSX.Element => {
<h3 className="text-xl">{langui.font}</h3>
<div className="grid gap-2">
<Button
active={!dyslexic}
active={!isDyslexic}
onClick={() => {
setDyslexic(false);
sendAnalytics("Settings", "Change font (Zen Maru Gothic)");
@ -207,7 +200,7 @@ export const SettingsPopup = (): JSX.Element => {
text="Zen Maru Gothic"
/>
<Button
active={dyslexic}
active={isDyslexic}
onClick={() => {
setDyslexic(true);
sendAnalytics("Settings", "Change font (OpenDyslexic)");

View File

@ -13,7 +13,8 @@ import { useSmartLanguage } from "hooks/useSmartLanguage";
import { PostWithTranslations } from "types/types";
import { filterHasAttributes, getStatusDescription } from "helpers/others";
import { prettySlug } from "helpers/formatters";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -48,7 +49,7 @@ export const PostPage = ({
displayTitle = true,
...otherProps
}: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: post.translations,
languageExtractor: useCallback(

View File

@ -11,8 +11,8 @@ import { ImageQuality } from "helpers/img";
import { useDeviceSupportsHover } from "hooks/useMediaQuery";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { TranslatedProps } from "types/TranslatedProps";
import { useUserSettings } from "contexts/UserSettingsContext";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -70,8 +70,8 @@ export const PreviewCard = ({
infoAppend,
disabled = false,
}: Props): JSX.Element => {
const { currency } = useUserSettings();
const { currencies } = useLocalData();
const currency = useAtomGetter(atoms.settings.currency);
const currencies = useAtomGetter(atoms.localData.currencies);
const isHoverable = useDeviceSupportsHover();
const router = useRouter();

View File

@ -6,7 +6,8 @@ import { Chip } from "components/Chip";
import { RecorderChipFragment } from "graphql/generated";
import { ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/others";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -21,7 +22,7 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export const RecorderChip = ({ recorder }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<ToolTip

View File

@ -8,8 +8,8 @@ import { cJoin } from "helpers/className";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { useScrollTopOnChange } from "hooks/useScrollTopOnChange";
import { Ids } from "types/ids";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
interface Group<T> {
name: string;
@ -71,7 +71,7 @@ export const SmartList = <T,>({
className,
}: Props<T>): JSX.Element => {
const [page, setPage] = useState(0);
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
useScrollTopOnChange(Ids.ContentPanel, [page], paginationScroolTop);
useEffect(() => setPage(0), [searchingTerm, groupingFunction, groupSortingFunction, items]);
@ -228,8 +228,8 @@ export const SmartList = <T,>({
*/
const DefaultRenderWhenEmpty = () => {
const { is3ColumnsLayout } = useContainerQueries();
const { langui } = useLocalData();
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const langui = useAtomGetter(atoms.localData.langui);
return (
<div className="grid h-full place-content-center">
<div

View File

@ -6,8 +6,8 @@ import { GetContentTextQuery, UploadImageFragment } from "graphql/generated";
import { prettyInlineTitle, prettySlug, slugify } from "helpers/formatters";
import { ImageQuality } from "helpers/img";
import { filterHasAttributes } from "helpers/others";
import { useLocalData } from "contexts/LocalDataContext";
import { useLightBox } from "contexts/LightBoxContext";
import { useAtomGetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";
/*
*
@ -42,8 +42,8 @@ export const ThumbnailHeader = ({
description,
languageSwitcher,
}: Props): JSX.Element => {
const { langui } = useLocalData();
const { showLightBox } = useLightBox();
const langui = useAtomGetter(atoms.localData.langui);
const { showLightBox } = useAtomGetter(atoms.lightBox);
return (
<>

View File

@ -5,8 +5,8 @@ import { getStatusDescription } from "helpers/others";
import { useSmartLanguage } from "hooks/useSmartLanguage";
import { Button } from "components/Inputs/Button";
import { cIf, cJoin } from "helpers/className";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -30,8 +30,8 @@ interface Props {
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
const DefinitionCard = ({ source, translations = [], index, categories }: Props): JSX.Element => {
const { isContentPanelAtLeastMd } = useContainerQueries();
const { langui } = useLocalData();
const isContentPanelAtLeastMd = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastMd);
const langui = useAtomGetter(atoms.localData.langui);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: translations,
languageExtractor: useCallback((item: Props["translations"][number]) => item.language, []),

View File

@ -1,152 +0,0 @@
import React, {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useEffect,
useState,
} from "react";
import { useRouter } from "next/router";
import { useLocalStorage } from "usehooks-ts";
import { isDefined } from "helpers/others";
import { RequiredNonNullable } from "types/types";
import { useStateWithLocalStorage } from "hooks/useStateWithLocalStorage";
import { useScrollIntoView } from "hooks/useScrollIntoView";
interface AppLayoutState {
subPanelOpen: boolean;
toggleSubPanelOpen: () => void;
setSubPanelOpen: Dispatch<SetStateAction<AppLayoutState["subPanelOpen"]>>;
configPanelOpen: boolean;
toggleConfigPanelOpen: () => void;
setConfigPanelOpen: Dispatch<SetStateAction<AppLayoutState["configPanelOpen"]>>;
mainPanelReduced: boolean;
toggleMainPanelReduced: () => void;
setMainPanelReduced: Dispatch<SetStateAction<AppLayoutState["mainPanelReduced"]>>;
mainPanelOpen: boolean;
toggleMainPanelOpen: () => void;
setMainPanelOpen: Dispatch<SetStateAction<AppLayoutState["mainPanelOpen"]>>;
menuGestures: boolean;
toggleMenuGestures: () => void;
setMenuGestures: Dispatch<SetStateAction<AppLayoutState["menuGestures"]>>;
}
const initialState: RequiredNonNullable<AppLayoutState> = {
subPanelOpen: false,
toggleSubPanelOpen: () => null,
setSubPanelOpen: () => null,
configPanelOpen: false,
setConfigPanelOpen: () => null,
toggleConfigPanelOpen: () => null,
mainPanelReduced: false,
setMainPanelReduced: () => null,
toggleMainPanelReduced: () => null,
mainPanelOpen: false,
toggleMainPanelOpen: () => null,
setMainPanelOpen: () => null,
menuGestures: true,
toggleMenuGestures: () => null,
setMenuGestures: () => null,
};
const AppLayoutContext = createContext<AppLayoutState>(initialState);
export const useAppLayout = (): AppLayoutState => useContext(AppLayoutContext);
interface Props {
children: ReactNode;
}
export const AppContextProvider = ({ children }: Props): JSX.Element => {
const router = useRouter();
const [subPanelOpen, setSubPanelOpen] = useStateWithLocalStorage(
"subPanelOpen",
initialState.subPanelOpen
);
const [configPanelOpen, setConfigPanelOpen] = useLocalStorage(
"configPanelOpen",
initialState.configPanelOpen
);
const [mainPanelReduced, setMainPanelReduced] = useLocalStorage(
"mainPanelReduced",
initialState.mainPanelReduced
);
const [mainPanelOpen, setMainPanelOpen] = useStateWithLocalStorage(
"mainPanelOpen",
initialState.mainPanelOpen
);
const [menuGestures, setMenuGestures] = useState(false);
const toggleSubPanelOpen = () => {
setSubPanelOpen((current) => (isDefined(current) ? !current : current));
};
const toggleConfigPanelOpen = () => {
setConfigPanelOpen((current) => (isDefined(current) ? !current : current));
};
const toggleMainPanelReduced = () => {
setMainPanelReduced((current) => (isDefined(current) ? !current : current));
};
const toggleMainPanelOpen = () => {
setMainPanelOpen((current) => (isDefined(current) ? !current : current));
};
const toggleMenuGestures = () => {
setMenuGestures((current) => !current);
};
useEffect(() => {
router.events.on("routeChangeStart", () => {
console.log("[Router Events] on routeChangeStart");
setConfigPanelOpen(false);
setMainPanelOpen(false);
setSubPanelOpen(false);
});
router.events.on("hashChangeStart", () => {
console.log("[Router Events] on hashChangeStart");
setSubPanelOpen(false);
});
}, [router.events, setConfigPanelOpen, setMainPanelOpen, setSubPanelOpen]);
useScrollIntoView();
return (
<AppLayoutContext.Provider
value={{
subPanelOpen,
configPanelOpen,
mainPanelReduced,
mainPanelOpen,
menuGestures,
setSubPanelOpen,
setConfigPanelOpen,
setMainPanelReduced,
setMainPanelOpen,
setMenuGestures,
toggleSubPanelOpen,
toggleConfigPanelOpen,
toggleMainPanelReduced,
toggleMainPanelOpen,
toggleMenuGestures,
}}>
{children}
</AppLayoutContext.Provider>
);
};

View File

@ -1,315 +0,0 @@
import React, { createContext, ReactNode, useContext, useMemo, useState } from "react";
import { useUserSettings } from "./UserSettingsContext";
import { useOnResize } from "hooks/useOnResize";
import { Ids } from "types/ids";
import { RequiredNonNullable } from "types/types";
type Size =
| "2xl"
| "2xs"
| "3xl"
| "4xl"
| "5xl"
| "6xl"
| "7xl"
| "lg"
| "md"
| "sm"
| "xl"
| "xs";
const sizes: Record<Size, number> = {
"2xl": 42,
"3xl": 48,
"4xl": 56,
"5xl": 64,
"6xl": 72,
"7xl": 80,
lg: 32,
md: 28,
sm: 24,
xl: 36,
xs: 19,
"2xs": 16,
};
type ContainerQueriesContainers = "contentPanel" | "screen" | "subPanel";
type ContainerQueriesKeys =
| "is1ColumnLayout"
| "is3ColumnsLayout"
| `is${Capitalize<ContainerQueriesContainers>}AtLeast${Capitalize<Size>}`;
type ContainerQueriesState = Record<ContainerQueriesKeys, boolean>;
const initialState: RequiredNonNullable<ContainerQueriesState> = {
is1ColumnLayout: false,
is3ColumnsLayout: false,
isContentPanelAtLeast2xl: false,
isContentPanelAtLeast2xs: false,
isContentPanelAtLeast3xl: false,
isContentPanelAtLeast4xl: false,
isContentPanelAtLeast5xl: false,
isContentPanelAtLeast6xl: false,
isContentPanelAtLeast7xl: false,
isContentPanelAtLeastLg: false,
isContentPanelAtLeastMd: false,
isContentPanelAtLeastSm: false,
isContentPanelAtLeastXl: false,
isContentPanelAtLeastXs: false,
isScreenAtLeast2xl: false,
isScreenAtLeast2xs: false,
isScreenAtLeast3xl: false,
isScreenAtLeast4xl: false,
isScreenAtLeast5xl: false,
isScreenAtLeast6xl: false,
isScreenAtLeast7xl: false,
isScreenAtLeastLg: false,
isScreenAtLeastMd: false,
isScreenAtLeastSm: false,
isScreenAtLeastXl: false,
isScreenAtLeastXs: false,
isSubPanelAtLeast2xl: false,
isSubPanelAtLeast2xs: false,
isSubPanelAtLeast3xl: false,
isSubPanelAtLeast4xl: false,
isSubPanelAtLeast5xl: false,
isSubPanelAtLeast6xl: false,
isSubPanelAtLeast7xl: false,
isSubPanelAtLeastLg: false,
isSubPanelAtLeastMd: false,
isSubPanelAtLeastSm: false,
isSubPanelAtLeastXl: false,
isSubPanelAtLeastXs: false,
};
const ContainerQueriesContext = createContext<ContainerQueriesState>(initialState);
export const useContainerQueries = (): ContainerQueriesState => useContext(ContainerQueriesContext);
interface Props {
children: ReactNode;
}
export const ContainerQueriesContextProvider = ({ children }: Props): JSX.Element => {
const [screenWidth, setScreenWidth] = useState(0);
const [contentPanelWidth, setContentPanelWidth] = useState(0);
const [subPanelWidth, setSubPanelWidth] = useState(0);
useOnResize(Ids.Body, (width) => setScreenWidth(width));
useOnResize(Ids.ContentPanel, (width) => setContentPanelWidth(width));
useOnResize(Ids.SubPanel, (width) => setSubPanelWidth(width));
const { fontSize } = useUserSettings();
const screenAtLeasts = useMemo(
() => ({
isScreenAtLeast2xs: applyContainerQuery(
fontSize,
screenWidth,
createAtLeastMediaQuery("2xs")
),
isScreenAtLeastXs: applyContainerQuery(fontSize, screenWidth, createAtLeastMediaQuery("xs")),
isScreenAtLeastSm: applyContainerQuery(fontSize, screenWidth, createAtLeastMediaQuery("sm")),
isScreenAtLeastMd: applyContainerQuery(fontSize, screenWidth, createAtLeastMediaQuery("md")),
isScreenAtLeastLg: applyContainerQuery(fontSize, screenWidth, createAtLeastMediaQuery("lg")),
isScreenAtLeastXl: applyContainerQuery(fontSize, screenWidth, createAtLeastMediaQuery("xl")),
isScreenAtLeast2xl: applyContainerQuery(
fontSize,
screenWidth,
createAtLeastMediaQuery("2xl")
),
isScreenAtLeast3xl: applyContainerQuery(
fontSize,
screenWidth,
createAtLeastMediaQuery("3xl")
),
isScreenAtLeast4xl: applyContainerQuery(
fontSize,
screenWidth,
createAtLeastMediaQuery("4xl")
),
isScreenAtLeast5xl: applyContainerQuery(
fontSize,
screenWidth,
createAtLeastMediaQuery("5xl")
),
isScreenAtLeast6xl: applyContainerQuery(
fontSize,
screenWidth,
createAtLeastMediaQuery("6xl")
),
isScreenAtLeast7xl: applyContainerQuery(
fontSize,
screenWidth,
createAtLeastMediaQuery("7xl")
),
}),
[screenWidth, fontSize]
);
const columnLayouts = useMemo(
() => ({
is1ColumnLayout: !screenAtLeasts.isScreenAtLeast5xl,
is3ColumnsLayout: screenAtLeasts.isScreenAtLeast5xl,
}),
[screenAtLeasts.isScreenAtLeast5xl]
);
const subPanelAtLeasts = useMemo(
() => ({
isSubPanelAtLeast2xs: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("2xs")
),
isSubPanelAtLeastXs: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("xs")
),
isSubPanelAtLeastSm: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("sm")
),
isSubPanelAtLeastMd: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("md")
),
isSubPanelAtLeastLg: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("lg")
),
isSubPanelAtLeastXl: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("xl")
),
isSubPanelAtLeast2xl: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("2xl")
),
isSubPanelAtLeast3xl: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("3xl")
),
isSubPanelAtLeast4xl: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("4xl")
),
isSubPanelAtLeast5xl: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("5xl")
),
isSubPanelAtLeast6xl: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("6xl")
),
isSubPanelAtLeast7xl: applyContainerQuery(
fontSize,
subPanelWidth,
createAtLeastMediaQuery("7xl")
),
}),
[subPanelWidth, fontSize]
);
const contentPanelAtLeasts = useMemo(
() => ({
isContentPanelAtLeast2xs: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("2xs")
),
isContentPanelAtLeastXs: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("xs")
),
isContentPanelAtLeastSm: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("sm")
),
isContentPanelAtLeastMd: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("md")
),
isContentPanelAtLeastLg: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("lg")
),
isContentPanelAtLeastXl: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("xl")
),
isContentPanelAtLeast2xl: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("2xl")
),
isContentPanelAtLeast3xl: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("3xl")
),
isContentPanelAtLeast4xl: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("4xl")
),
isContentPanelAtLeast5xl: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("5xl")
),
isContentPanelAtLeast6xl: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("6xl")
),
isContentPanelAtLeast7xl: applyContainerQuery(
fontSize,
contentPanelWidth,
createAtLeastMediaQuery("7xl")
),
}),
[contentPanelWidth, fontSize]
);
return (
<ContainerQueriesContext.Provider
value={{
...screenAtLeasts,
...contentPanelAtLeasts,
...subPanelAtLeasts,
...columnLayouts,
}}>
{children}
</ContainerQueriesContext.Provider>
);
};
type MediaQuery = { value: number; unit: "px" | "rem"; rule: "max" | "min" };
const createAtLeastMediaQuery = (size: Size): MediaQuery => ({
value: sizes[size],
unit: "rem",
rule: "min",
});
const applyContainerQuery = (fontSize: number, width: number, query: MediaQuery): boolean => {
const breakpoint = query.value * (query.unit === "rem" ? 16 : 1) * fontSize;
return query.rule === "min" ? width >= breakpoint : width < breakpoint;
};

View File

@ -1,58 +0,0 @@
import React, { createContext, ReactNode, useCallback, useContext, useState } from "react";
import { UploadImageFragment } from "graphql/generated";
import { RequiredNonNullable } from "types/types";
import { LightBox } from "components/LightBox";
import { filterDefined } from "helpers/others";
type LightBoxImagesNullable = (UploadImageFragment | string | null | undefined)[];
type LightBoxImages = (UploadImageFragment | string)[];
interface LightBoxState {
showLightBox: (images: LightBoxImagesNullable, index?: number) => void;
}
const initialState: RequiredNonNullable<LightBoxState> = {
showLightBox: () => null,
};
const LightBoxContext = createContext<LightBoxState>(initialState);
export const useLightBox = (): LightBoxState => useContext(LightBoxContext);
interface Props {
children: ReactNode;
}
export const LightBoxContextProvider = ({ children }: Props): JSX.Element => {
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
const [lightBoxImages, setLightBoxImages] = useState<LightBoxImages>([]);
const [lightBoxIndex, setLightBoxIndex] = useState(0);
const showLightBox = useCallback((images: LightBoxImagesNullable, index = 0) => {
const filteredImages = filterDefined(images);
setLightBoxIndex(index);
setLightBoxImages(filteredImages);
setLightBoxVisibility(true);
}, []);
const closeLightBox = useCallback(() => {
setLightBoxVisibility(false);
setTimeout(() => setLightBoxImages([]), 100);
}, []);
return (
<LightBoxContext.Provider value={{ showLightBox }}>
<LightBox
isVisible={isLightBoxVisible}
onCloseRequest={closeLightBox}
image={lightBoxImages[lightBoxIndex]}
isNextImageAvailable={lightBoxIndex < lightBoxImages.length - 1}
isPreviousImageAvailable={lightBoxIndex > 0}
onPressNext={() => setLightBoxIndex((current) => current + 1)}
onPressPrevious={() => setLightBoxIndex((current) => current - 1)}
/>
{children}
</LightBoxContext.Provider>
);
};

View File

@ -0,0 +1,54 @@
import React, { useCallback, useState } from "react";
import { useEffectOnce } from "usehooks-ts";
import { atom } from "jotai";
import { UploadImageFragment } from "graphql/generated";
import { LightBox } from "components/LightBox";
import { filterDefined } from "helpers/others";
import { atomPairing, useAtomSetter } from "helpers/atoms";
const lightBoxAtom = atomPairing(
atom<{
showLightBox: (
images: (UploadImageFragment | string | null | undefined)[],
index?: number
) => void;
}>({ showLightBox: () => null })
);
export const lightBox = lightBoxAtom[0];
export const LightBoxProvider = (): JSX.Element => {
const [isLightBoxVisible, setLightBoxVisibility] = useState(false);
const [lightBoxImages, setLightBoxImages] = useState<(UploadImageFragment | string)[]>([]);
const [lightBoxIndex, setLightBoxIndex] = useState(0);
const setShowLightBox = useAtomSetter(lightBoxAtom);
useEffectOnce(() =>
setShowLightBox({
showLightBox: (images, index = 0) => {
const filteredImages = filterDefined(images);
setLightBoxIndex(index);
setLightBoxImages(filteredImages);
setLightBoxVisibility(true);
},
})
);
const closeLightBox = useCallback(() => {
setLightBoxVisibility(false);
setTimeout(() => setLightBoxImages([]), 100);
}, []);
return (
<LightBox
isVisible={isLightBoxVisible}
onCloseRequest={closeLightBox}
image={lightBoxImages[lightBoxIndex]}
isNextImageAvailable={lightBoxIndex < lightBoxImages.length - 1}
isPreviousImageAvailable={lightBoxIndex > 0}
onPressNext={() => setLightBoxIndex((current) => current + 1)}
onPressPrevious={() => setLightBoxIndex((current) => current - 1)}
/>
);
};

View File

@ -1,70 +0,0 @@
import React, { createContext, ReactNode, useContext, useMemo } from "react";
import { useRouter } from "next/router";
import { useFetch } from "usehooks-ts";
import {
Langui,
Languages,
Currencies,
processCurrencies,
processLanguages,
processLangui,
} from "helpers/localData";
import { RequiredNonNullable } from "types/types";
import { LocalDataFile } from "graphql/fetchLocalData";
import {
LocalDataGetCurrenciesQuery,
LocalDataGetLanguagesQuery,
LocalDataGetWebsiteInterfacesQuery,
} from "graphql/generated";
interface LocalDataState {
langui: Langui;
languages: Languages;
currencies: Currencies;
}
const initialState: RequiredNonNullable<LocalDataState> = {
currencies: [],
languages: [],
langui: {},
};
const LocalDataContext = createContext<LocalDataState>(initialState);
export const useLocalData = (): LocalDataState => useContext(LocalDataContext);
interface Props {
children: ReactNode;
}
export const LocalDataContextProvider = ({ children }: Props): JSX.Element => {
const langui = useLangui();
const languages = useLanguages();
const currencies = useCurrencies();
return (
<LocalDataContext.Provider value={{ langui, languages, currencies }}>
{children}
</LocalDataContext.Provider>
);
};
const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`;
const useLangui = (): Langui => {
const { locale } = useRouter();
const { data: websiteInterfaces } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
getFileName("websiteInterfaces")
);
return useMemo(() => processLangui(websiteInterfaces, locale), [websiteInterfaces, locale]);
};
const useCurrencies = (): Currencies => {
const { data: currencies } = useFetch<LocalDataGetCurrenciesQuery>(getFileName("currencies"));
return useMemo(() => processCurrencies(currencies), [currencies]);
};
const useLanguages = (): Languages => {
const { data: languages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
return useMemo(() => processLanguages(languages), [languages]);
};

View File

@ -1,42 +0,0 @@
import React, {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from "react";
import { RequiredNonNullable } from "types/types";
interface TerminalState {
previousLines: string[];
previousCommands: string[];
setPreviousLines: Dispatch<SetStateAction<TerminalState["previousLines"]>>;
setPreviousCommands: Dispatch<SetStateAction<TerminalState["previousCommands"]>>;
}
const initialState: RequiredNonNullable<TerminalState> = {
previousLines: [],
previousCommands: [],
setPreviousLines: () => null,
setPreviousCommands: () => null,
};
const TerminalContext = createContext<TerminalState>(initialState);
export const useTerminalContext = (): TerminalState => useContext(TerminalContext);
interface Props {
children: ReactNode;
}
export const TerminalContextProvider = ({ children }: Props): JSX.Element => {
const [previousLines, setPreviousLines] = useState(initialState.previousLines);
const [previousCommands, setPreviousCommands] = useState(initialState.previousCommands);
return (
<TerminalContext.Provider
value={{ previousCommands, previousLines, setPreviousCommands, setPreviousLines }}>
{children}
</TerminalContext.Provider>
);
};

View File

@ -1,189 +0,0 @@
import { useLocalStorage } from "usehooks-ts";
import React, {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useEffect,
useLayoutEffect,
} from "react";
import { useRouter } from "next/router";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { RequiredNonNullable } from "types/types";
import { getDefaultPreferredLanguages } from "helpers/locales";
import { useDarkMode } from "hooks/useDarkMode";
import { SettingsPopup } from "components/Panels/SettingsPopup";
interface UserSettingsState {
fontSize: number;
setFontSize: Dispatch<SetStateAction<UserSettingsState["fontSize"]>>;
darkMode: boolean;
toggleDarkMode: () => void;
setDarkMode: Dispatch<SetStateAction<UserSettingsState["darkMode"]>>;
selectedThemeMode: boolean;
toggleSelectedThemeMode: () => void;
setSelectedThemeMode: Dispatch<SetStateAction<UserSettingsState["selectedThemeMode"]>>;
dyslexic: boolean;
toggleDyslexic: () => void;
setDyslexic: Dispatch<SetStateAction<UserSettingsState["dyslexic"]>>;
currency: string;
setCurrency: Dispatch<SetStateAction<UserSettingsState["currency"]>>;
playerName: string;
setPlayerName: Dispatch<SetStateAction<UserSettingsState["playerName"]>>;
preferredLanguages: string[];
setPreferredLanguages: Dispatch<SetStateAction<UserSettingsState["preferredLanguages"]>>;
}
const initialState: RequiredNonNullable<UserSettingsState> = {
fontSize: 1,
setFontSize: () => null,
darkMode: false,
toggleDarkMode: () => null,
setDarkMode: () => null,
selectedThemeMode: false,
toggleSelectedThemeMode: () => null,
setSelectedThemeMode: () => null,
dyslexic: false,
toggleDyslexic: () => null,
setDyslexic: () => null,
currency: "USD",
setCurrency: () => null,
playerName: "",
setPlayerName: () => null,
preferredLanguages: [],
setPreferredLanguages: () => null,
};
const UserSettingsContext = createContext<UserSettingsState>(initialState);
export const useUserSettings = (): UserSettingsState => useContext(UserSettingsContext);
interface Props {
children: ReactNode;
}
export const UserSettingsProvider = ({ children }: Props): JSX.Element => {
const router = useRouter();
const [fontSize, setFontSize] = useLocalStorage("fontSize", initialState.fontSize);
const [darkMode, selectedThemeMode, setDarkMode, setSelectedThemeMode] = useDarkMode(
"darkMode",
initialState.darkMode
);
const [dyslexic, setDyslexic] = useLocalStorage("dyslexic", initialState.dyslexic);
const [currency, setCurrency] = useLocalStorage("currency", initialState.currency);
const [playerName, setPlayerName] = useLocalStorage("playerName", initialState.playerName);
const [preferredLanguages, setPreferredLanguages] = useLocalStorage(
"preferredLanguages",
initialState.preferredLanguages
);
const toggleDarkMode = () => {
setDarkMode((current) => (isDefined(current) ? !current : current));
};
const toggleSelectedThemeMode = () => {
setSelectedThemeMode((current) => (isDefined(current) ? !current : current));
};
const toggleDyslexic = () => {
setDyslexic((current) => (isDefined(current) ? !current : current));
};
useEffect(() => {
if (preferredLanguages.length === 0) {
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
}
} else if (router.locale !== preferredLanguages[0]) {
/*
* Using a timeout to the code getting stuck into a loop when reaching the website with a
* different preferredLanguages[0] from router.locale
*/
setTimeout(
async () =>
router.replace(router.asPath, router.asPath, {
locale: preferredLanguages[0],
}),
250
);
}
}, [preferredLanguages, router, router.locale, router.locales, setPreferredLanguages]);
useLayoutEffect(() => {
const html = document.getElementsByTagName("html")[0];
if (isDefined(html)) {
html.style.fontSize = `${fontSize * 100}%`;
}
}, [fontSize]);
useLayoutEffect(() => {
const next = document.getElementById("__next");
if (isDefined(next)) {
if (darkMode) {
next.classList.add("set-theme-dark");
next.classList.remove("set-theme-light");
} else {
next.classList.add("set-theme-light");
next.classList.remove("set-theme-dark");
}
}
}, [darkMode]);
useLayoutEffect(() => {
const next = document.getElementById("__next");
if (isDefined(next)) {
if (dyslexic) {
next.classList.add("set-theme-font-dyslexic");
next.classList.remove("set-theme-font-standard");
} else {
next.classList.add("set-theme-font-standard");
next.classList.remove("set-theme-font-dyslexic");
}
}
}, [dyslexic]);
return (
<UserSettingsContext.Provider
value={{
fontSize,
darkMode,
selectedThemeMode,
dyslexic,
currency,
playerName,
preferredLanguages,
setFontSize,
setDarkMode,
setSelectedThemeMode,
setDyslexic,
setCurrency,
setPlayerName,
setPreferredLanguages,
toggleDarkMode,
toggleSelectedThemeMode,
toggleDyslexic,
}}>
<SettingsPopup />
{children}
</UserSettingsContext.Provider>
);
};

29
src/contexts/appLayout.ts Normal file
View File

@ -0,0 +1,29 @@
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useScrollIntoView } from "hooks/useScrollIntoView";
import { useAtomSetter } from "helpers/atoms";
import { atoms } from "contexts/atoms";
export const useAppLayout = (): void => {
const router = useRouter();
const setSettingsOpened = useAtomSetter(atoms.layout.settingsOpened);
const setMainPanelOpened = useAtomSetter(atoms.layout.mainPanelOpened);
const setSubPanelOpened = useAtomSetter(atoms.layout.subPanelOpened);
useEffect(() => {
router.events.on("routeChangeStart", () => {
console.log("[Router Events] on routeChangeStart");
setSettingsOpened(false);
setMainPanelOpened(false);
setSubPanelOpened(false);
});
router.events.on("hashChangeStart", () => {
console.log("[Router Events] on hashChangeStart");
setSubPanelOpened(false);
});
}, [router, setSettingsOpened, setMainPanelOpened, setSubPanelOpened]);
useScrollIntoView();
};

49
src/contexts/atoms.ts Normal file
View File

@ -0,0 +1,49 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { localData } from "contexts/localData";
import { containerQueries } from "contexts/containerQueries";
import { atomPairing } from "helpers/atoms";
import { settings } from "contexts/settings";
import { lightBox } from "contexts/LightBoxProvider";
/*
* I'm getting a weird error if I put those atoms in appLayout.ts
* So I'm putting the atoms here. Sucks, I know.
*/
/* [ APPLAYOUT ATOMS ] */
const mainPanelReduced = atomPairing(atomWithStorage("isMainPanelReduced", false));
const settingsOpened = atomPairing(atomWithStorage("isSettingsOpened", false));
const subPanelOpened = atomPairing(atomWithStorage("isSubPanelOpened", false));
const mainPanelOpened = atomPairing(atomWithStorage("isMainPanelOpened", false));
const menuGesturesEnabled = atomPairing(atomWithStorage("isMenuGesturesEnabled", false));
const terminalMode = atom((get) => get(settings.playerName[0]) === "root");
const layout = {
mainPanelReduced,
settingsOpened,
subPanelOpened,
mainPanelOpened,
menuGesturesEnabled,
terminalMode,
};
/* [ TERMINAL ATOMS ] */
const previousLines = atomPairing(atom<string[]>([]));
const previousCommands = atomPairing(atom<string[]>([]));
const terminal = {
previousLines,
previousCommands,
};
export const atoms = {
settings,
layout,
terminal,
localData,
lightBox,
containerQueries,
};

View File

@ -0,0 +1,146 @@
import { atom, Getter } from "jotai";
import { atomPairing, useAtomSetter } from "helpers/atoms";
import { useOnResize } from "hooks/useOnResize";
import { Ids } from "types/ids";
import { settings } from "contexts/settings";
type Size =
| "2xl"
| "2xs"
| "3xl"
| "4xl"
| "5xl"
| "6xl"
| "7xl"
| "lg"
| "md"
| "sm"
| "xl"
| "xs";
const sizes: Record<Size, number> = {
"2xl": 42,
"3xl": 48,
"4xl": 56,
"5xl": 64,
"6xl": 72,
"7xl": 80,
lg: 32,
md: 28,
sm: 24,
xl: 36,
xs: 19,
"2xs": 16,
};
const isAtLeastContainerQuery = (width: number, size: Size, fontSize: number): boolean =>
width >= sizes[size] * 16 * fontSize;
const screenWidth = atomPairing(atom(0));
const contentPanelWidth = atomPairing(atom(0));
const subPanelWidth = atomPairing(atom(0));
const screenGetter = (get: Getter, size: Size) =>
isAtLeastContainerQuery(get(screenWidth[0]), size, get(settings.fontSize[0]));
const isScreenAtLeast2xs = atom((get) => screenGetter(get, "2xs"));
const isScreenAtLeastXs = atom((get) => screenGetter(get, "xs"));
const isScreenAtLeastSm = atom((get) => screenGetter(get, "sm"));
const isScreenAtLeastMd = atom((get) => screenGetter(get, "md"));
const isScreenAtLeastLg = atom((get) => screenGetter(get, "lg"));
const isScreenAtLeastXl = atom((get) => screenGetter(get, "xl"));
const isScreenAtLeast2xl = atom((get) => screenGetter(get, "2xl"));
const isScreenAtLeast3xl = atom((get) => screenGetter(get, "3xl"));
const isScreenAtLeast4xl = atom((get) => screenGetter(get, "4xl"));
const isScreenAtLeast5xl = atom((get) => screenGetter(get, "5xl"));
const isScreenAtLeast6xl = atom((get) => screenGetter(get, "6xl"));
const isScreenAtLeast7xl = atom((get) => screenGetter(get, "7xl"));
const is1ColumnLayout = atom((get) => !get(isScreenAtLeast5xl));
const is3ColumnsLayout = atom((get) => get(isScreenAtLeast5xl));
const contentPanelGetter = (get: Getter, size: Size) =>
isAtLeastContainerQuery(get(contentPanelWidth[0]), size, get(settings.fontSize[0]));
const isContentPanelAtLeast2xs = atom((get) => contentPanelGetter(get, "2xs"));
const isContentPanelAtLeastXs = atom((get) => contentPanelGetter(get, "xs"));
const isContentPanelAtLeastSm = atom((get) => contentPanelGetter(get, "sm"));
const isContentPanelAtLeastMd = atom((get) => contentPanelGetter(get, "md"));
const isContentPanelAtLeastLg = atom((get) => contentPanelGetter(get, "lg"));
const isContentPanelAtLeastXl = atom((get) => contentPanelGetter(get, "xl"));
const isContentPanelAtLeast2xl = atom((get) => contentPanelGetter(get, "2xl"));
const isContentPanelAtLeast3xl = atom((get) => contentPanelGetter(get, "3xl"));
const isContentPanelAtLeast4xl = atom((get) => contentPanelGetter(get, "4xl"));
const isContentPanelAtLeast5xl = atom((get) => contentPanelGetter(get, "5xl"));
const isContentPanelAtLeast6xl = atom((get) => contentPanelGetter(get, "6xl"));
const isContentPanelAtLeast7xl = atom((get) => contentPanelGetter(get, "7xl"));
const subPanelGetter = (get: Getter, size: Size) =>
isAtLeastContainerQuery(get(subPanelWidth[0]), size, get(settings.fontSize[0]));
const isSubPanelAtLeast2xs = atom((get) => subPanelGetter(get, "2xs"));
const isSubPanelAtLeastXs = atom((get) => subPanelGetter(get, "xs"));
const isSubPanelAtLeastSm = atom((get) => subPanelGetter(get, "sm"));
const isSubPanelAtLeastMd = atom((get) => subPanelGetter(get, "md"));
const isSubPanelAtLeastLg = atom((get) => subPanelGetter(get, "lg"));
const isSubPanelAtLeastXl = atom((get) => subPanelGetter(get, "xl"));
const isSubPanelAtLeast2xl = atom((get) => subPanelGetter(get, "2xl"));
const isSubPanelAtLeast3xl = atom((get) => subPanelGetter(get, "3xl"));
const isSubPanelAtLeast4xl = atom((get) => subPanelGetter(get, "4xl"));
const isSubPanelAtLeast5xl = atom((get) => subPanelGetter(get, "5xl"));
const isSubPanelAtLeast6xl = atom((get) => subPanelGetter(get, "6xl"));
const isSubPanelAtLeast7xl = atom((get) => subPanelGetter(get, "7xl"));
export const containerQueries = {
is1ColumnLayout,
is3ColumnsLayout,
isScreenAtLeast2xs,
isScreenAtLeastXs,
isScreenAtLeastSm,
isScreenAtLeastMd,
isScreenAtLeastLg,
isScreenAtLeastXl,
isScreenAtLeast2xl,
isScreenAtLeast3xl,
isScreenAtLeast4xl,
isScreenAtLeast5xl,
isScreenAtLeast6xl,
isScreenAtLeast7xl,
isContentPanelAtLeast2xs,
isContentPanelAtLeastXs,
isContentPanelAtLeastSm,
isContentPanelAtLeastMd,
isContentPanelAtLeastLg,
isContentPanelAtLeastXl,
isContentPanelAtLeast2xl,
isContentPanelAtLeast3xl,
isContentPanelAtLeast4xl,
isContentPanelAtLeast5xl,
isContentPanelAtLeast6xl,
isContentPanelAtLeast7xl,
isSubPanelAtLeast2xs,
isSubPanelAtLeastXs,
isSubPanelAtLeastSm,
isSubPanelAtLeastMd,
isSubPanelAtLeastLg,
isSubPanelAtLeastXl,
isSubPanelAtLeast2xl,
isSubPanelAtLeast3xl,
isSubPanelAtLeast4xl,
isSubPanelAtLeast5xl,
isSubPanelAtLeast6xl,
isSubPanelAtLeast7xl,
};
export const useContainerQueries = (): void => {
const setScreenWidth = useAtomSetter(screenWidth);
const setContentPanelWidth = useAtomSetter(contentPanelWidth);
const setSubPanelWidth = useAtomSetter(subPanelWidth);
useOnResize(Ids.Body, (width) => setScreenWidth(width));
useOnResize(Ids.ContentPanel, (width) => setContentPanelWidth(width));
useOnResize(Ids.SubPanel, (width) => setSubPanelWidth(width));
};

59
src/contexts/localData.ts Normal file
View File

@ -0,0 +1,59 @@
import { atom } from "jotai";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useFetch } from "usehooks-ts";
import { atomPairing, useAtomSetter } from "helpers/atoms";
import {
Languages,
Currencies,
Langui,
processLangui,
processCurrencies,
processLanguages,
} from "helpers/localData";
import {
LocalDataGetWebsiteInterfacesQuery,
LocalDataGetCurrenciesQuery,
LocalDataGetLanguagesQuery,
} from "graphql/generated";
import { LocalDataFile } from "graphql/fetchLocalData";
const languages = atomPairing(atom<Languages>([]));
const currencies = atomPairing(atom<Currencies>([]));
const langui = atomPairing(atom<Langui>({}));
export const localData = {
languages: languages[0],
currencies: currencies[0],
langui: langui[0],
};
const getFileName = (name: LocalDataFile): string => `/local-data/${name}.json`;
export const useLocalData = (): void => {
const setLanguages = useAtomSetter(languages);
const setCurrencies = useAtomSetter(currencies);
const setLangui = useAtomSetter(langui);
const { locale } = useRouter();
const { data: rawLanguages } = useFetch<LocalDataGetLanguagesQuery>(getFileName("languages"));
const { data: rawCurrencies } = useFetch<LocalDataGetCurrenciesQuery>(getFileName("currencies"));
const { data: rawLangui } = useFetch<LocalDataGetWebsiteInterfacesQuery>(
getFileName("websiteInterfaces")
);
useEffect(() => {
console.log("[useLocalData] Refresh languages");
setLanguages(processLanguages(rawLanguages));
}, [rawLanguages, setLanguages]);
useEffect(() => {
console.log("[useLocalData] Refresh currencies");
setCurrencies(processCurrencies(rawCurrencies));
}, [rawCurrencies, setCurrencies]);
useEffect(() => {
console.log("[useLocalData] Refresh langui");
setLangui(processLangui(rawLangui, locale));
}, [locale, rawLangui, setLangui]);
};

103
src/contexts/settings.ts Normal file
View File

@ -0,0 +1,103 @@
import { useRouter } from "next/router";
import { useLayoutEffect, useEffect } from "react";
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { atomPairing, useAtomGetter, useAtomPair } from "helpers/atoms";
import { getDefaultPreferredLanguages } from "helpers/locales";
import { isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { usePrefersDarkMode } from "hooks/useMediaQuery";
export enum ThemeMode {
Dark = "dark",
Auto = "auto",
Light = "light",
}
const preferredLanguagesAtom = atomPairing(atomWithStorage<string[]>("preferredLanguages", []));
const themeModeAtom = atomPairing(atomWithStorage<ThemeMode>("themeMode", ThemeMode.Auto));
const darkModeAtom = atomPairing(atom(false));
const fontSizeAtom = atomPairing(atomWithStorage("fontSize", 1));
const dyslexicAtom = atomPairing(atomWithStorage("isDyslexic", false));
const currencyAtom = atomPairing(atomWithStorage("currency", "USD"));
const playerNameAtom = atomPairing(atomWithStorage("playerName", ""));
export const settings = {
preferredLanguages: preferredLanguagesAtom,
themeMode: themeModeAtom,
darkMode: darkModeAtom,
fontSize: fontSizeAtom,
dyslexic: dyslexicAtom,
currency: currencyAtom,
playerName: playerNameAtom,
};
export const useSettings = (): void => {
const router = useRouter();
const [preferredLanguages, setPreferredLanguages] = useAtomPair(preferredLanguagesAtom);
const fontSize = useAtomGetter(fontSizeAtom);
const isDyslexic = useAtomGetter(dyslexicAtom);
const [isDarkMode, setDarkMode] = useAtomPair(darkModeAtom);
const themeMode = useAtomGetter(themeModeAtom);
useLayoutEffect(() => {
const html = document.getElementsByTagName("html")[0];
if (isDefined(html)) {
html.style.fontSize = `${fontSize * 100}%`;
}
}, [fontSize]);
useLayoutEffect(() => {
const next = document.getElementById("__next");
if (isDefined(next)) {
if (isDyslexic) {
next.classList.add("set-theme-font-dyslexic");
next.classList.remove("set-theme-font-standard");
} else {
next.classList.add("set-theme-font-standard");
next.classList.remove("set-theme-font-dyslexic");
}
}
}, [isDyslexic]);
/* DARK MODE */
const prefersDarkMode = usePrefersDarkMode();
useEffect(() => {
setDarkMode(themeMode === ThemeMode.Auto ? prefersDarkMode : themeMode === ThemeMode.Dark);
}, [prefersDarkMode, setDarkMode, themeMode]);
useLayoutEffect(() => {
const next = document.getElementById("__next");
if (isDefined(next)) {
if (isDarkMode) {
next.classList.add("set-theme-dark");
next.classList.remove("set-theme-light");
} else {
next.classList.add("set-theme-light");
next.classList.remove("set-theme-dark");
}
}
}, [isDarkMode]);
/* PREFERRED LANGUAGES */
useEffect(() => {
if (preferredLanguages.length === 0) {
if (isDefinedAndNotEmpty(router.locale) && router.locales) {
setPreferredLanguages(getDefaultPreferredLanguages(router.locale, router.locales));
}
} else if (router.locale !== preferredLanguages[0]) {
/*
* Using a timeout to the code getting stuck into a loop when reaching the website with a
* different preferredLanguages[0] from router.locale
*/
setTimeout(
async () =>
router.replace(router.asPath, router.asPath, {
locale: preferredLanguages[0],
}),
250
);
}
}, [preferredLanguages, router, setPreferredLanguages]);
};

View File

@ -2,6 +2,7 @@ export const sendAnalytics = (category: string, event: string): void => {
try {
umami(`[${category}] ${event}`);
} catch (error) {
if (error instanceof ReferenceError) return;
console.log(error);
}
};

26
src/helpers/atoms.ts Normal file
View File

@ -0,0 +1,26 @@
import { atom, PrimitiveAtom, Atom, WritableAtom, useAtom } from "jotai";
import { Dispatch, SetStateAction } from "react";
type AtomPair<T> = [Atom<T>, WritableAtom<null, T>];
export const atomPairing = <T>(anAtom: PrimitiveAtom<T>): AtomPair<T> => {
const getter = atom((get) => get(anAtom));
const setter = atom(null, (_get, set, newText: T) => set(anAtom, newText));
return [getter, setter];
};
export const useAtomSetter = <T>(atomPair: AtomPair<T>): Dispatch<SetStateAction<T>> => {
const [, setter] = useAtom(atomPair[1]);
return setter as Dispatch<SetStateAction<T>>;
};
export const useAtomGetter = <T>(atomPair: Atom<T> | AtomPair<T>): T => {
const atomGet = Array.isArray(atomPair) ? atomPair[0] : atomPair;
const [getter] = useAtom(atomGet);
return getter;
};
export const useAtomPair = <T>(atomPair: AtomPair<T>): [T, Dispatch<SetStateAction<T>>] => [
useAtomGetter(atomPair),
useAtomSetter(atomPair),
];

View File

@ -1,18 +0,0 @@
import { Dispatch, SetStateAction, useEffect } from "react";
import { useLocalStorage } from "usehooks-ts";
import { usePrefersDarkMode } from "./useMediaQuery";
export const useDarkMode = (
key: string,
initialValue: boolean
): [boolean, boolean, Dispatch<SetStateAction<boolean>>, Dispatch<SetStateAction<boolean>>] => {
const [darkMode, setDarkMode] = useLocalStorage(key, initialValue);
const prefersDarkMode = usePrefersDarkMode();
const [selectedThemeMode, setSelectedThemeMode] = useLocalStorage("selectedThemeMode", false);
useEffect(() => {
if (!selectedThemeMode) setDarkMode(prefersDarkMode);
}, [selectedThemeMode, prefersDarkMode, setDarkMode]);
return [darkMode, selectedThemeMode, setDarkMode, setSelectedThemeMode];
};

View File

@ -1,6 +0,0 @@
import { useUserSettings } from "contexts/UserSettingsContext";
export const useIsTerminalMode = (): boolean => {
const { playerName } = useUserSettings();
return playerName === "root";
};

View File

@ -3,8 +3,8 @@ import { useEffect, useMemo, useState } from "react";
import { LanguageSwitcher } from "components/Inputs/LanguageSwitcher";
import { filterDefined, isDefined } from "helpers/others";
import { getPreferredLanguage } from "helpers/locales";
import { useUserSettings } from "contexts/UserSettingsContext";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
interface Props<T> {
items: T[];
@ -17,8 +17,8 @@ export const useSmartLanguage = <T>({
languageExtractor,
transform = (item) => item,
}: Props<T>): [T | undefined, typeof LanguageSwitcher, Parameters<typeof LanguageSwitcher>[0]] => {
const { preferredLanguages } = useUserSettings();
const { languages } = useLocalData();
const preferredLanguages = useAtomGetter(atoms.settings.preferredLanguages);
const languages = useAtomGetter(atoms.localData.languages);
const router = useRouter();
const availableLocales = useMemo(() => {

View File

@ -1,31 +0,0 @@
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { isDefined } from "helpers/others";
export const useStateWithLocalStorage = <T>(
key: string,
initialValue: T
): [T, Dispatch<SetStateAction<T>>] => {
const [value, setValue] = useState<T>(initialValue);
const [isFromLocaleStorage, setFromLocaleStorage] = useState<boolean>(false);
useEffect(() => {
try {
const item = localStorage.getItem(key);
if (isDefined(item)) {
setValue(JSON.parse(item) as T);
} else {
setValue(initialValue);
}
setFromLocaleStorage(true);
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error);
setValue(initialValue);
}
}, [initialValue, key]);
useEffect(() => {
if (isFromLocaleStorage) localStorage.setItem(key, JSON.stringify(value));
}, [value, key, isFromLocaleStorage]);
return [value, setValue];
};

View File

@ -5,7 +5,8 @@ import { ContentPanel } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { Img } from "components/Img";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -15,7 +16,7 @@ import { useLocalData } from "contexts/LocalDataContext";
interface Props extends AppLayoutRequired {}
const FourOhFour = ({ openGraph, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
contentPanel={

View File

@ -5,7 +5,8 @@ import { ContentPanel } from "components/Containers/ContentPanel";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { Img } from "components/Img";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -15,7 +16,7 @@ import { useLocalData } from "contexts/LocalDataContext";
interface Props extends AppLayoutRequired {}
const FiveHundred = ({ openGraph, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
contentPanel={

View File

@ -9,7 +9,6 @@ import "@fontsource/zen-maru-gothic/900.css";
import type { AppProps } from "next/app";
import Script from "next/script";
import { AppContextProvider } from "contexts/AppLayoutContext";
import "styles/debug.css";
import "styles/formatted.css";
@ -17,31 +16,31 @@ import "styles/others.css";
import "styles/rc-slider.css";
import "styles/tippy.css";
import { TerminalContextProvider } from "contexts/TerminalContext";
import { UserSettingsProvider as UserSettingsContextProvider } from "contexts/UserSettingsContext";
import { LocalDataContextProvider } from "contexts/LocalDataContext";
import { ContainerQueriesContextProvider } from "contexts/ContainerQueriesContext";
import { LightBoxContextProvider } from "contexts/LightBoxContext";
import { useLocalData } from "contexts/localData";
import { useAppLayout } from "contexts/appLayout";
import { LightBoxProvider } from "contexts/LightBoxProvider";
import { SettingsPopup } from "components/Panels/SettingsPopup";
import { useSettings } from "contexts/settings";
import { useContainerQueries } from "contexts/containerQueries";
const AccordsLibraryApp = (props: AppProps): JSX.Element => (
<LocalDataContextProvider>
<AppContextProvider>
<UserSettingsContextProvider>
<ContainerQueriesContextProvider>
<TerminalContextProvider>
<LightBoxContextProvider>
<Script
async
defer
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/umami.js`}
/>
<props.Component {...props.pageProps} />
</LightBoxContextProvider>
</TerminalContextProvider>
</ContainerQueriesContextProvider>
</UserSettingsContextProvider>
</AppContextProvider>
</LocalDataContextProvider>
);
const AccordsLibraryApp = (props: AppProps): JSX.Element => {
useLocalData();
useAppLayout();
useSettings();
useContainerQueries();
return (
<>
<SettingsPopup />
<LightBoxProvider />
<Script
async
defer
data-website-id={process.env.NEXT_PUBLIC_UMAMI_ID}
src={`${process.env.NEXT_PUBLIC_UMAMI_URL}/umami.js`}
/>
<props.Component {...props.pageProps} />
</>
);
};
export default AccordsLibraryApp;

View File

@ -1,6 +1,7 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -8,7 +9,7 @@ import { useLocalData } from "contexts/LocalDataContext";
*/
const AccordsHandbook = (props: PostStaticProps): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<PostPage
{...props}

View File

@ -7,8 +7,8 @@ import { cIf, cJoin } from "helpers/className";
import { randomInt } from "helpers/numbers";
import { RequestMailProps, ResponseMailProps } from "pages/api/mail";
import { sendAnalytics } from "helpers/analytics";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -17,8 +17,8 @@ import { useContainerQueries } from "contexts/ContainerQueriesContext";
const AboutUs = (props: PostStaticProps): JSX.Element => {
const router = useRouter();
const { langui } = useLocalData();
const { is1ColumnLayout } = useContainerQueries();
const langui = useAtomGetter(atoms.localData.langui);
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const [formResponse, setFormResponse] = useState("");
const [formState, setFormState] = useState<"completed" | "ongoing" | "stale">("stale");

View File

@ -7,7 +7,8 @@ import { SubPanel } from "components/Containers/SubPanel";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -17,7 +18,7 @@ import { useLocalData } from "contexts/LocalDataContext";
interface Props extends AppLayoutRequired {}
const AboutUs = (props: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
subPanel={

View File

@ -1,6 +1,7 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -8,7 +9,7 @@ import { useLocalData } from "contexts/LocalDataContext";
*/
const Legality = (props: PostStaticProps): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<PostPage
{...props}

View File

@ -1,6 +1,7 @@
import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -8,7 +9,7 @@ import { useLocalData } from "contexts/LocalDataContext";
*/
const SharingPolicy = (props: PostStaticProps): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<PostPage
{...props}

View File

@ -8,7 +8,8 @@ import { Icon } from "components/Ico";
import { getOpenGraph } from "helpers/openGraph";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -18,7 +19,7 @@ import { useLocalData } from "contexts/LocalDataContext";
interface Props extends AppLayoutRequired {}
const Archives = (props: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const subPanel = useMemo(
() => (
<SubPanel>

View File

@ -22,8 +22,8 @@ import { SmartList } from "components/SmartList";
import { cIf } from "helpers/className";
import { TextInput } from "components/Inputs/TextInput";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -45,9 +45,9 @@ interface Props extends AppLayoutRequired {
const Channel = ({ channel, ...otherProps }: Props): JSX.Element => {
const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(true);
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const hoverable = useDeviceSupportsHover();
const { isContentPanelAtLeast4xl } = useContainerQueries();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);

View File

@ -22,8 +22,8 @@ import { compareDate } from "helpers/date";
import { HorizontalLine } from "components/HorizontalLine";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -44,9 +44,9 @@ interface Props extends AppLayoutRequired {
}
const Videos = ({ videos, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const hoverable = useDeviceSupportsHover();
const { isContentPanelAtLeast4xl } = useContainerQueries();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(true);

View File

@ -10,7 +10,6 @@ import { NavOption } from "components/PanelComponents/NavOption";
import { ReturnButton } from "components/PanelComponents/ReturnButton";
import { ContentPanel, ContentPanelWidthSizes } from "components/Containers/ContentPanel";
import { SubPanel } from "components/Containers/SubPanel";
import { useAppLayout } from "contexts/AppLayoutContext";
import { GetVideoQuery } from "graphql/generated";
import { getReadySdk } from "graphql/sdk";
import { prettyDate, prettyShortenNumber } from "helpers/formatters";
@ -18,8 +17,8 @@ import { filterHasAttributes, isDefined } from "helpers/others";
import { getVideoFile } from "helpers/videos";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -31,9 +30,8 @@ interface Props extends AppLayoutRequired {
}
const Video = ({ video, ...otherProps }: Props): JSX.Element => {
const { isContentPanelAtLeast4xl } = useContainerQueries();
const { setSubPanelOpen } = useAppLayout();
const { langui } = useLocalData();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const langui = useAtomGetter(atoms.localData.langui);
const router = useRouter();
const subPanel = useMemo(
@ -47,29 +45,12 @@ const Video = ({ video, ...otherProps }: Props): JSX.Element => {
<HorizontalLine />
<NavOption
title={langui.video}
url="#video"
border
onClick={() => setSubPanelOpen(false)}
/>
<NavOption
title={langui.channel}
url="#channel"
border
onClick={() => setSubPanelOpen(false)}
/>
<NavOption
title={langui.description}
url="#description"
border
onClick={() => setSubPanelOpen(false)}
/>
<NavOption title={langui.video} url="#video" border />
<NavOption title={langui.channel} url="#channel" border />
<NavOption title={langui.description} url="#description" border />
</SubPanel>
),
[setSubPanelOpen, langui]
[langui]
);
const contentPanel = useMemo(

View File

@ -19,7 +19,8 @@ import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/local
import { getDescription } from "helpers/description";
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -32,7 +33,7 @@ interface Props extends AppLayoutRequired {
}
const Chronicle = ({ chronicle, chapters, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: chronicle.translations,
languageExtractor: useCallback(

View File

@ -12,7 +12,8 @@ import { getOpenGraph } from "helpers/openGraph";
import { TranslatedChroniclesList } from "components/Chronicles/ChroniclesList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -24,7 +25,7 @@ interface Props extends AppLayoutRequired {
}
const Chronicles = ({ chapters, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const subPanel = useMemo(
() => (
<SubPanel>

View File

@ -32,8 +32,8 @@ import { TranslatedPreviewLine } from "components/PreviewLine";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { Ids } from "types/ids";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -45,8 +45,11 @@ interface Props extends AppLayoutRequired {
}
const Content = ({ content, ...otherProps }: Props): JSX.Element => {
const { isContentPanelAtLeast2xl, is1ColumnLayout } = useContainerQueries();
const { langui, languages } = useLocalData();
const isContentPanelAtLeast2xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast2xl);
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const langui = useAtomGetter(atoms.localData.langui);
const languages = useAtomGetter(atoms.localData.languages);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: content.translations,

View File

@ -25,8 +25,8 @@ import { TranslatedPreviewCard } from "components/PreviewCard";
import { cJoin, cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -50,8 +50,8 @@ interface Props extends AppLayoutRequired {
const Contents = ({ contents, ...otherProps }: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const { langui } = useLocalData();
const { isContentPanelAtLeast4xl } = useContainerQueries();
const langui = useAtomGetter(atoms.localData.langui);
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const [groupingMethod, setGroupingMethod] = useState<number>(
DEFAULT_FILTERS_STATE.groupingMethod

View File

@ -18,8 +18,8 @@ import { TranslatedPreviewCard } from "components/PreviewCard";
import { HorizontalLine } from "components/HorizontalLine";
import { cJoin, cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { TranslatedPreviewFolder } from "components/Contents/PreviewFolder";
/*
@ -34,8 +34,8 @@ interface Props extends AppLayoutRequired {
}
const ContentsFolder = ({ openGraph, folder, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const { isContentPanelAtLeast4xl } = useContainerQueries();
const langui = useAtomGetter(atoms.localData.langui);
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const subPanel = useMemo(
() => (
@ -273,7 +273,7 @@ export const getStaticPaths: GetStaticPaths = async (context) => {
*/
const NoContentNorFolderMessage = () => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<div className="grid place-content-center">
<div

View File

@ -936,13 +936,8 @@ const LightThemeSection = ({ className, children }: ThemedSectionProps) => (
);
const WhiteSection = ({ className, children }: ThemedSectionProps) => (
<div
className={cJoin(
`mb-12 rounded-xl bg-[white] py-10 px-14 text-black drop-shadow-lg shadow-shade
set-theme-light`,
className
)}>
{children}
<div className="mb-12 rounded-xl bg-[white] py-10 px-14 drop-shadow-lg shadow-shade">
<div className={cJoin("text-black set-theme-light", className)}>{children}</div>
</div>
);

View File

@ -2,8 +2,8 @@ import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { getOpenGraph } from "helpers/openGraph";
import { Terminal } from "components/Cli/Terminal";
import { useIsTerminalMode } from "hooks/useIsTerminalMode";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -11,8 +11,8 @@ import { useLocalData } from "contexts/LocalDataContext";
*/
const Home = ({ ...otherProps }: PostStaticProps): JSX.Element => {
const { langui } = useLocalData();
const isTerminalMode = useIsTerminalMode();
const langui = useAtomGetter(atoms.localData.langui);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
if (isTerminalMode) {
return (

View File

@ -51,10 +51,8 @@ import { useIntersectionList } from "hooks/useIntersectionList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { Ids } from "types/ids";
import { useUserSettings } from "contexts/UserSettingsContext";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { useLightBox } from "contexts/LightBoxContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -74,14 +72,18 @@ interface Props extends AppLayoutRequired {
}
const LibrarySlug = ({ item, itemId, ...otherProps }: Props): JSX.Element => {
const { currency } = useUserSettings();
const { langui, currencies } = useLocalData();
const { isContentPanelAtLeast3xl, isContentPanelAtLeastSm } = useContainerQueries();
const currency = useAtomGetter(atoms.settings.currency);
const langui = useAtomGetter(atoms.localData.langui);
const currencies = useAtomGetter(atoms.localData.currencies);
const isContentPanelAtLeast3xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast3xl);
const isContentPanelAtLeastSm = useAtomGetter(atoms.containerQueries.isContentPanelAtLeastSm);
const hoverable = useDeviceSupportsHover();
const router = useRouter();
const { value: keepInfoVisible, toggle: toggleKeepInfoVisible } = useBoolean(false);
const { showLightBox } = useLightBox();
const { showLightBox } = useAtomGetter(atoms.lightBox);
useScrollTopOnChange(Ids.ContentPanel, [item]);
const currentIntersection = useIntersectionList(intersectionIds);
@ -692,7 +694,7 @@ const ContentLine = ({
parentSlug,
condensed,
}: ContentLineProps): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const { value: isOpened, toggle: toggleOpened } = useBoolean(false);
const [selectedTranslation] = useSmartLanguage({
items: content?.translations ?? [],

View File

@ -41,14 +41,13 @@ import { useSmartLanguage } from "hooks/useSmartLanguage";
import { TranslatedProps } from "types/TranslatedProps";
import { prettyInlineTitle, prettySlug } from "helpers/formatters";
import { useFullscreen } from "hooks/useFullscreen";
import { useUserSettings } from "contexts/UserSettingsContext";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { FilterSettings, useReaderSettings } from "hooks/useReaderSettings";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
const CUSTOM_DARK_DROPSHADOW = `
drop-shadow(0 0 0.5em rgb(var(--theme-color-shade) / 30%))
drop-shadow(0 1em 1em rgb(var(--theme-color-shade) / 40%))
drop-shadow(0 0 0.5em rgb(var(--theme-color-shade) / 30%))
drop-shadow(0 1em 1em rgb(var(--theme-color-shade) / 40%))
drop-shadow(0 2em 2em rgb(var(--theme-color-shade) / 60%))
drop-shadow(0 12em 12em rgb(var(--theme-color-shade) / 80%))`;
@ -90,9 +89,9 @@ const LibrarySlug = ({
item,
...otherProps
}: Props): JSX.Element => {
const { is1ColumnLayout } = useContainerQueries();
const { langui } = useLocalData();
const { darkMode } = useUserSettings();
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const langui = useAtomGetter(atoms.localData.langui);
const isDarkMode = useAtomGetter(atoms.settings.darkMode);
const {
filterSettings,
isSidePagesEnabled,
@ -453,7 +452,7 @@ const LibrarySlug = ({
display: "grid",
placeContent: "center",
filter: filterSettings.dropShadow
? darkMode
? isDarkMode
? CUSTOM_DARK_DROPSHADOW
: CUSTOM_LIGHT_DROPSHADOW
: undefined,
@ -631,7 +630,7 @@ const LibrarySlug = ({
is1ColumnLayout,
currentZoom,
filterSettings,
darkMode,
isDarkMode,
pageHeight,
effectiveDisplayMode,
firstPage,
@ -798,7 +797,7 @@ interface PageFiltersProps {
}
const PageFilters = ({ page, bookType, options }: PageFiltersProps) => {
const { darkMode } = useUserSettings();
const isDarkMode = useAtomGetter(atoms.settings.darkMode);
const commonCss = useMemo(
() => cJoin("absolute inset-0", cIf(page === "right", "[background-position-x:-100%]")),
[page]
@ -823,7 +822,7 @@ const PageFilters = ({ page, bookType, options }: PageFiltersProps) => {
commonCss,
`bg-blend-lighten mix-blend-multiply [background-image:url(/reader/book-fold.webp)]
[background-size:200%_100%]`,
cIf(!darkMode, "bg-[#FFF]/50")
cIf(!isDarkMode, "bg-[#FFF]/50")
)}
/>
)}
@ -834,7 +833,7 @@ const PageFilters = ({ page, bookType, options }: PageFiltersProps) => {
className={cJoin(
commonCss,
`bg-blend-lighten mix-blend-multiply [background-size:200%_100%]`,
cIf(!darkMode, "bg-[#FFF]/50"),
cIf(!isDarkMode, "bg-[#FFF]/50"),
cIf(
page === "single",
"[background-image:url(/reader/lighting-single-page.webp)]",
@ -846,7 +845,7 @@ const PageFilters = ({ page, bookType, options }: PageFiltersProps) => {
className={cJoin(
commonCss,
`bg-blend-lighten mix-blend-soft-light [background-size:200%_100%]`,
cIf(!darkMode, "bg-[#FFF]/30"),
cIf(!isDarkMode, "bg-[#FFF]/30"),
cIf(
page === "single",
"[background-image:url(/reader/specular-single-page.webp)]",
@ -889,8 +888,8 @@ interface ScanSetProps {
}
const ScanSet = ({ onClickOnImage, scanSet, id, title, content }: ScanSetProps): JSX.Element => {
const { is1ColumnLayout } = useContainerQueries();
const { langui } = useLocalData();
const is1ColumnLayout = useAtomGetter(atoms.containerQueries.is1ColumnLayout);
const langui = useAtomGetter(atoms.localData.langui);
const [selectedScan, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: scanSet,
languageExtractor: useCallback(

View File

@ -31,9 +31,9 @@ import { HorizontalLine } from "components/HorizontalLine";
import { cIf, cJoin } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
import { useLibraryItemUserStatus } from "hooks/useLibraryItemUserStatus";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
/*
*
@ -62,9 +62,11 @@ interface Props extends AppLayoutRequired {
const Library = ({ items, ...otherProps }: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const { langui, currencies } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const currencies = useAtomGetter(atoms.localData.currencies);
const { libraryItemUserStatus } = useLibraryItemUserStatus();
const { isContentPanelAtLeast4xl } = useContainerQueries();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);

View File

@ -5,7 +5,8 @@ import { SubPanel } from "components/Containers/SubPanel";
import { Icon } from "components/Ico";
import { getOpenGraph } from "helpers/openGraph";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -14,7 +15,7 @@ import { useLocalData } from "contexts/LocalDataContext";
interface Props extends AppLayoutRequired {}
const Merch = (props: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
return (
<AppLayout
subPanel={

View File

@ -4,14 +4,13 @@ import { PostPage } from "components/PostPage";
import { getPostStaticProps, PostStaticProps } from "graphql/getPostStaticProps";
import { getReadySdk } from "graphql/sdk";
import { filterHasAttributes, isDefined, isDefinedAndNotEmpty } from "helpers/others";
import { useIsTerminalMode } from "hooks/useIsTerminalMode";
import { Terminal } from "components/Cli/Terminal";
import { PostWithTranslations } from "types/types";
import { getDefaultPreferredLanguages, staticSmartLanguage } from "helpers/locales";
import { prettyTerminalBoxedTitle } from "helpers/terminal";
import { prettyMarkdown } from "helpers/description";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
* PAGE
@ -20,8 +19,8 @@ import { useLocalData } from "contexts/LocalDataContext";
interface Props extends PostStaticProps {}
const LibrarySlug = (props: Props): JSX.Element => {
const { langui } = useLocalData();
const isTerminalMode = useIsTerminalMode();
const langui = useAtomGetter(atoms.localData.langui);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const router = useRouter();
if (isTerminalMode) {

View File

@ -23,10 +23,9 @@ import { HorizontalLine } from "components/HorizontalLine";
import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { useIsTerminalMode } from "hooks/useIsTerminalMode";
import { Terminal } from "components/Cli/Terminal";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -48,8 +47,8 @@ interface Props extends AppLayoutRequired {
}
const News = ({ posts, ...otherProps }: Props): JSX.Element => {
const { isContentPanelAtLeast4xl } = useContainerQueries();
const { langui } = useLocalData();
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const langui = useAtomGetter(atoms.localData.langui);
const hoverable = useDeviceSupportsHover();
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);
const {
@ -57,7 +56,7 @@ const News = ({ posts, ...otherProps }: Props): JSX.Element => {
toggle: toggleKeepInfoVisible,
setValue: setKeepInfoVisible,
} = useBoolean(DEFAULT_FILTERS_STATE.keepInfoVisible);
const isTerminalMode = useIsTerminalMode();
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const subPanel = useMemo(
() => (

View File

@ -22,10 +22,8 @@ import { cIf, cJoin } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { Terminal } from "components/Cli/Terminal";
import { prettyTerminalBoxedTitle, prettyTerminalUnderlinedTitle } from "helpers/terminal";
import { useIsTerminalMode } from "hooks/useIsTerminalMode";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { useLightBox } from "contexts/LightBoxContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -37,10 +35,10 @@ interface Props extends AppLayoutRequired {
}
const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const router = useRouter();
const isTerminalMode = useIsTerminalMode();
const { showLightBox } = useLightBox();
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const { showLightBox } = useAtomGetter(atoms.lightBox);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: page.translations,
languageExtractor: useCallback(
@ -49,7 +47,7 @@ const WikiPage = ({ page, ...otherProps }: Props): JSX.Element => {
[]
),
});
const { is3ColumnsLayout } = useContainerQueries();
const is3ColumnsLayout = useAtomGetter(atoms.containerQueries.is3ColumnsLayout);
const subPanel = useMemo(
() => (

View File

@ -31,7 +31,8 @@ import { TranslatedNavOption } from "components/PanelComponents/NavOption";
import { useIntersectionList } from "hooks/useIntersectionList";
import { HorizontalLine } from "components/HorizontalLine";
import { getLangui } from "graphql/fetchLocalData";
import { useLocalData } from "contexts/LocalDataContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -44,7 +45,7 @@ interface Props extends AppLayoutRequired {
}
const Chronology = ({ chronologyItems, chronologyEras, ...otherProps }: Props): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const ids = useMemo(
() =>
filterHasAttributes(chronologyEras, ["attributes"] as const).map(
@ -315,7 +316,7 @@ interface ChronologyEventProps {
}
export const ChronologyEvent = ({ event, id }: ChronologyEventProps): JSX.Element => {
const { langui } = useLocalData();
const langui = useAtomGetter(atoms.localData.langui);
const [selectedTranslation, LanguageSwitcher, languageSwitcherProps] = useSmartLanguage({
items: event.translations ?? [],
languageExtractor: useCallback(

View File

@ -26,9 +26,8 @@ import { cIf } from "helpers/className";
import { getLangui } from "graphql/fetchLocalData";
import { sendAnalytics } from "helpers/analytics";
import { Terminal } from "components/Cli/Terminal";
import { useIsTerminalMode } from "hooks/useIsTerminalMode";
import { useLocalData } from "contexts/LocalDataContext";
import { useContainerQueries } from "contexts/ContainerQueriesContext";
import { atoms } from "contexts/atoms";
import { useAtomGetter } from "helpers/atoms";
/*
*
@ -52,9 +51,9 @@ interface Props extends AppLayoutRequired {
const Wiki = ({ pages, ...otherProps }: Props): JSX.Element => {
const hoverable = useDeviceSupportsHover();
const { langui } = useLocalData();
const { isContentPanelAtLeast4xl } = useContainerQueries();
const isTerminalMode = useIsTerminalMode();
const langui = useAtomGetter(atoms.localData.langui);
const isContentPanelAtLeast4xl = useAtomGetter(atoms.containerQueries.isContentPanelAtLeast4xl);
const isTerminalMode = useAtomGetter(atoms.layout.terminalMode);
const [searchName, setSearchName] = useState(DEFAULT_FILTERS_STATE.searchName);

View File

@ -45,12 +45,6 @@ export interface ChronicleWithTranslations extends Omit<Chronicle, "translations
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export type RequiredNonNullable<T> = {
[P in keyof T]-?: NonNullable<T[P]>;
};
// ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
export enum LibraryItemUserStatus {
None = 0,
Want = 1,