mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8be89710f4
|
|||
|
d6536506e3
|
|||
|
61ff18a093
|
|||
|
97dd35f9a9
|
|||
|
99279c70bb
|
|||
|
de68203a80
|
|||
|
b964e12f0e
|
|||
|
324e4eac31
|
@@ -1,4 +0,0 @@
|
||||
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"
|
||||
@@ -25,7 +25,6 @@
|
||||
"@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",
|
||||
|
||||
Generated
-28
@@ -38,9 +38,6 @@ 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
|
||||
@@ -1608,24 +1605,6 @@ 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==}
|
||||
|
||||
@@ -3580,13 +3559,6 @@ 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:
|
||||
|
||||
@@ -25,18 +25,17 @@ export default function Footer() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto flex gap-2">
|
||||
<Link
|
||||
to="/"
|
||||
<a
|
||||
href={import.meta.env.VITE_GITHUB_URL} // TODO ZOD
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out"
|
||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
|
||||
>
|
||||
<LuGithub />
|
||||
</Link>
|
||||
</a>
|
||||
|
||||
<Link
|
||||
to="/"
|
||||
target="_blank"
|
||||
to="/contact"
|
||||
rel="noopener noreferrer"
|
||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { type ChangeEvent, useReducer, useRef } from "react";
|
||||
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
||||
import { buildPgnString, getHeaders } from "#/utils/pgnUtils.ts";
|
||||
import { downloadString } from "#/utils/stringUtils.ts";
|
||||
|
||||
export const useChessGame = () => {
|
||||
const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleSavePgn = () => {
|
||||
const pgnString = buildPgnString(gameState);
|
||||
downloadString(pgnString, "game.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) {
|
||||
const headers = {
|
||||
...getHeaders(pgnString),
|
||||
title: "",
|
||||
subtitle: "",
|
||||
};
|
||||
gameDispatch({
|
||||
type: "SET_GAME",
|
||||
payload: { pgn: pgnString, headers: headers },
|
||||
});
|
||||
} else {
|
||||
e.target.files = null;
|
||||
handleClearGame();
|
||||
}
|
||||
};
|
||||
reader.readAsText(selectedFile);
|
||||
} else {
|
||||
handleClearGame();
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleClock = () => {
|
||||
gameDispatch({
|
||||
type: "TOGGLE_DIAGRAM_CLOCK",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
gameState,
|
||||
fileInputRef,
|
||||
handleSavePgn,
|
||||
handleSavePdf,
|
||||
handleClearGame,
|
||||
handleLoadPgn,
|
||||
handleToggleClock,
|
||||
};
|
||||
};
|
||||
@@ -1,79 +1,18 @@
|
||||
import { type ChangeEvent, useReducer, useRef } from "react";
|
||||
import { useChessGame } from "#/hooks/useChessGame.ts";
|
||||
import Chessboard from "#/pages/Chessboard.tsx";
|
||||
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
||||
|
||||
const ChessboardContainer = () => {
|
||||
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",
|
||||
});
|
||||
}
|
||||
};
|
||||
const chessGameProps = useChessGame();
|
||||
|
||||
return (
|
||||
<Chessboard
|
||||
gameState={gameState}
|
||||
handleLoadPgn={handleLoadPgn}
|
||||
handleClearGame={handleClearGame}
|
||||
handleSavePgn={handleSavePgn}
|
||||
handleSavePdf={handleSavePdf}
|
||||
fileInputRef={fileInputRef}
|
||||
gameState={chessGameProps.gameState}
|
||||
handleLoadPgn={chessGameProps.handleLoadPgn}
|
||||
handleClearGame={chessGameProps.handleClearGame}
|
||||
handleSavePgn={chessGameProps.handleSavePgn}
|
||||
handleSavePdf={chessGameProps.handleSavePdf}
|
||||
handleToggleClock={chessGameProps.handleToggleClock}
|
||||
fileInputRef={chessGameProps.fileInputRef}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ interface IProps {
|
||||
handleClearGame: () => void;
|
||||
handleSavePgn: () => void;
|
||||
handleSavePdf: () => void;
|
||||
handleToggleClock: () => void;
|
||||
fileInputRef: RefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
@@ -19,6 +20,7 @@ const Chessboard = ({
|
||||
handleClearGame,
|
||||
handleSavePgn,
|
||||
handleSavePdf,
|
||||
handleToggleClock,
|
||||
fileInputRef,
|
||||
}: IProps) => {
|
||||
return (
|
||||
@@ -47,6 +49,7 @@ const Chessboard = ({
|
||||
className="cursor-pointer"
|
||||
type="button"
|
||||
onClick={handleSavePgn}
|
||||
disabled={!gameState.pgn}
|
||||
>
|
||||
Save PGN
|
||||
</button>
|
||||
@@ -55,11 +58,21 @@ const Chessboard = ({
|
||||
className="cursor-pointer"
|
||||
type="button"
|
||||
onClick={handleSavePdf}
|
||||
disabled={!gameState.pgn}
|
||||
>
|
||||
Save PDF
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -7,10 +7,10 @@ import examplePdf1 from "@/assets/images/examplepdf1.webp?url";
|
||||
import examplePdf2 from "@/assets/images/examplepdf2.webp?url";
|
||||
|
||||
const Home = () => (
|
||||
<main className="px-4 pb-8 pt-14 place-self-center">
|
||||
<main className="px-4 pb-8 pt-14">
|
||||
<section
|
||||
id="hero"
|
||||
className="grid max-w-screen-2xl items-center gap-8 p-8 text-center md:grid-cols-2 md:text-left"
|
||||
className="grid max-w-screen-2xl items-center gap-8 p-8 text-center md:grid-cols-2 md:text-left mx-auto"
|
||||
>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-base-content md:text-4xl">
|
||||
@@ -37,7 +37,7 @@ const Home = () => (
|
||||
alt="Example PDF 1"
|
||||
width={500}
|
||||
height={500}
|
||||
priority={true}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -3,20 +3,60 @@ interface IDiagrams {
|
||||
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: string;
|
||||
headers: IHeader;
|
||||
diagrams: IDiagrams[];
|
||||
diagramClock: boolean;
|
||||
}
|
||||
|
||||
type GameAction =
|
||||
| { type: "SET_GAME"; payload: { pgn: string; headers: "" } }
|
||||
| { type: "CLEAR_GAME" };
|
||||
| { type: "SET_GAME"; payload: { pgn: string; headers: IHeader } }
|
||||
| { type: "CLEAR_GAME" }
|
||||
| { type: "SET_HEADERS"; payload: IHeader }
|
||||
| { type: "ADD_DIAGRAM"; payload: IDiagrams }
|
||||
| { type: "DELETE_DIAGRAM"; payload: { ply: number } }
|
||||
| { type: "TOGGLE_DIAGRAM_CLOCK" };
|
||||
|
||||
export const initialGameState = {
|
||||
const defaultHeaderFields = {
|
||||
event: "",
|
||||
site: "",
|
||||
date: "",
|
||||
round: "",
|
||||
white: "",
|
||||
black: "",
|
||||
result: "",
|
||||
eco: "",
|
||||
whiteElo: "",
|
||||
blackElo: "",
|
||||
plyCount: "",
|
||||
eventDate: "",
|
||||
source: "",
|
||||
title: "",
|
||||
subtitle: "",
|
||||
author: "",
|
||||
} satisfies IHeader;
|
||||
|
||||
export const initialGameState: IGameState = {
|
||||
pgn: "",
|
||||
headers: "",
|
||||
headers: defaultHeaderFields,
|
||||
diagrams: [],
|
||||
diagramClock: false,
|
||||
};
|
||||
@@ -26,12 +66,34 @@ export const gameReducer = (state: IGameState, action: GameAction) => {
|
||||
case "SET_GAME":
|
||||
return {
|
||||
pgn: action.payload.pgn,
|
||||
headers: "",
|
||||
headers: action.payload.headers,
|
||||
diagrams: [],
|
||||
diagramClock: false,
|
||||
};
|
||||
case "CLEAR_GAME":
|
||||
return initialGameState;
|
||||
case "SET_HEADERS":
|
||||
return {
|
||||
...state,
|
||||
headers: action.payload,
|
||||
};
|
||||
case "ADD_DIAGRAM":
|
||||
return {
|
||||
...state,
|
||||
diagrams: [...state.diagrams, action.payload],
|
||||
};
|
||||
case "DELETE_DIAGRAM":
|
||||
return {
|
||||
...state,
|
||||
diagrams: state.diagrams.filter(
|
||||
(diagram: IDiagrams) => diagram.ply !== action.payload.ply,
|
||||
),
|
||||
};
|
||||
case "TOGGLE_DIAGRAM_CLOCK":
|
||||
return {
|
||||
...state,
|
||||
diagramClock: !state.diagramClock,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export const downloadPDF = (pdf: Blob) => {
|
||||
const fileName = "game.pdf";
|
||||
const fileURL = URL.createObjectURL(pdf);
|
||||
const a = document.createElement("a");
|
||||
a.href = fileURL;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { IGameState } from "#/reducers/gameReducer.ts";
|
||||
|
||||
export const getHeaders = (pgn: string) => {
|
||||
const pgnHeader = pgn.split(/\n\n/g)[0];
|
||||
|
||||
const getHeaderField = (field: string) => {
|
||||
const regex = new RegExp(`(?<=${field}.").*(?=")`);
|
||||
const match = pgnHeader.match(regex);
|
||||
return match && match[0] !== "undefined" ? match[0] : "";
|
||||
};
|
||||
|
||||
return {
|
||||
event: getHeaderField("Event"),
|
||||
site: getHeaderField("Site"),
|
||||
date: getHeaderField("Date"),
|
||||
round: getHeaderField("Round"),
|
||||
white: getHeaderField("White"),
|
||||
black: getHeaderField("Black"),
|
||||
result: getHeaderField("Result"),
|
||||
eco: getHeaderField("ECO"),
|
||||
whiteElo: getHeaderField("WhiteElo"),
|
||||
blackElo: getHeaderField("BlackElo"),
|
||||
plyCount: getHeaderField("PlyCount"),
|
||||
eventDate: getHeaderField("EventDate"),
|
||||
source: getHeaderField("Source"),
|
||||
};
|
||||
};
|
||||
|
||||
// Create a string based on new header values
|
||||
export const buildPgnString = (game: IGameState) => {
|
||||
const moves = game.pgn?.split(/\n\n/g)[1];
|
||||
|
||||
// if customer headers are present, use them
|
||||
if (
|
||||
game.headers.title?.length > 0 ||
|
||||
game.headers.subtitle?.length > 0 ||
|
||||
game.headers.author?.length > 0
|
||||
) {
|
||||
return `[Result "${game.headers.result}"]
|
||||
[Title "${game.headers.title}"]
|
||||
[Subtitle "${game.headers.subtitle}"]
|
||||
[Date "${game.headers.date}"]
|
||||
[Author "${game.headers.author}"]\n
|
||||
${moves}`;
|
||||
}
|
||||
|
||||
return `[Event "${game.headers.event}"]
|
||||
[Site "${game.headers.site}"]
|
||||
[Date "${game.headers.date}"]
|
||||
[Round "${game.headers.round}"]
|
||||
[White "${game.headers.white}"]
|
||||
[Black "${game.headers.black}"]
|
||||
[Result "${game.headers.result}"]
|
||||
[ECO "${game.headers.eco}"]
|
||||
[WhiteElo "${game.headers.whiteElo}"]
|
||||
[BlackElo "${game.headers.blackElo}"]
|
||||
[PlyCount "${game.headers.plyCount}"]
|
||||
[EventDate "${game.headers.eventDate}"]
|
||||
[Source "${game.headers.source}"]\n
|
||||
${moves}`;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
export const downloadString = (string: string, filename: string) => {
|
||||
try {
|
||||
const element = document.createElement("a");
|
||||
const file = new Blob([string], { type: "text/plain" });
|
||||
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = filename;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
} catch (error) {
|
||||
throw new Error("Failed to download file");
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user