@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.
- package/README.md +0 -0
- package/dist/components/Dashboard.d.ts +14 -0
- package/dist/components/Dashboard.js +52 -0
- package/dist/components/DetourRouteSearch.d.ts +10 -0
- package/dist/components/DetourRouteSearch.js +119 -0
- package/dist/components/MapView.d.ts +14 -0
- package/dist/components/MapView.js +179 -0
- package/dist/components/RouteSearch.d.ts +10 -0
- package/dist/components/RouteSearch.js +95 -0
- package/dist/components/SearchBar.d.ts +10 -0
- package/dist/components/SearchBar.js +48 -0
- package/dist/components/TransportOptions.d.ts +3 -0
- package/dist/components/TransportOptions.js +24 -0
- package/dist/components/TripType.d.ts +3 -0
- package/dist/components/TripType.js +20 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +39 -0
- package/dist/lib/api.d.ts +21 -0
- package/dist/lib/api.js +58 -0
- package/dist/lib/geolocalisation.d.ts +9 -0
- package/dist/lib/geolocalisation.js +30 -0
- package/dist/lib/type.d.ts +32 -0
- package/dist/lib/type.js +2 -0
- package/package.json +31 -0
- package/src/components/Dashboard.tsx +183 -0
- package/src/components/DetourRouteSearch.tsx +217 -0
- package/src/components/MapView.tsx +201 -0
- package/src/components/RouteSearch.tsx +163 -0
- package/src/components/SearchBar.tsx +87 -0
- package/src/components/TransportOptions.tsx +53 -0
- package/src/components/TripType.tsx +47 -0
- package/src/components/styles.modules.css +207 -0
- package/src/components/wellknown.d.ts +1 -0
- package/src/css-module.d.ts +4 -0
- package/src/index.ts +10 -0
- package/src/lib/api.ts +74 -0
- package/src/lib/geolocalisation.ts +41 -0
- 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
|
+
}
|
package/dist/lib/api.js
ADDED
@@ -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,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
|
+
}
|
package/dist/lib/type.js
ADDED
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;
|