Add frontend POI management and travel time display

POIManager component in FilterPanel for creating/deleting POIs and
triggering distance calculations. PropertyCard shows travel time badges
(walk/cycle/transit) per POI. Map renders POI locations as red markers.
API client extended with POST body support for POI endpoints.
This commit is contained in:
Viktor Barzin 2026-02-08 13:16:32 +00:00
parent bb489c2032
commit 8509a0326f
No known key found for this signature in database
GPG key ID: 0EB088298288D958
9 changed files with 414 additions and 10 deletions

View file

@ -8,7 +8,7 @@ import "../assets/Map.css";
import { Metric, type ParameterValues } from "./Parameters";
import { PropertyCard } from "./PropertyCard";
import { ScrollArea } from "./ui/scroll-area";
import type { GeoJSONFeatureCollection, PropertyFeature, PropertyProperties } from "@/types";
import type { GeoJSONFeatureCollection, PropertyFeature, PropertyProperties, POI } from "@/types";
import { MAP_CONFIG, HEATMAP_CONFIG, PERCENTILE_CONFIG } from "@/constants";
import { getColorSchemeForMetric, getMetricInterpretation } from "@/constants/colorSchemes";
import { clone, percentile, calculateColorStops } from "@/utils/mapUtils";
@ -40,6 +40,7 @@ interface MapProps {
listingData: GeoJSONFeatureCollection;
queryParameters: ParameterValues | null;
onPropertyClick?: (property: PropertyProperties, coordinates: [number, number]) => void;
pois?: POI[];
}
interface FilterState {
@ -58,6 +59,7 @@ export function Map(props: MapProps) {
const updateTimeoutRef = useRef<number | null>(null);
const isMapLoadedRef = useRef<boolean>(false);
const lastDataLengthRef = useRef<number>(0);
const poiMarkersRef = useRef<mapboxgl.Marker[]>([]);
const filter: FilterState = { city: 'London', country: null, mode: Metric.qmprice };
if (props.queryParameters) {
@ -237,6 +239,33 @@ export function Map(props: MapProps) {
};
}, [data, updateHeatmap]);
// Update POI markers when pois prop changes
useEffect(() => {
if (!mapRef.current || !isMapLoadedRef.current) return;
// Remove existing markers
poiMarkersRef.current.forEach(m => m.remove());
poiMarkersRef.current = [];
if (!props.pois) return;
for (const poi of props.pois) {
const el = document.createElement('div');
el.className = 'poi-marker';
el.style.cssText = 'width:24px;height:24px;background:#ef4444;border:2px solid white;border-radius:50%;box-shadow:0 2px 4px rgba(0,0,0,0.3);cursor:pointer;';
el.title = poi.name;
const marker = new mapboxgl.Marker({ element: el })
.setLngLat([poi.longitude, poi.latitude])
.setPopup(new mapboxgl.Popup({ offset: 12 }).setHTML(
`<div style="padding:4px 8px"><strong>${poi.name}</strong><br/><span style="color:#666;font-size:12px">${poi.address}</span></div>`
))
.addTo(mapRef.current);
poiMarkersRef.current.push(marker);
}
}, [props.pois]);
function makeLegend(colorstops: [number, string][], minValue: number, maxValue: number) {
const svg_height = 280, svg_width = 80;
d3.select('#svg').selectAll('*').remove();