From 416dfc980f9f5b91933e02827b02e37c790192c3 Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Wed, 27 May 2026 23:19:33 +0530 Subject: [PATCH 1/2] feat: implement react-error-boundary for global and component-level error handling --- package-lock.json | 14 +- package.json | 1 + src/App.tsx | 6 +- .../ErrorBoundary/AppErrorFallback.tsx | 18 +++ .../ErrorBoundary/FeatureErrorFallback.tsx | 31 ++++ src/components/QuestionCard/AskAIBanner.tsx | 19 ++- src/components/Renderers/MathRenderer.tsx | 15 +- src/routes/AppRoutes.tsx | 137 ++++++++++++++++-- 8 files changed, 223 insertions(+), 18 deletions(-) create mode 100644 src/components/ErrorBoundary/AppErrorFallback.tsx create mode 100644 src/components/ErrorBoundary/FeatureErrorFallback.tsx diff --git a/package-lock.json b/package-lock.json index f5e295a..6d02deb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gate-prep", - "version": "0.10.6", + "version": "0.10.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gate-prep", - "version": "0.10.6", + "version": "0.10.7", "dependencies": { "@base-ui/react": "^1.2.0", "@nivo/calendar": "^0.99.0", @@ -38,6 +38,7 @@ "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-error-boundary": "^6.1.2", "react-katex": "^3.1.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.6.0", @@ -11913,6 +11914,15 @@ "react": "^19.1.0" } }, + "node_modules/react-error-boundary": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.1.2.tgz", + "integrity": "sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 7119cea..5e0e1fb 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "radix-ui": "^1.4.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-error-boundary": "^6.1.2", "react-katex": "^3.1.0", "react-markdown": "^10.1.0", "react-router-dom": "^7.6.0", diff --git a/src/App.tsx b/src/App.tsx index 668de3a..64df3bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,12 +7,14 @@ */ import { BrowserRouter as Router } from 'react-router-dom'; +import { ErrorBoundary } from 'react-error-boundary'; import AppProvider from './context/AppProvider.tsx'; import AuthProvider from './context/AuthProvider.tsx'; import StatsProvider from './context/StatsProvider.tsx'; import AppRoutes from './routes/AppRoutes.tsx'; import { SpeedInsights } from '@vercel/speed-insights/react'; import { GoalProvider } from './context/GoalProvider.tsx'; +import AppErrorFallback from './components/ErrorBoundary/AppErrorFallback.tsx'; /** * @function App @@ -33,7 +35,9 @@ function App() { {/* AppProvider manages general application settings, like sound effects. */} {/* AppRoutes contains all the defined application routes. */} - + + + {/* Vercel Speed Insights */} diff --git a/src/components/ErrorBoundary/AppErrorFallback.tsx b/src/components/ErrorBoundary/AppErrorFallback.tsx new file mode 100644 index 0000000..08fa5e2 --- /dev/null +++ b/src/components/ErrorBoundary/AppErrorFallback.tsx @@ -0,0 +1,18 @@ +export default function AppErrorFallback() { + return ( +
+

+ GATEQuest encountered an error +

+

+ An unexpected error has caused the application to stop. Please refresh to continue. +

