feat: add root and route-level error boundaries
This commit is contained in:
parent
e3d28e4127
commit
0da8397940
4 changed files with 109 additions and 0 deletions
56
apps/web/src/components/RootError.tsx
Normal file
56
apps/web/src/components/RootError.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
interface RootErrorProps {
|
||||||
|
error: Error;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootError({ error, reset }: RootErrorProps) {
|
||||||
|
return (
|
||||||
|
<section className="relative flex flex-col items-center justify-center min-h-screen text-center overflow-hidden px-6">
|
||||||
|
<div className="absolute inset-0 -z-10">
|
||||||
|
<div className="absolute top-0 left-1/2 h-72 w-184 -translate-x-1/2 rounded-full bg-(--color-primary) opacity-[0.10] blur-3xl" />
|
||||||
|
<div className="absolute top-10 left-1/2 h-72 w-184 -translate-x-1/2 rounded-full bg-(--color-accent) opacity-[0.10] blur-3xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="-rotate-1 mb-4">
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full bg-(--color-surface) px-4 py-1 text-xs font-semibold tracking-widest uppercase text-(--color-accent) border border-(--color-primary-light)">
|
||||||
|
something went wrong
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-5xl md:text-6xl font-black tracking-tight text-(--color-text) leading-[1.05]">
|
||||||
|
Unexpected{" "}
|
||||||
|
<span className="inline-block rotate-1 px-3 py-1 bg-(--color-primary) text-white rounded-xl">
|
||||||
|
error
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-6 text-lg font-medium text-(--color-text-muted) max-w-sm">
|
||||||
|
Something crashed. This has been noted —{" "}
|
||||||
|
<span className="text-(--color-accent) font-bold">it's not you</span>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{import.meta.env.DEV && (
|
||||||
|
<pre className="mt-4 max-w-xl w-full text-left text-xs bg-(--color-surface) border border-(--color-primary-light) rounded-2xl p-4 overflow-auto text-(--color-text-muted)">
|
||||||
|
{error.message}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex gap-3 flex-wrap justify-center">
|
||||||
|
<button
|
||||||
|
onClick={reset}
|
||||||
|
className="px-7 py-3 rounded-full text-white font-bold text-sm bg-(--color-primary) hover:bg-(--color-primary-dark)"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="px-7 py-3 rounded-full font-bold text-sm text-(--color-primary) border-2 border-(--color-primary) hover:bg-(--color-surface)"
|
||||||
|
>
|
||||||
|
Back to home
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
apps/web/src/components/RouteError.tsx
Normal file
49
apps/web/src/components/RouteError.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
interface RouteErrorProps {
|
||||||
|
error: Error;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RouteError({ error, reset }: RouteErrorProps) {
|
||||||
|
return (
|
||||||
|
<section className="relative flex flex-col items-center justify-center py-24 text-center overflow-hidden">
|
||||||
|
<div className="absolute inset-0 -z-10">
|
||||||
|
<div className="absolute top-0 left-1/2 h-72 w-184 -translate-x-1/2 rounded-full bg-(--color-primary) opacity-[0.10] blur-3xl" />
|
||||||
|
<div className="absolute top-10 left-1/2 h-72 w-184 -translate-x-1/2 rounded-full bg-(--color-accent) opacity-[0.10] blur-3xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="-rotate-1 mb-4">
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full bg-(--color-surface) px-4 py-1 text-xs font-semibold tracking-widest uppercase text-(--color-accent) border border-(--color-primary-light)">
|
||||||
|
something went wrong
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-5xl font-black tracking-tight text-(--color-text) leading-[1.05]">
|
||||||
|
This page{" "}
|
||||||
|
<span className="inline-block rotate-1 px-3 py-1 bg-(--color-primary) text-white rounded-xl">
|
||||||
|
crashed
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="mt-6 text-lg font-medium text-(--color-text-muted) max-w-sm">
|
||||||
|
Something went wrong loading this page.{" "}
|
||||||
|
<span className="text-(--color-accent) font-bold">Try again</span> or
|
||||||
|
head back home.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{import.meta.env.DEV && (
|
||||||
|
<pre className="mt-4 max-w-xl w-full text-left text-xs bg-(--color-surface) border border-(--color-primary-light) rounded-2xl p-4 overflow-auto text-(--color-text-muted)">
|
||||||
|
{error.message}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex gap-3 flex-wrap justify-center">
|
||||||
|
<button
|
||||||
|
onClick={reset}
|
||||||
|
className="px-7 py-3 rounded-full text-white font-bold text-sm bg-(--color-primary) hover:bg-(--color-primary-dark)"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ import { createRootRoute, Outlet } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
import Navbar from "../components/navbar/NavBar";
|
import Navbar from "../components/navbar/NavBar";
|
||||||
import NotFound from "../components/NotFound";
|
import NotFound from "../components/NotFound";
|
||||||
|
import RootError from "../components/RootError";
|
||||||
|
|
||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -18,4 +19,5 @@ const RootLayout = () => {
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RootLayout,
|
component: RootLayout,
|
||||||
notFoundComponent: NotFound,
|
notFoundComponent: NotFound,
|
||||||
|
errorComponent: RootError,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import type { GameSession, GameRequest, AnswerResult } from "@lila/shared";
|
||||||
import { QuestionCard } from "../components/game/QuestionCard";
|
import { QuestionCard } from "../components/game/QuestionCard";
|
||||||
import { ScoreScreen } from "../components/game/ScoreScreen";
|
import { ScoreScreen } from "../components/game/ScoreScreen";
|
||||||
import { GameSetup } from "../components/game/GameSetup";
|
import { GameSetup } from "../components/game/GameSetup";
|
||||||
|
import RouteError from "../components/RouteError";
|
||||||
import { authClient } from "../lib/auth-client";
|
import { authClient } from "../lib/auth-client";
|
||||||
|
|
||||||
type GameStartResponse = { success: true; data: GameSession };
|
type GameStartResponse = { success: true; data: GameSession };
|
||||||
|
|
@ -127,6 +128,7 @@ function Play() {
|
||||||
|
|
||||||
export const Route = createFileRoute("/play")({
|
export const Route = createFileRoute("/play")({
|
||||||
component: Play,
|
component: Play,
|
||||||
|
errorComponent: RouteError,
|
||||||
beforeLoad: async () => {
|
beforeLoad: async () => {
|
||||||
const { data: session } = await authClient.getSession();
|
const { data: session } = await authClient.getSession();
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue