Spaces:
Sleeping
Sleeping
File size: 12,759 Bytes
0f8617c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | 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='© <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;
|