Compare commits

...

5 Commits

Author SHA1 Message Date
owenrees f475732715 remove loading page 2026-06-11 15:47:34 +02:00
owenrees f4b9820b5b add loading page 2026-06-11 15:06:13 +02:00
owenrees 98a5bb742b generic error view with try again 2026-06-11 12:23:19 +02:00
owenrees 6ef08f4b91 generic error view with try again 2026-06-11 11:12:31 +02:00
owenrees 5bf4ec95c8 if no code is provided return 404 2026-06-11 10:14:08 +02:00
7 changed files with 164 additions and 22 deletions
+4 -1
View File
@@ -5,8 +5,11 @@
- privacy policy - have no access to anything you generate, only keep a count of success/failed pdf generations with timestamp and error message - 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 - a11y and lighthouse fixes
- check wcag compliance - check wcag compliance
- Callback page - improve loading display in suspense
- 429 lichess error handling - 429 lichess error handling
- add throw errors or return errors in server code - add throw errors or return errors in server code
- try/catch where needed in client code - 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 - 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
+68
View File
@@ -0,0 +1,68 @@
import { useNavigate, useRouter } from "@tanstack/react-router";
interface GenericErrorViewProps {
error?: Error | unknown;
resetErrorBoundary?: () => void;
}
const GenericErrorView = ({
error,
resetErrorBoundary,
}: GenericErrorViewProps) => {
const router = useRouter();
const navigate = useNavigate();
const errorMessage =
error instanceof Error
? error.message
: "An unexpected system error occurred.";
const handleTryAgain = async () => {
if (resetErrorBoundary) {
resetErrorBoundary();
}
await router.invalidate();
const currentSearch = router.state.location.search;
await navigate({ to: ".", search: currentSearch });
};
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-2 text-(--accent)">
Something went wrong
</h1>
<p className="text-sm text-(--neutral-content) mb-6 max-w-sm">
The application encountered an issue it couldn't recover from.
</p>
<div className="w-full mb-8 rounded-md bg-slate-300/60 border border-slate-800/80 p-4 text-left font-mono text-xs text-red-400 max-h-32 overflow-y-auto">
<span className="text-(--base-content) block mb-1">Error Logs:</span>
{errorMessage}
</div>
<div className="flex w-full gap-3 justify-center">
{resetErrorBoundary && (
<button
type="button"
onClick={handleTryAgain}
className="btn btn-secondary"
>
Try Again
</button>
)}
<button
type="button"
onClick={() => navigate({ to: "/" })}
className="btn btn-primary"
>
Go Home
</button>
</div>
</div>
</div>
);
};
export default GenericErrorView;
+34
View File
@@ -0,0 +1,34 @@
interface IProps {
title?: string;
description?: string;
}
const LoadingView = ({ title, description }: IProps) => {
return (
<div className="flex min-h-[calc(100vh-194px)] flex-col items-center justify-center p-6 bg-base-100">
<div className="relative flex max-w-sm flex-col items-center text-center">
{/* Animated Custom Loader */}
<div className="relative mb-6 flex h-16 w-16 items-center justify-center">
{/* Outer Pulsing Ring */}
<div className="absolute inset-0 animate-ping rounded-full bg-(--accent) opacity-20 [animation-duration:1.5s]"></div>
{/* Inner Spinning Ring */}
<div className="h-12 w-12 animate-spin rounded-full border-4 border-(--accent) border-t-transparent"></div>
</div>
{/* Loading Content */}
<h2 className="text-xl font-bold tracking-tight mb-2 text-(--base-content)">
{title || "Loading Data"}
</h2>
<p className="text-sm text-(--neutral-content) max-w-xs">
{description || "Please wait a moment while we load the data."}
</p>
{/* Decorative background glow matching your layout */}
<div className="absolute -inset-10 -z-10 bg-(--accent)/5 blur-2xl rounded-full pointer-events-none" />
</div>
</div>
);
};
export default LoadingView;
+22 -5
View File
@@ -1,4 +1,3 @@
import { useNavigate } from "@tanstack/react-router";
import { useServerFn } from "@tanstack/react-start"; import { useServerFn } from "@tanstack/react-start";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useLichessUser } from "#/context/LichessUserContext.tsx"; import { useLichessUser } from "#/context/LichessUserContext.tsx";
@@ -14,6 +13,8 @@ import {
} from "#/server/lichess.ts"; } from "#/server/lichess.ts";
import { getHeaders } from "#/utils/pgnUtils.ts"; import { getHeaders } from "#/utils/pgnUtils.ts";
type Status = "loading" | "success" | "error";
interface ITokenData { interface ITokenData {
access_token: string; access_token: string;
expires_in: number; expires_in: number;
@@ -85,24 +86,40 @@ export const useLichess = () => {
return getSessionFn(); return getSessionFn();
}; };
const useLichessCallback = async ({ code }: { code: string }) => { const useLichessCallback = ({ code }: { code: string }) => {
const navigate = useNavigate(); const [status, setStatus] = useState<Status>("loading");
const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
if (!code) return; if (!code) return;
const isMounted = true;
try {
(async () => { (async () => {
const tokenData = await lichessAccessToken({ code }); const tokenData = await lichessAccessToken({ code });
const userData = await getLichessUser({ tokenData }); const userData = await getLichessUser({ tokenData });
if (!isMounted) return;
await setLichessUser({ await setLichessUser({
username: userData.username, username: userData.username,
id: userData.id, id: userData.id,
token: tokenData.access_token, token: tokenData.access_token,
}); });
await navigate({ to: "/chessboard" });
setStatus("success");
})(); })();
}, [navigate, code]); } catch (err: unknown) {
if (isMounted && err instanceof Error) {
console.error("Lichess Authentication Error:", err);
setError(err.message || "Failed to authenticate with Lichess");
setStatus("error");
}
}
}, [code]);
return { status, error };
}; };
const getLichessUserStudies = async () => { const getLichessUserStudies = async () => {
+12 -3
View File
@@ -1,14 +1,23 @@
import { getRouteApi } from "@tanstack/react-router"; import { getRouteApi, useNavigate } from "@tanstack/react-router";
import GenericErrorView from "#/components/GenericErrorView.tsx";
import LoadingView from "#/components/LoadingView.tsx";
import { useLichess } from "#/hooks/useLichess.ts"; import { useLichess } from "#/hooks/useLichess.ts";
const routeApi = getRouteApi("/callback"); const routeApi = getRouteApi("/callback");
const Callback = () => { const Callback = () => {
const navigate = useNavigate();
const { useLichessCallback } = useLichess(); const { useLichessCallback } = useLichess();
const { code } = routeApi.useSearch(); const { code } = routeApi.useSearch();
useLichessCallback({ code }).then(); const { status, error } = useLichessCallback({ code });
return <div>Loading...</div>; if (status === "success") navigate({ to: "/chessboard" });
if (status === "error") return <GenericErrorView error={error} />;
return (
<LoadingView title="Logging in..." description="Please wait a moment..." />
);
}; };
export default Callback; export default Callback;
+4
View File
@@ -3,6 +3,8 @@ import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { type ReactNode, Suspense } from "react"; import { type ReactNode, Suspense } from "react";
import { ToastContainer } from "react-toastify"; import { ToastContainer } from "react-toastify";
import GenericErrorView from "#/components/GenericErrorView.tsx";
import LoadingView from "#/components/LoadingView.tsx";
import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx"; import { MatomoAnalytics } from "#/components/MatomoAnalytics.tsx";
import { LichessUserProvider } from "#/context/LichessUserContext.tsx"; import { LichessUserProvider } from "#/context/LichessUserContext.tsx";
import Footer from "../components/Footer"; import Footer from "../components/Footer";
@@ -43,6 +45,8 @@ export const Route = createRootRoute({
], ],
}), }),
shellComponent: RootDocument, shellComponent: RootDocument,
errorComponent: ({ error }) => <GenericErrorView error={error} />,
pendingComponent: () => <LoadingView />,
}); });
function RootDocument({ children }: { children: ReactNode }) { function RootDocument({ children }: { children: ReactNode }) {
+10 -3
View File
@@ -1,16 +1,23 @@
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute, notFound } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import GenericErrorView from "#/components/GenericErrorView.tsx";
import Callback from "#/pages/Callback.tsx"; import Callback from "#/pages/Callback.tsx";
const callbackSchema = z.object({ const callbackSchema = z.object({
code: z.string(), code: z.string(),
}); });
// type CallbackProps = z.infer<typeof callbackSchema>;
export const Route = createFileRoute("/callback")({ export const Route = createFileRoute("/callback")({
validateSearch: (search) => callbackSchema.parse(search), validateSearch: (search) => callbackSchema.parse(search),
beforeLoad: ({ search }) => {
if (!search.code) {
throw notFound();
}
},
component: RouteComponent, component: RouteComponent,
errorComponent: ({ error }) => <GenericErrorView error={error} />,
}); });
function RouteComponent() { function RouteComponent() {