← back to Handbag Auth Nextjs
src/components/DealsGrid.tsx
288 lines
'use client'
import { useState, useEffect } from 'react'
import Image from 'next/image'
interface Deal {
type: 'listing' | 'auction' | 'arbitrage'
id: string | number
title: string
description?: string
price: number
originalPrice?: number
currency: string
savings: number
savingsPercent: number
url?: string
imageUrl?: string
brand?: string
model?: string
condition?: string
source: string
metadata: Record<string, any>
}
interface DealsGridProps {
limit?: number
autoRefresh?: boolean
refreshInterval?: number
filterType?: 'all' | 'listing' | 'auction' | 'arbitrage'
}
export function DealsGrid({ limit = 10, autoRefresh = false, refreshInterval = 60000, filterType = 'all' }: DealsGridProps) {
const [deals, setDeals] = useState<Deal[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [stats, setStats] = useState<any>(null)
const fetchDeals = async () => {
try {
setLoading(true)
const response = await fetch(`/api/top-deals?limit=${limit}`)
const data = await response.json()
if (data.success) {
setDeals(data.deals)
setStats(data.stats)
setError(null)
} else {
setError(data.error || 'Failed to load deals')
}
} catch (err) {
setError('Failed to load deals')
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchDeals()
if (autoRefresh) {
const interval = setInterval(fetchDeals, refreshInterval)
return () => clearInterval(interval)
}
}, [limit, autoRefresh, refreshInterval, filterType])
const getTypeColor = (type: string) => {
switch (type) {
case 'listing':
return 'bg-blue-500'
case 'auction':
return 'bg-purple-500'
case 'arbitrage':
return 'bg-green-500'
default:
return 'bg-gray-500'
}
}
const getTypeIcon = (type: string) => {
switch (type) {
case 'listing':
return '🛍️'
case 'auction':
return '🔨'
case 'arbitrage':
return '💰'
default:
return '📦'
}
}
const formatPrice = (price: number, currency: string = 'USD') => {
const symbol = currency === 'JPY' ? '¥' : currency === 'HKD' ? 'HK$' : '$'
return `${symbol}${price.toLocaleString()}`
}
if (loading && deals.length === 0) {
return (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin h-12 w-12 border-4 border-purple-500 border-t-transparent rounded-full mx-auto mb-4"></div>
<p className="text-gray-600">Loading top deals...</p>
</div>
</div>
)
}
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
<p className="text-red-700">{error}</p>
<button
onClick={fetchDeals}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
>
Retry
</button>
</div>
)
}
return (
<div>
{/* Stats Bar */}
{stats && (
<div className="mb-6 bg-gradient-to-r from-purple-50 to-pink-50 rounded-lg p-4 border border-purple-200">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-6">
<div>
<span className="text-sm text-gray-600">Total Deals</span>
<p className="text-2xl font-bold text-purple-600">{deals.length}</p>
</div>
<div>
<span className="text-sm text-gray-600">Avg Savings</span>
<p className="text-2xl font-bold text-green-600">{stats.avgSavings?.toFixed(1)}%</p>
</div>
</div>
<div className="flex gap-3 text-sm">
{stats.listings > 0 && (
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full">
🛍️ {stats.listings} Listings
</span>
)}
{stats.auctions > 0 && (
<span className="px-3 py-1 bg-purple-100 text-purple-800 rounded-full">
🔨 {stats.auctions} Auctions
</span>
)}
{stats.arbitrage > 0 && (
<span className="px-3 py-1 bg-green-100 text-green-800 rounded-full">
💰 {stats.arbitrage} Arbitrage
</span>
)}
</div>
</div>
</div>
)}
{/* Deals Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-6">
{deals.filter(deal => filterType === 'all' || deal.type === filterType).map((deal, index) => (
<div
key={`${deal.type}-${deal.id}`}
className="group bg-white rounded-xl shadow-md hover:shadow-2xl transition-all duration-300 overflow-hidden border border-gray-200 hover:border-purple-500 cursor-pointer transform hover:-translate-y-1"
onClick={() => {
if (deal.url) {
window.open(deal.url, '_blank')
}
}}
>
{/* Deal Rank Badge */}
<div className="absolute top-2 left-2 z-10">
<div className="bg-gradient-to-r from-yellow-400 to-orange-500 text-white font-bold text-sm px-3 py-1 rounded-full shadow-lg">
#{index + 1}
</div>
</div>
{/* Type Badge */}
<div className="absolute top-2 right-2 z-10">
<div className={`${getTypeColor(deal.type)} text-white text-xs px-2 py-1 rounded-full shadow-lg`}>
{getTypeIcon(deal.type)} {deal.type.toUpperCase()}
</div>
</div>
{/* Image */}
<div className="relative h-48 bg-gray-100 overflow-hidden">
{deal.imageUrl ? (
<img
src={deal.imageUrl}
alt={deal.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
loading="lazy"
/>
) : (
<div className="flex items-center justify-center h-full bg-gradient-to-br from-purple-100 to-pink-100">
<span className="text-6xl">{getTypeIcon(deal.type)}</span>
</div>
)}
{/* Savings Badge - Overlaid on image */}
<div className="absolute bottom-2 left-2 right-2">
<div className="bg-red-600 text-white font-bold text-center py-2 rounded-lg shadow-lg">
<div className="text-2xl">{deal.savingsPercent.toFixed(0)}% OFF</div>
<div className="text-xs">Save {formatPrice(deal.savings)}</div>
</div>
</div>
</div>
{/* Content */}
<div className="p-4">
{/* Brand */}
{deal.brand && (
<div className="text-xs font-semibold text-purple-600 uppercase tracking-wide mb-1">
{deal.brand}
</div>
)}
{/* Title */}
<h3 className="font-bold text-gray-900 mb-2 line-clamp-2 min-h-[48px] group-hover:text-purple-600 transition-colors">
{deal.title}
</h3>
{/* Description */}
{deal.description && (
<p className="text-xs text-gray-600 mb-3 line-clamp-2 min-h-[32px]">
{deal.description}
</p>
)}
{/* Price Info */}
<div className="space-y-1 mb-3">
<div className="flex items-baseline gap-2">
<span className="text-2xl font-bold text-purple-600">
{formatPrice(deal.price, deal.currency)}
</span>
{deal.originalPrice && (
<span className="text-sm text-gray-500 line-through">
{formatPrice(deal.originalPrice, deal.currency)}
</span>
)}
</div>
</div>
{/* Metadata */}
<div className="flex flex-wrap gap-1 mb-3">
{deal.condition && (
<span className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded">
{deal.condition}
</span>
)}
{deal.metadata.verified && (
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">
✓ Verified
</span>
)}
</div>
{/* Source */}
<div className="text-xs text-gray-500 border-t pt-2">
{deal.source}
</div>
{/* CTA */}
<button className="w-full mt-3 bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold py-2 rounded-lg hover:from-purple-700 hover:to-pink-700 transition-all transform group-hover:scale-105">
View Deal →
</button>
</div>
</div>
))}
</div>
{/* Empty State */}
{deals.length === 0 && !loading && (
<div className="text-center py-12 bg-gray-50 rounded-lg">
<p className="text-gray-600 text-lg mb-4">No deals available at the moment</p>
<button
onClick={fetchDeals}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
Refresh
</button>
</div>
)}
</div>
)
}