diff --git a/components/CardDetailView.js b/components/CardDetailView.js new file mode 100644 index 0000000..6e3687c --- /dev/null +++ b/components/CardDetailView.js @@ -0,0 +1,494 @@ +/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */ +import CollectionSelectionModal from './CollectionSelectionModal'; +import CardDetailQuantityModal from './CardDetailQuantityModal'; +import CardDetailDeckModal from './CardDetailDeckModal'; +import { ManaCost, ColorIdentity, AdvancedManaCost } from './ManaSymbols'; +import ManaSymbolSettings from './ManaSymbolSettings'; +import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js'; +export default function CardDetailView(props) { + const { + activeTab, + adminLoading, + card, + cardCollections, + cardDecks, + collections, + decks, + formatCurrency, + getParticleColor, + getParticleCount, + getRarityColor, + getRarityGradient, + getRarityLabel, + getTCGIcon, + handleAddToCollection, + handleAddToDeck, + handleOwnershipUpdate, + handleToggleFavorite, + id, + isAdmin, + isFavorited, + loading, + manaSymbolSettings, + ownedQuantity, + particleStyles, + quantity, + router, + selectedCollection, + selectedDeck, + setActiveTab, + setCard, + setCardCollections, + setCardDecks, + setCollections, + setDecks, + setIsFavorited, + setLoading, + setManaSymbolSettings, + setOwnedQuantity, + setQuantity, + setSelectedCollection, + setSelectedDeck, + setShowCollectionModal, + setShowDeckModal, + setShowQuantityModal, + showCollectionModal, + showDeckModal, + showInitialLoading, + showQuantityModal + } = props; + + return ( + <> +
+ {/* Hero Section with Card Image and Basic Info */} +
+ {/* Animated Particles Background */} + {getParticleCount(card.rarity) > 0 && ( +
+ {particleStyles.map((style, i) => ( +
+
+
+ ))} +
+ )} + + {/* Background Pattern */} +
+
+
+ +
+
+ {/* Card Image */} +
+
+ {/* Rarity Glow Effect */} +
+
+ {card.image_url ? ( + {card.name} + ) : ( +
+
+
{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}) + + )} + +
+
+
+ + + +
+
+ +
+ {getTCGIcon(card.game)} + {card.game} + + {getRarityLabel(card.rarity)} + +
+
+ {formatCurrency(card.current_price || 0)} +
+ + {/* Admin Edit Button */} + {isAdmin && !adminLoading && ( +
+ +
+ )} +
+ + {/* Current Collections and Decks */} + {(cardCollections.length > 0 || cardDecks.length > 0) && ( +
+

Currently In:

+
+ {cardCollections.map(collection => ( +
+ 📁 + {collectionDisplayName(collection)} +
+ ))} + {cardDecks.map(deck => ( +
+ 🎴 + {deck.name} +
+ ))} +
+
+ )} +
+
+
+
+ + {/* Content Tabs */} +
+
+ {['details', 'price-history', 'purchase'].map((tab) => ( + + ))} +
+ + {/* 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' && ( + + )} +
+
+ + setShowQuantityModal(false)} + /> + + {/* 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(); + } + }} + /> + + { + setShowDeckModal(false); + setSelectedDeck(''); + }} + /> +
+ + ); +} diff --git a/lib/use-card-detail.js b/lib/use-card-detail.js new file mode 100644 index 0000000..a2c072f --- /dev/null +++ b/lib/use-card-detail.js @@ -0,0 +1,400 @@ +import { useState, useEffect, useMemo } from 'react'; +import { useRouter } from 'next/router'; +import { VOCAB, collectionDisplayName } from './collection-vocabulary.js'; + +/** + * Card/collection page state and handlers (god-component split). + */ +export function useCardDetail({ user, authLoading }) { + const router = useRouter(); + const isAdmin = user?.role === 'admin'; + const adminLoading = authLoading; + const { id } = router.query; + + 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 }); + + // 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); + } + }; + + const showInitialLoading = authLoading || loading; + + return { + activeTab, + card, + cardCollections, + cardDecks, + collections, + decks, + formatCurrency, + getParticleColor, + getParticleCount, + getRarityColor, + getRarityGradient, + getRarityLabel, + getTCGIcon, + handleAddToCollection, + handleAddToDeck, + handleOwnershipUpdate, + handleToggleFavorite, + id, + isAdmin, + adminLoading, + isFavorited, + loading, + manaSymbolSettings, + ownedQuantity, + particleStyles, + getParticleCount, + quantity, + router, + selectedCollection, + selectedDeck, + setActiveTab, + setCard, + setCardCollections, + setCardDecks, + setCollections, + setDecks, + setIsFavorited, + setLoading, + setManaSymbolSettings, + setOwnedQuantity, + setQuantity, + setSelectedCollection, + setSelectedDeck, + setShowCollectionModal, + setShowDeckModal, + setShowQuantityModal, + showCollectionModal, + showDeckModal, + showInitialLoading, + showQuantityModal + }; +} diff --git a/pages/card/[id].js b/pages/card/[id].js index aa1b700..bfe4fca 100644 --- a/pages/card/[id].js +++ b/pages/card/[id].js @@ -1,378 +1,38 @@ -/* 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 CardDetailView from '../../components/CardDetailView'; import { useAuth } from '../../lib/use-auth'; -import CollectionSelectionModal from '../../components/CollectionSelectionModal'; -import CardDetailQuantityModal from '../../components/CardDetailQuantityModal'; -import CardDetailDeckModal from '../../components/CardDetailDeckModal'; -import { ManaCost, ColorIdentity, AdvancedManaCost } from '../../components/ManaSymbols'; -import ManaSymbolSettings from '../../components/ManaSymbolSettings'; -import { VOCAB, collectionDisplayName } from '../../lib/collection-vocabulary.js'; +import { useCardDetail } from '../../lib/use-card-detail.js'; export default function CardDetail() { const router = useRouter(); - const { id } = router.query; - const { user, loading: authLoading } = useAuth(); + const { user } = useAuth(); + const detail = useCardDetail(); - 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) { + if (detail.loading) { return (
-
+
); } - if (!card) { + if (!detail.card) { return (
🃏
-

+

Card Not Found

The card you're looking for doesn't exist.

-
-
-
- - - -
-
- -
- {getTCGIcon(card.game)} - {card.game} - - {getRarityLabel(card.rarity)} - -
-
- {formatCurrency(card.current_price || 0)} -
- - {/* Admin Edit Button */} - {isAdmin && !adminLoading && ( -
- -
- )} -
- - {/* Current Collections and Decks */} - {(cardCollections.length > 0 || cardDecks.length > 0) && ( -
-

Currently In:

-
- {cardCollections.map(collection => ( -
- 📁 - {collectionDisplayName(collection)} -
- ))} - {cardDecks.map(deck => ( -
- 🎴 - {deck.name} -
- ))} -
-
- )} -
- - - - - {/* Content Tabs */} -
-
- {['details', 'price-history', 'purchase'].map((tab) => ( - - ))} -
- - {/* 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 -

-
-
-
-
-
- )} -
-
- - setShowQuantityModal(false)} - /> - - {/* 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(); - } - }} - /> - - { - setShowDeckModal(false); - setSelectedDeck(''); - }} - /> - + ); -} \ No newline at end of file +}