mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 09:56:57 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
a5bb7dafd9
|
|||
|
1c8f1674b7
|
|||
|
19c2951cc3
|
|||
|
1251921ea1
|
|||
|
7d8f390a50
|
|||
|
1d6a9f8e6e
|
|||
|
87fa6997ee
|
|||
|
f841b7aeee
|
|||
|
9fab5e10f2
|
|||
|
be47dc449e
|
|||
|
a0d2247ff8
|
|||
|
95bef61a29
|
|||
|
1a9d26a6ea
|
|||
|
50664f4759
|
|||
|
98fbc4d253
|
|||
|
b8bd48885e
|
|||
|
42ef8f20e0
|
|||
|
6b21f0bfac
|
|||
|
401ac760cf
|
@@ -0,0 +1,16 @@
|
|||||||
|
// ZOD
|
||||||
|
FEN string
|
||||||
|
Env Vars
|
||||||
|
API base URL
|
||||||
|
|
||||||
|
- rearrange buttons / header space - collapse on smaller screens
|
||||||
|
|
||||||
|
- add / edit headers
|
||||||
|
- custom headers - title etc
|
||||||
|
- Loading Chessboard with suspense and spinner
|
||||||
|
- toasts
|
||||||
|
|
||||||
|
- Lichess login
|
||||||
|
- stringUtils - downloadString - move it out of try / catch and let program use the try/catch
|
||||||
|
|
||||||
|
- logger
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.6.0",
|
||||||
|
"react-toastify": "^11.1.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+15
@@ -53,6 +53,9 @@ importers:
|
|||||||
react-icons:
|
react-icons:
|
||||||
specifier: ^5.6.0
|
specifier: ^5.6.0
|
||||||
version: 5.6.0(react@19.2.6)
|
version: 5.6.0(react@19.2.6)
|
||||||
|
react-toastify:
|
||||||
|
specifier: ^11.1.0
|
||||||
|
version: 11.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.18
|
specifier: ^4.1.18
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
@@ -1858,6 +1861,12 @@ packages:
|
|||||||
react-is@17.0.2:
|
react-is@17.0.2:
|
||||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||||
|
|
||||||
|
react-toastify@11.1.0:
|
||||||
|
resolution: {integrity: sha512-e9h23x3phN0wbFeB6yovmWp7lobzV4CaCH0LO8nVP6H7Y+3GbcLpIzMm9dJhcp1RXbpyfvjgpfXqO80QAmn7sg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18 || ^19
|
||||||
|
react-dom: ^18 || ^19
|
||||||
|
|
||||||
react@19.2.6:
|
react@19.2.6:
|
||||||
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3839,6 +3848,12 @@ snapshots:
|
|||||||
|
|
||||||
react-is@17.0.2: {}
|
react-is@17.0.2: {}
|
||||||
|
|
||||||
|
react-toastify@11.1.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6):
|
||||||
|
dependencies:
|
||||||
|
clsx: 2.1.1
|
||||||
|
react: 19.2.6
|
||||||
|
react-dom: 19.2.6(react@19.2.6)
|
||||||
|
|
||||||
react@19.2.6: {}
|
react@19.2.6: {}
|
||||||
|
|
||||||
readdirp@5.0.0: {}
|
readdirp@5.0.0: {}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import LichessPgnViewer from "lichess-pgn-viewer";
|
import LichessPgnViewer from "lichess-pgn-viewer";
|
||||||
import type PgnViewerType from "lichess-pgn-viewer/pgnViewer";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import type { IPosition } from "#/interfaces.ts";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gamePgn: string;
|
gamePgn: string;
|
||||||
|
handlePlyChange: ({ ply, fen }: IPosition) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PgnViewer = ({ gamePgn }: IProps) => {
|
const PgnViewer = ({ gamePgn, handlePlyChange }: IProps) => {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const viewerRef = useRef<PgnViewerType | null>(null);
|
const viewerRef = useRef(null);
|
||||||
|
|
||||||
|
// TODO make thin, and pull functions out
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element: HTMLElement | null = document.querySelector(".lpv-board");
|
const element: HTMLElement | null = document.querySelector(".lpv-board");
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
@@ -19,12 +21,65 @@ const PgnViewer = ({ gamePgn }: IProps) => {
|
|||||||
scrollToMove: false,
|
scrollToMove: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// set initial position - TODO change to on file load
|
||||||
|
// handlePlyChange({
|
||||||
|
// ply: 0,
|
||||||
|
// fen: viewerRef.current?.curData().fen,
|
||||||
|
// });
|
||||||
|
|
||||||
|
const boardButtons = document.querySelectorAll(".lpv__controls");
|
||||||
|
const movesList = document.querySelectorAll(".lpv__moves");
|
||||||
|
if (!boardButtons || !movesList) return;
|
||||||
|
|
||||||
|
const clickHandlerDelay = () => {
|
||||||
|
setTimeout(() => handleClick(), 250);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hack using DOM manipulation
|
||||||
|
const boardEventListener = () => {
|
||||||
|
const variationTags = document.querySelector("variation");
|
||||||
|
const moves = [...document.querySelectorAll("move")].filter((m) => {
|
||||||
|
if (!variationTags) return m.className !== "empty";
|
||||||
|
return (
|
||||||
|
m.parentNode?.querySelector("variation") && m.className !== "empty"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const ply = moves.findIndex((m) => m.classList.contains("current")) + 1;
|
||||||
|
return { moves, ply };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const { ply } = boardEventListener();
|
||||||
|
const fen = viewerRef.current?.curData().fen;
|
||||||
|
handlePlyChange({ ply, fen });
|
||||||
|
};
|
||||||
|
|
||||||
|
boardButtons.forEach((button) => {
|
||||||
|
button.addEventListener("click", handleClick);
|
||||||
|
button.addEventListener("touchstart", handleClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
movesList.forEach((move) => {
|
||||||
|
move.addEventListener("click", clickHandlerDelay);
|
||||||
|
move.addEventListener("touchstart", clickHandlerDelay);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
containerRef.current = null;
|
containerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boardButtons.forEach((button) => {
|
||||||
|
button.removeEventListener("click", handleClick);
|
||||||
|
button.removeEventListener("touchstart", handleClick);
|
||||||
|
});
|
||||||
|
|
||||||
|
movesList.forEach((move) => {
|
||||||
|
move.removeEventListener("click", clickHandlerDelay);
|
||||||
|
move.removeEventListener("touchstart", clickHandlerDelay);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, [gamePgn]);
|
}, [gamePgn, handlePlyChange]);
|
||||||
|
|
||||||
return <div ref={containerRef} className="lpv-board" />;
|
return <div ref={containerRef} className="lpv-board" />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +1,87 @@
|
|||||||
import { type ChangeEvent, useReducer, useRef } from "react";
|
import {
|
||||||
|
type ChangeEvent,
|
||||||
|
useCallback,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import type { IPosition } from "#/interfaces.ts";
|
||||||
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
||||||
|
import { downloadPDF } from "#/utils/pdfUtils.ts";
|
||||||
import { buildPgnString, getHeaders } from "#/utils/pgnUtils.ts";
|
import { buildPgnString, getHeaders } from "#/utils/pgnUtils.ts";
|
||||||
import { downloadString } from "#/utils/stringUtils.ts";
|
import { downloadString } from "#/utils/stringUtils.ts";
|
||||||
|
|
||||||
export const useChessGame = () => {
|
export const useChessGame = () => {
|
||||||
const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
|
const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
|
||||||
|
|
||||||
|
const [currentPosition, setCurrentPosition] = useState<IPosition>({
|
||||||
|
ply: 0,
|
||||||
|
fen: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [generatingPdf, setGeneratingPdf] = useState(false);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const handlePlyChange = useCallback(({ ply, fen }: IPosition) => {
|
||||||
|
setCurrentPosition({ ply, fen });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSavePgn = () => {
|
const handleSavePgn = () => {
|
||||||
const pgnString = buildPgnString(gameState);
|
const pgnString = buildPgnString(gameState);
|
||||||
downloadString(pgnString, "game.pgn");
|
downloadString(pgnString, "game.pgn");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSavePdf = () => {
|
const handleSavePdf = async () => {
|
||||||
console.log("Save PDF");
|
try {
|
||||||
|
setGeneratingPdf(true);
|
||||||
|
const { diagrams, diagramClock } = gameState;
|
||||||
|
const pgnString = buildPgnString(gameState);
|
||||||
|
|
||||||
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
|
const response = await fetch(`${apiBaseUrl}/pdf`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
pgn: pgnString,
|
||||||
|
diagrams,
|
||||||
|
diagramClock,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
downloadPDF(await response.blob());
|
||||||
|
|
||||||
|
// TODO add metrics logger - add success to analytics
|
||||||
|
|
||||||
|
toast.success("PDF successfully generated!", {
|
||||||
|
toastId: "pdf-success",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// TODO toast and remove console but pass to logger
|
||||||
|
const body = await response.json();
|
||||||
|
console.error("Error saving PDF:", body.error);
|
||||||
|
|
||||||
|
toast.error("An error has occurred. Please try again later.", {
|
||||||
|
toastId: "pdf-error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// TODO add metrics / logger
|
||||||
|
console.error("Error saving PDF:", error);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
toast.error("An error has occurred. Please try again later.", {
|
||||||
|
toastId: "pdf-error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setGeneratingPdf(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearGame = () => {
|
const handleClearGame = () => {
|
||||||
@@ -72,6 +140,19 @@ export const useChessGame = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleDiagram = () => {
|
||||||
|
if (!gameState.pgn) return;
|
||||||
|
const { ply, fen } = currentPosition;
|
||||||
|
if (ply === 0) return;
|
||||||
|
const exists = gameState.diagrams.some((d) => d.ply === ply);
|
||||||
|
|
||||||
|
gameDispatch(
|
||||||
|
exists
|
||||||
|
? { type: "DELETE_DIAGRAM", payload: { ply } }
|
||||||
|
: { type: "ADD_DIAGRAM", payload: { ply, fen } },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gameState,
|
gameState,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
@@ -80,5 +161,9 @@ export const useChessGame = () => {
|
|||||||
handleClearGame,
|
handleClearGame,
|
||||||
handleLoadPgn,
|
handleLoadPgn,
|
||||||
handleToggleClock,
|
handleToggleClock,
|
||||||
|
handlePlyChange,
|
||||||
|
handleToggleDiagram,
|
||||||
|
currentPosition,
|
||||||
|
generatingPdf,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
export interface IPosition {
|
||||||
|
ply: number;
|
||||||
|
fen: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IHeader {
|
||||||
|
event: string;
|
||||||
|
site: string;
|
||||||
|
date: string;
|
||||||
|
round: string;
|
||||||
|
white: string;
|
||||||
|
black: string;
|
||||||
|
result: string;
|
||||||
|
eco: string;
|
||||||
|
whiteElo: string;
|
||||||
|
blackElo: string;
|
||||||
|
plyCount: string;
|
||||||
|
eventDate: string;
|
||||||
|
source: string;
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGameState {
|
||||||
|
pgn: string;
|
||||||
|
headers: IHeader;
|
||||||
|
diagrams: IPosition[];
|
||||||
|
diagramClock: boolean;
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useChessGame } from "#/hooks/useChessGame.ts";
|
|
||||||
import Chessboard from "#/pages/Chessboard.tsx";
|
|
||||||
|
|
||||||
const ChessboardContainer = () => {
|
|
||||||
const chessGameProps = useChessGame();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chessboard
|
|
||||||
gameState={chessGameProps.gameState}
|
|
||||||
handleLoadPgn={chessGameProps.handleLoadPgn}
|
|
||||||
handleClearGame={chessGameProps.handleClearGame}
|
|
||||||
handleSavePgn={chessGameProps.handleSavePgn}
|
|
||||||
handleSavePdf={chessGameProps.handleSavePdf}
|
|
||||||
handleToggleClock={chessGameProps.handleToggleClock}
|
|
||||||
fileInputRef={chessGameProps.fileInputRef}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChessboardContainer;
|
|
||||||
+87
-54
@@ -1,28 +1,23 @@
|
|||||||
import type { ChangeEvent, RefObject } from "react";
|
|
||||||
import LichessButton from "#/components/LichessButton.tsx";
|
import LichessButton from "#/components/LichessButton.tsx";
|
||||||
import PgnViewer from "#/components/PgnViewer.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";
|
import { useChessGame } from "#/hooks/useChessGame.ts";
|
||||||
|
|
||||||
interface IProps {
|
const Chessboard = () => {
|
||||||
gameState: IGameState;
|
const {
|
||||||
handleLoadPgn: (e: ChangeEvent<HTMLInputElement>) => void;
|
gameState,
|
||||||
handleClearGame: () => void;
|
fileInputRef,
|
||||||
handleSavePgn: () => void;
|
currentPosition,
|
||||||
handleSavePdf: () => void;
|
generatingPdf,
|
||||||
handleToggleClock: () => void;
|
handleLoadPgn,
|
||||||
fileInputRef: RefObject<HTMLInputElement | null>;
|
handleClearGame,
|
||||||
}
|
handleSavePgn,
|
||||||
|
handleSavePdf,
|
||||||
|
handleToggleClock,
|
||||||
|
handleToggleDiagram,
|
||||||
|
handlePlyChange,
|
||||||
|
} = useChessGame();
|
||||||
|
|
||||||
const Chessboard = ({
|
|
||||||
gameState,
|
|
||||||
handleLoadPgn,
|
|
||||||
handleClearGame,
|
|
||||||
handleSavePgn,
|
|
||||||
handleSavePdf,
|
|
||||||
handleToggleClock,
|
|
||||||
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">
|
||||||
@@ -36,43 +31,81 @@ const Chessboard = ({
|
|||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<button
|
<div>
|
||||||
className="cursor-pointer"
|
<PgnViewer
|
||||||
type="button"
|
gamePgn={gameState.pgn || ""}
|
||||||
onClick={handleClearGame}
|
handlePlyChange={handlePlyChange}
|
||||||
>
|
/>
|
||||||
Clear Game
|
<div className="grid grid-cols-3 gap-2 max-w-150 mt-2">
|
||||||
</button>
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleClearGame}
|
||||||
|
>
|
||||||
|
Clear Game
|
||||||
|
</button>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<label
|
||||||
|
htmlFor="diagram-add"
|
||||||
|
className="text-sm text-(--base-content)"
|
||||||
|
>
|
||||||
|
Select Diagram
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="diagram-toggle"
|
||||||
|
name="diagram-toggle"
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox-accent"
|
||||||
|
checked={gameState.diagrams.some(
|
||||||
|
(d) => d.ply === currentPosition.ply,
|
||||||
|
)}
|
||||||
|
onChange={handleToggleDiagram}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<label className="toggle text-sm text-(--base-content)">
|
||||||
className="cursor-pointer"
|
Render move times
|
||||||
type="button"
|
<input
|
||||||
onClick={handleSavePgn}
|
id="render-times"
|
||||||
disabled={!gameState.pgn}
|
name="render-times"
|
||||||
>
|
type="checkbox"
|
||||||
Save PGN
|
checked={gameState.diagramClock}
|
||||||
</button>
|
onChange={handleToggleClock}
|
||||||
|
/>
|
||||||
|
<span className="slider round" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<div className="flex gap-4">
|
||||||
className="cursor-pointer"
|
{/*<button*/}
|
||||||
type="button"
|
{/* className="btn btn-primary"*/}
|
||||||
onClick={handleSavePdf}
|
{/* type="button"*/}
|
||||||
disabled={!gameState.pgn}
|
{/* onClick={handleClearGame}*/}
|
||||||
>
|
{/*>*/}
|
||||||
Save PDF
|
{/* Clear Game*/}
|
||||||
</button>
|
{/*</button>*/}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSavePgn}
|
||||||
|
disabled={!gameState.pgn}
|
||||||
|
>
|
||||||
|
Save PGN
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSavePdf}
|
||||||
|
disabled={!gameState.pgn || generatingPdf}
|
||||||
|
>
|
||||||
|
Save PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PgnViewer gamePgn={gameState.pgn || ""} />
|
|
||||||
<label htmlFor="diagram-clock">
|
|
||||||
Render Clock Times
|
|
||||||
<input
|
|
||||||
id="diagram-clock"
|
|
||||||
name="diagram-clock"
|
|
||||||
type="checkbox"
|
|
||||||
onChange={handleToggleClock}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-4
@@ -23,10 +23,7 @@ const Home = () => (
|
|||||||
Upload and convert your games into a book format, along with variation
|
Upload and convert your games into a book format, along with variation
|
||||||
and annotations.
|
and annotations.
|
||||||
</h2>
|
</h2>
|
||||||
<Link
|
<Link to="/chessboard" className="btn btn-primary btn-large">
|
||||||
to="/chessboard"
|
|
||||||
className="rounded-3xl text-xl font-bold btn btn-primary"
|
|
||||||
>
|
|
||||||
Get Started
|
Get Started
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,37 +1,10 @@
|
|||||||
interface IDiagrams {
|
import type { IGameState, IHeader, IPosition } from "#/interfaces.ts";
|
||||||
ply: number;
|
|
||||||
fen: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IHeader {
|
|
||||||
event: string;
|
|
||||||
site: string;
|
|
||||||
date: string;
|
|
||||||
round: string;
|
|
||||||
white: string;
|
|
||||||
black: string;
|
|
||||||
result: string;
|
|
||||||
eco: string;
|
|
||||||
whiteElo: string;
|
|
||||||
blackElo: string;
|
|
||||||
plyCount: string;
|
|
||||||
eventDate: string;
|
|
||||||
source: string;
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGameState {
|
|
||||||
pgn: string;
|
|
||||||
headers: IHeader;
|
|
||||||
diagrams: IDiagrams[];
|
|
||||||
diagramClock: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
type GameAction =
|
type GameAction =
|
||||||
| { type: "SET_GAME"; payload: { pgn: string; headers: IHeader } }
|
| { type: "SET_GAME"; payload: { pgn: string; headers: IHeader } }
|
||||||
| { type: "CLEAR_GAME" }
|
| { type: "CLEAR_GAME" }
|
||||||
| { type: "SET_HEADERS"; payload: IHeader }
|
| { type: "SET_HEADERS"; payload: IHeader }
|
||||||
| { type: "ADD_DIAGRAM"; payload: IDiagrams }
|
| { type: "ADD_DIAGRAM"; payload: IPosition }
|
||||||
| { type: "DELETE_DIAGRAM"; payload: { ply: number } }
|
| { type: "DELETE_DIAGRAM"; payload: { ply: number } }
|
||||||
| { type: "TOGGLE_DIAGRAM_CLOCK" };
|
| { type: "TOGGLE_DIAGRAM_CLOCK" };
|
||||||
|
|
||||||
@@ -86,7 +59,7 @@ export const gameReducer = (state: IGameState, action: GameAction) => {
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
diagrams: state.diagrams.filter(
|
diagrams: state.diagrams.filter(
|
||||||
(diagram: IDiagrams) => diagram.ply !== action.payload.ply,
|
(diagram: IPosition) => diagram.ply !== action.payload.ply,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
case "TOGGLE_DIAGRAM_CLOCK":
|
case "TOGGLE_DIAGRAM_CLOCK":
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TanStackDevtools } from "@tanstack/react-devtools";
|
|||||||
import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
|
import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
||||||
import { type ReactNode, Suspense } from "react";
|
import { type ReactNode, Suspense } from "react";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
|
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
|
||||||
import Footer from "../components/Footer";
|
import Footer from "../components/Footer";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
@@ -40,6 +41,7 @@ function RootDocument({ children }: { children: ReactNode }) {
|
|||||||
<body className="font-sans antialiased">
|
<body className="font-sans antialiased">
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
|
<ToastContainer position="bottom-right" />
|
||||||
<Footer />
|
<Footer />
|
||||||
<TanStackDevtools
|
<TanStackDevtools
|
||||||
config={{
|
config={{
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import ChessboardContainer from "#/pages/Chessboard.container.tsx";
|
import Chessboard from "#/pages/Chessboard.tsx";
|
||||||
|
|
||||||
export const Route = createFileRoute("/chessboard")({
|
export const Route = createFileRoute("/chessboard")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <ChessboardContainer />;
|
return <Chessboard />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "styles/_accordion.css";
|
@import "styles/_accordion.css";
|
||||||
|
@import "./styles/_input.css";
|
||||||
|
@import "./styles/_button.css";
|
||||||
@import "./styles/_file-input.css";
|
@import "./styles/_file-input.css";
|
||||||
@import './styles/_pgn-viewer.css';
|
@import './styles/_pgn-viewer.css';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
height: 50px;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
padding: 1rem 4rem;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: var(--bg-base);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
color: var(--accent);
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--accent);
|
||||||
|
color: var(--bg-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* Checkbox */
|
||||||
|
.checkbox-accent {
|
||||||
|
accent-color: var(--accent);
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle */
|
||||||
|
.toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
background-color: #ccc;
|
||||||
|
-webkit-transition: 0.4s;
|
||||||
|
transition: 0.4s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
left: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: 0.4s;
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: 0 0 1px var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(16px);
|
||||||
|
-ms-transform: translateX(16px);
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { IGameState } from "#/reducers/gameReducer.ts";
|
import type { IGameState } from "#/interfaces.ts";
|
||||||
|
|
||||||
export const getHeaders = (pgn: string) => {
|
export const getHeaders = (pgn: string) => {
|
||||||
const pgnHeader = pgn.split(/\n\n/g)[0];
|
const pgnHeader = pgn.split(/\n\n/g)[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user