- 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
925 lines
No EOL
40 KiB
JavaScript
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>
|
|
);
|
|
}
|