diff --git a/package-lock.json b/package-lock.json
index 8aad6bd..feea936 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -196,7 +196,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -616,7 +615,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
},
@@ -660,7 +658,6 @@
}
],
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
@@ -682,7 +679,6 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -4008,7 +4004,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.13.tgz",
"integrity": "sha512-i6DY9wnghE0ghHJfDrnnFNatn4CNBzMZv4xPzKB7Lb9zMAoImAxPKoGK9gLOm79aopDa07p6ytlFFWotvwj3DQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/query-core": "5.90.13"
},
@@ -4043,7 +4038,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.144.0.tgz",
"integrity": "sha512-GmRyIGmHtGj3VLTHXepIwXAxTcHyL5W7Vw7O1CnVEtFxQQWKMVOnWgI7tPY6FhlNwMKVb3n0mPFWz9KMYyd2GA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/react-store": "^0.8.0",
@@ -4115,7 +4109,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.144.0.tgz",
"integrity": "sha512-6oVERtK9XDHCP4XojgHsdHO56ZSj11YaWjF5g/zw39LhyA6Lx+/X86AEIHO4y0BUrMQaJfcjdAQMVSAs6Vjtdg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/history": "1.141.0",
"@tanstack/store": "^0.8.0",
@@ -4195,7 +4188,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -6733,7 +6725,6 @@
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -6742,7 +6733,8 @@
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==",
- "devOptional": true
+ "devOptional": true,
+ "peer": true
},
"node_modules/@types/react": {
"version": "18.3.26",
@@ -6762,7 +6754,6 @@
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -6839,7 +6830,6 @@
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.50.1",
"@typescript-eslint/types": "8.50.1",
@@ -7585,7 +7575,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -8056,7 +8045,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -8442,8 +8430,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
- "devOptional": true,
- "peer": true
+ "devOptional": true
},
"node_modules/d3": {
"version": "7.9.0",
@@ -8786,7 +8773,6 @@
"resolved": "https://packages.atlassian.com/api/npm/npm-remote/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=12"
}
@@ -9491,7 +9477,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -9552,7 +9537,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -9730,7 +9714,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -10803,7 +10786,6 @@
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -11495,7 +11477,6 @@
"integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -12142,7 +12123,6 @@
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.15.0.tgz",
"integrity": "sha512-pPeu/t4yPDX/+Uf9ibLUdmaKbNMlGxMAX+tBednYukol2qNk2TZXAlhdohWxjVvTO3is8crrUYv3Ok02oAaKzA==",
"license": "BSD-3-Clause",
- "peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -12907,7 +12887,6 @@
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -13039,7 +13018,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -13052,7 +13030,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -13435,7 +13412,6 @@
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.4.2.tgz",
"integrity": "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
}
@@ -13696,7 +13672,6 @@
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.1.0",
"seroval": "~1.3.0",
@@ -13709,7 +13684,6 @@
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
}
@@ -14232,7 +14206,6 @@
"resolved": "https://packages.atlassian.com/api/npm/npm-remote/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -14514,7 +14487,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14620,7 +14592,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -14747,7 +14718,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -14868,7 +14838,6 @@
"resolved": "https://packages.atlassian.com/api/npm/npm-remote/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -14922,7 +14891,6 @@
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@vitest/expect": "4.0.16",
"@vitest/mocker": "4.0.16",
@@ -15346,7 +15314,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src/components/directions/directions.spec.tsx b/src/components/directions/directions.spec.tsx
index b888329..e3f4d7a 100644
--- a/src/components/directions/directions.spec.tsx
+++ b/src/components/directions/directions.spec.tsx
@@ -5,7 +5,7 @@ import { DirectionsControl } from './directions';
const mockNavigate = vi.fn();
const mockRefetchDirections = vi.fn();
-const mockReverseGeocode = vi.fn().mockResolvedValue([]);
+const mockSetWaypointFromCoords = vi.fn().mockResolvedValue([]);
const mockAddEmptyWaypointToEnd = vi.fn();
const mockClearWaypoints = vi.fn();
const mockClearRoutes = vi.fn();
@@ -63,8 +63,8 @@ vi.mock('@/hooks/use-directions-queries', () => ({
useDirectionsQuery: vi.fn(() => ({
refetch: mockRefetchDirections,
})),
- useReverseGeocodeDirections: vi.fn(() => ({
- reverseGeocode: mockReverseGeocode,
+ useSetWaypointFromCoords: vi.fn(() => ({
+ setWaypointFromCoords: mockSetWaypointFromCoords,
})),
}));
@@ -312,14 +312,14 @@ describe('DirectionsControl URL parsing', () => {
render();
- expect(mockReverseGeocode).toHaveBeenCalledTimes(2);
- expect(mockReverseGeocode).toHaveBeenCalledWith(
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledTimes(2);
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
13.365016850476763,
52.483706198952575,
0,
{ isPermalink: true }
);
- expect(mockReverseGeocode).toHaveBeenCalledWith(
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
13.422421655040836,
52.49336042169804,
1,
@@ -335,14 +335,14 @@ describe('DirectionsControl URL parsing', () => {
render();
- expect(mockReverseGeocode).toHaveBeenCalledTimes(2);
- expect(mockReverseGeocode).toHaveBeenCalledWith(
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledTimes(2);
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
103.66492937866911,
1.4827280571964963,
0,
{ isPermalink: true }
);
- expect(mockReverseGeocode).toHaveBeenCalledWith(
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
103.66421854954496,
1.4840285187178779,
1,
@@ -358,7 +358,7 @@ describe('DirectionsControl URL parsing', () => {
render();
- expect(mockReverseGeocode).not.toHaveBeenCalled();
+ expect(mockSetWaypointFromCoords).not.toHaveBeenCalled();
});
it('should handle coordinates near edge of valid range', async () => {
@@ -369,11 +369,11 @@ describe('DirectionsControl URL parsing', () => {
render();
- expect(mockReverseGeocode).toHaveBeenCalledTimes(2);
- expect(mockReverseGeocode).toHaveBeenCalledWith(179.9, 89, 0, {
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledTimes(2);
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(179.9, 89, 0, {
isPermalink: true,
});
- expect(mockReverseGeocode).toHaveBeenCalledWith(-179.9, -89, 1, {
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(-179.9, -89, 1, {
isPermalink: true,
});
});
diff --git a/src/components/directions/directions.tsx b/src/components/directions/directions.tsx
index 3147dd5..6bef13d 100644
--- a/src/components/directions/directions.tsx
+++ b/src/components/directions/directions.tsx
@@ -17,7 +17,7 @@ import { useNavigate } from '@tanstack/react-router';
import { useDirectionsStore } from '@/stores/directions-store';
import {
useDirectionsQuery,
- useReverseGeocodeDirections,
+ useSetWaypointFromCoords,
} from '@/hooks/use-directions-queries';
import { useOptimizedRouteQuery } from '@/hooks/use-optimized-route-query';
import { Sparkles } from 'lucide-react';
@@ -41,7 +41,7 @@ export const DirectionsControl = () => {
const updateDateTime = useCommonStore((state) => state.updateDateTime);
const dateTime = useCommonStore((state) => state.dateTime);
const { refetch: refetchDirections } = useDirectionsQuery();
- const { reverseGeocode } = useReverseGeocodeDirections();
+ const { setWaypointFromCoords } = useSetWaypointFromCoords();
const { optimizeRoute, isPending: isOptimizing } = useOptimizedRouteQuery();
const isOptimized = useDirectionsStore((state) => state.isOptimized);
const activeRouteIndex = useDirectionsStore(
@@ -66,7 +66,7 @@ export const DirectionsControl = () => {
if (!isValidCoordinates(lat, lng) || isNaN(lng) || isNaN(lat)) continue;
const index = i / 2;
- reverseGeocode(lng, lat, index, { isPermalink: true });
+ setWaypointFromCoords(lng, lat, index, { isPermalink: true });
}
refetchDirections();
}
diff --git a/src/components/directions/waypoints/waypoint-item.spec.tsx b/src/components/directions/waypoints/waypoint-item.spec.tsx
index 1cfbe5d..0708dbd 100644
--- a/src/components/directions/waypoints/waypoint-item.spec.tsx
+++ b/src/components/directions/waypoints/waypoint-item.spec.tsx
@@ -1,4 +1,4 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Waypoint } from './waypoint-item';
@@ -7,6 +7,17 @@ const mockReceiveGeocodeResults = vi.fn();
const mockUpdateTextInput = vi.fn();
const mockDoRemoveWaypoint = vi.fn();
const mockRefetchDirections = vi.fn();
+const mockSetWaypointFromCoords = vi.fn().mockResolvedValue([]);
+const mockFlyTo = vi.fn();
+const { mockToastError } = vi.hoisted(() => ({ mockToastError: vi.fn() }));
+
+vi.mock('sonner', () => ({
+ toast: { error: mockToastError },
+}));
+
+vi.mock('react-map-gl/maplibre', () => ({
+ useMap: vi.fn(() => ({ mainMap: { flyTo: mockFlyTo } })),
+}));
vi.mock('@dnd-kit/sortable', () => ({
useSortable: vi.fn(() => ({
@@ -38,7 +49,7 @@ vi.mock('@/stores/directions-store', () => ({
{
title: 'Berlin, Germany',
addressindex: 0,
- lngLat: [13.4, 52.5],
+ displaylnglat: [13.4, 52.5],
selected: true,
},
],
@@ -64,6 +75,9 @@ vi.mock('@/hooks/use-directions-queries', () => ({
useDirectionsQuery: vi.fn(() => ({
refetch: mockRefetchDirections,
})),
+ useSetWaypointFromCoords: vi.fn(() => ({
+ setWaypointFromCoords: mockSetWaypointFromCoords,
+ })),
}));
vi.mock('@/components/ui/waypoint-search', () => ({
@@ -203,4 +217,117 @@ describe('Waypoint', () => {
'Waypoint 2'
);
});
+
+ describe('zoom-to-waypoint button', () => {
+ it('flies the map to the selected waypoint coords', async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByTestId('zoom-to-waypoint-button'));
+
+ expect(mockFlyTo).toHaveBeenCalledWith({
+ center: [13.4, 52.5],
+ zoom: 14,
+ });
+ });
+
+ it('is disabled when the waypoint has no selected coords', () => {
+ render();
+
+ expect(screen.getByTestId('zoom-to-waypoint-button')).toBeDisabled();
+ });
+ });
+
+ describe('use current location button', () => {
+ const originalGeolocation = navigator.geolocation;
+
+ afterEach(() => {
+ Object.defineProperty(navigator, 'geolocation', {
+ configurable: true,
+ value: originalGeolocation,
+ });
+ });
+
+ const stubGeolocation = (
+ getCurrentPosition: Geolocation['getCurrentPosition']
+ ) => {
+ Object.defineProperty(navigator, 'geolocation', {
+ configurable: true,
+ value: { getCurrentPosition },
+ });
+ };
+
+ it('sets the waypoint from coords on success and refetches directions', async () => {
+ stubGeolocation((success) => {
+ success({
+ coords: { longitude: 13.4, latitude: 52.5 },
+ } as GeolocationPosition);
+ });
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByTestId('use-current-location-button'));
+
+ expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(13.4, 52.5, 0);
+ expect(mockRefetchDirections).toHaveBeenCalled();
+ expect(mockToastError).not.toHaveBeenCalled();
+ });
+
+ it('shows the permission-denied toast when access is blocked', async () => {
+ stubGeolocation((_success, error) => {
+ error?.({
+ code: 1,
+ PERMISSION_DENIED: 1,
+ POSITION_UNAVAILABLE: 2,
+ TIMEOUT: 3,
+ message: '',
+ } as GeolocationPositionError);
+ });
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByTestId('use-current-location-button'));
+
+ expect(mockToastError).toHaveBeenCalledWith(
+ "We couldn't get your location. Please check your browser settings and allow location access."
+ );
+ expect(mockSetWaypointFromCoords).not.toHaveBeenCalled();
+ });
+
+ it('shows the generic toast for non-permission errors', async () => {
+ stubGeolocation((_success, error) => {
+ error?.({
+ code: 3,
+ PERMISSION_DENIED: 1,
+ POSITION_UNAVAILABLE: 2,
+ TIMEOUT: 3,
+ message: '',
+ } as GeolocationPositionError);
+ });
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByTestId('use-current-location-button'));
+
+ expect(mockToastError).toHaveBeenCalledWith(
+ "We couldn't get your location. Please try again."
+ );
+ });
+
+ it('shows an error when the browser has no geolocation API', async () => {
+ Object.defineProperty(navigator, 'geolocation', {
+ configurable: true,
+ value: undefined,
+ });
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByTestId('use-current-location-button'));
+
+ expect(mockToastError).toHaveBeenCalledWith(
+ "Your browser doesn't support geolocation."
+ );
+ expect(mockSetWaypointFromCoords).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/src/components/directions/waypoints/waypoint-item.tsx b/src/components/directions/waypoints/waypoint-item.tsx
index 87229d0..42beb54 100644
--- a/src/components/directions/waypoints/waypoint-item.tsx
+++ b/src/components/directions/waypoints/waypoint-item.tsx
@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
+import { useMap } from 'react-map-gl/maplibre';
import type { ActiveWaypoint } from '@/components/types';
import { Button } from '@/components/ui/button';
@@ -10,9 +11,13 @@ import {
TooltipTrigger,
} from '@/components/ui/tooltip';
import { WaypointSearch } from '@/components/ui/waypoint-search';
-import { GripVertical, Trash } from 'lucide-react';
+import { GripVertical, Locate, Search, Trash } from 'lucide-react';
+import { toast } from 'sonner';
import { useDirectionsStore } from '@/stores/directions-store';
-import { useDirectionsQuery } from '@/hooks/use-directions-queries';
+import {
+ useDirectionsQuery,
+ useSetWaypointFromCoords,
+} from '@/hooks/use-directions-queries';
interface WaypointProps {
id: string;
@@ -34,11 +39,14 @@ export const Waypoint = ({ id, index }: WaypointProps) => {
);
const updateTextInput = useDirectionsStore((state) => state.updateTextInput);
const { refetch: refetchDirections } = useDirectionsQuery();
+ const { setWaypointFromCoords } = useSetWaypointFromCoords();
const doRemoveWaypoint = useDirectionsStore(
(state) => state.doRemoveWaypoint
);
+ const { mainMap } = useMap();
const waypoint = waypoints[index];
const { userInput, geocodeResults } = waypoint!;
+ const selectedCoords = geocodeResults?.find((r) => r.selected)?.displaylnglat;
const handleGeocodeResults = useCallback(
(addresses: ActiveWaypoint[]) => {
@@ -47,6 +55,36 @@ export const Waypoint = ({ id, index }: WaypointProps) => {
[receiveGeocodeResults, index]
);
+ const handleUseCurrentLocation = useCallback(() => {
+ if (!navigator.geolocation) {
+ toast.error("Your browser doesn't support geolocation.");
+ return;
+ }
+ navigator.geolocation.getCurrentPosition(
+ async (pos) => {
+ await setWaypointFromCoords(
+ pos.coords.longitude,
+ pos.coords.latitude,
+ index
+ );
+ refetchDirections();
+ },
+ (error) => {
+ toast.error(
+ error.code === error.PERMISSION_DENIED
+ ? "We couldn't get your location. Please check your browser settings and allow location access."
+ : "We couldn't get your location. Please try again."
+ );
+ },
+ { enableHighAccuracy: true, timeout: 10000 }
+ );
+ }, [setWaypointFromCoords, refetchDirections, index]);
+
+ const handleZoomToWaypoint = useCallback(() => {
+ if (!mainMap || !selectedCoords) return;
+ mainMap.flyTo({ center: [selectedCoords[0], selectedCoords[1]], zoom: 14 });
+ }, [mainMap, selectedCoords]);
+
const handleResultSelect = useCallback(
(result: ActiveWaypoint) => {
updateTextInput({
@@ -103,28 +141,63 @@ export const Waypoint = ({ id, index }: WaypointProps) => {
}
rightContent={
-
-
-
-
-
- Remove this waypoint
-
-
+
+
+
+
+
+
+ Use my current location
+
+
+
+
+
+
+
+ Zoom to this waypoint
+
+
+
+
+
+
+
+ Remove this waypoint
+
+
+
}
/>
diff --git a/src/components/map/index.spec.tsx b/src/components/map/index.spec.tsx
index 0cfe82d..5b0af83 100644
--- a/src/components/map/index.spec.tsx
+++ b/src/components/map/index.spec.tsx
@@ -170,8 +170,8 @@ vi.mock('@/hooks/use-directions-queries', () => ({
useDirectionsQuery: vi.fn(() => ({
refetch: vi.fn(),
})),
- useReverseGeocodeDirections: vi.fn(() => ({
- reverseGeocode: vi.fn().mockResolvedValue([]),
+ useSetWaypointFromCoords: vi.fn(() => ({
+ setWaypointFromCoords: vi.fn().mockResolvedValue([]),
})),
}));
diff --git a/src/components/map/index.tsx b/src/components/map/index.tsx
index 156579a..9753191 100644
--- a/src/components/map/index.tsx
+++ b/src/components/map/index.tsx
@@ -64,7 +64,7 @@ import { useDirectionsStore } from '@/stores/directions-store';
import { useIsochronesStore } from '@/stores/isochrones-store';
import {
useDirectionsQuery,
- useReverseGeocodeDirections,
+ useSetWaypointFromCoords,
} from '@/hooks/use-directions-queries';
import {
useIsochronesQuery,
@@ -125,8 +125,7 @@ export const MapComponent = () => {
const { refetch: refetchDirections } = useDirectionsQuery();
const { refetch: refetchIsochrones } = useIsochronesQuery();
- const { reverseGeocode: reverseGeocodeDirections } =
- useReverseGeocodeDirections();
+ const { setWaypointFromCoords } = useSetWaypointFromCoords();
const { reverseGeocode: reverseGeocodeIsochrones } =
useReverseGeocodeIsochrones();
const [heightgraphHoverDistance, setHeightgraphHoverDistance] = useState<
@@ -247,7 +246,7 @@ export const MapComponent = () => {
const updateWaypointPosition = useCallback(
(object: { latLng: { lat: number; lng: number }; index: number }) => {
- reverseGeocodeDirections(
+ setWaypointFromCoords(
object.latLng.lng,
object.latLng.lat,
object.index
@@ -255,7 +254,7 @@ export const MapComponent = () => {
refetchDirections();
});
},
- [reverseGeocodeDirections, refetchDirections]
+ [setWaypointFromCoords, refetchDirections]
);
const updateIsoPosition = useCallback(
diff --git a/src/hooks/use-directions-queries.ts b/src/hooks/use-directions-queries.ts
index 9c6b792..6856ea9 100644
--- a/src/hooks/use-directions-queries.ts
+++ b/src/hooks/use-directions-queries.ts
@@ -125,7 +125,7 @@ export function useDirectionsQuery() {
});
}
-export function useReverseGeocodeDirections() {
+export function useSetWaypointFromCoords() {
const receiveGeocodeResults = useDirectionsStore(
(state) => state.receiveGeocodeResults
);
@@ -137,7 +137,7 @@ export function useReverseGeocodeDirections() {
(state) => state.updatePlaceholderAddressAtIndex
);
- const reverseGeocode = async (
+ const setWaypointFromCoords = async (
lng: number,
lat: number,
index: number,
@@ -156,7 +156,6 @@ export function useReverseGeocodeDirections() {
// Set placeholder immediately
updatePlaceholderAddressAtIndex(index, lng, lat);
- // Use raw coordinates directly — no reverse geocoding
const lngLat: [number, number] = [lng, lat];
const address: ActiveWaypoint = {
title: `${lng.toFixed(6)}, ${lat.toFixed(6)}`,
@@ -177,7 +176,7 @@ export function useReverseGeocodeDirections() {
return addresses;
};
- return { reverseGeocode };
+ return { setWaypointFromCoords };
}
async function fetchForwardGeocode(