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
|
|
@ -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<GeoJSONFeatureCollection | null>(null);
|
||||
|
|
@ -54,6 +58,7 @@ function AppContent() {
|
|||
} | null>(null);
|
||||
const [poiTravelFilters, setPoiTravelFilters] = useState<Record<number, POITravelFilter>>({});
|
||||
const [currentMetric, setCurrentMetric] = useState<Metric>(DEFAULT_FILTER_VALUES.metric);
|
||||
const [listingType, setListingType] = useState<ListingType>(DEFAULT_FILTER_VALUES.listing_type);
|
||||
const isMobile = useIsMobile();
|
||||
const [, setActiveCardFeature] = useState<PropertyFeature | null>(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<string, unknown>)[key] = (DEFAULT_FILTER_VALUES as Record<string, unknown>)[key];
|
||||
}
|
||||
loadListings(updated);
|
||||
};
|
||||
|
||||
const renderMainContent = () => {
|
||||
if (!processedListingData) {
|
||||
return (
|
||||
|
|
@ -383,7 +413,7 @@ function AppContent() {
|
|||
<div className="text-6xl mb-4 animate-pulse">🏠</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Loading Properties...</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
Fetching listings with default filters. You can adjust filters on the left.
|
||||
Fetching listings with default filters. Adjust filters above to refine results.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -391,7 +421,7 @@ function AppContent() {
|
|||
<div className="text-6xl mb-4">🏠</div>
|
||||
<h2 className="text-xl font-semibold mb-2">Welcome to Property Explorer</h2>
|
||||
<p className="text-muted-foreground mb-4">
|
||||
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.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -520,40 +550,46 @@ function AppContent() {
|
|||
>
|
||||
<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">
|
||||
<Filter className="h-6 w-6" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-80 p-0">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 min-h-0">
|
||||
<FilterPanel
|
||||
onSubmit={onSubmit}
|
||||
currentMetric={currentMetric}
|
||||
isLoading={isLoading}
|
||||
listingCount={processedListingData?.features.length}
|
||||
user={user}
|
||||
onTaskCreated={handlePOITaskCreated}
|
||||
onStartPoiPicking={handleStartPoiPicking}
|
||||
pickedPoiLocation={pickedPoiLocation}
|
||||
userPOIs={userPOIs}
|
||||
poiTravelFilters={poiTravelFilters}
|
||||
onPoiTravelFiltersChange={setPoiTravelFilters}
|
||||
/>
|
||||
<Dialog open={mobileFilterOpen} onOpenChange={setMobileFilterOpen}>
|
||||
<Button size="lg" className="rounded-full shadow-lg h-14 w-14" onClick={() => setMobileFilterOpen(true)}>
|
||||
<Filter className="h-6 w-6" />
|
||||
</Button>
|
||||
<DialogContent className="max-w-full h-full max-h-full rounded-none sm:max-w-full p-0">
|
||||
<DialogHeader className="px-4 pt-4 pb-2">
|
||||
<DialogTitle>Filters</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="flex-1 min-h-0 px-0">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 min-h-0">
|
||||
<FilterPanel
|
||||
onSubmit={(action, params) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 p-4">
|
||||
<VisualizationCard
|
||||
metric={currentMetric}
|
||||
onMetricChange={handleMetricChange}
|
||||
userPOIs={userPOIs}
|
||||
onPoiMetricChange={setPoiMetricSelection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 p-4">
|
||||
<VisualizationCard
|
||||
metric={currentMetric}
|
||||
onMetricChange={handleMetricChange}
|
||||
userPOIs={userPOIs}
|
||||
onPoiMetricChange={setPoiMetricSelection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Bottom Sheet */}
|
||||
|
|
@ -596,7 +632,7 @@ function AppContent() {
|
|||
|
||||
return (
|
||||
<div className="h-screen flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
{/* Header with Listing Type Toggle */}
|
||||
<Header
|
||||
user={user}
|
||||
tasks={tasks}
|
||||
|
|
@ -611,55 +647,60 @@ function AppContent() {
|
|||
return result;
|
||||
}}
|
||||
onTaskCompleted={handleTaskCompleted}
|
||||
listingType={listingType}
|
||||
onListingTypeChange={setListingType}
|
||||
/>
|
||||
|
||||
{isMobile ? (
|
||||
renderMobileLayout()
|
||||
) : (
|
||||
/* Desktop layout */
|
||||
<div className="flex-1 flex overflow-hidden min-h-0">
|
||||
{/* Filter Panel - Desktop (fixed sidebar) */}
|
||||
<div className="w-80 shrink-0 h-full overflow-hidden">
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 min-h-0">
|
||||
<FilterPanel
|
||||
onSubmit={onSubmit}
|
||||
currentMetric={currentMetric}
|
||||
isLoading={isLoading}
|
||||
listingCount={processedListingData?.features.length}
|
||||
user={user}
|
||||
onTaskCreated={handlePOITaskCreated}
|
||||
onStartPoiPicking={handleStartPoiPicking}
|
||||
pickedPoiLocation={pickedPoiLocation}
|
||||
userPOIs={userPOIs}
|
||||
poiTravelFilters={poiTravelFilters}
|
||||
onPoiTravelFiltersChange={setPoiTravelFilters}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0 p-4 border-r">
|
||||
<VisualizationCard
|
||||
metric={currentMetric}
|
||||
onMetricChange={handleMetricChange}
|
||||
userPOIs={userPOIs}
|
||||
onPoiMetricChange={setPoiMetricSelection}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
/* Desktop layout: no sidebar, full-width main area */
|
||||
<>
|
||||
{/* Horizontal Filter Bar */}
|
||||
<FilterBar
|
||||
onSubmit={onSubmit}
|
||||
isLoading={isLoading}
|
||||
user={user}
|
||||
userPOIs={userPOIs}
|
||||
onPOIsChange={setUserPOIs}
|
||||
poiTravelFilters={poiTravelFilters}
|
||||
onPoiTravelFiltersChange={setPoiTravelFilters}
|
||||
listingType={listingType}
|
||||
onListingTypeChange={setListingType}
|
||||
poiPickerActive={poiPickerActive}
|
||||
onPoiPickerActiveChange={setPoiPickerActive}
|
||||
pickedPoiLocation={pickedPoiLocation}
|
||||
onPickedPoiLocationChange={setPickedPoiLocation}
|
||||
currentMetric={currentMetric}
|
||||
onTaskCreated={handlePOITaskCreated}
|
||||
/>
|
||||
|
||||
{/* Active Filter Chips */}
|
||||
{queryParameters && (
|
||||
<FilterChips
|
||||
values={queryParameters}
|
||||
defaults={{ ...DEFAULT_FILTER_VALUES, available_from: new Date() }}
|
||||
onRemove={handleRemoveChip}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Streaming Progress Bar */}
|
||||
<div className="relative shrink-0">
|
||||
<StreamingProgressBar
|
||||
progress={streamingProgress}
|
||||
isLoading={isLoading}
|
||||
onCancel={() => abortControllerRef.current?.abort()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main View Area */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden min-h-0">
|
||||
{/* Streaming Progress Bar */}
|
||||
<div className="relative shrink-0">
|
||||
<StreamingProgressBar progress={streamingProgress} isLoading={isLoading} onCancel={() => abortControllerRef.current?.abort()} />
|
||||
</div>
|
||||
|
||||
{/* Main content area (full width) */}
|
||||
<main className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
{/* Map/List Container */}
|
||||
<div className="flex-1 flex overflow-hidden min-h-0">
|
||||
{renderMainContent()}
|
||||
</div>
|
||||
|
||||
{/* Stats Bar */}
|
||||
{/* Stats Bar with Metric Selector */}
|
||||
{processedListingData && processedListingData.features.length > 0 && (
|
||||
<div className="shrink-0">
|
||||
<StatsBar
|
||||
|
|
@ -667,11 +708,15 @@ function AppContent() {
|
|||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
likedCount={likedCount}
|
||||
metric={currentMetric}
|
||||
onMetricChange={handleMetricChange}
|
||||
userPOIs={userPOIs}
|
||||
onPoiMetricChange={setPoiMetricSelection}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Swipe Review Mode Overlay */}
|
||||
|
|
|
|||
|
|
@ -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