← back to Handbag Auth Nextjs
src/components/ListingGrid.tsx
225 lines
'use client'
import Image from 'next/image'
import Link from 'next/link'
import { useState } from 'react'
interface ListingGridProps {
listings: any[]
loading: boolean
}
export function ListingGrid({ listings, loading }: ListingGridProps) {
const [imageErrors, setImageErrors] = useState<Set<number>>(new Set())
const [loadedImages, setLoadedImages] = useState<Set<number>>(new Set())
const handleImageError = (id: number) => {
setImageErrors(prev => new Set(prev).add(id))
}
const handleImageLoad = (id: number) => {
setLoadedImages(prev => new Set(prev).add(id))
}
if (loading) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="bg-white rounded-2xl shadow-lg overflow-hidden animate-pulse">
<div className="w-full h-64 sm:h-80 bg-gradient-to-br from-gray-200 to-gray-300"></div>
<div className="p-4 sm:p-5 space-y-3">
<div className="h-4 bg-gray-300 rounded-full w-3/4"></div>
<div className="h-4 bg-gray-300 rounded-full w-1/2"></div>
<div className="h-8 bg-gray-300 rounded-lg w-full"></div>
</div>
</div>
))}
</div>
)
}
if (listings.length === 0) {
return (
<div className="text-center py-16 px-4">
<div className="w-20 h-20 mx-auto mb-4 bg-gradient-to-br from-purple-100 to-blue-100 rounded-full flex items-center justify-center">
<svg
className="w-10 h-10 text-purple-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-2">No listings found</h3>
<p className="text-gray-500 mb-6">Try adjusting your filters or search terms</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-gradient-to-r from-purple-600 to-blue-500 text-white rounded-lg font-semibold hover:shadow-lg transition-all"
style={{ fontSize: '16px', minHeight: '48px' }}
>
Reset Filters
</button>
</div>
)
}
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
{listings.map((listing: any, index: number) => (
<div
key={listing.id}
className="bg-white rounded-2xl shadow-lg overflow-hidden hover:shadow-2xl transition-all duration-200 transform hover:-translate-y-1 active:scale-98 cursor-pointer"
style={{
animation: `fadeIn 0.3s ease-out ${index * 0.05}s both`
}}
>
{/* Image Section */}
<div className="relative w-full h-64 sm:h-80 bg-gradient-to-br from-gray-100 to-gray-200 overflow-hidden">
{(listing.imageUrl || listing.thumbnailUrl) && !imageErrors.has(listing.id) ? (
<>
<div
className={`absolute inset-0 bg-gradient-to-br from-purple-100 to-blue-100 transition-opacity duration-500 ${
loadedImages.has(listing.id) ? 'opacity-0' : 'opacity-100'
}`}
style={{
filter: 'blur(20px)',
transform: 'scale(1.1)'
}}
/>
<Image
src={listing.imageUrl || listing.thumbnailUrl}
alt={listing.title || 'Handbag'}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className={`object-cover transition-all duration-500 ${
loadedImages.has(listing.id) ? 'opacity-100 scale-100' : 'opacity-0 scale-105'
}`}
unoptimized
loading={index < 3 ? 'eager' : 'lazy'}
onLoad={() => handleImageLoad(listing.id)}
onError={() => handleImageError(listing.id)}
/>
</>
) : (
<div className="w-full h-full flex items-center justify-center">
<svg
className="w-16 h-16 text-gray-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
)}
{/* Deal Badge */}
{listing.dealAnalysis?.isDeal === 1 && listing.dealAnalysis?.dealPercentage && (
<div className="absolute top-3 right-3 bg-gradient-to-r from-red-500 to-pink-500 text-white px-3 py-1.5 rounded-full text-sm font-bold shadow-lg animate-pulse">
{Math.round(listing.dealAnalysis.dealPercentage)}% OFF
</div>
)}
{/* Brand Badge */}
<div className="absolute bottom-3 left-3 bg-white/95 backdrop-blur-sm px-3 py-1.5 rounded-full shadow-lg">
<span className="text-xs font-bold text-purple-600 uppercase tracking-wide">{listing.brand}</span>
</div>
</div>
{/* Content Section */}
<div className="p-4 sm:p-5">
{/* Title & Condition */}
<div className="flex items-start justify-between gap-2 mb-3">
<h3 className="text-base sm:text-lg font-bold text-gray-900 line-clamp-2 flex-1">
{listing.title}
</h3>
{listing.condition && (
<span className="text-xs text-gray-700 bg-gray-100 px-2.5 py-1 rounded-full font-medium whitespace-nowrap">
{listing.condition}
</span>
)}
</div>
{/* Model */}
{listing.model && (
<p className="text-sm text-gray-600 mb-3 font-medium line-clamp-1">
{listing.model}
</p>
)}
{/* Price Section - Mobile Optimized */}
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl p-3 sm:p-4 mb-4">
<div className="flex items-center justify-between">
<div>
<p className="text-2xl sm:text-3xl font-bold text-gray-900">
${listing.priceUsd?.toLocaleString() || 'N/A'}
</p>
{listing.priceJpy && (
<p className="text-xs sm:text-sm text-gray-600 mt-1">
¥{listing.priceJpy.toLocaleString()}
</p>
)}
</div>
{listing.dealAnalysis?.avgUsPrice && (
<div className="text-right">
<p className="text-xs sm:text-sm text-gray-500 line-through">
${listing.dealAnalysis.avgUsPrice.toLocaleString()}
</p>
<p className="text-xs sm:text-sm font-bold text-green-600">
Save ${(listing.dealAnalysis.avgUsPrice - listing.priceUsd).toLocaleString()}
</p>
</div>
)}
</div>
</div>
{/* Quick Info - Simplified for Mobile */}
<div className="grid grid-cols-2 gap-2 mb-4 text-xs">
{listing.listingType && (
<div className="bg-gray-50 rounded-lg p-2">
<p className="text-gray-500 uppercase tracking-wide font-medium mb-0.5">Type</p>
<p className={`font-bold ${listing.listingType === 'auction' ? 'text-yellow-700' : 'text-blue-700'}`}>
{listing.listingType.toUpperCase()}
</p>
</div>
)}
{listing.auctionEndDate && (
<div className="bg-gray-50 rounded-lg p-2">
<p className="text-gray-500 uppercase tracking-wide font-medium mb-0.5">Ends</p>
<p className="text-gray-900 font-bold">{listing.auctionEndDate}</p>
</div>
)}
</div>
{/* Action Button - Mobile Optimized */}
{listing.productUrl && (
<a
href={listing.productUrl}
target="_blank"
rel="noopener noreferrer"
className="block w-full bg-gradient-to-r from-purple-600 to-blue-600 text-white text-center px-4 py-3 sm:py-3.5 rounded-xl font-bold text-sm sm:text-base hover:from-purple-700 hover:to-blue-700 transition-all shadow-md hover:shadow-xl active:scale-95"
style={{ fontSize: '16px', minHeight: '48px' }}
>
View Listing →
</a>
)}
</div>
</div>
))}
</div>
)
}