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(