has issues

This commit is contained in:
Andras Schmelczer 2026-05-25 13:20:17 +01:00
parent 2e112d7398
commit c645b0f1d4
96 changed files with 2147083 additions and 5787 deletions

View file

@ -15,16 +15,28 @@ import { useAiFilters } from '../../hooks/useAiFilters';
import { useUrlSync } from '../../hooks/useUrlSync';
import { useTutorial } from '../../hooks/useTutorial';
import { getTutorialStyles } from '../../lib/tutorial-styles';
import { travelFieldKey, useTravelTime } from '../../hooks/useTravelTime';
import {
MAX_TRAVEL_MINUTES,
parseServerMode,
resolveTransitVariant,
travelFieldKey,
useTravelTime,
} from '../../hooks/useTravelTime';
import { apiUrl, authHeaders, buildFilterString } from '../../lib/api';
import { useFilterCounts } from '../../hooks/useFilterCounts';
import { trackEvent } from '../../lib/analytics';
import { INITIAL_VIEW_STATE, POSTCODE_SEARCH_ZOOM } from '../../lib/consts';
import {
INITIAL_VIEW_STATE,
POSTCODE_SEARCH_ZOOM,
POSTCODE_ZOOM_THRESHOLD,
} from '../../lib/consts';
import type { OverlayId } from '../../lib/overlays';
import { useLicense } from '../../hooks/useLicense';
import { stateToParams } from '../../lib/url-state';
import {
AreaPane,
Filters,
OverlayPane,
POIPane,
PropertiesPane,
UpgradeModal,
@ -62,6 +74,7 @@ export default function MapPage({
initialFilters,
initialViewState,
initialPOICategories,
initialOverlays,
initialTab,
initialLoading,
theme,
@ -92,11 +105,15 @@ export default function MapPage({
const { t } = useTranslation();
const [selectedPOICategories, setSelectedPOICategories] =
useState<Set<string>>(initialPOICategories);
const [activeOverlays, setActiveOverlays] = useState<Set<OverlayId>>(
() => new Set(initialOverlays ?? [])
);
const [leftPaneWidth, leftPaneHandlers] = usePaneResize(384, 200, 0.45, 'left');
const [rightPaneWidth, rightPaneHandlers] = usePaneResize(384, 200, 0.45, 'right');
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [mobileBottomSheetHeight, setMobileBottomSheetHeight] = useState(0);
const [poiPaneOpen, setPoiPaneOpen] = useState(false);
const [overlayPaneOpen, setOverlayPaneOpen] = useState(false);
const [currentLocation, setCurrentLocation] = useState<{ lat: number; lng: number } | null>(null);
const {
@ -141,6 +158,8 @@ export default function MapPage({
handleSetEntries,
handleTimeRangeChange,
handleToggleBest,
handleToggleNoChange,
handleToggleNoBuses,
} = useTravelTime(initialTravelTime);
const mapFlyToRef = useRef<MapFlyTo | null>(null);
@ -181,8 +200,10 @@ export default function MapPage({
async (query: string) => {
const context = {
filters,
// Send the resolved variant so the AI sees the actual server mode in use,
// not just the UI-side base mode. Lets the model refine no-change/no-buses.
travelTime: activeEntries.map((entry) => ({
mode: entry.mode,
mode: resolveTransitVariant(entry),
label: entry.label,
min: entry.timeRange?.[0],
max: entry.timeRange?.[1],
@ -194,17 +215,29 @@ export default function MapPage({
if (!result) return;
handleSetFilters(result.filters);
// Filter out variants the UI cannot represent (e.g. transit-one-change*)
// FIRST so the same filtered list drives both entry state and fly-to.
// Otherwise we'd fly to a destination for a mode the user can't see.
const representable = result.travelTimeFilters
.map((tt) => ({ tt, parsed: parseServerMode(tt.mode) }))
.filter((x): x is { tt: typeof x.tt; parsed: NonNullable<typeof x.parsed> } => !!x.parsed);
handleSetEntries(
result.travelTimeFilters.map((travelTimeFilter) => ({
mode: travelTimeFilter.mode,
slug: travelTimeFilter.slug,
label: travelTimeFilter.label,
timeRange: [travelTimeFilter.min ?? 0, travelTimeFilter.max ?? 120] as [number, number],
representable.map(({ tt, parsed }) => ({
mode: parsed.mode,
noChange: parsed.noChange,
noBuses: parsed.noBuses,
slug: tt.slug,
label: tt.label,
timeRange: [
tt.min ?? 0,
Math.min(tt.max ?? MAX_TRAVEL_MINUTES, MAX_TRAVEL_MINUTES),
] as [number, number],
useBest: false,
}))
);
const firstTravelTime = result.travelTimeFilters[0];
const firstTravelTime = representable[0]?.tt;
if (!firstTravelTime?.slug) return;
try {
@ -411,6 +444,7 @@ export default function MapPage({
}, []);
const pois = usePOIData(mapData.bounds, selectedPOICategories);
const overlaysZoomedIn = (mapData.currentView?.zoom ?? 0) >= POSTCODE_ZOOM_THRESHOLD;
const actualListingsFilterParam = useMemo(
() => buildFilterString(filters, features),
[filters, features]
@ -430,7 +464,8 @@ export default function MapPage({
selectedPOICategories,
rightPaneTab,
entries,
shareCode
shareCode,
activeOverlays
);
useInitialMapPageView(mapData, initialViewState, initialTab, setRightPaneTab);
@ -485,6 +520,7 @@ export default function MapPage({
filters,
features,
travelTimeEntries: entries,
selectedOverlays: activeOverlays,
shareCode,
t,
onExportStateChange,
@ -502,9 +538,19 @@ export default function MapPage({
selectedPOICategories,
rightPaneTab,
entries,
shareCode
shareCode,
activeOverlays
).toString(),
[entries, features, filters, rightPaneTab, selectedPOICategories, shareCode, shareAndSaveView]
[
activeOverlays,
entries,
features,
filters,
rightPaneTab,
selectedPOICategories,
shareCode,
shareAndSaveView,
]
);
const handleSaveSearch = useCallback(
async (name: string) => {
@ -540,6 +586,7 @@ export default function MapPage({
theme={theme}
ogMode={ogMode}
travelTimeEntries={entries}
activeOverlays={activeOverlays}
/>
);
}
@ -595,6 +642,17 @@ export default function MapPage({
</Suspense>
);
const renderOverlayPane = () => (
<Suspense fallback={<PaneFallback />}>
<OverlayPane
selectedOverlays={activeOverlays}
onOverlaysChange={setActiveOverlays}
zoomedIn={overlaysZoomedIn}
onClose={() => setOverlayPaneOpen(false)}
/>
</Suspense>
);
const renderFilters = (options?: { destinationDropdownPortal?: boolean }) => (
<Suspense fallback={<PaneFallback />}>
<Filters
@ -620,6 +678,8 @@ export default function MapPage({
onTravelTimeRangeChange={handleTimeRangeChange}
onTravelTimeDragEnd={handleTravelTimeDragEnd}
onTravelTimeToggleBest={handleToggleBest}
onTravelTimeToggleNoChange={handleToggleNoChange}
onTravelTimeToggleNoBuses={handleToggleNoBuses}
aiFilterLoading={aiFilterLoading}
aiFilterError={aiFilterError}
aiFilterErrorType={aiFilterErrorType}
@ -645,7 +705,14 @@ export default function MapPage({
</Suspense>
);
const handleTogglePoiPane = () => setPoiPaneOpen((open) => !open);
const handleTogglePoiPane = () => {
setOverlayPaneOpen(false);
setPoiPaneOpen((open) => !open);
};
const handleToggleOverlayPane = () => {
setPoiPaneOpen(false);
setOverlayPaneOpen((open) => !open);
};
const handleMobileDrawerTabChange = (tab: 'area' | 'properties') => {
if (tab === 'properties') {
handlePropertiesTabClick();
@ -713,6 +780,7 @@ export default function MapPage({
initialLoading={initialLoading}
mapData={mapData}
pois={pois}
activeOverlays={activeOverlays}
mapViewFeature={mapViewFeature}
filterRange={filterRange}
viewSource={viewSource}
@ -743,6 +811,9 @@ export default function MapPage({
onTogglePoiPane={handleTogglePoiPane}
poiButtonLabel={t('poiPane.pointsOfInterest')}
poiPane={renderPOIPane()}
overlayPaneOpen={overlayPaneOpen}
onToggleOverlayPane={handleToggleOverlayPane}
overlayPane={renderOverlayPane()}
filtersPane={renderFilters({ destinationDropdownPortal: false })}
mobileLegend={
<MobileMapLegend
@ -777,6 +848,7 @@ export default function MapPage({
filtersPane={renderFilters()}
mapData={mapData}
pois={pois}
activeOverlays={activeOverlays}
mapViewFeature={mapViewFeature}
filterRange={filterRange}
viewSource={viewSource}
@ -801,6 +873,9 @@ export default function MapPage({
poiPaneOpen={poiPaneOpen}
onTogglePoiPane={handleTogglePoiPane}
poiPane={renderPOIPane()}
overlayPaneOpen={overlayPaneOpen}
onToggleOverlayPane={handleToggleOverlayPane}
overlayPane={renderOverlayPane()}
showSelectionPane={!!selectedHexagon}
rightPaneWidth={rightPaneWidth}
rightPaneHandlers={rightPaneHandlers}