fix carte

This commit is contained in:
David 2025-12-03 22:24:48 +01:00
parent 7fc43444a9
commit 55e44ab21c
7 changed files with 296 additions and 1 deletions

View File

@ -25,6 +25,7 @@
"@nestjs/websockets": "^10.4.20", "@nestjs/websockets": "^10.4.20",
"@sentry/node": "^10.19.0", "@sentry/node": "^10.19.0",
"@sentry/profiling-node": "^10.19.0", "@sentry/profiling-node": "^10.19.0",
"@types/leaflet": "^1.9.21",
"@types/mjml": "^4.7.4", "@types/mjml": "^4.7.4",
"@types/nodemailer": "^7.0.2", "@types/nodemailer": "^7.0.2",
"@types/opossum": "^8.1.9", "@types/opossum": "^8.1.9",
@ -40,6 +41,7 @@
"helmet": "^7.2.0", "helmet": "^7.2.0",
"ioredis": "^5.8.1", "ioredis": "^5.8.1",
"joi": "^17.11.0", "joi": "^17.11.0",
"leaflet": "^1.9.4",
"mjml": "^4.16.1", "mjml": "^4.16.1",
"nestjs-pino": "^4.4.1", "nestjs-pino": "^4.4.1",
"nodemailer": "^7.0.9", "nodemailer": "^7.0.9",
@ -53,6 +55,7 @@
"pino": "^8.17.1", "pino": "^8.17.1",
"pino-http": "^8.6.0", "pino-http": "^8.6.0",
"pino-pretty": "^10.3.0", "pino-pretty": "^10.3.0",
"react-leaflet": "^5.0.0",
"reflect-metadata": "^0.1.14", "reflect-metadata": "^0.1.14",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
@ -3827,6 +3830,17 @@
"@opentelemetry/api": "^1.3.0" "@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": { "node_modules/@sentry-internal/node-cpu-profiler": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz",
@ -4965,6 +4979,12 @@
"@types/send": "*" "@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": { "node_modules/@types/graceful-fs": {
"version": "4.1.9", "version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -5047,6 +5067,15 @@
"@types/node": "*" "@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": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -11135,6 +11164,12 @@
"safe-buffer": "~5.1.0" "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": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -13387,6 +13422,29 @@
"node": ">= 0.8" "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": { "node_modules/react-is": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -13394,6 +13452,20 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@ -13815,6 +13887,13 @@
"node": ">=10" "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": { "node_modules/schema-utils": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",

View File

@ -41,6 +41,7 @@
"@nestjs/websockets": "^10.4.20", "@nestjs/websockets": "^10.4.20",
"@sentry/node": "^10.19.0", "@sentry/node": "^10.19.0",
"@sentry/profiling-node": "^10.19.0", "@sentry/profiling-node": "^10.19.0",
"@types/leaflet": "^1.9.21",
"@types/mjml": "^4.7.4", "@types/mjml": "^4.7.4",
"@types/nodemailer": "^7.0.2", "@types/nodemailer": "^7.0.2",
"@types/opossum": "^8.1.9", "@types/opossum": "^8.1.9",
@ -56,6 +57,7 @@
"helmet": "^7.2.0", "helmet": "^7.2.0",
"ioredis": "^5.8.1", "ioredis": "^5.8.1",
"joi": "^17.11.0", "joi": "^17.11.0",
"leaflet": "^1.9.4",
"mjml": "^4.16.1", "mjml": "^4.16.1",
"nestjs-pino": "^4.4.1", "nestjs-pino": "^4.4.1",
"nodemailer": "^7.0.9", "nodemailer": "^7.0.9",
@ -69,6 +71,7 @@
"pino": "^8.17.1", "pino": "^8.17.1",
"pino-http": "^8.6.0", "pino-http": "^8.6.0",
"pino-pretty": "^10.3.0", "pino-pretty": "^10.3.0",
"react-leaflet": "^5.0.0",
"reflect-metadata": "^0.1.14", "reflect-metadata": "^0.1.14",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",

View File

@ -6,10 +6,17 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { searchPorts, Port } from '@/lib/api/ports'; 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: () => <div className="h-80 bg-gray-100 rounded-lg flex items-center justify-center">Chargement de la carte...</div>,
});
interface Package { interface Package {
type: 'caisse' | 'colis' | 'palette' | 'autre'; type: 'caisse' | 'colis' | 'palette' | 'autre';
@ -85,6 +92,8 @@ export default function AdvancedSearchPage() {
const [destinationSearch, setDestinationSearch] = useState(''); const [destinationSearch, setDestinationSearch] = useState('');
const [showOriginDropdown, setShowOriginDropdown] = useState(false); const [showOriginDropdown, setShowOriginDropdown] = useState(false);
const [showDestinationDropdown, setShowDestinationDropdown] = useState(false); const [showDestinationDropdown, setShowDestinationDropdown] = useState(false);
const [selectedOriginPort, setSelectedOriginPort] = useState<Port | null>(null);
const [selectedDestinationPort, setSelectedDestinationPort] = useState<Port | null>(null);
// Port autocomplete queries // Port autocomplete queries
const { data: originPortsData } = useQuery({ const { data: originPortsData } = useQuery({
@ -209,6 +218,7 @@ export default function AdvancedSearchPage() {
onClick={() => { onClick={() => {
setSearchForm({ ...searchForm, origin: port.code }); setSearchForm({ ...searchForm, origin: port.code });
setOriginSearch(port.displayName); setOriginSearch(port.displayName);
setSelectedOriginPort(port);
setShowOriginDropdown(false); setShowOriginDropdown(false);
}} }}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-100 last:border-b-0" 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={() => { onClick={() => {
setSearchForm({ ...searchForm, destination: port.code }); setSearchForm({ ...searchForm, destination: port.code });
setDestinationSearch(port.displayName); setDestinationSearch(port.displayName);
setSelectedDestinationPort(port);
setShowDestinationDropdown(false); setShowDestinationDropdown(false);
}} }}
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-100 last:border-b-0" 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() {
)} )}
</div> </div>
</div> </div>
{/* Carte interactive de la route maritime */}
{selectedOriginPort && selectedDestinationPort && (
<div className="mt-6 border border-gray-200 rounded-lg overflow-hidden">
<div className="bg-gray-50 px-4 py-3 border-b border-gray-200">
<h3 className="text-sm font-semibold text-gray-900">
Route maritime : {selectedOriginPort.name} {selectedDestinationPort.name}
</h3>
<p className="text-xs text-gray-500 mt-1">
Distance approximative et visualisation de la route
</p>
</div>
<PortRouteMap
portA={{
lat: selectedOriginPort.coordinates.latitude,
lng: selectedOriginPort.coordinates.longitude,
}}
portB={{
lat: selectedDestinationPort.coordinates.latitude,
lng: selectedDestinationPort.coordinates.longitude,
}}
height="400px"
/>
</div>
)}
</div> </div>
); );

View File

@ -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: () => (
<div className="h-96 bg-gray-100 rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Chargement de la carte...</p>
</div>
</div>
),
});
export default function DemoPage() {
const portA = { lat: 43.2965, lng: 5.3698 }; // Marseille
const portB = { lat: 41.3851, lng: 2.1734 }; // Barcelone
return (
<div className="w-full min-h-screen p-8 bg-gray-50">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Démo Carte Maritime</h1>
<p className="text-gray-600">
Visualisation de la route entre Marseille et Barcelone
</p>
</div>
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
<div className="bg-blue-600 px-6 py-4">
<h2 className="text-white text-lg font-semibold">
Route: Port de Marseille Port de Barcelone
</h2>
<p className="text-blue-100 text-sm mt-1">
Distance approximative: ~350 km par la mer
</p>
</div>
<PortRouteMap portA={portA} portB={portB} height="600px" />
<div className="px-6 py-4 bg-gray-50 border-t">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<h3 className="font-semibold text-gray-900 mb-2">📍 Port d'origine</h3>
<p className="text-gray-600">Marseille, France</p>
<p className="text-gray-500 text-xs">Lat: {portA.lat}, Lng: {portA.lng}</p>
</div>
<div>
<h3 className="font-semibold text-gray-900 mb-2">📍 Port de destination</h3>
<p className="text-gray-600">Barcelone, Espagne</p>
<p className="text-gray-500 text-xs">Lat: {portB.lat}, Lng: {portB.lng}</p>
</div>
</div>
</div>
</div>
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="text-blue-900 font-semibold mb-2"> Informations</h3>
<ul className="text-blue-800 text-sm space-y-1">
<li> Carte interactive OpenStreetMap</li>
<li> Marqueurs positionnés sur les ports</li>
<li> Ligne bleue représentant la route maritime</li>
<li> Zoom et navigation disponibles</li>
</ul>
</div>
</div>
</div>
);
}

