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:
parent
01dae5dfbd
commit
81d31eaecf
5 changed files with 227 additions and 88 deletions
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue