deckhearth/pages/card/[id].js
Randall Stillwell 8bacbaca39 fix(lint): clear ESLint baseline in pages/
Resolve react-hooks, no-unescaped-entities, and no-img-element findings
under pages/ with lint-only changes so npx eslint pages/ exits clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 00:48:31 -05:00

918 lines
No EOL
37 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
import { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/router';
import Layout from '../../components/Layout';
import { useAuth } from '../../lib/use-auth';
import CollectionSelectionModal from '../../components/CollectionSelectionModal';
import { ManaCost, ColorIdentity, AdvancedManaCost } from '../../components/ManaSymbols';
import ManaSymbolSettings from '../../components/ManaSymbolSettings';
import { VOCAB, collectionDisplayName } from '../../lib/collection-vocabulary.js';
export default function CardDetail() {
const router = useRouter();
const { id } = router.query;
const { user, loading: authLoading } = useAuth();
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);
const [collections, setCollections] = useState([]);
const [decks, setDecks] = useState([]);
const [cardCollections, setCardCollections] = useState([]);
const [cardDecks, setCardDecks] = useState([]);
// Mana symbol settings
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
const isAdmin = user?.role === 'admin';
const adminLoading = authLoading;
// Fetch card data from API
useEffect(() => {
const fetchCard = async () => {
if (!id) return;
try {
const response = await fetch(`/api/cards/${id}`);
if (response.ok) {
const cardData = await response.json();
setCard(cardData);
// Fetch user's ownership of this card
const token = localStorage.getItem('auth_token');
if (token) {
try {
const ownershipResponse = await fetch(`/api/cards/${id}/ownership`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (ownershipResponse.ok) {
const ownershipData = await ownershipResponse.json();
setOwnedQuantity(ownershipData.quantity || 0);
}
} catch (error) {
console.error('Error fetching ownership:', error);
}
// Check if card is favorited
try {
const favoritesResponse = await fetch(`/api/favorites?type=card`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (favoritesResponse.ok) {
const favoritesData = await favoritesResponse.json();
const isCardFavorited = favoritesData.favorites.some(fav => fav.item_id == id);
setIsFavorited(isCardFavorited);
}
} catch (error) {
console.error('Error checking favorites:', error);
}
}
} else {
console.error('Failed to fetch card');
}
} catch (error) {
console.error('Error fetching card:', error);
} finally {
setLoading(false);
}
};
fetchCard();
}, [id]);
// Fetch user's collections and decks
useEffect(() => {
const fetchUserData = async () => {
try {
const token = localStorage.getItem('auth_token');
const headers = {
'Authorization': `Bearer ${token}`
};
// Fetch collections
const collectionsResponse = await fetch('/api/collections', { headers });
if (collectionsResponse.ok) {
const collectionsData = await collectionsResponse.json();
setCollections(collectionsData);
}
// Fetch decks
const decksResponse = await fetch('/api/decks', { headers });
if (decksResponse.ok) {
const decksData = await decksResponse.json();
setDecks(decksData);
}
// Fetch card's current collections and decks
if (card) {
const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`, { headers });
if (cardCollectionsResponse.ok) {
const cardCollectionsData = await cardCollectionsResponse.json();
setCardCollections(cardCollectionsData);
}
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`, { headers });
if (cardDecksResponse.ok) {
const cardDecksData = await cardDecksResponse.json();
setCardDecks(cardDecksData);
}
}
} catch (error) {
console.error('Error fetching user data:', error);
}
};
if (card) {
fetchUserData();
}
}, [card, id]);
const getRarityGradient = (rarity) => {
const rarityKey = rarity?.toLowerCase();
const gradients = {
'common': '#9ca3af, #6b7280, #4b5563', // Subtle gray glow
'uncommon': '#10b981, #059669, #047857', // Green glow
'rare': '#f59e0b, #d97706, #b45309', // Gold glow
'mythic': '#fbbf24, #f59e0b, #d97706', // Rich gold glow
'holographic': '#ec4899, #db2777, #be185d', // Pink glow
'enchanted': '#a855f7, #9333ea, #7c3aed', // Purple glow
'super rare': '#3b82f6, #2563eb, #1d4ed8', // Blue glow
'legendary': '#fbbf24, #f59e0b, #ea580c' // Vibrant gold-orange glow
};
return gradients[rarityKey] || '#6b7280, #4b5563, #374151';
};
const getParticleCount = (rarity) => {
const rarityKey = rarity?.toLowerCase();
const particleCounts = {
'common': 0,
'uncommon': 15,
'rare': 25,
'mythic': 40,
'holographic': 50,
'enchanted': 60,
'super rare': 45,
'legendary': 80
};
return particleCounts[rarityKey] || 0;
};
const particleStyles = useMemo(() => {
if (!card) return [];
const count = getParticleCount(card.rarity);
return Array.from({ length: count }, (_, i) => ({
left: `${((i * 37) % 100)}%`,
top: `${((i * 53) % 100)}%`,
animationDelay: `${i % 3}s`,
animationDuration: `${3 + (i % 2)}s`,
}));
// eslint-disable-next-line react-hooks/exhaustive-deps -- stable decorative layout per card id/rarity
}, [card?.id, card?.rarity]);
const getParticleColor = (rarity) => {
const rarityKey = rarity?.toLowerCase();
const colors = {
'common': '#ffffff',
'uncommon': '#10b981',
'rare': '#f59e0b',
'mythic': '#ffd700',
'holographic': '#ff6b6b',
'enchanted': '#a855f7',
'super rare': '#3b82f6',
'legendary': '#ffd700'
};
return colors[rarityKey] || '#ffffff';
};
const getTCGIcon = (game) => {
const icons = {
'MTG': '🔮',
'Pokemon': '⚡',
'Lorcana': '✨'
};
return icons[game] || '🃏';
};
const formatCurrency = (amount) => {
if (!amount) return '$0.00';
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',
'super rare': 'Super Rare',
'legendary': 'Legendary'
};
return rarityMap[rarity?.toLowerCase()] || rarity;
};
const getRarityColor = (rarity) => {
const colors = {
'common': '#6B7280',
'uncommon': '#10B981',
'rare': '#F59E0B',
'mythic': '#FFD700',
'holographic': '#FF6B6B',
'enchanted': '#A855F7',
'super rare': '#3B82F6',
'legendary': '#FFD700'
};
return colors[rarity?.toLowerCase()] || '#6B7280';
};
const handleOwnershipUpdate = async (newQuantity) => {
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/cards/${id}/ownership`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ quantity: newQuantity })
});
if (response.ok) {
setOwnedQuantity(newQuantity);
setShowQuantityModal(false);
} else {
console.error('Failed to update ownership');
}
} catch (error) {
console.error('Error updating ownership:', error);
}
};
const handleAddToCollection = async () => {
try {
const token = localStorage.getItem('auth_token');
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
const response = await fetch(`/api/cards/${id}/collections`, {
method: 'POST',
headers,
body: JSON.stringify({ collectionId: selectedCollection })
});
if (response.ok) {
// Refresh card collections
const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (cardCollectionsResponse.ok) {
const cardCollectionsData = await cardCollectionsResponse.json();
setCardCollections(cardCollectionsData);
}
setShowCollectionModal(false);
setSelectedCollection('');
} else {
console.error('Failed to add to collection');
}
} catch (error) {
console.error('Error adding to collection:', error);
}
};
const handleAddToDeck = async () => {
try {
const token = localStorage.getItem('auth_token');
const headers = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
};
const response = await fetch(`/api/cards/${id}/decks`, {
method: 'POST',
headers,
body: JSON.stringify({ deckId: selectedDeck })
});
if (response.ok) {
// Refresh card decks
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (cardDecksResponse.ok) {
const cardDecksData = await cardDecksResponse.json();
setCardDecks(cardDecksData);
}
setShowDeckModal(false);
setSelectedDeck('');
} else {
console.error('Failed to add to deck');
}
} catch (error) {
console.error('Error adding to deck:', error);
}
};
const handleToggleFavorite = async () => {
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/cards/${id}/favorite`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ favorited: !isFavorited })
});
if (response.ok) {
setIsFavorited(!isFavorited);
} else {
console.error('Failed to toggle favorite');
}
} catch (error) {
console.error('Error toggling favorite:', error);
}
};
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&apos;re looking for doesn&apos;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}>
<div style={{ backgroundColor: 'var(--bg-primary)', minHeight: '100vh' }}>
{/* 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, var(--bg-secondary), var(--bg-primary))'
}}
>
{/* Animated Particles Background */}
{getParticleCount(card.rarity) > 0 && (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{particleStyles.map((style, i) => (
<div
key={i}
className="absolute animate-float"
style={style}
>
<div
className="w-1 h-1 rounded-full opacity-60"
style={{
backgroundColor: getParticleColor(card.rarity),
boxShadow: `0 0 6px ${getParticleColor(card.rarity)}`
}}
/>
</div>
))}
</div>
)}
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<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='%23000000' 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">
{/* Rarity Glow Effect */}
<div
className="absolute inset-0 rounded-2xl blur-xl opacity-60 animate-pulse"
style={{
background: `linear-gradient(135deg, ${getRarityGradient(card.rarity)})`,
transform: 'scale(1.1)'
}}
/>
<div
className="relative w-80 h-112 rounded-2xl overflow-hidden shadow-2xl transform transition-all duration-300 hover:scale-105"
style={{
background: `linear-gradient(135deg, ${getRarityColor(card.rarity)}15, ${getRarityColor(card.rarity)}08)`,
boxShadow: `0 20px 40px rgba(0, 0, 0, 0.3), 0 0 30px ${getRarityColor(card.rarity)}40`
}}
>
{card.image_url ? (
<img
src={card.image_url}
alt={card.name}
className="w-full h-full object-cover"
/>
) : (
<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_name}</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 style={{ color: 'var(--text-primary)' }}>
<div className="mb-6">
<h1 className="text-4xl font-bold mb-2">{card.name}</h1>
<p className="text-xl opacity-90 mb-4">{card.oracle_text || card.card_type}</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">
In {VOCAB.MY_COLLECTION} ({ownedQuantity})
</span>
)}
<button
onClick={handleToggleFavorite}
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' : VOCAB.ADD_TO_MY_COLLECTION}
</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>
{VOCAB.ADD_TO_LIST}
</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.current_price || 0)}
</div>
{/* Admin Edit Button */}
{isAdmin && !adminLoading && (
<div className="mt-4">
<button
onClick={() => router.push(`/admin/card-editor?id=${card.id}`)}
className="px-4 py-2 rounded-xl font-medium bg-orange-500 hover:bg-orange-600 text-white transition-all duration-200 flex items-center gap-2 shadow-lg hover:shadow-xl"
>
<span></span>
Edit Card (Admin)
</button>
</div>
)}
</div>
{/* Current Collections and Decks */}
{(cardCollections.length > 0 || cardDecks.length > 0) && (
<div className="mb-6 p-4 rounded-xl bg-white bg-opacity-10 backdrop-blur-sm">
<h4 className="font-semibold mb-3">Currently In:</h4>
<div className="space-y-2">
{cardCollections.map(collection => (
<div key={collection.id} className="flex items-center gap-2 text-sm">
<span>📁</span>
<span>{collectionDisplayName(collection)}</span>
</div>
))}
{cardDecks.map(deck => (
<div key={deck.id} className="flex items-center gap-2 text-sm">
<span>🎴</span>
<span>{deck.name}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
</div>
{/* Content Tabs */}
<div
className="container mx-auto px-6 py-8"
style={{ backgroundColor: 'var(--bg-primary)', color: 'var(--text-primary)' }}
>
<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_name}</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.card_number}</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.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>
{card.mana_cost && (
<div className="flex justify-between py-3 border-b" style={{ borderColor: 'var(--border)' }}>
<span style={{ color: 'var(--text-secondary)' }}>Cost to Play</span>
<ManaCost cost={card.mana_cost} size="md" useSVG={manaSymbolSettings.useSVG} />
</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.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 */}
<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.oracle_text && (
<div className="p-4 rounded-xl" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<p style={{ color: 'var(--text-primary)' }}>{card.oracle_text}</p>
</div>
)}
</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 Placeholder */}
<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="h-64 flex items-center justify-center">
<p style={{ color: 'var(--text-secondary)' }}>
Price history data will be available soon
</p>
</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.current_price || 0)}
</div>
</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">
<a
href={`https://www.tcgplayer.com/search/${card.game.toLowerCase()}/product?productName=${encodeURIComponent(card.name)}`}
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">🃏</span>
<div>
<h4 className="font-semibold" style={{ color: 'var(--text-primary)' }}>
TCGPlayer
</h4>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
View on TCGPlayer
</p>
</div>
</div>
</a>
<a
href={`https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(card.name + ' ' + card.game)}`}
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">🛒</span>
<div>
<h4 className="font-semibold" style={{ color: 'var(--text-primary)' }}>
eBay
</h4>
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
View on eBay
</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' : VOCAB.ADD_TO_MY_COLLECTION}
</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={() => handleOwnershipUpdate(quantity)}
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' : VOCAB.ADD_TO_MY_COLLECTION}
</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 Selection Modal */}
<CollectionSelectionModal
isOpen={showCollectionModal}
onClose={() => setShowCollectionModal(false)}
cards={card ? [card] : []}
onAddToCollections={(results, selectedCollectionIds, cards) => {
const successCount = results.filter(r => r.success).length;
if (successCount > 0) {
// Refresh card collections
const fetchCardCollections = async () => {
try {
const token = localStorage.getItem('auth_token');
const response = await fetch(`/api/cards/${id}/collections`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const data = await response.json();
setCardCollections(data);
}
} catch (error) {
console.error('Error refreshing card collections:', error);
}
};
fetchCardCollections();
}
}}
/>
{/* 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>
{decks
.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={handleAddToDeck}
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>
)}
</div>
</Layout>
);
}