View File

@ -18,17 +18,20 @@
"@tanstack/react-query": "^5.90.2", "@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12", "@tanstack/react-virtual": "^3.13.12",
"@types/leaflet": "^1.9.21",
"axios": "^1.12.2", "axios": "^1.12.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"leaflet": "^1.9.4",
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"next": "14.0.4", "next": "14.0.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.64.0", "react-hook-form": "^7.64.0",
"react-leaflet": "^4.2.1",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^2.1.0", "tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@ -2317,6 +2320,17 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT" "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": { "node_modules/@reduxjs/toolkit": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
@ -2710,6 +2724,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/graceful-fs": {
"version": "4.1.9", "version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@ -2766,6 +2786,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "20.19.19", "version": "20.19.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.19.tgz",
@ -8228,6 +8257,12 @@
"node": ">=0.10" "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": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -9552,6 +9587,20 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT" "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": { "node_modules/react-redux": {
"version": "9.2.0", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",

View File

@ -24,17 +24,20 @@
"@tanstack/react-query": "^5.90.2", "@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12", "@tanstack/react-virtual": "^3.13.12",
"@types/leaflet": "^1.9.21",
"axios": "^1.12.2", "axios": "^1.12.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
"leaflet": "^1.9.4",
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"next": "14.0.4", "next": "14.0.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.64.0", "react-hook-form": "^7.64.0",
"react-leaflet": "^4.2.1",
"recharts": "^3.2.1", "recharts": "^3.2.1",
"tailwind-merge": "^2.1.0", "tailwind-merge": "^2.1.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

@ -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 (
<div style={{ height }}>
<MapContainer
center={[center.lat, center.lng]}
zoom={4}
style={{ height: "100%", width: "100%" }}
scrollWheelZoom={false}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<Marker position={[portA.lat, portA.lng]} />
<Marker position={[portB.lat, portB.lng]} />
<Polyline positions={positions} pathOptions={{ color: "#2563eb", weight: 3, opacity: 0.7 }} />
</MapContainer>
</div>
);
}