mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 09:56:57 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
459b79535b
|
|||
|
9415f3b6b8
|
|||
|
95973937f5
|
|||
|
9bb2cf331f
|
|||
|
9acd458898
|
|||
|
7209a88827
|
|||
|
9ecbce5a15
|
|||
|
6a9a51c1ed
|
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const GITHUB_URL =
|
||||
"https://github.com/therealowenrees/chess-scribe-website";
|
||||
+39
@@ -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();
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user