diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 171d573..527d6ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,13 +8,16 @@ import AlertError from './components/AlertError'; import LoginModal from './components/LoginModal'; import AuthCallback from './components/AuthCallback'; import { Map } from './components/Map'; -import { FilterPanel, type ParameterValues, DEFAULT_FILTER_VALUES, Metric } from './components/FilterPanel'; +import { type ParameterValues, DEFAULT_FILTER_VALUES, Metric, ListingType } from './components/FilterPanel'; +import { FilterBar } from './components/FilterBar'; +import { FilterChips } from './components/FilterChips'; import { VisualizationCard } from './components/VisualizationCard'; import { Header } from './components/Header'; import { StatsBar } from './components/StatsBar'; import { ListView } from './components/ListView'; import { StreamingProgressBar } from './components/StreamingProgressBar'; -import { Sheet, SheetContent, SheetTrigger } from './components/ui/sheet'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from './components/ui/dialog'; +import { ScrollArea } from './components/ui/scroll-area'; import { Button } from './components/ui/button'; import { Filter, Heart } from 'lucide-react'; import type { GeoJSONFeatureCollection, PropertyProperties, PropertyFeature, POI, POITravelFilter } from '@/types'; @@ -32,6 +35,7 @@ import { MobileBottomSheet } from './components/MobileBottomSheet'; import { SwipeReviewMode } from './components/SwipeReviewMode'; import { FavoritesView } from './components/FavoritesView'; import { ListingDetailSheet } from './components/ListingDetailSheet'; +import { FilterPanel } from './components/FilterPanel'; function AppContent() { const [listingData, setListingData] = useState(null); @@ -54,6 +58,7 @@ function AppContent() { } | null>(null); const [poiTravelFilters, setPoiTravelFilters] = useState>({}); const [currentMetric, setCurrentMetric] = useState(DEFAULT_FILTER_VALUES.metric); + const [listingType, setListingType] = useState(DEFAULT_FILTER_VALUES.listing_type); const isMobile = useIsMobile(); const [, setActiveCardFeature] = useState(null); const [showReviewMode, setShowReviewMode] = useState(false); @@ -373,6 +378,31 @@ function AppContent() { // Optionally: pan map to coordinates }; + /** Handle removing a filter chip: reset the field to its default value and re-submit */ + const handleRemoveChip = (key: keyof ParameterValues) => { + if (!queryParameters) return; + const updated = { ...queryParameters }; + // For paired keys (price, beds) reset both ends + switch (key) { + case 'min_price': + case 'max_price': + updated.min_price = DEFAULT_FILTER_VALUES.min_price; + updated.max_price = DEFAULT_FILTER_VALUES.max_price; + break; + case 'min_bedrooms': + case 'max_bedrooms': + updated.min_bedrooms = DEFAULT_FILTER_VALUES.min_bedrooms; + updated.max_bedrooms = DEFAULT_FILTER_VALUES.max_bedrooms; + break; + case 'furnish_types': + updated.furnish_types = []; + break; + default: + (updated as Record)[key] = (DEFAULT_FILTER_VALUES as Record)[key]; + } + loadListings(updated); + }; + const renderMainContent = () => { if (!processedListingData) { return ( @@ -383,7 +413,7 @@ function AppContent() {
🏠

Loading Properties...

- Fetching listings with default filters. You can adjust filters on the left. + Fetching listings with default filters. Adjust filters above to refine results.

) : ( @@ -391,7 +421,7 @@ function AppContent() {
🏠

Welcome to Property Explorer

- Use the filters on the left to find properties. Apply filters to visualize existing data or refresh to fetch new listings. + Use the filters above to find properties. Apply filters to visualize existing data or refresh to fetch new listings.

)} @@ -520,40 +550,46 @@ function AppContent() { > - - - - - -
-
- + + + + + Filters + + +
+
+ { + setMobileFilterOpen(false); + onSubmit(action, params); + }} + currentMetric={currentMetric} + isLoading={isLoading} + listingCount={processedListingData?.features.length} + user={user} + onTaskCreated={handlePOITaskCreated} + onStartPoiPicking={handleStartPoiPicking} + pickedPoiLocation={pickedPoiLocation} + userPOIs={userPOIs} + poiTravelFilters={poiTravelFilters} + onPoiTravelFiltersChange={setPoiTravelFilters} + /> +
+
+ +
-
- -
-
- - + + +
{/* Bottom Sheet */} @@ -596,7 +632,7 @@ function AppContent() { return (
- {/* Header */} + {/* Header with Listing Type Toggle */}
{isMobile ? ( renderMobileLayout() ) : ( - /* Desktop layout */ -
- {/* Filter Panel - Desktop (fixed sidebar) */} -
-
-
- -
-
- -
-
+ /* Desktop layout: no sidebar, full-width main area */ + <> + {/* Horizontal Filter Bar */} + + + {/* Active Filter Chips */} + {queryParameters && ( + + )} + + {/* Streaming Progress Bar */} +
+ abortControllerRef.current?.abort()} + />
- {/* Main View Area */} -
- {/* Streaming Progress Bar */} -
- abortControllerRef.current?.abort()} /> -
- + {/* Main content area (full width) */} +
{/* Map/List Container */}
{renderMainContent()}
- {/* Stats Bar */} + {/* Stats Bar with Metric Selector */} {processedListingData && processedListingData.features.length > 0 && (
)} -
-
+ + )} {/* Swipe Review Mode Overlay */} diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index aef17cf..022fa20 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -2,6 +2,7 @@ import type { AuthUser } from '@/auth/types'; import type { TaskState } from '@/types'; import { Button } from './ui/button'; import { Separator } from './ui/separator'; +import { Tabs, TabsList, TabsTrigger } from './ui/tabs'; import { LogOut, Home } from 'lucide-react'; import { logout } from '@/auth/authService'; import { clearPasskeyUser } from '@/auth/passkeyService'; @@ -9,6 +10,7 @@ import { HealthIndicator } from './HealthIndicator'; import { TaskIndicator } from './TaskIndicator'; import { MobileMenu } from './MobileMenu'; import { useIsMobile } from '@/hooks/use-mobile'; +import { ListingType } from './FilterPanel'; interface HeaderProps { user: AuthUser; @@ -23,6 +25,9 @@ interface HeaderProps { onCancelTask: (taskId: string) => Promise; onClearAllTasks: () => Promise; onTaskCompleted?: () => void; + // Listing type toggle + listingType?: ListingType; + onListingTypeChange?: (type: ListingType) => void; } export function Header({ @@ -33,6 +38,8 @@ export function Header({ onCancelTask, onClearAllTasks, onTaskCompleted, + listingType, + onListingTypeChange, }: HeaderProps) { const isMobile = useIsMobile(); @@ -53,6 +60,26 @@ export function Header({ Wrongmove
+ {/* Listing Type Toggle (Rent / Buy) */} + {listingType && onListingTypeChange && ( + <> + + onListingTypeChange(v as ListingType)} + > + + + Rent + + + Buy + + + + + )} + {/* Desktop-only items */} {!isMobile && ( <> diff --git a/frontend/src/components/StatsBar.tsx b/frontend/src/components/StatsBar.tsx index 308a346..1c14211 100644 --- a/frontend/src/components/StatsBar.tsx +++ b/frontend/src/components/StatsBar.tsx @@ -1,7 +1,9 @@ -import { BarChart3, MapPin, PoundSterling, Maximize2, List, Map as MapIcon, Heart } from 'lucide-react'; +import { BarChart3, MapPin, PoundSterling, Maximize2, List, Map as MapIcon, Heart, Palette } from 'lucide-react'; import { Button } from './ui/button'; -import type { GeoJSONFeatureCollection, PropertyFeature } from '@/types'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; +import type { GeoJSONFeatureCollection, PropertyFeature, POI } from '@/types'; import { formatCurrency } from '@/utils/format'; +import { Metric } from './FilterPanel'; export type ViewMode = 'map' | 'list' | 'split' | 'saved'; @@ -10,6 +12,11 @@ interface StatsBarProps { viewMode: ViewMode; onViewModeChange: (mode: ViewMode) => void; likedCount?: number; + // Metric selector (moved from VisualizationCard) + metric?: Metric; + onMetricChange?: (metric: Metric) => void; + userPOIs?: POI[]; + onPoiMetricChange?: (selection: { poiId: number; poiName: string; travelMode: 'WALK' | 'BICYCLE' | 'TRANSIT' } | null) => void; } interface ListingStats { @@ -54,7 +61,15 @@ function calculateStats(data: GeoJSONFeatureCollection | null): ListingStats { return { count, avgPrice, avgPricePerSqm, avgSize }; } -export function StatsBar({ listingData, viewMode, onViewModeChange, likedCount = 0 }: StatsBarProps) { +export function StatsBar({ + listingData, + viewMode, + onViewModeChange, + likedCount = 0, + metric, + onMetricChange, + userPOIs, +}: StatsBarProps) { const stats = calculateStats(listingData); return ( @@ -75,57 +90,83 @@ export function StatsBar({ listingData, viewMode, onViewModeChange, likedCount =
- Avg £/m²: {formatCurrency(stats.avgPricePerSqm)} + Avg £/m²: {formatCurrency(stats.avgPricePerSqm)}
- Avg: {Math.round(stats.avgSize)} m² + Avg: {Math.round(stats.avgSize)} m²
)} - {/* View Mode Toggle */} -
- - - - + )} + + {/* View Mode Toggle */} +
+ + + + +
);