← back to Wine Finder

public/js/dynamic-search.js

397 lines

/**
 * Dynamic Search Enhancement
 * Real-time search, advanced filters, and stock-style price charts
 */

// State
let searchTimeout = null;
let previousSearchResults = [];
let currentViewMode = 'grid';

// DOM Elements
const searchInput = document.getElementById('searchInput');
const clearSearchBtn = document.getElementById('clearSearch');
const searchResultCount = document.getElementById('searchResultCount');
const toggleFiltersBtn = document.getElementById('toggleFilters');
const advancedFilters = document.getElementById('advancedFilters');
const filterChevron = document.getElementById('filterChevron');
const statsSection = document.getElementById('statsSection');

// Filter elements
const minPriceSlider = document.getElementById('minPrice');
const maxPriceSlider = document.getElementById('maxPrice');
const minRatingSlider = document.getElementById('minRating');
const vintageYearSlider = document.getElementById('vintageYear');
const countryFilter = document.getElementById('countryFilter');
const applyFiltersBtn = document.getElementById('applyFilters');
const resetFiltersBtn = document.getElementById('resetFilters');

// View mode buttons
const viewModeGrid = document.getElementById('viewModeGrid');
const viewModeList = document.getElementById('viewModeList');

// Initialize dynamic search
function initDynamicSearch() {
  // Real-time search with debouncing
  searchInput.addEventListener('input', (e) => {
    const query = e.target.value.trim();

    // Show/hide clear button
    clearSearchBtn.style.display = query ? 'flex' : 'none';

    // Debounce search
    clearTimeout(searchTimeout);
    if (query.length >= 2) {
      searchTimeout = setTimeout(() => {
        performDynamicSearch(query);
      }, 500); // 500ms debounce
    } else if (query.length === 0) {
      hideResults();
      statsSection.style.display = 'none';
      searchResultCount.textContent = '';
    }
  });

  // Clear search
  clearSearchBtn.addEventListener('click', () => {
    searchInput.value = '';
    clearSearchBtn.style.display = 'none';
    hideResults();
    statsSection.style.display = 'none';
    searchResultCount.textContent = '';
  });

  // Toggle advanced filters
  toggleFiltersBtn.addEventListener('click', () => {
    const isHidden = advancedFilters.style.display === 'none';
    advancedFilters.style.display = isHidden ? 'block' : 'none';
    filterChevron.textContent = isHidden ? '▲' : '▼';
    advancedFilters.classList.toggle('filters-open', isHidden);
  });

  // Filter range sliders - update values
  minPriceSlider.addEventListener('input', updatePriceRangeDisplay);
  maxPriceSlider.addEventListener('input', updatePriceRangeDisplay);
  minRatingSlider.addEventListener('input', () => {
    document.getElementById('ratingValue').textContent = `${minRatingSlider.value}+`;
  });
  vintageYearSlider.addEventListener('input', () => {
    const year = vintageYearSlider.value;
    document.getElementById('vintageValue').textContent = year === '1990' ? 'Any' : `${year}+`;
  });

  // Apply filters
  applyFiltersBtn.addEventListener('click', () => {
    if (currentResults.length > 0) {
      sortAndDisplayResults();
    }
  });

  // Reset filters
  resetFiltersBtn.addEventListener('click', resetAllFilters);

  // View mode toggle
  viewModeGrid.addEventListener('click', () => setViewMode('grid'));
  viewModeList.addEventListener('click', () => setViewMode('list'));
}

// Update price range display
function updatePriceRangeDisplay() {
  let minPrice = parseInt(minPriceSlider.value);
  let maxPrice = parseInt(maxPriceSlider.value);

  // Ensure min doesn't exceed max
  if (minPrice > maxPrice) {
    const temp = maxPrice;
    maxPrice = minPrice;
    minPrice = temp;
    minPriceSlider.value = minPrice;
    maxPriceSlider.value = maxPrice;
  }

  const maxText = maxPrice >= 500 ? '$500+' : `$${maxPrice}`;
  document.getElementById('priceRangeValue').textContent = `$${minPrice} - ${maxText}`;
}

