Compare commits

..

6 Commits

Author SHA1 Message Date
owenrees 2d56700dd8 reduce spacing between header details sections 2026-06-02 22:47:54 +02:00
owenrees 4cd7b0199e update header fields on typing 2026-06-02 22:46:24 +02:00
owenrees 5ed83a20d8 populate header fields 2026-06-02 22:33:27 +02:00
owenrees 00267b05ba browser tab 2026-06-02 22:02:51 +02:00
owenrees 7e205df26d add headers fields 2026-06-02 22:01:41 +02:00
owenrees 551b07fc42 example env file 2026-06-02 21:34:39 +02:00
9 changed files with 261 additions and 30 deletions
+4
View File
@@ -0,0 +1,4 @@
DISCORD_CONTACT_WEBHOOK=
DISCORD_ERROR_LOG_WEBHOOK=
LICHESS_CLIENT_ID=
VITE_API_BASE_URL=
+1
View File
@@ -3,6 +3,7 @@ FEN string
Env Vars
API base URL
- favicon and page titles
- rearrange buttons / header space - collapse on smaller screens
- add / edit headers
+54
View File
@@ -0,0 +1,54 @@
import type { ChangeEvent } from "react";
import FormField from "#/components/FormField.tsx";
import type { IHeader } from "#/interfaces.ts";
interface IProps {
headers: IHeader;
updateHeaders: (e: ChangeEvent<HTMLInputElement>, fieldName: string) => void;
}
const CustomHeaders = ({ headers, updateHeaders }: IProps) => {
return (
<div className="mt-4 w-full">
<details className="bg-(--neutral-content)/20 p-4 rounded-lg">
<summary>
Custom Headers (PDF)
<p className="text-sm font-normal">
Text in these fields will overwrite the PGN headers in the generated
PDF
</p>
</summary>
<div>
<form className="grid place-items-center gap-4 sm:grid-cols-2 mt-2">
<FormField
fieldName="title"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="subtitle"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="date"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="author"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
</form>
</div>
</details>
</div>
);
};
export default CustomHeaders;
+29
View File
@@ -0,0 +1,29 @@
import type { ChangeEvent } from "react";
import type { IHeader } from "#/interfaces.ts";
export interface IFormField {
fieldName: string;
type: string;
headers: IHeader;
updateHeaders: (e: ChangeEvent<HTMLInputElement>, fieldName: string) => void;
}
const FormField = ({ fieldName, type, headers, updateHeaders }: IFormField) => {
return (
<div className="flex flex-col gap-2">
<label htmlFor={fieldName} className="text-sm capitalize">
{fieldName}
</label>
<input
className="headers__input"
type={type}
id={fieldName}
name={fieldName}
value={headers[fieldName]}
onChange={(e) => updateHeaders(e, fieldName)}
/>
</div>
);
};
export default FormField;
+105
View File
@@ -0,0 +1,105 @@
import type { ChangeEvent } from "react";
import FormField from "#/components/FormField.tsx";
import type { IHeader } from "#/interfaces.ts";
interface IProps {
headers: IHeader;
updateHeaders: (e: ChangeEvent<HTMLInputElement>, fieldName: string) => void;
}
const HeaderFields = ({ headers, updateHeaders }: IProps) => {
return (
<div className="mt-4 w-full">
<details className="bg-(--neutral-content)/15 p-4 rounded-lg">
<summary>
Headers
<p className="text-sm font-normal">Edit the game PGN headers</p>
</summary>
<div>
<form className="grid place-items-center gap-4 sm:grid-cols-2 mt-2">
<FormField
fieldName="event"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="site"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="date"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="round"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="white"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="black"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="result"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="eco"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="whiteElo"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="blackElo"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="plyCount"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="eventDate"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
<FormField
fieldName="source"
type="text"
headers={headers}
updateHeaders={updateHeaders}
/>
</form>
</div>
</details>
</div>
);
};
export default HeaderFields;
+15 -1
View File
@@ -6,7 +6,7 @@ import {
useState,
} from "react";
import { toast } from "react-toastify";
import type { IPosition } from "#/interfaces.ts";
import type { IHeader, IPosition } from "#/interfaces.ts";
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
import { downloadPDF } from "#/utils/pdfUtils.ts";
import { buildPgnString, getHeaders } from "#/utils/pgnUtils.ts";
@@ -118,6 +118,7 @@ export const useChessGame = () => {
...getHeaders(pgnString),
title: "",
subtitle: "",
author: "",
};
gameDispatch({
type: "SET_GAME",
@@ -153,6 +154,18 @@ export const useChessGame = () => {
);
};
const updateHeaders = (
e: ChangeEvent<HTMLInputElement>,
fieldName: string,
) => {
e.preventDefault();
const updatedHeaders = {
...gameState.headers,
[fieldName]: e.target.value,
} as IHeader;
gameDispatch({ type: "SET_HEADERS", payload: updatedHeaders });
};
return {
gameState,
fileInputRef,
@@ -165,5 +178,6 @@ export const useChessGame = () => {
handleToggleDiagram,
currentPosition,
generatingPdf,
updateHeaders,
};
};
+3
View File
@@ -17,6 +17,9 @@ export interface IHeader {
plyCount: string;
eventDate: string;
source: string;
title: string;
subtitle: string;
author: string;
[key: string]: string;
}
+31 -25
View File
@@ -1,3 +1,5 @@
import CustomHeaders from "#/components/CustomHeaders.tsx";
import HeaderFields from "#/components/HeaderFields.tsx";
import LichessButton from "#/components/LichessButton.tsx";
import PgnViewer from "#/components/PgnViewer.tsx";
import Section from "#/components/Section.tsx";
@@ -16,12 +18,13 @@ const Chessboard = () => {
handleToggleClock,
handleToggleDiagram,
handlePlyChange,
updateHeaders,
} = useChessGame();
return (
<main className="px-4 pb-8 place-self-center min-h-[calc(100vh-226px)]">
<Section title="Convert PGN to PDF">
<LichessButton /> or{" "}
<LichessButton onClickHandler={() => console.log("Lichess Login")} /> or{" "}
<input
type="file"
id="file-input"
@@ -78,32 +81,35 @@ const Chessboard = () => {
</div>
</div>
<div className="flex gap-4">
{/*<button*/}
{/* className="btn btn-primary"*/}
{/* type="button"*/}
{/* onClick={handleClearGame}*/}
{/*>*/}
{/* Clear Game*/}
{/*</button>*/}
<div className="flex flex-col gap-4 items-center">
<div className="flex justify-center gap-4">
<button
className="btn btn-secondary"
type="button"
onClick={handleSavePgn}
disabled={!gameState.pgn}
>
Save PGN
</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>
<button
className="btn btn-secondary"
type="button"
onClick={handleSavePdf}
disabled={!gameState.pgn || generatingPdf}
>
Save PDF
</button>
<HeaderFields
headers={gameState.headers}
updateHeaders={updateHeaders}
/>
<CustomHeaders
headers={gameState.headers}
updateHeaders={updateHeaders}
/>
</div>
</div>
</main>
+19 -4
View File
@@ -1,9 +1,24 @@
/* Headers */
.headers__input {
height: 3rem;
padding: 0 1rem;
line-height: 1.5rem;
border-radius: 0.5rem;
border: 1px solid var(--neutral-content);
&:focus {
outline: none;
border-color: var(--accent);
background-color: var(--header-bg);
}
}
/* Checkbox */
.checkbox-accent {
accent-color: var(--accent);
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--accent);
width: 18px;
height: 18px;
cursor: pointer;
}
/* Toggle */