← back to Wine Finder Next

components/SearchContent.tsx

323 lines

'use client'

import { useState, useEffect } from 'react'
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import WineCard from '@/components/WineCard'
import SearchBar from '@/components/SearchBar'
import FilterBar from '@/components/FilterBar'
import CategoryDropdowns from '@/components/CategoryDropdowns'
import SortDropdown from '@/components/SortDropdown'
import SkeletonLoader from '@/components/SkeletonLoader'
import EmptyState from '@/components/EmptyState'

interface Wine {
  name: string
  vintage?: string
  price: string
  rating?: number
  image?: string
  source: string
  url?: string
  winery?: string
  region?: string
  country?: string
  type?: string
  varietal?: string
}

interface FilterOptions {
  sources: string[]
  minPrice: number
  maxPrice: number
  minRating: number
  region: string
  wineType: string
}

interface SearchContentProps {
  initialQuery: string
  router: AppRouterInstance
}

export default function SearchContent({ initialQuery, router }: SearchContentProps) {
  const [query, setQuery] = useState(initialQuery)
  const [wines, setWines] = useState<Wine[]>([])
  const [filteredWines, setFilteredWines] = useState<Wine[]>([])
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [sortBy, setSortBy] = useState('price-asc')
  const [filters, setFilters] = useState<FilterOptions>({
    sources: ['vivino', 'totalwine', 'kl'],
    minPrice: 0,
    maxPrice: 1000,
    minRating: 0,
    region: '',
    wineType: '',
  })

  // Load initial search
  useEffect(() => {
    if (initialQuery) {
      performSearch(initialQuery)
    }
  }, [initialQuery])

  // Apply filters and sorting whenever wines, filters, or sortBy changes
  useEffect(() => {
    applyFiltersAndSort()
  }, [wines, filters, sortBy])

  const performSearch = async (searchQuery: string) => {
    if (!searchQuery.trim()) return

    setLoading(true)
    setError(null)

    try {
      const sourcesParam = filters.sources.join(',')
      const response = await fetch(
        `/api/search?q=${encodeURIComponent(searchQuery)}&sources=${sourcesParam}`
      )
      const data = await response.json()

      if (data.error) {
        setError(data.error)
        setWines([])
      } else {
        setWines(data.wines || [])
      }
    } catch (err) {
      console.error('Search error:', err)
      setError('Failed to search wines. Please try again.')
      setWines([])
    } finally {
      setLoading(false)
    }
  }

  const handleSearch = (searchQuery: string) => {
    setQuery(searchQuery)
    router.push(`/search?q=${encodeURIComponent(searchQuery)}`)
    performSearch(searchQuery)
  }

  const applyFiltersAndSort = () => {
    let filtered = [...wines]

    // Filter by source
    if (filters.sources.length > 0) {
      filtered = filtered.filter((wine) =>
        filters.sources.some((source) =>
          wine.source.toLowerCase().includes(source.toLowerCase())
        )
      )
    }

    // Filter by price
    filtered = filtered.filter((wine) => {
      const price = parseFloat(wine.price)
      return price >= filters.minPrice && price <= filters.maxPrice
    })

    // Filter by rating
    if (filters.minRating > 0) {
      filtered = filtered.filter((wine) =>
        wine.rating ? wine.rating >= filters.minRating : false
      )
    }

    // Filter by region
    if (filters.region) {
      filtered = filtered.filter((wine) =>
        wine.region?.toLowerCase().includes(filters.region.toLowerCase()) ||
        wine.country?.toLowerCase().includes(filters.region.toLowerCase())
      )
    }

    // Filter by wine type
    if (filters.wineType) {
      filtered = filtered.filter((wine) =>
        wine.type?.toLowerCase().includes(filters.wineType.toLowerCase()) ||
        wine.varietal?.toLowerCase().includes(filters.wineType.toLowerCase())
      )
    }

    // Sort results
    switch (sortBy) {
      case 'price-asc':
        filtered.sort((a, b) => parseFloat(a.price) - parseFloat(b.price))
        break
      case 'price-desc':
        filtered.sort((a, b) => parseFloat(b.price) - parseFloat(a.price))
        break
      case 'rating-desc':
        filtered.sort((a, b) => (b.rating || 0) - (a.rating || 0))
        break
      case 'rating-asc':
        filtered.sort((a, b) => (a.rating || 0) - (b.rating || 0))
        break
      case 'name':
        filtered.sort((a, b) => a.name.localeCompare(b.name))
        break
    }

    setFilteredWines(filtered)
  }

  const handleFilterChange = (newFilters: Partial<FilterOptions>) => {
    setFilters((prev) => ({ ...prev, ...newFilters }))
  }

  const handleResetFilters = () => {
    setFilters({
      sources: ['vivino', 'totalwine', 'kl'],
      minPrice: 0,
      maxPrice: 1000,
      minRating: 0,
      region: '',
      wineType: '',
    })
  }

  return (
    <div className="min-h-screen bg-gradient-to-br from-wine-50 via-purple-50 to-pink-50">
      {/* Header with Search */}
      <header className="sticky top-0 z-40 bg-gradient-to-r from-wine-900 via-burgundy-800 to-purple-900 shadow-wine-lg">
        <div className="container mx-auto px-4 py-4">
          <div className="flex items-center justify-between mb-4">
            <button
              onClick={() => router.push('/')}
              className="text-white hover:text-gold-300 transition-colors flex items-center gap-2 touch-target"
              aria-label="Back to home"
            >
              <svg
                className="w-6 h-6"
                fill="none"
                stroke="currentColor"
                viewBox="0 0 24 24"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M10 19l-7-7m0 0l7-7m-7 7h18"
                />
              </svg>
              <span className="font-semibold hidden sm:inline">Home</span>
            </button>
            <h1 className="text-xl sm:text-2xl font-bold text-white">
              Wine Search
            </h1>
            <div className="w-11" /> {/* Spacer for centering */}
          </div>

          <SearchBar onSearch={handleSearch} initialQuery={query} />
        </div>
      </header>

      {/* Main Content */}
      <main className="container mx-auto px-4 py-6">
        {/* Results Header with Filter & Sort */}
        {(wines.length > 0 || loading) && (
          <div className="mb-6 animate-slide-down">
            <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-4">
              <div>
                <h2 className="text-2xl font-bold text-gray-900">
                  {loading ? (
                    <span className="animate-pulse">Searching...</span>
                  ) : (
                    <>
                      {filteredWines.length} {filteredWines.length === 1 ? 'Wine' : 'Wines'} Found
                    </>
                  )}
                </h2>
                {query && !loading && (
                  <p className="text-gray-600 mt-1">
                    Results for "{query}"
                  </p>
                )}
              </div>

              <div className="flex items-center gap-3">
                <SortDropdown value={sortBy} onChange={setSortBy} />
                <button
                  onClick={handleResetFilters}
                  className="px-4 py-2 text-sm font-medium text-wine-700 hover:text-wine-900 hover:bg-wine-50 rounded-lg transition-colors touch-target"
                  aria-label="Reset filters"
                >
                  Reset
                </button>
              </div>
            </div>

            <FilterBar filters={filters} onChange={handleFilterChange} />
          </div>
        )}

        {/* Loading State */}
        {loading && (
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
            {[...Array(8)].map((_, i) => (
              <SkeletonLoader key={i} />
            ))}
          </div>
        )}

        {/* Error State */}
        {error && !loading && (
          <EmptyState
            icon="⚠️"
            title="Something went wrong"
            message={error}
            action={{
              label: 'Try Again',
              onClick: () => query && performSearch(query),
            }}
          />
        )}

        {/* Empty Results */}
        {!loading && !error && filteredWines.length === 0 && wines.length > 0 && (
          <EmptyState
            icon="🔍"
            title="No wines match your filters"
            message="Try adjusting your filters or search for something else"
            action={{
              label: 'Reset Filters',
              onClick: handleResetFilters,
            }}
          />
        )}

        {/* No Search Yet */}
        {!loading && !error && wines.length === 0 && !query && (
          <EmptyState
            icon="🍷"
            title="Start your wine discovery"
            message="Search for wines by name, region, or varietal"
          />
        )}

        {/* Wine Grid */}
        {!loading && filteredWines.length > 0 && (
          <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 animate-fade-in">
            {filteredWines.map((wine, index) => (
              <div key={index} className="animate-slide-up" style={{ animationDelay: `${index * 0.05}s` }}>
                <WineCard wine={wine} />
              </div>
            ))}
          </div>
        )}

        {/* Load More (Future Enhancement) */}
        {filteredWines.length > 0 && (
          <div className="text-center mt-12">
            <p className="text-gray-500 text-sm">
              Showing {filteredWines.length} of {wines.length} results
            </p>
          </div>
        )}
      </main>
    </div>
  )
}