// Reset all filters
function resetAllFilters() {
  minPriceSlider.value = 0;
  maxPriceSlider.value = 500;
  minRatingSlider.value = 0;
  vintageYearSlider.value = 1990;
  countryFilter.value = '';

  document.getElementById('priceRangeValue').textContent = '$0 - $500+';
  document.getElementById('ratingValue').textContent = '0.0+';
  document.getElementById('vintageValue').textContent = 'Any';

  if (currentResults.length > 0) {
    sortAndDisplayResults();
  }
}

// Perform dynamic search
async function performDynamicSearch(query) {
  showLoading();
  updateSearchResultCount('Searching...');

  try {
    // Get selected sources
    const sources = [];
    document.querySelectorAll('.source-checkbox input:checked').forEach(checkbox => {
      sources.push(checkbox.value);
    });

    const response = await fetch('/api/scraper/search', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        query,
        sources: sources.length > 0 ? sources : ['vivino', 'totalwine', 'ucdavis-ava']
      })
    });

    if (!response.ok) {
      throw new Error('Search failed');
    }

    const data = await response.json();

    if (data.success && data.wines) {
      currentResults = data.wines;
      previousSearchResults = [...currentResults];
      displayResults(data.wines);
      updateSearchStats(data.wines);
      updateSearchResultCount(`${data.wines.length} result${data.wines.length !== 1 ? 's' : ''}`);
      statsSection.style.display = 'block';
    } else {
      showNoResults();
      statsSection.style.display = 'none';
    }
  } catch (error) {
    console.error('Dynamic search error:', error);
    showNoResults();
    updateSearchResultCount('Error');
  } finally {
    hideLoading();
  }
}

// Update search result count badge
function updateSearchResultCount(text) {
  searchResultCount.textContent = text;
  searchResultCount.style.display = text ? 'inline-block' : 'none';
}

// Update search statistics
function updateSearchStats(wines) {
  const totalResults = wines.length;
  const winesWithPrices = wines.filter(w => w.price && w.price > 0);
  const winesWithRatings = wines.filter(w => w.rating && w.rating > 0);

  const avgPrice = winesWithPrices.length > 0
    ? winesWithPrices.reduce((sum, w) => sum + w.price, 0) / winesWithPrices.length
    : 0;

  const avgRating = winesWithRatings.length > 0
    ? winesWithRatings.reduce((sum, w) => sum + w.rating, 0) / winesWithRatings.length
    : 0;

  const sources = new Set(wines.map(w => w.source)).size;

  // Calculate price change vs previous search
  let priceChange = 0;
  let priceChangePercent = 0;
  if (previousSearchResults.length > 0) {
    const prevWinesWithPrices = previousSearchResults.filter(w => w.price && w.price > 0);
    const prevAvgPrice = prevWinesWithPrices.length > 0
      ? prevWinesWithPrices.reduce((sum, w) => sum + w.price, 0) / prevWinesWithPrices.length
      : 0;

    if (prevAvgPrice > 0) {
      priceChange = avgPrice - prevAvgPrice;
      priceChangePercent = (priceChange / prevAvgPrice) * 100;
    }
  }

  // Update stat cards with animation
  animateStatValue('statTotalResults', totalResults);
  animateStatValue('statAvgPrice', `$${avgPrice.toFixed(2)}`);
  animateStatValue('statAvgRating', avgRating.toFixed(1));
  animateStatValue('statSourceCount', sources);

  // Update price change indicator
  const priceChangeEl = document.getElementById('statPriceChange');
  if (priceChange !== 0) {
    const changeText = `${priceChange > 0 ? '+' : ''}$${Math.abs(priceChange).toFixed(2)} (${priceChangePercent > 0 ? '+' : ''}${priceChangePercent.toFixed(1)}%)`;
    priceChangeEl.textContent = changeText;
    priceChangeEl.className = `stat-change ${priceChange > 0 ? 'positive' : 'negative'}`;
    priceChangeEl.style.display = 'inline-block';
  } else {
    priceChangeEl.style.display = 'none';
  }
}

// Animate stat value changes
function animateStatValue(elementId, value) {
  const element = document.getElementById(elementId);
  if (!element) return;

  element.classList.add('stat-updating');
  setTimeout(() => {
    element.textContent = value;
    element.classList.remove('stat-updating');
  }, 150);
}

