diff --git a/src/pages/Chessboard.container.tsx b/src/pages/Chessboard.container.tsx index e8caf00..ef6f687 100644 --- a/src/pages/Chessboard.container.tsx +++ b/src/pages/Chessboard.container.tsx @@ -1,13 +1,16 @@ import { type ChangeEvent, useReducer, useRef } from "react"; import Chessboard from "#/pages/Chessboard.tsx"; import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts"; +import { buildPgnString, getHeaders } from "#/utils/pgnUtils.ts"; +import { downloadString } from "#/utils/stringUtils.ts"; const ChessboardContainer = () => { const [gameState, gameDispatch] = useReducer(gameReducer, initialGameState); const fileInputRef = useRef(null); const handleSavePgn = () => { - console.log("Save PGN"); + const pgnString = buildPgnString(gameState); + downloadString(pgnString, "game.pgn"); }; const handleSavePdf = () => { @@ -44,25 +47,23 @@ const ChessboardContainer = () => { } if (pgnString) { + const headers = { + ...getHeaders(pgnString), + title: "", + subtitle: "", + }; gameDispatch({ type: "SET_GAME", - payload: { pgn: pgnString, headers: "" }, + payload: { pgn: pgnString, headers: headers }, }); } else { - gameDispatch({ - type: "CLEAR_GAME", - }); + e.target.files = null; + handleClearGame(); } }; reader.readAsText(selectedFile); } else { - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - e.target.files = null; - gameDispatch({ - type: "CLEAR_GAME", - }); + handleClearGame(); } }; diff --git a/src/reducers/gameReducer.ts b/src/reducers/gameReducer.ts index 38cbe1d..c253ec7 100644 --- a/src/reducers/gameReducer.ts +++ b/src/reducers/gameReducer.ts @@ -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: "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: "", - 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: action.payload, + }; default: return state; } diff --git a/src/utils/pdfUtils.ts b/src/utils/pdfUtils.ts new file mode 100644 index 0000000..5110c09 --- /dev/null +++ b/src/utils/pdfUtils.ts @@ -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(); +}; diff --git a/src/utils/pgnUtils.ts b/src/utils/pgnUtils.ts new file mode 100644 index 0000000..ffb48a8 --- /dev/null +++ b/src/utils/pgnUtils.ts @@ -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}`; +}; diff --git a/src/utils/stringUtils.ts b/src/utils/stringUtils.ts new file mode 100644 index 0000000..b18dd30 --- /dev/null +++ b/src/utils/stringUtils.ts @@ -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"); + } +};