← back to Wine Finder Next
components/mobile/FilterSheet.tsx
241 lines
'use client'
import { useState } from 'react';
interface FilterSheetProps {
isOpen: boolean;
onClose: () => void;
onApply: (filters: FilterState) => void;
initialFilters?: FilterState;
}
export interface FilterState {
priceMin?: number;
priceMax?: number;
ratingMin?: number;
sources?: string[];
type?: string[];
inStockOnly?: boolean;
sortBy?: 'price-asc' | 'price-desc' | 'rating' | 'name';
}
const WINE_TYPES = ['Red', 'White', 'Rosé', 'Sparkling', 'Dessert', 'Fortified'];
const SOURCES = ['Vivino', 'Wine.com', 'Total Wine', 'K&L Wines', 'Wine Access'];
export default function FilterSheet({
isOpen,
onClose,
onApply,
initialFilters = {}
}: FilterSheetProps) {
const [filters, setFilters] = useState<FilterState>(initialFilters);
const handleApply = () => {
onApply(filters);
onClose();
};
const handleReset = () => {
setFilters({});
};
if (!isOpen) return null;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 z-40 animate-fadeIn"
onClick={onClose}
/>
{/* Bottom Sheet */}
<div className="fixed inset-x-0 bottom-0 z-50 bg-white rounded-t-3xl shadow-2xl max-h-[85vh] overflow-hidden animate-slideUp">
{/* Handle */}
<div className="flex justify-center pt-3 pb-2">
<div className="w-12 h-1 bg-gray-300 rounded-full" />
</div>
{/* Header */}
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h3 className="text-xl font-bold text-gray-900">Filters</h3>
<button
onClick={handleReset}
className="text-purple-600 font-semibold text-sm"
>
Reset All
</button>
</div>
{/* Scrollable Content */}
<div className="overflow-y-auto max-h-[calc(85vh-200px)] px-6 py-4 space-y-6">
{/* Sort By */}
<div>
<label className="block text-sm font-bold text-gray-900 mb-2">
Sort By
</label>
<select
value={filters.sortBy || 'price-asc'}
onChange={(e) => setFilters({ ...filters, sortBy: e.target.value as any })}
className="w-full px-4 py-3 border-2 border-gray-300 rounded-xl text-base focus:border-purple-500 focus:outline-none"
style={{ fontSize: '16px' }} // Prevent iOS zoom
>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="rating">Rating: High to Low</option>
<option value="name">Name: A to Z</option>
</select>
</div>
{/* Price Range */}
<div>
<label className="block text-sm font-bold text-gray-900 mb-2">
Price Range
</label>
<div className="grid grid-cols-2 gap-3">
<input
type="number"
placeholder="Min $"
value={filters.priceMin || ''}
onChange={(e) => setFilters({ ...filters, priceMin: parseInt(e.target.value) || undefined })}
className="px-4 py-3 border-2 border-gray-300 rounded-xl focus:border-purple-500 focus:outline-none"
style={{ fontSize: '16px' }}
/>
<input
type="number"
placeholder="Max $"
value={filters.priceMax || ''}
onChange={(e) => setFilters({ ...filters, priceMax: parseInt(e.target.value) || undefined })}
className="px-4 py-3 border-2 border-gray-300 rounded-xl focus:border-purple-500 focus:outline-none"
style={{ fontSize: '16px' }}
/>
</div>
</div>
{/* Rating */}
<div>
<label className="block text-sm font-bold text-gray-900 mb-2">
Minimum Rating
</label>
<div className="flex gap-2">
{[3, 3.5, 4, 4.5, 5].map((rating) => (
<button
key={rating}
onClick={() => setFilters({ ...filters, ratingMin: rating })}
className={`flex-1 py-3 rounded-xl font-bold transition-all ${
filters.ratingMin === rating
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white shadow-lg'
: 'bg-gray-100 text-gray-700'
}`}
>
{rating}+
</button>
))}
</div>
</div>
{/* Wine Type */}
<div>
<label className="block text-sm font-bold text-gray-900 mb-2">
Wine Type
</label>
<div className="flex flex-wrap gap-2">
{WINE_TYPES.map((type) => (
<button
key={type}
onClick={() => {
const current = filters.type || [];
const updated = current.includes(type)
? current.filter(t => t !== type)
: [...current, type];
setFilters({ ...filters, type: updated.length > 0 ? updated : undefined });
}}
className={`px-4 py-2 rounded-full font-semibold text-sm transition-all ${
(filters.type || []).includes(type)
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-700'
}`}
>
{type}
</button>
))}
</div>
</div>
{/* Sources */}
<div>
<label className="block text-sm font-bold text-gray-900 mb-2">
Retailers
</label>
<div className="space-y-2">
{SOURCES.map((source) => (
<label
key={source}
className="flex items-center gap-3 p-3 border-2 border-gray-200 rounded-xl cursor-pointer hover:border-purple-300 transition-colors"
>
<input
type="checkbox"
checked={(filters.sources || []).includes(source)}
onChange={(e) => {
const current = filters.sources || [];
const updated = e.target.checked
? [...current, source]
: current.filter(s => s !== source);
setFilters({ ...filters, sources: updated.length > 0 ? updated : undefined });
}}
className="w-5 h-5 text-purple-600 rounded"
/>
<span className="font-medium text-gray-900">{source}</span>
</label>
))}
</div>
</div>
{/* In Stock Only */}
<div>
<label className="flex items-center gap-3 p-4 border-2 border-gray-200 rounded-xl cursor-pointer hover:border-purple-300 transition-colors">
<input
type="checkbox"
checked={filters.inStockOnly || false}
onChange={(e) => setFilters({ ...filters, inStockOnly: e.target.checked })}
className="w-6 h-6 text-purple-600 rounded"
/>
<div>
<div className="font-bold text-gray-900">In Stock Only</div>
<div className="text-sm text-gray-600">Show only available wines</div>
</div>
</label>
</div>
</div>
{/* Footer Actions */}
<div className="px-6 py-4 border-t border-gray-200 bg-white flex gap-3">
<button
onClick={onClose}
className="flex-1 py-4 border-2 border-gray-300 text-gray-700 rounded-xl font-bold hover:bg-gray-50 transition-all"
>
Cancel
</button>
<button
onClick={handleApply}
className="flex-1 py-4 bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-xl font-bold hover:from-purple-700 hover:to-pink-700 transition-all shadow-lg"
>
Apply Filters
</button>
</div>
</div>
<style jsx>{`
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`}</style>
</>
);
}