mirror of
https://github.com/TheRealOwenRees/chess-scribe-website.git
synced 2026-07-23 01:46:57 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f475732715
|
|||
|
f4b9820b5b
|
|||
|
98a5bb742b
|
|||
|
6ef08f4b91
|
|||
|
5bf4ec95c8
|
@@ -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
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
+31
-14
@@ -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;
|
||||||
|
|
||||||
(async () => {
|
const isMounted = true;
|
||||||
const tokenData = await lichessAccessToken({ code });
|
|
||||||
const userData = await getLichessUser({ tokenData });
|
|
||||||
|
|
||||||
await setLichessUser({
|
try {
|
||||||
username: userData.username,
|
(async () => {
|
||||||
id: userData.id,
|
const tokenData = await lichessAccessToken({ code });
|
||||||
token: tokenData.access_token,
|
const userData = await getLichessUser({ tokenData });
|
||||||
});
|
|
||||||
await navigate({ to: "/chessboard" });
|
if (!isMounted) return;
|
||||||
})();
|
|
||||||
}, [navigate, code]);
|
await setLichessUser({
|
||||||
|
username: userData.username,
|
||||||
|
id: userData.id,
|
||||||
|
token: tokenData.access_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
setStatus("success");
|
||||||
|
})();
|
||||||
|
} 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
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user