← back to Wine Finder Next
components/FilterBar.tsx
188 lines
'use client'
import { useState } from 'react'
interface FilterBarProps {
filters: {
sources: string[]
minPrice: number
maxPrice: number
minRating: number
region: string
wineType: string
}
onChange: (filters: any) => void
}
export default function FilterBar({ filters, onChange }: FilterBarProps) {
const [isExpanded, setIsExpanded] = useState(false)
const sources = [
{ id: 'vivino', label: 'Vivino', icon: '🍇' },
{ id: 'totalwine', label: 'Total Wine', icon: '🏪' },
{ id: 'kl', label: 'K&L', icon: '🍷' },
]
const wineTypes = [
{ value: '', label: 'All Types' },
{ value: 'red', label: 'Red' },
{ value: 'white', label: 'White' },
{ value: 'rosé', label: 'Rosé' },
{ value: 'sparkling', label: 'Sparkling' },
{ value: 'dessert', label: 'Dessert' },
]
const priceRanges = [
{ min: 0, max: 1000, label: 'All Prices' },
{ min: 0, max: 20, label: 'Under $20' },
{ min: 20, max: 50, label: '$20 - $50' },
{ min: 50, max: 100, label: '$50 - $100' },
{ min: 100, max: 1000, label: '$100+' },
]
const handleSourceToggle = (sourceId: string) => {
const newSources = filters.sources.includes(sourceId)
? filters.sources.filter((s) => s !== sourceId)
: [...filters.sources, sourceId]
// Ensure at least one source is selected
if (newSources.length > 0) {
onChange({ sources: newSources })
}
}
const handlePriceRangeChange = (min: number, max: number) => {
onChange({ minPrice: min, maxPrice: max })
}
return (
<div className="bg-white rounded-2xl shadow-wine p-4 sm:p-6 border border-wine-100">
{/* Mobile Toggle */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between sm:hidden mb-4"
aria-label={isExpanded ? 'Hide filters' : 'Show filters'}
>
<span className="font-semibold text-gray-900 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
Filters
</span>
<svg
className={`w-5 h-5 transform transition-transform ${isExpanded ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<div className={`space-y-6 ${isExpanded ? 'block' : 'hidden sm:block'}`}>
{/* Source Filters */}
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<span>Data Sources</span>
<span className="text-xs text-gray-500 font-normal">
({filters.sources.length} selected)
</span>
</h3>
<div className="flex flex-wrap gap-2">
{sources.map((source) => (
<button
key={source.id}
onClick={() => handleSourceToggle(source.id)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all min-h-[44px] ${
filters.sources.includes(source.id)
? 'bg-wine-600 text-white shadow-wine hover:bg-wine-700'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
aria-pressed={filters.sources.includes(source.id)}
>
<span className="mr-1">{source.icon}</span>
{source.label}
</button>
))}
</div>
</div>
{/* Price Range */}
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-3">Price Range</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2">
{priceRanges.map((range) => (
<button
key={range.label}
onClick={() => handlePriceRangeChange(range.min, range.max)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all min-h-[44px] ${
filters.minPrice === range.min && filters.maxPrice === range.max
? 'bg-gold-500 text-white shadow-gold'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
aria-pressed={filters.minPrice === range.min && filters.maxPrice === range.max}
>
{range.label}
</button>
))}
</div>
</div>
{/* Minimum Rating */}
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-3">
Minimum Rating: {filters.minRating > 0 ? `${filters.minRating}/100` : 'Any'}
</h3>
<input
type="range"
min="0"
max="100"
step="10"
value={filters.minRating}
onChange={(e) => onChange({ minRating: parseInt(e.target.value) })}
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-wine-600"
aria-label="Minimum rating slider"
/>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>0</span>
<span>50</span>
<span>100</span>
</div>
</div>
{/* Wine Type */}
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-3">Wine Type</h3>
<select
value={filters.wineType}
onChange={(e) => onChange({ wineType: e.target.value })}
className="w-full px-4 py-3 rounded-lg border-2 border-gray-200 focus:border-wine-600 focus:ring-2 focus:ring-wine-200 outline-none transition-all text-gray-900"
style={{ fontSize: '16px' }} // Prevent iOS zoom
aria-label="Select wine type"
>
{wineTypes.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
{/* Region Search */}
<div>
<h3 className="text-sm font-semibold text-gray-700 mb-3">Region</h3>
<input
type="text"
value={filters.region}
onChange={(e) => onChange({ region: e.target.value })}
placeholder="e.g. Napa Valley, Bordeaux..."
className="w-full px-4 py-3 rounded-lg border-2 border-gray-200 focus:border-wine-600 focus:ring-2 focus:ring-wine-200 outline-none transition-all"
style={{ fontSize: '16px' }} // Prevent iOS zoom
aria-label="Filter by region"
/>
</div>
</div>
</div>
)
}