/* 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}
25 ? 9 : radius > 10 ? 10 : 11}
height={420} markers={markers} radius={radius}/>
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 });