Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Server-state lives in TanStack Query. The global `QueryClient` (`src/lib/tanstac

- `src/components/map/` — MapLibre map. `index.tsx` is the orchestrator; `parts/` holds map sublayers (route lines, isochrone polygons, hover popups, draw controls, marker icons). `valhalla-layers.ts` defines internal Valhalla edge/node/shortcut/access-restriction MVT layer IDs.
- `src/components/directions/`, `src/components/isochrones/`, `src/components/tiles/` — the three tab panels.
- Permalinks (`wps` URL param ↔ waypoints/isochrone center) are handled by `src/hooks/use-directions-permalink.ts` / `use-isochrones-permalink.ts`, called from `RoutePlanner`. They must **not** live inside the tab panels: `SheetContent` unmounts while the sidebar is closed (the default on mobile), so a pasted link would never load.
- `src/components/quick-settings.tsx` — left-sidebar "General settings" collapsible panel. Hosts the most-used controls inline so they're visible without opening the advanced panel: ferry/highway/toll icon buttons (a state-decorated `IconEnumButton` for each), a `DateTimeButton`, an alternates slider, and the directions language picker. Used by both the directions and isochrones tabs (the latter passes `showAlternates={false} showLanguage={false}`). Renders the `SettingsButton` ("Advanced settings") at the bottom — that's the entry point to `SettingsPanel`.
- `src/components/settings-panel/` — full ("advanced") costing options panel. `settings-options.ts` holds `settingsInit`, `settingsInitTruckOverride`, the per-profile `profileSettings` / `generalSettings` lists, and the `languageOptions` / language storage helpers. `settings-panel.tsx` renders `Profile Settings` + `General Settings`, **filtering out** `use_highways`, `use_tolls`, `use_ferry`, `alternates` since those moved to QuickSettings (params still flow through `filter-profile-settings.ts` for API requests).
- `src/components/ui/` — shadcn/ui primitives (do not rename — they're tracked by `components.json`). `icon-enum-setting.tsx` (`IconEnumButton`), `date-time-button.tsx`, and `calendar.tsx` are the QuickSettings building blocks.
Expand Down
141 changes: 0 additions & 141 deletions src/components/directions/directions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,10 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DirectionsControl } from './directions';

const mockNavigate = vi.fn();
const mockRefetchDirections = vi.fn();
const mockSetWaypointFromCoords = vi.fn().mockResolvedValue([]);
const mockAddEmptyWaypointToEnd = vi.fn();
const mockClearWaypoints = vi.fn();
const mockClearRoutes = vi.fn();

vi.mock('@tanstack/react-router', () => ({
useNavigate: vi.fn(() => mockNavigate),
}));

vi.mock('@/utils/parse-url-params', () => ({
parseUrlParams: vi.fn(() => ({})),
}));

const mockWaypoints = [
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' },
Expand Down Expand Up @@ -61,15 +50,6 @@ vi.mock('@/stores/directions-store', () => {
};
});

vi.mock('@/hooks/use-directions-queries', () => ({
useDirectionsQuery: vi.fn(() => ({
refetch: mockRefetchDirections,
})),
useSetWaypointFromCoords: vi.fn(() => ({
setWaypointFromCoords: mockSetWaypointFromCoords,
})),
}));

vi.mock('@/hooks/use-optimized-route-query', () => ({
useOptimizedRouteQuery: vi.fn(() => ({
optimizeRoute: vi.fn(),
Expand Down Expand Up @@ -216,125 +196,4 @@ describe('DirectionsControl', () => {
expect(screen.getByTestId('mock-route-card-1')).toBeInTheDocument();
expect(screen.getByTestId('mock-route-card-2')).toBeInTheDocument();
});

it('should sync waypoints to URL', () => {
mockWaypoints.length = 0;
mockWaypoints.push(
{
id: '0',
geocodeResults: [
{ selected: true, sourcelnglat: [13.4, 52.5] },
] as never[],
userInput: 'Berlin',
},
{
id: '1',
geocodeResults: [
{ selected: true, sourcelnglat: [10.0, 48.0] },
] as never[],
userInput: 'Munich',
}
);

render(<DirectionsControl />);

expect(mockNavigate).toHaveBeenCalledWith({
search: expect.any(Function),
replace: true,
});

const navigateCall = mockNavigate.mock.calls[0]?.[0] as {
search: (prev: Record<string, unknown>) => Record<string, unknown>;
};
const searchFn = navigateCall.search;
const result = searchFn({});

expect(result.wps).toBe('13.4,52.5,10,48');
});
});

describe('DirectionsControl URL parsing', () => {
beforeEach(() => {
vi.clearAllMocks();
mockResults.data = null;
mockWaypoints.length = 0;
mockWaypoints.push(
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' }
);
});

it('should process URL params with valid coordinates (Berlin)', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '13.365016850476763,52.483706198952575,13.422421655040836,52.49336042169804',
});

render(<DirectionsControl />);

expect(mockSetWaypointFromCoords).toHaveBeenCalledTimes(2);
expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
13.365016850476763,
52.483706198952575,
0,
{ isPermalink: true }
);
expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
13.422421655040836,
52.49336042169804,
1,
{ isPermalink: true }
);
});

it('should process URL params with valid coordinates where lng > 90 (Singapore)', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '103.66492937866911,1.4827280571964963,103.66421854954496,1.4840285187178779',
});

render(<DirectionsControl />);

expect(mockSetWaypointFromCoords).toHaveBeenCalledTimes(2);
expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
103.66492937866911,
1.4827280571964963,
0,
{ isPermalink: true }
);
expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(
103.66421854954496,
1.4840285187178779,
1,
{ isPermalink: true }
);
});

it('should skip truly invalid coordinates from URL', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '999,999',
});

render(<DirectionsControl />);

expect(mockSetWaypointFromCoords).not.toHaveBeenCalled();
});

it('should handle coordinates near edge of valid range', async () => {
const parseUrlParams = await import('@/utils/parse-url-params');
vi.mocked(parseUrlParams.parseUrlParams).mockReturnValue({
wps: '179.9,89,-179.9,-89',
});

render(<DirectionsControl />);

expect(mockSetWaypointFromCoords).toHaveBeenCalledTimes(2);
expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(179.9, 89, 0, {
isPermalink: true,
});
expect(mockSetWaypointFromCoords).toHaveBeenCalledWith(-179.9, -89, 1, {
isPermalink: true,
});
});
});
66 changes: 1 addition & 65 deletions src/components/directions/directions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import { useCallback } from 'react';

import { Waypoints } from './waypoints/waypoint-list';

Expand All @@ -9,14 +9,7 @@ import type { ParsedDirectionsGeometry } from '@/components/types';
import { Button } from '@/components/ui/button';
import { MapPinPlus, MapPinXInside } from 'lucide-react';
import { RouteCard } from './route-card';
import { parseUrlParams } from '@/utils/parse-url-params';
import { isValidCoordinates } from '@/utils/geom';
import { useNavigate } from '@tanstack/react-router';
import { useDirectionsStore } from '@/stores/directions-store';
import {
useDirectionsQuery,
useSetWaypointFromCoords,
} from '@/hooks/use-directions-queries';
import { useOptimizedRouteQuery } from '@/hooks/use-optimized-route-query';
import { Sparkles } from 'lucide-react';
import {
Expand All @@ -33,11 +26,6 @@ export const DirectionsControl = () => {
);
const clearWaypoints = useDirectionsStore((state) => state.clearWaypoints);
const clearRoutes = useDirectionsStore((state) => state.clearRoutes);
const initialUrlParams = useRef(parseUrlParams());
const urlParamsProcessed = useRef(false);
const navigate = useNavigate({ from: '/$activeTab' });
const { refetch: refetchDirections } = useDirectionsQuery();
const { setWaypointFromCoords } = useSetWaypointFromCoords();
const { optimizeRoute, isPending: isOptimizing } = useOptimizedRouteQuery();
const isOptimized = useDirectionsStore((state) => state.isOptimized);
const activeRouteIndex = useDirectionsStore(
Expand All @@ -47,58 +35,6 @@ export const DirectionsControl = () => {
(state) => state.setActiveRouteIndex
);

useEffect(() => {
if (urlParamsProcessed.current) return;

const alreadyHydrated = useDirectionsStore
.getState()
.waypoints.some((wp) => wp.geocodeResults.some((r) => r.selected));
if (alreadyHydrated) {
urlParamsProcessed.current = true;
return;
}

const wpsParam = initialUrlParams.current.wps;

if (wpsParam) {
const coordinates = wpsParam.split(',').map(Number);

for (let i = 0; i < coordinates.length; i += 2) {
const lng = coordinates[i]!;
const lat = coordinates[i + 1]!;

if (!isValidCoordinates(lat, lng) || isNaN(lng) || isNaN(lat)) continue;

const index = i / 2;
setWaypointFromCoords(lng, lat, index, { isPermalink: true });
}
refetchDirections();
}

urlParamsProcessed.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
const wps: number[] = [];

for (const wp of waypoints) {
for (const result of wp.geocodeResults) {
if (result.selected && result.sourcelnglat) {
wps.push(result.sourcelnglat[0], result.sourcelnglat[1]);
}
}
}

navigate({
search: (prev) => ({
...prev,
wps: wps.length > 0 ? wps.join(',') : undefined,
}),
replace: true,
});
}, [waypoints, navigate]);

const handleAddWaypoint = useCallback(() => {
addEmptyWaypointToEnd();
}, [addEmptyWaypointToEnd]);
Expand Down
Loading
Loading