feat: add SavedView, integrate decisions into App with review mode and client-side filtering

This commit is contained in:
Viktor Barzin 2026-02-21 13:55:05 +00:00
parent 341de89004
commit 43084ef19a
No known key found for this signature in database
GPG key ID: 0EB088298288D958
3 changed files with 124 additions and 6 deletions

View file

@ -15,15 +15,18 @@ import { ListView } from './components/ListView';
import { StreamingProgressBar } from './components/StreamingProgressBar';
import { Sheet, SheetContent, SheetTrigger } from './components/ui/sheet';
import { Button } from './components/ui/button';
import { Filter } from 'lucide-react';
import { Filter, Heart } from 'lucide-react';
import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI, POITravelFilter } from '@/types';
import { refreshListings, streamListingGeoJSON, fetchUserPOIs, type StreamingProgress } from '@/services';
import { setOnUnauthorized } from '@/services/apiClient';
import { clearPasskeyUser } from './auth/passkeyService';
import { poiMetricPropertyName, injectPoiMetricProperty } from '@/utils/poiUtils';
import { useTaskProgress } from '@/hooks/useTaskProgress';
import { useDecisions } from '@/hooks/useDecisions';
import { useIsMobile } from '@/hooks/use-mobile';
import { MobileBottomSheet } from './components/MobileBottomSheet';
import { SwipeReviewMode } from './components/SwipeReviewMode';
import { SavedView } from './components/SavedView';
function isTerminalStatus(status: string): boolean {
return status === 'SUCCESS' || status === 'FAILURE' || status === 'REVOKED';
@ -52,6 +55,10 @@ function App() {
const [currentMetric, setCurrentMetric] = useState<Metric>(DEFAULT_FILTER_VALUES.metric);
const isMobile = useIsMobile();
const [activeCardFeature, setActiveCardFeature] = useState<PropertyFeature | null>(null);
const [showReviewMode, setShowReviewMode] = useState(false);
// Decision state (like/dislike)
const { decide, clear, getDecision, likedCount, isLoaded: isDecisionsLoaded } = useDecisions(user);
// Explicit task ID set by fetch-data action (to track as "active")
const [explicitTaskId, setExplicitTaskId] = useState<string | null>(null);
@ -226,8 +233,18 @@ function App() {
});
}
// Filter out disliked listings (client-side for instant feedback)
if (isDecisionsLoaded) {
features = features.filter((f) => {
const parts = f.properties.url.split('/');
const id = parseInt(parts[parts.length - 1], 10);
const type = f.properties.listing_type === 'BUY' ? 'BUY' : 'RENT';
return getDecision(id, type) !== 'disliked';
});
}
return { ...listingData, features };
}, [listingData, poiMetricSelection, poiTravelFilters]);
}, [listingData, poiMetricSelection, poiTravelFilters, isDecisionsLoaded, getDecision]);
// Compute the effective metric string for the heatmap
const effectiveMetric = useMemo(() => {
@ -345,6 +362,15 @@ function App() {
);
}
if (viewMode === 'saved') {
return (
<SavedView
listingData={processedListingData}
getDecision={getDecision}
/>
);
}
return (
<>
{/* Map View */}
@ -420,8 +446,17 @@ function App() {
)}
</div>
{/* Filter FAB */}
{/* Filter & Review FABs */}
<div className="fixed bottom-24 right-4 z-50 flex flex-col gap-2">
<Button
size="lg"
variant="outline"
className="rounded-full shadow-lg h-14 w-14 bg-background"
onClick={() => setShowReviewMode(true)}
disabled={!processedListingData || processedListingData.features.length === 0}
>
<Heart className="h-6 w-6" />
</Button>
<Sheet open={mobileFilterOpen} onOpenChange={setMobileFilterOpen}>
<SheetTrigger asChild>
<Button size="lg" className="rounded-full shadow-lg h-14 w-14">
@ -567,6 +602,7 @@ function App() {
listingData={processedListingData}
viewMode={viewMode}
onViewModeChange={setViewMode}
likedCount={likedCount}
/>
</div>
)}
@ -574,6 +610,17 @@ function App() {
</div>
)}
{/* Swipe Review Mode Overlay */}
{showReviewMode && processedListingData && (
<SwipeReviewMode
features={processedListingData.features}
onDecide={decide}
onClear={clear}
onClose={() => setShowReviewMode(false)}
getDecision={getDecision}
/>
)}
{/* Error Dialog */}
<AlertError message={submitError} open={alertDialogIsOpen} setIsOpen={setAlertDialogIsOpen} />
</div>