Compare commits

...

2 Commits

11 changed files with 251 additions and 10 deletions
+4
View File
@@ -0,0 +1,4 @@
DISCORD_CONTACT_WEBHOOK="https://discord.com/api/webhooks/1166012977313497248/bDOfla0XnRgkBfDJYeRKs_QNe8P_S2kzXokuZNsLGfZN1mvfQAecYJlIrOC0qfMTOEpl"
DISCORD_ERROR_LOG_WEBHOOK="https://discord.com/api/webhooks/1174005798200938618/lLgOihkU9MlYwW2slPOTIK3q1kX80f04BSQbimdhHGFgOrN6zC5s2MlmtlSk8K6POZcJ"
LICHESS_CLIENT_ID="chess-scribe.org"
WEBSITE_URL="http://localhost:3000"
+1
View File
@@ -25,6 +25,7 @@
"@tanstack/react-start": "latest",
"@tanstack/router-plugin": "^1.132.0",
"@unpic/react": "^1.0.2",
"jotai": "^2.20.0",
"lichess-pgn-viewer": "^2.4.5",
"nitro": "npm:nitro-nightly@latest",
"react": "^19.2.0",
+28
View File
@@ -38,6 +38,9 @@ importers:
'@unpic/react':
specifier: ^1.0.2
version: 1.0.2(next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
jotai:
specifier: ^2.20.0
version: 2.20.0(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.15)(react@19.2.6)
lichess-pgn-viewer:
specifier: ^2.4.5
version: 2.4.5
@@ -1605,6 +1608,24 @@ packages:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
jotai@2.20.0:
resolution: {integrity: sha512-b5GAqgmXmXzB4WPaTH26ppk9Sl7AA9WSQX7yfdM+gJ1rFROiWcVbi97gFuN/yVCojOcbcvop2sfLL+fjxW0JVg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@babel/core': '>=7.0.0'
'@babel/template': '>=7.0.0'
'@types/react': '>=17.0.0'
react: '>=17.0.0'
peerDependenciesMeta:
'@babel/core':
optional: true
'@babel/template':
optional: true
'@types/react':
optional: true
react:
optional: true
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3559,6 +3580,13 @@ snapshots:
jiti@2.7.0: {}
jotai@2.20.0(@babel/core@7.29.7)(@babel/template@7.29.7)(@types/react@19.2.15)(react@19.2.6):
optionalDependencies:
'@babel/core': 7.29.7
'@babel/template': 7.29.7
'@types/react': 19.2.15
react: 19.2.6
js-tokens@4.0.0: {}
js-yaml@4.1.1:
+1
View File
@@ -15,6 +15,7 @@ const LichessButton = ({ onClickHandler }: IProps) => {
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
>
<title>Lichess Logo</title>
<path d="M38.956.5c-3.53.418-6.452.902-9.286 2.984C5.534 1.786-.692 18.533.68 29.364 3.493 50.214 31.918 55.785 41.329 41.7c-7.444 7.696-19.276 8.752-28.323 3.084C3.959 39.116-.506 27.392 4.683 17.567 9.873 7.742 18.996 4.535 29.03 6.405c2.43-1.418 5.225-3.22 7.655-3.187l-1.694 4.86 12.752 21.37c-.439 5.654-5.459 6.112-5.459 6.112-.574-1.47-1.634-2.942-4.842-6.036-3.207-3.094-17.465-10.177-15.788-16.207-2.001 6.967 10.311 14.152 14.04 17.663 3.73 3.51 5.426 6.04 5.795 6.756 0 0 9.392-2.504 7.838-8.927L37.4 7.171z" />
</svg>
</div>
+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 { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
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;
+57 -3
View File
@@ -1,13 +1,67 @@
import type { ChangeEvent, RefObject } from "react";
import LichessButton from "#/components/LichessButton.tsx";
import PgnViewer from "#/components/PgnViewer.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 (
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
<Section title="Convert PGN to PDF">
<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>
<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>
);
);
};
export default Chessboard;
+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;
}
};
+2 -1
View File
@@ -1,6 +1,7 @@
@import "tailwindcss";
@import "./styles/_accordion.css";
@import "styles/_accordion.css";
@import "./styles/_file-input.css";
@import './styles/_pgn-viewer.css';
@plugin "@tailwindcss/typography";
+7
View File
@@ -0,0 +1,7 @@
@import "./vendor/lichess-pgn-viewer.css";
.lpv-board {
max-width: 600px;
--board-color: #b58863;
font-family: "Noto Sans", sans-serif;
}
File diff suppressed because one or more lines are too long