deckhearth/components/CardDetailView.js

495 lines
21 KiB
JavaScript
Raw Normal View History

/* 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>
</>
);
}