Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 src/components/route-planner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const RoutePlanner = () => {

const handleProfileChange = (value: Profile) => {
navigate({
search: (prev) => ({ ...prev, profile: value }),
search: (prev) => ({ ...prev, profile: value, costing: undefined }),
replace: true,
});

Expand Down
2 changes: 2 additions & 0 deletions src/components/settings-panel/settings-panel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ const mockRefetchIsochrones = vi.fn();

const mockUseParams = vi.fn(() => ({ activeTab: 'directions' }));
const mockUseSearch = vi.fn(() => ({ profile: 'bicycle' }));
const mockNavigate = vi.fn();

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

vi.mock('@/stores/common-store', () => ({
Expand Down
3 changes: 3 additions & 0 deletions src/components/settings-panel/settings-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
import { useParams, useSearch } from '@tanstack/react-router';
import { useDirectionsQuery } from '@/hooks/use-directions-queries';
import { useIsochronesQuery } from '@/hooks/use-isochrones-queries';
import { useSettingsUrlSync } from '@/hooks/use-settings-url-sync';
import { CollapsibleSection } from '@/components/ui/collapsible-section';
import { ServerSettings } from '@/components/settings-panel/server-settings';
import { MultiSelectSetting } from '../ui/multiselect-setting';
Expand All @@ -53,6 +54,8 @@ export const SettingsPanel = () => {
const { refetch: refetchDirections } = useDirectionsQuery();
const { refetch: refetchIsochrones } = useIsochronesQuery();

useSettingsUrlSync();

const [language, setLanguage] = useState<DirectionsLanguage>(() =>
getDirectionsLanguage()
);
Expand Down
46 changes: 46 additions & 0 deletions src/hooks/use-settings-url-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useRef } from 'react';
import { useNavigate, useSearch } from '@tanstack/react-router';
import { useCommonStore, type Profile } from '@/stores/common-store';
import type { PossibleSettings } from '@/components/types';
import {
serializeCostingOptions,
deserializeCostingOptions,
} from '@/utils/costing-url';

// flow-
// on mount-use the existing params and apply to settings
// on change in setting we keep the url in sync
export function useSettingsUrlSync() {
const settings = useCommonStore((state) => state.settings);
const updateSettings = useCommonStore((state) => state.updateSettings);
const { profile, costing: urlCosting } = useSearch({ from: '/$activeTab' });
const navigate = useNavigate({ from: '/$activeTab' });

// for the initial costing value from the URL before any effects run
const initialCostingRef = useRef(urlCosting);
const initializedRef = useRef(false);

useEffect(() => {
const costingOptions = deserializeCostingOptions(initialCostingRef.current);
for (const [key, value] of Object.entries(costingOptions)) {
updateSettings(
key as keyof PossibleSettings,
value as PossibleSettings[keyof PossibleSettings]
);
}
initializedRef.current = true;
}, []);

Check warning on line 32 in src/hooks/use-settings-url-sync.ts

View workflow job for this annotation

GitHub Actions / test

React Hook useEffect has a missing dependency: 'updateSettings'. Either include it or remove the dependency array

useEffect(() => {
if (!initializedRef.current) return;

const costing = serializeCostingOptions(
settings,
(profile || 'bicycle') as Profile
);
navigate({
search: (prev) => ({ ...prev, costing }),
replace: true,
});
}, [settings, profile, navigate]);
}
2 changes: 1 addition & 1 deletion src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const activeTabRoute = createRoute({
component: App,
validateSearch: zodValidator(searchParamsSchema),
search: {
middlewares: [retainSearchParams(['profile', 'style'])],
middlewares: [retainSearchParams(['profile', 'style', 'costing'])],
},
beforeLoad: ({ params, search }) => {
if (!isValidTab(params.activeTab)) {
Expand Down
58 changes: 58 additions & 0 deletions src/utils/costing-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
settingsInit,
settingsInitTruckOverride,
} from '@/components/settings-panel/settings-options';
import type { PossibleSettings } from '@/components/types';
import type { Profile } from '@/stores/common-store';

// Keys too large to serialize in the URL
const EXCLUDED_KEYS = new Set<keyof PossibleSettings>(['exclude_polygons']);

function getDefaultSettings(profile: Profile): typeof settingsInit {
return profile === 'truck' ? settingsInitTruckOverride : settingsInit;
}

// syncs only the non-default costing options to a json string for use in the url
// returns undefined if all settings are at their defaults.
export function serializeCostingOptions(
settings: PossibleSettings,
profile: Profile
): string | undefined {
const defaults = getDefaultSettings(profile) as PossibleSettings;
const nonDefault: Record<string, unknown> = {};

for (const key of Object.keys(settings) as (keyof PossibleSettings)[]) {
if (EXCLUDED_KEYS.has(key)) continue;

const value = settings[key];
const defaultValue = defaults[key];

if (JSON.stringify(value) !== JSON.stringify(defaultValue)) {
nonDefault[key] = value;
}
}

if (Object.keys(nonDefault).length === 0) return undefined;
return JSON.stringify(nonDefault);
}

// for json parsing
// if sting is invalid it should return empty obj
export function deserializeCostingOptions(
costing: string | undefined
): Partial<PossibleSettings> {
if (!costing) return {};
try {
const parsed: unknown = JSON.parse(costing);
if (
typeof parsed !== 'object' ||
parsed === null ||
Array.isArray(parsed)
) {
return {};
}
return parsed as Partial<PossibleSettings>;
} catch {
return {};
}
}
1 change: 1 addition & 0 deletions src/utils/route-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const searchParamsSchema = z.object({
generalize: z.number().optional(),
denoise: z.number().optional(),
style: mapStyleSchema.optional(),
costing: z.string().optional(),
});

export type SearchParamsSchema = z.infer<typeof searchParamsSchema>;
Expand Down
Loading