Compare commits

...

17 Commits

Author SHA1 Message Date
owenrees 2e05f86d5f update lighthouse todos 2026-06-12 16:57:10 +02:00
owenrees 4577755c93 add aria label to footer logo link 2026-06-12 16:52:40 +02:00
owenrees a110c7490c change h6 to h5 to fix heading structure 2026-06-12 16:49:55 +02:00
owenrees 0b10637df4 avoid prerendering callback page 2026-06-12 16:46:41 +02:00
owenrees 41cbd5db16 fix aria reference 2026-06-12 16:45:05 +02:00
owenrees af6a8426ad aria labels and font size changes 2026-06-12 15:17:29 +02:00
owenrees 0dde848cfe add aria labels to non-labelled icons 2026-06-12 15:04:53 +02:00
owenrees ee9968e99b pass matomo analytics env vars with zod 2026-06-12 14:48:52 +02:00
owenrees 7384a3f825 custom 404 page 2026-06-12 14:17:29 +02:00
owenrees 887a43636f export session interface to create a user interface for context 2026-06-12 11:27:12 +02:00
owenrees 734c631356 export interfaces 2026-06-12 11:22:54 +02:00
owenrees 802d46191e do not set isLoggedInBoolean since it is now removed 2026-06-12 11:22:41 +02:00
owenrees 4edcb49f58 fix button text based on user existing, rather that the removed isLogged in boolean 2026-06-12 11:21:50 +02:00
owenrees 030d44226d remove isLoggedIn booelan from lichess user, either a user exists or doesnt 2026-06-12 11:21:05 +02:00
owenrees f12176e713 remove isLoggedIn booelan from lichess user, either a user exists or doesnt 2026-06-12 11:20:55 +02:00
owenrees 3da81680a1 split schema for server functions 2026-06-12 11:04:43 +02:00
owenrees 0b32e77d7e add zod schema and validation to server 2026-06-12 10:58:59 +02:00
18 changed files with 131 additions and 83 deletions
+4 -1
View File
@@ -1,4 +1,7 @@
VITE_API_BASE_URL=
VITE_MATOMO_URL=
VITE_MATOMO_SITE_ID=
DISCORD_CONTACT_WEBHOOK=
DISCORD_ERROR_LOG_WEBHOOK=
LICHESS_CLIENT_ID=
VITE_API_BASE_URL=
+3 -7
View File
@@ -1,15 +1,11 @@
- og / json-sd
- logger
- bring sections headers on all pages other than home page further up (reduce top margin / padding)
- set game current position on game load to ply 0 and whatever the FEN is (starting position probably, but could be custom position)
- privacy policy - have no access to anything you generate, only keep a count of success/failed pdf generations with timestamp and error message
- a11y and lighthouse fixes
- check wcag compliance
- 429 lichess error handling
- add throw errors or return errors in server code
- check wcag compliance - footer colour contrast
- 429 lichess error handling - retry after 10 seconds
- try/catch where needed in client code
- generate state and check it against the state returned from lichess https://lichess.org/api#tag/oauth/GET/oauth
- checkbox and toggle disabled states
- 404 page
- lichess data with zod validation
- react query on for study and chapter load, to allow caching
- https://specification.website/checklist/
+1 -4
View File
@@ -9,10 +9,7 @@ interface IProps {
const AccordionItem = ({ title, id, children }: IProps) => (
<div className="hero__accordion-item">
<details className="hero__accordion-details" name="linked">
<summary
className="hero__accordion-item__summary"
aria-describedby={title}
>
<summary className="hero__accordion-item__summary" id={`${id}-summary`}>
<span className="hero__accordion-item__title-container">
<span className="hero__accordion-item__title">{title}</span>
</span>
+1
View File
@@ -13,6 +13,7 @@ const CoffeeWidget = ({ height, width }: IProps) => {
target="_blank"
className="ml-auto flex items-center gap-2 text-sm"
rel="noopener"
aria-label="Buy Me A Coffee"
>
<Image
src={buyMeACoffeeLogo}
+1 -1
View File
@@ -7,7 +7,7 @@ interface IProps {
const Feature = ({ title, children }: IProps) => (
<div className="mb-auto">
<h6 className="text-(--neutral-content) text-2xl font-bold">{title}</h6>
<h5 className="text-(--neutral-content) text-2xl font-bold">{title}</h5>
<p className="text-(--neutral-content) font-medium" data-testid="text">
{children}
</p>
+6 -5
View File
@@ -10,26 +10,26 @@ import footerLogo from "@/assets/images/footerLogo.svg?url";
export default function Footer() {
return (
<footer className="text-white w-full grid-cols-2 items-center bg-(--accent) px-10 py-5">
<div className="flex w-full items-center">
<Link to="/">
<div className="flex w-full items-center gap-2">
<Link to="/" aria-label="Home">
<Image
src={footerLogo}
alt="ChessScribe Logo"
width={10}
height={10}
className="hidden h-10 w-10 sm:block"
aria-label="ChessScribe Logo"
/>
</Link>
<div>
<p data-testid="copyright">
Copyright © 2023 - {new Date().getFullYear()}
</p>
<p>© 2023 - {new Date().getFullYear()}</p>
</div>
<div className="ml-auto flex gap-2">
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out cursor-pointer"
>
<LuGithub />
@@ -38,6 +38,7 @@ export default function Footer() {
<Link
to="/contact"
rel="noopener noreferrer"
aria-label="Contact"
className="text-2xl hover:text-(--neutral-content) transition-colors duration-300 ease-in-out"
>
<LuMail />
+2 -4
View File
@@ -6,7 +6,7 @@ const LichessButton = () => {
const { user } = useLichessUser();
const onClickHandler = () => {
return user?.isLoggedIn ? lichessLogout() : lichessLogin();
return user ? lichessLogout() : lichessLogin();
};
return (
@@ -26,9 +26,7 @@ const LichessButton = () => {
</svg>
</div>
<p>
{user?.isLoggedIn ? `Logout ${user.username}` : "Log into Lichess.org"}
</p>
<p>{user ? `Logout ${user.username}` : "Log into Lichess.org"}</p>
</button>
);
};
+3 -2
View File
@@ -1,9 +1,10 @@
import { trackAppRouter } from "@socialgouv/matomo-next";
import { useLocation, useSearch } from "@tanstack/react-router";
import { useEffect } from "react";
import { env } from "#/env.ts";
const MATOMO_URL = import.meta.env.NEXT_PUBLIC_MATOMO_URL;
const MATOMO_SITE_ID = import.meta.env.NEXT_PUBLIC_MATOMO_SITE_ID;
const MATOMO_URL = env.VITE_MATOMO_URL;
const MATOMO_SITE_ID = env.VITE_MATOMO_SITE_ID;
export function MatomoAnalytics() {
const location = useLocation();
+21
View File
@@ -0,0 +1,21 @@
import { Link } from "@tanstack/react-router";
const NotFoundView = () => {
return (
<div className="flex min-h-[calc(100vh-194px)] flex-col items-center justify-center p-6">
<div className="relative flex max-w-md flex-col items-center text-center">
<h1 className="text-2xl font-bold tracking-tight mb-4 text-(--accent)">
The page you are looking for does not exist.
</h1>
<div className="flex w-full gap-3 justify-center">
<Link to="/" className="btn btn-primary">
Go Home
</Link>
</div>
</div>
</div>
);
};
export default NotFoundView;
+1 -1
View File
@@ -14,7 +14,7 @@ const Section = ({ title, smallTitle, description, children }: IProps) => {
return (
<section className="mx-auto mt-8 grid max-w-5xl place-items-center gap-4 p-8 text-center md:grid-cols-3 md:text-left text-(--accent)">
<h3 className="col-span-full mb-4 text-center text-lg font-bold tracking-wider text-primary">
<h3 className="col-span-full mb-4 text-center text-2xl font-bold tracking-wider text-primary">
{smallTitle}
</h3>
<h4 className="col-span-full mb-4 text-center text-4xl font-extrabold text-(--neutral-content) md:text-5xl">
+2 -7
View File
@@ -9,14 +9,10 @@ import {
useEffect,
useState,
} from "react";
import type { ISession } from "#/interfaces.ts";
import { getSession } from "#/server/lichess.ts";
interface ILichessUser {
username: string;
id: string;
isLoggedIn: boolean;
}
interface ILichessUser extends Omit<ISession, "token"> {}
interface ILichessUserContextType {
user: ILichessUser | null;
@@ -38,7 +34,6 @@ export const LichessUserProvider = ({ children }: { children: ReactNode }) => {
setUser({
username: session.username,
id: session.id,
isLoggedIn: true,
});
}
})();
+2
View File
@@ -2,6 +2,8 @@ import { z } from "zod";
const clientSchema = z.object({
VITE_API_BASE_URL: z.string("Invalid API Base URL format"),
VITE_MATOMO_URL: z.string("Invalid URL format"),
VITE_MATOMO_SITE_ID: z.string("Invalid Site ID"),
});
const serverSchema = z.object({
+6 -20
View File
@@ -1,6 +1,11 @@
import { useServerFn } from "@tanstack/react-start";
import { useEffect, useState } from "react";
import { useLichessUser } from "#/context/LichessUserContext.tsx";
import type {
ITokenData,
IUserStudy,
IUserStudyChapter,
} from "#/interfaces.ts";
import {
getSession,
getStudyChapters,
@@ -15,25 +20,6 @@ import { getHeaders } from "#/utils/pgnUtils.ts";
type Status = "loading" | "success" | "error";
interface ITokenData {
access_token: string;
expires_in: number;
token_type: string;
}
export type IUserStudy = {
id: string;
name: string;
createdAt: number;
updatedAt: number;
};
export interface IUserStudyChapter {
chapterId: string;
name: string;
pgn: string;
}
export const useLichess = () => {
const { user, setUser } = useLichessUser();
const [userStudies, setUserStudies] = useState<IUserStudy[]>([]);
@@ -65,7 +51,7 @@ export const useLichess = () => {
token: string;
}) => {
await setSessionFn({ data: { username, id, token } });
setUser({ username, id, isLoggedIn: true });
setUser({ username, id });
};
const getLichessUser = async ({ tokenData }: { tokenData: ITokenData }) => {
+19
View File
@@ -1,3 +1,22 @@
import type { infer } from "zod";
import type { SessionSchema, TokenResponseSchema } from "#/schemas.ts";
export interface ISession extends infer<typeof SessionSchema> {}
export interface ITokenData extends infer<typeof TokenResponseSchema> {}
export interface IUserStudy {
id: string;
name: string;
createdAt: number;
updatedAt: number;
}
export interface IUserStudyChapter {
chapterId: string;
name: string;
pgn: string;
}
export interface IPosition {
ply: number;
fen: string;
+2
View File
@@ -6,6 +6,7 @@ import { ToastContainer } from "react-toastify";
import GenericErrorView from "#/components/GenericErrorView.tsx";
import LoadingView from "#/components/LoadingView.tsx";
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
import NotFoundView from "#/components/NotFoundView.tsx";
import { LichessUserProvider } from "#/context/LichessUserContext.tsx";
import Footer from "../components/Footer";
import Header from "../components/Header";
@@ -47,6 +48,7 @@ export const Route = createRootRoute({
shellComponent: RootDocument,
errorComponent: ({ error }) => <GenericErrorView error={error} />,
pendingComponent: () => <LoadingView />,
notFoundComponent: () => <NotFoundView />,
});
function RootDocument({ children }: { children: ReactNode }) {
+19
View File
@@ -0,0 +1,19 @@
import { z } from "zod";
export const SessionSchema = z.object({
username: z.string(),
id: z.string(),
token: z.string(),
});
export const TokenResponseSchema = z.object({
access_token: z.string(),
expires_in: z.number(),
scope: z.string().optional(),
token_type: z.string(),
});
export const LichessAccountSchema = z.object({
id: z.string(),
username: z.string(),
});
+24 -25
View File
@@ -6,8 +6,14 @@ import {
getCookie,
setCookie,
} from "@tanstack/react-start/server";
import { z } from "zod";
import { env } from "#/env.ts";
import { base64UrlEncode, sha256 } from "#/lib/encodeDecode.ts";
import {
LichessAccountSchema,
SessionSchema,
TokenResponseSchema,
} from "#/schemas.ts";
const createVerifier = () => base64UrlEncode(randomBytes(32));
const createChallenge = (verifier: string) => base64UrlEncode(sha256(verifier));
@@ -22,11 +28,8 @@ export const getSession = createServerFn({ method: "GET" }).handler(
const session = getCookie("lichess-session");
if (!session) return null;
try {
return JSON.parse(session) as {
username: string;
id: string;
token: string;
};
const parsed = SessionSchema.safeParse(JSON.parse(session));
return parsed.success ? parsed.data : null;
} catch {
return null;
}
@@ -34,25 +37,15 @@ export const getSession = createServerFn({ method: "GET" }).handler(
);
export const setSession = createServerFn({ method: "POST" })
.inputValidator(
(data: { username: string; id: string; token: string }) => data,
)
.inputValidator(SessionSchema)
.handler(async ({ data }) => {
setCookie(
"lichess-session",
JSON.stringify({
username: data.username,
id: data.id,
token: data.token,
}),
{
setCookie("lichess-session", JSON.stringify(data), {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7,
},
);
});
setCookie("lichess-oauth-verifier", "", {
path: "/",
@@ -66,12 +59,13 @@ export const setSession = createServerFn({ method: "POST" })
});
export const getToken = createServerFn({ method: "POST" })
.inputValidator((data: { code: string }) => data)
.inputValidator(z.object({ code: z.string() }))
.handler(async ({ data }) => {
const verifier = getCookie("lichess-oauth-verifier");
if (!verifier)
if (!verifier) {
throw new Error("OAuth session expired. Please login again.");
}
const response = await fetch("https://lichess.org/api/token", {
method: "POST",
@@ -87,14 +81,16 @@ export const getToken = createServerFn({ method: "POST" })
}),
});
if (!response.ok)
if (!response.ok) {
throw new Error(`Token exchange failed: ${await response.text()}`);
}
return response.json();
const json = await response.json();
return TokenResponseSchema.parse(json);
});
export const getUser = createServerFn({ method: "GET" })
.inputValidator((data: { access_token: string }) => data)
export const getUser = createServerFn({ method: "POST" })
.inputValidator(z.object({ access_token: z.string() }))
.handler(async ({ data }) => {
const response = await fetch("https://lichess.org/api/account", {
headers: {
@@ -102,7 +98,10 @@ export const getUser = createServerFn({ method: "GET" })
},
});
return response.json();
if (!response.ok) throw new Error("Failed to fetch Lichess account");
const json = await response.json();
return LichessAccountSchema.parse(json);
});
const deleteAccessToken = createServerFn().handler(async () => {
+8
View File
@@ -19,6 +19,14 @@ const config = defineConfig({
enabled: true,
crawlLinks: true,
},
pages: [
{
path: "/callback",
prerender: {
enabled: false,
},
},
],
sitemap: {
enabled: true,
host: HOST_URL,