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
76 changes: 66 additions & 10 deletions eduaid_web/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,75 @@ import Text_Input from "./pages/Text_Input";
import Output from "./pages/Output";
import Previous from "./pages/Previous";
import NotFound from "./pages/PageNotFound";
import ErrorBoundary from "./components/errorBoundary/ErrorBoundary";


function App() {
const handleBoundaryError = (error, errorInfo) => {
// Central place to integrate Sentry/LogRocket later
console.error("App-level boundary captured:", error);
console.error("App-level stack:", errorInfo?.componentStack);
};

return (
<HashRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/question-type" element={<Question_Type />} />
<Route path="/input" element={<Text_Input />} />
<Route path="/output" element={<Output />} />
<Route path="/history" element={<Previous />} />
<Route path="*" element={<NotFound />} />
</Routes>
</HashRouter>
<ErrorBoundary
title="The app ran into an unexpected error."
message="Please try again. If the issue continues, refresh the page."
onError={handleBoundaryError}
>
<HashRouter>
<Routes>
<Route
path="/"
element={
<ErrorBoundary title="Home failed to load.">
<Home />
</ErrorBoundary>
}
/>
<Route
path="/question-type"
element={
<ErrorBoundary title="Question type page failed to load.">
<Question_Type />
</ErrorBoundary>
}
/>
<Route
path="/input"
element={
<ErrorBoundary title="Input page failed to load.">
<Text_Input />
</ErrorBoundary>
}
/>
<Route
path="/output"
element={
<ErrorBoundary title="Output page failed to load.">
<Output />
</ErrorBoundary>
}
/>
<Route
path="/history"
element={
<ErrorBoundary title="History page failed to load.">
<Previous />
</ErrorBoundary>
}
/>
<Route
path="*"
element={
<ErrorBoundary title="Page failed to load.">
<NotFound />
</ErrorBoundary>
}
/>
</Routes>
</HashRouter>
</ErrorBoundary>
);
}

Expand Down
75 changes: 75 additions & 0 deletions eduaid_web/src/components/errorBoundary/ErrorBoundary.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
.error-boundary-container {
min-height: 220px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
box-sizing: border-box;
}

.error-boundary-card {
width: 100%;
max-width: 720px;
background: #141828;
border: 1px solid #2b3350;
border-radius: 12px;
padding: 20px;
color: #f3f6ff;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
}

.error-boundary-title {
margin: 0 0 8px;
font-size: 1.25rem;
line-height: 1.3;
color: #ffb4b4;
}

.error-boundary-message {
margin: 0 0 14px;
font-size: 0.98rem;
color: #d7ddf5;
}

.error-boundary-details {
margin: 0 0 14px;
padding: 10px;
background: #0d1120;
border: 1px solid #2f395c;
border-radius: 8px;
color: #c6d0f8;
font-size: 0.78rem;
line-height: 1.4;
overflow: auto;
max-height: 220px;
white-space: pre-wrap;
overflow-wrap: anywhere;
}

.error-boundary-button {
border: none;
border-radius: 8px;
background: #4f7cff;
color: #ffffff;
padding: 10px 14px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease;
}

.error-boundary-button:hover {
background: #3f68e0;
}

.error-boundary-button:focus {
outline: 2px solid #9ab4ff;
outline-offset: 2px;
}

.error-boundary-retry-limit {
margin: 8px 0 0;
color: #ffd3d3;
font-size: 0.9rem;
}
97 changes: 97 additions & 0 deletions eduaid_web/src/components/errorBoundary/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { Component } from "react";
import "./ErrorBoundary.css";

class ErrorBoundary extends Component {
static MAX_RETRIES = 3;

constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
retryCount: 0,
};
}

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

componentDidCatch(error, errorInfo) {
// Optional callback for external monitoring
if(typeof this.props.onError === "function") {
try{
this.props.onError(error,errorInfo);
} catch(callbackError){
console.error("ErrorBoundary onError callback failed:", callbackError);
}
}

// This is for debugging and future monitoring integration.
console.error("ErrorBoundary caught an error:", error);
console.error("Component stack:", errorInfo?.componentStack);

this.setState({
errorInfo,
});
}

handleRetry = () => {
if (this.state.retryCount >= ErrorBoundary.MAX_RETRIES) return;

this.setState((prevState) => ({
hasError: false,
error: null,
errorInfo: null,
retryCount: prevState.retryCount + 1,
}));
};

render() {
if (this.state.hasError) {
const canRetry = this.state.retryCount < ErrorBoundary.MAX_RETRIES;
const {
title = "Something went wrong in this section.",
message = "An unexpected error occurred. You can try again.",
showDetails = process.env.NODE_ENV !== "production",
} = this.props;

return (
<div className="error-boundary-container" role="alert" aria-live="assertive">
<div className="error-boundary-card">
<h2 className="error-boundary-title">{title}</h2>
<p className="error-boundary-message">{message}</p>

{showDetails && this.state.error && (
<pre className="error-boundary-details">
{this.state.error.toString()}
{this.state.errorInfo?.componentStack
? `\n\n${this.state.errorInfo.componentStack}`
: ""}
</pre>
)}

{canRetry ? (
<button
type="button"
className="error-boundary-button"
onClick={this.handleRetry}
>
Try Again ({ErrorBoundary.MAX_RETRIES - this.state.retryCount} left)
</button>
) : (
<p className="error-boundary-retry-limit">
Maximum retry attempts reached. Please refresh the page.
</p>
)}
</div>
</div>
);
}

return this.props.children;
}
}

export default ErrorBoundary;