From 5fb351011d03dd2c29e13f1d5899fb5f6247dd3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Philipe?= Date: Mon, 4 Dec 2023 23:21:50 +0100 Subject: [PATCH] replace react state mangement by zustand --- src/client/esbuild.js | 2 +- src/client/package-lock.json | 56 +++- src/client/package.json | 4 +- src/client/src/App.tsx | 19 +- src/client/src/components/Dates.tsx | 7 +- src/client/src/components/Header.tsx | 18 +- src/client/src/components/Heatmap.tsx | 6 +- .../src/components/KeyboardSelector.tsx | 25 +- src/client/src/components/Keymaps.tsx | 6 +- src/client/src/components/Stats.tsx | 16 +- src/client/src/hooks/useCharacters.ts | 46 +-- src/client/src/hooks/useCounts.ts | 56 +--- src/client/src/hooks/useDates.ts | 47 +-- src/client/src/hooks/useFetchStatus.ts | 13 + src/client/src/hooks/useHandAndFingerUsage.ts | 45 --- src/client/src/hooks/useKeyboards.ts | 44 +-- src/client/src/hooks/useKeymaps.ts | 43 +-- src/client/src/state/appState.tsx | 57 ++-- src/client/src/state/date.tsx | 146 +++------ src/client/src/state/fetch.tsx | 128 ++------ src/client/src/state/keyboard.tsx | 72 ++--- src/client/src/state/keyboardData.tsx | 295 +++++++----------- src/client/src/state/keyboards.tsx | 53 ++-- src/client/src/store/actions.ts | 15 - src/client/src/store/constants.ts | 14 - src/client/src/store/reducer.ts | 31 -- src/client/tsconfig.json | 2 +- src/client/yarn.lock | 21 +- 28 files changed, 445 insertions(+), 842 deletions(-) create mode 100644 src/client/src/hooks/useFetchStatus.ts delete mode 100644 src/client/src/hooks/useHandAndFingerUsage.ts delete mode 100644 src/client/src/store/actions.ts delete mode 100644 src/client/src/store/constants.ts delete mode 100644 src/client/src/store/reducer.ts diff --git a/src/client/esbuild.js b/src/client/esbuild.js index 988669f..2d222ba 100644 --- a/src/client/esbuild.js +++ b/src/client/esbuild.js @@ -8,7 +8,7 @@ const buildOptions = { bundle: true, minify: true, outfile: "build/static/main.js", - format: "esm", + format: "cjs", metafile: true, treeShaking: true, sourcemap: true, diff --git a/src/client/package-lock.json b/src/client/package-lock.json index a1ccc40..c6806b2 100644 --- a/src/client/package-lock.json +++ b/src/client/package-lock.json @@ -13,11 +13,13 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "heatmap.js": "^2.0.5", + "immer": "^10.0.3", "keystats-common": "file:../common", "material-symbols": "^0.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^4.4.7" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", @@ -3898,10 +3900,9 @@ } }, "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "dev": true, + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -5926,6 +5927,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", @@ -6939,6 +6950,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7199,6 +7218,33 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz", + "integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } } } } diff --git a/src/client/package.json b/src/client/package.json index 6f3d0fb..03a7704 100644 --- a/src/client/package.json +++ b/src/client/package.json @@ -9,11 +9,13 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "heatmap.js": "^2.0.5", + "immer": "^10.0.3", "keystats-common": "file:../common", "material-symbols": "^0.13.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "web-vitals": "^2.1.4" + "web-vitals": "^2.1.4", + "zustand": "^4.4.7" }, "scripts": { "start": "PORT=6001 node esbuild.js -s", diff --git a/src/client/src/App.tsx b/src/client/src/App.tsx index 73ecfd2..3c99559 100644 --- a/src/client/src/App.tsx +++ b/src/client/src/App.tsx @@ -5,21 +5,20 @@ import "./App.css"; import KeymapsComponent from "~/components/Keymaps.js"; import StatsComponent from "~/components/Stats.js"; import Dates from "~/components/Dates.js"; -import ApplicationStateProvider from "./state/appState.js"; +import { useStoreInit } from "./state/appState.js"; import Header from "./components/Header.js"; function App() { + useStoreInit(); return ( - -
-
- -
- - -
+
+
+ +
+ +
- +
); } diff --git a/src/client/src/components/Dates.tsx b/src/client/src/components/Dates.tsx index fbc3947..5a3ed01 100644 --- a/src/client/src/components/Dates.tsx +++ b/src/client/src/components/Dates.tsx @@ -1,10 +1,10 @@ -import React, { useState, useCallback, useRef, useEffect } from "react"; +import React, { useState, useCallback, useRef } from "react"; import dayjs from "dayjs"; import dayjsen from "dayjs/locale/en.js"; import isSameOrBefore from "dayjs/plugin/isSameOrBefore.js"; -import { useDatesActions, useDatesContext } from "~/state/date.js"; import * as classes from "./Dates.module.css"; +import useDates from "~/hooks/useDates.js"; dayjs.extend(isSameOrBefore); dayjs.locale("en-europe", { ...dayjsen, weekStart: 1 }); @@ -261,8 +261,7 @@ function DatePicker({ } export default function Dates(): React.ReactElement | null { - const { dates, date: selectedDate } = useDatesContext(); - const { setDate } = useDatesActions(); + const { date: selectedDate, dates, setDate } = useDates(); if (!dates) return null; diff --git a/src/client/src/components/Header.tsx b/src/client/src/components/Header.tsx index 0a7a268..f6e8048 100644 --- a/src/client/src/components/Header.tsx +++ b/src/client/src/components/Header.tsx @@ -1,29 +1,23 @@ import React, { useEffect } from "react"; -import { useFetchContext } from "~/state/fetch.js"; -import { useKeyboardActions, useKeyboardContext } from "~/state/keyboard.js"; -import { useKeyboardDataActions } from "~/state/keyboardData.js"; -import { useKeyboardsContext } from "~/state/keyboards.js"; import KeyboardSelector from "./KeyboardSelector.js"; +import useKeyboards from "~/hooks/useKeyboards.js"; +import useFetchStatus from "~/hooks/useFetchStatus.js"; + import * as classes from "./Header.module.css"; export default function Header(): React.ReactElement { - const { pendingCount, errors } = useFetchContext(); - const keyboards = useKeyboardsContext(); - const keyboard = useKeyboardContext(); - const { setKeyboard } = useKeyboardActions(); - const { refresh } = useKeyboardDataActions(); + const { loading, errors, refresh } = useFetchStatus(); + const { keyboard, keyboards, setKeyboard } = useKeyboards(); // Set the first keyboard of the list as the default - // TODO: use LocalStorage to keep that setting between page reloads + // If there were non in the storage useEffect(() => { if (!keyboard && keyboards && keyboards.length !== 0) { setKeyboard(keyboards[0]); } }, [keyboard, keyboards]); - const loading = pendingCount !== 0; - return (
diff --git a/src/client/src/components/Heatmap.tsx b/src/client/src/components/Heatmap.tsx index 43eb642..19c47fe 100644 --- a/src/client/src/components/Heatmap.tsx +++ b/src/client/src/components/Heatmap.tsx @@ -106,7 +106,11 @@ export default function HeatmapComponent({ // }, }); - heatmap.current.setData(formatData(matrix, total)); + try { + heatmap.current.setData(formatData(matrix, total)); + } catch (error) { + console.error(error); + } } }, [matrix, total]); diff --git a/src/client/src/components/KeyboardSelector.tsx b/src/client/src/components/KeyboardSelector.tsx index 56b3881..7c82731 100644 --- a/src/client/src/components/KeyboardSelector.tsx +++ b/src/client/src/components/KeyboardSelector.tsx @@ -14,16 +14,15 @@ export default function KeyboardSelector({ keyboards, onChange, }: KeyboardSelectorProps): React.ReactElement { - const self = useRef(null); - const kMap = useRef>(new Map()); - const options = useRef>([]); + const self = useRef(null); + const options = useRef>([]); const [visible, setVisible] = useState(false); const [hovered, setHovered] = useState(null); useEffect(() => { if (visible) { - const item = self.current?.querySelector( + const item = self.current?.querySelector( "[role='menuitem']:not([tabindex='-1'])", ); item?.focus(); @@ -32,14 +31,14 @@ export default function KeyboardSelector({ }, [selectedKeyboard, visible]); const onKeyUp = useCallback( - (event: React.KeyboardEvent) => { + (event: React.KeyboardEvent) => { console.log(event); event.preventDefault(); event.stopPropagation(); const el = event.target as HTMLDivElement; const kb = keyboards.find((kb) => kb.id === hovered); - const index = keyboards.indexOf(kb); - const item = self.current?.querySelector( + const index = keyboards.indexOf(kb!); + const item = self.current?.querySelector( "[role='menuitem']:not([tabindex='-1'])", ); @@ -91,7 +90,7 @@ export default function KeyboardSelector({ const menuItemOnKeyUp = useCallback( (kb: Keyboard) => { - return (event: React.KeyboardEvent) => { + return (event: React.KeyboardEvent) => { console.log("MI PRESS", event); event.preventDefault(); event.stopPropagation(); @@ -111,11 +110,11 @@ export default function KeyboardSelector({ className={classes.keyboardSelector} tabIndex={0} role="listbox" - onClick={(e) => { - if (e.target.getAttribute("role") !== "menuitem") { + onClick={(e: React.MouseEvent) => { + const target = e.target as HTMLButtonElement; + if (target.getAttribute("role") !== "menuitem") { setVisible(true); - // self.current?.focus(); - const item = self.current?.querySelector( + const item = self.current?.querySelector( "[role='menuitem']:not([tabindex='-1'])", ); item?.focus(); @@ -150,7 +149,7 @@ export default function KeyboardSelector({ setHovered(kb.id); }} onMouseOut={() => { - setHovered(selectedKeyboard?.id); + setHovered(selectedKeyboard?.id || null); }} onClick={() => { onChange(kb); diff --git a/src/client/src/components/Keymaps.tsx b/src/client/src/components/Keymaps.tsx index 5c84bc2..94ba037 100644 --- a/src/client/src/components/Keymaps.tsx +++ b/src/client/src/components/Keymaps.tsx @@ -1,11 +1,13 @@ import React from "react"; -import { useKeyboardData } from "~/state/keyboardData.js"; +import useCounts from "~/hooks/useCounts.js"; +import useKeymaps from "~/hooks/useKeymaps.js"; import HeatmapComponent from "./Heatmap.js"; import * as classes from "./Keymaps.module.css"; export default function Keymaps() { - const { counts, keymaps } = useKeyboardData(); + const { counts } = useCounts(); + const { keymaps } = useKeymaps(); return (
diff --git a/src/client/src/components/Stats.tsx b/src/client/src/components/Stats.tsx index 0bd68e4..6b81ba4 100644 --- a/src/client/src/components/Stats.tsx +++ b/src/client/src/components/Stats.tsx @@ -1,14 +1,11 @@ import React, { useCallback, useMemo } from "react"; import { Tabs, Tab } from "./Tabs.js"; import IconOrChar from "./IconOrChar.js"; -import { - Character, - // RepetitionsBody, - // TotalCountBody, -} from "keystats-common/dto/keyboard"; -import { useKeyboardData } from "~/state/keyboardData.js"; +import { Character } from "keystats-common/dto/keyboard"; import * as classes from "./Stats.module.css"; +import useCharacters from "~/hooks/useCharacters.js"; +import useCounts from "~/hooks/useCounts.js"; const numberFormater = new Intl.NumberFormat("en-US", {}); function formatNumber(n: number): string { @@ -33,11 +30,8 @@ const ROW_NAMES = ["Top row", "Home row", "Bottom row", "Thumb row"]; const p = (v: number, total: number): string => ((100 * v) / total).toFixed(2); export default function StatsComponent(): React.ReactElement { - const { - counts: totals, - characters, - handAndFingerUsage: repetitions, - } = useKeyboardData(); + const { characters } = useCharacters(); + const { counts: totals, handAndFingerUsage: repetitions } = useCounts(); const { totalKeypresses } = totals; let records: Character[] = []; let totalCharacters = 0; diff --git a/src/client/src/hooks/useCharacters.ts b/src/client/src/hooks/useCharacters.ts index 6a170ba..dc58dce 100644 --- a/src/client/src/hooks/useCharacters.ts +++ b/src/client/src/hooks/useCharacters.ts @@ -1,44 +1,8 @@ -import dayjs from "dayjs"; -import { useCallback, useEffect, useState } from "react"; +import { useAppState } from "~/state/appState.js"; +import { KeyboardDataState } from "~/state/keyboardData.js"; -import { getCharacterCounts, Keyboard } from "../lib/api.js"; -import { FilterQuery } from "keystats-common/dto/keyboard"; -import { useFetchActions } from "~/state/fetch.js"; +export default function useCharacters(): Pick { + const characters = useAppState((state) => state.characters); -type Data = Awaited>; - -export default function useCharacters( - keyboard: Keyboard | null, - date?: dayjs.Dayjs | null, -): [Data | null, () => Promise] { - const [state, setState] = useState(null); - const { setLoading, addError } = useFetchActions(); - - const fetchData = useCallback(async () => { - if (!keyboard) { - return; - } - setLoading(true); - try { - const filters: FilterQuery = {}; - if (date) { - filters.date = date; - } - const data = await getCharacterCounts(keyboard.id, filters); - setState(data); - } catch (error) { - console.error(error); - if (error instanceof Error) { - addError(error); - } - } finally { - setLoading(false); - } - }, [date, keyboard]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return [state, fetchData]; + return { characters }; } diff --git a/src/client/src/hooks/useCounts.ts b/src/client/src/hooks/useCounts.ts index 4686733..ee861b6 100644 --- a/src/client/src/hooks/useCounts.ts +++ b/src/client/src/hooks/useCounts.ts @@ -1,43 +1,15 @@ -import { useCallback, useEffect, useState } from "react"; - -import { getTotalCounts, Keyboard } from "../lib/api.js"; -import dayjs from "dayjs"; -import { FilterQuery } from "keystats-common/dto/keyboard"; -import { useFetchActions } from "~/state/fetch.js"; - -type Data = Awaited>; - -export default function useCounts( - keyboard: Keyboard | null, - date?: dayjs.Dayjs | null, -): [Data | null, () => Promise] { - const [state, setState] = useState(null); - const { setLoading, addError } = useFetchActions(); - - const fetchData = useCallback(async () => { - if (!keyboard) return; - setLoading(true); - try { - const filters: FilterQuery = {}; - if (date) { - filters.date = date; - } - - const data = await getTotalCounts(keyboard.id, filters); - setState(data); - } catch (error) { - console.error(error); - if (error instanceof Error) { - addError(error); - } - } finally { - setLoading(false); - } - }, [keyboard, date]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return [state, fetchData]; +import { useAppState } from "~/state/appState.js"; +import { KeyboardDataState } from "~/state/keyboardData.js"; + +export default function useCounts(): Pick< + KeyboardDataState, + "counts" | "handAndFingerUsage" +> { + const counts = useAppState((state) => state.counts); + const handAndFingerUsage = useAppState((state) => state.handAndFingerUsage); + + return { + counts, + handAndFingerUsage, + }; } diff --git a/src/client/src/hooks/useDates.ts b/src/client/src/hooks/useDates.ts index fa8d337..4717833 100644 --- a/src/client/src/hooks/useDates.ts +++ b/src/client/src/hooks/useDates.ts @@ -1,36 +1,17 @@ -import { useCallback, useEffect, useState } from "react"; +import { DateState } from "~/state/date.js"; +import { useAppState } from "~/state/appState.js"; -import { getDates, Keyboard } from "../lib/api.js"; -import dayjs from "dayjs"; -import { useFetchActions } from "~/state/fetch.js"; -type Data = dayjs.Dayjs[]; +export default function useDates(): Pick< + DateState, + "dates" | "date" | "setDate" +> { + const dates = useAppState((state) => state.dates); + const date = useAppState((state) => state.date); + const setDate = useAppState((state) => state.setDate); -export default function useData( - keyboard: Keyboard | null, -): [Data | null, () => Promise] { - const [state, setState] = useState(null); - const { setLoading, addError } = useFetchActions(); - - const fetchData = useCallback(async () => { - if (!keyboard) { - return; - } - setLoading(true); - try { - const data = await getDates(keyboard.id); - setState(data.map((date) => dayjs(date))); - } catch (error) { - console.error(error); - if (error instanceof Error) { - addError(error); - } - } - setLoading(false); - }, [keyboard]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return [state, fetchData]; + return { + date, + dates, + setDate, + }; } diff --git a/src/client/src/hooks/useFetchStatus.ts b/src/client/src/hooks/useFetchStatus.ts new file mode 100644 index 0000000..7cbc7f4 --- /dev/null +++ b/src/client/src/hooks/useFetchStatus.ts @@ -0,0 +1,13 @@ +import { useAppState } from "~/state/appState.js"; + +export default function useFetchStatus(): { + loading: boolean; + errors: Error[]; + refresh: () => Promise; +} { + const loading = useAppState((state) => state.pendingCount !== 0); + const errors = useAppState((state) => state.errors); + const refresh = useAppState((state) => state.refresh); + + return { loading, errors, refresh }; +} diff --git a/src/client/src/hooks/useHandAndFingerUsage.ts b/src/client/src/hooks/useHandAndFingerUsage.ts deleted file mode 100644 index 6423354..0000000 --- a/src/client/src/hooks/useHandAndFingerUsage.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useReducer, useCallback, useEffect, useState } from "react"; -import { setLoading, setError, setData } from "../store/actions.js"; -import dataReducer, { State } from "../store/reducer.js"; - -import { getHandAndFingerUsage, Keyboard } from "../lib/api.js"; -import dayjs from "dayjs"; -import { FilterQuery } from "keystats-common/dto/keyboard"; -import { useFetchActions } from "~/state/fetch.js"; -type Data = Awaited>; - -export default function useData( - keyboard: Keyboard | null, - date?: dayjs.Dayjs | null, -): [Data | null, () => Promise] { - const [state, setState] = useState(null); - const { setLoading, addError } = useFetchActions(); - - const fetchData = useCallback(async () => { - if (!keyboard) { - return; - } - setLoading(true); - try { - const filters: FilterQuery = {}; - if (date) { - filters.date = date; - } - const data = await getHandAndFingerUsage(keyboard.id, filters); - setState(data); - } catch (error) { - console.error(error); - if (error instanceof Error) { - addError(error); - } - } finally { - setLoading(false); - } - }, [keyboard, date]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return [state, fetchData]; -} diff --git a/src/client/src/hooks/useKeyboards.ts b/src/client/src/hooks/useKeyboards.ts index 36400eb..227ab52 100644 --- a/src/client/src/hooks/useKeyboards.ts +++ b/src/client/src/hooks/useKeyboards.ts @@ -1,34 +1,16 @@ -import { useReducer, useCallback, useEffect, useState } from "react"; -import { setLoading, setError, setData } from "../store/actions.js"; -import dataReducer, { State } from "../store/reducer.js"; +import { KeyboardState } from "~/state/keyboard.js"; +import { useAppState } from "~/state/appState.js"; +import { KeyboardsState } from "~/state/keyboards.js"; -import { listKeyboards } from "../lib/api.js"; -import { useFetchActions } from "~/state/fetch.js"; +export default function useKeyboards(): Pick< + KeyboardState & KeyboardsState, + "keyboard" | "keyboards" | "setKeyboard" +> { + const keyboards = useAppState((state) => state.keyboards); + const [keyboard, setKeyboard] = useAppState((state) => [ + state.keyboard, + state.setKeyboard, + ]); -type Data = Awaited>; - -export default function useData(): [Data | null, () => Promise] { - const [state, setState] = useState([]); - const { setLoading, addError } = useFetchActions(); - - const fetchData = useCallback(async () => { - setLoading(true); - try { - const data = await listKeyboards(); - setState(data); - } catch (error) { - console.error(error); - if (error instanceof Error) { - addError(error); - } - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return [state, fetchData]; + return { keyboard, keyboards, setKeyboard }; } diff --git a/src/client/src/hooks/useKeymaps.ts b/src/client/src/hooks/useKeymaps.ts index f5d738b..0090023 100644 --- a/src/client/src/hooks/useKeymaps.ts +++ b/src/client/src/hooks/useKeymaps.ts @@ -1,39 +1,10 @@ -import { useReducer, useCallback, useEffect, useState } from "react"; -import { setLoading, setError, setData } from "../store/actions.js"; -import dataReducer, { State } from "../store/reducer.js"; +import { KeyboardDataState } from "~/state/keyboardData.js"; +import { useAppState } from "~/state/appState.js"; -import { getKeyboardKeymaps, Keyboard } from "../lib/api.js"; -import { useFetchActions } from "~/state/fetch.js"; -type Data = Awaited>; +export default function useKeymaps(): Pick { + const keymaps = useAppState((state) => state.keymaps); -export default function useData( - keyboard: Keyboard | null, -): [Data | null, () => Promise] { - const [state, setState] = useState(null); - const { setLoading, addError } = useFetchActions(); - - const fetchData = useCallback(async () => { - if (!keyboard) { - return; - } - - setLoading(true); - try { - const data = await getKeyboardKeymaps(keyboard.id); - setState(data); - } catch (error) { - console.error(error); - if (error instanceof Error) { - addError(error); - } - } finally { - setLoading(false); - } - }, [keyboard]); - - useEffect(() => { - fetchData(); - }, [fetchData]); - - return [state, fetchData]; + return { + keymaps, + }; } diff --git a/src/client/src/state/appState.tsx b/src/client/src/state/appState.tsx index ba1f4ed..b7f558a 100644 --- a/src/client/src/state/appState.tsx +++ b/src/client/src/state/appState.tsx @@ -1,22 +1,39 @@ -import React from "react"; -import { DatesProvider } from "./date.js"; -import { FetchContextProvider } from "./fetch.js"; -import { KeyboardProvider } from "./keyboard.js"; -import { KeyboardDataProvider } from "./keyboardData.js"; -import { KeyboardsProvider } from "./keyboards.js"; +import { useEffect } from "react"; +import { create } from "zustand"; +import { DateState, dateStore } from "./date.js"; +import { FetchState, fetchStore } from "./fetch.js"; +import { KeyboardState, keyboardStore } from "./keyboard.js"; +import { KeyboardDataState, keyboardDataStore } from "./keyboardData.js"; +import { KeyboardsState, keyboardsStore } from "./keyboards.js"; -export default function ApplicationStateProvider({ - children, -}: React.PropsWithChildren): React.ReactElement { - return ( - - - - - {children} - - - - - ); +export const useAppState = create< + FetchState & KeyboardsState & KeyboardState & KeyboardDataState & DateState +>()((...a) => ({ + ...fetchStore(...a), + ...keyboardsStore(...a), + ...keyboardStore(...a), + ...keyboardDataStore(...a), + ...dateStore(...a), +})); + +export function useStoreInit() { + const keyboard = useAppState((state) => state.keyboard); + const date = useAppState((state) => state.date); + const fetchKeyboards = useAppState((state) => state.fetchKeyboards); + const fetchDates = useAppState((state) => state.fetchDates); + const fetchKeymaps = useAppState((state) => state.fetchKeymaps); + const refresh = useAppState((state) => state.refresh); + + useEffect(() => { + fetchKeyboards(); + fetchDates(); + }, []); + + useEffect(() => { + fetchKeymaps(); + }, [keyboard]); + + useEffect(() => { + refresh(); + }, [keyboard, date]); } diff --git a/src/client/src/state/date.tsx b/src/client/src/state/date.tsx index ecd4413..7c541ed 100644 --- a/src/client/src/state/date.tsx +++ b/src/client/src/state/date.tsx @@ -1,111 +1,45 @@ -import React, { - createContext, - useContext, - useEffect, - useMemo, - useReducer, -} from "react"; import dayjs from "dayjs"; -import { useKeyboardContext } from "./keyboard.js"; -import useDates from "~/hooks/useDates.js"; +import { KeyboardState } from "./keyboard.js"; +import { StateCreator } from "zustand"; +import { FetchState } from "./fetch.js"; +import { getDates } from "~/lib/api.js"; -interface DateContextType { - dates: dayjs.Dayjs[]; +export interface DateState { date: dayjs.Dayjs | null; -} - -interface SetDatesAction { - type: "SET_DATES"; - payload: dayjs.Dayjs[]; -} - -interface SetDateAction { - type: "SET_DATE"; - payload: dayjs.Dayjs | null; -} - -type DateActionType = SetDatesAction | SetDateAction; - -interface DateActionsType { - setDates: (dates: dayjs.Dayjs[]) => void; + dates: dayjs.Dayjs[]; setDate: (date: dayjs.Dayjs | null) => void; - refresh: () => void; -} - -const DateContext = createContext({ dates: [], date: null }); -const DateActionsContext = createContext({ - // eslint-disable-next-line @typescript-eslint/no-empty-function - setDates: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setDate: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - refresh: () => {}, + setDates: (dates: dayjs.Dayjs[]) => void; + fetchDates: () => Promise; +} + +export const dateStore: StateCreator< + DateState & FetchState & KeyboardState, + [], + [], + DateState +> = (set, get) => ({ + date: null, + dates: [], + setDate: (date: dayjs.Dayjs | null) => { + set(() => ({ date })); + }, + setDates: (dates: dayjs.Dayjs[]) => { + set(() => ({ dates })); + }, + async fetchDates(): Promise { + const { setLoading, addError, keyboard } = get(); + if (keyboard === null) return; + + try { + setLoading(true); + const data = await getDates(keyboard.id); + set(() => ({ dates: data.map((date) => dayjs(date)) })); + } catch (error) { + if (error instanceof Error) { + addError(error); + } + } finally { + setLoading(false); + } + }, }); - -export function useDatesContext() { - return useContext(DateContext); -} - -export function useDatesActions() { - return useContext(DateActionsContext); -} - -function dateReducer( - state: DateContextType, - action: DateActionType, -): DateContextType { - switch (action.type) { - case "SET_DATES": - return { - ...state, - dates: action.payload, - }; - - case "SET_DATE": - return { - ...state, - date: action.payload, - }; - } - - return state; -} - -export function DatesProvider({ - children, -}: React.PropsWithChildren): React.ReactElement { - const [state, dispatch] = useReducer(dateReducer, { - dates: [], - date: null, - }); - - const keyboard = useKeyboardContext(); - - const [dates, refreshDates] = useDates(keyboard); - - const actions = useMemo(() => { - return { - setDates(dates: dayjs.Dayjs[]) { - dispatch({ type: "SET_DATES", payload: dates }); - }, - setDate(date: dayjs.Dayjs | null) { - dispatch({ type: "SET_DATE", payload: date }); - }, - refresh() { - refreshDates(); - }, - }; - }, [dispatch]); - - useEffect(() => { - if (dates) actions.setDates(dates); - }, [dates, actions]); - - return ( - - - {children} - - - ); -} diff --git a/src/client/src/state/fetch.tsx b/src/client/src/state/fetch.tsx index 78d1f16..f309ab2 100644 --- a/src/client/src/state/fetch.tsx +++ b/src/client/src/state/fetch.tsx @@ -1,110 +1,40 @@ -import React, { createContext, useContext, useMemo, useReducer } from "react"; +import { StateCreator } from "zustand"; -interface FetchContext { +export interface FetchState { pendingCount: number; errors: Error[]; -} - -interface FetchActionContext { setLoading: (loading: boolean) => void; addError: (error: Error) => void; clearErrors: () => void; } -interface FetchSetLoading { - type: "SET_LOADING"; - payload: boolean; -} - -interface FetchAddError { - type: "ADD_ERROR"; - payload: Error; -} - -interface ClearErrors { - type: "CLEAR_ERRORS"; -} - -type FetchAction = FetchSetLoading | FetchAddError | ClearErrors; - -const Ctx = createContext({ +export const fetchStore: StateCreator = ( + set, + get, +) => ({ pendingCount: 0, errors: [], + setLoading: (loading: boolean) => { + get().clearErrors(); + let pendingCount = get().pendingCount; + if (loading) { + pendingCount++; + } else { + pendingCount--; + } + if (pendingCount < 0) { + pendingCount = 0; + } + set(() => ({ pendingCount: pendingCount })); + }, + addError: (error: Error) => { + set((state) => ({ + errors: state.errors.concat([error]), + })); + }, + clearErrors: () => { + set(() => ({ + errors: [], + })); + }, }); - -const ActionCtx = createContext({ - // eslint-disable-next-line @typescript-eslint/no-empty-function - setLoading: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - addError: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - clearErrors: () => {}, -}); - -export function useFetchContext() { - const ctx = useContext(Ctx); - - return ctx; -} - -export function useFetchActions() { - return useContext(ActionCtx); -} - -function fetchReducer(state: FetchContext, action: FetchAction): FetchContext { - switch (action.type) { - case "SET_LOADING": - if (action.payload) { - return { - pendingCount: state.pendingCount + 1, - errors: [], - }; - } else { - const pendingCount = state.pendingCount - 1; - return { - ...state, - pendingCount: pendingCount >= 0 ? pendingCount : 0, - }; - } - case "ADD_ERROR": - return { - ...state, - errors: [...state.errors, action.payload], - }; - - case "CLEAR_ERRORS": - return { - ...state, - errors: [], - }; - - default: - return state; - } -} - -export function FetchContextProvider({ - children, -}: React.PropsWithChildren): React.ReactElement { - const [state, dispatch] = useReducer(fetchReducer, { - pendingCount: 0, - errors: [], - }); - - const actions = useMemo(() => { - return { - setLoading: (loading: boolean) => { - dispatch({ type: "SET_LOADING", payload: loading }); - }, - addError: (error: Error) => - dispatch({ type: "ADD_ERROR", payload: error }), - clearErrors: () => dispatch({ type: "CLEAR_ERRORS" }), - }; - }, [dispatch]); - - return ( - - {children} - - ); -} diff --git a/src/client/src/state/keyboard.tsx b/src/client/src/state/keyboard.tsx index d64f690..e9c09c0 100644 --- a/src/client/src/state/keyboard.tsx +++ b/src/client/src/state/keyboard.tsx @@ -1,34 +1,6 @@ -import React, { createContext, useContext, useMemo, useReducer } from "react"; +import { StateCreator } from "zustand"; import { Keyboard } from "~/lib/api.js"; -interface SetKeybaordAction { - type: "SET_KEYBOARD"; - payload: Keyboard; -} - -const KeyboardContext = createContext(null); -const KeyboardActions = createContext<{ - setKeyboard: (keyboard: Keyboard) => void; -}>({ - // eslint-disable-next-line @typescript-eslint/no-empty-function - setKeyboard: () => {}, -}); - -export function useKeyboardContext() { - return useContext(KeyboardContext); -} - -export function useKeyboardActions() { - return useContext(KeyboardActions); -} - -function keyboardReducer( - state: Keyboard | null, - action: SetKeybaordAction, -): Keyboard | null { - return action.type === "SET_KEYBOARD" ? action.payload : state; -} - const LS_KEY = "keystats:keyboard"; function readFromLocalStorage(): Keyboard | null { const data = window.localStorage.getItem(LS_KEY); @@ -55,28 +27,22 @@ function writeToLocalStorage(keyboard: Keyboard | null) { } } -export function KeyboardProvider({ - children, -}: React.PropsWithChildren): React.ReactElement { - const [keyboard, dispatch] = useReducer( - keyboardReducer, - readFromLocalStorage(), - ); - - const actions = useMemo(() => { - return { - setKeyboard(keyboard: Keyboard) { - dispatch({ type: "SET_KEYBOARD", payload: keyboard }); - writeToLocalStorage(keyboard); - }, - }; - }, [dispatch]); - - return ( - - - {children} - - - ); +export interface KeyboardState { + keyboard: Keyboard | null; + setKeyboard: (keyboard: Keyboard) => void; } + +export const keyboardStore: StateCreator< + KeyboardState, + [], + [], + KeyboardState +> = (set) => ({ + keyboard: readFromLocalStorage(), + setKeyboard(keyboard: Keyboard) { + set(() => { + writeToLocalStorage(keyboard); + return { keyboard: keyboard }; + }); + }, +}); diff --git a/src/client/src/state/keyboardData.tsx b/src/client/src/state/keyboardData.tsx index 47c9415..149ce13 100644 --- a/src/client/src/state/keyboardData.tsx +++ b/src/client/src/state/keyboardData.tsx @@ -1,82 +1,38 @@ -import React, { - createContext, - useContext, - useEffect, - useMemo, - useReducer, -} from "react"; -import useCharacters from "~/hooks/useCharacters.js"; -import useCounts from "~/hooks/useCounts.js"; -import useHandAndFingerUsage from "~/hooks/useHandAndFingerUsage.js"; -import useKeymaps from "~/hooks/useKeymaps.js"; +import { FilterQuery } from "keystats-common/dto/keyboard"; +import { StateCreator } from "zustand"; import { getCharacterCounts, getHandAndFingerUsage, getKeyboardKeymaps, getTotalCounts, } from "~/lib/api.js"; -import { useDatesActions, useDatesContext } from "./date.js"; -import { useKeyboardContext } from "./keyboard.js"; - -interface KeyboardDataContextType { - keymaps: Awaited>; - characters: Awaited>; - counts: Awaited>; - handAndFingerUsage: Awaited>; -} - -interface SetKeymapsAction { - type: "SET_KEYMAPS"; - payload: Awaited>; -} - -interface SetCharactersAction { - type: "SET_CHARACTERS"; - payload: Awaited>; -} - -interface SetCounts { - type: "SET_COUNTS"; - payload: Awaited>; -} - -interface SetHandAndFingerUsage { - type: "SET_HAND_AND_FINGER_USAGE"; - payload: Awaited>; -} - -type KeyboardDataActionType = - | SetKeymapsAction - | SetCharactersAction - | SetCounts - | SetHandAndFingerUsage; - -interface KeyboardDataActionsType { - setKeymaps: (keymaps: Awaited>) => void; - setCharacters: ( - characters: Awaited>, - ) => void; - setCounts: (counts: Awaited>) => void; - setHandAndFingerUsage: ( - handAndFingerUsage: Awaited>, - ) => void; - refresh: () => void; +import { DateState } from "./date.js"; +import { FetchState } from "./fetch.js"; +import { KeyboardState } from "./keyboard.js"; + +type Keymaps = Awaited>; +type Characters = Awaited>; +type Counts = Awaited>; +type HandAndFingerUsage = Awaited>; + +export interface KeyboardDataState { + keymaps: Keymaps; + characters: Characters; + counts: Counts; + handAndFingerUsage: HandAndFingerUsage; + fetchKeymaps: () => Promise; + fetchCaracters: () => Promise; + fetchCounts: () => Promise; + fetchHandAndFingerUsage: () => Promise; + refresh: () => Promise; } -const KeyboardDataActions = createContext({ - // eslint-disable-next-line @typescript-eslint/no-empty-function - setKeymaps: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setCharacters: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setCounts: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setHandAndFingerUsage: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - refresh: () => {}, -}); - -const DEFAULT_KEYBOARD_DATA = { +export const keyboardDataStore: StateCreator< + KeyboardDataState & FetchState & KeyboardState & DateState, + [], + [], + KeyboardDataState +> = (set, get) => ({ keymaps: [], characters: { records: [], totalCharacters: 0 }, counts: { @@ -105,125 +61,86 @@ const DEFAULT_KEYBOARD_DATA = { handRepetitions: [[], []], fingerRepetitions: [], }, -}; - -const KeyboardDataContext = createContext( - DEFAULT_KEYBOARD_DATA, -); - -export function useKeyboardData() { - return useContext(KeyboardDataContext); -} - -export function useKeyboardDataActions() { - return useContext(KeyboardDataActions); -} - -function keyboardDataReducer( - state: KeyboardDataContextType, - action: KeyboardDataActionType, -): KeyboardDataContextType { - switch (action.type) { - case "SET_KEYMAPS": - return { - ...state, - keymaps: action.payload, - }; - - case "SET_CHARACTERS": - return { - ...state, - characters: action.payload, - }; - - case "SET_COUNTS": - return { - ...state, - counts: action.payload, - }; - - case "SET_HAND_AND_FINGER_USAGE": - return { - ...state, - handAndFingerUsage: action.payload, - }; - } - - return state; -} - -export function KeyboardDataProvider({ - children, -}: React.PropsWithChildren): React.ReactElement { - const [data, dispatch] = useReducer( - keyboardDataReducer, - DEFAULT_KEYBOARD_DATA, - ); - - const keyboard = useKeyboardContext(); - const { date } = useDatesContext(); - const { refresh: refreshDates } = useDatesActions(); - - const [keymaps, refreshKeymaps] = useKeymaps(keyboard); - const [characters, refreshCharacters] = useCharacters(keyboard, date); - const [counts, refreshCounts] = useCounts(keyboard, date); - const [handAndFingerUsage, refreshHandAndFingerUsage] = - useHandAndFingerUsage(keyboard); - - const actions = useMemo(() => { - return { - setKeymaps: (keymaps: Awaited>) => { - dispatch({ type: "SET_KEYMAPS", payload: keymaps }); - }, - setCharacters: ( - characters: Awaited>, - ) => { - dispatch({ type: "SET_CHARACTERS", payload: characters }); - }, - setCounts: (counts: Awaited>) => { - dispatch({ type: "SET_COUNTS", payload: counts }); - }, - setHandAndFingerUsage: ( - handAndFingerUsage: Awaited>, - ) => { - dispatch({ - type: "SET_HAND_AND_FINGER_USAGE", - payload: handAndFingerUsage, - }); - }, - refresh: () => { - refreshDates(); - refreshKeymaps(); - refreshCharacters(); - refreshCounts(); - refreshHandAndFingerUsage(); - }, - }; - }, [dispatch]); - - useEffect(() => { - if (keymaps) actions.setKeymaps(keymaps); - }, [keymaps, actions]); - - useEffect(() => { - if (characters) actions.setCharacters(characters); - }, [characters, actions]); + fetchKeymaps: async () => { + const { setLoading, addError, keyboard } = get(); + if (keyboard === null) return; + + try { + setLoading(true); + const data = await getKeyboardKeymaps(keyboard.id); + set(() => ({ keymaps: data })); + } catch (error) { + if (error instanceof Error) { + addError(error); + } + } finally { + setLoading(false); + } + }, + fetchCaracters: async () => { + const { setLoading, addError, keyboard, date } = get(); + if (keyboard === null) return; - useEffect(() => { - if (counts) actions.setCounts(counts); - }, [counts, actions]); + const filters: FilterQuery = {}; + if (date) { + filters.date = date; + } + try { + setLoading(true); + const data = await getCharacterCounts(keyboard.id, filters); + set(() => ({ characters: data })); + } catch (error) { + if (error instanceof Error) { + addError(error); + } + } finally { + setLoading(false); + } + }, + fetchCounts: async () => { + const { setLoading, addError, keyboard, date } = get(); + if (keyboard === null) return; - useEffect(() => { - if (handAndFingerUsage) { - actions.setHandAndFingerUsage(handAndFingerUsage); + const filters: FilterQuery = {}; + if (date) { + filters.date = date; } - }, [handAndFingerUsage, actions]); + try { + setLoading(true); + const data = await getTotalCounts(keyboard.id, filters); + set(() => ({ counts: data })); + } catch (error) { + if (error instanceof Error) { + addError(error); + } + } finally { + setLoading(false); + } + }, + fetchHandAndFingerUsage: async () => { + const { setLoading, addError, keyboard, date } = get(); + if (keyboard === null) return; - return ( - - - {children} - - - ); -} + const filters: FilterQuery = {}; + if (date) { + filters.date = date; + } + try { + setLoading(true); + const data = await getHandAndFingerUsage(keyboard.id, filters); + set(() => ({ handAndFingerUsage: data })); + } catch (error) { + if (error instanceof Error) { + addError(error); + } + } finally { + setLoading(false); + } + }, + refresh: async () => { + const { fetchCaracters, fetchCounts, fetchHandAndFingerUsage } = get(); + fetchCaracters(); + fetchCounts(); + fetchHandAndFingerUsage(); + }, +}); diff --git a/src/client/src/state/keyboards.tsx b/src/client/src/state/keyboards.tsx index ceec537..e6a0203 100644 --- a/src/client/src/state/keyboards.tsx +++ b/src/client/src/state/keyboards.tsx @@ -1,27 +1,32 @@ -import React, { createContext, useContext, useEffect } from "react"; -import { Keyboard } from "~/lib/api.js"; -import useKeyboards from "~/hooks/useKeyboards.js"; -import { useFetchActions } from "./fetch.js"; +import { listKeyboards } from "~/lib/api.js"; +import { StateCreator } from "zustand"; +import { FetchState } from "./fetch.js"; -type KeyboardsContextType = Keyboard[]; -const DEFAULT_KEYBOARDS_CONTEXT: Keyboard[] = []; - -const KeyboardsContext = createContext( - DEFAULT_KEYBOARDS_CONTEXT, -); - -export function useKeyboardsContext() { - return useContext(KeyboardsContext); +export interface KeyboardsState { + keyboards: Awaited>; + fetchKeyboards: () => Promise; } -export function KeyboardsProvider({ - children, -}: React.PropsWithChildren): React.ReactElement { - const [keyboards] = useKeyboards(); - - return ( - - {children} - - ); -} +export const keyboardsStore: StateCreator< + KeyboardsState & FetchState, + [], + [], + KeyboardsState +> = (set, get) => ({ + keyboards: [], + async fetchKeyboards() { + get().setLoading(true); + try { + const data = await listKeyboards(); + set(() => ({ + keyboards: data, + })); + } catch (error) { + if (error instanceof Error) { + get().addError(error); + } + } finally { + get().setLoading(false); + } + }, +}); diff --git a/src/client/src/store/actions.ts b/src/client/src/store/actions.ts deleted file mode 100644 index 0694356..0000000 --- a/src/client/src/store/actions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ActionTypes, Action } from "./constants.js"; - -export function setLoading( - value: boolean, -): Action { - return { type: ActionTypes.SetLoading, payload: value }; -} - -export function setError(value: Error): Action { - return { type: ActionTypes.SetError, payload: value }; -} - -export function setData(data: T): Action { - return { type: ActionTypes.SetData, payload: data }; -} diff --git a/src/client/src/store/constants.ts b/src/client/src/store/constants.ts deleted file mode 100644 index 1f89d6f..0000000 --- a/src/client/src/store/constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -export enum ActionTypes { - SetLoading = "set_loading", - SetError = "set_error", - SetData = "set_data", -} - -export type Action = { - type: A; - payload: P; -}; - -export type LoadingAction = Action; -export type ErrorAction = Action; -export type DataAction = Action; diff --git a/src/client/src/store/reducer.ts b/src/client/src/store/reducer.ts deleted file mode 100644 index 3cf07f5..0000000 --- a/src/client/src/store/reducer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - ActionTypes, - LoadingAction, - ErrorAction, - DataAction, -} from "./constants.js"; - -export interface State { - loading: boolean; - error: Error | null; - data: T | null; -} - -export default function dataReducer( - state: State, - action: LoadingAction | ErrorAction | DataAction, -) { - switch (action.type) { - case ActionTypes.SetLoading: - return { ...state, loading: action.payload }; - - case ActionTypes.SetError: - return { ...state, error: action.payload }; - - case ActionTypes.SetData: - return { ...state, data: action.payload }; - - default: - return state; - } -} diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index ed77fca..27f6397 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -20,7 +20,7 @@ "paths": { "~/*": ["src/*"] }, - "plugins": [{ "name": "typescript-plugin-css-modules" }] + "plugins": [{ "name": "typescript-plugin-css-modules" }] }, "include": ["./src", "../server/dto/", "../common/"] } diff --git a/src/client/yarn.lock b/src/client/yarn.lock index 1577b2e..452d413 100644 --- a/src/client/yarn.lock +++ b/src/client/yarn.lock @@ -588,7 +588,7 @@ dependencies: "@types/react" "*" -"@types/react@*": +"@types/react@*", "@types/react@>=16.8": version "18.2.28" resolved "https://registry.npmjs.org/@types/react/-/react-18.2.28.tgz" integrity sha512-ad4aa/RaaJS3hyGz0BGegdnSRXQBkd1CCYDCdNjBPg90UUpLgo+WlJqb9fMYUxtehmzF3PJaTWqRZjko6BRzBg== @@ -2108,6 +2108,11 @@ image-size@~0.5.0: resolved "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz" integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ== +immer@^10.0.3, immer@>=9.0: + version "10.0.3" + resolved "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz" + integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A== + immer@^9.0.7: version "9.0.21" resolved "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz" @@ -3195,7 +3200,7 @@ react-is@^18.0.0: resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== -react@^18.0.0, react@^18.2.0: +"react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^18.0.0, react@^18.2.0, react@>=16.8: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -3773,6 +3778,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -3933,3 +3943,10 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.4.7: + version "4.4.7" + resolved "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz" + integrity sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw== + dependencies: + use-sync-external-store "1.2.0"