WFDASHDRIVER-APP/app/map/page.tsx
SJCoetz db10eb95c7 Add PWA support and UI redesign
Add full PWA support (icons, manifest, service worker, pwa lib and bridge) and introduce install/offline UI. Generate app icons via a new scripts/generate-icons.js and include multiple icon sizes in public/icons; update manifest and layout head meta/preconnect. Revamp global styles (globals.css) with new utilities, animations, spacing for bottom nav, card shadows, skeletons and accessibility tweaks. Polish major screens (login, collections, map, settings) with improved headers, loading states, touch/press feedback, and layout adjustments; add new components (InstallPrompt, OfflineIndicator, PWABridge) and update various components to match new visual/UX patterns. Update Dockerfile to generate icons and use Next.js static export output (/out) for nginx, and bump package setup to support icon generation.
2026-05-27 00:34:50 +02:00

142 lines
No EOL
5.7 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback, Suspense } 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>
),
});
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>
}>
<CollectionMap items={items} onNavigate={handleNavigate} />
</Suspense>
)}
</main>
<BottomNav />
</div>
);
}