/* Me2You - Real Leaflet/OpenStreetMap integration */ /* global React, L, Icon, panelStyle, btnPrimary, btnGhost, btnSmall, PRODUCTS */ const { useState, useEffect, useRef } = React; /* South Africa cities for sample data */ const CITY_COORDS = { 'Soweto': [-26.2678, 27.8585], 'Pretoria': [-25.7479, 28.2293], 'Khayelitsha': [-34.0399, 18.6776], 'Sandton': [-26.1076, 28.0567], 'Mamelodi': [-25.7234, 28.3848], 'Durban': [-29.8587, 31.0218], 'Polokwane': [-23.9045, 29.4689], 'Cape Town': [-33.9249, 18.4241], 'Bloemfontein': [-29.0852, 26.1596], 'Johannesburg': [-26.2041, 28.0473], 'Germiston': [-26.2179, 28.1665], 'East London': [-33.0153, 27.9116], 'Stellenbosch': [-33.9322, 18.8602], 'Centurion': [-25.8601, 28.1881], 'Port Elizabeth': [-33.9608, 25.6022], 'Alexandra': [-26.1015, 28.0937], 'Rustenburg': [-25.6672, 27.2424], 'Nelspruit': [-25.4753, 30.9694], 'Tembisa': [-25.9961, 28.2272], 'Midrand': [-25.9893, 28.1263], 'Roodepoort': [-26.1625, 27.8725], 'Kempton Park': [-26.1015, 28.2293], }; /* Pickup-point partners across SA */ const PICKUP_POINTS = [ { name:'PEP Maponya Mall', partner:'PEP', lat:-26.2734, lng:27.8941 }, { name:'Pargo Menlyn Park', partner:'Pargo',lat:-25.7838, lng:28.2774 }, { name:'Paxi Sandton City', partner:'Paxi', lat:-26.1085, lng:28.0568 }, { name:'PEP Mitchells Plain', partner:'PEP', lat:-34.0466, lng:18.6172 }, { name:'Pargo Gateway Theatre', partner:'Pargo',lat:-29.7239, lng:31.0689 }, { name:'Paxi Cresta Mall', partner:'Paxi', lat:-26.1334, lng:27.9744 }, ]; /* --- LeafletMap - generic wrapper --- */ function LeafletMap({ center, zoom = 12, height = 400, markers = [], radius, routeFrom, routeTo, onMarkerClick }) { const mapRef = useRef(null); const mapInstanceRef = useRef(null); const layersRef = useRef([]); useEffect(() => { if (!mapRef.current || mapInstanceRef.current) return; const map = L.map(mapRef.current, { zoomControl: true, scrollWheelZoom: false }).setView(center, zoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '(c) OpenStreetMap', maxZoom: 19 }).addTo(map); mapInstanceRef.current = map; return () => { map.remove(); mapInstanceRef.current = null; }; }, []); useEffect(() => { const map = mapInstanceRef.current; if (!map) return; map.setView(center, zoom); }, [center, zoom]); // Update markers, radius, route on changes useEffect(() => { const map = mapInstanceRef.current; if (!map) return; layersRef.current.forEach(l => map.removeLayer(l)); layersRef.current = []; // Radius circle if (radius) { const circle = L.circle(center, { radius: radius * 1000, color:'#F89A1F', fillColor:'#F89A1F', fillOpacity: 0.12, weight: 2, dashArray:'6, 6' }).addTo(map); layersRef.current.push(circle); } // Markers with custom orange icons markers.forEach(m => { const color = m.color || '#F89A1F'; const html = `
${m.label || ''}
`; const icon = L.divIcon({ html, className:'', iconSize:[24,24], iconAnchor:[12,24] }); const marker = L.marker([m.lat, m.lng], { icon }).addTo(map); if (m.popup) marker.bindPopup(m.popup); if (onMarkerClick) marker.on('click', () => onMarkerClick(m)); layersRef.current.push(marker); }); // Route polyline if (routeFrom && routeTo) { const line = L.polyline([routeFrom, routeTo], { color:'#F89A1F', weight:4, opacity:0.7, dashArray:'8, 8' }).addTo(map); layersRef.current.push(line); // Auto-fit bounds map.fitBounds(line.getBounds(), { padding:[40,40] }); } }, [markers, radius, center, routeFrom, routeTo, onMarkerClick]); return (
); } /* --- MapSearch - browse with radius --- */ function MapSearch({ onSelect }) { const [center, setCenter] = useState([-26.2041, 28.0473]); // Joburg default const [radius, setRadius] = useState(15); const [selectedCity, setSelectedCity] = useState('Johannesburg'); // Filter listings within radius const haversine = (a, b) => { const R = 6371; const dLat = (b[0]-a[0]) * Math.PI / 180; const dLng = (b[1]-a[1]) * Math.PI / 180; const lat1 = a[0] * Math.PI / 180, lat2 = b[0] * Math.PI / 180; const x = Math.sin(dLat/2)**2 + Math.cos(lat1)*Math.cos(lat2)*Math.sin(dLng/2)**2; return 2 * R * Math.asin(Math.sqrt(x)); }; const nearbyListings = PRODUCTS .map(p => ({ ...p, coords: CITY_COORDS[p.location] })) .filter(p => p.coords && haversine(center, p.coords) <= radius); const markers = [ ...nearbyListings.map((p, i) => ({ lat: p.coords[0] + (Math.random()-0.5)*0.02, // jitter so they don't overlap lng: p.coords[1] + (Math.random()-0.5)*0.02, label: 'R', color: '#F89A1F', popup: `${p.title}
R ${p.price.toLocaleString('en-ZA')} - ${p.location}
${p.seller}`, })), ...PICKUP_POINTS .filter(pt => haversine(center, [pt.lat, pt.lng]) <= radius * 1.5) .map(pt => ({ lat: pt.lat, lng: pt.lng, label:'P', color:'#2D7AC7', popup: `${pt.name}
Pickup partner: ${pt.partner}`, })), ]; return (

Browse on map

{nearbyListings.length} listings within {radius} km of {selectedCity}

setRadius(+e.target.value)} style={{accentColor:'var(--brand-orange)',width:140}}/> {radius} km
25 ? 9 : radius > 10 ? 10 : 11} height={420} markers={markers} radius={radius}/>
Listings
Pickup points (PEP / Pargo / Paxi)
{radius} km search radius
); } /* --- DriverLiveMap - live tracking for orders --- */ function DriverLiveMap({ pickup, dropoff, driver, height = 220 }) { const [driverPos, setDriverPos] = useState(pickup); const stepRef = useRef(0); // Animate driver position from pickup -> dropoff useEffect(() => { if (!pickup || !dropoff) return; const id = setInterval(() => { stepRef.current = (stepRef.current + 0.02) % 1; const t = stepRef.current; setDriverPos([ pickup[0] + (dropoff[0] - pickup[0]) * t, pickup[1] + (dropoff[1] - pickup[1]) * t, ]); }, 1500); return () => clearInterval(id); }, [pickup, dropoff]); const markers = [ { lat: pickup[0], lng: pickup[1], label:'A', color:'#1F1A17', popup:'Pickup' }, { lat: dropoff[0], lng: dropoff[1], label:'B', color:'#2F9E5A', popup:'Delivery' }, { lat: driverPos[0], lng: driverPos[1], label:'D', color:'#F89A1F', popup:`Driver: ${driver || 'Bongani K.'}` }, ]; return ( ); } Object.assign(window, { LeafletMap, MapSearch, DriverLiveMap, CITY_COORDS, PICKUP_POINTS });