diff --git a/src/App.tsx b/src/App.tsx index 66e2efa2..9afe3a3a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { AuthProvider } from "@/hooks/useAuth"; import { LocationAutoDetectProvider } from "@/components/providers/LocationAutoDetectProvider"; +import { ErrorBoundary } from "@/components/ErrorBoundary"; import { Footer } from "@/components/layout/Footer"; import Index from "./pages/Index"; import Parks from "./pages/Parks"; @@ -116,13 +117,15 @@ function AppContent() { } const App = () => ( - - - - - - - + + + + + + + + + ); export default App; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..d127c7ee --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,55 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { AlertCircle } from 'lucide-react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo); + } + + private handleReset = () => { + this.setState({ hasError: false, error: undefined }); + window.location.reload(); + }; + + public render() { + if (this.state.hasError) { + return ( + + + + Something went wrong + + + {this.state.error?.message || 'An unexpected error occurred'} + + + Reload Page + + + + + ); + } + + return this.props.children; + } +} diff --git a/src/hooks/useLocationAutoDetect.ts b/src/hooks/useLocationAutoDetect.ts index 0e3f5d65..504bedc1 100644 --- a/src/hooks/useLocationAutoDetect.ts +++ b/src/hooks/useLocationAutoDetect.ts @@ -24,27 +24,36 @@ export function useLocationAutoDetect() { // Only run auto-detection after preferences have loaded if (loading) return; - // Check if localStorage is available - if (!isLocalStorageAvailable()) { - console.warn('localStorage is not available, skipping location auto-detection'); - return; - } - - // Check if we've already attempted detection - const hasAttemptedDetection = localStorage.getItem('location_detection_attempted'); - - // Auto-detect if we haven't attempted it yet and auto_detect is enabled - if (preferences.auto_detect && !hasAttemptedDetection) { - autoDetectPreferences().then(() => { - if (isLocalStorageAvailable()) { - localStorage.setItem('location_detection_attempted', 'true'); + // Defer auto-detection to not block initial render + const timeoutId = setTimeout(() => { + try { + // Check if localStorage is available + if (!isLocalStorageAvailable()) { + console.warn('localStorage is not available, skipping location auto-detection'); + return; } - }).catch((error) => { - console.error('❌ Failed to auto-detect location:', error); - if (isLocalStorageAvailable()) { - localStorage.setItem('location_detection_attempted', 'true'); + + // Check if we've already attempted detection + const hasAttemptedDetection = localStorage.getItem('location_detection_attempted'); + + // Auto-detect if we haven't attempted it yet and auto_detect is enabled + if (preferences.auto_detect && !hasAttemptedDetection) { + autoDetectPreferences().then(() => { + if (isLocalStorageAvailable()) { + localStorage.setItem('location_detection_attempted', 'true'); + } + }).catch((error) => { + console.error('❌ Failed to auto-detect location:', error); + if (isLocalStorageAvailable()) { + localStorage.setItem('location_detection_attempted', 'true'); + } + }); } - }); - } - }, [user, loading, preferences.auto_detect]); + } catch (error) { + console.error('❌ Error in location auto-detection:', error); + } + }, 1000); // Defer by 1 second to allow app to render first + + return () => clearTimeout(timeoutId); + }, [user, loading, preferences.auto_detect, autoDetectPreferences]); } \ No newline at end of file diff --git a/src/hooks/useUnitPreferences.ts b/src/hooks/useUnitPreferences.ts index 06202dcc..e1867ebe 100644 --- a/src/hooks/useUnitPreferences.ts +++ b/src/hooks/useUnitPreferences.ts @@ -15,7 +15,25 @@ export function useUnitPreferences() { const [loading, setLoading] = useState(true); useEffect(() => { - loadPreferences(); + let mounted = true; + + const load = async () => { + try { + await loadPreferences(); + } catch (error) { + console.error('Failed to load preferences:', error); + } finally { + if (mounted) { + setLoading(false); + } + } + }; + + load(); + + return () => { + mounted = false; + }; }, [user]); const loadPreferences = async () => { @@ -58,7 +76,15 @@ export function useUnitPreferences() { const autoDetectPreferences = useCallback(async () => { try { - const response = await supabase.functions.invoke('detect-location'); + // Add timeout to prevent hanging + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Location detection timeout')), 5000) + ); + + const response = await Promise.race([ + supabase.functions.invoke('detect-location'), + timeoutPromise + ]) as any; if (response.data && response.data.measurementSystem) { const newPreferences: UnitPreferences = {
+ {this.state.error?.message || 'An unexpected error occurred'} +