// Set view mode (grid/list)
function setViewMode(mode) {
  currentViewMode = mode;
  viewModeGrid.classList.toggle('active', mode === 'grid');
  viewModeList.classList.toggle('active', mode === 'list');

  const resultsTable = document.getElementById('resultsTable');
  resultsTable.classList.toggle('grid-view', mode === 'grid');
  resultsTable.classList.toggle('list-view', mode === 'list');

  // Re-display results with new view
  if (currentResults.length > 0) {
    displayResults(currentResults);
  }
}

// Enhanced chart creation with stock-style
window.createStockStyleChart = function(wineName, priceHistory) {
  if (!priceHistory || priceHistory.length === 0) {
    console.warn('No price history available');
    return;
  }

  const chartSection = document.getElementById('chartSection');
  const chartTitle = document.getElementById('chartTitle');
  const chartCanvas = document.getElementById('priceChart');

  chartSection.style.display = 'block';
  chartTitle.textContent = `${wineName} - Price Trends`;

  // Prepare data
  const labels = priceHistory.map(p => new Date(p.timestamp).toLocaleDateString());
  const prices = priceHistory.map(p => p.price);

  const currentPrice = prices[prices.length - 1];
  const firstPrice = prices[0];
  const priceChange = currentPrice - firstPrice;
  const priceChangePercent = ((priceChange / firstPrice) * 100);

  const isPositive = priceChange >= 0;

  // Update chart stats
  document.getElementById('chartCurrentPrice').textContent = `$${currentPrice.toFixed(2)}`;
  document.getElementById('chartPriceChange').textContent =
    `${isPositive ? '+' : ''}$${priceChange.toFixed(2)} (${isPositive ? '+' : ''}${priceChangePercent.toFixed(2)}%)`;
  document.getElementById('chartPriceChange').className =
    `chart-price-change ${isPositive ? 'positive' : 'negative'}`;

  // Create gradient
  const ctx = chartCanvas.getContext('2d');
  const gradient = ctx.createLinearGradient(0, 0, 0, 400);
  gradient.addColorStop(0, isPositive ? 'rgba(16, 185, 129, 0.4)' : 'rgba(239, 68, 68, 0.4)');
  gradient.addColorStop(1, isPositive ? 'rgba(16, 185, 129, 0.01)' : 'rgba(239, 68, 68, 0.01)');

  // Destroy existing chart
  if (currentChart) {
    currentChart.destroy();
  }

  // Create new chart
  currentChart = new Chart(ctx, {
    type: 'line',
    data: {
      labels: labels,
      datasets: [{
        label: 'Price (USD)',
        data: prices,
        borderColor: isPositive ? '#10b981' : '#ef4444',
        backgroundColor: gradient,
        borderWidth: 3,
        fill: true,
        tension: 0.4,
        pointRadius: 4,
        pointHoverRadius: 6,
        pointBackgroundColor: '#ffffff',
        pointBorderColor: isPositive ? '#10b981' : '#ef4444',
        pointBorderWidth: 2
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      interaction: {
        intersect: false,
        mode: 'index'
      },
      plugins: {
        legend: {
          display: false
        },
        tooltip: {
          backgroundColor: 'rgba(17, 17, 17, 0.95)',
          titleColor: '#ffffff',
          bodyColor: '#9ca3af',
          padding: 12,
          displayColors: false,
          callbacks: {
            label: function(context) {
              return `$${context.parsed.y.toFixed(2)}`;
            }
          }
        }
      },
      scales: {
        x: {
          grid: {
            color: 'rgba(255, 255, 255, 0.05)',
            drawBorder: false
          },
          ticks: {
            color: '#6b7280',
            maxRotation: 45,
            minRotation: 45
          }
        },
        y: {
          grid: {
            color: 'rgba(255, 255, 255, 0.05)',
            drawBorder: false
          },
          ticks: {
            color: '#6b7280',
            callback: function(value) {
              return '$' + value.toFixed(0);
            }
          },
          beginAtZero: false
        }
      }
    }
  });

  // Timeframe buttons
  document.querySelectorAll('.chart-timeframe-btn').forEach(btn => {
    btn.addEventListener('click', function() {
      document.querySelectorAll('.chart-timeframe-btn').forEach(b => b.classList.remove('active'));
      this.classList.add('active');
      // TODO: Filter data by timeframe
    });
  });
};

// Initialize on DOM load
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', initDynamicSearch);
} else {
  initDynamicSearch();
}