← back to Wine Finder

public/js/app.js

602 lines

// State management
let currentResults = [];
let currentChart = null;

// DOM Elements
const searchInput = document.getElementById('searchInput');
const searchBtn = document.getElementById('searchBtn');
const sortSelect = document.getElementById('sortSelect');
const resultsTable = document.getElementById('resultsTable');
const loadingIndicator = document.getElementById('loadingIndicator');
const noResults = document.getElementById('noResults');
const alertsSection = document.getElementById('alertsSection');
const alertsContainer = document.getElementById('alertsContainer');
const chartSection = document.getElementById('chartSection');
const chartTitle = document.getElementById('chartTitle');
const closeChartBtn = document.getElementById('closeChartBtn');
const logoutBtn = document.getElementById('logoutBtn');

// LocalStorage keys for UI preferences
const LS_SORT_KEY = 'wine-finder.sort';

// Initialize
document.addEventListener('DOMContentLoaded', () => {
  // Hydrate saved sort preference BEFORE any list render
  try {
    const savedSort = localStorage.getItem(LS_SORT_KEY);
    if (savedSort && sortSelect) {
      const opt = Array.from(sortSelect.options).find(o => o.value === savedSort);
      if (opt) sortSelect.value = savedSort;
    }
  } catch (e) { /* localStorage may be disabled */ }
  setupEventListeners();
});

function setupEventListeners() {
  // Search
  searchBtn.addEventListener('click', handleSearch);
  searchInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') {
      handleSearch();
    }
  });

  // Quick search buttons
  document.querySelectorAll('.quick-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const winery = btn.getAttribute('data-winery');
      handleQuickSearch(winery);
    });
  });

  // Sort
  sortSelect.addEventListener('change', () => {
    try { localStorage.setItem(LS_SORT_KEY, sortSelect.value); } catch (e) { /* noop */ }
    sortAndDisplayResults();
  });

  // Chart close
  closeChartBtn.addEventListener('click', () => {
    chartSection.style.display = 'none';
    if (currentChart) {
      currentChart.destroy();
      currentChart = null;
    }
  });

  // Logout
  logoutBtn.addEventListener('click', () => {
    window.location.href = '/logout';
  });
}

// Search handler
async function handleSearch() {
  const query = searchInput.value.trim();

  if (!query) {
    alert('Please enter a search term');
    return;
  }

  await performSearch({ query });
}

// Quick search handler
async function handleQuickSearch(winery) {
  await performSearch({ winery });
}

// Perform search
async function performSearch(params) {
  showLoading();
  hideAlerts();
  hideResults();
  hideChart();

  const btnText = searchBtn.querySelector('.btn-text');
  const btnLoader = searchBtn.querySelector('.btn-loader');

  try {
    btnText.style.display = 'none';
    btnLoader.style.display = 'inline-block';
    searchBtn.disabled = true;

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

    // Add sources to params
    params.sources = sources.length > 0 ? sources : ['kl', 'vivino', 'totalwine'];

    const response = await fetch('/api/scraper/search', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(params),
      credentials: 'same-origin' // Include session cookies
    });

    // Check for authentication errors
    if (response.status === 401) {
      window.location.href = '/login';
      return;
    }

    const data = await response.json();

    if (data.success) {
      currentResults = data.wines || [];

      // Show alerts if any
      if (data.alerts && data.alerts.length > 0) {
        displayAlerts(data.alerts);
      }

      // Display results
      if (currentResults.length > 0) {
        sortAndDisplayResults();
      } else {
        showNoResults();
      }
    } else {
      const errorMsg = data.error || 'Unknown error occurred';
      let detailedError = 'Search failed: ' + errorMsg;

      // Add helpful context based on error type
      if (errorMsg.includes('403') || errorMsg.includes('blocked') || errorMsg.includes('Forbidden')) {
        detailedError += '\n\nNote: The K&L Wine website may be blocking our requests. The app is showing demo data to allow you to test features. Price tracking and alerts will resume when access is restored.';
      } else if (errorMsg.includes('timeout') || errorMsg.includes('ETIMEDOUT')) {
        detailedError += '\n\nThe wine retailer\'s website is taking too long to respond. Please try again in a few moments.';
      } else if (errorMsg.includes('ENOTFOUND') || errorMsg.includes('DNS')) {
        detailedError += '\n\nUnable to reach the wine retailer\'s website. Please check your internet connection.';
      }

      showError(detailedError);
      showNoResults();
    }

  } catch (error) {
    console.error('Search error:', error);

    // Determine error type and show appropriate message
    let errorMessage = 'Connection error. ';

    if (error.name === 'TypeError' && error.message.includes('fetch')) {
      errorMessage += 'Unable to reach the server. Please check your internet connection and try again.';
    } else if (error.name === 'AbortError') {
      errorMessage += 'Request timed out. The server may be experiencing high load. Please try again in a moment.';
    } else if (error.message) {
      errorMessage += error.message;
    } else {
      errorMessage += 'An unexpected error occurred. Please try again or contact support if the issue persists.';
    }

    // Show error in a more visible way
    showError(errorMessage);
    showNoResults();
  } finally {
    hideLoading();
    btnText.style.display = 'inline';
    btnLoader.style.display = 'none';
    searchBtn.disabled = false;
  }
}

