WFDASHDRIVER-APP/app/map/page.tsx
SJCoetz 398d9edcbb Add map error boundary and move Leaflet CSS
Import global Leaflet CSS in the app layout, create a MapErrorBoundary in the map page to catch map load failures and show a retry UI, and wrap the dynamic CollectionMap with that boundary. Adjust CollectionMap by removing the local Leaflet CSS import, cleaning up unused imports/types, and changing the container to absolute/full-bleed styling so the map fills its parent. These changes improve robustness against map loading errors and ensure Leaflet styles are applied app-wide.
2026-05-27 01:17:56 +02:00

170 lines
No EOL
6.8 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, Suspense, Component } from 'react';
import { useRouter } from 'next/navigation';
import dynamic from 'next/dynamic';
import { OrderItem } from '@/lib/types';
import { useAuth } from '@/components/AuthProvider';
import BottomNav from '@/components/BottomNav';
import LocationTracker from '@/components/LocationTracker';
import { getAssignedCollections, isAuthenticated } from '@/lib/pocketbase';
import { cn } from '@/lib/utils';
import { RefreshCw, MapPin, AlertCircle } from 'lucide-react';
const CollectionMap = dynamic(() => import('@/components/CollectionMap'), {
ssr: false,
loading: () => (
<div className="h-full flex items-center justify-center bg-gray-100">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-[3px] border-orange-500 border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-gray-400">Loading map...</p>
</div>
</div>
),
});
class MapErrorBoundary extends Component<{ children: React.ReactNode }, { hasError: boolean; error: string }> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false, error: '' };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error: error.message };
}
render() {
if (this.state.hasError) {
return (
<div className="h-full flex flex-col items-center justify-center bg-gray-100 p-4">
<div className="w-16 h-16 bg-red-50 rounded-2xl flex items-center justify-center mb-4">
<AlertCircle size={32} className="text-red-400" />
</div>
<p className="text-gray-600 text-sm mb-4 text-center">Map failed to load.</p>
<button onClick={() => this.setState({ hasError: false })} className="px-5 py-2.5 bg-orange-500 text-white rounded-xl text-sm font-medium active:bg-orange-600">
Try Again
</button>
</div>
);
}
return this.props.children;
}
}
export default function MapPage() {
const router = useRouter();
const { isLoggedIn, isLoading: authLoading } = useAuth();
const [items, setItems] = useState<OrderItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
if (!authLoading && !isLoggedIn) router.push('/login/');
}, [isLoggedIn, authLoading, router]);
const loadItems = useCallback(async (showLoading = true) => {
if (!isAuthenticated()) return;
if (showLoading) setIsLoading(true);
setError('');
try {
const data = await getAssignedCollections();
setItems(data);
} catch (err) {
console.error('Failed to load collections:', err);
setError('Failed to load collections');
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, []);
useEffect(() => { loadItems(); }, [loadItems]);
const handleNavigate = (lat: number, lng: number, name: string) => {
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const url = isIOS
? `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}`
: `https://www.google.com/maps/dir/?api=1&destination=${lat},${lng}`;
window.open(url, '_blank');
};
if (authLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="flex flex-col items-center gap-3">
<div className="w-10 h-10 border-[3px] border-orange-500 border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-gray-400">Loading...</p>
</div>
</div>
);
}
if (!isLoggedIn) return null;
return (
<div className="h-screen flex flex-col bg-gray-50">
<LocationTracker />
<header className="bg-orange-600 text-white safe-top z-40 shadow-lg shadow-orange-600/20 flex-shrink-0">
<div className="px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 bg-white/15 rounded-xl flex items-center justify-center backdrop-blur-sm">
<MapPin size={18} />
</div>
<div>
<h1 className="text-lg font-bold leading-tight">Map</h1>
<p className="text-[11px] text-orange-100">{items.length} stop{items.length !== 1 ? 's' : ''}</p>
</div>
</div>
<button
onClick={() => { setIsRefreshing(true); loadItems(false); }}
disabled={isRefreshing}
className="p-2.5 rounded-xl bg-white/15 backdrop-blur-sm active:bg-white/25 transition-all touch-manipulation"
>
<RefreshCw size={18} className={cn(isRefreshing && 'animate-spin')} />
</button>
</div>
</header>
<main className="flex-1 relative">
{isLoading ? (
<div className="h-full flex items-center justify-center bg-gray-100">
<div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-[3px] border-orange-500 border-t-transparent rounded-full animate-spin" />
<p className="text-sm text-gray-400">Loading map...</p>
</div>
</div>
) : error ? (
<div className="h-full flex flex-col items-center justify-center bg-gray-100 p-4">
<div className="w-16 h-16 bg-red-50 rounded-2xl flex items-center justify-center mb-4">
<AlertCircle size={32} className="text-red-400" />
</div>
<p className="text-gray-600 text-sm mb-4 text-center">{error}</p>
<button onClick={() => loadItems()} className="px-5 py-2.5 bg-orange-500 text-white rounded-xl text-sm font-medium active:bg-orange-600 touch-manipulation press-feedback">
Try Again
</button>
</div>
) : items.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center bg-gray-100 p-4">
<div className="w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mb-4">
<MapPin size={32} className="text-gray-300" />
</div>
<p className="text-gray-600 font-semibold">No collections to show</p>
<p className="text-gray-400 text-sm mt-1">All collections completed!</p>
</div>
) : (
<Suspense fallback={
<div className="h-full flex items-center justify-center bg-gray-100">
<div className="w-8 h-8 border-[3px] border-orange-500 border-t-transparent rounded-full animate-spin" />
</div>
}>
<MapErrorBoundary>
<CollectionMap items={items} onNavigate={handleNavigate} />
</MapErrorBoundary>
</Suspense>
)}
</main>
<BottomNav />
</div>
);
}