set and clear game state using reducers - removed the need for global state management

This commit is contained in:
2026-06-01 11:22:56 +02:00
parent a69911d457
commit 38abf30ecc
5 changed files with 198 additions and 36 deletions
@@ -1,31 +0,0 @@
import LichessPgnViewer from "lichess-pgn-viewer";
import type PgnViewer from "lichess-pgn-viewer/pgnViewer";
import { useEffect, useRef } from "react";
interface IProps {
pgn: string;
}
const PgnViewerComponent = ({ pgn }: IProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const viewerRef = useRef<PgnViewer | null>(null);
useEffect(() => {
if (!containerRef.current) return;
viewerRef.current = LichessPgnViewer(containerRef.current, {
pgn,
scrollToMove: false,
});
return () => {
if (containerRef.current) {
containerRef.current.innerHTML = "";
}
};
}, [pgn]);
return <div ref={containerRef} className="lpv-board" />;
};
export default PgnViewerComponent;
+32
View File
@@ -0,0 +1,32 @@
import LichessPgnViewer from "lichess-pgn-viewer";
import type PgnViewerType from "lichess-pgn-viewer/pgnViewer";
import { useEffect, useRef } from "react";
interface IProps {
gamePgn: string;
}
const PgnViewer = ({ gamePgn }: IProps) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const viewerRef = useRef<PgnViewerType | null>(null);
useEffect(() => {
const element: HTMLElement | null = document.querySelector(".lpv-board");
if (!element) return;
viewerRef.current = LichessPgnViewer(element, {
pgn: gamePgn,
scrollToMove: false,
});
return () => {
if (containerRef.current) {
containerRef.current = null;
}
};
}, [gamePgn]);
return <div ref={containerRef} className="lpv-board" />;
};
export default PgnViewer;
+75 -1
View File
@@ -1,7 +1,81 @@
import { type ChangeEvent, useReducer, useRef } from "react";
import Chessboard from "#/pages/Chessboard.tsx"; import Chessboard from "#/pages/Chessboard.tsx";
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
const ChessboardContainer = () => { const ChessboardContainer = () => {
return <Chessboard />; const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
const fileInputRef = useRef<HTMLInputElement | null>(null);
const handleSavePgn = () => {
console.log("Save PGN");
};
const handleSavePdf = () => {
console.log("Save PDF");
};
const handleClearGame = () => {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
gameDispatch({
type: "CLEAR_GAME",
});
};
const handleLoadPgn = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const selectedFile = e.target.files?.[0];
if (selectedFile?.name.endsWith(".pgn")) {
const reader = new FileReader();
reader.onload = (event) => {
const pgnData = event.target?.result;
let pgnString = "";
if (pgnData) {
if (typeof pgnData === "string") {
pgnString = pgnData;
} else {
pgnString = new TextDecoder().decode(pgnData);
}
}
if (pgnString) {
gameDispatch({
type: "SET_GAME",
payload: { pgn: pgnString, headers: "" },
});
} else {
gameDispatch({
type: "CLEAR_GAME",
});
}
};
reader.readAsText(selectedFile);
} else {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
e.target.files = null;
gameDispatch({
type: "CLEAR_GAME",
});
}
};
return (
<Chessboard
gameState={gameState}
handleLoadPgn={handleLoadPgn}
handleClearGame={handleClearGame}
handleSavePgn={handleSavePgn}
handleSavePdf={handleSavePdf}
fileInputRef={fileInputRef}
/>
);
}; };
export default ChessboardContainer; export default ChessboardContainer;
+53 -4
View File
@@ -1,16 +1,65 @@
import type { ChangeEvent, RefObject } from "react";
import LichessButton from "#/components/LichessButton.tsx"; import LichessButton from "#/components/LichessButton.tsx";
import PgnViewerComponent from "#/components/LichessPgnViewerComponent.tsx"; import PgnViewer from "#/components/PgnViewer.tsx";
import Section from "#/components/Section.tsx"; import Section from "#/components/Section.tsx";
import type { IGameState } from "#/reducers/gameReducer.ts";
const Chessboard = () => { interface IProps {
gameState: IGameState;
handleLoadPgn: (e: ChangeEvent<HTMLInputElement>) => void;
handleClearGame: () => void;
handleSavePgn: () => void;
handleSavePdf: () => void;
fileInputRef: RefObject<HTMLInputElement | null>;
}
const Chessboard = ({
gameState,
handleLoadPgn,
handleClearGame,
handleSavePgn,
handleSavePdf,
fileInputRef,
}: IProps) => {
return ( return (
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]"> <main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
<Section title="Convert PGN to PDF"> <Section title="Convert PGN to PDF">
<LichessButton /> or{" "} <LichessButton /> or{" "}
<input type="file" id="file-input" className="file-input max-w-xs" /> <input
type="file"
id="file-input"
ref={fileInputRef}
className="file-input max-w-xs"
onChange={handleLoadPgn}
/>
</Section> </Section>
<PgnViewerComponent pgn="" /> <div className="flex gap-4">
<button
className="cursor-pointer"
type="button"
onClick={handleClearGame}
>
Clear Game
</button>
<button
className="cursor-pointer"
type="button"
onClick={handleSavePgn}
>
Save PGN
</button>
<button
className="cursor-pointer"
type="button"
onClick={handleSavePdf}
>
Save PDF
</button>
</div>
<PgnViewer gamePgn={gameState.pgn || ""} />
</main> </main>
); );
}; };
+38
View File
@@ -0,0 +1,38 @@
interface IDiagrams {
ply: number;
fen: string;
}
export interface IGameState {
pgn: string;
headers: string;
diagrams: IDiagrams[];
diagramClock: boolean;
}
type GameAction =
| { type: "SET_GAME"; payload: { pgn: string; headers: "" } }
| { type: "CLEAR_GAME" };
export const initialGameState = {
pgn: "",
headers: "",
diagrams: [],
diagramClock: false,
};
export const gameReducer = (state: IGameState, action: GameAction) => {
switch (action.type) {
case "SET_GAME":
return {
pgn: action.payload.pgn,
headers: "",
diagrams: [],
diagramClock: false,
};
case "CLEAR_GAME":
return initialGameState;
default:
return state;
}
};