From d9fb1504c80f5c9fda75dbf852ef7528683b9684 Mon Sep 17 00:00:00 2001
From: Arezki Bazizi
Date: Sat, 15 Feb 2025 19:33:08 +0100
Subject: [PATCH 01/12] ajout les villes de passsage et de sauvegardement de
trajet
---
src/components/MapBoxMap.jsx | 908 ++++++++++++++++------------
src/components/sidebar/nav-main.jsx | 43 +-
2 files changed, 573 insertions(+), 378 deletions(-)
diff --git a/src/components/MapBoxMap.jsx b/src/components/MapBoxMap.jsx
index 2485822..1223a56 100644
--- a/src/components/MapBoxMap.jsx
+++ b/src/components/MapBoxMap.jsx
@@ -1,377 +1,531 @@
-"use client";
-
-import React, { useState, useRef, useEffect } from "react";
-import { AppSidebar } from "@/components/sidebar/app-sidebar";
-import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
-import MapDisplay from "@/components/sidebar/MapDisplay";
-import mapboxgl from "mapbox-gl";
-import { useTheme } from "@/hooks/theme-provider";
-import { useLocation } from "react-router-dom";
-
-mapboxgl.accessToken = "pk.eyJ1Ijoic3lsdmFpbmNvc3RlcyIsImEiOiJjbTNxZXNtN3cwa2hpMmpxdWd2cndhdnYwIn0.V2ZAp-BqZq6KIHQ6Lu8eAQ";
-
-const fetchCoordinatesFromCity = async (city) => {
- const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(city)}.json?access_token=${mapboxgl.accessToken}`;
- try {
- const response = await fetch(url);
- const data = await response.json();
- return data.features[0]?.center || null;
- } catch (error) {
- console.error(`Erreur lors de la récupération des coordonnées pour ${city}:`, error);
- return null;
- }
-};
-
-export default function Page() {
- const [startCoords, setStartCoords] = useState(null);
- const [endCoords, setEndCoords] = useState(null);
- const [transportMode, setTransportMode] = useState("driving");
- const [routeInstructions, setRouteInstructions] = useState([]);
- const [pois, setPois] = useState({ hotel: [], restaurant: [], gas_station: [], park: [] });
- const [parcoursData, setParcoursData] = useState(null); // État pour les données GeoJSON
-
- const { theme } = useTheme();
- const isDarkMode = theme === "dark";
- const markersRef = useRef([]); // Stocker les marqueurs pour nettoyage ultérieur
- const location = useLocation(); // Lire les paramètres de l'URL
- const mapRef = useRef(null);
- const mapContainerRef = useRef(null);
- const queryParams = new URLSearchParams(location.search);
- const startCity = queryParams.get("startCity");
- const endCity = queryParams.get("endCity");
-
- useEffect(() => {
- const initializeCoords = async () => {
- if (startCity) {
- const start = await fetchCoordinatesFromCity(startCity);
- setStartCoords(start);
- }
- if (endCity) {
- const end = await fetchCoordinatesFromCity(endCity);
- setEndCoords(end);
- }
- };
-
- initializeCoords();
- }, [startCity, endCity]);
-
-
- const fetchRoute = async () => {
- if (!startCoords || !endCoords) return;
-
- const url = `https://api.mapbox.com/directions/v5/mapbox/${transportMode}/${startCoords[0]},${startCoords[1]};${endCoords[0]},${endCoords[1]}?geometries=geojson&steps=true&access_token=${mapboxgl.accessToken}`;
- try {
- const response = await fetch(url);
- const data = await response.json();
-
- const route = data.routes[0]?.geometry;
- const steps = data.routes[0]?.legs[0]?.steps || [];
-
- if (route && mapRef.current) {
- if (mapRef.current.getLayer("route")) mapRef.current.removeLayer("route");
- if (mapRef.current.getSource("route")) mapRef.current.removeSource("route");
-
- mapRef.current.addSource("route", { type: "geojson", data: { type: "Feature", geometry: route } });
- mapRef.current.addLayer({
- id: "route",
- type: "line",
- source: "route",
- layout: { "line-join": "round", "line-cap": "round" },
- paint: { "line-color": "#007bff", "line-width": 5 },
- });
-
- mapRef.current.fitBounds([startCoords, endCoords], { padding: 50 });
-
- setRouteInstructions(
- steps.map((step) => ({
- instruction: step.maneuver.instruction,
- type: step.maneuver.type,
- modifier: step.maneuver.modifier,
- }))
- );
-
- fetchPois(route); // Rechercher les POI
- }
- } catch (error) {
- console.error("Erreur lors de la récupération de l’itinéraire :", error);
- }
- };
-
- const fetchPois = async (geometry) => {
- if (!geometry) return;
-
- const bbox = calculateBoundingBox(geometry.coordinates);
- const categories = ["hotel", "restaurant", "gas_station", "park"];
- const poisByCategory = { hotel: [], restaurant: [], gas_station: [], park: [] };
-
- clearMarkers(); // Nettoyer les anciens marqueurs
-
- for (const category of categories) {
- const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${category}.json?bbox=${bbox.join(",")}&access_token=${mapboxgl.accessToken}`;
- try {
- const response = await fetch(url);
- const data = await response.json();
-
- if (data.features) {
- const filteredPois = filterPoisByProximity(
- data.features.map((feature) => ({
- name: feature.text,
- coords: feature.geometry.coordinates,
- category,
- })),
- geometry.coordinates,
- 5 // Distance maximale en kilomètres
- );
- poisByCategory[category] = filteredPois;
- }
- } catch (error) {
- console.error(`Erreur lors de la récupération des POI pour ${category}:`, error);
- }
- }
-
- setPois(poisByCategory); // Mettre à jour les POI dans le state
- addMarkers(poisByCategory); // Ajouter les nouveaux marqueurs
- // addParcoursMarkers(); // Ajouter les marqueurs pour les parcours
- };
-
- const addParcoursMarkers = (parcoursData) => {
- if (!parcoursData || !parcoursData.features) return;
-
- const map = mapRef.current;
- const allParcours = [];
-
- // Préparation des données pour les clusters
- parcoursData.features.forEach((parcours) => {
- const { coordinates } = parcours.geometry;
- const { name, distance } = parcours.properties;
- const [lat, lon] = coordinates;
-
- allParcours.push({
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [lon, lat],
- },
- properties: {
- name,
- distance, // Garder la distance pour l'affichage
- },
- });
- });
-
- // Ajouter une source GeoJSON pour les clusters des parcours
- map.addSource('parcours', {
- type: 'geojson',
- data: {
- type: 'FeatureCollection',
- features: allParcours,
- },
- cluster: true,
- clusterMaxZoom: 14,
- clusterRadius: 50,
- });
-
- // Ajouter des couches pour les clusters des parcours
- map.addLayer({
- id: 'parcours-clusters',
- type: 'circle',
- source: 'parcours',
- filter: ['has', 'point_count'],
- paint: {
- 'circle-color': '#51bbd6',
- 'circle-radius': [
- 'interpolate',
- ['linear'],
- ['get', 'point_count'],
- 0,
- 20,
- 100,
- 40,
- ],
- },
- });
-
- // Ajouter une couche pour afficher le nombre de points dans chaque cluster
- map.addLayer({
- id: 'parcours-cluster-count',
- type: 'symbol',
- source: 'parcours',
- filter: ['has', 'point_count'],
- layout: {
- 'text-field': '{point_count_abbreviated}', // Afficher le nombre abrégé de points dans le cluster
- 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
- 'text-size': 12,
- },
- paint: {
- 'text-color': '#ffffff',
- },
- });
-
- // Ajouter une couche pour les parcours individuels (non clusterisés)
- map.addLayer({
- id: 'parcours-individual-points',
- type: 'circle',
- source: 'parcours',
- filter: ['!has', 'point_count'],
- paint: {
- 'circle-color': '#f28cb1',
- 'circle-radius': 15,
- },
- });
-
- // Événement de clic sur le cluster pour zoomer
- map.on('click', 'parcours-cluster-count', (e) => {
- const features = map.queryRenderedFeatures(e.point, {
- layers: ['parcours-cluster-count'],
- });
- const clusterId = features[0].properties.cluster_id;
- map.getSource('parcours').getClusterExpansionZoom(clusterId, (err, zoom) => {
- if (err) return;
-
- map.easeTo({
- center: features[0].geometry.coordinates,
- zoom: zoom,
- });
- });
- });
-
- // Événement de clic sur les points individuels pour afficher un popup avec des détails
- map.on('click', 'parcours-individual-points', (e) => {
- const coordinates = e.features[0].geometry.coordinates.slice();
- const name = e.features[0].properties.name;
- const distance = e.features[0].properties.distance;
-
- // Conversion de la distance en kilomètres pour l'afficher dans le popup
- const distanceInKm = (distance / 1000).toFixed(3);
-
- new mapboxgl.Popup()
- .setLngLat(coordinates)
- .setHTML(`
- ${name}
- Distance: ${distanceInKm} km
- `)
- .addTo(map);
- });
- };
-
- const filterPoisByProximity = (pois, routeCoordinates, maxDistance) => {
- return pois.filter((poi) => {
- return routeCoordinates.some((coordinate) => {
- const distance = calculateDistance(coordinate, poi.coords);
- return distance <= maxDistance;
- });
- });
- };
-
- const calculateDistance = ([lng1, lat1], [lng2, lat2]) => {
- const toRad = (deg) => (deg * Math.PI) / 180;
- const R = 6371; // Rayon de la Terre en km
- const dLat = toRad(lat2 - lat1);
- const dLng = toRad(lng2 - lng1);
- const a =
- Math.sin(dLat / 2) * Math.sin(dLat / 2) +
- Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
- return R * c;
- };
-
- const calculateBoundingBox = (coordinates) => {
- let minLng = Infinity,
- minLat = Infinity,
- maxLng = -Infinity,
- maxLat = -Infinity;
-
- coordinates.forEach(([lng, lat]) => {
- if (lng < minLng) minLng = lng;
- if (lat < minLat) minLat = lat;
- if (lng > maxLng) maxLng = lng;
- if (lat > maxLat) maxLat = lat;
- });
-
- return [minLng, minLat, maxLng, maxLat];
- };
-
- const clearMarkers = () => {
- markersRef.current.forEach((marker) => marker.remove());
- markersRef.current = [];
- };
-
- const addMarkers = (poisByCategory) => {
- const map = mapRef.current;
-
- Object.keys(poisByCategory).forEach((category) => {
- poisByCategory[category].forEach((poi) => {
- if (!poi.coords || !poi.name) return;
-
- const marker = new mapboxgl.Marker({
- color: category === "hotel"
- ? "blue"
- : category === "restaurant"
- ? "red"
- : category === "park"
- ? "green"
- : "orange",
- })
- .setLngLat(poi.coords)
- .setPopup(
- new mapboxgl.Popup().setHTML(`
- ${poi.name}
- ${category}
- `)
- )
- .addTo(map);
-
- markersRef.current.push(marker);
- });
- });
- };
-
- useEffect(() => {
- // Charger le fichier GeoJSON
- fetch("/data_extraction/marqueurs3.geojson")
- .then((response) => response.json())
- .then((data) => {
- setParcoursData(data);
- addParcoursMarkers(data); // Appeler la fonction pour ajouter les marqueurs
- })
- .catch((error) => console.error("Erreur lors de la récupération des données GeoJSON:", error));
-
- fetchRoute();
- }, [startCoords, endCoords, transportMode]);
-
- return (
-
-
-
-
- {/* SidebarTrigger uniquement */}
-
-
-
-
- {/* MapDisplay pour prendre tout l'espace */}
-
-
-
-
-
- );
-
- };
-
+"use client";
+
+import React, { useState, useRef, useEffect } from "react";
+import { AppSidebar } from "@/components/sidebar/app-sidebar";
+import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
+import MapDisplay from "@/components/sidebar/MapDisplay";
+import mapboxgl from "mapbox-gl";
+import { useTheme } from "@/hooks/theme-provider";
+import { useLocation } from "react-router-dom";
+import { NavMain } from "@/components/sidebar/nav-main";
+import { FaTrash, FaEye, FaEyeSlash } from "react-icons/fa";
+
+mapboxgl.accessToken = "pk.eyJ1Ijoic3lsdmFpbmNvc3RlcyIsImEiOiJjbTNxZXNtN3cwa2hpMmpxdWd2cndhdnYwIn0.V2ZAp-BqZq6KIHQ6Lu8eAQ";
+
+const fetchCoordinatesFromCity = async (city) => {
+ const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(city)}.json?access_token=${mapboxgl.accessToken}`;
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+ return data.features[0]?.center || null;
+ } catch (error) {
+ console.error(`Erreur lors de la récupération des coordonnées pour ${city}:`, error);
+ return null;
+ }
+};
+
+export default function Page() {
+ const [startCoords, setStartCoords] = useState(null);
+ const [endCoords, setEndCoords] = useState(null);
+ const [waypoints, setWaypoints] = useState([]); // Déclaration de setWaypoints
+ const [transportMode, setTransportMode] = useState("driving");
+ const [routeInstructions, setRouteInstructions] = useState([]);
+ const [pois, setPois] = useState({ hotel: [], restaurant: [], gas_station: [], park: [] });
+ const [parcoursData, setParcoursData] = useState(null); // État pour les données GeoJSON
+ const [query, setQuery] = useState(""); // État pour le champ de saisie
+ const [suggestions, setSuggestions] = useState([]); // État pour les suggestions
+ const [showWaypoints, setShowWaypoints] = useState(true); // État pour contrôler la visibilité des villes de passage
+ const [showWaypointsBox, setShowWaypointsBox] = useState(true); // État pour contrôler la visibilité de la boîte des villes de passage
+
+ const { theme } = useTheme();
+ const isDarkMode = theme === "dark";
+ const markersRef = useRef([]); // Stocker les marqueurs pour nettoyage ultérieur
+ const location = useLocation(); // Lire les paramètres de l'URL
+ const mapRef = useRef(null);
+ const mapContainerRef = useRef(null);
+ const queryParams = new URLSearchParams(location.search);
+ const startCity = queryParams.get("startCity");
+ const endCity = queryParams.get("endCity");
+
+ useEffect(() => {
+ const initializeCoords = async () => {
+ if (startCity) {
+ const start = await fetchCoordinatesFromCity(startCity);
+ setStartCoords(start);
+ }
+ if (endCity) {
+ const end = await fetchCoordinatesFromCity(endCity);
+ setEndCoords(end);
+ }
+ };
+
+ initializeCoords();
+ }, [startCity, endCity]);
+
+ const fetchSuggestions = async (query) => {
+ if (query.length < 3) {
+ setSuggestions([]); // Ne pas afficher de suggestions si la requête est trop courte
+ return;
+ }
+
+ const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(query)}.json?access_token=${mapboxgl.accessToken}`;
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+ const results = data.features.map((feature) => ({
+ name: feature.place_name,
+ coords: feature.geometry.coordinates,
+ }));
+ setSuggestions(results); // Mettre à jour l'état avec les suggestions
+ } catch (error) {
+ console.error("Erreur lors de la récupération des suggestions :", error);
+ }
+ };
+
+ const handleQueryChange = (e) => {
+ const value = e.target.value;
+ setQuery(value);
+ fetchSuggestions(value); // Appeler la fonction pour récupérer les suggestions
+ };
+
+ const addWaypoint = (suggestion) => {
+ if (suggestion) {
+ const exists = waypoints.some((waypoint) => waypoint.name === suggestion.name);
+ if (exists) {
+ alert("Cette ville est déjà ajoutée comme point de passage.");
+ return;
+ }
+
+ // Ajoutez la ville à la liste des waypoints
+ setWaypoints((prevWaypoints) => {
+ const newWaypoints = [
+ ...prevWaypoints,
+ { name: suggestion.name, coords: suggestion.coords },
+ ];
+
+ // Mettre à jour le chemin après l'ajout
+ fetchRoute(newWaypoints); // Passez les nouveaux waypoints à fetchRoute
+
+ return newWaypoints;
+ });
+
+ // Créer un marqueur jaune sur la carte
+ const marker = new mapboxgl.Marker({ color: "yellow" })
+ .setLngLat(suggestion.coords)
+ .setPopup(new mapboxgl.Popup().setHTML(`${suggestion.name}`))
+ .addTo(mapRef.current);
+
+ markersRef.current.push(marker); // Ajouter le marqueur à la référence
+
+ setQuery("");
+ setSuggestions([]);
+ }
+ };
+
+ const removeWaypoint = (index) => {
+ setWaypoints((prevWaypoints) => {
+ const newWaypoints = prevWaypoints.filter((_, i) => i !== index);
+
+ if (markersRef.current[index]) {
+ markersRef.current[index].remove(); // Supprimez le marqueur de la carte
+ markersRef.current.splice(index, 1); // Supprimez le marqueur de la référence
+ }
+
+ // Mettre à jour le chemin après la suppression
+ fetchRoute(newWaypoints); // Passez les nouveaux waypoints à fetchRoute
+
+ return newWaypoints;
+ });
+ };
+
+ const fetchRoute = async (waypointsToUse) => {
+ if (!startCoords || !endCoords) return;
+
+ const waypointCoords = waypointsToUse.map((waypoint) => waypoint.coords);
+ const routePath = [startCoords, ...waypointCoords, endCoords]
+ .map(([lng, lat]) => `${lng},${lat}`)
+ .join(";");
+
+ const url = `https://api.mapbox.com/directions/v5/mapbox/driving/${routePath}?geometries=geojson&steps=true&access_token=${mapboxgl.accessToken}`;
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+
+ const route = data.routes[0]?.geometry;
+ const steps = data.routes[0]?.legs[0]?.steps || [];
+
+ if (route && mapRef.current) {
+ if (mapRef.current.getLayer("route")) mapRef.current.removeLayer("route");
+ if (mapRef.current.getSource("route")) mapRef.current.removeSource("route");
+
+ mapRef.current.addSource("route", { type: "geojson", data: { type: "Feature", geometry: route } });
+ mapRef.current.addLayer({
+ id: "route",
+ type: "line",
+ source: "route",
+ layout: { "line-join": "round", "line-cap": "round" },
+ paint: { "line-color": "#007bff", "line-width": 5 },
+ });
+
+ setRouteInstructions(
+ steps.map((step) => ({
+ instruction: step.maneuver.instruction,
+ type: step.maneuver.type,
+ modifier: step.maneuver.modifier,
+ }))
+ );
+
+ fetchPois(route); // Rechercher les POI
+ }
+ } catch (error) {
+ console.error("Erreur lors de la récupération de l'itinéraire :", error);
+ }
+ };
+
+ const fetchPois = async (geometry) => {
+ if (!geometry) return;
+
+ const bbox = calculateBoundingBox(geometry.coordinates);
+ const categories = ["hotel", "restaurant", "gas_station", "park"];
+ const poisByCategory = { hotel: [], restaurant: [], gas_station: [], park: [] };
+
+ clearMarkers(); // Nettoyer les anciens marqueurs
+
+ for (const category of categories) {
+ const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${category}.json?bbox=${bbox.join(",")}&access_token=${mapboxgl.accessToken}`;
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.features) {
+ const filteredPois = filterPoisByProximity(
+ data.features.map((feature) => ({
+ name: feature.text,
+ coords: feature.geometry.coordinates,
+ category,
+ })),
+ geometry.coordinates,
+ 5 // Distance maximale en kilomètres
+ );
+ poisByCategory[category] = filteredPois;
+ }
+ } catch (error) {
+ console.error(`Erreur lors de la récupération des POI pour ${category}:`, error);
+ }
+ }
+
+ setPois(poisByCategory); // Mettre à jour les POI dans le state
+ addMarkers(poisByCategory); // Ajouter les nouveaux marqueurs
+ // addParcoursMarkers(); // Ajouter les marqueurs pour les parcours
+ };
+
+ const addParcoursMarkers = (parcoursData) => {
+ if (!parcoursData || !parcoursData.features) return;
+
+ const map = mapRef.current;
+ const allParcours = [];
+
+ // Préparation des données pour les clusters
+ parcoursData.features.forEach((parcours) => {
+ const { coordinates } = parcours.geometry;
+ const { name, distance } = parcours.properties;
+ const [lat, lon] = coordinates;
+
+ allParcours.push({
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: [lon, lat],
+ },
+ properties: {
+ name,
+ distance, // Garder la distance pour l'affichage
+ },
+ });
+ });
+
+ // Ajouter une source GeoJSON pour les clusters des parcours
+ map.addSource('parcours', {
+ type: 'geojson',
+ data: {
+ type: 'FeatureCollection',
+ features: allParcours,
+ },
+ cluster: true,
+ clusterMaxZoom: 14,
+ clusterRadius: 50,
+ });
+
+ // Ajouter des couches pour les clusters des parcours
+ map.addLayer({
+ id: 'parcours-clusters',
+ type: 'circle',
+ source: 'parcours',
+ filter: ['has', 'point_count'],
+ paint: {
+ 'circle-color': '#51bbd6',
+ 'circle-radius': [
+ 'interpolate',
+ ['linear'],
+ ['get', 'point_count'],
+ 0,
+ 20,
+ 100,
+ 40,
+ ],
+ },
+ });
+
+ // Ajouter une couche pour afficher le nombre de points dans chaque cluster
+ map.addLayer({
+ id: 'parcours-cluster-count',
+ type: 'symbol',
+ source: 'parcours',
+ filter: ['has', 'point_count'],
+ layout: {
+ 'text-field': '{point_count_abbreviated}', // Afficher le nombre abrégé de points dans le cluster
+ 'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
+ 'text-size': 12,
+ },
+ paint: {
+ 'text-color': '#ffffff',
+ },
+ });
+
+ // Ajouter une couche pour les parcours individuels (non clusterisés)
+ map.addLayer({
+ id: 'parcours-individual-points',
+ type: 'circle',
+ source: 'parcours',
+ filter: ['!has', 'point_count'],
+ paint: {
+ 'circle-color': '#f28cb1',
+ 'circle-radius': 15,
+ },
+ });
+
+ // Événement de clic sur le cluster pour zoomer
+ map.on('click', 'parcours-cluster-count', (e) => {
+ const features = map.queryRenderedFeatures(e.point, {
+ layers: ['parcours-cluster-count'],
+ });
+ const clusterId = features[0].properties.cluster_id;
+ map.getSource('parcours').getClusterExpansionZoom(clusterId, (err, zoom) => {
+ if (err) return;
+
+ map.easeTo({
+ center: features[0].geometry.coordinates,
+ zoom: zoom,
+ });
+ });
+ });
+
+ // Événement de clic sur les points individuels pour afficher un popup avec des détails
+ map.on('click', 'parcours-individual-points', (e) => {
+ const coordinates = e.features[0].geometry.coordinates.slice();
+ const name = e.features[0].properties.name;
+ const distance = e.features[0].properties.distance;
+
+ // Conversion de la distance en kilomètres pour l'afficher dans le popup
+ const distanceInKm = (distance / 1000).toFixed(3);
+
+ new mapboxgl.Popup()
+ .setLngLat(coordinates)
+ .setHTML(`
+ ${name}
+ Distance: ${distanceInKm} km
+ `)
+ .addTo(map);
+ });
+ };
+
+ const filterPoisByProximity = (pois, routeCoordinates, maxDistance) => {
+ return pois.filter((poi) => {
+ return routeCoordinates.some((coordinate) => {
+ const distance = calculateDistance(coordinate, poi.coords);
+ return distance <= maxDistance;
+ });
+ });
+ };
+
+ const calculateDistance = ([lng1, lat1], [lng2, lat2]) => {
+ const toRad = (deg) => (deg * Math.PI) / 180;
+ const R = 6371; // Rayon de la Terre en km
+ const dLat = toRad(lat2 - lat1);
+ const dLng = toRad(lng2 - lng1);
+ const a =
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return R * c;
+ };
+
+ const calculateBoundingBox = (coordinates) => {
+ let minLng = Infinity,
+ minLat = Infinity,
+ maxLng = -Infinity,
+ maxLat = -Infinity;
+
+ coordinates.forEach(([lng, lat]) => {
+ if (lng < minLng) minLng = lng;
+ if (lat < minLat) minLat = lat;
+ if (lng > maxLng) maxLng = lng;
+ if (lat > maxLat) maxLat = lat;
+ });
+
+ return [minLng, minLat, maxLng, maxLat];
+ };
+
+ const clearMarkers = () => {
+ markersRef.current.forEach((marker) => marker.remove());
+ markersRef.current = [];
+ };
+
+ const addMarkers = (poisByCategory) => {
+ const map = mapRef.current;
+
+ Object.keys(poisByCategory).forEach((category) => {
+ poisByCategory[category].forEach((poi) => {
+ if (!poi.coords || !poi.name) return;
+
+ const marker = new mapboxgl.Marker({
+ color: category === "hotel"
+ ? "blue"
+ : category === "restaurant"
+ ? "red"
+ : category === "park"
+ ? "green"
+ : "orange",
+ })
+ .setLngLat(poi.coords)
+ .setPopup(
+ new mapboxgl.Popup().setHTML(`
+ ${poi.name}
+ ${category}
+ `)
+ )
+ .addTo(map);
+
+ markersRef.current.push(marker);
+ });
+ });
+ };
+
+ const addWaypointMarker = (suggestion) => {
+ if (suggestion) {
+ // Ajoutez la ville à la liste des waypoints
+ setWaypoints((prevWaypoints) => [
+ ...prevWaypoints,
+ { name: suggestion.name, coords: suggestion.coords },
+ ]);
+
+ // Appeler la fonction pour ajouter le marqueur sur la carte
+ const marker = new mapboxgl.Marker({ color: "yellow" }) // Marqueur jaune
+ .setLngLat(suggestion.coords)
+ .setPopup(new mapboxgl.Popup().setHTML(`${suggestion.name}`))
+ .addTo(mapRef.current);
+
+ markersRef.current.push(marker); // Ajouter le marqueur à la référence
+ }
+ };
+
+ useEffect(() => {
+ // Charger le fichier GeoJSON
+ fetch("/data_extraction/marqueurs3.geojson")
+ .then((response) => response.json())
+ .then((data) => {
+ setParcoursData(data);
+ addParcoursMarkers(data); // Appeler la fonction pour ajouter les marqueurs
+ })
+ .catch((error) => console.error("Erreur lors de la récupération des données GeoJSON:", error));
+
+ fetchRoute();
+ }, [startCoords, endCoords, transportMode]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Boîte pour ajouter une ville de passage */}
+ {showWaypointsBox && ( // Affichez la boîte seulement si showWaypointsBox est vrai
+
+
Ajouter une ville (point de passage)
+
+
+
+
+ {suggestions.map((suggestion, index) => (
+ - addWaypoint(suggestion)}
+ >
+ {suggestion.name}
+
+ ))}
+
+
+ {/* Liste des villes de passage ajoutées */}
+
+
Villes de passage :
+
+ {waypoints.map((waypoint, index) => (
+ -
+ {waypoint.name}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Icône d'œil fixe pour cacher ou afficher la boîte d'ajout de ville de passage */}
+
+
+
+ );
+
+ };
+
diff --git a/src/components/sidebar/nav-main.jsx b/src/components/sidebar/nav-main.jsx
index 44d9052..724805e 100644
--- a/src/components/sidebar/nav-main.jsx
+++ b/src/components/sidebar/nav-main.jsx
@@ -1,5 +1,6 @@
"use client";
+import React from "react";
import { ChevronRight } from "lucide-react"
import {
Collapsible,
@@ -19,10 +20,44 @@ import {
} from "@/components/ui/sidebar"
import { NavItinerary } from "@/components/sidebar/NavItinerary"
import { NavSettings } from "@/components/sidebar/NavSettings";
+import { FaSave } from "react-icons/fa";
+import mapboxgl from "mapbox-gl";
-export function NavMain({ items, setStartCoords, routeDuration, setEndCoords, setTransportMode, routeInstructions, transportMode }) {
+export function NavMain({ items, setStartCoords, routeDuration, setEndCoords, setTransportMode, routeInstructions, transportMode, onSaveRoute, setWaypoints, addWaypointMarker }) {
const { state } = useSidebar(); // "expanded" ou "collapsed"
+ const handleSaveRoute = async () => {
+ // Préparez les données à envoyer à l'API
+ const routeData = {
+ startCoords,
+ endCoords,
+ waypoints,
+ transportMode,
+ routeInstructions,
+ };
+
+ try {
+ const response = await fetch('/api/save-route', { // Remplacez par l'URL de votre API
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(routeData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Erreur lors de l\'enregistrement du trajet');
+ }
+
+ const result = await response.json();
+ alert('Trajet enregistré avec succès !');
+ console.log(result); // Affichez le résultat si nécessaire
+ } catch (error) {
+ console.error('Erreur:', error);
+ alert('Échec de l\'enregistrement du trajet.');
+ }
+ };
+
return (
Navigation
@@ -73,6 +108,12 @@ export function NavMain({ items, setStartCoords, routeDuration, setEndCoords, se
))}
+
);
From fd4d3bd04c776224a9cd02e5cc2c5fc8ad52efea Mon Sep 17 00:00:00 2001
From: Arezki Bazizi
Date: Sat, 15 Feb 2025 19:51:23 +0100
Subject: [PATCH 02/12] ajout de la sauvegarde des trajet
---
src/components/MapBoxMap.jsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/MapBoxMap.jsx b/src/components/MapBoxMap.jsx
index 1223a56..5a01972 100644
--- a/src/components/MapBoxMap.jsx
+++ b/src/components/MapBoxMap.jsx
@@ -62,6 +62,13 @@ export default function Page() {
initializeCoords();
}, [startCity, endCity]);
+ // Ajoutez un autre useEffect pour mettre à jour le trajet
+ useEffect(() => {
+ if (startCoords && endCoords) {
+ fetchRoute(waypoints); // Appelez fetchRoute avec les waypoints actuels
+ }
+ }, [startCoords, endCoords, waypoints]);
+
const fetchSuggestions = async (query) => {
if (query.length < 3) {
setSuggestions([]); // Ne pas afficher de suggestions si la requête est trop courte
From 0d580b58599a99f0313a59d5f2f9a83027f0c454 Mon Sep 17 00:00:00 2001
From: Arezki Bazizi
Date: Fri, 21 Mar 2025 15:10:07 +0100
Subject: [PATCH 03/12] fixing the map
---
src/components/MapBoxMap.jsx | 117 +++++++++++++++++++++-------
src/components/sidebar/nav-main.jsx | 1 +
2 files changed, 88 insertions(+), 30 deletions(-)
diff --git a/src/components/MapBoxMap.jsx b/src/components/MapBoxMap.jsx
index 5a01972..b73a277 100644
--- a/src/components/MapBoxMap.jsx
+++ b/src/components/MapBoxMap.jsx
@@ -17,7 +17,11 @@ const fetchCoordinatesFromCity = async (city) => {
try {
const response = await fetch(url);
const data = await response.json();
- return data.features[0]?.center || null;
+ const coords = data.features[0]?.center || null;
+ if (coords && Array.isArray(coords) && coords.length === 2) {
+ return coords; // Assurez-vous que les coordonnées sont au format [lng, lat]
+ }
+ return null;
} catch (error) {
console.error(`Erreur lors de la récupération des coordonnées pour ${city}:`, error);
return null;
@@ -63,11 +67,15 @@ export default function Page() {
}, [startCity, endCity]);
// Ajoutez un autre useEffect pour mettre à jour le trajet
+ useEffect(() => {
+ fetchRoute(waypoints); // Appelez fetchRoute avec les waypoints actuels
+ }, [startCoords, endCoords, waypoints, transportMode]); // Ajoutez transportMode ici
+
useEffect(() => {
if (startCoords && endCoords) {
fetchRoute(waypoints); // Appelez fetchRoute avec les waypoints actuels
}
- }, [startCoords, endCoords, waypoints]);
+ }, [startCoords, endCoords, waypoints, transportMode]); // Ajoutez transportMode ici
const fetchSuggestions = async (query) => {
if (query.length < 3) {
@@ -146,18 +154,26 @@ export default function Page() {
};
const fetchRoute = async (waypointsToUse) => {
- if (!startCoords || !endCoords) return;
+ if (!startCoords || !endCoords) {
+ console.warn("Les coordonnées de départ ou d'arrivée ne sont pas définies.");
+ return; // Ne pas continuer si les coordonnées ne sont pas disponibles
+ }
const waypointCoords = waypointsToUse.map((waypoint) => waypoint.coords);
const routePath = [startCoords, ...waypointCoords, endCoords]
.map(([lng, lat]) => `${lng},${lat}`)
.join(";");
- const url = `https://api.mapbox.com/directions/v5/mapbox/driving/${routePath}?geometries=geojson&steps=true&access_token=${mapboxgl.accessToken}`;
+ const url = `https://api.mapbox.com/directions/v5/mapbox/${transportMode}/${routePath}?geometries=geojson&steps=true&access_token=${mapboxgl.accessToken}`;
try {
const response = await fetch(url);
const data = await response.json();
+ if (!data.routes || data.routes.length === 0) {
+ console.warn("Aucune route trouvée.");
+ return; // Ne pas continuer si aucune route n'est trouvée
+ }
+
const route = data.routes[0]?.geometry;
const steps = data.routes[0]?.legs[0]?.steps || [];
@@ -193,8 +209,8 @@ export default function Page() {
if (!geometry) return;
const bbox = calculateBoundingBox(geometry.coordinates);
- const categories = ["hotel", "restaurant", "gas_station", "park"];
- const poisByCategory = { hotel: [], restaurant: [], gas_station: [], park: [] };
+ const categories = ["hotel", "restaurant", "gas_station", "park", "parking"];
+ const poisByCategory = { hotel: [], restaurant: [], gas_station: [], park: [], parking: [] };
clearMarkers(); // Nettoyer les anciens marqueurs
@@ -206,13 +222,13 @@ export default function Page() {
if (data.features) {
const filteredPois = filterPoisByProximity(
- data.features.map((feature) => ({
- name: feature.text,
- coords: feature.geometry.coordinates,
- category,
- })),
- geometry.coordinates,
- 5 // Distance maximale en kilomètres
+ data.features.map((feature) => ({
+ name: feature.text,
+ coords: feature.geometry.coordinates,
+ category,
+ })),
+ geometry.coordinates,
+ 5 // Distance maximale en kilomètres
);
poisByCategory[category] = filteredPois;
}
@@ -223,7 +239,6 @@ export default function Page() {
setPois(poisByCategory); // Mettre à jour les POI dans le state
addMarkers(poisByCategory); // Ajouter les nouveaux marqueurs
- // addParcoursMarkers(); // Ajouter les marqueurs pour les parcours
};
const addParcoursMarkers = (parcoursData) => {
@@ -232,7 +247,6 @@ export default function Page() {
const map = mapRef.current;
const allParcours = [];
- // Préparation des données pour les clusters
parcoursData.features.forEach((parcours) => {
const { coordinates } = parcours.geometry;
const { name, distance } = parcours.properties;
@@ -246,11 +260,16 @@ export default function Page() {
},
properties: {
name,
- distance, // Garder la distance pour l'affichage
+ distance,
},
});
});
+ // Vérifiez si la source existe déjà avant de l'ajouter
+ if (map.getSource('parcours')) {
+ map.removeSource('parcours');
+ }
+
// Ajouter une source GeoJSON pour les clusters des parcours
map.addSource('parcours', {
type: 'geojson',
@@ -395,23 +414,45 @@ export default function Page() {
poisByCategory[category].forEach((poi) => {
if (!poi.coords || !poi.name) return;
- const marker = new mapboxgl.Marker({
- color: category === "hotel"
- ? "blue"
- : category === "restaurant"
- ? "red"
- : category === "park"
- ? "green"
- : "orange",
- })
- .setLngLat(poi.coords)
- .setPopup(
- new mapboxgl.Popup().setHTML(`
+ // Définir la couleur et l'icône en fonction de la catégorie
+ let color;
+ let icon;
+ switch (category) {
+ case "hotel":
+ color = "blue";
+ icon = ""; // Utilisez une icône d'hôtel
+ break;
+ case "restaurant":
+ color = "red";
+ icon = ""; // Utilisez une icône de restaurant
+ break;
+ case "park":
+ color = "green";
+ icon = ""; // Utilisez une icône de parc
+ break;
+ case "gas_station":
+ color = "orange";
+ icon = ""; // Utilisez une icône de station-service
+ break;
+ case "parking": // Ajoutez une condition pour les parkings
+ color = "gray"; // Couleur pour le parking
+ icon = "P"; // Utilisez un symbole "P" pour le parking
+ break;
+ default:
+ color = "gray";
+ icon = ""; // Icône par défaut
+ }
+
+ const marker = new mapboxgl.Marker({ color })
+ .setLngLat(poi.coords)
+ .setPopup(
+ new mapboxgl.Popup().setHTML(`
${poi.name}
${category}
+ ${icon}
`)
- )
- .addTo(map);
+ )
+ .addTo(map);
markersRef.current.push(marker);
});
@@ -449,6 +490,22 @@ export default function Page() {
fetchRoute();
}, [startCoords, endCoords, transportMode]);
+ useEffect(() => {
+ if (mapRef.current && startCoords && endCoords) {
+ // Marqueur pour le point de départ
+ new mapboxgl.Marker({ color: "blue" })
+ .setLngLat(startCoords)
+ .setPopup(new mapboxgl.Popup().setHTML("Point de départ"))
+ .addTo(mapRef.current);
+
+ // Marqueur pour le point d'arrivée
+ new mapboxgl.Marker({ color: "green" })
+ .setLngLat(endCoords)
+ .setPopup(new mapboxgl.Popup().setHTML("Point d'arrivée"))
+ .addTo(mapRef.current);
+ }
+ }, [startCoords, endCoords]);
+
return (
Date: Sat, 22 Mar 2025 18:26:59 +0100
Subject: [PATCH 04/12] adding the fetchPlacesInCities and the saving logic
---
src/components/MapBoxMap.jsx | 357 +++++++++++-----------------
src/components/sidebar/nav-main.jsx | 260 ++++++++++----------
2 files changed, 283 insertions(+), 334 deletions(-)
diff --git a/src/components/MapBoxMap.jsx b/src/components/MapBoxMap.jsx
index b73a277..8e9277e 100644
--- a/src/components/MapBoxMap.jsx
+++ b/src/components/MapBoxMap.jsx
@@ -34,12 +34,12 @@ export default function Page() {
const [waypoints, setWaypoints] = useState([]); // Déclaration de setWaypoints
const [transportMode, setTransportMode] = useState("driving");
const [routeInstructions, setRouteInstructions] = useState([]);
- const [pois, setPois] = useState({ hotel: [], restaurant: [], gas_station: [], park: [] });
const [parcoursData, setParcoursData] = useState(null); // État pour les données GeoJSON
const [query, setQuery] = useState(""); // État pour le champ de saisie
const [suggestions, setSuggestions] = useState([]); // État pour les suggestions
const [showWaypoints, setShowWaypoints] = useState(true); // État pour contrôler la visibilité des villes de passage
const [showWaypointsBox, setShowWaypointsBox] = useState(true); // État pour contrôler la visibilité de la boîte des villes de passage
+ const [places, setPlaces] = useState([]); // Initialisez places avec un tableau vide
const { theme } = useTheme();
const isDarkMode = theme === "dark";
@@ -164,6 +164,8 @@ export default function Page() {
.map(([lng, lat]) => `${lng},${lat}`)
.join(";");
+ console.log("Coordonnées du trajet :", routePath); // Ajoutez cette ligne pour déboguer
+
const url = `https://api.mapbox.com/directions/v5/mapbox/${transportMode}/${routePath}?geometries=geojson&steps=true&access_token=${mapboxgl.accessToken}`;
try {
const response = await fetch(url);
@@ -198,47 +200,22 @@ export default function Page() {
}))
);
- fetchPois(route); // Rechercher les POI
- }
- } catch (error) {
- console.error("Erreur lors de la récupération de l'itinéraire :", error);
- }
- };
-
- const fetchPois = async (geometry) => {
- if (!geometry) return;
+ // Récupérer les hôtels à proximité du trajet
+ const hotels = await fetchNearbyHotels(waypointCoords);
+ addHotelMarkers(hotels); // Ajouter les marqueurs pour les hôtels
- const bbox = calculateBoundingBox(geometry.coordinates);
- const categories = ["hotel", "restaurant", "gas_station", "park", "parking"];
- const poisByCategory = { hotel: [], restaurant: [], gas_station: [], park: [], parking: [] };
+ // Récupérer les établissements dans la ville d'arrivée
+ const arrivalPlaces = await fetchPlacesInCities([endCity]);
+ addHotelMarkers(arrivalPlaces); // Ajouter les marqueurs pour les établissements dans la ville d'arrivée
- clearMarkers(); // Nettoyer les anciens marqueurs
-
- for (const category of categories) {
- const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${category}.json?bbox=${bbox.join(",")}&access_token=${mapboxgl.accessToken}`;
- try {
- const response = await fetch(url);
- const data = await response.json();
-
- if (data.features) {
- const filteredPois = filterPoisByProximity(
- data.features.map((feature) => ({
- name: feature.text,
- coords: feature.geometry.coordinates,
- category,
- })),
- geometry.coordinates,
- 5 // Distance maximale en kilomètres
- );
- poisByCategory[category] = filteredPois;
- }
- } catch (error) {
- console.error(`Erreur lors de la récupération des POI pour ${category}:`, error);
+ // Récupérer les établissements proches du trajet
+ const routeCoords = await getRouteCoordinates(startCoords, endCoords, waypointsToUse);
+ const nearbyPlaces = await fetchNearbyPlaces(routeCoords);
+ addHotelMarkers(nearbyPlaces); // Ajouter les marqueurs pour les établissements proches
}
+ } catch (error) {
+ console.error("Erreur lors de la récupération de l'itinéraire :", error);
}
-
- setPois(poisByCategory); // Mettre à jour les POI dans le state
- addMarkers(poisByCategory); // Ajouter les nouveaux marqueurs
};
const addParcoursMarkers = (parcoursData) => {
@@ -329,182 +306,135 @@ export default function Page() {
'circle-radius': 15,
},
});
-
- // Événement de clic sur le cluster pour zoomer
- map.on('click', 'parcours-cluster-count', (e) => {
- const features = map.queryRenderedFeatures(e.point, {
- layers: ['parcours-cluster-count'],
- });
- const clusterId = features[0].properties.cluster_id;
- map.getSource('parcours').getClusterExpansionZoom(clusterId, (err, zoom) => {
- if (err) return;
-
- map.easeTo({
- center: features[0].geometry.coordinates,
- zoom: zoom,
- });
- });
- });
-
- // Événement de clic sur les points individuels pour afficher un popup avec des détails
- map.on('click', 'parcours-individual-points', (e) => {
- const coordinates = e.features[0].geometry.coordinates.slice();
- const name = e.features[0].properties.name;
- const distance = e.features[0].properties.distance;
-
- // Conversion de la distance en kilomètres pour l'afficher dans le popup
- const distanceInKm = (distance / 1000).toFixed(3);
-
- new mapboxgl.Popup()
- .setLngLat(coordinates)
- .setHTML(`
- ${name}
- Distance: ${distanceInKm} km
- `)
- .addTo(map);
- });
};
- const filterPoisByProximity = (pois, routeCoordinates, maxDistance) => {
- return pois.filter((poi) => {
- return routeCoordinates.some((coordinate) => {
- const distance = calculateDistance(coordinate, poi.coords);
- return distance <= maxDistance;
- });
- });
+ const clearMarkers = () => {
+ markersRef.current.forEach((marker) => marker.remove());
+ markersRef.current = [];
};
- const calculateDistance = ([lng1, lat1], [lng2, lat2]) => {
- const toRad = (deg) => (deg * Math.PI) / 180;
- const R = 6371; // Rayon de la Terre en km
- const dLat = toRad(lat2 - lat1);
- const dLng = toRad(lng2 - lng1);
- const a =
- Math.sin(dLat / 2) * Math.sin(dLat / 2) +
- Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
- return R * c;
- };
+ const fetchNearbyHotels = async (routeCoordinates) => {
+ if (!routeCoordinates || routeCoordinates.length === 0) return [];
- const calculateBoundingBox = (coordinates) => {
- let minLng = Infinity,
- minLat = Infinity,
- maxLng = -Infinity,
- maxLat = -Infinity;
-
- coordinates.forEach(([lng, lat]) => {
- if (lng < minLng) minLng = lng;
- if (lat < minLat) minLat = lat;
- if (lng > maxLng) maxLng = lng;
- if (lat > maxLat) maxLat = lat;
- });
+ const hotels = [];
+
+ // Parcourez chaque segment de l'itinéraire
+ for (let i = 0; i < routeCoordinates.length - 1; i++) {
+ const [start, end] = [routeCoordinates[i], routeCoordinates[i + 1]];
+ const midLng = (start[0] + end[0]) / 2;
+ const midLat = (start[1] + end[1]) / 2;
- return [minLng, minLat, maxLng, maxLat];
- };
+ const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/hotel.json?proximity=${midLng},${midLat}&access_token=${mapboxgl.accessToken}`;
- const clearMarkers = () => {
- markersRef.current.forEach((marker) => marker.remove());
- markersRef.current = [];
+ console.log("URL Mapbox :", url); // Ajoutez cette ligne pour déboguer
+
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.features) {
+ const segmentHotels = data.features.map((feature) => ({
+ name: feature.text || "Hôtel sans nom",
+ coords: feature.geometry.coordinates,
+ }));
+ hotels.push(...segmentHotels); // Ajouter les hôtels récupérés à la liste
+ } else {
+ console.warn("Aucun hôtel trouvé à proximité.");
+ }
+ } catch (error) {
+ console.error("Erreur lors de la récupération des hôtels :", error);
+ }
+ }
+
+ console.log("Hôtels récupérés le long du trajet :", hotels); // Affichez la liste des hôtels récupérés
+ return hotels; // Retourner la liste des hôtels
};
- const addMarkers = (poisByCategory) => {
+ const addHotelMarkers = (places) => {
const map = mapRef.current;
- Object.keys(poisByCategory).forEach((category) => {
- poisByCategory[category].forEach((poi) => {
- if (!poi.coords || !poi.name) return;
-
- // Définir la couleur et l'icône en fonction de la catégorie
- let color;
- let icon;
- switch (category) {
- case "hotel":
- color = "blue";
- icon = ""; // Utilisez une icône d'hôtel
- break;
- case "restaurant":
- color = "red";
- icon = ""; // Utilisez une icône de restaurant
- break;
- case "park":
- color = "green";
- icon = ""; // Utilisez une icône de parc
- break;
- case "gas_station":
- color = "orange";
- icon = ""; // Utilisez une icône de station-service
- break;
- case "parking": // Ajoutez une condition pour les parkings
- color = "gray"; // Couleur pour le parking
- icon = "P"; // Utilisez un symbole "P" pour le parking
- break;
- default:
- color = "gray";
- icon = ""; // Icône par défaut
- }
+ if (!Array.isArray(places)) {
+ console.warn("Les établissements doivent être un tableau.");
+ return; // Ne pas continuer si places n'est pas un tableau
+ }
- const marker = new mapboxgl.Marker({ color })
- .setLngLat(poi.coords)
- .setPopup(
- new mapboxgl.Popup().setHTML(`
- ${poi.name}
- ${category}
- ${icon}
- `)
- )
- .addTo(map);
-
- markersRef.current.push(marker);
- });
+ places.forEach((place) => {
+ const marker = new mapboxgl.Marker({ color: "blue" }) // Couleur pour les établissements
+ .setLngLat(place.coords)
+ .setPopup(new mapboxgl.Popup().setHTML(`${place.name}`))
+ .addTo(map);
+
+ markersRef.current.push(marker); // Ajouter le marqueur à la référence
});
};
- const addWaypointMarker = (suggestion) => {
- if (suggestion) {
- // Ajoutez la ville à la liste des waypoints
- setWaypoints((prevWaypoints) => [
- ...prevWaypoints,
- { name: suggestion.name, coords: suggestion.coords },
- ]);
-
- // Appeler la fonction pour ajouter le marqueur sur la carte
- const marker = new mapboxgl.Marker({ color: "yellow" }) // Marqueur jaune
- .setLngLat(suggestion.coords)
- .setPopup(new mapboxgl.Popup().setHTML(`${suggestion.name}`))
- .addTo(mapRef.current);
-
- markersRef.current.push(marker); // Ajouter le marqueur à la référence
+ const fetchPlacesInCities = async (cities) => {
+ const allPlaces = [];
+
+ for (const city of cities) {
+ const coords = await fetchCoordinatesFromCity(city);
+ if (!coords) continue; // Passer à la prochaine ville si les coordonnées ne sont pas disponibles
+
+ const [lng, lat] = coords;
+ const url = `https://api.mapbox.com/search/searchbox/v1/forward?q=${encodeURIComponent("restaurant")}&proximity=${lng},${lat}&access_token=${mapboxgl.accessToken}`;
+
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.features) {
+ const places = data.features.map((feature) => ({
+ name: feature.properties.name || "Établissement sans nom",
+ coords: feature.geometry.coordinates,
+ description: feature.properties.description || "Aucune description disponible",
+ image: feature.properties.image || "URL_de_l_image_par_défaut.jpg",
+ }));
+ allPlaces.push(...places); // Ajouter les établissements récupérés à la liste
+ }
+ } catch (error) {
+ console.error("Erreur lors de la récupération des établissements :", error);
+ }
}
+
+ return allPlaces; // Retourner la liste de tous les établissements
};
- useEffect(() => {
- // Charger le fichier GeoJSON
- fetch("/data_extraction/marqueurs3.geojson")
- .then((response) => response.json())
- .then((data) => {
- setParcoursData(data);
- addParcoursMarkers(data); // Appeler la fonction pour ajouter les marqueurs
- })
- .catch((error) => console.error("Erreur lors de la récupération des données GeoJSON:", error));
-
- fetchRoute();
- }, [startCoords, endCoords, transportMode]);
+ const fetchNearbyPlaces = async (routeCoords) => {
+ const places = [];
+ for (const coord of routeCoords) {
+ const [lng, lat] = coord; // Décomposer les coordonnées
+ const url = `https://api.mapbox.com/search/searchbox/v1/forward?q=${encodeURIComponent("restaurant")}&proximity=${lng},${lat}&access_token=${mapboxgl.accessToken}`;
- useEffect(() => {
- if (mapRef.current && startCoords && endCoords) {
- // Marqueur pour le point de départ
- new mapboxgl.Marker({ color: "blue" })
- .setLngLat(startCoords)
- .setPopup(new mapboxgl.Popup().setHTML("Point de départ"))
- .addTo(mapRef.current);
+ try {
+ const response = await fetch(url);
+ const data = await response.json();
- // Marqueur pour le point d'arrivée
- new mapboxgl.Marker({ color: "green" })
- .setLngLat(endCoords)
- .setPopup(new mapboxgl.Popup().setHTML("Point d'arrivée"))
- .addTo(mapRef.current);
+ if (data.features) {
+ const nearbyPlaces = data.features.map((feature) => ({
+ name: feature.properties.name || "Établissement sans nom",
+ coords: feature.geometry.coordinates,
+ description: feature.properties.description || "Aucune description disponible",
+ image: feature.properties.image || "URL_de_l_image_par_défaut.jpg",
+ }));
+ places.push(...nearbyPlaces); // Ajouter les établissements à la liste
+ }
+ } catch (error) {
+ console.error("Erreur lors de la récupération des établissements :", error);
+ }
}
- }, [startCoords, endCoords]);
+ return places; // Retourner la liste des établissements
+ };
+
+ useEffect(() => {
+ const fetchPlaces = async () => {
+ const cities = [endCity, ...waypoints.map(waypoint => waypoint.name)]; // Inclure la ville d'arrivée et les villes de passage
+ const placesInCities = await fetchPlacesInCities(cities);
+ setPlaces(placesInCities); // Mettez à jour l'état avec les établissements récupérés
+ addHotelMarkers(placesInCities); // Ajouter les marqueurs pour les établissements récupérés
+ };
+
+ fetchPlaces();
+ }, [endCoords, waypoints]); // Déclencher cet effet lorsque endCoords ou waypoints changent
return (
@@ -516,7 +446,9 @@ export default function Page() {
setTransportMode={setTransportMode}
transportMode={transportMode}
routeInstructions={routeInstructions}
- pois={pois} // Ajout des POI dans la sidebar
+ places={places}
+ startCoords={startCoords}
+ endCoords={endCoords}
/>
@@ -540,23 +472,23 @@ export default function Page() {
@@ -583,13 +515,12 @@ export default function Page() {
{/* Icône d'œil fixe pour cacher ou afficher la boîte d'ajout de ville de passage */}
);
-
- };
+}
diff --git a/src/components/sidebar/nav-main.jsx b/src/components/sidebar/nav-main.jsx
index aef9e0a..969feaf 100644
--- a/src/components/sidebar/nav-main.jsx
+++ b/src/components/sidebar/nav-main.jsx
@@ -1,121 +1,139 @@
-"use client";
-
-import React from "react";
-import { ChevronRight } from "lucide-react"
-import {
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
-} from "@/components/ui/collapsible"
-
-import {
- SidebarGroup,
- SidebarGroupLabel,
- SidebarMenu,
- SidebarMenuButton,
- SidebarMenuItem,
- SidebarMenuSub,
- SidebarMenuSubButton,
- SidebarMenuSubItem,
- useSidebar,
-} from "@/components/ui/sidebar"
-import { NavItinerary } from "@/components/sidebar/NavItinerary"
-import { NavSettings } from "@/components/sidebar/NavSettings";
-import { FaSave } from "react-icons/fa";
-import mapboxgl from "mapbox-gl";
-
-export function NavMain({ items, setStartCoords, routeDuration, setEndCoords, setTransportMode, routeInstructions, transportMode, onSaveRoute, setWaypoints, addWaypointMarker }) {
- const { state } = useSidebar(); // "expanded" ou "collapsed"
-
- const handleSaveRoute = async () => {
- // Préparez les données à envoyer à l'API
- const routeData = {
- startCoords,
- endCoords,
- waypoints,
- transportMode,
- routeInstructions,
- };
-
- try {
- const response = await fetch('/api/save-route', { // Remplacez par l'URL de votre API
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(routeData),
- });
-
- if (!response.ok) {
- throw new Error('Erreur lors de l\'enregistrement du trajet');
- }
-
- const result = await response.json();
- alert('Trajet enregistré avec succès !');
- console.log(result); // Affichez le résultat si nécessaire
- } catch (error) {
- console.error('Erreur:', error);
- alert('Échec de l\'enregistrement du trajet.');
- }
- };
-
- return (
-
- Navigation
-
- {items.map((item) => (
-
-
-
-
- {item.icon && }
- {item.title}
-
-
-
-
- {item.showItinerary && (
-
- )}
- {item.showSettings && (
-
- )}
- {item.items && !item.showItinerary && !item.showSettings && (
-
- {item.items?.map((subItem) => (
-
-
-
- {subItem.title}
-
-
-
- ))}
-
- )}
-
-
-
- ))}
-
-
-
- );
-}
+"use client";
+
+import React from "react";
+import { ChevronRight } from "lucide-react"
+import {
+ Collapsible,
+ CollapsibleContent,
+ CollapsibleTrigger,
+} from "@/components/ui/collapsible"
+
+import {
+ SidebarGroup,
+ SidebarGroupLabel,
+ SidebarMenu,
+ SidebarMenuButton,
+ SidebarMenuItem,
+ SidebarMenuSub,
+ SidebarMenuSubButton,
+ SidebarMenuSubItem,
+ useSidebar,
+} from "@/components/ui/sidebar"
+import { NavItinerary } from "@/components/sidebar/NavItinerary"
+import { NavSettings } from "@/components/sidebar/NavSettings";
+import { FaSave } from "react-icons/fa";
+import mapboxgl from "mapbox-gl";
+
+export function NavMain({ items, setStartCoords, routeDuration, setEndCoords, setTransportMode, routeInstructions, transportMode, onSaveRoute, setWaypoints, addWaypointMarker, pois, places = [], startCoords, endCoords, waypoints }) {
+ const { state } = useSidebar(); // "expanded" ou "collapsed"
+
+ console.log("Données reçues dans NavMain :", { startCoords, endCoords, waypoints, transportMode, routeInstructions }); // Pour déboguer
+
+ const handleSaveRoute = async () => {
+ const routeData = {
+ startCoords,
+ endCoords,
+ waypoints,
+ transportMode,
+ routeInstructions,
+ };
+
+ try {
+ const response = await fetch('/api/save-route', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(routeData),
+ });
+
+ if (!response.ok) {
+ throw new Error('Erreur lors de l\'enregistrement du trajet');
+ }
+
+ const result = await response.json();
+ alert('Trajet enregistré avec succès !');
+ console.log(result); // Affichez le résultat si nécessaire
+ } catch (error) {
+ console.error('Erreur:', error);
+ alert('Échec de l\'enregistrement du trajet.');
+ }
+ };
+
+ return (
+
+ Navigation
+
+ {items.map((item) => (
+
+
+
+
+ {item.icon && }
+ {item.title}
+
+
+
+
+ {item.showItinerary && (
+
+ )}
+ {item.showSettings && (
+
+ )}
+ {item.items && !item.showItinerary && !item.showSettings && (
+
+ {item.items?.map((subItem) => (
+
+
+
+ {subItem.title}
+
+
+
+ ))}
+
+ )}
+
+
+
+ ))}
+
+
+
+ {/* Afficher la liste des établissements */}
+
+
Établissements à proximité :
+
+ {places.length > 0 ? (
+ places.map((place, index) => (
+ -
+ {place.name}
+
{place.description}
+
+ ))
+ ) : (
+ - Aucun établissement trouvé.
+ )}
+
+
+
+ );
+}
From ebb5c0a883423a4818142c2146a09eeaa3aafefc Mon Sep 17 00:00:00 2001
From: Arezki Bazizi
Date: Sat, 22 Mar 2025 18:32:52 +0100
Subject: [PATCH 05/12] fixng addMarkers
---
src/components/MapBoxMap.jsx | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/components/MapBoxMap.jsx b/src/components/MapBoxMap.jsx
index 8e9277e..bfca8fc 100644
--- a/src/components/MapBoxMap.jsx
+++ b/src/components/MapBoxMap.jsx
@@ -358,6 +358,9 @@ export default function Page() {
return; // Ne pas continuer si places n'est pas un tableau
}
+ // Supprimer les anciens marqueurs avant d'ajouter de nouveaux
+ clearMarkers();
+
places.forEach((place) => {
const marker = new mapboxgl.Marker({ color: "blue" }) // Couleur pour les établissements
.setLngLat(place.coords)
From 456f5a70d3fa24e340482816fdcc3899766797ca Mon Sep 17 00:00:00 2001
From: sylvain
Date: Fri, 13 Jun 2025 12:06:41 +0200
Subject: [PATCH 06/12] Ajout de la logique complete du front pour les articles
---
package-lock.json | 522 ++++++++++++++++++++++++++-
package.json | 1 +
src/components/news/FavoriteList.jsx | 63 ++++
src/components/ui/select.jsx | 120 ++++++
src/layouts/BaseLayouts.jsx | 6 +-
src/layouts/components/Header.jsx | 31 +-
src/pages/EcoNews.jsx | 181 ++++++++++
src/router/routes.jsx | 6 +-
8 files changed, 906 insertions(+), 24 deletions(-)
create mode 100644 src/components/news/FavoriteList.jsx
create mode 100644 src/components/ui/select.jsx
create mode 100644 src/pages/EcoNews.jsx
diff --git a/package-lock.json b/package-lock.json
index 50f9904..0d44ed8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
+ "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
@@ -1224,6 +1225,12 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
@@ -1704,6 +1711,463 @@
}
}
},
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz",
+ "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.10",
+ "@radix-ui/react-focus-guards": "1.1.2",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.7",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
+ "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
+ "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
+ "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
+ "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
+ "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
+ "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
+ "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/react-separator": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz",
@@ -1812,6 +2276,39 @@
}
}
},
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
@@ -1845,6 +2342,21 @@
}
}
},
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
+ "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-rect": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
@@ -7132,16 +7644,16 @@
}
},
"node_modules/react-remove-scroll": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.2.tgz",
- "integrity": "sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
+ "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
"license": "MIT",
"dependencies": {
"react-remove-scroll-bar": "^2.3.7",
- "react-style-singleton": "^2.2.1",
+ "react-style-singleton": "^2.2.3",
"tslib": "^2.1.0",
"use-callback-ref": "^1.3.3",
- "use-sidecar": "^1.1.2"
+ "use-sidecar": "^1.1.3"
},
"engines": {
"node": ">=10"
diff --git a/package.json b/package.json
index 5e5614d..ac8fd2a 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
+ "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.1",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
diff --git a/src/components/news/FavoriteList.jsx b/src/components/news/FavoriteList.jsx
new file mode 100644
index 0000000..5e0ffee
--- /dev/null
+++ b/src/components/news/FavoriteList.jsx
@@ -0,0 +1,63 @@
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+import api from "@/security/auth/Api";
+import { useEffect, useState } from "react";
+
+function FavoritesList() {
+ const [favorites, setFavorites] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ api.get("/favorites")
+ .then((res) => {
+ setFavorites(res.data);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error("[FavoritesList] Erreur lors du fetch:", err);
+ setLoading(false);
+ });
+ }, []);
+
+ return (
+
+
⭐ Mes articles favoris
+
+ {loading ? (
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+ ) : favorites.length === 0 ? (
+
Aucun favori pour le moment.
+ ) : (
+
+ {favorites.map((fav, index) => (
+
+ {fav.image_url && (
+
+ )}
+
+ {fav.title}
+
+ {fav.description?.slice(0, 200)}...
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default FavoritesList;
diff --git a/src/components/ui/select.jsx b/src/components/ui/select.jsx
new file mode 100644
index 0000000..a7b197e
--- /dev/null
+++ b/src/components/ui/select.jsx
@@ -0,0 +1,120 @@
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { Check, ChevronDown, ChevronUp } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Select = SelectPrimitive.Root
+
+const SelectGroup = SelectPrimitive.Group
+
+const SelectValue = SelectPrimitive.Value
+
+const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className
+ )}
+ {...props}>
+ {children}
+
+
+
+
+))
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+
+const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
+const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+))
+SelectContent.displayName = SelectPrimitive.Content.displayName
+
+const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+SelectLabel.displayName = SelectPrimitive.Label.displayName
+
+const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+))
+SelectItem.displayName = SelectPrimitive.Item.displayName
+
+const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
+
+))
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+}
diff --git a/src/layouts/BaseLayouts.jsx b/src/layouts/BaseLayouts.jsx
index 546ba0f..e6aabbf 100644
--- a/src/layouts/BaseLayouts.jsx
+++ b/src/layouts/BaseLayouts.jsx
@@ -10,15 +10,15 @@ const BaseLayout = () => {
const isRegisterPage = location.pathname === '/register';
const isMapBoxPage = location.pathname === '/mapbox';
const isHelpAdminPage = location.pathname === '/help-admin';
-
+ const isEcoNewsPage = location.pathname === '/eco-news';
return (
- {!isMapBoxPage && !isHelpAdminPage && }
+ {!isMapBoxPage && !isHelpAdminPage && !isEcoNewsPage && }
{!isRegisterPage && !isLoginPage && !isMapBoxPage && } {/* Affiche le Header sauf sur /login et /register */}
{/* Ajustez pt-20 selon la hauteur de votre Header */}
- {!isRegisterPage && !isLoginPage && !isMapBoxPage && !isHelpAdminPage && }
+ {!isRegisterPage && !isLoginPage && !isMapBoxPage && !isHelpAdminPage && !isEcoNewsPage && }
);
};
diff --git a/src/layouts/components/Header.jsx b/src/layouts/components/Header.jsx
index b4b96fe..e52d208 100644
--- a/src/layouts/components/Header.jsx
+++ b/src/layouts/components/Header.jsx
@@ -1,19 +1,17 @@
-import React from 'react';
-import { Link, useNavigate } from 'react-router-dom';
-import { useAuth } from '@/security/auth/AuthContext';
-import { ModeToggle } from '../../hooks/mode-toggle.jsx';
import { Button } from '@/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger
} from "@/components/ui/dropdown-menu";
import Helper from '@/help/Helper.jsx';
+import { useAuth } from '@/security/auth/AuthContext';
import { CircleUserRound } from 'lucide-react';
-
+import { Link, useNavigate } from 'react-router-dom';
+import { ModeToggle } from '../../hooks/mode-toggle.jsx';
const Header = () => {
const { user, logout } = useAuth();
const navigate = useNavigate();
@@ -51,10 +49,13 @@ const Header = () => {
GreenTrip
-
+
{renderUserOptions()}
-
+
+
+
+
{user ? (
@@ -71,7 +72,7 @@ const Header = () => {
) : (!isHelpAdminPage &&
-
+
)}
diff --git a/src/pages/EcoNews.jsx b/src/pages/EcoNews.jsx
new file mode 100644
index 0000000..eb355ab
--- /dev/null
+++ b/src/pages/EcoNews.jsx
@@ -0,0 +1,181 @@
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Skeleton } from "@/components/ui/skeleton";
+import api from "@/security/auth/Api";
+import { useAuth } from '@/security/auth/AuthContext';
+import { useEffect, useState } from "react";
+
+const topics = [
+ { label: "🌿 Écologie", value: "écologie" },
+ { label: "🚴 Voyages durables", value: "voyage durable" },
+ { label: "🌍 Climat", value: "climat" },
+ { label: "🏕️ Tourisme vert", value: "tourisme vert" },
+ { label: "🔋 Énergies renouvelables", value: "énergies renouvelables" },
+ { label: "🛤️ Mobilité douce", value: "mobilité douce" },
+];
+
+function ArticleCard({ article, onFavorite, onView, isAuthenticated, openDialog }) {
+ return (
+
+ {article.image_url && (
+
+ )}
+
+ {article.title}
+
+ {new Date(article.pubDate).toLocaleDateString()}
+
+
+ {article.description?.slice(0, 200)}...
+
+
+
+
+
+
+
+ );
+}
+
+function EcoNews() {
+ const [articles, setArticles] = useState([]);
+ const [favorites, setFavorites] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState("écologie");
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const { user } = useAuth();
+ const isAuthenticated = !!user;
+
+ useEffect(() => {
+ setLoading(true);
+ fetch(
+ `https://newsdata.io/api/1/news?apikey=pub_b409c31e41304c848838bb4047b7c6b9&language=fr&q=${search}`
+ )
+ .then((res) => res.json())
+ .then((data) => {
+ setArticles(data.results || []);
+ setLoading(false);
+ })
+ .catch((err) => {
+ console.error(err);
+ setLoading(false);
+ });
+ }, [search]);
+
+ useEffect(() => {
+ if (!isAuthenticated) return;
+ api.get("/favorites")
+ .then((res) => setFavorites(res.data || []))
+ .catch((err) => console.error("[EcoNews] Erreur chargement favoris:", err));
+ }, [isAuthenticated]);
+
+ const saveToFavorites = (article) => {
+ api.post("/favorites", article)
+ .then(() => {
+ setFavorites((prev) => [...prev, article]);
+ console.log("[EcoNews] Article ajouté aux favoris.");
+ })
+ .catch((err) => console.error("[EcoNews] Erreur lors de l'ajout aux favoris:", err));
+ };
+
+ const trackView = (article) => {
+ api.post("/api/news/view", {
+ title: article.title,
+ url: article.link,
+ })
+ .then(() => window.open(article.link, '_blank'))
+ .catch((err) => console.error("[EcoNews] Erreur tracking:", err));
+ };
+
+ return (
+
+
📰 Actualités écologiques
+
+
+
+
+
+
+
+ {loading ? (
+
+ {[...Array(4)].map((_, i) => (
+
+ ))}
+
+ ) : (
+
+ {articles.map((article, index) => (
+
setDialogOpen(true)}
+ />
+ ))}
+
+ )}
+
+ {isAuthenticated && (
+
+
⭐ Mes favoris
+ {favorites.length === 0 ? (
+
Aucun favori pour le moment.
+ ) : (
+
+ {favorites.map((fav, index) => (
+
{ }}
+ onView={trackView}
+ isAuthenticated={isAuthenticated}
+ openDialog={() => { }}
+ />
+ ))}
+
+ )}
+
+ )}
+
+ );
+}
+
+export default EcoNews;
diff --git a/src/router/routes.jsx b/src/router/routes.jsx
index f5b944c..b3d53e5 100644
--- a/src/router/routes.jsx
+++ b/src/router/routes.jsx
@@ -7,7 +7,7 @@ import Test from '../pages/Test.jsx';
import Login from '../security/Login.jsx';
import Register from '../security/Register.jsx';
import HelpAdmin from '../help/HelpAdmin.jsx'
-
+import EcoNews from '../pages/EcoNews.jsx';
const routes = [
{
path: '/',
@@ -25,6 +25,10 @@ const routes = [
path: '/mapbox',
element: ,
},
+ {
+ path: '/eco-news',
+ element: ,
+ },
{
path: '/login',
element: ,
From 6abba146665743f846a98bf619507906b661f5d3 Mon Sep 17 00:00:00 2001
From: sylvain
Date: Fri, 13 Jun 2025 22:48:31 +0200
Subject: [PATCH 07/12] Ajout de l'empreinte carbone
---
package-lock.json | 175 ++++++++++++++--
package.json | 2 +-
src/layouts/BaseLayouts.jsx | 8 +-
src/layouts/components/Footer.jsx | 27 ++-
src/pages/CarbonCalculator.jsx | 192 ++++++++++++++++++
src/pages/EcoNews.jsx | 319 +++++++++++++++++++++---------
src/router/routes.jsx | 5 +
7 files changed, 601 insertions(+), 127 deletions(-)
create mode 100644 src/pages/CarbonCalculator.jsx
diff --git a/package-lock.json b/package-lock.json
index 0d44ed8..ddfb68b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,7 @@
"@iconify/react": "^5.1.0",
"@mui/material": "^6.1.7",
"@radix-ui/react-avatar": "^1.1.2",
- "@radix-ui/react-collapsible": "^1.1.2",
+ "@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-select": "^2.2.5",
@@ -1287,19 +1287,97 @@
}
},
"node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz",
+ "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.4",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
- "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/primitive": "1.1.1",
- "@radix-ui/react-compose-refs": "1.1.1",
- "@radix-ui/react-context": "1.1.1",
- "@radix-ui/react-id": "1.1.0",
- "@radix-ui/react-presence": "1.1.2",
- "@radix-ui/react-primitive": "2.0.1",
- "@radix-ui/react-use-controllable-state": "1.1.0",
- "@radix-ui/react-use-layout-effect": "1.1.0"
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
+ "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
@@ -1316,6 +1394,81 @@
}
}
},
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
diff --git a/package.json b/package.json
index ac8fd2a..61be21f 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"@iconify/react": "^5.1.0",
"@mui/material": "^6.1.7",
"@radix-ui/react-avatar": "^1.1.2",
- "@radix-ui/react-collapsible": "^1.1.2",
+ "@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-select": "^2.2.5",
diff --git a/src/layouts/BaseLayouts.jsx b/src/layouts/BaseLayouts.jsx
index e6aabbf..ddcbad4 100644
--- a/src/layouts/BaseLayouts.jsx
+++ b/src/layouts/BaseLayouts.jsx
@@ -1,8 +1,7 @@
-import React from 'react';
import { Outlet, useLocation } from 'react-router-dom';
+import Background from './Background.jsx';
import Footer from './components/Footer.jsx';
import Header from './components/Header.jsx';
-import Background from './Background.jsx';
const BaseLayout = () => {
const location = useLocation();
@@ -11,14 +10,15 @@ const BaseLayout = () => {
const isMapBoxPage = location.pathname === '/mapbox';
const isHelpAdminPage = location.pathname === '/help-admin';
const isEcoNewsPage = location.pathname === '/eco-news';
+ const isCarbonPage = location.pathname === '/carbon-footprint';
return (
- {!isMapBoxPage && !isHelpAdminPage && !isEcoNewsPage && }
+ {!isMapBoxPage && !isHelpAdminPage && !isEcoNewsPage && !isCarbonPage && }
{!isRegisterPage && !isLoginPage && !isMapBoxPage && } {/* Affiche le Header sauf sur /login et /register */}
{/* Ajustez pt-20 selon la hauteur de votre Header */}
- {!isRegisterPage && !isLoginPage && !isMapBoxPage && !isHelpAdminPage && !isEcoNewsPage && }
+ {!isRegisterPage && !isLoginPage && !isMapBoxPage && !isHelpAdminPage && }
);
};
diff --git a/src/layouts/components/Footer.jsx b/src/layouts/components/Footer.jsx
index f9f2d8c..a3b497e 100644
--- a/src/layouts/components/Footer.jsx
+++ b/src/layouts/components/Footer.jsx
@@ -1,15 +1,15 @@
// Footer.jsx
-import React, { useState } from 'react';
-import { Link } from 'react-router-dom';
-import { Icon } from '@iconify/react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
- DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
+import { Icon } from '@iconify/react';
+import { useState } from 'react';
+import { Link } from 'react-router-dom';
const Footer = () => {
const [region, setRegion] = useState('France');
@@ -33,25 +33,23 @@ const Footer = () => {
to="/en-fr/about"
className="text-sm md:text-base lg:text-xl leading-7 md:leading-11 lg:leading-13 hover:opacity-70 transition duration-300 text-brand-dark dark:text-gray-200"
>
- A propos
+ À propos
- Blog
+ Empreinte carbone
- Forum
+ Éco-actualités
@@ -59,19 +57,19 @@ const Footer = () => {
{/* Section Get our App */}