// Sort and display results
function sortAndDisplayResults() {
  const sortBy = sortSelect.value;
  let sorted = [...currentResults];

  switch (sortBy) {
    case 'name':
      sorted.sort((a, b) => a.name.localeCompare(b.name));
      break;
    case 'price-asc':
      sorted.sort((a, b) => (a.price || 0) - (b.price || 0));
      break;
    case 'price-desc':
      sorted.sort((a, b) => (b.price || 0) - (a.price || 0));
      break;
    case 'rating-desc':
      sorted.sort((a, b) => (b.rating || 0) - (a.rating || 0));
      break;
  }

  displayResults(sorted);
}

// Display results
function displayResults(wines) {
  resultsTable.innerHTML = '';

  wines.forEach(wine => {
    const wineItem = createWineItem(wine);
    resultsTable.appendChild(wineItem);
  });

  resultsTable.style.display = 'block';
  noResults.style.display = 'none';
}

// Create wine item element
function createWineItem(wine) {
  const item = document.createElement('div');
  item.className = 'wine-item';

  const header = document.createElement('div');
  header.className = 'wine-item-header';

  const name = document.createElement('div');
  name.className = 'wine-name';
  name.textContent = wine.name;

  const price = document.createElement('div');
  price.className = 'wine-price';
  price.textContent = wine.price ? `$${wine.price.toFixed(2)}` : 'N/A';

  header.appendChild(name);
  header.appendChild(price);

  const details = document.createElement('div');
  details.className = 'wine-details';

  if (wine.vintage) {
    details.appendChild(createDetailItem('Vintage:', wine.vintage));
  }

  if (wine.rating) {
    const ratingItem = createDetailItem('Rating:', `${wine.rating} pts`);
    ratingItem.querySelector('.wine-detail-item span:last-child').className = 'wine-rating';
    details.appendChild(ratingItem);
  }

  if (wine.source) {
    details.appendChild(createDetailItem('Source:', wine.source));
  }

  if (wine.availability) {
    details.appendChild(createDetailItem('Availability:', wine.availability));
  }

  const actions = document.createElement('div');
  actions.className = 'wine-actions';

  const historyBtn = document.createElement('button');
  historyBtn.className = 'view-history-btn';
  historyBtn.textContent = 'VIEW PRICE HISTORY';
  historyBtn.addEventListener('click', () => loadWineHistory(wine.name));

  actions.appendChild(historyBtn);

  if (wine.url) {
    const linkBtn = document.createElement('button');
    linkBtn.className = 'view-history-btn';
    linkBtn.textContent = 'VIEW ON SITE';
    linkBtn.style.marginLeft = '10px';
    linkBtn.addEventListener('click', () => window.open(wine.url, '_blank', 'noopener,noreferrer'));
    actions.appendChild(linkBtn);
  }

  item.appendChild(header);
  item.appendChild(details);
  item.appendChild(actions);

  return item;
}

