Extract useCardDetail hook and CardDetailView (Brief 3).

Completes card detail god-component split: thin page composer with loading/not-found branches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-06-03 17:01:27 -05:00
parent 8e2c0e47a8
commit 4a1adb6bcc
3 changed files with 905 additions and 779 deletions

View file

@ -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 (
<>
<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>
<CardDetailQuantityModal
isOpen={showQuantityModal}
ownedQuantity={ownedQuantity}
quantity={quantity}
onQuantityChange={setQuantity}
onConfirm={handleOwnershipUpdate}
onClose={() => setShowQuantityModal(false)}
/>
{/* 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();
}
}}
/>
<CardDetailDeckModal
isOpen={showDeckModal}
card={card}
decks={decks}
selectedDeck={selectedDeck}
onSelectedDeckChange={setSelectedDeck}
onAdd={handleAddToDeck}
onClose={() => {
setShowDeckModal(false);
setSelectedDeck('');
}}
/>
</div>
</>
);
}

400
lib/use-card-detail.js Normal file
View file

@ -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
};
}

View file

@ -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 { useRouter } from 'next/router';
import Layout from '../../components/Layout'; import Layout from '../../components/Layout';
import CardDetailView from '../../components/CardDetailView';
import { useAuth } from '../../lib/use-auth'; import { useAuth } from '../../lib/use-auth';
import CollectionSelectionModal from '../../components/CollectionSelectionModal'; import { useCardDetail } from '../../lib/use-card-detail.js';
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';
export default function CardDetail() { export default function CardDetail() {
const router = useRouter(); const router = useRouter();
const { id } = router.query; const { user } = useAuth();
const { user, loading: authLoading } = useAuth(); const detail = useCardDetail();
const [card, setCard] = useState(null); if (detail.loading) {
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 ( return (
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <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 className="animate-spin rounded-full h-32 w-32 border-b-2" style={{ borderColor: 'var(--text-accent-light)' }} />
</div> </div>
</Layout> </Layout>
); );
} }
if (!card) { if (!detail.card) {
return ( return (
<Layout user={user}> <Layout user={user}>
<div className="flex items-center justify-center min-h-screen"> <div className="flex items-center justify-center min-h-screen">
<div className="text-center"> <div className="text-center">
<div className="text-6xl mb-4">🃏</div> <div className="text-6xl mb-4">🃏</div>
<h2 className="text-2xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}> <h2 className="text-xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
Card Not Found Card Not Found
</h2> </h2>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}> <p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
The card you&apos;re looking for doesn&apos;t exist. The card you&apos;re looking for doesn&apos;t exist.
</p> </p>
<button <button
type="button"
onClick={() => router.push('/cards')} 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" className="px-6 py-3 rounded-xl font-medium gradient-bg-purple text-white hover:shadow-lg transition-all duration-200"
> >
@ -386,435 +46,7 @@ export default function CardDetail() {
return ( return (
<Layout user={user}> <Layout user={user}>
<div style={{ backgroundColor: 'var(--bg-primary)', minHeight: '100vh' }}> <CardDetailView {...detail} />
{/* 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>
<CardDetailQuantityModal
isOpen={showQuantityModal}
ownedQuantity={ownedQuantity}
quantity={quantity}
onQuantityChange={setQuantity}
onConfirm={handleOwnershipUpdate}
onClose={() => setShowQuantityModal(false)}
/>
{/* 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();
}
}}
/>
<CardDetailDeckModal
isOpen={showDeckModal}
card={card}
decks={decks}
selectedDeck={selectedDeck}
onSelectedDeckChange={setSelectedDeck}
onAdd={handleAddToDeck}
onClose={() => {
setShowDeckModal(false);
setSelectedDeck('');
}}
/>
</div>
</Layout> </Layout>
); );
} }