+ +
+ ); +} diff --git a/src/components/ErrorBoundary/FeatureErrorFallback.tsx b/src/components/ErrorBoundary/FeatureErrorFallback.tsx new file mode 100644 index 0000000..a702fed --- /dev/null +++ b/src/components/ErrorBoundary/FeatureErrorFallback.tsx @@ -0,0 +1,31 @@ +import type { FallbackProps } from 'react-error-boundary'; +import { useNavigate } from 'react-router-dom'; + +export default function FeatureErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + const navigate = useNavigate(); + + return ( +
+

+ Something went wrong in this section. +

+

+ {error?.message || 'An unexpected error occurred.'} +

+
+ + +
+
+ ); +} diff --git a/src/components/QuestionCard/AskAIBanner.tsx b/src/components/QuestionCard/AskAIBanner.tsx index 957b1a1..67c8b3c 100644 --- a/src/components/QuestionCard/AskAIBanner.tsx +++ b/src/components/QuestionCard/AskAIBanner.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { CircleNotchIcon, ArrowSquareOutIcon, @@ -134,4 +135,20 @@ const AskAIBanner: React.FC = ({ provider, onClick }) => { ); }; -export default AskAIBanner; +function AskAIBannerFallback() { + return ( +
+ AI assistant failed to load. +
+ ); +} + +function AskAIBannerWithBoundary(props: AskAIBannerProps) { + return ( + + + + ); +} + +export default AskAIBannerWithBoundary; diff --git a/src/components/Renderers/MathRenderer.tsx b/src/components/Renderers/MathRenderer.tsx index de083bc..9f5c116 100644 --- a/src/components/Renderers/MathRenderer.tsx +++ b/src/components/Renderers/MathRenderer.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; import { InlineMath, BlockMath } from 'react-katex'; import 'katex/dist/katex.min.css'; import TableRenderer from '../Renderers/TableRenderer.js'; @@ -316,4 +317,16 @@ const MathRenderer = ({ text }: MathRendererProps) => { ); }; -export default MathRenderer; +function MathRendererFallback() { + return [Render failed]; +} + +function MathRendererWithBoundary(props: MathRendererProps) { + return ( + + + + ); +} + +export default MathRendererWithBoundary; diff --git a/src/routes/AppRoutes.tsx b/src/routes/AppRoutes.tsx index 74d2caf..66c5cf1 100644 --- a/src/routes/AppRoutes.tsx +++ b/src/routes/AppRoutes.tsx @@ -9,10 +9,9 @@ import LandingPage from '../pages/LandingPage.jsx'; import Layout from '../components/Layout.jsx'; import Dashboard from '../pages/Dashboard.jsx'; import Practice from '../pages/Practice/Practice.js'; - import SettingsRoutes from './SettingsRoutes.js'; import About from '../pages/About.jsx'; -import { Navigate, Route, Routes } from 'react-router-dom'; +import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; import useAuth from '../hooks/useAuth.ts'; import DonationPage from '../pages/Donations.tsx'; import PracticeList from '@/pages/Practice/PracticeList.tsx'; @@ -27,6 +26,8 @@ import TopicTestSessionPage from '@/pages/TopicTest/TopicTestSession.tsx'; import TopicTestResult from '@/pages/TopicTest/TopicTestResult.tsx'; import TestSolutionView from '@/pages/TopicTest/TestSolutionView.tsx'; import TopicReviewLayout from '@/pages/TopicTest/TopicReviewLayout.tsx'; +import { ErrorBoundary } from 'react-error-boundary'; +import FeatureErrorFallback from '@/components/ErrorBoundary/FeatureErrorFallback.tsx'; /** * @function AppRoutes @@ -37,6 +38,7 @@ import TopicReviewLayout from '@/pages/TopicTest/TopicReviewLayout.tsx'; export default function AppRoutes() { // isLogin and loading states are consumed from the AuthContext. const { isLogin, loading } = useAuth(); + const location = useLocation(); return ( @@ -62,37 +64,145 @@ export default function AppRoutes() { {/* The main dashboard, the first page after login. */} } /> {/* The practice section has nested routes for subjects and individual questions. */} - } /> - } /> - } /> + + + + + } + /> + + + + } + /> + + + + } + /> + {/* Settings routes are modularized into their own component for clarity. */} } /> {/* A static 'About' page. */} } /> } /> {/* The revision section has nested routes for revision list and individual questions. */} - } /> - } /> + + + + + } + /> + + + + } + /> } + element={ + + + + } /> {/* Topic Test */} - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> } + element={ + + + + } /> - }> + + + + + } + > } /> } /> + {/* A catch-all route to handle undefined paths within the app. */} {/* It redirects the user to the root to prevent 404 errors. */} } /> @@ -102,3 +212,4 @@ export default function AppRoutes() { ); } + From 30af2df01946720c1c378a6663843435a1b4b2cf Mon Sep 17 00:00:00 2001 From: Karan Singh Date: Wed, 27 May 2026 23:30:12 +0530 Subject: [PATCH 2/2] fixes some issues raised by codeRabbit --- src/App.tsx | 9 ++++++++- .../ErrorBoundary/FeatureErrorFallback.tsx | 20 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 64df3bf..36238c9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,7 +35,14 @@ function App() { {/* AppProvider manages general application settings, like sound effects. */} {/* AppRoutes contains all the defined application routes. */} - + { + // Log to console in all environments. + // Replace with your monitoring client (e.g. Sentry.captureException) when ready. + console.error('[AppErrorBoundary]', error, info.componentStack); + }} + > diff --git a/src/components/ErrorBoundary/FeatureErrorFallback.tsx b/src/components/ErrorBoundary/FeatureErrorFallback.tsx index a702fed..b9b65ee 100644 --- a/src/components/ErrorBoundary/FeatureErrorFallback.tsx +++ b/src/components/ErrorBoundary/FeatureErrorFallback.tsx @@ -1,16 +1,34 @@ import type { FallbackProps } from 'react-error-boundary'; import { useNavigate } from 'react-router-dom'; +function getUserFriendlyMessage(error: Error | undefined): string { + if (!error?.message) return 'An unexpected error occurred. Please try again.'; + const msg = error.message.toLowerCase(); + if (msg.includes('network') || msg.includes('fetch') || msg.includes('failed to fetch')) { + return 'A network error occurred. Please check your connection and try again.'; + } + if (msg.includes('timeout')) { + return 'The request timed out. Please try again.'; + } + if (msg.includes('unauthorized') || msg.includes('403') || msg.includes('401')) { + return 'You do not have permission to view this section. Please log in again.'; + } + return 'An unexpected error occurred. Please try again.'; +} + export default function FeatureErrorFallback({ error, resetErrorBoundary }: FallbackProps) { const navigate = useNavigate(); + // Raw error is available here for logging/telemetry only — not rendered to the UI. + // console.error('[FeatureErrorBoundary]', error); + return (

Something went wrong in this section.

- {error?.message || 'An unexpected error occurred.'} + {getUserFriendlyMessage(error)}