- Added package.json with dependencies for Next.js, React, and Tailwind CSS. - Created postcss.config.js for Tailwind CSS and autoprefixer integration. - Added SVG icon for the application in public/icons/icon.svg. - Created manifest.json for PWA support with app details and icon. - Configured Tailwind CSS in tailwind.config.js with custom colors and animations. - Set up TypeScript configuration in tsconfig.json for strict type checking and module resolution.
158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback, Suspense } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import dynamic from 'next/dynamic';
|
|
import { Collection } 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';
|
|
|
|
// Dynamic import for Leaflet to avoid SSR issues
|
|
const CollectionMap = dynamic(() => import('@/components/CollectionMap'), {
|
|
ssr: false,
|
|
loading: () => (
|
|
<div className="h-full flex items-center justify-center bg-gray-100">
|
|
<div className="w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
),
|
|
});
|
|
|
|
export default function MapPage() {
|
|
const router = useRouter();
|
|
const { isLoggedIn, isLoading: authLoading } = useAuth();
|
|
const [collections, setCollections] = useState<Collection[]>([]);
|
|
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 loadCollections = useCallback(async (showLoading = true) => {
|
|
if (!isAuthenticated()) return;
|
|
if (showLoading) setIsLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
const data = await getAssignedCollections();
|
|
setCollections(data);
|
|
} catch (err) {
|
|
console.error('Failed to load collections:', err);
|
|
setError('Failed to load collections');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setIsRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadCollections();
|
|
}, [loadCollections]);
|
|
|
|
const handleNavigate = (lat: number, lng: number, name: string) => {
|
|
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
let url: string;
|
|
|
|
if (isIOS) {
|
|
url = `maps://maps.apple.com/?q=${encodeURIComponent(name)}&ll=${lat},${lng}`;
|
|
} else {
|
|
url = `https://www.google.com/maps?q=${lat},${lng}`;
|
|
}
|
|
|
|
window.open(url, '_blank');
|
|
};
|
|
|
|
if (authLoading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
|
<div className="w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isLoggedIn) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="h-screen flex flex-col bg-gray-50">
|
|
<LocationTracker />
|
|
|
|
{/* Header */}
|
|
<header className="bg-orange-600 text-white safe-top z-40 shadow-md flex-shrink-0">
|
|
<div className="px-4 py-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<MapPin size={20} />
|
|
<h1 className="text-lg font-bold">Collection Map</h1>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="bg-orange-700/50 px-2 py-1 rounded-full text-xs">
|
|
{collections.length} stops
|
|
</span>
|
|
<button
|
|
onClick={() => {
|
|
setIsRefreshing(true);
|
|
loadCollections(false);
|
|
}}
|
|
disabled={isRefreshing}
|
|
className="p-2 rounded-lg bg-orange-700/50 active:bg-orange-700 transition-colors"
|
|
>
|
|
<RefreshCw
|
|
size={18}
|
|
className={cn(isRefreshing && 'animate-spin')}
|
|
/>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Map */}
|
|
<main className="flex-1 relative">
|
|
{isLoading ? (
|
|
<div className="h-full flex items-center justify-center bg-gray-100">
|
|
<div className="w-8 h-8 border-2 border-orange-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
) : error ? (
|
|
<div className="h-full flex flex-col items-center justify-center bg-gray-100 p-4">
|
|
<AlertCircle size={48} className="text-red-400 mb-3" />
|
|
<p className="text-gray-600 text-sm mb-4 text-center">{error}</p>
|
|
<button
|
|
onClick={() => loadCollections()}
|
|
className="px-4 py-2 bg-orange-500 text-white rounded-lg text-sm font-medium"
|
|
>
|
|
Try Again
|
|
</button>
|
|
</div>
|
|
) : collections.length === 0 ? (
|
|
<div className="h-full flex flex-col items-center justify-center bg-gray-100 p-4">
|
|
<MapPin size={48} className="text-gray-300 mb-3" />
|
|
<p className="text-gray-600 font-medium">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-2 border-orange-500 border-t-transparent rounded-full animate-spin" />
|
|
</div>
|
|
}
|
|
>
|
|
<CollectionMap
|
|
collections={collections}
|
|
onNavigate={handleNavigate}
|
|
/>
|
|
</Suspense>
|
|
)}
|
|
</main>
|
|
|
|
<BottomNav />
|
|
</div>
|
|
);
|
|
}
|