Snaplocal / client /src /pages /ExploreMap.jsx
Kuruva Laxmi
SnapLocal MVP - Complete platform with 4 MVP features
0f8617c
Raw
History Blame Contribute Delete
12.8 kB
import { useState, useEffect } from 'react';
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import api from '../api/api';
import { useAuth } from '../context/AuthContext';
import { Star, MapPin, Navigation, Crosshair, Filter, Loader2, Search as SearchIcon } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
// Fix for default marker icons in Leaflet
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
});
const RecenterMap = ({ coords }) => {
const map = useMap();
useEffect(() => {
if (coords) {
map.setView(coords, 13);
}
}, [coords, map]);
return null;
};
const ExploreMap = () => {
const [photographers, setPhotographers] = useState([]);
const [userLocation, setUserLocation] = useState(null);
const [loading, setLoading] = useState(true);
const [isFilterOpen, setIsFilterOpen] = useState(false);
const [filters, setFilters] = useState({
specialty: '',
price: 'all',
availableNow: false
});
const navigate = useNavigate();
useEffect(() => {
// Get user's current location
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const coords = [position.coords.latitude, position.coords.longitude];
setUserLocation(coords);
fetchNearby(coords[0], coords[1]);
},
(err) => {
console.error('Location error:', err);
// Default to some city center if blocked (e.g. Hyderabad)
const defaultCoords = [17.3850, 78.4867];
setUserLocation(defaultCoords);
fetchNearby(defaultCoords[0], defaultCoords[1]);
}
);
}
}, []);
const fetchNearby = async (lat, lng) => {
try {
setLoading(true);
const response = await api.get(`/users/nearby?lat=${lat}&lng=${lng}&radius=10000`);
setPhotographers(response.data);
setLoading(false);
} catch (error) {
console.error('Failed to fetch nearby photographers:', error);
setLoading(false);
}
};
const filteredPhotographers = photographers.filter(pg => {
if (filters.specialty && !pg.specialty?.toLowerCase().includes(filters.specialty.toLowerCase())) return false;
if (filters.price !== 'all') {
const price = pg.price || 0;
if (filters.price === 'budget' && price > 50) return false;
if (filters.price === 'mid' && (price < 50 || price > 150)) return false;
if (filters.price === 'pro' && price < 150) return false;
}
return true;
});
return (
<div className="h-[calc(100vh-100px)] -mt-8 flex flex-col relative overflow-hidden bg-gray-50">
{/* Control Bar */}
<div className="absolute top-6 left-6 right-6 z-[1000] flex items-center space-x-4">
<div className="flex-1 max-w-lg bg-white/90 backdrop-blur-xl rounded-[2rem] shadow-2xl border border-white/20 p-2 flex items-center">
<div className="pl-4 pr-3 text-blue-600">
<SearchIcon size={20} />
</div>
<input
type="text"
placeholder="Search for a city or specialty..."
className="flex-1 bg-transparent border-none outline-none font-bold text-sm text-gray-800 placeholder:text-gray-400"
/>
<button
onClick={() => setIsFilterOpen(!isFilterOpen)}
className={`p-3 rounded-2xl transition-all ${isFilterOpen ? 'bg-blue-600 text-white shadow-lg' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'}`}
>
<Filter size={18} />
</button>
</div>
<div className="bg-white/90 backdrop-blur-xl px-6 py-3 rounded-2xl shadow-xl border border-white/20 flex items-center space-x-2">
<Crosshair size={18} className="text-blue-600" />
<span className="text-sm font-black text-gray-800 uppercase tracking-wider">Nearby Pros: {filteredPhotographers.length}</span>
</div>
</div>
{/* Filter Drawer */}
{isFilterOpen && (
<div className="absolute top-24 left-6 z-[1000] w-72 bg-white rounded-3xl shadow-2xl border border-gray-100 p-6 animate-in slide-in-from-top-4 duration-300">
<h4 className="text-xs font-black text-gray-400 uppercase tracking-widest mb-4">Refine Discovery</h4>
<div className="space-y-6">
<div className="space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase">Specialty</label>
<input
type="text"
value={filters.specialty}
onChange={(e) => setFilters({ ...filters, specialty: e.target.value })}
className="w-full p-3 bg-gray-50 rounded-xl border border-gray-100 text-sm font-bold"
placeholder="Wedding, Portrait..."
/>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black text-gray-400 uppercase">Price Range</label>
<div className="grid grid-cols-2 gap-2">
{['all', 'budget', 'mid', 'pro'].map(p => (
<button
key={p}
onClick={() => setFilters({ ...filters, price: p })}
className={`py-2 rounded-xl text-[10px] font-black uppercase tracking-wider border transition-all ${filters.price === p ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-500 border-gray-100 hover:border-blue-200'}`}
>
{p}
</button>
))}
</div>
</div>
<label className="flex items-center space-x-3 cursor-pointer group">
<input
type="checkbox"
checked={filters.availableNow}
onChange={(e) => setFilters({ ...filters, availableNow: e.target.checked })}
className="hidden"
/>
<div className={`w-10 h-6 rounded-full p-1 transition-colors ${filters.availableNow ? 'bg-green-500' : 'bg-gray-200'}`}>
<div className={`bg-white w-4 h-4 rounded-full transition-transform shadow-sm ${filters.availableNow ? 'translate-x-4' : ''}`}></div>
</div>
<span className="text-xs font-bold text-gray-700">Available Now</span>
</label>
</div>
</div>
)}
{/* Map Component */}
<div className="flex-1 relative">
{userLocation ? (
<MapContainer
center={userLocation}
zoom={13}
className="h-full w-full grayscale-[0.2] contrast-[1.1]"
zoomControl={false}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<RecenterMap coords={userLocation} />
{/* User Marker */}
<Marker position={userLocation} icon={new L.DivIcon({
className: 'user-location-marker',
html: `<div class="w-4 h-4 bg-blue-600 rounded-full border-2 border-white shadow-xl animate-pulse"></div>`
})}>
<Popup>You are here</Popup>
</Marker>
{/* Photographer Markers */}
{filteredPhotographers.map(pg => (
<Marker
key={pg._id}
position={[pg.location.coordinates[1], pg.location.coordinates[0]]}
icon={new L.DivIcon({
className: 'photographer-marker',
html: `<div class="bg-white p-1 rounded-xl shadow-2xl border border-gray-100 hover:scale-110 transition-transform"><img src="${pg.profilePicture}" class="w-10 h-10 rounded-lg object-cover" /></div>`
})}
>
<Popup className="custom-popup">
<div className="p-4 min-w-[200px] text-left">
<div className="flex items-center space-x-3 mb-4">
<img src={pg.profilePicture} className="w-12 h-12 rounded-2xl object-cover shadow-lg" alt="" />
<div>
<h4 className="font-black text-gray-900 leading-tight">{pg.firstName}</h4>
<p className="text-[10px] text-blue-600 font-bold uppercase tracking-widest">{pg.specialty}</p>
</div>
</div>
<div className="flex justify-between items-center mb-4 text-xs font-black">
<div className="flex items-center text-amber-500">
<Star size={12} className="fill-current mr-1" />
<span>{pg.rating?.toFixed(1) || 4.9}</span>
</div>
<div className="text-gray-900">${pg.price}/hr</div>
</div>
<button
onClick={() => navigate(`/photographer/${pg._id}`)}
className="w-full bg-gray-900 text-white py-3 rounded-xl font-black text-[10px] uppercase tracking-widest hover:bg-gray-800 transition shadow-xl"
>
View Portfolio
</button>
</div>
</Popup>
</Marker>
))}
</MapContainer>
) : (
<div className="flex flex-col items-center justify-center h-full space-y-4">
<Loader2 className="animate-spin text-blue-600" size={48} />
<p className="text-gray-500 font-bold">Initializing SnapLocal Pro Map...</p>
</div>
)}
</div>
<style>{`
.leaflet-popup-content-wrapper {
padding: 0 !important;
border-radius: 2rem !important;
overflow: hidden !important;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.1) !important;
}
.leaflet-popup-content {
margin: 0 !important;
}
.leaflet-popup-tip-container {
display: none !important;
}
`}</style>
</div>
);
};
export default ExploreMap;