Compare commits

..

8 Commits

10 changed files with 154 additions and 75 deletions
+3 -14
View File
@@ -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)
+1 -1
View File
@@ -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>
+2 -1
View File
@@ -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"
+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();
+11 -1
View File
@@ -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 = () => {
try {
const pgnString = buildPgnString(gameState); const pgnString = buildPgnString(gameState);
downloadString(pgnString, "game.pgn"); 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",
+15 -1
View File
@@ -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,6 +47,7 @@ const Chessboard = () => {
</Section> </Section>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Suspense fallback={<LoadingBoard />}>
<div> <div>
<PgnViewer <PgnViewer
gamePgn={gameState.pgn || ""} gamePgn={gameState.pgn || ""}
@@ -80,6 +93,7 @@ const Chessboard = () => {
</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">
+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 { .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;
+2 -2
View File
@@ -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;
} }
-4
View File
@@ -1,5 +1,4 @@
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" });
@@ -7,7 +6,4 @@ export const downloadString = (string: string, filename: string) => {
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");
}
}; };