feat: Add comprehensive error boundaries for graceful error handling
Implemented React error boundaries to catch and handle errors gracefully: **Core Error Handling Components:** - Created ErrorBoundary class component with error catching and logging - Created specialized fallback UIs (MinimalErrorFallback, DataErrorFallback, ComponentErrorFallback, FormErrorFallback, ChartErrorFallback, ImageErrorFallback) - Added withErrorBoundary HOC for easy component wrapping - Created errorLogger service with Sentry integration placeholder **Error Logging Service (errorLogger.ts):** - Centralized error logging with severity levels (FATAL, ERROR, WARNING, INFO, DEBUG) - Context enrichment (URL, userAgent, timestamp, environment) - Local storage of last 10 errors in sessionStorage for debugging - User context management (setUser, clearUser) - Breadcrumb support for debugging trails **App Integration:** - Wrapped root layout with top-level ErrorBoundary for catastrophic errors - Added NetworkStatusIndicator to main page for offline sync visibility - Wrapped daily summary section with isolated DataErrorFallback - Added error boundary to AI assistant page with ComponentErrorFallback - Wrapped feeding tracking form with FormErrorFallback using withErrorBoundary HOC - Protected analytics charts with isolated ChartErrorFallback boundaries **Error Recovery Features:** - Isolated error boundaries prevent cascade failures - Retry buttons on all fallback UIs - Error count tracking with user warnings - Development-mode error details display - Automatic error logging to service (when Sentry integrated) Next: Integration with Sentry for production error tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import { lazy, Suspense } from 'react';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
|
||||
import { ComponentErrorFallback } from '@/components/common/ErrorFallbacks';
|
||||
|
||||
// Lazy load the AI chat interface component
|
||||
const AIChatInterface = lazy(() =>
|
||||
@@ -16,9 +18,14 @@ export default function AIAssistantPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<Suspense fallback={<LoadingFallback variant="chat" />}>
|
||||
<AIChatInterface />
|
||||
</Suspense>
|
||||
<ErrorBoundary
|
||||
isolate
|
||||
fallback={<ComponentErrorFallback error={new Error('AI Assistant failed to load')} />}
|
||||
>
|
||||
<Suspense fallback={<LoadingFallback variant="chat" />}>
|
||||
<AIChatInterface />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</AppShell>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from '@mui/material';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
|
||||
import { ChartErrorFallback } from '@/components/common/ErrorFallbacks';
|
||||
import {
|
||||
TrendingUp,
|
||||
Hotel,
|
||||
@@ -232,21 +234,27 @@ export default function AnalyticsPage() {
|
||||
</Box>
|
||||
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<WeeklySleepChart />
|
||||
</Box>
|
||||
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<WeeklySleepChart />
|
||||
</Box>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<FeedingFrequencyGraph />
|
||||
</Box>
|
||||
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<FeedingFrequencyGraph />
|
||||
</Box>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<GrowthCurve />
|
||||
</Box>
|
||||
<ErrorBoundary isolate fallback={<ChartErrorFallback />}>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<GrowthCurve />
|
||||
</Box>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tabValue} index={3}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { ThemeRegistry } from '@/components/ThemeRegistry';
|
||||
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
|
||||
// import { PerformanceMonitor } from '@/components/common/PerformanceMonitor'; // Temporarily disabled
|
||||
import './globals.css';
|
||||
|
||||
@@ -37,10 +38,12 @@ export default function RootLayout({
|
||||
<link rel="apple-touch-icon" href="/icon-192x192.png" />
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<ThemeRegistry>
|
||||
{/* <PerformanceMonitor /> */}
|
||||
{children}
|
||||
</ThemeRegistry>
|
||||
<ErrorBoundary>
|
||||
<ThemeRegistry>
|
||||
{/* <PerformanceMonitor /> */}
|
||||
{children}
|
||||
</ThemeRegistry>
|
||||
</ErrorBoundary>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,9 @@ import { Box, Typography, Button, Paper, Grid, CircularProgress } from '@mui/mat
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { EmailVerificationBanner } from '@/components/common/EmailVerificationBanner';
|
||||
import { ErrorBoundary } from '@/components/common/ErrorBoundary';
|
||||
import { DataErrorFallback } from '@/components/common/ErrorFallbacks';
|
||||
import { NetworkStatusIndicator } from '@/components/common/NetworkStatusIndicator';
|
||||
import {
|
||||
Restaurant,
|
||||
Hotel,
|
||||
@@ -84,6 +87,7 @@ export default function HomePage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<AppShell>
|
||||
<NetworkStatusIndicator />
|
||||
<Box>
|
||||
<EmailVerificationBanner />
|
||||
|
||||
@@ -139,59 +143,64 @@ export default function HomePage() {
|
||||
<Typography variant="h6" gutterBottom fontWeight="600" sx={{ mb: 2 }}>
|
||||
Today's Summary{selectedChild ? ` - ${selectedChild.name}` : ''}
|
||||
</Typography>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : !dailySummary ? (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{children.length === 0
|
||||
? 'Add a child to start tracking'
|
||||
: 'No activities tracked today'}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={4}>
|
||||
<Box textAlign="center">
|
||||
<Restaurant sx={{ fontSize: 32, color: 'primary.main', mb: 1 }} />
|
||||
<Typography variant="h5" fontWeight="600">
|
||||
{dailySummary.feedingCount || 0}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Feedings
|
||||
</Typography>
|
||||
</Box>
|
||||
<ErrorBoundary
|
||||
isolate
|
||||
fallback={<DataErrorFallback error={new Error('Failed to load summary')} />}
|
||||
>
|
||||
<Paper sx={{ p: 3 }}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : !dailySummary ? (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{children.length === 0
|
||||
? 'Add a child to start tracking'
|
||||
: 'No activities tracked today'}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={4}>
|
||||
<Box textAlign="center">
|
||||
<Restaurant sx={{ fontSize: 32, color: 'primary.main', mb: 1 }} />
|
||||
<Typography variant="h5" fontWeight="600">
|
||||
{dailySummary.feedingCount || 0}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Feedings
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Box textAlign="center">
|
||||
<Hotel sx={{ fontSize: 32, color: 'info.main', mb: 1 }} />
|
||||
<Typography variant="h5" fontWeight="600">
|
||||
{dailySummary.sleepTotalMinutes
|
||||
? formatSleepHours(dailySummary.sleepTotalMinutes)
|
||||
: '0m'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Sleep
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Box textAlign="center">
|
||||
<BabyChangingStation sx={{ fontSize: 32, color: 'warning.main', mb: 1 }} />
|
||||
<Typography variant="h5" fontWeight="600">
|
||||
{dailySummary.diaperCount || 0}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Diapers
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Box textAlign="center">
|
||||
<Hotel sx={{ fontSize: 32, color: 'info.main', mb: 1 }} />
|
||||
<Typography variant="h5" fontWeight="600">
|
||||
{dailySummary.sleepTotalMinutes
|
||||
? formatSleepHours(dailySummary.sleepTotalMinutes)
|
||||
: '0m'}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Sleep
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Box textAlign="center">
|
||||
<BabyChangingStation sx={{ fontSize: 32, color: 'warning.main', mb: 1 }} />
|
||||
<Typography variant="h5" fontWeight="600">
|
||||
{dailySummary.diaperCount || 0}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Diapers
|
||||
</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
)}
|
||||
</Paper>
|
||||
</ErrorBoundary>
|
||||
|
||||
{/* Next Predicted Activity */}
|
||||
<Box sx={{ mt: 4 }}>
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||
import { withErrorBoundary } from '@/components/common/ErrorFallbacks';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { trackingApi, Activity } from '@/lib/api/tracking';
|
||||
import { childrenApi, Child } from '@/lib/api/children';
|
||||
@@ -60,7 +61,7 @@ interface FeedingData {
|
||||
amountDescription?: string;
|
||||
}
|
||||
|
||||
export default function FeedingTrackPage() {
|
||||
function FeedingTrackPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const [children, setChildren] = useState<Child[]>([]);
|
||||
@@ -654,3 +655,5 @@ export default function FeedingTrackPage() {
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
||||
export default withErrorBoundary(FeedingTrackPage, 'form');
|
||||
|
||||
Reference in New Issue
Block a user