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

@ -1,6 +1,51 @@
import { ExternalLink, Bed, Maximize2, PoundSterling, Clock, Building } from 'lucide-react';
import { ExternalLink, Bed, Maximize2, PoundSterling, Clock, Building, Footprints, Bike, Train } from 'lucide-react';
import { Button } from './ui/button';
import type { PropertyProperties } from '@/types';
import type { PropertyProperties, POIDistanceInfo } from '@/types';
function formatDuration(seconds: number): string {
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}h${mins}m` : `${hours}h`;
}
function TravelModeIcon({ mode }: { mode: string }) {
switch (mode) {
case 'WALK': return <Footprints className="h-3 w-3" />;
case 'BICYCLE': return <Bike className="h-3 w-3" />;
case 'TRANSIT': return <Train className="h-3 w-3" />;
default: return null;
}
}
function POIDistanceBadges({ distances }: { distances: POIDistanceInfo[] }) {
if (!distances || distances.length === 0) return null;
// Group by POI name
const byPoi = new Map<string, POIDistanceInfo[]>();
for (const d of distances) {
const existing = byPoi.get(d.poi_name) || [];
existing.push(d);
byPoi.set(d.poi_name, existing);
}
return (
<div className="flex flex-wrap gap-1.5 mt-1.5">
{Array.from(byPoi.entries()).map(([poiName, dists]) => (
<div key={poiName} className="flex items-center gap-1 text-xs text-muted-foreground bg-muted/50 px-1.5 py-0.5 rounded">
<span className="font-medium">{poiName}:</span>
{dists.map(d => (
<span key={d.travel_mode} className="flex items-center gap-0.5" title={`${d.travel_mode} to ${poiName}`}>
<TravelModeIcon mode={d.travel_mode} />
{formatDuration(d.duration_seconds)}
</span>
))}
</div>
))}
</div>
);
}
interface PropertyCardProps {
property: PropertyProperties;
@ -91,6 +136,7 @@ export function PropertyCard({
</span>
<span className="truncate">{property.agency}</span>
</div>
<POIDistanceBadges distances={property.poi_distances || []} />
</div>
</div>
);
@ -171,6 +217,14 @@ export function PropertyCard({
<span>Seen {lastSeenDays} days ago</span>
</div>
{/* POI Distances */}
{property.poi_distances && property.poi_distances.length > 0 && (
<div className="mt-3 pt-3 border-t">
<div className="text-xs font-medium text-muted-foreground mb-1">Travel times</div>
<POIDistanceBadges distances={property.poi_distances} />
</div>
)}
{/* Price history */}
{property.price_history.length > 1 && (
<div className="mt-3 pt-3 border-t">