← back to Watches

src/components/AdvancedFilters.jsx

261 lines

import React, { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { FiFilter, FiX, FiChevronDown, FiSearch } from 'react-icons/fi';

/**
 * Advanced filtering component with multi-select and range filters
 * Features: price range, year range, movement type, series, sorting
 */
function AdvancedFilters({ watches, onFilterChange, className = '' }) {
  const [isExpanded, setIsExpanded] = useState(false);
  const [filters, setFilters] = useState({
    searchTerm: '',
    series: [],
    movementTypes: [],
    priceRange: [0, 100000],
    yearRange: [1948, 2024],
    sortBy: 'appreciation-desc'
  });

  // Extract unique values from watches
  const filterOptions = useMemo(() => {
    const series = [...new Set(watches.map(w => w.series))].sort();
    const movements = [...new Set(watches.map(w => {
      const movement = w.specifications?.movement || '';
      if (movement.includes('Automatic')) return 'Automatic';
      if (movement.includes('Manual')) return 'Manual';
      if (movement.includes('Quartz')) return 'Quartz';
      return 'Other';
    }))].filter(Boolean).sort();

    const prices = watches.map(w => w.priceHistory[w.priceHistory.length - 1]?.price || 0);
    const years = watches.map(w => w.yearIntroduced);

    return {
      series,
      movements,
      minPrice: Math.floor(Math.min(...prices) / 1000) * 1000,
      maxPrice: Math.ceil(Math.max(...prices) / 1000) * 1000,
      minYear: Math.min(...years),
      maxYear: Math.max(...years)
    };
  }, [watches]);

  const updateFilter = (key, value) => {
    const newFilters = { ...filters, [key]: value };
    setFilters(newFilters);
    onFilterChange(newFilters);
  };

  const toggleArrayFilter = (key, value) => {
    const current = filters[key];
    const newValue = current.includes(value)
      ? current.filter(v => v !== value)
      : [...current, value];
    updateFilter(key, newValue);
  };

  const clearFilters = () => {
    const defaultFilters = {
      searchTerm: '',
      series: [],
      movementTypes: [],
      priceRange: [filterOptions.minPrice, filterOptions.maxPrice],
      yearRange: [filterOptions.minYear, filterOptions.maxYear],
      sortBy: 'appreciation-desc'
    };
    setFilters(defaultFilters);
    onFilterChange(defaultFilters);
  };

  const activeFilterCount =
    filters.series.length +
    filters.movementTypes.length +
    (filters.searchTerm ? 1 : 0);

  return (
    <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-md ${className}`}>
      {/* Filter Header */}
      <div
        className="p-4 flex items-center justify-between cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
        onClick={() => setIsExpanded(!isExpanded)}
      >
        <div className="flex items-center space-x-3">
          <FiFilter className="text-omega-red text-xl" />
          <h3 className="text-lg font-semibold text-gray-800 dark:text-white">
            Advanced Filters
          </h3>
          {activeFilterCount > 0 && (
            <span className="bg-omega-red text-white text-xs font-bold px-2 py-1 rounded-full">
              {activeFilterCount}
            </span>
          )}
        </div>
        <div className="flex items-center space-x-2">
          {activeFilterCount > 0 && (
            <button
              onClick={(e) => {
                e.stopPropagation();
                clearFilters();
              }}
              className="text-sm text-gray-500 hover:text-omega-red transition-colors"
            >
              Clear All
            </button>
          )}
          <FiChevronDown
            className={`text-gray-500 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
          />
        </div>
      </div>

      {/* Expanded Filters */}
      <AnimatePresence>
        {isExpanded && (
          <motion.div
            initial={{ height: 0, opacity: 0 }}
            animate={{ height: 'auto', opacity: 1 }}
            exit={{ height: 0, opacity: 0 }}
            transition={{ duration: 0.3 }}
            className="overflow-hidden border-t border-gray-200 dark:border-gray-700"
          >
            <div className="p-6 space-y-6">
              {/* Search */}
              <div>
                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                  Search
                </label>
                <div className="relative">
                  <FiSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" />
                  <input
                    type="text"
                    placeholder="Search by model, reference, or specs..."
                    value={filters.searchTerm}
                    onChange={(e) => updateFilter('searchTerm', e.target.value)}
                    className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-omega-red focus:border-transparent dark:bg-gray-700 dark:text-white"
                  />
                </div>
              </div>

              <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
                {/* Series Filter */}
                <div>
                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                    Series
                  </label>
                  <div className="space-y-2 max-h-40 overflow-y-auto">
                    {filterOptions.series.map(series => (
                      <label key={series} className="flex items-center space-x-2 cursor-pointer">
                        <input
                          type="checkbox"
                          checked={filters.series.includes(series)}
                          onChange={() => toggleArrayFilter('series', series)}
                          className="w-4 h-4 text-omega-red border-gray-300 rounded focus:ring-omega-red"
                        />
                        <span className="text-sm text-gray-700 dark:text-gray-300">{series}</span>
                      </label>
                    ))}
                  </div>
                </div>

                {/* Movement Type Filter */}
                <div>
                  <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                    Movement Type
                  </label>
                  <div className="space-y-2">
                    {filterOptions.movements.map(movement => (
                      <label key={movement} className="flex items-center space-x-2 cursor-pointer">
                        <input
                          type="checkbox"
                          checked={filters.movementTypes.includes(movement)}
                          onChange={() => toggleArrayFilter('movementTypes', movement)}
                          className="w-4 h-4 text-omega-red border-gray-300 rounded focus:ring-omega-red"
                        />
                        <span className="text-sm text-gray-700 dark:text-gray-300">{movement}</span>
                      </label>
                    ))}
                  </div>
                </div>
              </div>

              {/* Price Range */}
              <div>
                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                  Price Range: ${filters.priceRange[0].toLocaleString()} - ${filters.priceRange[1].toLocaleString()}
                </label>
                <div className="flex items-center space-x-4">
                  <input
                    type="range"
                    min={filterOptions.minPrice}
                    max={filterOptions.maxPrice}
                    value={filters.priceRange[0]}
                    onChange={(e) => updateFilter('priceRange', [parseInt(e.target.value), filters.priceRange[1]])}
                    className="flex-1"
                  />
                  <input
                    type="range"
                    min={filterOptions.minPrice}
                    max={filterOptions.maxPrice}
                    value={filters.priceRange[1]}
                    onChange={(e) => updateFilter('priceRange', [filters.priceRange[0], parseInt(e.target.value)])}
                    className="flex-1"
                  />
                </div>
              </div>

              {/* Year Range */}
              <div>
                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                  Year Range: {filters.yearRange[0]} - {filters.yearRange[1]}
                </label>
                <div className="flex items-center space-x-4">
                  <input
                    type="range"
                    min={filterOptions.minYear}
                    max={filterOptions.maxYear}
                    value={filters.yearRange[0]}
                    onChange={(e) => updateFilter('yearRange', [parseInt(e.target.value), filters.yearRange[1]])}
                    className="flex-1"
                  />
                  <input
                    type="range"
                    min={filterOptions.minYear}
                    max={filterOptions.maxYear}
                    value={filters.yearRange[1]}
                    onChange={(e) => updateFilter('yearRange', [filters.yearRange[0], parseInt(e.target.value)])}
                    className="flex-1"
                  />
                </div>
              </div>

              {/* Sort By */}
              <div>
                <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
                  Sort By
                </label>
                <select
                  value={filters.sortBy}
                  onChange={(e) => updateFilter('sortBy', e.target.value)}
                  className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-omega-red focus:border-transparent dark:bg-gray-700 dark:text-white"
                >
                  <option value="appreciation-desc">Highest Appreciation</option>
                  <option value="appreciation-asc">Lowest Appreciation</option>
                  <option value="price-desc">Highest Price</option>
                  <option value="price-asc">Lowest Price</option>
                  <option value="year-desc">Newest First</option>
                  <option value="year-asc">Oldest First</option>
                  <option value="name-asc">Name (A-Z)</option>
                  <option value="name-desc">Name (Z-A)</option>
                </select>
              </div>
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

export default AdvancedFilters;