mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 09:56:57 +00:00
save as PGN, reuse clear game handler, get headers
This commit is contained in:
@@ -1,13 +1,16 @@
|
|||||||
import { type ChangeEvent, useReducer, useRef } from "react";
|
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";
|
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
||||||
|
import { buildPgnString, getHeaders } from "#/utils/pgnUtils.ts";
|
||||||
|
import { downloadString } from "#/utils/stringUtils.ts";
|
||||||
|
|
||||||
const ChessboardContainer = () => {
|
const ChessboardContainer = () => {
|
||||||
const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
|
const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState);
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const handleSavePgn = () => {
|
const handleSavePgn = () => {
|
||||||
console.log("Save PGN");
|
const pgnString = buildPgnString(gameState);
|
||||||
|
downloadString(pgnString, "game.pgn");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSavePdf = () => {
|
const handleSavePdf = () => {
|
||||||
@@ -44,25 +47,23 @@ const ChessboardContainer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pgnString) {
|
if (pgnString) {
|
||||||
|
const headers = {
|
||||||
|
...getHeaders(pgnString),
|
||||||
|
title: "",
|
||||||
|
subtitle: "",
|
||||||
|
};
|
||||||
gameDispatch({
|
gameDispatch({
|
||||||
type: "SET_GAME",
|
type: "SET_GAME",
|
||||||
payload: { pgn: pgnString, headers: "" },
|
payload: { pgn: pgnString, headers: headers },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
gameDispatch({
|
e.target.files = null;
|
||||||
type: "CLEAR_GAME",
|
handleClearGame();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
reader.readAsText(selectedFile);
|
reader.readAsText(selectedFile);
|
||||||
} else {
|
} else {
|
||||||
if (fileInputRef.current) {
|
handleClearGame();
|
||||||
fileInputRef.current.value = "";
|
|
||||||
}
|
|
||||||
e.target.files = null;
|
|
||||||
gameDispatch({
|
|
||||||
type: "CLEAR_GAME",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,20 +3,60 @@ interface IDiagrams {
|
|||||||
fen: string;
|
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 {
|
export interface IGameState {
|
||||||
pgn: string;
|
pgn: string;
|
||||||
headers: string;
|
headers: IHeader;
|
||||||
diagrams: IDiagrams[];
|
diagrams: IDiagrams[];
|
||||||
diagramClock: boolean;
|
diagramClock: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameAction =
|
type GameAction =
|
||||||
| { type: "SET_GAME"; payload: { pgn: string; headers: "" } }
|
| { type: "SET_GAME"; payload: { pgn: string; headers: "" } }
|
||||||
| { type: "CLEAR_GAME" };
|
| { type: "CLEAR_GAME" }
|
||||||
|
| { type: "SET_HEADERS"; payload: IHeader }
|
||||||
|
| { type: "ADD_DIAGRAM"; payload: IDiagrams }
|
||||||
|
| { type: "DELETE_DIAGRAM"; payload: { ply: number } }
|
||||||
|
| { type: "TOGGLE_DIAGRAM_CLOCK"; payload: boolean };
|
||||||
|
|
||||||
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: "",
|
pgn: "",
|
||||||
headers: "",
|
headers: defaultHeaderFields,
|
||||||
diagrams: [],
|
diagrams: [],
|
||||||
diagramClock: false,
|
diagramClock: false,
|
||||||
};
|
};
|
||||||
@@ -26,12 +66,34 @@ export const gameReducer = (state: IGameState, action: GameAction) => {
|
|||||||
case "SET_GAME":
|
case "SET_GAME":
|
||||||
return {
|
return {
|
||||||
pgn: action.payload.pgn,
|
pgn: action.payload.pgn,
|
||||||
headers: "",
|
headers: action.payload.headers,
|
||||||
diagrams: [],
|
diagrams: [],
|
||||||
diagramClock: false,
|
diagramClock: false,
|
||||||
};
|
};
|
||||||
case "CLEAR_GAME":
|
case "CLEAR_GAME":
|
||||||
return initialGameState;
|
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: action.payload,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
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