import { useState, useEffect } 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 } 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 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 ( ); } if (!card) { return ( 🃏 Card Not Found The card you're looking for doesn't exist. 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 ); } return ( {/* Hero Section with Card Image and Basic Info */} {/* Animated Particles Background */} {getParticleCount(card.rarity) > 0 && ( {Array.from({ length: getParticleCount(card.rarity) }).map((_, i) => ( ))} )} {/* Background Pattern */} {/* Card Image */} {/* Rarity Glow Effect */} {card.image_url ? ( ) : ( {getTCGIcon(card.game)} {card.name} {card.set_name} {getRarityLabel(card.rarity)} )} {/* Card Info */} {card.name} {card.oracle_text || card.card_type} {/* Ownership Status and Actions */} Ownership Status {ownedQuantity > 0 && ( In {VOCAB.MY_COLLECTION} ({ownedQuantity}) )} {isFavorited ? '❤️' : '🤍'} 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" > 📦 {ownedQuantity > 0 ? 'Update Quantity' : VOCAB.ADD_TO_MY_COLLECTION} 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" > 📁 {VOCAB.ADD_TO_LIST} 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" > 🎴 Add to Deck {getTCGIcon(card.game)} {card.game} {getRarityLabel(card.rarity)} {formatCurrency(card.current_price || 0)} {/* Admin Edit Button */} {isAdmin && !adminLoading && ( 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" > ✏️ Edit Card (Admin) )} {/* Current Collections and Decks */} {(cardCollections.length > 0 || cardDecks.length > 0) && ( Currently In: {cardCollections.map(collection => ( 📁 {collection.name} ))} {cardDecks.map(deck => ( 🎴 {deck.name} ))} )} {/* Content Tabs */} {['details', 'price-history', 'purchase'].map((tab) => ( 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'} ))} {/* Tab Content */} {activeTab === 'details' && ( {/* Card Metadata */} Card Information TCG {card.game} Set {card.set_name} Card Number {card.card_number} Type {card.card_type} Rarity {getRarityLabel(card.rarity)} {card.mana_cost && ( Cost to Play )} {card.power && ( Power {card.power} )} {card.toughness && ( Toughness {card.toughness} )} {card.current_price && ( Current Price {formatCurrency(card.current_price)} )} {/* Card Text */} Card Text {card.oracle_text && ( {card.oracle_text} )} )} {activeTab === 'price-history' && ( Price History {/* Price Graph Placeholder */} Price Trend Price history data will be available soon {/* Price Statistics Cards */} {/* Current Price */} 💰 Current Price {formatCurrency(card.current_price || 0)} )} {activeTab === 'purchase' && ( Where to Buy 🃏 TCGPlayer View on TCGPlayer 🛒 eBay View on eBay )} {/* Quantity Modal */} {showQuantityModal && ( {ownedQuantity > 0 ? 'Update Quantity' : VOCAB.ADD_TO_MY_COLLECTION} Quantity 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)' }} > - {quantity} 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)' }} > + 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} 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 )} {/* Collection Selection Modal */} 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 && ( Add to Deck Select Deck 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)' }} > Choose a deck... {decks .filter(deck => deck.game === card.game) .map(deck => ( {deck.name} ))} Add to Deck { 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 )} ); }
The card you're looking for doesn't exist.
{card.set_name}
{card.oracle_text || card.card_type}
{card.oracle_text}
Price history data will be available soon
View on TCGPlayer
View on eBay