feat: integrate FilterBar into layout, remove sidebar

Replace the fixed w-80 sidebar with a horizontal FilterBar below the
header, giving the map full viewport width. Key changes:

- App.tsx: Remove sidebar layout, add FilterBar + FilterChips + inline
  StreamingProgressBar between header and main content area
- Header.tsx: Add Rent/Buy listing type toggle (compact Tabs) after logo
- StatsBar.tsx: Add "Color by" metric selector (moved from
  VisualizationCard) as a compact Select alongside view mode toggles
- Mobile: Replace Sheet-based filter panel with full-screen Dialog
This commit is contained in:
Viktor Barzin 2026-02-28 16:16:03 +00:00
parent 4053c0c759
commit 8f112f30e3
No known key found for this signature in database
GPG key ID: 0EB088298288D958
3 changed files with 235 additions and 122 deletions

View file

@ -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<boolean>;
onClearAllTasks: () => Promise<boolean>;
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({
<span className="font-semibold text-lg hidden sm:inline">Wrongmove</span>
</div>
{/* Listing Type Toggle (Rent / Buy) */}
{listingType && onListingTypeChange && (
<>
<Separator orientation="vertical" className="h-6" />
<Tabs
value={listingType}
onValueChange={(v) => onListingTypeChange(v as ListingType)}
>
<TabsList className="h-8 w-auto p-0.5">
<TabsTrigger value={ListingType.RENT} className="h-7 px-3 text-xs flex-initial">
Rent
</TabsTrigger>
<TabsTrigger value={ListingType.BUY} className="h-7 px-3 text-xs flex-initial">
Buy
</TabsTrigger>
</TabsList>
</Tabs>
</>
)}
{/* Desktop-only items */}
{!isMobile && (
<>

View file

@ -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 =
</div>
<div className="hidden lg:flex items-center gap-1.5">
<BarChart3 className="h-4 w-4" />
<span>Avg £/m²: <span className="font-medium text-foreground">{formatCurrency(stats.avgPricePerSqm)}</span></span>
<span>Avg &pound;/m&sup2;: <span className="font-medium text-foreground">{formatCurrency(stats.avgPricePerSqm)}</span></span>
</div>
<div className="hidden lg:flex items-center gap-1.5">
<Maximize2 className="h-4 w-4" />
<span>Avg: <span className="font-medium text-foreground">{Math.round(stats.avgSize)} m²</span></span>
<span>Avg: <span className="font-medium text-foreground">{Math.round(stats.avgSize)} m&sup2;</span></span>
</div>
</>
)}
</div>
{/* View Mode Toggle */}
<div className="flex items-center gap-1 bg-background rounded-md border p-0.5">
<Button
variant={viewMode === 'map' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2"
onClick={() => onViewModeChange('map')}
>
<MapIcon className="h-4 w-4" />
<span className="hidden sm:inline ml-1">Map</span>
</Button>
<Button
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2"
onClick={() => onViewModeChange('list')}
>
<List className="h-4 w-4" />
<span className="hidden sm:inline ml-1">List</span>
</Button>
<Button
variant={viewMode === 'split' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2 hidden md:flex"
onClick={() => onViewModeChange('split')}
>
<div className="flex gap-0.5">
<div className="w-2 h-4 bg-current rounded-sm opacity-60" />
<div className="w-2 h-4 border border-current rounded-sm" />
<div className="flex items-center gap-2">
{/* Color-by Metric Selector */}
{metric && onMetricChange && (
<div className="hidden md:flex items-center gap-1.5">
<Palette className="h-3.5 w-3.5 text-muted-foreground" />
<Select
value={metric}
onValueChange={(value) => onMetricChange(value as Metric)}
>
<SelectTrigger className="h-7 text-xs w-[110px] border-0 bg-transparent shadow-none">
<SelectValue placeholder="Color by" />
</SelectTrigger>
<SelectContent>
<SelectItem value={Metric.qmprice}>Price/m&sup2;</SelectItem>
<SelectItem value={Metric.rooms}>Bedrooms</SelectItem>
<SelectItem value={Metric.qm}>Size (m&sup2;)</SelectItem>
<SelectItem value={Metric.price}>Total Price</SelectItem>
{userPOIs && userPOIs.length > 0 && (
<SelectItem value={Metric.poi_travel}>Travel Time</SelectItem>
)}
</SelectContent>
</Select>
</div>
<span className="hidden sm:inline ml-1">Split</span>
</Button>
<Button
variant={viewMode === 'saved' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2"
onClick={() => onViewModeChange('saved')}
>
<Heart className="h-4 w-4" />
<span className="hidden sm:inline ml-1">Saved{likedCount > 0 ? ` (${likedCount})` : ''}</span>
</Button>
)}
{/* View Mode Toggle */}
<div className="flex items-center gap-1 bg-background rounded-md border p-0.5">
<Button
variant={viewMode === 'map' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2"
onClick={() => onViewModeChange('map')}
>
<MapIcon className="h-4 w-4" />
<span className="hidden sm:inline ml-1">Map</span>
</Button>
<Button
variant={viewMode === 'list' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2"
onClick={() => onViewModeChange('list')}
>
<List className="h-4 w-4" />
<span className="hidden sm:inline ml-1">List</span>
</Button>
<Button
variant={viewMode === 'split' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2 hidden md:flex"
onClick={() => onViewModeChange('split')}
>
<div className="flex gap-0.5">
<div className="w-2 h-4 bg-current rounded-sm opacity-60" />
<div className="w-2 h-4 border border-current rounded-sm" />
</div>
<span className="hidden sm:inline ml-1">Split</span>
</Button>
<Button
variant={viewMode === 'saved' ? 'secondary' : 'ghost'}
size="sm"
className="h-7 px-2"
onClick={() => onViewModeChange('saved')}
>
<Heart className="h-4 w-4" />
<span className="hidden sm:inline ml-1">Saved{likedCount > 0 ? ` (${likedCount})` : ''}</span>
</Button>
</div>
</div>
</div>
);