← back to Handbag Auth Nextjs
src/app/marketplace/page.tsx
459 lines
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import toast, { Toaster } from 'react-hot-toast'
import { DataAttribution } from '@/components/DataAttribution'
import { BagDetailModal } from '@/components/BagDetailModal'
import type { MarketplaceBag, MarketplaceStats } from '@/types/marketplace'
export default function Marketplace() {
const [filter, setFilter] = useState('all')
const [sort, setSort] = useState('best-deal')
const [bags, setBags] = useState<MarketplaceBag[]>([])
const [loading, setLoading] = useState(true)
const [stats, setStats] = useState<MarketplaceStats | null>(null)
const [error, setError] = useState<string | null>(null)
const [selectedBag, setSelectedBag] = useState<MarketplaceBag | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
// Sort bags when sort option changes
const sortBags = (bagsToSort: MarketplaceBag[], sortBy: string) => {
const sorted = [...bagsToSort]
switch (sortBy) {
case 'best-deal':
// Sort by highest appreciation (best deals first)
return sorted.sort((a, b) => b.appreciation - a.appreciation)
case 'price-low':
return sorted.sort((a, b) => a.totalValue - b.totalValue)
case 'price-high':
return sorted.sort((a, b) => b.totalValue - a.totalValue)
case 'appreciation':
return sorted.sort((a, b) => b.appreciation - a.appreciation)
case 'trending':
default:
// Keep original order (most recent/trending)
return sorted
}
}
// Re-sort when sort option changes
useEffect(() => {
setBags(currentBags => sortBags(currentBags, sort))
}, [sort])
// Load bags when filter changes (single useEffect - no duplicates)
useEffect(() => {
loadBags(filter)
}, [filter])
const handleBagClick = (bag: MarketplaceBag) => {
setSelectedBag(bag)
setIsModalOpen(true)
}
const loadBags = async (brand: string = 'all') => {
setLoading(true)
setError(null)
const url = brand === 'all'
? '/api/marketplace/real'
: `/api/marketplace/real?brand=${brand}`
try {
const res = await fetch(url)
const data = await res.json()
if (data.success) {
// Sort bags by best deal (highest appreciation) by default
const sortedBags = sortBags(data.bags, sort)
setBags(sortedBags)
setStats(data.stats)
} else {
throw new Error(data.error || 'Failed to load handbags')
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load handbags'
console.error('Failed to load real data:', err)
setError(errorMessage)
toast.error(errorMessage)
} finally {
setLoading(false)
}
}
// Mini price chart component
const MiniPriceChart = ({ data }: { data: { year: number; price: number }[] }) => {
if (!data || data.length < 2) return null
const maxPrice = Math.max(...data.map(d => d.price))
const minPrice = Math.min(...data.map(d => d.price))
const range = maxPrice - minPrice
const points = data
.map((d, i) => {
const x = (i / (data.length - 1)) * 100
const y = 100 - ((d.price - minPrice) / range) * 100
return `${x},${y}`
})
.join(' ')
return (
<div className="relative h-20 mb-4">
<svg viewBox="0 0 100 100" className="w-full h-full" preserveAspectRatio="none">
<defs>
<linearGradient id={`gradient-${data[0].year}`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#fbbf24" />
<stop offset="100%" stopColor="#f59e0b" />
</linearGradient>
</defs>
<polyline
fill="none"
stroke={`url(#gradient-${data[0].year})`}
strokeWidth="2"
points={points}
vectorEffect="non-scaling-stroke"
/>
</svg>
<div className="absolute bottom-0 left-0 right-0 flex justify-between text-xs text-gray-500">
<span>{data[0].year}</span>
<span className="text-amber-400 font-bold">${(data[data.length - 1].price).toLocaleString()}</span>
<span>{data[data.length - 1].year}</span>
</div>
</div>
)
}
return (
<div className="min-h-screen bg-black text-white">
<Toaster position="top-right" />
<BagDetailModal bag={selectedBag} isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
{/* Navigation */}
<nav className="fixed top-0 left-0 right-0 z-50 bg-black/80 backdrop-blur-xl border-b border-white/10">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<Link href="/" className="flex items-center space-x-3">
<div className="w-12 h-12 bg-gradient-to-br from-yellow-400 via-amber-500 to-orange-600 rounded-xl flex items-center justify-center shadow-2xl shadow-amber-500/50">
<span className="text-2xl">💎</span>
</div>
<div>
<div className="text-xl font-bold tracking-tight">LUXVAULT</div>
<div className="text-xs text-gray-400 tracking-wide">Marketplace</div>
</div>
</Link>
<div className="flex items-center space-x-4">
<Link href="/admin" className="px-6 py-2.5 text-sm font-semibold text-white hover:text-gray-300 transition-colors">
Admin
</Link>
<button className="px-6 py-2.5 bg-gradient-to-r from-yellow-400 to-amber-500 rounded-xl text-black font-bold text-sm hover:shadow-2xl hover:shadow-amber-500/50 transition-all transform hover:scale-105">
Connect Wallet
</button>
</div>
</div>
</div>
</nav>
{/* Header */}
<div className="pt-32 pb-12 bg-gradient-to-b from-gray-900 to-black">
<div className="max-w-7xl mx-auto px-6">
<h1 className="text-5xl md:text-6xl font-extrabold mb-4">
Discover <span className="bg-gradient-to-r from-amber-400 to-orange-500 bg-clip-text text-transparent">Luxury</span>
</h1>
<p className="text-xl text-gray-400 mb-8">Browse authenticated luxury handbags with historical price data</p>
{/* Filters */}
<div className="flex flex-wrap gap-4 items-center justify-between">
<div className="flex flex-wrap gap-3">
<button
onClick={() => setFilter('all')}
className={`px-6 py-2.5 rounded-xl font-semibold text-sm transition-all ${
filter === 'all'
? 'bg-amber-500 text-black'
: 'bg-white/10 text-gray-400 hover:bg-white/20'
}`}
>
All Brands
</button>
<button
onClick={() => setFilter('hermes')}
className={`px-6 py-2.5 rounded-xl font-semibold text-sm transition-all ${
filter === 'hermes'
? 'bg-amber-500 text-black'
: 'bg-white/10 text-gray-400 hover:bg-white/20'
}`}
>
Hermès
</button>
<button
onClick={() => setFilter('chanel')}
className={`px-6 py-2.5 rounded-xl font-semibold text-sm transition-all ${
filter === 'chanel'
? 'bg-amber-500 text-black'
: 'bg-white/10 text-gray-400 hover:bg-white/20'
}`}
>
Chanel
</button>
<button
onClick={() => setFilter('lv')}
className={`px-6 py-2.5 rounded-xl font-semibold text-sm transition-all ${
filter === 'lv'
? 'bg-amber-500 text-black'
: 'bg-white/10 text-gray-400 hover:bg-white/20'
}`}
>
Louis Vuitton
</button>
</div>
<select
value={sort}
onChange={(e) => setSort(e.target.value)}
className="px-6 py-2.5 bg-white/10 border border-white/20 rounded-xl text-white font-semibold text-sm focus:outline-none focus:border-amber-500"
>
<option value="best-deal">🔥 Best Deals (Default)</option>
<option value="appreciation">Highest Appreciation</option>
<option value="price-high">Price: High to Low</option>
<option value="price-low">Price: Low to High</option>
<option value="trending">Trending</option>
</select>
</div>
</div>
</div>
{/* Marketplace Grid */}
<div className="py-12 bg-black">
<div className="max-w-7xl mx-auto px-6">
{/* Loading State */}
{loading && (
<div className="text-center py-20">
<div className="inline-block animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-amber-500 mb-4"></div>
<p className="text-gray-400 text-lg">Loading REAL handbags from database...</p>
<p className="text-sm text-gray-500 mt-2">Querying 27,927 listings from Yahoo Japan</p>
</div>
)}
{/* Results */}
{!loading && bags.length === 0 && (
<div className="text-center py-20">
<p className="text-2xl text-gray-400">No bags found for this brand</p>
<button
onClick={() => setFilter('all')}
className="mt-4 px-6 py-3 bg-amber-500 rounded-xl hover:bg-amber-600 transition"
>
Show All Brands
</button>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{!loading && bags.map((bag) => (
<div
key={bag.id}
onClick={() => handleBagClick(bag)}
className="group relative bg-gradient-to-br from-gray-900 to-black border border-white/10 rounded-3xl overflow-hidden hover:border-amber-500/50 transition-all duration-500 hover:shadow-2xl hover:shadow-amber-500/20 transform hover:-translate-y-2 cursor-pointer"
>
{/* Appreciation Badge */}
<div className="absolute top-4 right-4 z-10 px-4 py-2 bg-green-500/90 backdrop-blur-sm rounded-full text-xs font-bold text-black">
+{bag.appreciation}%
</div>
{/* Image */}
<div className="relative w-full h-64 bg-gradient-to-br from-gray-800 to-gray-900 flex items-center justify-center overflow-hidden">
<img
src={bag.image}
alt={`${bag.brand} ${bag.model}`}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-black to-transparent"></div>
</div>
{/* Content */}
<div className="p-6">
<div className="mb-4">
<div className="text-sm text-amber-400 font-semibold mb-1">{bag.brand}</div>
<h3 className="text-2xl font-bold mb-2">{bag.model}</h3>
<p className="text-sm text-gray-400">
{bag.material} • {bag.hardware} • {bag.color}
</p>
</div>
<div className="space-y-3 mb-6">
{/* COMMENTED OUT - SHARES FUNCTIONALITY
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Price per Share</span>
<span className="text-2xl font-bold text-amber-400">${bag.pricePerShare}</span>
</div>
*/}
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Current Price</span>
<span className="text-2xl font-bold text-amber-400">${bag.totalValue.toLocaleString()}</span>
</div>
{/* COMMENTED OUT - SHARES FUNCTIONALITY
<div className="flex items-center justify-between">
<span className="text-gray-400 text-sm">Available Shares</span>
<span className="text-sm font-semibold">{bag.sharesAvailable.toLocaleString()} / {bag.totalShares.toLocaleString()}</span>
</div>
*/}
</div>
{/* COMMENTED OUT - SHARES PROGRESS BAR
<div className="mb-6">
<div className="w-full h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-amber-400 to-orange-500"
style={{ width: `${(bag.sharesAvailable / bag.totalShares) * 100}%` }}
></div>
</div>
<p className="text-xs text-gray-500 mt-2">
{((bag.sharesAvailable / bag.totalShares) * 100).toFixed(0)}% available
</p>
</div>
*/}
{/* Action Buttons */}
<div className="flex gap-3 mb-6">
{/* COMMENTED OUT - SHARES FUNCTIONALITY
<button className="flex-1 px-6 py-3 bg-gradient-to-r from-yellow-400 to-amber-500 rounded-xl text-black font-bold text-sm hover:shadow-2xl hover:shadow-amber-500/50 transition-all transform hover:scale-105">
Invest Now
</button>
*/}
<button className="flex-1 px-6 py-3 bg-gradient-to-r from-yellow-400 to-amber-500 rounded-xl text-black font-bold text-sm hover:shadow-2xl hover:shadow-amber-500/50 transition-all transform hover:scale-105">
View Listing
</button>
<button className="px-6 py-3 bg-white/10 border border-white/20 rounded-xl text-white font-semibold text-sm hover:bg-white/20 transition-all">
Details
</button>
</div>
{/* Price History Chart */}
<div className="border-t border-white/10 pt-4 mb-4">
<h4 className="text-xs font-semibold text-gray-400 mb-3">PRICE HISTORY (2015-2025)</h4>
<MiniPriceChart data={bag.priceHistory} />
</div>
{/* Last Sale Info */}
{bag.lastSale && (
<div className="bg-black/30 rounded-xl p-4 mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-gray-400">LAST SALE</span>
<span className="text-xs text-green-400">✓ Verified</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-300">{new Date(bag.lastSale.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</span>
<span className="text-lg font-bold text-amber-400">${bag.lastSale.price.toLocaleString()}</span>
</div>
<p className="text-xs text-gray-500 mt-1">Source: {bag.lastSale.source}</p>
</div>
)}
{/* Data Source Attribution */}
<div className="border-t border-white/10 pt-4">
<div className="space-y-2">
<div className="flex items-start gap-2">
<span className="text-sm">📊</span>
<div className="flex-1">
<p className="text-xs font-medium text-blue-400">{bag.dataSource}</p>
<p className="text-xs text-gray-500 mt-1">{bag.methodology}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-sm">✅</span>
<div className="flex-1">
<p className="text-xs text-gray-400">Confidence: {bag.confidence}%</p>
<div className="w-full h-1.5 bg-gray-800 rounded-full overflow-hidden mt-1">
<div
className="h-full bg-gradient-to-r from-green-500 to-emerald-500"
style={{ width: `${bag.confidence}%` }}
/>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-sm">⏰</span>
<p className="text-xs text-gray-400">Updated: Static estimate (not live pricing)</p>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Overall Data Attribution Section - REAL DATABASE */}
<div className="py-16 bg-gradient-to-b from-black to-gray-900">
<div className="max-w-7xl mx-auto px-6">
<h2 className="text-3xl font-bold mb-8 text-center">Data Transparency - REAL DATABASE</h2>
{stats && (
<div className="bg-black/40 rounded-2xl p-8 mb-8 border border-green-500">
<h3 className="text-2xl font-bold text-green-400 mb-6">✅ 100% REAL DATA FROM YAHOO JAPAN AUCTIONS</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="text-center">
<div className="text-4xl font-bold text-white">{stats.totalListings?.toLocaleString()}</div>
<div className="text-sm text-gray-400 mt-2">Total Real Listings</div>
</div>
<div className="text-center">
<div className="text-4xl font-bold text-white">{stats.totalBrands}</div>
<div className="text-sm text-gray-400 mt-2">Luxury Brands</div>
</div>
<div className="text-center">
<div className="text-4xl font-bold text-white">${stats.avgPrice?.toLocaleString()}</div>
<div className="text-sm text-gray-400 mt-2">Average Price</div>
</div>
<div className="text-center">
<div className="text-4xl font-bold text-green-400">100%</div>
<div className="text-sm text-gray-400 mt-2">Data Confidence</div>
</div>
</div>
<div className="mt-6 pt-6 border-t border-white/10">
<p className="text-sm text-gray-300">📅 Last Crawl: {stats.lastCrawl}</p>
<p className="text-sm text-gray-300">🗄️ Database: /root/Projects/handbag-auth-nextjs/data/handbags.db</p>
<p className="text-sm text-gray-300">🔗 Source: Yahoo Japan Auctions (Nov 8-14, 2025)</p>
</div>
</div>
)}
<DataAttribution
sources={[
{
type: 'database',
name: 'REAL Yahoo Japan Auction Database',
description: `${stats?.totalListings.toLocaleString()} live luxury handbag listings with actual prices, images, and product URLs`
},
{
type: 'api',
name: 'Live Crawler System',
description: 'Automated scraping from Yahoo Japan specialist luxury sellers'
}
]}
lastUpdated={`Real-time from database (last crawl: ${stats?.lastCrawl || 'Loading...'})`}
confidence={100}
showPremiumUpsell={false}
/>
</div>
</div>
{/* Footer */}
<footer className="relative py-12 bg-black border-t border-white/10">
<div className="max-w-7xl mx-auto px-6 text-center">
<p className="text-gray-500 text-sm">
© 2025 LuxVault. All rights reserved. Securities offered through regulated broker-dealer.
</p>
</div>
</footer>
</div>
)
}