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:
parent
4053c0c759
commit
8f112f30e3
3 changed files with 235 additions and 122 deletions
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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 £/m²: <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²</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²</SelectItem>
|
||||
<SelectItem value={Metric.rooms}>Bedrooms</SelectItem>
|
||||
<SelectItem value={Metric.qm}>Size (m²)</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>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue