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
|
- favicon and page titles - SEO related things
|
||||||
FEN string
|
|
||||||
Env Vars
|
|
||||||
API base URL
|
|
||||||
|
|
||||||
- favicon and page titles
|
|
||||||
- rearrange buttons / header space - collapse on smaller screens
|
- rearrange buttons / header space - collapse on smaller screens
|
||||||
|
|
||||||
- add / edit headers
|
|
||||||
- custom headers - title etc
|
|
||||||
- Loading Chessboard with suspense and spinner
|
|
||||||
- toasts
|
|
||||||
|
|
||||||
- Lichess login
|
- 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) => (
|
const Feature = ({ title, children }: IProps) => (
|
||||||
<div className="mb-auto">
|
<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">
|
<p className="text-(--neutral-content) font-medium" data-testid="text">
|
||||||
{children}
|
{children}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Image } from "@unpic/react";
|
|||||||
import { LuGithub, LuMail } from "react-icons/lu";
|
import { LuGithub, LuMail } from "react-icons/lu";
|
||||||
|
|
||||||
import CoffeeWidget from "#/components/CoffeeWidget.tsx";
|
import CoffeeWidget from "#/components/CoffeeWidget.tsx";
|
||||||
|
import { GITHUB_URL } from "#/config.ts";
|
||||||
import footerLogo from "@/assets/images/footerLogo.svg?url";
|
import footerLogo from "@/assets/images/footerLogo.svg?url";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
@@ -26,7 +27,7 @@ export default function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="ml-auto flex gap-2">
|
<div className="ml-auto flex gap-2">
|
||||||
<a
|
<a
|
||||||
href={import.meta.env.VITE_GITHUB_URL} // TODO ZOD
|
href={GITHUB_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
|
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,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
import { env } from "#/env.ts";
|
||||||
import type { IHeader, IPosition } from "#/interfaces.ts";
|
import type { IHeader, IPosition } from "#/interfaces.ts";
|
||||||
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
import { gameReducer, initialGameState } from "#/reducers/gameReducer.ts";
|
||||||
import { downloadPDF } from "#/utils/pdfUtils.ts";
|
import { downloadPDF } from "#/utils/pdfUtils.ts";
|
||||||
@@ -29,8 +30,17 @@ export const useChessGame = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSavePgn = () => {
|
const handleSavePgn = () => {
|
||||||
const pgnString = buildPgnString(gameState);
|
try {
|
||||||
downloadString(pgnString, "game.pgn");
|
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 () => {
|
const handleSavePdf = async () => {
|
||||||
@@ -39,7 +49,7 @@ export const useChessGame = () => {
|
|||||||
const { diagrams, diagramClock } = gameState;
|
const { diagrams, diagramClock } = gameState;
|
||||||
const pgnString = buildPgnString(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`, {
|
const response = await fetch(`${apiBaseUrl}/pdf`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
+57
-43
@@ -1,10 +1,22 @@
|
|||||||
|
import { lazy, Suspense } from "react";
|
||||||
import CustomHeaders from "#/components/CustomHeaders.tsx";
|
import CustomHeaders from "#/components/CustomHeaders.tsx";
|
||||||
import HeaderFields from "#/components/HeaderFields.tsx";
|
import HeaderFields from "#/components/HeaderFields.tsx";
|
||||||
import LichessButton from "#/components/LichessButton.tsx";
|
import LichessButton from "#/components/LichessButton.tsx";
|
||||||
import PgnViewer from "#/components/PgnViewer.tsx";
|
|
||||||
import Section from "#/components/Section.tsx";
|
import Section from "#/components/Section.tsx";
|
||||||
import { useChessGame } from "#/hooks/useChessGame.ts";
|
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 Chessboard = () => {
|
||||||
const {
|
const {
|
||||||
gameState,
|
gameState,
|
||||||
@@ -35,51 +47,53 @@ const Chessboard = () => {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<Suspense fallback={<LoadingBoard />}>
|
||||||
<PgnViewer
|
<div>
|
||||||
gamePgn={gameState.pgn || ""}
|
<PgnViewer
|
||||||
handlePlyChange={handlePlyChange}
|
gamePgn={gameState.pgn || ""}
|
||||||
/>
|
handlePlyChange={handlePlyChange}
|
||||||
<div className="grid grid-cols-3 gap-2 max-w-150 mt-2">
|
/>
|
||||||
<button
|
<div className="grid grid-cols-3 gap-2 max-w-150 mt-2">
|
||||||
className="btn btn-primary"
|
<button
|
||||||
type="button"
|
className="btn btn-primary"
|
||||||
onClick={handleClearGame}
|
type="button"
|
||||||
>
|
onClick={handleClearGame}
|
||||||
Clear Game
|
|
||||||
</button>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<label
|
|
||||||
htmlFor="diagram-add"
|
|
||||||
className="text-sm text-(--base-content)"
|
|
||||||
>
|
>
|
||||||
Select Diagram
|
Clear Game
|
||||||
</label>
|
</button>
|
||||||
<input
|
<div className="flex gap-2 items-center">
|
||||||
id="diagram-toggle"
|
<label
|
||||||
name="diagram-toggle"
|
htmlFor="diagram-add"
|
||||||
type="checkbox"
|
className="text-sm text-(--base-content)"
|
||||||
className="checkbox-accent"
|
>
|
||||||
checked={gameState.diagrams.some(
|
Select Diagram
|
||||||
(d) => d.ply === currentPosition.ply,
|
</label>
|
||||||
)}
|
<input
|
||||||
onChange={handleToggleDiagram}
|
id="diagram-toggle"
|
||||||
/>
|
name="diagram-toggle"
|
||||||
</div>
|
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)">
|
<label className="toggle text-sm text-(--base-content)">
|
||||||
Render move times
|
Render move times
|
||||||
<input
|
<input
|
||||||
id="render-times"
|
id="render-times"
|
||||||
name="render-times"
|
name="render-times"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={gameState.diagramClock}
|
checked={gameState.diagramClock}
|
||||||
onChange={handleToggleClock}
|
onChange={handleToggleClock}
|
||||||
/>
|
/>
|
||||||
<span className="slider round" />
|
<span className="slider round" />
|
||||||
</label>
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Suspense>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 items-center">
|
<div className="flex flex-col gap-4 items-center">
|
||||||
<div className="flex justify-center gap-4">
|
<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 {
|
.rise-in {
|
||||||
animation: rise-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
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 {
|
@keyframes rise-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: "\276F";
|
content: "\276F";
|
||||||
color: var(--base-content);
|
color: var(--neutral-content);
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hero__accordion-item__title {
|
.hero__accordion-item__title {
|
||||||
color: var(--base-content);
|
color: var(--neutral-content);
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
export const downloadString = (string: string, filename: string) => {
|
export const downloadString = (string: string, filename: string) => {
|
||||||
try {
|
const element = document.createElement("a");
|
||||||
const element = document.createElement("a");
|
const file = new Blob([string], { type: "text/plain" });
|
||||||
const file = new Blob([string], { type: "text/plain" });
|
|
||||||
|
|
||||||
element.href = URL.createObjectURL(file);
|
element.href = URL.createObjectURL(file);
|
||||||
element.download = filename;
|
element.download = filename;
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
element.click();
|
element.click();
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Failed to download file");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user