@navigoo/map-components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +0 -0
  2. package/dist/components/Dashboard.d.ts +14 -0
  3. package/dist/components/Dashboard.js +52 -0
  4. package/dist/components/DetourRouteSearch.d.ts +10 -0
  5. package/dist/components/DetourRouteSearch.js +119 -0
  6. package/dist/components/MapView.d.ts +14 -0
  7. package/dist/components/MapView.js +179 -0
  8. package/dist/components/RouteSearch.d.ts +10 -0
  9. package/dist/components/RouteSearch.js +95 -0
  10. package/dist/components/SearchBar.d.ts +10 -0
  11. package/dist/components/SearchBar.js +48 -0
  12. package/dist/components/TransportOptions.d.ts +3 -0
  13. package/dist/components/TransportOptions.js +24 -0
  14. package/dist/components/TripType.d.ts +3 -0
  15. package/dist/components/TripType.js +20 -0
  16. package/dist/index.d.ts +10 -0
  17. package/dist/index.js +39 -0
  18. package/dist/lib/api.d.ts +21 -0
  19. package/dist/lib/api.js +58 -0
  20. package/dist/lib/geolocalisation.d.ts +9 -0
  21. package/dist/lib/geolocalisation.js +30 -0
  22. package/dist/lib/type.d.ts +32 -0
  23. package/dist/lib/type.js +2 -0
  24. package/package.json +31 -0
  25. package/src/components/Dashboard.tsx +183 -0
  26. package/src/components/DetourRouteSearch.tsx +217 -0
  27. package/src/components/MapView.tsx +201 -0
  28. package/src/components/RouteSearch.tsx +163 -0
  29. package/src/components/SearchBar.tsx +87 -0
  30. package/src/components/TransportOptions.tsx +53 -0
  31. package/src/components/TripType.tsx +47 -0
  32. package/src/components/styles.modules.css +207 -0
  33. package/src/components/wellknown.d.ts +1 -0
  34. package/src/css-module.d.ts +4 -0
  35. package/src/index.ts +10 -0
  36. package/src/lib/api.ts +74 -0
  37. package/src/lib/geolocalisation.ts +41 -0
  38. package/src/lib/type.ts +37 -0
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.getCurrentPosition = exports.ApiClient = exports.TripType = exports.TransportOptions = exports.Dashboard = exports.MapView = exports.DetourRouteSearch = exports.RouteSearch = exports.SearchBar = void 0;
21
+ var SearchBar_1 = require("./components/SearchBar");
22
+ Object.defineProperty(exports, "SearchBar", { enumerable: true, get: function () { return __importDefault(SearchBar_1).default; } });
23
+ var RouteSearch_1 = require("./components/RouteSearch");
24
+ Object.defineProperty(exports, "RouteSearch", { enumerable: true, get: function () { return __importDefault(RouteSearch_1).default; } });
25
+ var DetourRouteSearch_1 = require("./components/DetourRouteSearch");
26
+ Object.defineProperty(exports, "DetourRouteSearch", { enumerable: true, get: function () { return __importDefault(DetourRouteSearch_1).default; } });
27
+ var MapView_1 = require("./components/MapView");
28
+ Object.defineProperty(exports, "MapView", { enumerable: true, get: function () { return __importDefault(MapView_1).default; } });
29
+ var Dashboard_1 = require("./components/Dashboard");
30
+ Object.defineProperty(exports, "Dashboard", { enumerable: true, get: function () { return __importDefault(Dashboard_1).default; } });
31
+ var TransportOptions_1 = require("./components/TransportOptions");
32
+ Object.defineProperty(exports, "TransportOptions", { enumerable: true, get: function () { return __importDefault(TransportOptions_1).default; } });
33
+ var TripType_1 = require("./components/TripType");
34
+ Object.defineProperty(exports, "TripType", { enumerable: true, get: function () { return __importDefault(TripType_1).default; } });
35
+ var api_1 = require("./lib/api");
36
+ Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return api_1.ApiClient; } });
37
+ var geolocalisation_1 = require("./lib/geolocalisation");
38
+ Object.defineProperty(exports, "getCurrentPosition", { enumerable: true, get: function () { return geolocalisation_1.getCurrentPosition; } });
39
+ __exportStar(require("./lib/type"), exports);
@@ -0,0 +1,21 @@
1
+ import { Place, Route } from './type';
2
+ export declare class ApiClient {
3
+ private baseUrl;
4
+ constructor(baseUrl: string);
5
+ searchPlaces(name: string): Promise<Place[]>;
6
+ findClosestPlace(lat: number, lng: number): Promise<Place | null>;
7
+ calculateRoute(points: {
8
+ lat: number;
9
+ lng: number;
10
+ }[], mode: 'driving' | 'walking' | 'cycling', startPlaceName: string, endPlaceName: string): Promise<Route[]>;
11
+ calculateRouteWithDetour(start: {
12
+ lat: number;
13
+ lng: number;
14
+ }, detour: {
15
+ lat: number;
16
+ lng: number;
17
+ }, end: {
18
+ lat: number;
19
+ lng: number;
20
+ }, mode: string, startPlaceName: string, detourPlaceName: string, endPlaceName: string): Promise<Route[]>;
21
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ApiClient = void 0;
4
+ class ApiClient {
5
+ baseUrl;
6
+ constructor(baseUrl) {
7
+ this.baseUrl = baseUrl;
8
+ }
9
+ async searchPlaces(name) {
10
+ const response = await fetch(`${this.baseUrl}/api/places?name=${encodeURIComponent(name)}`);
11
+ const data = await response.json();
12
+ if (response.ok && data.success) {
13
+ return data.data.filter((place) => place.coordinates !== null);
14
+ }
15
+ throw new Error(data.error || 'Failed to search places');
16
+ }
17
+ async findClosestPlace(lat, lng) {
18
+ const response = await fetch(`${this.baseUrl}/api/places/closest?lat=${lat}&lng=${lng}`);
19
+ const data = await response.json();
20
+ if (response.ok && data.success) {
21
+ return data.data;
22
+ }
23
+ return null;
24
+ }
25
+ async calculateRoute(points, mode, startPlaceName, endPlaceName) {
26
+ const response = await fetch(`${this.baseUrl}/api/routes`, {
27
+ method: 'POST',
28
+ headers: { 'Content-Type': 'application/json' },
29
+ body: JSON.stringify({ points, mode, startPlaceName, endPlaceName }),
30
+ });
31
+ const data = await response.json();
32
+ if (response.ok && data.routes) {
33
+ return data.routes;
34
+ }
35
+ throw new Error(data.error || 'Failed to calculate route');
36
+ }
37
+ async calculateRouteWithDetour(start, detour, end, mode, startPlaceName, detourPlaceName, endPlaceName) {
38
+ const response = await fetch(`${this.baseUrl}/api/routes/with-detour`, {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({
42
+ start,
43
+ detour,
44
+ end,
45
+ transportMode: mode,
46
+ startPlaceName,
47
+ detourPlaceName,
48
+ endPlaceName,
49
+ }),
50
+ });
51
+ const data = await response.json();
52
+ if (response.ok && data.routes) {
53
+ return data.routes;
54
+ }
55
+ throw new Error(data.error || 'Failed to calculate route with detour');
56
+ }
57
+ }
58
+ exports.ApiClient = ApiClient;
@@ -0,0 +1,9 @@
1
+ export interface GeolocationResult {
2
+ latitude: number;
3
+ longitude: number;
4
+ }
5
+ export interface GeolocationError {
6
+ code: number;
7
+ message: string;
8
+ }
9
+ export declare const getCurrentPosition: () => Promise<GeolocationResult>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCurrentPosition = void 0;
4
+ const getCurrentPosition = () => {
5
+ return new Promise((resolve, reject) => {
6
+ if (!navigator.geolocation) {
7
+ reject({
8
+ code: -1,
9
+ message: 'Geolocation is not supported by this browser.',
10
+ });
11
+ return;
12
+ }
13
+ navigator.geolocation.getCurrentPosition((position) => {
14
+ resolve({
15
+ latitude: position.coords.latitude,
16
+ longitude: position.coords.longitude,
17
+ });
18
+ }, (error) => {
19
+ reject({
20
+ code: error.code,
21
+ message: error.message,
22
+ });
23
+ }, {
24
+ enableHighAccuracy: true,
25
+ timeout: 10000,
26
+ maximumAge: 0,
27
+ });
28
+ });
29
+ };
30
+ exports.getCurrentPosition = getCurrentPosition;
@@ -0,0 +1,32 @@
1
+ export interface Coordinates {
2
+ lat: number;
3
+ lng: number;
4
+ }
5
+ export interface Place {
6
+ id: string | number;
7
+ name: string;
8
+ coordinates: Coordinates | null;
9
+ }
10
+ export interface RouteStep {
11
+ geometry: string;
12
+ source: string;
13
+ target: string;
14
+ distance: number;
15
+ duration: number;
16
+ }
17
+ export interface Route {
18
+ distance: number;
19
+ duration: number;
20
+ steps: RouteStep[];
21
+ startPlaceName?: string;
22
+ endPlaceName?: string;
23
+ geometry: string;
24
+ }
25
+ export interface GeolocationResult {
26
+ latitude: number;
27
+ longitude: number;
28
+ }
29
+ export interface GeolocationError {
30
+ code: number;
31
+ message: string;
32
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@navigoo/map-components",
3
+ "version": "1.0.0",
4
+ "description": "Reusable React components for mapping and routing in Yaoundé",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublish": "npm run build"
10
+ },
11
+ "dependencies": {
12
+ "leaflet": "^1.9.4",
13
+ "wellknown": "^0.5.0",
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0",
16
+ "lucide-react": "^0.263.0"
17
+ },
18
+ "devDependencies": {
19
+ "typescript": "^5.0.0",
20
+ "@types/leaflet": "^1.9.4",
21
+ "@types/react": "^18.2.0",
22
+ "@types/react-dom": "^18.2.0"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "publishConfig": {
29
+ "registry": "https://registry.npmjs.org/"
30
+ }
31
+ }
@@ -0,0 +1,183 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { GeolocationResult, Place, Route } from '../lib/type';
3
+ import { getCurrentPosition } from '../lib/geolocalisation';
4
+ import SearchBar from './SearchBar';
5
+ import RouteSearch from './RouteSearch';
6
+ import DetourRouteSearch from './DetourRouteSearch';
7
+ import TransportOptions from './TransportOptions';
8
+ import TripType from './TripType';
9
+ import styles from './styles.module.css';
10
+ import { Footprints, Search, ArrowRight, RefreshCcw, MapPin } from 'lucide-react';
11
+ import { ApiClient } from '../lib/api';
12
+
13
+ interface DashboardProps {
14
+ apiClient: ApiClient;
15
+ setUserLocation: (location: GeolocationResult | null) => void;
16
+ setSearchedPlace: (place: Place | null) => void;
17
+ setIsTracking: (tracking: boolean) => void;
18
+ setRoutes: (routes: Route[]) => void;
19
+ setSelectedRouteIndex: (index: number) => void;
20
+ isTracking: boolean;
21
+ }
22
+
23
+ const Dashboard: React.FC<DashboardProps> = ({
24
+ apiClient,
25
+ setUserLocation,
26
+ setSearchedPlace,
27
+ setIsTracking,
28
+ setRoutes,
29
+ setSelectedRouteIndex,
30
+ isTracking,
31
+ }) => {
32
+ const [activeSection, setActiveSection] = useState<string | null>(null);
33
+ const [geoLoading, setGeoLoading] = useState(false);
34
+ const [geoError, setGeoError] = useState<string | null>(null);
35
+
36
+ const toggleSection = (section: string) => {
37
+ setActiveSection(activeSection === section ? null : section);
38
+ };
39
+
40
+ const handleToggleTracking = () => {
41
+ setIsTracking(!isTracking);
42
+ if (isTracking) {
43
+ setUserLocation(null);
44
+ setRoutes([]);
45
+ setSelectedRouteIndex(0);
46
+ }
47
+ };
48
+
49
+ const handleGeolocation = useCallback(async () => {
50
+ if (geoLoading || isTracking) return;
51
+ setGeoLoading(true);
52
+ setGeoError(null);
53
+
54
+ try {
55
+ const position: GeolocationResult = await getCurrentPosition();
56
+ setUserLocation(position);
57
+ setGeoError(null);
58
+ setRoutes([]);
59
+ setSelectedRouteIndex(0);
60
+ } catch {
61
+ setGeoError('Impossible d\'obtenir votre position');
62
+ } finally {
63
+ setGeoLoading(false);
64
+ }
65
+ }, [geoLoading, isTracking, setUserLocation, setRoutes, setSelectedRouteIndex]);
66
+
67
+ return (
68
+ <div>
69
+ <div className={styles.header}>
70
+ <h1 className={styles.headerTitle}>
71
+ <span className={styles.titleAccent}>Navi</span>goo
72
+ </h1>
73
+ </div>
74
+ <div className={styles.mainContainer}>
75
+ <div className={styles.sidebar}>
76
+ <button
77
+ onClick={() => toggleSection('geolocation')}
78
+ className={`${styles.sidebarButton} ${activeSection === 'geolocation' ? styles.active : ''}`}
79
+ title="Géolocalisation et suivi"
80
+ >
81
+ <Footprints size={24} color="#000" />
82
+ </button>
83
+ <button
84
+ onClick={() => toggleSection('search')}
85
+ className={`${styles.sidebarButton} ${activeSection === 'search' ? styles.active : ''}`}
86
+ title="Recherche de lieu"
87
+ >
88
+ <Search size={24} color="#000" />
89
+ </button>
90
+ <button
91
+ onClick={() => toggleSection('route')}
92
+ className={`${styles.sidebarButton} ${activeSection === 'route' ? styles.active : ''}`}
93
+ title="Tracé d'itinéraire"
94
+ >
95
+ <ArrowRight size={24} color="#000" />
96
+ </button>
97
+ <button
98
+ onClick={() => toggleSection('routeWithDetour')}
99
+ className={`${styles.sidebarButton} ${activeSection === 'routeWithDetour' ? styles.active : ''}`}
100
+ title="Tracé d'itinéraire avec détour"
101
+ >
102
+ <RefreshCcw size={24} color="#000" />
103
+ </button>
104
+ </div>
105
+ <div className={`${styles.contentPanel} ${activeSection ? styles.open : ''}`}>
106
+ {activeSection === 'geolocation' && (
107
+ <div className={styles.sectionContainer}>
108
+ <h2 className={styles.sectionTitle}>
109
+ <span className={styles.titleAccent}>Géo</span>localisation
110
+ </h2>
111
+ <div className={styles.toggleContainer}>
112
+ <label className={styles.switch}>
113
+ <input type="checkbox" checked={isTracking} onChange={handleToggleTracking} />
114
+ <span className={styles.slider}></span>
115
+ </label>
116
+ <span className={styles.toggleLabel}>
117
+ Suivi: {isTracking ? 'Activé' : 'Désactivé'}
118
+ </span>
119
+ </div>
120
+ <button
121
+ onClick={handleGeolocation}
122
+ className={styles.locationButton}
123
+ disabled={geoLoading || isTracking}
124
+ >
125
+ {geoLoading ? (
126
+ 'Chargement...'
127
+ ) : (
128
+ <>
129
+ <MapPin size={18} color="#000" style={{ marginRight: '8px' }} />
130
+ Ma position
131
+ </>
132
+ )}
133
+ </button>
134
+ {geoError && <div className={styles.error}>{geoError}</div>}
135
+ </div>
136
+ )}
137
+ {activeSection === 'search' && (
138
+ <div className={styles.sectionContainer}>
139
+ <h2 className={styles.sectionTitle}>
140
+ <span className={styles.titleAccent}>Re</span>cherche
141
+ </h2>
142
+ <SearchBar
143
+ apiClient={apiClient}
144
+ setUserLocation={setUserLocation}
145
+ setSearchedPlace={setSearchedPlace}
146
+ />
147
+ </div>
148
+ )}
149
+ {activeSection === 'route' && (
150
+ <div className={styles.sectionContainer}>
151
+ <h2 className={styles.sectionTitle}>
152
+ <span className={styles.titleAccent}>Itiné</span>raire
153
+ </h2>
154
+ <RouteSearch
155
+ apiClient={apiClient}
156
+ setRoutes={setRoutes}
157
+ setSelectedRouteIndex={setSelectedRouteIndex}
158
+ />
159
+ <TransportOptions />
160
+ <TripType />
161
+ </div>
162
+ )}
163
+ {activeSection === 'routeWithDetour' && (
164
+ <div className={styles.sectionContainer}>
165
+ <h2 className={styles.sectionTitle}>
166
+ <span className={styles.titleAccent}>Itiné</span>raire avec détour
167
+ </h2>
168
+ <DetourRouteSearch
169
+ apiClient={apiClient}
170
+ setRoutes={setRoutes}
171
+ setSelectedRouteIndex={setSelectedRouteIndex}
172
+ />
173
+ <TransportOptions />
174
+ <TripType />
175
+ </div>
176
+ )}
177
+ </div>
178
+ </div>
179
+ </div>
180
+ );
181
+ };
182
+
183
+ export default Dashboard;
@@ -0,0 +1,217 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Place, Route } from '../lib/type';
3
+ import { ApiClient } from '../lib/api';
4
+ import styles from './styles.module.css';
5
+
6
+ interface DetourRouteSearchProps {
7
+ apiClient: ApiClient;
8
+ setRoutes: (routes: Route[]) => void;
9
+ setSelectedRouteIndex: (index: number) => void;
10
+ }
11
+
12
+ const DetourRouteSearch: React.FC<DetourRouteSearchProps> = ({ apiClient, setRoutes, setSelectedRouteIndex }) => {
13
+ const [startQuery, setStartQuery] = useState('');
14
+ const [detourQuery, setDetourQuery] = useState('');
15
+ const [endQuery, setEndQuery] = useState('');
16
+ const [startResults, setStartResults] = useState<Place[]>([]);
17
+ const [detourResults, setDetourResults] = useState<Place[]>([]);
18
+ const [endResults, setEndResults] = useState<Place[]>([]);
19
+ const [selectedStart, setSelectedStart] = useState<Place | null>(null);
20
+ const [selectedDetour, setSelectedDetour] = useState<Place | null>(null);
21
+ const [selectedEnd, setSelectedEnd] = useState<Place | null>(null);
22
+ const [loading, setLoading] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+ const [transportMode, setTransportMode] = useState<'driving' | 'walking' | 'cycling'>('driving');
25
+
26
+ useEffect(() => {
27
+ const handleTransportChange = (e: Event) => {
28
+ const customEvent = e as CustomEvent;
29
+ if (customEvent.detail === 'taxi' || customEvent.detail === 'bus') {
30
+ setTransportMode('driving');
31
+ } else if (customEvent.detail === 'moto') {
32
+ setTransportMode('cycling');
33
+ }
34
+ };
35
+ window.addEventListener('transportChange', handleTransportChange);
36
+ return () => window.removeEventListener('transportChange', handleTransportChange);
37
+ }, []);
38
+
39
+ const handleSearch = async (query: string, setResults: (results: Place[]) => void) => {
40
+ if (!query) return;
41
+ setLoading(true);
42
+ setError(null);
43
+ try {
44
+ const places = await apiClient.searchPlaces(query);
45
+ setResults(places);
46
+ if (places.length === 0) {
47
+ setError('Aucun lieu trouvé');
48
+ }
49
+ } catch (err) {
50
+ setError('Erreur lors de la recherche');
51
+ } finally {
52
+ setLoading(false);
53
+ }
54
+ };
55
+
56
+ const handleSelectStart = (place: Place) => {
57
+ setStartQuery(place.name);
58
+ setSelectedStart(place);
59
+ setStartResults([]);
60
+ setRoutes([]);
61
+ setSelectedRouteIndex(0);
62
+ };
63
+
64
+ const handleSelectDetour = (place: Place) => {
65
+ setDetourQuery(place.name);
66
+ setSelectedDetour(place);
67
+ setDetourResults([]);
68
+ setRoutes([]);
69
+ setSelectedRouteIndex(0);
70
+ };
71
+
72
+ const handleSelectEnd = (place: Place) => {
73
+ setEndQuery(place.name);
74
+ setSelectedEnd(place);
75
+ setEndResults([]);
76
+ setRoutes([]);
77
+ setSelectedRouteIndex(0);
78
+ };
79
+
80
+ const handleCalculateRoute = async () => {
81
+ if (
82
+ !selectedStart ||
83
+ !selectedDetour ||
84
+ !selectedEnd ||
85
+ !selectedStart.coordinates ||
86
+ !selectedDetour.coordinates ||
87
+ !selectedEnd.coordinates
88
+ ) {
89
+ setError('Veuillez sélectionner un départ, un détour et une destination');
90
+ return;
91
+ }
92
+ setLoading(true);
93
+ setError(null);
94
+ try {
95
+ const routes = await apiClient.calculateRouteWithDetour(
96
+ {
97
+ lat: selectedStart.coordinates.lat,
98
+ lng: selectedStart.coordinates.lng,
99
+ },
100
+ {
101
+ lat: selectedDetour.coordinates.lat,
102
+ lng: selectedDetour.coordinates.lng,
103
+ },
104
+ {
105
+ lat: selectedEnd.coordinates.lat,
106
+ lng: selectedEnd.coordinates.lng,
107
+ },
108
+ transportMode,
109
+ selectedStart.name,
110
+ selectedDetour.name,
111
+ selectedEnd.name
112
+ );
113
+ setRoutes(routes);
114
+ setSelectedRouteIndex(0);
115
+ } catch (err) {
116
+ setError('Erreur lors du calcul de l\'itinéraire');
117
+ } finally {
118
+ setLoading(false);
119
+ }
120
+ };
121
+
122
+ return (
123
+ <div className={styles.routeSearchSection}>
124
+ <div className={styles.searchGroup}>
125
+ <label htmlFor="start" className={styles.label}>Départ</label>
126
+ <input
127
+ id="start"
128
+ type="text"
129
+ value={startQuery}
130
+ onChange={(e) => {
131
+ setStartQuery(e.target.value);
132
+ handleSearch(e.target.value, setStartResults);
133
+ }}
134
+ placeholder="Point de départ"
135
+ className={styles.searchInput}
136
+ />
137
+ {startResults.length > 0 && (
138
+ <ul className={styles.searchResults}>
139
+ {startResults.map((place) => (
140
+ <li
141
+ key={place.id}
142
+ onClick={() => handleSelectStart(place)}
143
+ className={styles.searchResultItem}
144
+ >
145
+ {place.name}
146
+ </li>
147
+ ))}
148
+ </ul>
149
+ )}
150
+ </div>
151
+ <div className={styles.searchGroup}>
152
+ <label htmlFor="detour" className={styles.label}>Détour</label>
153
+ <input
154
+ id="detour"
155
+ type="text"
156
+ value={detourQuery}
157
+ onChange={(e) => {
158
+ setDetourQuery(e.target.value);
159
+ handleSearch(e.target.value, setDetourResults);
160
+ }}
161
+ placeholder="Point de détour"
162
+ className={styles.searchInput}
163
+ />
164
+ {detourResults.length > 0 && (
165
+ <ul className={styles.searchResults}>
166
+ {detourResults.map((place) => (
167
+ <li
168
+ key={place.id}
169
+ onClick={() => handleSelectDetour(place)}
170
+ className={styles.searchResultItem}
171
+ >
172
+ {place.name}
173
+ </li>
174
+ ))}
175
+ </ul>
176
+ )}
177
+ </div>
178
+ <div className={styles.searchGroup}>
179
+ <label htmlFor="end" className={styles.label}>Destination</label>
180
+ <input
181
+ id="end"
182
+ type="text"
183
+ value={endQuery}
184
+ onChange={(e) => {
185
+ setEndQuery(e.target.value);
186
+ handleSearch(e.target.value, setEndResults);
187
+ }}
188
+ placeholder="Point d'arrivée"
189
+ className={styles.searchInput}
190
+ />
191
+ {endResults.length > 0 && (
192
+ <ul className={styles.searchResults}>
193
+ {endResults.map((place) => (
194
+ <li
195
+ key={place.id}
196
+ onClick={() => handleSelectEnd(place)}
197
+ className={styles.searchResultItem}
198
+ >
199
+ {place.name}
200
+ </li>
201
+ ))}
202
+ </ul>
203
+ )}
204
+ </div>
205
+ {error && <div className={styles.error}>{error}</div>}
206
+ <button
207
+ onClick={handleCalculateRoute}
208
+ disabled={loading || !selectedStart || !selectedDetour || !selectedEnd}
209
+ className={`${styles.searchButton} ${loading || !selectedStart || !selectedDetour || !selectedEnd ? styles.disabled : ''}`}
210
+ >
211
+ {loading ? 'Calcul...' : 'Calculer l\'itinéraire'}
212
+ </button>
213
+ </div>
214
+ );
215
+ };
216
+
217
+ export default DetourRouteSearch;