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,4 +1,4 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import './App.css';
import { getUser } from './auth/authService';
import { getStoredPasskeyUser } from './auth/passkeyService';
@ -17,6 +17,7 @@ import { Button } from './components/ui/button';
import { Filter } from 'lucide-react';
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI } from '@/types';
import { refreshListings, fetchTasksForUser, streamListingGeoJSON, fetchUserPOIs, type StreamingProgress } from '@/services';
import { poiMetricPropertyName, injectPoiMetricProperty } from '@/utils/poiUtils';
function App() {
const [listingData, setListingData] = useState<GeoJSONFeatureCollection | null>(null);
@ -31,6 +32,14 @@ function App() {
const [highlightedProperty, setHighlightedProperty] = useState<string | null>(null);
const [streamingProgress, setStreamingProgress] = useState<StreamingProgress | null>(null);
const [userPOIs, setUserPOIs] = useState<POI[]>([]);
const [poiPickerActive, setPoiPickerActive] = useState(false);
const [pickedPoiLocation, setPickedPoiLocation] = useState<{ lat: number; lng: number } | null>(null);
const [poiMetricSelection, setPoiMetricSelection] = useState<{
poiId: number;
poiName: string;
travelMode: 'WALK' | 'BICYCLE' | 'TRANSIT';
} | null>(null);
const [maxTravelMinutes, setMaxTravelMinutes] = useState<number | undefined>(undefined);
// Ref to track accumulated features during streaming
const accumulatedFeaturesRef = useRef<PropertyFeature[]>([]);
@ -108,7 +117,7 @@ function App() {
try {
for await (const batch of streamListingGeoJSON(user, parameters, (progress) => {
setStreamingProgress(progress);
})) {
}, { includePoiDistances: userPOIs.length > 0 })) {
accumulatedFeaturesRef.current.push(...batch);
scheduleUpdate();
}
@ -125,7 +134,43 @@ function App() {
setIsLoading(false);
setStreamingProgress(null);
}
}, [user]);
}, [user, userPOIs]);
// Compute processed listing data: inject synthetic POI metric property & apply max travel filter
const processedListingData = useMemo(() => {
if (!listingData) return null;
let features = listingData.features;
// Inject synthetic flat property for the selected POI metric
if (poiMetricSelection) {
features = injectPoiMetricProperty(
features,
poiMetricSelection.poiId,
poiMetricSelection.travelMode,
);
}
// Filter by max travel time
if (maxTravelMinutes !== undefined && poiMetricSelection) {
const maxSeconds = maxTravelMinutes * 60;
const propName = poiMetricPropertyName(poiMetricSelection.poiId, poiMetricSelection.travelMode);
features = features.filter((f) => {
const value = (f.properties as Record<string, unknown>)[propName] as number | undefined;
return value !== undefined && value <= maxSeconds;
});
}
return { ...listingData, features };
}, [listingData, poiMetricSelection, maxTravelMinutes]);
// Compute the effective metric string for the heatmap
const effectiveMetric = useMemo(() => {
if (queryParameters?.metric === Metric.poi_travel && poiMetricSelection) {
return poiMetricPropertyName(poiMetricSelection.poiId, poiMetricSelection.travelMode);
}
return queryParameters?.metric;
}, [queryParameters?.metric, poiMetricSelection]);
// Auto-load data with default filters when user is authenticated
useEffect(() => {
@ -142,6 +187,12 @@ function App() {
loadListings(defaultParams);
}, [user, loadListings]);
const handleTaskCompleted = useCallback(() => {
if (queryParameters) {
loadListings(queryParameters);
}
}, [queryParameters, loadListings]);
if (!user) {
return <LoginModal isOpen={user === null} onPasskeyLogin={handlePasskeyLogin} />;
}
@ -179,7 +230,7 @@ function App() {
};
const renderMainContent = () => {
if (!listingData) {
if (!processedListingData) {
return (
<div className="flex-1 flex items-center justify-center bg-muted/20">
<div className="text-center p-8 max-w-md">
@ -205,7 +256,7 @@ function App() {
);
}
if (listingData.features.length === 0) {
if (processedListingData.features.length === 0) {
return (
<div className="flex-1 flex items-center justify-center">
<div className="text-center p-8">
@ -225,10 +276,14 @@ function App() {
{(viewMode === 'map' || viewMode === 'split') && (
<div className={`relative ${viewMode === 'split' ? 'w-1/2' : 'flex-1'}`} style={{ minHeight: 0 }}>
<Map
listingData={listingData}
listingData={processedListingData}
queryParameters={queryParameters}
effectiveMetric={effectiveMetric}
onPropertyClick={handlePropertyClick}
pois={userPOIs}
isPickingPOI={poiPickerActive}
onPoiLocationPick={handlePoiLocationPick}
onCancelPoiPicking={handleCancelPoiPicking}
/>
</div>
)}
@ -237,9 +292,10 @@ function App() {
{(viewMode === 'list' || viewMode === 'split') && (
<div className={`${viewMode === 'split' ? 'w-1/2 border-l' : 'flex-1'}`}>
<ListView
listingData={listingData}
listingData={processedListingData}
onPropertyClick={handlePropertyClick}
highlightedPropertyUrl={highlightedProperty}
poiMetricSelection={poiMetricSelection}
/>
</div>
)}
@ -259,6 +315,20 @@ function App() {
}
};
const handleStartPoiPicking = () => {
setPoiPickerActive(true);
setPickedPoiLocation(null);
};
const handlePoiLocationPick = (lat: number, lng: number) => {
setPickedPoiLocation({ lat, lng });
setPoiPickerActive(false);
};
const handleCancelPoiPicking = () => {
setPoiPickerActive(false);
};
return (
<div className="h-screen flex flex-col overflow-hidden">
{/* Header */}
@ -266,6 +336,7 @@ function App() {
user={user}
taskID={taskID}
onTaskCancelled={handleTaskCancelled}
onTaskCompleted={handleTaskCompleted}
/>
{/* Main content area */}
@ -276,9 +347,15 @@ function App() {
onSubmit={onSubmit}
onMetricChange={handleMetricChange}
isLoading={isLoading}
listingCount={listingData?.features.length}
listingCount={processedListingData?.features.length}
user={user}
onTaskCreated={handlePOITaskCreated}
onStartPoiPicking={handleStartPoiPicking}
pickedPoiLocation={pickedPoiLocation}
userPOIs={userPOIs}
onPoiMetricChange={setPoiMetricSelection}
maxTravelMinutes={maxTravelMinutes}
onMaxTravelMinutesChange={setMaxTravelMinutes}
/>
</div>
@ -295,9 +372,15 @@ function App() {
onSubmit={onSubmit}
onMetricChange={handleMetricChange}
isLoading={isLoading}
listingCount={listingData?.features.length}
listingCount={processedListingData?.features.length}
user={user}
onTaskCreated={handlePOITaskCreated}
onStartPoiPicking={handleStartPoiPicking}
pickedPoiLocation={pickedPoiLocation}
userPOIs={userPOIs}
onPoiMetricChange={setPoiMetricSelection}
maxTravelMinutes={maxTravelMinutes}
onMaxTravelMinutesChange={setMaxTravelMinutes}
/>
</SheetContent>
</Sheet>
@ -316,10 +399,10 @@ function App() {
</div>
{/* Stats Bar */}
{listingData && listingData.features.length > 0 && (
{processedListingData && processedListingData.features.length > 0 && (
<div className="shrink-0">
<StatsBar
listingData={listingData}
listingData={processedListingData}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>