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:
parent
bb489c2032
commit
8509a0326f
9 changed files with 414 additions and 10 deletions
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue