← back to Wine Finder Next
src/components/ListingsTable.tsx
283 lines
'use client';
import { useMemo, useState } from 'react';
import { WineListing, ArbitrageOpportunity } from '../types/wine';
interface ListingsTableProps {
listings: WineListing[];
}
export default function ListingsTable({ listings }: ListingsTableProps) {
const [activeTab, setActiveTab] = useState<'all' | 'auction' | 'retail' | 'arbitrage'>('all');
// Calculate arbitrage opportunities
const arbitrageOpportunities = useMemo(() => {
const retail = listings.filter((l) => l.provider === 'retail');
const auction = listings.filter((l) => l.provider === 'auction');
const opportunities: ArbitrageOpportunity[] = [];
auction.forEach((auctionListing) => {
retail.forEach((retailListing) => {
// Fuzzy match wine names
const nameMatch = auctionListing.name.toLowerCase().includes(retailListing.name.toLowerCase()) ||
retailListing.name.toLowerCase().includes(auctionListing.name.toLowerCase());
if (nameMatch && auctionListing.currentPrice.amount > retailListing.currentPrice.amount) {
const spread = auctionListing.currentPrice.amount - retailListing.currentPrice.amount;
const spreadPercentage = (spread / retailListing.currentPrice.amount) * 100;
if (spread > 10) { // Only show significant arbitrage (>$10)
opportunities.push({
wine: {
id: retailListing.id,
name: retailListing.name,
vintage: retailListing.vintage,
region: retailListing.region,
rating: retailListing.rating,
},
retailListing,
auctionListing,
spread,
spreadPercentage,
});
}
}
});
});
return opportunities.sort((a, b) => b.spread - a.spread);
}, [listings]);
// Best deal of the day (highest $ saved)
const bestDeal = useMemo(() => {
if (arbitrageOpportunities.length === 0) return null;
return arbitrageOpportunities[0];
}, [arbitrageOpportunities]);
// Filter listings by tab
const filteredListings = useMemo(() => {
if (activeTab === 'auction') return listings.filter(l => l.provider === 'auction');
if (activeTab === 'retail') return listings.filter(l => l.provider === 'retail');
if (activeTab === 'arbitrage') return []; // Show arbitrage table instead
return listings;
}, [listings, activeTab]);
return (
<div className="w-full max-w-7xl mx-auto">
{/* Best Deal Hero Banner */}
{bestDeal && (
<div className="mb-8 bg-gradient-to-r from-red-600 via-rose-600 to-pink-600 text-white rounded-xl p-8 shadow-2xl">
<div className="text-center mb-4">
<div className="inline-flex items-center gap-3 bg-white text-red-600 px-6 py-2 rounded-full font-black text-xl">
🔥 BEST ARBITRAGE OPPORTUNITY 🔥
</div>
</div>
<div className="grid md:grid-cols-3 gap-6 text-center">
<div>
<div className="text-sm opacity-90 mb-2">Wine</div>
<div className="font-bold text-2xl">{bestDeal.wine.name}</div>
{bestDeal.wine.vintage && <div className="text-lg opacity-90">{bestDeal.wine.vintage}</div>}
</div>
<div>
<div className="text-sm opacity-90 mb-2">Buy Retail</div>
<div className="font-black text-4xl text-yellow-300">${bestDeal.retailListing.currentPrice.amount.toFixed(2)}</div>
<div className="text-sm opacity-90">{bestDeal.retailListing.providerName}</div>
</div>
<div>
<div className="text-sm opacity-90 mb-2">Sell at Auction</div>
<div className="font-black text-4xl text-green-300">${bestDeal.auctionListing.currentPrice.amount.toFixed(2)}</div>
<div className="text-sm opacity-90">{bestDeal.auctionListing.providerName}</div>
</div>
</div>
<div className="mt-6 text-center">
<div className="text-3xl font-black text-green-300 drop-shadow-lg">
💰 PROFIT: ${bestDeal.spread.toFixed(2)} ({bestDeal.spreadPercentage.toFixed(1)}%)
</div>
</div>
</div>
)}
{/* Tabs */}
<div className="flex gap-2 mb-6 border-b-2 border-gray-200">
<button
onClick={() => setActiveTab('all')}
className={`px-6 py-3 font-semibold transition-all ${
activeTab === 'all'
? 'border-b-4 border-purple-600 text-purple-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
All Listings ({listings.length})
</button>
<button
onClick={() => setActiveTab('auction')}
className={`px-6 py-3 font-semibold transition-all ${
activeTab === 'auction'
? 'border-b-4 border-purple-600 text-purple-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Auctions ({listings.filter(l => l.provider === 'auction').length})
</button>
<button
onClick={() => setActiveTab('retail')}
className={`px-6 py-3 font-semibold transition-all ${
activeTab === 'retail'
? 'border-b-4 border-purple-600 text-purple-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Retail ({listings.filter(l => l.provider === 'retail').length})
</button>
<button
onClick={() => setActiveTab('arbitrage')}
className={`px-6 py-3 font-semibold transition-all ${
activeTab === 'arbitrage'
? 'border-b-4 border-red-600 text-red-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
🔥 Arbitrage ({arbitrageOpportunities.length})
</button>
</div>
{/* Arbitrage Opportunities Table */}
{activeTab === 'arbitrage' && (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{arbitrageOpportunities.length === 0 ? (
<div className="p-12 text-center text-gray-500">
<div className="text-6xl mb-4">🔍</div>
<div className="text-xl font-semibold">No arbitrage opportunities found</div>
<div className="text-sm mt-2">Try searching for premium wines that appear on both retail and auction sites</div>
</div>
) : (
<table className="w-full">
<thead className="bg-gradient-to-r from-red-600 to-pink-600 text-white">
<tr>
<th className="px-6 py-4 text-left font-bold">Wine</th>
<th className="px-6 py-4 text-right font-bold">Buy (Retail)</th>
<th className="px-6 py-4 text-right font-bold">Sell (Auction)</th>
<th className="px-6 py-4 text-right font-bold">Last Sale</th>
<th className="px-6 py-4 text-right font-bold">Profit ($)</th>
<th className="px-6 py-4 text-right font-bold">Profit (%)</th>
</tr>
</thead>
<tbody>
{arbitrageOpportunities.map((opp, idx) => (
<tr key={idx} className="border-b border-gray-200 hover:bg-pink-50 transition-colors">
<td className="px-6 py-4">
<div className="font-semibold text-gray-900">{opp.wine.name}</div>
{opp.wine.vintage && <div className="text-sm text-gray-600">{opp.wine.vintage}</div>}
{opp.wine.region && <div className="text-xs text-gray-500">{opp.wine.region}</div>}
</td>
<td className="px-6 py-4 text-right">
<div className="font-bold text-green-600">${opp.retailListing.currentPrice.amount.toFixed(2)}</div>
<div className="text-xs text-gray-500">{opp.retailListing.providerName}</div>
</td>
<td className="px-6 py-4 text-right">
<div className="font-bold text-blue-600">${opp.auctionListing.currentPrice.amount.toFixed(2)}</div>
<div className="text-xs text-gray-500">{opp.auctionListing.providerName}</div>
{opp.auctionListing.bidCount && (
<div className="text-xs text-gray-400">{opp.auctionListing.bidCount} bids</div>
)}
</td>
<td className="px-6 py-4 text-right">
{(opp.auctionListing as any).lastSale ? (
<>
<div className="font-semibold text-purple-600">${(opp.auctionListing as any).lastSale.price.toFixed(2)}</div>
<div className="text-xs text-gray-500">{(opp.auctionListing as any).lastSale.source}</div>
<div className="text-xs text-gray-400">{(opp.auctionListing as any).lastSale.daysAgo}d ago</div>
</>
) : (
<div className="text-xs text-gray-400">No data</div>
)}
</td>
<td className="px-6 py-4 text-right">
<div className="font-black text-xl text-red-600">${opp.spread.toFixed(2)}</div>
</td>
<td className="px-6 py-4 text-right">
<div className="font-bold text-red-600">{opp.spreadPercentage.toFixed(1)}%</div>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
{/* Regular Listings Table */}
{activeTab !== 'arbitrage' && (
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
{filteredListings.length === 0 ? (
<div className="p-12 text-center text-gray-500">
<div className="text-6xl mb-4">🍷</div>
<div className="text-xl font-semibold">No wines found</div>
<div className="text-sm mt-2">Try a different search query</div>
</div>
) : (
<table className="w-full">
<thead className="bg-gradient-to-r from-purple-600 to-pink-600 text-white">
<tr>
<th className="px-6 py-4 text-left font-bold">Wine</th>
<th className="px-6 py-4 text-left font-bold">Type</th>
<th className="px-6 py-4 text-left font-bold">Source</th>
<th className="px-6 py-4 text-right font-bold">Price</th>
<th className="px-6 py-4 text-center font-bold">Rating</th>
<th className="px-6 py-4 text-center font-bold">Link</th>
</tr>
</thead>
<tbody>
{filteredListings.map((wine) => (
<tr key={wine.id} className="border-b border-gray-200 hover:bg-purple-50 transition-colors">
<td className="px-6 py-4">
<div className="font-semibold text-gray-900">{wine.name}</div>
{wine.vintage && <div className="text-sm text-gray-600">{wine.vintage}</div>}
{wine.region && <div className="text-xs text-gray-500">{wine.region}</div>}
</td>
<td className="px-6 py-4">
<span className={`px-3 py-1 rounded-full text-xs font-semibold ${
wine.provider === 'auction'
? 'bg-blue-100 text-blue-800'
: 'bg-green-100 text-green-800'
}`}>
{wine.provider === 'auction' ? '🔨 Auction' : '🛒 Retail'}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-700">{wine.providerName}</td>
<td className="px-6 py-4 text-right">
<div className="font-bold text-lg text-purple-600">${wine.currentPrice.amount.toFixed(2)}</div>
{wine.bidCount !== undefined && (
<div className="text-xs text-gray-500">{wine.bidCount} bids</div>
)}
</td>
<td className="px-6 py-4 text-center">
{wine.rating && (
<div className="flex items-center justify-center gap-1">
<span className="text-yellow-500">⭐</span>
<span className="font-semibold">{wine.rating}</span>
</div>
)}
</td>
<td className="px-6 py-4 text-center">
<a
href={wine.url}
target="_blank"
rel="noopener noreferrer"
className="text-purple-600 hover:text-purple-800 font-semibold hover:underline"
>
View →
</a>
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}
</div>
);
}