Compare commits

..

8 Commits

10 changed files with 154 additions and 75 deletions
+4 -15
View File
@@ -1,17 +1,6 @@
// ZOD
FEN string
Env Vars
API base URL
- favicon and page titles
- favicon and page titles - SEO related things
- rearrange buttons / header space - collapse on smaller screens
- add / edit headers
- custom headers - title etc
- Loading Chessboard with suspense and spinner
- toasts
- Lichess login
- stringUtils - downloadString - move it out of try / catch and let program use the try/catch
- logger
- logger
- contact form
- bring sections headers on all pages other than home page further up (reduce top margin / padding)
+1 -1
View File
@@ -7,7 +7,7 @@ interface IProps {
const Feature = ({ title, children }: IProps) => (
<div className="mb-auto">
<h6 className="text-(--base-content) text-2xl font-bold">{title}</h6>
<h6 className="text-(--neutral-content) text-2xl font-bold">{title}</h6>
<p className="text-(--neutral-content) font-medium" data-testid="text">
{children}
</p>
+2 -1
View File
@@ -4,6 +4,7 @@ import { Image } from "@unpic/react";
import { LuGithub, LuMail } from "react-icons/lu";
import CoffeeWidget from "#/components/CoffeeWidget.tsx";
import { GITHUB_URL } from "#/config.ts";
import footerLogo from "@/assets/images/footerLogo.svg?url";
export default function Footer() {
@@ -26,7 +27,7 @@ export default function Footer() {
</div>
<div className="ml-auto flex gap-2">
<a
href={import.meta.env.VITE_GITHUB_URL} // TODO ZOD
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
+2
View File
@@ -0,0 +1,2 @@
export const GITHUB_URL =
"https://github.com/therealowenrees/chess-scribe-website";
+39
View File
@@ -0,0 +1,39 @@
import { z } from "zod";
const clientSchema = z.object({
VITE_API_BASE_URL: z.string("Invalid API Base URL format"),
});
const serverSchema = z.object({
DISCORD_CONTACT_WEBHOOK: z.string("Invalid Discord Contact Webhook URL"),
DISCORD_ERROR_LOG_WEBHOOK: z.string("Invalid Discord Error Log Webhook URL"),
LICHESS_CLIENT_ID: z.string("Invalid Lichess Client ID"),
});
const fullSchema = z.object({
...clientSchema.shape,
...serverSchema.shape,
});
const isServer = typeof window === "undefined";
const rawEnv = isServer ? process.env : import.meta.env;
const getEnv = () => {
if (isServer) {
const _env = fullSchema.safeParse(rawEnv);
if (!_env.success) {
console.error("❌ Invalid Server Environment Variables:");
throw new Error("Invalid environment variables");
}
return _env.data;
} else {
const _env = clientSchema.safeParse(rawEnv);
if (!_env.success) {
console.error("❌ Invalid Client Environment Variables:");
throw new Error("Invalid environment variables");
}
return _env.data as z.infer<typeof fullSchema>;
}
};
export const env = getEnv();
+13 -3
View File
@@ -6,6 +6,7 @@ import {
useState,
} from "react";
import { toast } from "react-toastify";
import { env } from "#/env.ts";
import type { IHeader, IPosition } from "#/interfaces.ts";
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
import { downloadPDF } from "#/utils/pdfUtils.ts";
@@ -29,8 +30,17 @@ export const useChessGame = () => {
}, []);
const handleSavePgn = () => {
const pgnString = buildPgnString(gameState);
downloadString(pgnString, "game.pgn");
try {
const pgnString = buildPgnString(gameState);
downloadString(pgnString, "game.pgn");
} catch (error) {
// TODO logger
console.error(error);
toast.error("An error occurred while saving the PGN. Please try again.", {
toastId: "pgn-download-error",
});
}
};
const handleSavePdf = async () => {
@@ -39,7 +49,7 @@ export const useChessGame = () => {
const { diagrams, diagramClock } = gameState;
const pgnString = buildPgnString(gameState);
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
const apiBaseUrl = env.VITE_API_BASE_URL;
const response = await fetch(`${apiBaseUrl}/pdf`, {
method: "POST",
+57 -43
View File
@@ -1,10 +1,22 @@
import { lazy, Suspense } from "react";
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";
import { useChessGame } from "#/hooks/useChessGame.ts";
const PgnViewer = lazy(() => import("#/components/PgnViewer.tsx"));
const LoadingBoard = () => {
return (
<div className="relative grid h-125 w-160 items-center text-center">
<div id="loading-bar-spinner" className="spinner">
<div className="spinner-icon"></div>
</div>
</div>
);
};
const Chessboard = () => {
const {
gameState,
@@ -35,51 +47,53 @@ const Chessboard = () => {
</Section>
<div className="grid grid-cols-2 gap-4">
<div>
<PgnViewer
gamePgn={gameState.pgn || ""}
handlePlyChange={handlePlyChange}
/>
<div className="grid grid-cols-3 gap-2 max-w-150 mt-2">
<button
className="btn btn-primary"
type="button"
onClick={handleClearGame}
>
Clear Game
</button>
<div className="flex gap-2 items-center">
<label
htmlFor="diagram-add"
className="text-sm text-(--base-content)"
<Suspense fallback={<LoadingBoard />}>
<div>
<PgnViewer
gamePgn={gameState.pgn || ""}
handlePlyChange={handlePlyChange}
/>
<div className="grid grid-cols-3 gap-2 max-w-150 mt-2">
<button
className="btn btn-primary"
type="button"
onClick={handleClearGame}
>
Select Diagram
</label>
<input
id="diagram-toggle"
name="diagram-toggle"
type="checkbox"
className="checkbox-accent"
checked={gameState.diagrams.some(
(d) => d.ply === currentPosition.ply,
)}
onChange={handleToggleDiagram}
/>
</div>
Clear Game
</button>
<div className="flex gap-2 items-center">
<label
htmlFor="diagram-add"
className="text-sm text-(--base-content)"
>
Select Diagram
</label>
<input
id="diagram-toggle"
name="diagram-toggle"
type="checkbox"
className="checkbox-accent"
checked={gameState.diagrams.some(
(d) => d.ply === currentPosition.ply,
)}
onChange={handleToggleDiagram}
/>
</div>
<label className="toggle text-sm text-(--base-content)">
Render move times
<input
id="render-times"
name="render-times"
type="checkbox"
checked={gameState.diagramClock}
onChange={handleToggleClock}
/>
<span className="slider round" />
</label>
<label className="toggle text-sm text-(--base-content)">
Render move times
<input
id="render-times"
name="render-times"
type="checkbox"
checked={gameState.diagramClock}
onChange={handleToggleClock}
/>
<span className="slider round" />
</label>
</div>
</div>
</div>
</Suspense>
<div className="flex flex-col gap-4 items-center">
<div className="flex justify-center gap-4">
+28
View File
@@ -98,10 +98,38 @@ body {
}
}
#loading-bar-spinner.spinner {
left: 50%;
margin-left: -20px;
top: 40%;
margin-top: -20px;
position: absolute;
z-index: 19 !important;
animation: loading-bar-spinner 400ms linear infinite;
}
#loading-bar-spinner.spinner .spinner-icon {
width: 40px;
height: 40px;
border: solid 4px transparent;
border-top-color: var(--accent) !important;
border-left-color: var(--neutral-content) !important;
border-radius: 50%;
}
.rise-in {
animation: rise-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes loading-bar-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes rise-in {
from {
opacity: 0;
+2 -2
View File
@@ -40,7 +40,7 @@
&:after {
content: "\276F";
color: var(--base-content);
color: var(--neutral-content);
margin-right: 1rem;
transform: rotate(90deg);
transition: transform 0.3s ease;
@@ -48,7 +48,7 @@
}
.hero__accordion-item__title {
color: var(--base-content);
color: var(--neutral-content);
font-size: 1.25rem;
font-weight: 600;
}
+6 -10
View File
@@ -1,13 +1,9 @@
export const downloadString = (string: string, filename: string) => {
try {
const element = document.createElement("a");
const file = new Blob([string], { type: "text/plain" });
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");
}
element.href = URL.createObjectURL(file);
element.download = filename;
document.body.appendChild(element);
element.click();
};