Use zustand for state management

This commit is contained in:
Adrian Rumpold
2025-04-24 11:14:05 +02:00
parent a15ceaa6e3
commit e8a9a42ef4
5 changed files with 76 additions and 33 deletions

View File

@@ -12,7 +12,8 @@
"@tanstack/react-query-devtools": "^5.74.6", "@tanstack/react-query-devtools": "^5.74.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.5.1" "react-router-dom": "^7.5.1",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",
@@ -1471,7 +1472,7 @@
"version": "19.1.2", "version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@@ -1963,7 +1964,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
@@ -3436,6 +3437,35 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zustand": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
} }
} }
} }

View File

@@ -14,7 +14,8 @@
"@tanstack/react-query-devtools": "^5.74.6", "@tanstack/react-query-devtools": "^5.74.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-router-dom": "^7.5.1" "react-router-dom": "^7.5.1",
"zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.22.0", "@eslint/js": "^9.22.0",

View File

@@ -1,14 +1,17 @@
import { useState } from "react";
import Panel from "./components/Panel";
import { useQueries } from "@tanstack/react-query"; import { useQueries } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./App.css";
import ArticleSelector from "./components/ArticleSelector";
import TOC from "./components/TOC";
import { getArticleIds, getToc } from "./lib/api"; import { getArticleIds, getToc } from "./lib/api";
import { Language } from "./lib/types"; import { Language } from "./lib/types";
import ArticleSelector from "./components/ArticleSelector";
import Panel from "./components/Panel";
import TOC from "./components/TOC";
import useUIStore from "./store/uiStore";
import "./App.css";
type Props = { type Props = {
celexId: string; celexId: string;
articleId: number; articleId: number;
@@ -16,10 +19,7 @@ type Props = {
function App({ celexId, articleId }: Props) { function App({ celexId, articleId }: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const [numPanels, setNumPanels] = useState(1); const { numPanels, addPanel } = useUIStore();
const [selectedParagraphId, setSelectedParagraphId] = useState<string | null>(
null
);
const results = useQueries({ const results = useQueries({
queries: [ queries: [
@@ -76,9 +76,7 @@ function App({ celexId, articleId }: Props) {
selectedId={articleId} selectedId={articleId}
onSelected={(id) => navigate(`/${celexId}/articles/${id}`)} onSelected={(id) => navigate(`/${celexId}/articles/${id}`)}
/> />
<button onClick={() => setNumPanels((prev) => prev + 1)}> <button onClick={addPanel}>Add Panel</button>
Add Panel
</button>
</div> </div>
<div className="panel-container"> <div className="panel-container">
<TOC <TOC
@@ -94,8 +92,6 @@ function App({ celexId, articleId }: Props) {
Object.values(Language)[index % Object.values(Language).length] Object.values(Language)[index % Object.values(Language).length]
} }
articleId={articleId} articleId={articleId}
selectedParagraphId={selectedParagraphId || undefined}
onParagraphSelected={setSelectedParagraphId}
/> />
))} ))}
</div> </div>

View File

@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { getArticle } from "../lib/api"; import { getArticle } from "../lib/api";
import { Language } from "../lib/types"; import { Language } from "../lib/types";
import useUIStore from "../store/uiStore";
import LanguageSwitcher from "./LanguageSwitcher"; import LanguageSwitcher from "./LanguageSwitcher";
import "./Panel.css"; import "./Panel.css";
@@ -9,17 +10,11 @@ type PanelProps = {
celexId: string; celexId: string;
language?: Language; language?: Language;
articleId: number; articleId: number;
selectedParagraphId?: string;
onParagraphSelected(paragraphId: string): void;
}; };
function Panel({ function Panel({ celexId, language, articleId }: PanelProps) {
celexId, const { selectedParagraphId, setSelectedParagraphId } = useUIStore();
language,
articleId,
onParagraphSelected,
selectedParagraphId,
}: PanelProps) {
const [lang, setLang] = useState(language || Language.ENG); const [lang, setLang] = useState(language || Language.ENG);
const articleRef = useRef<HTMLDivElement>(null); const articleRef = useRef<HTMLDivElement>(null);
const { data, isPending, error } = useQuery({ const { data, isPending, error } = useQuery({
@@ -39,11 +34,11 @@ function Panel({
p.classList.remove("highlight"); p.classList.remove("highlight");
}); });
if (selectedParagraphId) { if (selectedParagraphId) {
const selectedParagraph = Array.from(paragraphs).find( const el = Array.from(paragraphs).find(
(p) => p.getAttribute("data-paragraph-id") === selectedParagraphId (p) => p.getAttribute("data-paragraph-id") === selectedParagraphId
); );
if (selectedParagraph) { if (el) {
selectedParagraph.classList.add("highlight"); el.classList.add("highlight");
} }
} }
@@ -58,7 +53,7 @@ function Panel({
return; return;
} }
target.classList.add("highlight"); target.classList.add("highlight");
onParagraphSelected(paragraphId); setSelectedParagraphId(paragraphId);
}; };
paragraphs.forEach((element) => { paragraphs.forEach((element) => {
@@ -72,7 +67,7 @@ function Panel({
element.removeEventListener("click", handleClick(element)); element.removeEventListener("click", handleClick(element));
}); });
}; };
}, [articleRef, data, selectedParagraphId, onParagraphSelected]); }, [articleRef, data, selectedParagraphId, setSelectedParagraphId]);
if (isPending) return "Loading..."; if (isPending) return "Loading...";
if (error) return "An error has occurred: " + error.message; if (error) return "An error has occurred: " + error.message;

View File

@@ -0,0 +1,21 @@
import { create } from "zustand";
interface UIState {
numPanels: number;
addPanel: () => void;
removePanel: () => void;
selectedParagraphId: string | null;
setSelectedParagraphId: (selectedParagraphId: string | null) => void;
}
const useUIStore = create<UIState>((set) => ({
numPanels: 1,
selectedParagraphId: null,
addPanel: () => set((state) => ({ numPanels: state.numPanels + 1 })),
removePanel: () =>
set((state) => ({ numPanels: Math.max(state.numPanels - 1, 1) })),
setSelectedParagraphId: (selectedParagraphId: string | null) =>
set({ selectedParagraphId }),
}));
export default useUIStore;