Auto-reload listings on task completion and show all POIs in detail view

Thread onTaskCompleted callback from TaskIndicator through Header to App.tsx
so listings auto-refresh when a background task (e.g. POI distance calculation)
completes. Add AllPOIDistances component to PropertyCard that shows all user
POIs with travel times or — placeholder for missing modes.
This commit is contained in:
Viktor Barzin 2026-02-08 15:11:21 +00:00
parent 01dae5dfbd
commit 81d31eaecf
No known key found for this signature in database
GPG key ID: 0EB088298288D958
5 changed files with 227 additions and 88 deletions

View file

@ -1,6 +1,6 @@
import { ExternalLink, Bed, Maximize2, PoundSterling, Clock, Building, Footprints, Bike, Train } from 'lucide-react';
import { Button } from './ui/button';
import type { PropertyProperties, POIDistanceInfo } from '@/types';
import type { PropertyProperties, POIDistanceInfo, POI } from '@/types';
function formatDuration(seconds: number): string {
const minutes = Math.round(seconds / 60);
@ -47,11 +47,43 @@ function POIDistanceBadges({ distances }: { distances: POIDistanceInfo[] }) {
);
}
const TRAVEL_MODES: Array<'WALK' | 'BICYCLE' | 'TRANSIT'> = ['WALK', 'BICYCLE', 'TRANSIT'];
function AllPOIDistances({ pois, distances }: { pois: POI[]; distances?: POIDistanceInfo[] }) {
// Index distances by poi_id + travel_mode for O(1) lookup
const distMap = new Map<string, POIDistanceInfo>();
if (distances) {
for (const d of distances) {
distMap.set(`${d.poi_id}_${d.travel_mode}`, d);
}
}
return (
<div className="flex flex-wrap gap-1.5 mt-1.5">
{pois.map(poi => (
<div key={poi.id} 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">{poi.name}:</span>
{TRAVEL_MODES.map(mode => {
const dist = distMap.get(`${poi.id}_${mode}`);
return (
<span key={mode} className="flex items-center gap-0.5" title={`${mode} to ${poi.name}`}>
<TravelModeIcon mode={mode} />
{dist ? formatDuration(dist.duration_seconds) : '—'}
</span>
);
})}
</div>
))}
</div>
);
}
interface PropertyCardProps {
property: PropertyProperties;
variant?: 'compact' | 'full';
isHighlighted?: boolean;
avgPricePerSqm?: number;
allPOIs?: POI[];
onClick?: () => void;
}
@ -60,6 +92,7 @@ export function PropertyCard({
variant = 'compact',
isHighlighted = false,
avgPricePerSqm,
allPOIs,
onClick,
}: PropertyCardProps) {
const lastSeenDate = property.last_seen.split('T')[0];
@ -218,12 +251,17 @@ export function PropertyCard({
</div>
{/* POI Distances */}
{property.poi_distances && property.poi_distances.length > 0 && (
{allPOIs && allPOIs.length > 0 ? (
<div className="mt-3 pt-3 border-t">
<div className="text-xs font-medium text-muted-foreground mb-1">Travel times</div>
<AllPOIDistances pois={allPOIs} distances={property.poi_distances} />
</div>
) : 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>
)}
) : null}
{/* Price history */}
{property.price_history.length > 1 && (