Skip to content
Open
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ The Vite `base` is derived from `package.json` `homepage` (see `vite.config.ts
Three Zustand stores, each with `immer` + `devtools`:

- `src/stores/common-store.ts` β€” settings panel/directions panel open state, costing settings, dateTime, map-ready flag. `Profile` enum and `profileEnum` zod schema live here.
- `src/stores/directions-store.ts` β€” waypoints (with geocode results), route results, highlighted maneuver, optimized-route flag, active-route index.
- `src/stores/directions-store.ts` β€” waypoints (each with a selected address + geocode candidates), route results, highlighted maneuver, optimized-route flag, active-route index.
- `src/stores/isochrones-store.ts` β€” input/result, range/interval/denoise/generalize, color palette, opacity.

Server-state lives in TanStack Query. The global `QueryClient` (`src/lib/tanstack-query/root-provider.tsx`) sets `refetchOnWindowFocus: false`, `retry: 1`, `staleTime: 5min`, `gcTime: 10min`. Query hooks are in `src/hooks/use-*-queries.ts`. They read inputs directly from Zustand stores via `useStore.getState()` and from the router via `router.state.location.search` rather than parameters β€” keep that pattern when adding new queries.
Expand Down
33 changes: 15 additions & 18 deletions src/components/directions/directions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ vi.mock('@/utils/parse-url-params', () => ({
}));

const mockWaypoints = [
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' },
{ id: '0', geocodeResults: [], selectedAddress: null, userInput: '' },
{ id: '1', geocodeResults: [], selectedAddress: null, userInput: '' },
];

const mockResults = {
Expand Down Expand Up @@ -54,8 +54,8 @@ vi.mock('@/stores/directions-store', () => {
);
return {
defaultWaypoints: [
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' },
{ id: '0', geocodeResults: [], selectedAddress: null, userInput: '' },
{ id: '1', geocodeResults: [], selectedAddress: null, userInput: '' },
],
useDirectionsStore,
};
Expand Down Expand Up @@ -107,8 +107,8 @@ describe('DirectionsControl', () => {
mockResults.data = null;
mockWaypoints.length = 0;
mockWaypoints.push(
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' }
{ id: '0', geocodeResults: [], selectedAddress: null, userInput: '' },
{ id: '1', geocodeResults: [], selectedAddress: null, userInput: '' }
);
});

Expand Down Expand Up @@ -160,12 +160,11 @@ describe('DirectionsControl', () => {
mockWaypoints.push(
{
id: '0',
geocodeResults: [
{ selected: true, sourcelnglat: [13.4, 52.5] },
] as never[],
geocodeResults: [],
selectedAddress: { sourcelnglat: [13.4, 52.5] } as never,
userInput: 'Berlin',
},
{ id: '1', geocodeResults: [], userInput: '' }
{ id: '1', geocodeResults: [], selectedAddress: null, userInput: '' }
);

render(<DirectionsControl />);
Expand Down Expand Up @@ -222,16 +221,14 @@ describe('DirectionsControl', () => {
mockWaypoints.push(
{
id: '0',
geocodeResults: [
{ selected: true, sourcelnglat: [13.4, 52.5] },
] as never[],
geocodeResults: [],
selectedAddress: { sourcelnglat: [13.4, 52.5] } as never,
userInput: 'Berlin',
},
{
id: '1',
geocodeResults: [
{ selected: true, sourcelnglat: [10.0, 48.0] },
] as never[],
geocodeResults: [],
selectedAddress: { sourcelnglat: [10.0, 48.0] } as never,
userInput: 'Munich',
}
);
Expand Down Expand Up @@ -259,8 +256,8 @@ describe('DirectionsControl URL parsing', () => {
mockResults.data = null;
mockWaypoints.length = 0;
mockWaypoints.push(
{ id: '0', geocodeResults: [], userInput: '' },
{ id: '1', geocodeResults: [], userInput: '' }
{ id: '0', geocodeResults: [], selectedAddress: null, userInput: '' },
{ id: '1', geocodeResults: [], selectedAddress: null, userInput: '' }
);
});

Expand Down
13 changes: 6 additions & 7 deletions src/components/directions/directions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const DirectionsControl = () => {

const alreadyHydrated = useDirectionsStore
.getState()
.waypoints.some((wp) => wp.geocodeResults.some((r) => r.selected));
.waypoints.some((wp) => wp.selectedAddress);
if (alreadyHydrated) {
urlParamsProcessed.current = true;
return;
Expand Down Expand Up @@ -83,10 +83,9 @@ export const DirectionsControl = () => {
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]);
}
const lngLat = wp.selectedAddress?.sourcelnglat;
if (lngLat) {
wps.push(lngLat[0], lngLat[1]);
}
}

Expand All @@ -108,8 +107,8 @@ export const DirectionsControl = () => {
clearRoutes();
}, [clearWaypoints, clearRoutes]);

const activeWaypointsCount = waypoints.filter((wp) =>
wp.geocodeResults.some((r) => r.selected)
const activeWaypointsCount = waypoints.filter(
(wp) => wp.selectedAddress
).length;

return (
Expand Down
40 changes: 24 additions & 16 deletions src/components/directions/waypoints/waypoint-item.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event';
import { Waypoint } from './waypoint-item';

const mockReceiveGeocodeResults = vi.fn();
const mockUpdateTextInput = vi.fn();
const mockSelectAddress = vi.fn();
const mockDoRemoveWaypoint = vi.fn();
const mockRefetchDirections = vi.fn();
const mockSetWaypointFromCoords = vi.fn().mockResolvedValue([]);
Expand Down Expand Up @@ -45,29 +45,38 @@ vi.mock('@/stores/directions-store', () => ({
{
id: 'wp-1',
userInput: 'Berlin',
geocodeResults: [
{
title: 'Berlin, Germany',
addressindex: 0,
displaylnglat: [13.4, 52.5],
selected: true,
},
],
geocodeResults: [],
selectedAddress: {
title: 'Berlin, Germany',
addressindex: 0,
displaylnglat: [13.4, 52.5],
},
},
{
id: 'wp-2',
userInput: 'Munich',
geocodeResults: [],
selectedAddress: null,
},
],
receiveGeocodeResults: mockReceiveGeocodeResults,
updateTextInput: mockUpdateTextInput,
selectAddress: mockSelectAddress,
doRemoveWaypoint: mockDoRemoveWaypoint,
})
),
defaultWaypoints: [
{ id: 'default-1', userInput: '', geocodeResults: [] },
{ id: 'default-2', userInput: '', geocodeResults: [] },
{
id: 'default-1',
userInput: '',
geocodeResults: [],
selectedAddress: null,
},
{
id: 'default-2',
userInput: '',
geocodeResults: [],
selectedAddress: null,
},
],
}));

Expand Down Expand Up @@ -171,16 +180,15 @@ describe('Waypoint', () => {
});
});

it('should call updateTextInput and refetchDirections when result is selected', async () => {
it('should call selectAddress and refetchDirections when result is selected', async () => {
const user = userEvent.setup();
render(<Waypoint id="wp-1" index={0} />);

await user.click(screen.getByTestId('select-result'));

expect(mockUpdateTextInput).toHaveBeenCalledWith({
inputValue: 'Selected',
expect(mockSelectAddress).toHaveBeenCalledWith({
index: 0,
addressindex: 0,
address: { title: 'Selected', addressindex: 0, lngLat: [0, 0] },
});
expect(mockRefetchDirections).toHaveBeenCalled();
});
Expand Down
20 changes: 6 additions & 14 deletions src/components/directions/waypoints/waypoint-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@ export const Waypoint = ({ id, index }: WaypointProps) => {
const receiveGeocodeResults = useDirectionsStore(
(state) => state.receiveGeocodeResults
);
const updateTextInput = useDirectionsStore((state) => state.updateTextInput);
const selectAddress = useDirectionsStore((state) => state.selectAddress);
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 { userInput, geocodeResults, selectedAddress } = waypoint!;
const selectedCoords = selectedAddress?.displaylnglat;

const handleGeocodeResults = useCallback(
(addresses: ActiveWaypoint[]) => {
Expand Down Expand Up @@ -87,15 +87,10 @@ export const Waypoint = ({ id, index }: WaypointProps) => {

const handleResultSelect = useCallback(
(result: ActiveWaypoint) => {
updateTextInput({
inputValue: result.title,
index: index,
addressindex: result.addressindex,
});

selectAddress({ index, address: result });
refetchDirections();
},
[updateTextInput, index, refetchDirections]
[selectAddress, index, refetchDirections]
);

const style = {
Expand Down Expand Up @@ -185,10 +180,7 @@ export const Waypoint = ({ id, index }: WaypointProps) => {
refetchDirections();
}}
data-testid="remove-waypoint-button"
disabled={
waypoints.length < 3 &&
!geocodeResults?.some((r) => r.selected)
}
disabled={waypoints.length < 3 && !selectedAddress}
>
<Trash className="size-3" />
</Button>
Expand Down
37 changes: 11 additions & 26 deletions src/components/isochrones/isochrones.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,16 @@ const mockResults = {
show: true,
};

const mockGeocodeResults: {
selected: boolean;
sourcelnglat: [number, number];
}[] = [];
let mockSelectedAddress: { sourcelnglat: [number, number] } | null = null;

vi.mock('@/stores/isochrones-store', () => {
const getState = () => ({
results: mockResults,
selectedAddress: mockSelectedAddress,
});
const useIsochronesStore = Object.assign(
vi.fn((selector) =>
selector({
results: mockResults,
geocodeResults: mockGeocodeResults,
})
),
{
getState: () => ({
results: mockResults,
geocodeResults: mockGeocodeResults,
}),
}
vi.fn((selector) => selector(getState())),
{ getState }
);
return { useIsochronesStore };
});
Expand Down Expand Up @@ -96,7 +87,7 @@ describe('IsochronesControl', () => {
beforeEach(() => {
vi.clearAllMocks();
mockResults.data = null;
mockGeocodeResults.length = 0;
mockSelectedAddress = null;
});

it('should render without crashing', () => {
Expand Down Expand Up @@ -143,10 +134,7 @@ describe('IsochronesControl', () => {
});

it('should sync geocode results to URL', () => {
mockGeocodeResults.push({
selected: true,
sourcelnglat: [13.4, 52.5],
});
mockSelectedAddress = { sourcelnglat: [13.4, 52.5] };

render(<IsochronesControl />);

Expand All @@ -157,10 +145,7 @@ describe('IsochronesControl', () => {
});

it('should call navigate with wps parameter when center exists', () => {
mockGeocodeResults.push({
selected: true,
sourcelnglat: [13.4, 52.5],
});
mockSelectedAddress = { sourcelnglat: [13.4, 52.5] };

render(<IsochronesControl />);

Expand Down Expand Up @@ -197,7 +182,7 @@ describe('IsochronesControl URL parsing', () => {
beforeEach(() => {
vi.clearAllMocks();
mockResults.data = null;
mockGeocodeResults.length = 0;
mockSelectedAddress = null;
});

it('should process URL params with valid coordinates', async () => {
Expand Down
17 changes: 5 additions & 12 deletions src/components/isochrones/isochrones.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
export const IsochronesControl = () => {
const { mainMap } = useMap();
const results = useIsochronesStore((state) => state.results);
const geocodeResults = useIsochronesStore((state) => state.geocodeResults);
const selectedAddress = useIsochronesStore((state) => state.selectedAddress);
const initialUrlParams = useRef(parseUrlParams());
const urlParamsProcessed = useRef(false);
const navigate = useNavigate({ from: '/$activeTab' });
Expand All @@ -26,9 +26,8 @@ export const IsochronesControl = () => {
useEffect(() => {
if (urlParamsProcessed.current || !mainMap) return;

const alreadyHydrated = useIsochronesStore
.getState()
.geocodeResults.some((r) => r.selected);
const alreadyHydrated =
useIsochronesStore.getState().selectedAddress !== null;
if (alreadyHydrated) {
urlParamsProcessed.current = true;
return;
Expand Down Expand Up @@ -61,19 +60,13 @@ export const IsochronesControl = () => {

// Sync isochrone center to URL
useEffect(() => {
let center: string | undefined;

for (const result of geocodeResults) {
if (result.selected && result.sourcelnglat) {
center = result.sourcelnglat.join(',');
}
}
const center = selectedAddress?.sourcelnglat?.join(',');

navigate({
search: (prev) => ({ ...prev, wps: center || undefined }),
replace: true,
});
}, [geocodeResults, navigate]);
}, [selectedAddress, navigate]);

return (
<>
Expand Down
2 changes: 1 addition & 1 deletion src/components/map/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ vi.mock('@/stores/directions-store', () => ({
vi.mock('@/stores/isochrones-store', () => ({
useIsochronesStore: vi.fn((selector) => {
const state = {
geocodeResults: [],
selectedAddress: null,
};
return selector(state);
}),
Expand Down
Loading
Loading