diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json
index 74aa707..62d7fb2 100644
--- a/apps/backend/package-lock.json
+++ b/apps/backend/package-lock.json
@@ -25,6 +25,7 @@
"@nestjs/websockets": "^10.4.20",
"@sentry/node": "^10.19.0",
"@sentry/profiling-node": "^10.19.0",
+ "@types/leaflet": "^1.9.21",
"@types/mjml": "^4.7.4",
"@types/nodemailer": "^7.0.2",
"@types/opossum": "^8.1.9",
@@ -40,6 +41,7 @@
"helmet": "^7.2.0",
"ioredis": "^5.8.1",
"joi": "^17.11.0",
+ "leaflet": "^1.9.4",
"mjml": "^4.16.1",
"nestjs-pino": "^4.4.1",
"nodemailer": "^7.0.9",
@@ -53,6 +55,7 @@
"pino": "^8.17.1",
"pino-http": "^8.6.0",
"pino-pretty": "^10.3.0",
+ "react-leaflet": "^5.0.0",
"reflect-metadata": "^0.1.14",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
@@ -3827,6 +3830,17 @@
"@opentelemetry/api": "^1.3.0"
}
},
+ "node_modules/@react-leaflet/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
"node_modules/@sentry-internal/node-cpu-profiler": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz",
@@ -4965,6 +4979,12 @@
"@types/send": "*"
}
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -5047,6 +5067,15 @@
"@types/node": "*"
}
},
+ "node_modules/@types/leaflet": {
+ "version": "1.9.21",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
+ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/methods": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -11135,6 +11164,12 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -13387,6 +13422,29 @@
"node": ">= 0.8"
}
},
+ "node_modules/react": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
+ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
+ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.1"
+ }
+ },
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -13394,6 +13452,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-leaflet": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
+ "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^3.0.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -13815,6 +13887,13 @@
"node": ">=10"
}
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
diff --git a/apps/backend/package.json b/apps/backend/package.json
index e2c5fb8..41b911f 100644
--- a/apps/backend/package.json
+++ b/apps/backend/package.json
@@ -41,6 +41,7 @@
"@nestjs/websockets": "^10.4.20",
"@sentry/node": "^10.19.0",
"@sentry/profiling-node": "^10.19.0",
+ "@types/leaflet": "^1.9.21",
"@types/mjml": "^4.7.4",
"@types/nodemailer": "^7.0.2",
"@types/opossum": "^8.1.9",
@@ -56,6 +57,7 @@
"helmet": "^7.2.0",
"ioredis": "^5.8.1",
"joi": "^17.11.0",
+ "leaflet": "^1.9.4",
"mjml": "^4.16.1",
"nestjs-pino": "^4.4.1",
"nodemailer": "^7.0.9",
@@ -69,6 +71,7 @@
"pino": "^8.17.1",
"pino-http": "^8.6.0",
"pino-pretty": "^10.3.0",
+ "react-leaflet": "^5.0.0",
"reflect-metadata": "^0.1.14",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1",
diff --git a/apps/frontend/app/dashboard/search-advanced/page.tsx b/apps/frontend/app/dashboard/search-advanced/page.tsx
index 8c4fcb3..53be855 100644
--- a/apps/frontend/app/dashboard/search-advanced/page.tsx
+++ b/apps/frontend/app/dashboard/search-advanced/page.tsx
@@ -6,10 +6,17 @@
'use client';
-import { useState } from 'react';
+import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';
import { searchPorts, Port } from '@/lib/api/ports';
+import dynamic from 'next/dynamic';
+
+// Import dynamique pour éviter les erreurs SSR avec Leaflet
+const PortRouteMap = dynamic(() => import('@/components/PortRouteMap'), {
+ ssr: false,
+ loading: () =>
Chargement de la carte...
,
+});
interface Package {
type: 'caisse' | 'colis' | 'palette' | 'autre';
@@ -85,6 +92,8 @@ export default function AdvancedSearchPage() {
const [destinationSearch, setDestinationSearch] = useState('');
const [showOriginDropdown, setShowOriginDropdown] = useState(false);
const [showDestinationDropdown, setShowDestinationDropdown] = useState(false);
+ const [selectedOriginPort, setSelectedOriginPort] = useState(null);
+ const [selectedDestinationPort, setSelectedDestinationPort] = useState(null);
// Port autocomplete queries
const { data: originPortsData } = useQuery({
@@ -209,6 +218,7 @@ export default function AdvancedSearchPage() {
onClick={() => {
setSearchForm({ ...searchForm, origin: port.code });
setOriginSearch(port.displayName);
+ setSelectedOriginPort(port);
setShowOriginDropdown(false);
}}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-100 last:border-b-0"
@@ -253,6 +263,7 @@ export default function AdvancedSearchPage() {
onClick={() => {
setSearchForm({ ...searchForm, destination: port.code });
setDestinationSearch(port.displayName);
+ setSelectedDestinationPort(port);
setShowDestinationDropdown(false);
}}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-100 last:border-b-0"
@@ -267,6 +278,31 @@ export default function AdvancedSearchPage() {
)}
+
+ {/* Carte interactive de la route maritime */}
+ {selectedOriginPort && selectedDestinationPort && (
+
+
+
+ Route maritime : {selectedOriginPort.name} → {selectedDestinationPort.name}
+
+
+ Distance approximative et visualisation de la route
+
+
+
+
+ )}
);
diff --git a/apps/frontend/app/demo-carte/page.tsx b/apps/frontend/app/demo-carte/page.tsx
new file mode 100644
index 0000000..0916d15
--- /dev/null
+++ b/apps/frontend/app/demo-carte/page.tsx
@@ -0,0 +1,72 @@
+"use client";
+
+import dynamic from 'next/dynamic';
+
+// Import dynamique pour éviter les erreurs SSR avec Leaflet
+const PortRouteMap = dynamic(() => import('@/components/PortRouteMap'), {
+ ssr: false,
+ loading: () => (
+
+
+
+
Chargement de la carte...
+
+
+ ),
+});
+
+export default function DemoPage() {
+ const portA = { lat: 43.2965, lng: 5.3698 }; // Marseille
+ const portB = { lat: 41.3851, lng: 2.1734 }; // Barcelone
+
+ return (
+
+
+
+
Démo Carte Maritime
+
+ Visualisation de la route entre Marseille et Barcelone
+
+
+
+
+
+
+ Route: Port de Marseille → Port de Barcelone
+
+
+ Distance approximative: ~350 km par la mer
+
+
+
+
+
+
+
+
+
📍 Port d'origine
+
Marseille, France
+
Lat: {portA.lat}, Lng: {portA.lng}
+
+
+
📍 Port de destination
+
Barcelone, Espagne
+
Lat: {portB.lat}, Lng: {portB.lng}
+
+
+
+
+
+
+
ℹ️ Informations
+
+ - • Carte interactive OpenStreetMap
+ - • Marqueurs positionnés sur les ports
+ - • Ligne bleue représentant la route maritime
+ - • Zoom et navigation disponibles
+
+
+
+
+ );
+}
diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json
index ab3d9be..2c862b3 100644
--- a/apps/frontend/package-lock.json
+++ b/apps/frontend/package-lock.json
@@ -18,17 +18,20 @@
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
+ "@types/leaflet": "^1.9.21",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
"file-saver": "^2.0.5",
"framer-motion": "^12.23.24",
+ "leaflet": "^1.9.4",
"lucide-react": "^0.294.0",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.64.0",
+ "react-leaflet": "^4.2.1",
"recharts": "^3.2.1",
"tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7",
@@ -2317,6 +2320,17 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT"
},
+ "node_modules/@react-leaflet/core": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz",
+ "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/@reduxjs/toolkit": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
@@ -2710,6 +2724,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -2766,6 +2786,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/leaflet": {
+ "version": "1.9.21",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
+ "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
@@ -8228,6 +8257,12 @@
"node": ">=0.10"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -9552,6 +9587,20 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT"
},
+ "node_modules/react-leaflet": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
+ "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^2.1.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index d14ac53..4aa2e93 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -24,17 +24,20 @@
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
+ "@types/leaflet": "^1.9.21",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
"file-saver": "^2.0.5",
"framer-motion": "^12.23.24",
+ "leaflet": "^1.9.4",
"lucide-react": "^0.294.0",
"next": "14.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.64.0",
+ "react-leaflet": "^4.2.1",
"recharts": "^3.2.1",
"tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7",
diff --git a/apps/frontend/src/components/PortRouteMap.tsx b/apps/frontend/src/components/PortRouteMap.tsx
new file mode 100644
index 0000000..ea2654f
--- /dev/null
+++ b/apps/frontend/src/components/PortRouteMap.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import { MapContainer, TileLayer, Polyline, Marker } from "react-leaflet";
+import "leaflet/dist/leaflet.css";
+import L from "leaflet";
+
+interface PortRouteMapProps {
+ portA: { lat: number; lng: number };
+ portB: { lat: number; lng: number };
+ height?: string;
+}
+
+// Fix Leaflet marker icons
+const DefaultIcon = L.icon({
+ iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
+ shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
+ iconSize: [25, 41],
+ iconAnchor: [12, 41],
+});
+L.Marker.prototype.options.icon = DefaultIcon;
+
+export default function PortRouteMap({ portA, portB, height = "500px" }: PortRouteMapProps) {
+ const center = {
+ lat: (portA.lat + portB.lat) / 2,
+ lng: (portA.lng + portB.lng) / 2,
+ };
+
+ const positions: [number, number][] = [
+ [portA.lat, portA.lng],
+ [portB.lat, portB.lng],
+ ];
+
+ return (
+
+ );
+}