2026-01-21 17:34:48 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState, useRef } from 'react';
|
|
|
|
|
import dynamic from 'next/dynamic';
|
|
|
|
|
|
2026-01-22 19:25:25 +01:00
|
|
|
// Import conditionnel de Leaflet uniquement côté client
|
|
|
|
|
let L: any;
|
2026-01-21 17:34:48 +01:00
|
|
|
if (typeof window !== 'undefined') {
|
2026-01-22 19:25:25 +01:00
|
|
|
L = require('leaflet');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fix pour les icônes Leaflet avec Next.js
|
|
|
|
|
if (typeof window !== 'undefined' && L) {
|
2026-01-21 17:34:48 +01:00
|
|
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
|
|
|
|
L.Icon.Default.mergeOptions({
|
|
|
|
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png',
|
|
|
|
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png',
|
|
|
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png',
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Import dynamique pour éviter les problèmes SSR avec Leaflet
|
|
|
|
|
const MapContainer = dynamic(() => import('react-leaflet').then((mod) => mod.MapContainer), { ssr: false });
|
|
|
|
|
const TileLayer = dynamic(() => import('react-leaflet').then((mod) => mod.TileLayer), { ssr: false });
|
|
|
|
|
const Marker = dynamic(() => import('react-leaflet').then((mod) => mod.Marker), { ssr: false });
|
|
|
|
|
const Popup = dynamic(() => import('react-leaflet').then((mod) => mod.Popup), { ssr: false });
|
|
|
|
|
const Polyline = dynamic(() => import('react-leaflet').then((mod) => mod.Polyline), { ssr: false });
|
|
|
|
|
|
|
|
|
|
interface TrajetMapProps {
|
|
|
|
|
adresseDepart: string;
|
|
|
|
|
adresseArrivee: string;
|
|
|
|
|
adherentNom?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Coordinates {
|
|
|
|
|
lat: number;
|
|
|
|
|
lng: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RouteInfo {
|
|
|
|
|
distance: number; // en mètres
|
|
|
|
|
duration: number; // en secondes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function TrajetMap({ adresseDepart, adresseArrivee, adherentNom }: TrajetMapProps) {
|
|
|
|
|
const [departCoords, setDepartCoords] = useState<Coordinates | null>(null);
|
|
|
|
|
const [arriveeCoords, setArriveeCoords] = useState<Coordinates | null>(null);
|
|
|
|
|
const [routeInfo, setRouteInfo] = useState<RouteInfo | null>(null);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
2026-01-22 19:25:25 +01:00
|
|
|
const [mounted, setMounted] = useState(false);
|
2026-01-21 17:34:48 +01:00
|
|
|
// Cache simple pour éviter de regéocoder les mêmes adresses
|
|
|
|
|
const geocodeCacheRef = useRef<Map<string, Coordinates>>(new Map());
|
|
|
|
|
|
2026-01-22 19:25:25 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
setMounted(true);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-21 17:34:48 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
// Réinitialiser les coordonnées quand les adresses changent
|
|
|
|
|
setDepartCoords(null);
|
|
|
|
|
setArriveeCoords(null);
|
|
|
|
|
setRouteInfo(null);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
if (adresseDepart && adresseArrivee && adresseDepart.length >= 5 && adresseArrivee.length >= 5) {
|
|
|
|
|
// Délai réduit pour une réponse plus rapide
|
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
|
geocodeAddresses();
|
|
|
|
|
}, 400);
|
|
|
|
|
|
|
|
|
|
return () => clearTimeout(timeoutId);
|
|
|
|
|
}
|
|
|
|
|
}, [adresseDepart, adresseArrivee]);
|
|
|
|
|
|
|
|
|
|
const geocodeAddresses = async () => {
|
|
|
|
|
if (!adresseDepart || !adresseArrivee) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier que les adresses ont au moins 5 caractères
|
|
|
|
|
if (adresseDepart.length < 5 || adresseArrivee.length < 5) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Géocoder les deux adresses en parallèle pour plus de rapidité
|
|
|
|
|
// (l'API Nominatim permet généralement 1 req/sec, mais on peut essayer)
|
|
|
|
|
const [departResult, arriveeResult] = await Promise.all([
|
|
|
|
|
geocodeAddress(adresseDepart).catch(() => null),
|
|
|
|
|
// Petit délai pour la deuxième requête pour respecter les limites
|
|
|
|
|
new Promise(resolve => setTimeout(resolve, 1000)).then(() =>
|
|
|
|
|
geocodeAddress(adresseArrivee).catch(() => null)
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (departResult && arriveeResult) {
|
|
|
|
|
setDepartCoords(departResult);
|
|
|
|
|
setArriveeCoords(arriveeResult);
|
|
|
|
|
|
|
|
|
|
// Calculer la distance et le temps estimé
|
|
|
|
|
const info = calculateRouteInfo(departResult, arriveeResult);
|
|
|
|
|
setRouteInfo(info);
|
|
|
|
|
} else if (!departResult) {
|
|
|
|
|
setError(`Impossible de trouver l'adresse de départ: "${adresseDepart}"`);
|
|
|
|
|
} else {
|
|
|
|
|
setError(`Impossible de trouver l'adresse d'arrivée: "${adresseArrivee}"`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Erreur lors du géocodage:', err);
|
|
|
|
|
setError('Erreur lors du chargement de la carte. Veuillez réessayer.');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const geocodeAddress = async (address: string): Promise<Coordinates | null> => {
|
|
|
|
|
if (!address || address.length < 5) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Nettoyer l'adresse pour améliorer les résultats
|
|
|
|
|
const cleanAddress = address.trim().toLowerCase();
|
|
|
|
|
|
|
|
|
|
// Vérifier le cache
|
|
|
|
|
if (geocodeCacheRef.current.has(cleanAddress)) {
|
|
|
|
|
return geocodeCacheRef.current.get(cleanAddress)!;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch(
|
|
|
|
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address.trim())}&limit=1&addressdetails=1&countrycodes=fr`,
|
|
|
|
|
{
|
|
|
|
|
headers: {
|
|
|
|
|
'User-Agent': 'MAD Platform',
|
|
|
|
|
'Accept-Language': 'fr-FR,fr;q=0.9',
|
|
|
|
|
'Referer': window.location.origin,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data && data.length > 0 && data[0].lat && data[0].lon) {
|
|
|
|
|
const lat = parseFloat(data[0].lat);
|
|
|
|
|
const lng = parseFloat(data[0].lon);
|
|
|
|
|
|
|
|
|
|
// Vérifier que les coordonnées sont valides
|
|
|
|
|
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
|
|
|
|
|
const coords = { lat, lng };
|
|
|
|
|
// Mettre en cache (limiter à 50 entrées pour éviter la surconsommation mémoire)
|
|
|
|
|
if (geocodeCacheRef.current.size < 50) {
|
|
|
|
|
geocodeCacheRef.current.set(cleanAddress, coords);
|
|
|
|
|
}
|
|
|
|
|
return coords;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.warn('Réponse non OK de Nominatim:', response.status);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Erreur de géocodage:', error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const calculateRouteInfo = (depart: Coordinates, arrivee: Coordinates): RouteInfo => {
|
|
|
|
|
// Calcul de la distance à vol d'oiseau (formule de Haversine)
|
|
|
|
|
const R = 6371e3; // Rayon de la Terre en mètres
|
|
|
|
|
const φ1 = (depart.lat * Math.PI) / 180;
|
|
|
|
|
const φ2 = (arrivee.lat * Math.PI) / 180;
|
|
|
|
|
const Δφ = ((arrivee.lat - depart.lat) * Math.PI) / 180;
|
|
|
|
|
const Δλ = ((arrivee.lng - depart.lng) * Math.PI) / 180;
|
|
|
|
|
|
|
|
|
|
const a =
|
|
|
|
|
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
|
|
|
|
|
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
|
|
|
|
|
|
const distance = R * c; // Distance en mètres
|
|
|
|
|
|
|
|
|
|
// Estimation du temps : vitesse moyenne de 50 km/h en ville
|
|
|
|
|
// On multiplie la distance par 1.3 pour tenir compte des détours
|
|
|
|
|
const distanceWithDetour = distance * 1.3;
|
|
|
|
|
const vitesseMoyenne = 50; // km/h
|
|
|
|
|
const duration = (distanceWithDetour / 1000 / vitesseMoyenne) * 3600; // en secondes
|
|
|
|
|
|
|
|
|
|
return { distance: distanceWithDetour, duration };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatDistance = (meters: number): string => {
|
|
|
|
|
if (meters < 1000) {
|
|
|
|
|
return `${Math.round(meters)} m`;
|
|
|
|
|
}
|
|
|
|
|
return `${(meters / 1000).toFixed(1)} km`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatDuration = (seconds: number): string => {
|
|
|
|
|
const hours = Math.floor(seconds / 3600);
|
|
|
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
|
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
return `${hours}h${minutes > 0 ? minutes : ''}`;
|
|
|
|
|
}
|
|
|
|
|
return `${minutes} min`;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-22 19:25:25 +01:00
|
|
|
// Ne pas rendre la carte côté serveur
|
|
|
|
|
if (typeof window === 'undefined' || !mounted) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full flex items-center justify-center text-gray-400">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-sm">Chargement de la carte...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 17:34:48 +01:00
|
|
|
if (!adresseDepart || !adresseArrivee) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full flex items-center justify-center text-gray-400">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-sm">Remplissez les adresses pour voir la carte</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full flex items-center justify-center bg-white rounded-xl">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<svg className="animate-spin h-12 w-12 text-lblue mx-auto mb-4" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
|
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-sm font-medium text-gray-700 mb-1">Chargement de la carte...</p>
|
|
|
|
|
<p className="text-xs text-gray-500">Géocodage des adresses en cours</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full flex items-center justify-center text-gray-400 bg-white rounded-xl p-8">
|
|
|
|
|
<div className="text-center max-w-md">
|
|
|
|
|
<svg className="w-20 h-20 mx-auto mb-4 opacity-50 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-sm font-medium text-gray-700 mb-2">{error}</p>
|
|
|
|
|
<p className="text-xs text-gray-500 mb-4">Vérifiez que les adresses sont correctes et complètes</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={geocodeAddresses}
|
|
|
|
|
className="px-4 py-2 text-xs font-medium text-white bg-lblue rounded-lg hover:bg-dblue transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Réessayer
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!departCoords || !arriveeCoords) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full flex items-center justify-center text-gray-400">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<svg className="w-16 h-16 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
|
|
|
|
</svg>
|
|
|
|
|
<p className="text-sm">Chargement de la carte...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculer le centre et le zoom optimal pour afficher les deux points
|
|
|
|
|
const centerLat = departCoords && arriveeCoords ? (departCoords.lat + arriveeCoords.lat) / 2 : 48.8566;
|
|
|
|
|
const centerLng = departCoords && arriveeCoords ? (departCoords.lng + arriveeCoords.lng) / 2 : 2.3522;
|
|
|
|
|
|
|
|
|
|
// Calculer le zoom optimal pour voir les deux points
|
|
|
|
|
let optimalZoom = 12;
|
|
|
|
|
if (departCoords && arriveeCoords) {
|
|
|
|
|
const latDiff = Math.abs(departCoords.lat - arriveeCoords.lat);
|
|
|
|
|
const lngDiff = Math.abs(departCoords.lng - arriveeCoords.lng);
|
|
|
|
|
const maxDiff = Math.max(latDiff, lngDiff);
|
|
|
|
|
|
|
|
|
|
if (maxDiff > 0.5) optimalZoom = 7;
|
|
|
|
|
else if (maxDiff > 0.2) optimalZoom = 8;
|
|
|
|
|
else if (maxDiff > 0.1) optimalZoom = 9;
|
|
|
|
|
else if (maxDiff > 0.05) optimalZoom = 10;
|
|
|
|
|
else if (maxDiff > 0.02) optimalZoom = 11;
|
|
|
|
|
else optimalZoom = 12;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="h-full flex flex-col min-h-[500px]">
|
|
|
|
|
{/* Informations du trajet */}
|
|
|
|
|
{routeInfo && (
|
|
|
|
|
<div className="bg-white rounded-lg border border-gray-200 p-4 mb-4 shadow-sm">
|
|
|
|
|
<div className="grid grid-cols-2 gap-6">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="w-10 h-10 rounded-lg bg-lgreen/10 flex items-center justify-center">
|
|
|
|
|
<svg className="w-5 h-5 text-lgreen" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Distance</div>
|
|
|
|
|
<div className="text-xl font-semibold text-gray-900">{formatDistance(routeInfo.distance)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-3 border-l border-gray-200 pl-6">
|
|
|
|
|
<div className="w-10 h-10 rounded-lg bg-lblue/10 flex items-center justify-center">
|
|
|
|
|
<svg className="w-5 h-5 text-lblue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Temps estimé</div>
|
|
|
|
|
<div className="text-xl font-semibold text-gray-900">{formatDuration(routeInfo.duration)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Carte */}
|
|
|
|
|
<div className="flex-1 rounded-xl overflow-hidden border-2 border-gray-300 shadow-2xl" style={{ minHeight: '500px' }}>
|
|
|
|
|
<MapContainer
|
|
|
|
|
center={[centerLat, centerLng]}
|
|
|
|
|
zoom={optimalZoom}
|
|
|
|
|
style={{ height: '100%', width: '100%', zIndex: 0 }}
|
|
|
|
|
zoomControl={true}
|
|
|
|
|
scrollWheelZoom={true}
|
|
|
|
|
>
|
|
|
|
|
<TileLayer
|
|
|
|
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
|
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
|
|
|
/>
|
|
|
|
|
<Marker
|
|
|
|
|
position={[departCoords.lat, departCoords.lng]}
|
|
|
|
|
icon={L.divIcon({
|
|
|
|
|
className: 'custom-marker-depart',
|
|
|
|
|
html: `
|
|
|
|
|
<div style="
|
|
|
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border-radius: 50% 50% 50% 0;
|
|
|
|
|
transform: rotate(-45deg);
|
|
|
|
|
border: 4px solid white;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
">
|
|
|
|
|
<div style="
|
|
|
|
|
transform: rotate(45deg);
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
">A</div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
iconSize: [40, 40],
|
|
|
|
|
iconAnchor: [20, 40],
|
|
|
|
|
popupAnchor: [0, -40],
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<Popup className="custom-popup">
|
|
|
|
|
<div className="text-sm p-2">
|
|
|
|
|
<div className="font-bold text-lgreen mb-1 flex items-center gap-2">
|
|
|
|
|
<div className="w-3 h-3 rounded-full bg-lgreen"></div>
|
|
|
|
|
Départ
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-gray-700 font-medium">{adresseDepart}</div>
|
|
|
|
|
{adherentNom && <div className="text-gray-500 mt-1 text-xs">{adherentNom}</div>}
|
|
|
|
|
</div>
|
|
|
|
|
</Popup>
|
|
|
|
|
</Marker>
|
|
|
|
|
<Marker
|
|
|
|
|
position={[arriveeCoords.lat, arriveeCoords.lng]}
|
|
|
|
|
icon={L.divIcon({
|
|
|
|
|
className: 'custom-marker-arrivee',
|
|
|
|
|
html: `
|
|
|
|
|
<div style="
|
|
|
|
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
border-radius: 50% 50% 50% 0;
|
|
|
|
|
transform: rotate(-45deg);
|
|
|
|
|
border: 4px solid white;
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
">
|
|
|
|
|
<div style="
|
|
|
|
|
transform: rotate(45deg);
|
|
|
|
|
color: white;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
">B</div>
|
|
|
|
|
</div>
|
|
|
|
|
`,
|
|
|
|
|
iconSize: [40, 40],
|
|
|
|
|
iconAnchor: [20, 40],
|
|
|
|
|
popupAnchor: [0, -40],
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<Popup className="custom-popup">
|
|
|
|
|
<div className="text-sm p-2">
|
|
|
|
|
<div className="font-bold text-lblue mb-1 flex items-center gap-2">
|
|
|
|
|
<div className="w-3 h-3 rounded-full bg-lblue"></div>
|
|
|
|
|
Arrivée
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-gray-700 font-medium">{adresseArrivee}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Popup>
|
|
|
|
|
</Marker>
|
|
|
|
|
<Polyline
|
|
|
|
|
positions={[
|
|
|
|
|
[departCoords.lat, departCoords.lng],
|
|
|
|
|
[arriveeCoords.lat, arriveeCoords.lng],
|
|
|
|
|
]}
|
|
|
|
|
pathOptions={{
|
|
|
|
|
color: '#6B46C1',
|
|
|
|
|
weight: 5,
|
|
|
|
|
opacity: 0.8,
|
|
|
|
|
dashArray: '10, 10',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</MapContainer>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|