// Create detail item
function createDetailItem(label, value) {
  const wrapper = document.createElement('div');

  const item = document.createElement('div');
  item.className = 'wine-detail-item';

  const labelSpan = document.createElement('span');
  labelSpan.className = 'wine-detail-label';
  labelSpan.textContent = label;

  const valueSpan = document.createElement('span');
  valueSpan.textContent = value;

  item.appendChild(labelSpan);
  item.appendChild(valueSpan);
  wrapper.appendChild(item);

  return wrapper;
}

// Display alerts
function displayAlerts(alerts) {
  alertsContainer.innerHTML = '';

  alerts.forEach(alert => {
    const alertItem = document.createElement('div');
    alertItem.className = 'alert-item';

    const title = document.createElement('div');
    title.className = 'alert-title';
    title.textContent = `🚨 ${alert.wine}`;

    const details = document.createElement('div');
    details.className = 'alert-details';
    details.innerHTML = `
      Price increased from <strong>$${alert.oldPrice.toFixed(2)}</strong>
      to <strong>$${alert.newPrice.toFixed(2)}</strong>
      (${alert.percentageIncrease.toFixed(1)}% increase)
    `;

    alertItem.appendChild(title);
    alertItem.appendChild(details);
    alertsContainer.appendChild(alertItem);
  });

  alertsSection.style.display = 'block';
}

// Load wine history
async function loadWineHistory(wineName) {
  try {
    const response = await fetch(`/api/history/wine?name=${encodeURIComponent(wineName)}`, {
      credentials: 'same-origin' // Include session cookies
    });

    // Check for authentication errors
    if (response.status === 401) {
      window.location.href = '/login';
      return;
    }

    const data = await response.json();

    if (data.success && data.history.length > 0) {
      displayChart(wineName, data.chartData);
    } else {
      showError(`No historical price data available for "${wineName}" yet.\n\nHistorical data is recorded each time you search. Try searching again later to build up price history.`);
    }
  } catch (error) {
    console.error('Error loading history:', error);
    showError('Failed to load price history.\n\nThis could be due to a connection issue with Google Sheets or a temporary server problem. Please try again in a moment.');
  }
}

