deckhearth/pages/card/[id].js
Randall Stillwell e75a4a6649 Major redesign: Enhanced card display with particle effects, improved filters, and search functionality
- Redesigned card display with 2.5:3.5 aspect ratio and image-only view
- Added infinite scroll to replace pagination
- Implemented authentic card back placeholders for MTG, Pokemon, and Lorcana
- Added rarity-based particle effects with tiered intensity (mythic/enchanted/rare/uncommon)
- Enhanced hover details panel with structured card information
- Fixed search functionality with debouncing and Enter key support
- Improved filter system with working TCG, rarity, set, and price filters
- Added favorite system for cards in both hover and detail views
- Updated card detail page with comprehensive metadata and actions
- Fixed API filtering with proper Vercel Postgres implementation
- Added particle animations and rarity glow effects
- Improved overall UX with better visual hierarchy and interactions
2025-07-23 21:26:54 -05:00

925 lines
No EOL
40 KiB
JavaScript

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Layout from '../../components/Layout';
export default function CardDetail() {
const router = useRouter();
const { id } = router.query;
const user = {
email: 'me@randallstillwell.com',
role: 'user'
};
const [card, setCard] = useState(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('details');
const [ownedQuantity, setOwnedQuantity] = useState(0);
const [showQuantityModal, setShowQuantityModal] = useState(false);
const [showCollectionModal, setShowCollectionModal] = useState(false);
const [showDeckModal, setShowDeckModal] = useState(false);
const [selectedCollection, setSelectedCollection] = useState('');
const [selectedDeck, setSelectedDeck] = useState('');
const [quantity, setQuantity] = useState(1);
const [isFavorited, setIsFavorited] = useState(false);
// Mock card data - in real app this would come from API
const mockCards = {
'1': {
id: 1,
name: "Black Lotus",
game: "MTG",
rarity: "mythic",
rarityColor: "#FFD700",
rarityGradient: "from-yellow-400 to-orange-500",
set: "Alpha",
value: 25000,
image: "/api/placeholder/1",
description: "The most iconic Magic card ever printed",
type: "Artifact",
condition: "Near Mint",
artist: "Christopher Rush",
cardNumber: "232",
flavorText: "The most powerful artifact ever created.",
manaCost: "{0}",
power: null,
toughness: null,
text: "{T}, Sacrifice Black Lotus: Add three mana of any one color to your mana pool.",
rulings: [
"Black Lotus is banned in all formats except Vintage.",
"The card was printed in Alpha, Beta, and Unlimited editions.",
"It's considered one of the Power Nine."
],
priceHistory: [
{ date: '2023-01-01', price: 22000 },
{ date: '2023-04-01', price: 23500 },
{ date: '2023-07-01', price: 24000 },
{ date: '2023-10-01', price: 24500 },
{ date: '2024-01-01', price: 25000 }
],
purchaseLinks: [
{ name: 'TCGPlayer', url: 'https://www.tcgplayer.com/search/magic/product?productName=black+lotus', icon: '🃏' },
{ name: 'eBay', url: 'https://www.ebay.com/sch/i.html?_nkw=black+lotus+magic+alpha', icon: '🛒' },
{ name: 'Card Kingdom', url: 'https://www.cardkingdom.com/catalog/search?search=black+lotus', icon: '👑' },
{ name: 'Star City Games', url: 'https://starcitygames.com/search?searchQuery=black+lotus', icon: '⭐' }
]
},
'2': {
id: 2,
name: "Charizard",
game: "Pokemon",
rarity: "holographic",
rarityColor: "#FF6B6B",
rarityGradient: "from-red-400 to-pink-500",
set: "Base Set",
value: 350,
image: "/api/placeholder/2",
description: "The classic holographic Charizard",
type: "Fire",
condition: "Lightly Played",
artist: "Mitsuhiro Arita",
cardNumber: "4/102",
flavorText: "Spits fire that is hot enough to melt boulders.",
manaCost: null,
power: "100",
toughness: null,
text: "Fire Spin: 100 damage. Discard 2 Energy cards attached to Charizard in order to use this attack.",
rulings: [
"This is the most valuable card from the original Base Set.",
"The holographic version is significantly more valuable than the non-holo.",
"First printed in 1999."
],
priceHistory: [
{ date: '2023-01-01', price: 300 },
{ date: '2023-04-01', price: 320 },
{ date: '2023-07-01', price: 330 },
{ date: '2023-10-01', price: 340 },
{ date: '2024-01-01', price: 350 }
],
purchaseLinks: [
{ name: 'TCGPlayer', url: 'https://www.tcgplayer.com/search/pokemon/product?productName=charizard+base+set', icon: '🃏' },
{ name: 'eBay', url: 'https://www.ebay.com/sch/i.html?_nkw=charizard+base+set+holographic', icon: '🛒' },
{ name: 'Pokemon Center', url: 'https://www.pokemoncenter.com/search?q=charizard', icon: '⚡' }
]
},
'3': {
id: 3,
name: "Mickey Mouse",
game: "Lorcana",
rarity: "enchanted",
rarityColor: "#A855F7",
rarityGradient: "from-purple-400 to-indigo-500",
set: "First Chapter",
value: 45,
image: "/api/placeholder/3",
description: "Disney's iconic character in card form",
type: "Character",
condition: "Near Mint",
artist: "Disney",
cardNumber: "1/204",
flavorText: "The most magical mouse of all.",
manaCost: "2",
power: "2",
toughness: "2",
text: "When this character enters play, you may draw a card.",
rulings: [
"This is the first Disney card game featuring Mickey Mouse.",
"The enchanted version has special foil treatment.",
"Released in 2023."
],
priceHistory: [
{ date: '2023-08-01', price: 40 },
{ date: '2023-10-01', price: 42 },
{ date: '2023-12-01', price: 44 },
{ date: '2024-01-01', price: 45 }
],
purchaseLinks: [
{ name: 'TCGPlayer', url: 'https://www.tcgplayer.com/search/lorcana/product?productName=mickey+mouse', icon: '🃏' },
{ name: 'eBay', url: 'https://www.ebay.com/sch/i.html?_nkw=mickey+mouse+lorcana+enchanted', icon: '🛒' },
{ name: 'Disney Store', url: 'https://www.shopdisney.com/search?searchQuery=lorcana', icon: '✨' }
]
}
};
// Mock collections and decks data
const mockCollections = [
{ id: 1, name: 'My MTG Collection', game: 'MTG' },
{ id: 2, name: 'Pokemon Favorites', game: 'Pokemon' },
{ id: 3, name: 'Lorcana Disney', game: 'Lorcana' },
{ id: 4, name: 'Rare Cards', game: 'MTG' },
{ id: 5, name: 'Holographic Collection', game: 'Pokemon' }
];
const mockDecks = [
{ id: 1, name: 'MTG Control Deck', game: 'MTG' },
{ id: 2, name: 'Pokemon Aggro', game: 'Pokemon' },
{ id: 3, name: 'Lorcana Midrange', game: 'Lorcana' },
{ id: 4, name: 'MTG Combo', game: 'MTG' },
{ id: 5, name: 'Pokemon Stall', game: 'Pokemon' }
];
useEffect(() => {
if (id && mockCards[id]) {
setCard(mockCards[id]);
setLoading(false);
} else if (id) {
// Card not found
setLoading(false);
}
}, [id]);
const getTCGGradient = (game) => {
const gradients = {
'MTG': 'from-purple-600 via-purple-500 to-indigo-600',
'Pokemon': 'from-blue-600 via-blue-500 to-cyan-600',
'Lorcana': 'from-pink-600 via-pink-500 to-purple-600'
};
return gradients[game] || 'from-gray-600 to-gray-700';
};
const getTCGIcon = (game) => {
const icons = {
'MTG': '🔮',
'Pokemon': '⚡',
'Lorcana': '✨'
};
return icons[game] || '🃏';
};
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
const getRarityLabel = (rarity) => {
const rarityMap = {
'common': 'Common',
'uncommon': 'Uncommon',
'rare': 'Rare',
'mythic': 'Mythic',
'holographic': 'Holographic',
'enchanted': 'Enchanted'
};
return rarityMap[rarity] || rarity;
};
if (loading) {
return (
<Layout user={user}>
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2" style={{ borderColor: 'var(--text-accent-light)' }}></div>
</div>
</Layout>
);
}
if (!card) {
return (
<Layout user={user}>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="text-6xl mb-4">🃏</div>
<h2 className="text-2xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
Card Not Found
</h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
The card you're looking for doesn't exist.
</p>
<button
onClick={() => router.push('/cards')}
className="px-6 py-3 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200"
>
Back to Cards
</button>
</div>
</div>
</Layout>
);
}
return (
<Layout user={user}>
{/* Hero Section with Card Image and Basic Info */}
<div
className="relative min-h-96 flex items-center justify-center overflow-hidden"
style={{
background: `linear-gradient(135deg, ${getTCGGradient(card.game).replace('from-', '').replace('via-', '').replace('to-', '')})`
}}
>
{/* Background Pattern */}
<div className="absolute inset-0 opacity-10">
<div className="absolute inset-0" style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.1'%3E%3Ccircle cx='30' cy='30' r='2'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`
}}></div>
</div>
<div className="relative z-10 container mx-auto px-6 py-12">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{/* Card Image */}
<div className="flex justify-center">
<div className="relative group">
<div
className="w-80 h-112 rounded-2xl overflow-hidden shadow-2xl transform transition-all duration-300 hover:scale-105"
style={{
background: `linear-gradient(135deg, ${card.rarityGradient.replace('from-', '').replace('to-', '')})`,
boxShadow: `0 20px 40px rgba(0, 0, 0, 0.3)`
}}
>
<div className="w-full h-full flex items-center justify-center text-white">
<div className="text-center p-8">
<div className="text-6xl mb-4">{getTCGIcon(card.game)}</div>
<h1 className="text-2xl font-bold mb-2">{card.name}</h1>
<p className="text-sm opacity-90">{card.set}</p>
<div className="mt-4">
<span className="px-3 py-1 rounded-full text-xs font-medium bg-white bg-opacity-20 backdrop-blur-sm">
{getRarityLabel(card.rarity)}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Card Info */}
<div className="text-white">
<div className="mb-6">
<h1 className="text-4xl font-bold mb-2">{card.name}</h1>
<p className="text-xl opacity-90 mb-4">{card.description}</p>
{/* Ownership Status and Actions */}
<div className="mb-4 p-4 rounded-xl bg-white bg-opacity-10 backdrop-blur-sm">
<div className="flex items-center justify-between mb-3">
<span className="font-semibold">Ownership Status</span>
<div className="flex items-center gap-2">
{ownedQuantity > 0 && (
<span className="px-3 py-1 rounded-full text-sm font-medium bg-green-500 bg-opacity-80">
Owned ({ownedQuantity})
</span>
)}
<button
onClick={() => setIsFavorited(!isFavorited)}
className={`p-2 rounded-full transition-all duration-200 ${
isFavorited
? 'bg-red-500 bg-opacity-80 text-white'
: 'bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30'
}`}
>
{isFavorited ? '❤️' : '🤍'}
</button>
</div>
</div>
<div className="flex items-center gap-4 flex-wrap">
<button
onClick={() => setShowQuantityModal(true)}
className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2"
>
<span>📦</span>
{ownedQuantity > 0 ? 'Update Quantity' : 'Mark as Owned'}
</button>
<button
onClick={() => setShowCollectionModal(true)}
className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2"
>
<span>📁</span>
Add to Collection
</button>
<button
onClick={() => setShowDeckModal(true)}
className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2"
>
<span>🎴</span>
Add to Deck
</button>
</div>
</div>
<div className="flex items-center gap-4 mb-6">
<span className="text-3xl">{getTCGIcon(card.game)}</span>
<span className="text-lg">{card.game}</span>
<span className="px-3 py-1 rounded-full text-sm font-medium bg-white bg-opacity-20 backdrop-blur-sm">
{getRarityLabel(card.rarity)}
</span>
</div>
<div className="text-3xl font-bold gradient-text-gold">
{formatCurrency(card.value)}
</div>
</div>
{/* Quick Actions */}
<div className="flex flex-wrap gap-3">
{card.purchaseLinks.slice(0, 3).map((link, index) => (
<a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-xl font-medium bg-white bg-opacity-20 backdrop-blur-sm hover:bg-opacity-30 transition-all duration-200 flex items-center gap-2"
>
<span>{link.icon}</span>
{link.name}
</a>
))}
</div>
</div>
</div>
</div>
</div>
{/* Content Tabs */}
<div className="container mx-auto px-6 py-8">
<div className="flex border-b" style={{ borderColor: 'var(--border)' }}>
{['details', 'price-history', 'purchase'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`px-6 py-3 font-medium transition-all duration-200 ${
activeTab === tab
? 'border-b-2 font-semibold'
: 'hover:opacity-70'
}`}
style={{
color: activeTab === tab ? 'var(--text-accent)' : 'var(--text-secondary)',
borderColor: activeTab === tab ? 'var(--text-accent)' : 'transparent'
}}
>
{tab === 'details' && 'Card Details'}
{tab === 'price-history' && 'Price History'}
{tab === 'purchase' && 'Purchase Options'}
</button>
))}
</div>
{/* Tab Content */}
<div className="mt-8">
{activeTab === 'details' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Card Metadata */}
<div className="space-y-6">
<h3 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Card Information
</h3>
<div className="space-y-4">
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>TCG</span>
<span style={{ color: 'var(--text-primary)' }}>{card.game}</span>
</div>
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Set</span>
<span style={{ color: 'var(--text-primary)' }}>{card.set}</span>
</div>
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Card Number</span>
<span style={{ color: 'var(--text-primary)' }}>{card.cardNumber}</span>
</div>
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Type</span>
<span style={{ color: 'var(--text-primary)' }}>{card.type}</span>
</div>
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Rarity</span>
<span style={{ color: 'var(--text-primary)' }}>{getRarityLabel(card.rarity)}</span>
</div>
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Artist</span>
<span style={{ color: 'var(--text-primary)' }}>{card.artist}</span>
</div>
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Condition</span>
<span style={{ color: 'var(--text-primary)' }}>{card.condition}</span>
</div>
{card.manaCost && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Cost to Play</span>
<span style={{ color: 'var(--text-primary)' }}>{card.manaCost}</span>
</div>
)}
{card.power && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Power</span>
<span style={{ color: 'var(--text-primary)' }}>{card.power}</span>
</div>
)}
{card.toughness && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Toughness</span>
<span style={{ color: 'var(--text-primary)' }}>{card.toughness}</span>
</div>
)}
{card.hp && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>HP</span>
<span style={{ color: 'var(--text-primary)' }}>{card.hp}</span>
</div>
)}
{card.form && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Form</span>
<span style={{ color: 'var(--text-primary)' }}>{card.form}</span>
</div>
)}
{card.weakness && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Weakness</span>
<span style={{ color: 'var(--text-primary)' }}>{card.weakness}</span>
</div>
)}
{card.retreat_cost && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Retreat Cost</span>
<span style={{ color: 'var(--text-primary)' }}>{card.retreat_cost}</span>
</div>
)}
{card.current_price && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Current Price</span>
<span style={{ color: 'var(--text-primary)' }}>{formatCurrency(card.current_price)}</span>
</div>
)}
</div>
</div>
{/* Card Text and Rulings */}
<div className="space-y-6">
<h3 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Card Text
</h3>
<div className="space-y-4">
{card.flavorText && (
<div className="p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<p className="italic text-sm" style={{ color: 'var(--text-secondary)' }}>
"{card.flavorText}"
</p>
</div>
)}
{card.text && (
<div className="p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<p style={{ color: 'var(--text-primary)' }}>{card.text}</p>
</div>
)}
</div>
{card.rulings && card.rulings.length > 0 && (
<div className="mt-6">
<h4 className="text-lg font-semibold mb-3" style={{ color: 'var(--text-primary)' }}>
Rulings
</h4>
<ul className="space-y-2">
{card.rulings.map((ruling, index) => (
<li key={index} className="flex items-start gap-2">
<span className="text-xs mt-1"></span>
<span style={{ color: 'var(--text-secondary)' }}>{ruling}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
{activeTab === 'price-history' && (
<div className="space-y-6">
<h3 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Price History
</h3>
{/* Price Graph */}
<div className="p-6 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<h4 className="text-lg font-semibold mb-6" style={{ color: 'var(--text-primary)' }}>
Price Trend
</h4>
<div className="relative h-64">
{/* Graph Container */}
<div className="absolute inset-0 flex items-end justify-between px-4 pb-4">
{card.priceHistory.map((entry, index) => {
const maxPrice = Math.max(...card.priceHistory.map(p => p.price));
const minPrice = Math.min(...card.priceHistory.map(p => p.price));
const priceRange = maxPrice - minPrice;
const height = priceRange > 0 ? ((entry.price - minPrice) / priceRange) * 100 : 50;
return (
<div key={index} className="flex flex-col items-center">
{/* Bar */}
<div
className="w-8 rounded-t-lg transition-all duration-300 hover:scale-110"
style={{
height: `${height}%`,
background: `linear-gradient(135deg, ${card.rarityGradient.replace('from-', '').replace('to-', '')})`,
minHeight: '4px'
}}
></div>
{/* Price Label */}
<div className="mt-2 text-center">
<div className="text-xs font-semibold" style={{ color: 'var(--text-primary)' }}>
{formatCurrency(entry.price)}
</div>
<div className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{new Date(entry.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</div>
</div>
</div>
);
})}
</div>
{/* Grid Lines */}
<div className="absolute inset-0 pointer-events-none">
{[0, 25, 50, 75, 100].map((line) => (
<div
key={line}
className="absolute w-full border-t border-dashed opacity-20"
style={{
top: `${100 - line}%`,
borderColor: 'var(--border)'
}}
></div>
))}
</div>
</div>
</div>
{/* Price Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Current Price */}
<div className="p-6 rounded-xl border transition-all duration-200 hover:shadow-lg hover:scale-105"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)'
}}>
<div className="text-center">
<div className="text-2xl mb-2">💰</div>
<h5 className="font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Current Price
</h5>
<div className="text-xl font-bold gradient-text-gold">
{formatCurrency(card.value)}
</div>
</div>
</div>
{/* Lowest Price */}
<div className="p-6 rounded-xl border transition-all duration-200 hover:shadow-lg hover:scale-105"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)'
}}>
<div className="text-center">
<div className="text-2xl mb-2">📉</div>
<h5 className="font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Lowest Price
</h5>
<div className="text-xl font-bold" style={{ color: 'var(--text-primary)' }}>
{formatCurrency(Math.min(...card.priceHistory.map(p => p.price)))}
</div>
</div>
</div>
{/* Highest Price */}
<div className="p-6 rounded-xl border transition-all duration-200 hover:shadow-lg hover:scale-105"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)'
}}>
<div className="text-center">
<div className="text-2xl mb-2">📈</div>
<h5 className="font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Highest Price
</h5>
<div className="text-xl font-bold" style={{ color: 'var(--text-primary)' }}>
{formatCurrency(Math.max(...card.priceHistory.map(p => p.price)))}
</div>
</div>
</div>
{/* Average Price */}
<div className="p-6 rounded-xl border transition-all duration-200 hover:shadow-lg hover:scale-105"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)'
}}>
<div className="text-center">
<div className="text-2xl mb-2">📊</div>
<h5 className="font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
Average Price
</h5>
<div className="text-xl font-bold" style={{ color: 'var(--text-primary)' }}>
{formatCurrency(card.priceHistory.reduce((sum, p) => sum + p.price, 0) / card.priceHistory.length)}
</div>
</div>
</div>
</div>
{/* Price Change Indicator */}
<div className="p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="flex items-center justify-between">
<span style={{ color: 'var(--text-secondary)' }}>Price Change</span>
<div className="flex items-center gap-2">
{(() => {
const firstPrice = card.priceHistory[0].price;
const lastPrice = card.priceHistory[card.priceHistory.length - 1].price;
const change = lastPrice - firstPrice;
const changePercent = ((change / firstPrice) * 100).toFixed(1);
const isPositive = change >= 0;
return (
<>
<span className={`font-semibold ${isPositive ? 'text-green-500' : 'text-red-500'}`}>
{isPositive ? '+' : ''}{formatCurrency(change)}
</span>
<span className={`text-sm ${isPositive ? 'text-green-500' : 'text-red-500'}`}>
({isPositive ? '+' : ''}{changePercent}%)
</span>
<span className="text-2xl">
{isPositive ? '📈' : '📉'}
</span>
</>
);
})()}
</div>
</div>
</div>
</div>
)}
{activeTab === 'purchase' && (
<div className="space-y-6">
<h3 className="text-xl font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
Where to Buy
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{card.purchaseLinks.map((link, index) => (
<a
key={index}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="p-6 rounded-xl border transition-all duration-200 hover:shadow-lg hover:scale-105"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)'
}}
>
<div className="flex items-center gap-3">
<span className="text-2xl">{link.icon}</span>
<div>
<h4 className="font-semibold" style={{ color: 'var(--text-primary)' }}>
{link.name}
</h4>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
View on {link.name}
</p>
</div>
</div>
</a>
))}
</div>
</div>
)}
</div>
</div>
{/* Quantity Modal */}
{showQuantityModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl p-6 max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<h3 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
{ownedQuantity > 0 ? 'Update Quantity' : 'Mark as Owned'}
</h3>
<div className="mb-6">
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Quantity
</label>
<div className="flex items-center gap-4">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="w-10 h-10 rounded-lg flex items-center justify-center border transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
-
</button>
<span className="text-2xl font-bold px-4" style={{ color: 'var(--text-primary)' }}>
{quantity}
</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="w-10 h-10 rounded-lg flex items-center justify-center border transition-all duration-200 hover:shadow-md"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
+
</button>
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => {
setOwnedQuantity(quantity);
setShowQuantityModal(false);
}}
className="flex-1 px-4 py-2 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200"
>
{ownedQuantity > 0 ? 'Update' : 'Mark as Owned'}
</button>
<button
onClick={() => setShowQuantityModal(false)}
className="flex-1 px-4 py-2 rounded-xl font-medium border transition-all duration-200"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Collection Modal */}
{showCollectionModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl p-6 max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<h3 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Add to Collection
</h3>
<div className="mb-6">
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Select Collection
</label>
<select
value={selectedCollection}
onChange={(e) => setSelectedCollection(e.target.value)}
className="w-full p-3 rounded-xl border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="">Choose a collection...</option>
{mockCollections
.filter(collection => collection.game === card.game)
.map(collection => (
<option key={collection.id} value={collection.id}>
{collection.name}
</option>
))}
</select>
</div>
<div className="flex gap-3">
<button
onClick={() => {
if (selectedCollection) {
// Here you would typically make an API call to add the card to the collection
alert(`Added ${card.name} to collection!`);
setShowCollectionModal(false);
setSelectedCollection('');
}
}}
disabled={!selectedCollection}
className={`flex-1 px-4 py-2 rounded-xl font-medium transition-all duration-200 ${
selectedCollection
? 'gradient-bg-purple text-white hover:shadow-lg'
: 'opacity-50 cursor-not-allowed'
}`}
>
Add to Collection
</button>
<button
onClick={() => {
setShowCollectionModal(false);
setSelectedCollection('');
}}
className="flex-1 px-4 py-2 rounded-xl font-medium border transition-all duration-200"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Deck Modal */}
{showDeckModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl p-6 max-w-md w-full mx-4" style={{ backgroundColor: 'var(--bg-primary)' }}>
<h3 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
Add to Deck
</h3>
<div className="mb-6">
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Select Deck
</label>
<select
value={selectedDeck}
onChange={(e) => setSelectedDeck(e.target.value)}
className="w-full p-3 rounded-xl border transition-all duration-200"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border)',
color: 'var(--text-primary)'
}}
>
<option value="">Choose a deck...</option>
{mockDecks
.filter(deck => deck.game === card.game)
.map(deck => (
<option key={deck.id} value={deck.id}>
{deck.name}
</option>
))}
</select>
</div>
<div className="flex gap-3">
<button
onClick={() => {
if (selectedDeck) {
// Here you would typically make an API call to add the card to the deck
alert(`Added ${card.name} to deck!`);
setShowDeckModal(false);
setSelectedDeck('');
}
}}
disabled={!selectedDeck}
className={`flex-1 px-4 py-2 rounded-xl font-medium transition-all duration-200 ${
selectedDeck
? 'gradient-bg-purple text-white hover:shadow-lg'
: 'opacity-50 cursor-not-allowed'
}`}
>
Add to Deck
</button>
<button
onClick={() => {
setShowDeckModal(false);
setSelectedDeck('');
}}
className="flex-1 px-4 py-2 rounded-xl font-medium border transition-all duration-200"
style={{
borderColor: 'var(--border)',
color: 'var(--text-secondary)'
}}
>
Cancel
</button>
</div>
</div>
</div>
)}
</Layout>
);
}