← 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>
)
}