// Display chart
function displayChart(wineName, chartData) {
  chartTitle.textContent = `PRICE HISTORY: ${wineName}`;
  chartSection.style.display = 'block';

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

  const ctx = document.getElementById('priceChart').getContext('2d');

  currentChart = new Chart(ctx, {
    type: 'line',
    data: {
      labels: chartData.labels,
      datasets: [{
        label: 'Price ($)',
        data: chartData.prices,
        borderColor: '#ec4899',
        backgroundColor: 'rgba(236, 72, 153, 0.15)',
        borderWidth: 3,
        tension: 0.4,
        fill: true,
        pointBackgroundColor: '#ec4899',
        pointBorderColor: '#ffffff',
        pointBorderWidth: 2,
        pointRadius: 7,
        pointHoverRadius: 10,
        pointHoverBackgroundColor: '#db2777',
        pointHoverBorderWidth: 3
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: true,
      aspectRatio: 2,
      plugins: {
        legend: {
          display: true,
          labels: {
            color: '#ffffff',
            font: {
              family: 'Inter',
              size: 15,
              weight: '600'
            },
            padding: 20,
            usePointStyle: true,
            pointStyle: 'circle'
          }
        },
        tooltip: {
          backgroundColor: 'rgba(17, 17, 17, 0.98)',
          titleColor: '#ffffff',
          bodyColor: '#9ca3af',
          borderColor: 'rgba(236, 72, 153, 0.5)',
          borderWidth: 2,
          titleFont: {
            family: 'Inter',
            size: 15,
            weight: '700'
          },
          bodyFont: {
            family: 'Inter',
            size: 14,
            weight: '500'
          },
          padding: 16,
          displayColors: false,
          cornerRadius: 12,
          caretSize: 8
        }
      },
      scales: {
        y: {
          beginAtZero: false,
          ticks: {
            color: '#9ca3af',
            font: {
              family: 'Inter',
              size: 13,
              weight: '500'
            },
            padding: 12,
            callback: function(value) {
              return '$' + value.toFixed(2);
            }
          },
          grid: {
            color: 'rgba(255, 255, 255, 0.06)',
            borderColor: 'rgba(255, 255, 255, 0.1)',
            drawBorder: false
          }
        },
        x: {
          ticks: {
            color: '#9ca3af',
            font: {
              family: 'Inter',
              size: 13,
              weight: '500'
            },
            padding: 12
          },
          grid: {
            color: 'rgba(255, 255, 255, 0.06)',
            borderColor: 'rgba(255, 255, 255, 0.1)',
            drawBorder: false
          }
        }
      }
    }
  });

  // Scroll to chart
  chartSection.scrollIntoView({ behavior: 'smooth' });
}

// UI state functions
function showLoading() {
  loadingIndicator.style.display = 'block';
}

function hideLoading() {
  loadingIndicator.style.display = 'none';
}

function showNoResults() {
  noResults.style.display = 'block';
  resultsTable.style.display = 'none';
}

function hideResults() {
  resultsTable.style.display = 'none';
  noResults.style.display = 'none';
}

function hideAlerts() {
  alertsSection.style.display = 'none';
}

function hideChart() {
  chartSection.style.display = 'none';
  if (currentChart) {
    currentChart.destroy();
    currentChart = null;
  }
}

function showError(message) {
  // Create a more prominent error notification
  const errorDiv = document.createElement('div');
  errorDiv.className = 'error-notification';
  errorDiv.style.cssText = `
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: linear-gradient(135deg, #1a1a1a 0%, #2d1a2d 100%);
    border: 2px solid #ec4899;
    border-radius: 16px;
    padding: 30px;
    max-width: 500px;
    width: 90%;
    box-shadow: 0 10px 40px rgba(236, 72, 153, 0.3);
    z-index: 10000;
    color: #ffffff;
    font-family: 'Inter', sans-serif;
  `;

  const icon = document.createElement('div');
  icon.style.cssText = 'font-size: 48px; text-align: center; margin-bottom: 20px;';
  icon.textContent = '⚠️';

  const text = document.createElement('div');
  text.style.cssText = 'font-size: 16px; line-height: 1.6; white-space: pre-line; margin-bottom: 20px;';
  text.textContent = message;

  const closeBtn = document.createElement('button');
  closeBtn.textContent = 'OK';
  closeBtn.style.cssText = `
    width: 100%;
    padding: 12px;
    background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
    border: none;
    border-radius: 8px;
    color: white;
    font-weight: 600;
    font-size: 14px;
    cursor: pointer;
    transition: all 0.2s;
  `;
  closeBtn.onmouseover = () => {
    closeBtn.style.transform = 'translateY(-2px)';
    closeBtn.style.boxShadow = '0 4px 12px rgba(236, 72, 153, 0.4)';
  };
  closeBtn.onmouseout = () => {
    closeBtn.style.transform = 'translateY(0)';
    closeBtn.style.boxShadow = 'none';
  };
  closeBtn.onclick = () => {
    document.body.removeChild(overlay);
  };

  // Create overlay
  const overlay = document.createElement('div');
  overlay.style.cssText = `
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    backdrop-filter: blur(4px);
    z-index: 9999;
  `;
  overlay.onclick = (e) => {
    if (e.target === overlay) {
      document.body.removeChild(overlay);
    }
  };

  errorDiv.appendChild(icon);
  errorDiv.appendChild(text);
  errorDiv.appendChild(closeBtn);
  overlay.appendChild(errorDiv);
  document.body.appendChild(overlay);

  // Auto-close after 10 seconds if user doesn't click
  setTimeout(() => {
    if (document.body.contains(overlay)) {
      document.body.removeChild(overlay);
    }
  }, 10000);
}