krishnasales_mobile/src/components/maps/NearestStoresMap.tsx
2026-08-06 16:33:57 +05:30

253 lines
8.6 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { useJsApiLoader, GoogleMap, InfoWindow } from '@react-google-maps/api';
const libraries: ("marker")[] = ["marker"];
import { CustomAdvancedMarker } from './CustomAdvancedMarker';
import { PIPELINE, BASE_URL } from '../../api/config';
import { Loader2, MapPin, ChevronDown, ChevronUp } from 'lucide-react';
// Interfaces for the API response
interface StoreLocation {
latitude: number;
longitude: number;
}
interface Store {
area: string;
business_name: string;
distance_km: number;
location: StoreLocation;
route_name: string;
store_code: string;
}
interface NearestStoresResponse {
message: string;
response: {
count: number;
stores: Store[];
success: boolean;
};
}
const mapContainerStyle = {
width: '100%',
height: '100%',
minHeight: '400px',
borderRadius: '16px'
};
const defaultCenter = { lat: 12.9716, lng: 77.5946 };
export function NearestStoresMap() {
const [userLocation, setUserLocation] = useState<{ lat: number, lng: number } | null>(null);
const [stores, setStores] = useState<Store[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedStore, setSelectedStore] = useState<Store | null>(null);
const [isListExpanded, setIsListExpanded] = useState(false);
const isLocalhost = typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
const { isLoaded } = useJsApiLoader({
id: 'google-map-script',
googleMapsApiKey: isLocalhost ? '' : (import.meta.env.VITE_GOOGLE_MAPS_API_KEY || ''),
libraries
});
const fetchNearestStores = async (lat: number, lng: number) => {
setLoading(true);
setError(null);
try {
const response = await fetch(`${BASE_URL}${PIPELINE.endpoints.nearestStores}`, {
method: 'POST',
headers: {
'accept': 'application/json, text/plain, */*',
'content-type': 'application/json',
'groupid': '25',
'orgid': '57',
'templateid': '189',
'x-pipeline-version': 'latest'
},
body: JSON.stringify({
latitude: lat,
longitude: lng
})
});
if (!response.ok) {
throw new Error(`Failed to fetch stores: ${response.statusText}`);
}
const data = await response.json() as NearestStoresResponse;
if (data.response && data.response.stores) {
setStores(data.response.stores);
}
} catch (err: any) {
setError(err.message || 'An error occurred while fetching stores.');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!navigator.geolocation) {
setError('Geolocation is not supported by your browser.');
return;
}
setLoading(true);
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
setUserLocation({ lat, lng });
fetchNearestStores(lat, lng);
},
(err) => {
setError(err.message || 'Failed to get location');
setLoading(false);
},
{ enableHighAccuracy: true }
);
}, []);
const onLoad = useCallback(function callback(map: google.maps.Map) {
if (userLocation) {
const bounds = new window.google.maps.LatLngBounds();
bounds.extend(userLocation);
stores.forEach(store => {
if (store.location) {
bounds.extend({ lat: store.location.latitude, lng: store.location.longitude });
}
});
if (stores.length > 0) {
map.fitBounds(bounds);
}
}
}, [userLocation, stores]);
if (!isLoaded) {
return (
<div className="w-full h-[400px] flex items-center justify-center bg-slate-50 rounded-xl border border-slate-200">
<Loader2 className="w-8 h-8 text-primary animate-spin" />
</div>
);
}
return (
<div className="w-full flex flex-col bg-transparent">
{error && (
<div className="bg-red-50 text-red-600 p-3 m-4 rounded-lg text-sm border border-red-100">
{error}
</div>
)}
{loading && !userLocation && (
<div className="text-sm text-slate-500 flex items-center gap-2 p-4">
<Loader2 className="w-4 h-4 animate-spin" />
Locating you...
</div>
)}
<div className="w-full h-[50vh] min-h-[350px] relative pt-4">
<GoogleMap
mapContainerStyle={mapContainerStyle}
center={userLocation || defaultCenter}
zoom={userLocation ? 13 : 5}
onLoad={onLoad}
options={{
streetViewControl: false,
mapTypeControl: false,
fullscreenControl: false,
mapId: "DEMO_MAP_ID"
}}
>
{/* User's Current Location Marker */}
{userLocation && (
<CustomAdvancedMarker
position={userLocation}
iconUrl="https://maps.google.com/mapfiles/ms/icons/blue-dot.png"
title="You are here"
/>
)}
{/* Stores Markers */}
{stores.map((store) => (
store.location && (
<CustomAdvancedMarker
key={store.store_code}
position={{ lat: store.location.latitude, lng: store.location.longitude }}
onClick={() => setSelectedStore(store)}
title={store.business_name}
/>
)
))}
{/* Info Window for Selected Store */}
{selectedStore && selectedStore.location && (
<InfoWindow
position={{ lat: selectedStore.location.latitude, lng: selectedStore.location.longitude }}
onCloseClick={() => setSelectedStore(null)}
>
<div className="p-1 max-w-[200px]">
<h3 className="font-semibold text-sm text-slate-800">{selectedStore.business_name}</h3>
<p className="text-xs text-slate-500 mt-1">{selectedStore.area}</p>
<div className="flex justify-between items-center mt-2 pt-2 border-t border-slate-100">
<span className="text-[10px] bg-primary/10 text-primary px-1.5 py-0.5 rounded">
{selectedStore.route_name}
</span>
<span className="text-[10px] text-slate-400 font-medium">
{selectedStore.distance_km} km
</span>
</div>
</div>
</InfoWindow>
)}
</GoogleMap>
</div>
<div className="mt-4 mb-4 bg-card rounded-2xl shadow-[0_1px_0_0_rgba(0,0,0,0.02),0_20px_40px_-24px_rgba(20,80,60,0.15)] p-5 flex flex-col gap-4">
<div className="flex justify-between items-center cursor-pointer" onClick={() => setIsListExpanded(!isListExpanded)}>
<h3 className="font-bold text-[17px] text-strong flex items-center gap-2">
<MapPin className="w-5 h-5 text-primary" />
Nearest Stores ({stores.length})
</h3>
<button className="text-slate-400 hover:text-slate-600 transition-colors p-1">
{isListExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</button>
</div>
{isListExpanded && (
<>
{loading && stores.length === 0 && (
<div className="py-4 text-center text-sm text-slate-500">
Fetching nearby stores...
</div>
)}
{!loading && stores.length === 0 && userLocation && (
<div className="py-4 text-center text-sm text-slate-500">
No stores found nearby.
</div>
)}
<div className="flex flex-col gap-3">
{stores.map(store => (
<div key={store.store_code} className="z-card !p-3 hover:!border-primary/40 cursor-pointer" onClick={() => setSelectedStore(store)}>
<div className="flex justify-between items-start mb-1">
<span className="font-bold text-[17px] text-strong line-clamp-1">{store.business_name}</span>
</div>
<p className="text-sm text-muted line-clamp-1">{store.area}</p>
<div className="flex justify-between items-center mt-3 pt-3 border-t border-slate-200/60">
<span className="text-xs font-semibold text-primary uppercase tracking-wider">{store.route_name}</span>
<span className="text-xs font-bold text-strong">{store.distance_km} km</span>
</div>
</div>
))}
</div>
</>
)}
</div>
</div>
);
}