Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions frontend/__tests__/ErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { ErrorBoundary } from "../components/ErrorBoundary";

const ThrowingChild = () => {
throw new Error("Test render failure");
};

describe("ErrorBoundary", () => {
// Prevent console error spam during expected throw
const originalError = console.error;
beforeAll(() => {
console.error = jest.fn();
});
afterAll(() => {
console.error = originalError;
});

it("renders custom fallback UI when a child component throws", () => {
render(
<ErrorBoundary fallback={<div>Fallback UI Rendered</div>}>
<ThrowingChild />
</ErrorBoundary>
);

expect(screen.getByText("Fallback UI Rendered")).toBeInTheDocument();
});
});
53 changes: 53 additions & 0 deletions frontend/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// frontend/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
children: ReactNode;
fallback?: ReactNode;
}

interface State {
hasError: boolean;
error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
public state: State = { hasError: false, error: null };

public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("Uncaught error in ErrorBoundary:", error, errorInfo.componentStack);
}

public resetError = () => {
this.setState({ hasError: false, error: null });
};

public render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}

return (
<div className="p-6 rounded-lg bg-red-900 border border-red-700 text-center text-white my-4">
<h2 className="text-xl font-bold text-red-100 mb-2">Something went wrong</h2>
<p className="text-sm text-red-200 mb-4">
{this.state.error?.message || "An unexpected error occurred in this section."}
</p>
<button
onClick={this.resetError}
className="px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-white rounded font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-400"
>
Try Again
</button>
</div>
);
}

return this.props.children;
}
}
20 changes: 13 additions & 7 deletions frontend/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import NextApp, { type AppContext, type AppInitialProps, type AppProps } from "next/app";
import { useState, useEffect } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import { Toaster } from "sonner";
import Navbar from "@/components/Navbar";
import { PriceProvider } from "@/lib/priceContext";
import { I18nProvider } from "@/lib/i18n";
import { connectWallet, getConnectedPublicKey } from "@/lib/wallet";
import { ErrorBoundary } from "@/components/ErrorBoundary";

import { loadStarterAccount } from "@/lib/starterAccount";
import "@/styles/globals.css";

Expand All @@ -22,6 +25,7 @@ App.getInitialProps = async (appContext: AppContext): Promise<AppInitialProps> =

export default function App({ Component, pageProps }: AppProps) {
const [publicKey, setPublicKey] = useState<string | null>(null);
const router = useRouter();

useEffect(() => {
// Test seam: e2e tests inject a public key via window.addInitScript
Expand Down Expand Up @@ -71,12 +75,14 @@ export default function App({ Component, pageProps }: AppProps) {
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<Toaster position="top-right" richColors closeButton />
<div className="min-h-screen bg-[#f0f7f0]">
<Navbar publicKey={publicKey} onConnect={handleConnect} onDisconnect={() => setPublicKey(null)} />
<main>
<Component {...pageProps} publicKey={publicKey} onConnect={handleConnect} />
</main>
</div>
<ErrorBoundary key={router.asPath}>
<div className="min-h-screen bg-[#f0f7f0]">
<Navbar publicKey={publicKey} onConnect={handleConnect} onDisconnect={() => setPublicKey(null)} />
<main>
<Component {...pageProps} publicKey={publicKey} onConnect={handleConnect} />
</main>
</div>
</ErrorBoundary>
</I18nProvider>
);
}
}
30 changes: 30 additions & 0 deletions frontend/pages/_error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextPageContext } from "next";
import Link from "next/link";

function ErrorPage({ statusCode }: { statusCode?: number }) {
return (
<div className="min-h-screen bg-[#0B0F19] text-white flex flex-col items-center justify-center p-6 text-center">
<h1 className="text-5xl font-bold text-[#10B981] mb-4">
{statusCode ? `Error ${statusCode}` : "An Error Occurred"}
</h1>
<p className="text-gray-400 mb-6 max-w-md">
{statusCode === 404
? "The page you are looking for could not be found."
: "A server-side error occurred while rendering this page."}
</p>
<Link
href="/"
className="px-6 py-3 bg-[#10B981] hover:bg-emerald-600 text-white font-semibold rounded-lg transition-colors"
>
Return to Home
</Link>
</div>
);
}

ErrorPage.getInitialProps = ({ res, err }: NextPageContext) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
return { statusCode };
};

export default ErrorPage;
1 change: 1 addition & 0 deletions frontend/pages/donors/[publicKey].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function formatDate(iso: string): string {

function BadgePill({ tier, earnedAt }: { tier: BadgeTier; earnedAt: string }) {
const meta = BADGE_META[tier];
if (!meta) return null; // or use optional chaining below
return (
<div
title={`${meta.label} — earned ${formatDate(earnedAt)}`}
Expand Down
3 changes: 2 additions & 1 deletion frontend/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

Loading