Extract Card3D and CardBack from pages/cards.js (Brief 1).
Moves ~530 lines of 3D card rendering into components/Card3D.js so the cards page god-component split can continue incrementally. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2273fc6be0
commit
471ce3b774
2 changed files with 530 additions and 531 deletions
530
components/Card3D.js
Normal file
530
components/Card3D.js
Normal file
|
|
@ -0,0 +1,530 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External card image URLs; next/image migration is out of scope. */
|
||||
import { useState } from 'react';
|
||||
import { ManaCost } from './ManaSymbols';
|
||||
import { VOCAB } from '../lib/collection-vocabulary.js';
|
||||
|
||||
export default function Card3D({ card, viewMode, getRarityLabel, getRarityColor, useManaSvg = false }) {
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
setCardPosition({
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const getTCGColor = (game) => {
|
||||
const colors = {
|
||||
'MTG': '#8B5CF6', // purple
|
||||
'Pokemon': '#3B82F6', // blue
|
||||
'Lorcana': '#EC4899' // pink
|
||||
};
|
||||
return colors[game] || '#6B7280';
|
||||
};
|
||||
|
||||
const getSetColor = (setName) => {
|
||||
// Generate a consistent color based on set name
|
||||
const hash = setName.split('').reduce((a, b) => {
|
||||
a = ((a << 5) - a) + b.charCodeAt(0);
|
||||
return a & a;
|
||||
}, 0);
|
||||
const hue = Math.abs(hash) % 360;
|
||||
return `hsl(${hue}, 70%, 60%)`;
|
||||
};
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const getRarityGradient = (rarity) => {
|
||||
const gradients = {
|
||||
common: 'from-gray-400 to-gray-500',
|
||||
uncommon: 'from-green-400 to-emerald-500',
|
||||
rare: 'from-yellow-400 to-orange-500',
|
||||
mythic: 'from-yellow-400 to-orange-500',
|
||||
holographic: 'from-red-400 to-pink-500',
|
||||
enchanted: 'from-purple-400 to-indigo-500',
|
||||
ultra: 'from-blue-400 to-cyan-500'
|
||||
};
|
||||
return gradients[rarity] || 'from-gray-400 to-gray-500';
|
||||
};
|
||||
|
||||
const getRarityGlow = (rarity) => {
|
||||
const glows = {
|
||||
common: '0 0 15px rgba(156, 163, 175, 0.4)',
|
||||
uncommon: '0 0 15px rgba(34, 197, 94, 0.4)',
|
||||
rare: '0 0 20px rgba(251, 191, 36, 0.5)',
|
||||
mythic: '0 0 20px rgba(255, 215, 0, 0.4), 0 0 40px rgba(255, 215, 0, 0.2), 0 0 60px rgba(255, 215, 0, 0.1)',
|
||||
holographic: '0 0 25px rgba(239, 68, 68, 0.6)',
|
||||
enchanted: '0 0 20px rgba(168, 85, 247, 0.4), 0 0 40px rgba(168, 85, 247, 0.2), 0 0 60px rgba(168, 85, 247, 0.1)',
|
||||
ultra: '0 0 25px rgba(59, 130, 246, 0.6)'
|
||||
};
|
||||
return glows[rarity] || '0 0 15px rgba(156, 163, 175, 0.4)';
|
||||
};
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return (
|
||||
<div
|
||||
className="card hover:shadow-xl transition-all duration-300 cursor-pointer"
|
||||
style={{
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-24 rounded-lg overflow-hidden" style={{
|
||||
background: `linear-gradient(135deg, ${getRarityGradient(card.rarity).replace('from-', '').replace('to-', '')})`,
|
||||
boxShadow: `0 4px 8px rgba(0, 0, 0, 0.3)`
|
||||
}}>
|
||||
<div className="w-full h-full flex items-center justify-center text-white font-bold text-xs">
|
||||
{card.game}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||
{card.name}
|
||||
</h3>
|
||||
<p className="text-sm mb-2" style={{ color: 'var(--text-secondary)' }}>
|
||||
{card.set_name} • {getRarityLabel(card.rarity)}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{card.oracle_text || card.card_type}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold gradient-text-purple">
|
||||
{formatCurrency(card.current_price || 0)}
|
||||
</p>
|
||||
<span
|
||||
className="px-2 py-1 text-xs rounded-full text-white font-medium"
|
||||
style={{ backgroundColor: getRarityColor(card.rarity) }}
|
||||
>
|
||||
{getRarityLabel(card.rarity)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Card Container with proper ratio and rarity glow */}
|
||||
<div
|
||||
className="relative cursor-pointer transition-transform duration-300 hover:scale-105 w-full"
|
||||
style={{
|
||||
aspectRatio: '5/7',
|
||||
boxShadow: getRarityGlow(card.rarity),
|
||||
borderRadius: '12px',
|
||||
border: `2px solid ${getRarityColor(card.rarity)}20`,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
{/* Particle Effects for All Rarities */}
|
||||
{card.rarity !== 'common' && (
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
{/* Get particle count and colors based on rarity */}
|
||||
{(() => {
|
||||
const rarityConfig = {
|
||||
'mythic': {
|
||||
particleCount: 16,
|
||||
sparkleCount: 10,
|
||||
particleColor: '#FFD700',
|
||||
sparkleColor: '#FFA500',
|
||||
glowColor: '#FFD700'
|
||||
},
|
||||
'enchanted': {
|
||||
particleCount: 14,
|
||||
sparkleCount: 8,
|
||||
particleColor: '#A855F7',
|
||||
sparkleColor: '#EC4899',
|
||||
glowColor: '#A855F7'
|
||||
},
|
||||
'rare': {
|
||||
particleCount: 10,
|
||||
sparkleCount: 6,
|
||||
particleColor: '#3B82F6',
|
||||
sparkleColor: '#60A5FA',
|
||||
glowColor: '#3B82F6'
|
||||
},
|
||||
'uncommon': {
|
||||
particleCount: 6,
|
||||
sparkleCount: 4,
|
||||
particleColor: '#10B981',
|
||||
sparkleColor: '#34D399',
|
||||
glowColor: '#10B981'
|
||||
}
|
||||
};
|
||||
|
||||
const config = rarityConfig[card.rarity];
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Particles - Edge framing */}
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(config.particleCount)].map((_, i) => {
|
||||
// Position particles around the card edges
|
||||
let left, top;
|
||||
const edgeIndex = i % 4; // 4 edges
|
||||
const positionOnEdge = Math.floor(i / 4);
|
||||
|
||||
if (edgeIndex === 0) {
|
||||
// Top edge
|
||||
left = `${10 + (positionOnEdge * 20)}%`;
|
||||
top = '2%';
|
||||
} else if (edgeIndex === 1) {
|
||||
// Right edge
|
||||
left = '98%';
|
||||
top = `${10 + (positionOnEdge * 20)}%`;
|
||||
} else if (edgeIndex === 2) {
|
||||
// Bottom edge
|
||||
left = `${10 + (positionOnEdge * 20)}%`;
|
||||
top = '98%';
|
||||
} else {
|
||||
// Left edge
|
||||
left = '2%';
|
||||
top = `${10 + (positionOnEdge * 20)}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-1 h-1 rounded-full"
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
backgroundColor: config.particleColor,
|
||||
animation: `edgeFloat ${4 + i * 0.3}s ease-in-out infinite`,
|
||||
animationDelay: `${i * 0.2}s`,
|
||||
opacity: 0.7,
|
||||
filter: 'blur(1px)',
|
||||
boxShadow: `0 0 8px ${config.particleColor}, 0 0 16px ${config.particleColor}, 0 0 24px ${config.particleColor}`,
|
||||
mixBlendMode: 'screen'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Sparkle Effects - Corner highlights */}
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(config.sparkleCount)].map((_, i) => {
|
||||
// Position sparkles in the corners and edge centers
|
||||
let left, top;
|
||||
if (i < 2) {
|
||||
// Top corners
|
||||
left = i === 0 ? '5%' : '95%';
|
||||
top = '5%';
|
||||
} else if (i < 4) {
|
||||
// Bottom corners
|
||||
left = i === 2 ? '5%' : '95%';
|
||||
top = '95%';
|
||||
} else if (i < 6) {
|
||||
// Edge centers
|
||||
left = i === 4 ? '50%' : '50%';
|
||||
top = i === 4 ? '5%' : '95%';
|
||||
} else {
|
||||
// Side centers
|
||||
left = i === 6 ? '5%' : '95%';
|
||||
top = '50%';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`sparkle-${i}`}
|
||||
className="absolute w-0.5 h-0.5"
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
backgroundColor: config.sparkleColor,
|
||||
animation: `sparkle ${2 + i * 0.4}s ease-in-out infinite`,
|
||||
animationDelay: `${i * 0.2}s`,
|
||||
opacity: 0.9,
|
||||
filter: 'blur(0.5px)',
|
||||
boxShadow: `0 0 4px ${config.sparkleColor}, 0 0 8px ${config.sparkleColor}`,
|
||||
mixBlendMode: 'screen'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Rarity Aura - Blend with overall glow */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: `radial-gradient(circle at 50% 50%, ${config.glowColor}20 0%, ${config.glowColor}10 40%, transparent 70%)`,
|
||||
animation: 'aura 4s ease-in-out infinite',
|
||||
mixBlendMode: 'screen'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{/* Card Image */}
|
||||
<div className="w-full h-full relative">
|
||||
{card.image_url ? (
|
||||
<img
|
||||
src={card.image_url}
|
||||
alt={card.name}
|
||||
className="w-full h-full object-cover"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
e.target.nextSibling.style.display = 'flex';
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Card Back placeholder when no image */}
|
||||
<div
|
||||
className={`w-full h-full absolute inset-0 ${!card.image_url ? 'block' : 'hidden'}`}
|
||||
style={{ borderRadius: '10px' }}
|
||||
>
|
||||
<CardBack game={card.game} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Owned Quantity Chip - Only show if owned */}
|
||||
{card.quantity > 0 && (
|
||||
<div className="absolute bottom-0 left-0">
|
||||
<div
|
||||
className="px-3 py-1 text-xs font-bold text-white shadow-lg"
|
||||
style={{
|
||||
backgroundColor: 'var(--text-accent)',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
borderTop: '2px solid var(--text-accent)',
|
||||
borderLeft: '2px solid var(--text-accent)',
|
||||
borderRight: '2px solid var(--text-accent)',
|
||||
transform: 'translateY(2px)'
|
||||
}}
|
||||
>
|
||||
{card.quantity}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Favorite Button */}
|
||||
<div className="absolute top-2 right-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsFavorited(!isFavorited);
|
||||
}}
|
||||
className={`p-2 rounded-full transition-all duration-200 shadow-lg ${
|
||||
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>
|
||||
|
||||
{/* Hover Details Panel */}
|
||||
{isHovered && (
|
||||
<div
|
||||
className="fixed z-50 bg-black bg-opacity-90 text-white p-4 rounded-lg shadow-2xl max-w-sm"
|
||||
style={{
|
||||
left: cardPosition.x + cardPosition.width,
|
||||
top: cardPosition.y,
|
||||
pointerEvents: 'none',
|
||||
minHeight: cardPosition.height,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Card Title */}
|
||||
<div>
|
||||
<div className="text-sm font-bold text-white mb-1">{card.name}</div>
|
||||
</div>
|
||||
|
||||
{/* Top Section - TCG, Set, Type, HP in 2x2 grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">TCG</div>
|
||||
<div className="text-xs text-white">{card.game}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Set</div>
|
||||
<div className="text-xs text-white">{card.set_name}</div>
|
||||
</div>
|
||||
{card.type && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Type</div>
|
||||
<div className="text-xs text-white flex items-center gap-1">
|
||||
{card.game === 'Pokemon' && card.type === 'Lightning' && (
|
||||
<span className="text-yellow-400">⚡</span>
|
||||
)}
|
||||
{card.type}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{card.hp && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">HP</div>
|
||||
<div className="text-xs text-white">{card.hp}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Card Type and Form */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Card type</div>
|
||||
<div className="text-xs text-white">{card.card_type || 'Card'}</div>
|
||||
</div>
|
||||
{card.form && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Form</div>
|
||||
<div className="text-xs text-white">{card.form}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cost to Play */}
|
||||
{card.mana_cost && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Cost to Play</div>
|
||||
<ManaCost cost={card.mana_cost} size="sm" useSVG={useManaSvg} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Card Rule Section */}
|
||||
{card.game === 'Pokemon' && card.form && card.form.includes('V') && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Card rule</div>
|
||||
<div className="text-xs text-white leading-relaxed">
|
||||
V rule: When your Pokémon V is Knocked Out, your opponent takes 2 Prize cards.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Abilities/Attacks/Moves Section */}
|
||||
{card.oracle_text && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Card Text</div>
|
||||
<div className="text-xs text-white leading-relaxed">{card.oracle_text}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quote Section */}
|
||||
{card.flavor_text && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Quote</div>
|
||||
<div className="text-xs text-white italic leading-relaxed">"{card.flavor_text}"</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Price */}
|
||||
{card.current_price && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Current Price</div>
|
||||
<div className="text-xs text-white">{formatCurrency(card.current_price)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Section - Weaknesses and Retreat */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{card.weakness && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Weaknesses</div>
|
||||
<div className="text-xs text-white flex items-center gap-1">
|
||||
{card.game === 'Pokemon' && (
|
||||
<span className="text-red-500">👊</span>
|
||||
)}
|
||||
{card.weakness}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{card.retreat_cost && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Retreat</div>
|
||||
<div className="text-xs text-white flex items-center gap-1">
|
||||
{card.game === 'Pokemon' && (
|
||||
<>
|
||||
<span className="text-gray-400">⭐</span>
|
||||
<span className="text-gray-400">⭐</span>
|
||||
</>
|
||||
)}
|
||||
{card.retreat_cost}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Decks and Collections */}
|
||||
{(card.decks && card.decks.length > 0) || (card.collections && card.collections.length > 0) && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Included In</div>
|
||||
<div className="text-xs text-white">
|
||||
{card.decks && card.decks.length > 0 && (
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Decks:</span> {card.decks.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{card.collections && card.collections.length > 0 && (
|
||||
<div>
|
||||
<span className="font-semibold">{VOCAB.LISTS}:</span> {card.collections.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Card Back Component for placeholders
|
||||
function CardBack({ game }) {
|
||||
const getCardBackImage = () => {
|
||||
switch (game) {
|
||||
case 'MTG':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjOEI0NTEzIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNBMDUyMkQiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiM0QTkwRTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPk1BR0lDPC90ZXh0Pgo8dGV4dCB4PSIxMDAiIHk9IjQ1IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5UaGUgR2F0aGVyaW5nPC90ZXh0Pgo8Y2lyY2xlIGN4PSI3MCIgY3k9IjcwIiByPSI0IiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMzAiIGN4PSI3MCIgcj0iNCIgZmlsbD0iI0Y1OTlFMEIiLz4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iODAiIHI9IjQiIGZpbGw9IiM0QTkwRTIiLz4KPGNpcmNsZSBjeD0iNzAiIGN5PSI5MCIgcj0iNCIgZmlsbD0iIzEwQjk4MSIvPgo8Y2lyY2xlIGN4PSIxMzAiIGN5PSI5MCIgcj0iNCIgZmlsbD0iYmxhY2siLz4KPHRleHQgeD0iMTAwIiB5PSIxMjAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMyMEIyQUEiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkRFQ0tNQVNURVI8L3RleHQ+Cjwvc3ZnPgo=';
|
||||
case 'Pokemon':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjRkY2QjZCIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNGRjhFOEUiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGRkQ3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlBPS0VNT048L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iNDUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRyYWRpbmcgQ2FyZCBHYW1lPC90ZXh0Pgo8Y2lyY2xlIGN4PSIxMDAiIGN5PSI3MCIgcj0iMjAiIGZpbGw9IndoaXRlIi8+Cjx0ZXh0IHg9IjEwMCIgeT0iNzgiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZmlsbD0iIzFGN0Y3RiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+4pePPC90ZXh0Pgo8dGV4dCB4PSIxMDAiIHk9IjEyMCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI0ZGRDcwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+R0FNRSBGUkVBSzwvdGV4dD4KPC9zdmc+Cg==';
|
||||
case 'Lorcana':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjRUM0ODk5Ii8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNGNDcyQjYiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGQkJGMjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkRJU05FWTwvdGV4dD4KPHRleHQgeD0iMTAwIiB5PSI0NSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TG9yY2FuYTwvdGV4dD4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iNzAiIHI9IjIwIiBmaWxsPSJ3aGl0ZSIvPgo8dGV4dCB4PSIxMDAiIHk9Ijc4IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIGZpbGw9IiMxRjdGN0YiIHRleHQtYW5jaG9yPSJtaWRkbGUiPvCfkqQ8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iMTIwIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjRjU5RTBCIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5SQVZFTlNERVJHPzwvdGV4dD4KPC9zdmc+Cg==';
|
||||
default:
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjNkI3MjgwIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiM5Q0EzQUYiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5UQ0c8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iNDUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRyYWRpbmcgQ2FyZDwvdGV4dD4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iNzAiIHI9IjIwIiBmaWxsPSJ3aGl0ZSIvPgo8dGV4dCB4PSIxMDAiIHk9Ijc4IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIGZpbGw9IiMxRjdGN0YiIHRleHQtYW5jaG9yPSJtaWRkbGUiPvCfkqQ8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iMTIwIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjNkI3MjgwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5DQVJEPC90ZXh0Pgo8L3N2Zz4K';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={getCardBackImage()}
|
||||
alt={`${game} card back`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
531
pages/cards.js
531
pages/cards.js
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -973,533 +972,3 @@ export default function Cards() {
|
|||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
||||
// 3D Card Component
|
||||
function Card3D({ card, viewMode, getRarityLabel, getRarityColor }) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [cardPosition, setCardPosition] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||
const [isFavorited, setIsFavorited] = useState(false);
|
||||
|
||||
const handleMouseMove = (e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
setCardPosition({
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const getTCGColor = (game) => {
|
||||
const colors = {
|
||||
'MTG': '#8B5CF6', // purple
|
||||
'Pokemon': '#3B82F6', // blue
|
||||
'Lorcana': '#EC4899' // pink
|
||||
};
|
||||
return colors[game] || '#6B7280';
|
||||
};
|
||||
|
||||
const getSetColor = (setName) => {
|
||||
// Generate a consistent color based on set name
|
||||
const hash = setName.split('').reduce((a, b) => {
|
||||
a = ((a << 5) - a) + b.charCodeAt(0);
|
||||
return a & a;
|
||||
}, 0);
|
||||
const hue = Math.abs(hash) % 360;
|
||||
return `hsl(${hue}, 70%, 60%)`;
|
||||
};
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const getRarityGradient = (rarity) => {
|
||||
const gradients = {
|
||||
common: 'from-gray-400 to-gray-500',
|
||||
uncommon: 'from-green-400 to-emerald-500',
|
||||
rare: 'from-yellow-400 to-orange-500',
|
||||
mythic: 'from-yellow-400 to-orange-500',
|
||||
holographic: 'from-red-400 to-pink-500',
|
||||
enchanted: 'from-purple-400 to-indigo-500',
|
||||
ultra: 'from-blue-400 to-cyan-500'
|
||||
};
|
||||
return gradients[rarity] || 'from-gray-400 to-gray-500';
|
||||
};
|
||||
|
||||
const getRarityGlow = (rarity) => {
|
||||
const glows = {
|
||||
common: '0 0 15px rgba(156, 163, 175, 0.4)',
|
||||
uncommon: '0 0 15px rgba(34, 197, 94, 0.4)',
|
||||
rare: '0 0 20px rgba(251, 191, 36, 0.5)',
|
||||
mythic: '0 0 20px rgba(255, 215, 0, 0.4), 0 0 40px rgba(255, 215, 0, 0.2), 0 0 60px rgba(255, 215, 0, 0.1)',
|
||||
holographic: '0 0 25px rgba(239, 68, 68, 0.6)',
|
||||
enchanted: '0 0 20px rgba(168, 85, 247, 0.4), 0 0 40px rgba(168, 85, 247, 0.2), 0 0 60px rgba(168, 85, 247, 0.1)',
|
||||
ultra: '0 0 25px rgba(59, 130, 246, 0.6)'
|
||||
};
|
||||
return glows[rarity] || '0 0 15px rgba(156, 163, 175, 0.4)';
|
||||
};
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return (
|
||||
<div
|
||||
className="card hover:shadow-xl transition-all duration-300 cursor-pointer"
|
||||
style={{
|
||||
transition: 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-16 h-24 rounded-lg overflow-hidden" style={{
|
||||
background: `linear-gradient(135deg, ${getRarityGradient(card.rarity).replace('from-', '').replace('to-', '')})`,
|
||||
boxShadow: `0 4px 8px rgba(0, 0, 0, 0.3)`
|
||||
}}>
|
||||
<div className="w-full h-full flex items-center justify-center text-white font-bold text-xs">
|
||||
{card.game}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||
{card.name}
|
||||
</h3>
|
||||
<p className="text-sm mb-2" style={{ color: 'var(--text-secondary)' }}>
|
||||
{card.set_name} • {getRarityLabel(card.rarity)}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
{card.oracle_text || card.card_type}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-lg font-bold gradient-text-purple">
|
||||
{formatCurrency(card.current_price || 0)}
|
||||
</p>
|
||||
<span
|
||||
className="px-2 py-1 text-xs rounded-full text-white font-medium"
|
||||
style={{ backgroundColor: getRarityColor(card.rarity) }}
|
||||
>
|
||||
{getRarityLabel(card.rarity)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Card Container with proper ratio and rarity glow */}
|
||||
<div
|
||||
className="relative cursor-pointer transition-transform duration-300 hover:scale-105 w-full"
|
||||
style={{
|
||||
aspectRatio: '5/7',
|
||||
boxShadow: getRarityGlow(card.rarity),
|
||||
borderRadius: '12px',
|
||||
border: `2px solid ${getRarityColor(card.rarity)}20`,
|
||||
overflow: 'hidden'
|
||||
}}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
{/* Particle Effects for All Rarities */}
|
||||
{card.rarity !== 'common' && (
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden">
|
||||
{/* Get particle count and colors based on rarity */}
|
||||
{(() => {
|
||||
const rarityConfig = {
|
||||
'mythic': {
|
||||
particleCount: 16,
|
||||
sparkleCount: 10,
|
||||
particleColor: '#FFD700',
|
||||
sparkleColor: '#FFA500',
|
||||
glowColor: '#FFD700'
|
||||
},
|
||||
'enchanted': {
|
||||
particleCount: 14,
|
||||
sparkleCount: 8,
|
||||
particleColor: '#A855F7',
|
||||
sparkleColor: '#EC4899',
|
||||
glowColor: '#A855F7'
|
||||
},
|
||||
'rare': {
|
||||
particleCount: 10,
|
||||
sparkleCount: 6,
|
||||
particleColor: '#3B82F6',
|
||||
sparkleColor: '#60A5FA',
|
||||
glowColor: '#3B82F6'
|
||||
},
|
||||
'uncommon': {
|
||||
particleCount: 6,
|
||||
sparkleCount: 4,
|
||||
particleColor: '#10B981',
|
||||
sparkleColor: '#34D399',
|
||||
glowColor: '#10B981'
|
||||
}
|
||||
};
|
||||
|
||||
const config = rarityConfig[card.rarity];
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Particles - Edge framing */}
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(config.particleCount)].map((_, i) => {
|
||||
// Position particles around the card edges
|
||||
let left, top;
|
||||
const edgeIndex = i % 4; // 4 edges
|
||||
const positionOnEdge = Math.floor(i / 4);
|
||||
|
||||
if (edgeIndex === 0) {
|
||||
// Top edge
|
||||
left = `${10 + (positionOnEdge * 20)}%`;
|
||||
top = '2%';
|
||||
} else if (edgeIndex === 1) {
|
||||
// Right edge
|
||||
left = '98%';
|
||||
top = `${10 + (positionOnEdge * 20)}%`;
|
||||
} else if (edgeIndex === 2) {
|
||||
// Bottom edge
|
||||
left = `${10 + (positionOnEdge * 20)}%`;
|
||||
top = '98%';
|
||||
} else {
|
||||
// Left edge
|
||||
left = '2%';
|
||||
top = `${10 + (positionOnEdge * 20)}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-1 h-1 rounded-full"
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
backgroundColor: config.particleColor,
|
||||
animation: `edgeFloat ${4 + i * 0.3}s ease-in-out infinite`,
|
||||
animationDelay: `${i * 0.2}s`,
|
||||
opacity: 0.7,
|
||||
filter: 'blur(1px)',
|
||||
boxShadow: `0 0 8px ${config.particleColor}, 0 0 16px ${config.particleColor}, 0 0 24px ${config.particleColor}`,
|
||||
mixBlendMode: 'screen'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Sparkle Effects - Corner highlights */}
|
||||
<div className="absolute inset-0">
|
||||
{[...Array(config.sparkleCount)].map((_, i) => {
|
||||
// Position sparkles in the corners and edge centers
|
||||
let left, top;
|
||||
if (i < 2) {
|
||||
// Top corners
|
||||
left = i === 0 ? '5%' : '95%';
|
||||
top = '5%';
|
||||
} else if (i < 4) {
|
||||
// Bottom corners
|
||||
left = i === 2 ? '5%' : '95%';
|
||||
top = '95%';
|
||||
} else if (i < 6) {
|
||||
// Edge centers
|
||||
left = i === 4 ? '50%' : '50%';
|
||||
top = i === 4 ? '5%' : '95%';
|
||||
} else {
|
||||
// Side centers
|
||||
left = i === 6 ? '5%' : '95%';
|
||||
top = '50%';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`sparkle-${i}`}
|
||||
className="absolute w-0.5 h-0.5"
|
||||
style={{
|
||||
left,
|
||||
top,
|
||||
backgroundColor: config.sparkleColor,
|
||||
animation: `sparkle ${2 + i * 0.4}s ease-in-out infinite`,
|
||||
animationDelay: `${i * 0.2}s`,
|
||||
opacity: 0.9,
|
||||
filter: 'blur(0.5px)',
|
||||
boxShadow: `0 0 4px ${config.sparkleColor}, 0 0 8px ${config.sparkleColor}`,
|
||||
mixBlendMode: 'screen'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Rarity Aura - Blend with overall glow */}
|
||||
<div
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: `radial-gradient(circle at 50% 50%, ${config.glowColor}20 0%, ${config.glowColor}10 40%, transparent 70%)`,
|
||||
animation: 'aura 4s ease-in-out infinite',
|
||||
mixBlendMode: 'screen'
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
{/* Card Image */}
|
||||
<div className="w-full h-full relative">
|
||||
{card.image_url ? (
|
||||
<img
|
||||
src={card.image_url}
|
||||
alt={card.name}
|
||||
className="w-full h-full object-cover"
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
objectPosition: 'center',
|
||||
borderRadius: '10px'
|
||||
}}
|
||||
onError={(e) => {
|
||||
e.target.style.display = 'none';
|
||||
e.target.nextSibling.style.display = 'flex';
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Card Back placeholder when no image */}
|
||||
<div
|
||||
className={`w-full h-full absolute inset-0 ${!card.image_url ? 'block' : 'hidden'}`}
|
||||
style={{ borderRadius: '10px' }}
|
||||
>
|
||||
<CardBack game={card.game} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Owned Quantity Chip - Only show if owned */}
|
||||
{card.quantity > 0 && (
|
||||
<div className="absolute bottom-0 left-0">
|
||||
<div
|
||||
className="px-3 py-1 text-xs font-bold text-white shadow-lg"
|
||||
style={{
|
||||
backgroundColor: 'var(--text-accent)',
|
||||
borderRadius: '8px 8px 0 0',
|
||||
borderTop: '2px solid var(--text-accent)',
|
||||
borderLeft: '2px solid var(--text-accent)',
|
||||
borderRight: '2px solid var(--text-accent)',
|
||||
transform: 'translateY(2px)'
|
||||
}}
|
||||
>
|
||||
{card.quantity}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Favorite Button */}
|
||||
<div className="absolute top-2 right-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsFavorited(!isFavorited);
|
||||
}}
|
||||
className={`p-2 rounded-full transition-all duration-200 shadow-lg ${
|
||||
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>
|
||||
|
||||
{/* Hover Details Panel */}
|
||||
{isHovered && (
|
||||
<div
|
||||
className="fixed z-50 bg-black bg-opacity-90 text-white p-4 rounded-lg shadow-2xl max-w-sm"
|
||||
style={{
|
||||
left: cardPosition.x + cardPosition.width,
|
||||
top: cardPosition.y,
|
||||
pointerEvents: 'none',
|
||||
minHeight: cardPosition.height,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Card Title */}
|
||||
<div>
|
||||
<div className="text-sm font-bold text-white mb-1">{card.name}</div>
|
||||
</div>
|
||||
|
||||
{/* Top Section - TCG, Set, Type, HP in 2x2 grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">TCG</div>
|
||||
<div className="text-xs text-white">{card.game}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Set</div>
|
||||
<div className="text-xs text-white">{card.set_name}</div>
|
||||
</div>
|
||||
{card.type && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Type</div>
|
||||
<div className="text-xs text-white flex items-center gap-1">
|
||||
{card.game === 'Pokemon' && card.type === 'Lightning' && (
|
||||
<span className="text-yellow-400">⚡</span>
|
||||
)}
|
||||
{card.type}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{card.hp && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">HP</div>
|
||||
<div className="text-xs text-white">{card.hp}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Card Type and Form */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Card type</div>
|
||||
<div className="text-xs text-white">{card.card_type || 'Card'}</div>
|
||||
</div>
|
||||
{card.form && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Form</div>
|
||||
<div className="text-xs text-white">{card.form}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cost to Play */}
|
||||
{card.mana_cost && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Cost to Play</div>
|
||||
<ManaCost cost={card.mana_cost} size="sm" useSVG={manaSymbolSettings.useSVG} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Card Rule Section */}
|
||||
{card.game === 'Pokemon' && card.form && card.form.includes('V') && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Card rule</div>
|
||||
<div className="text-xs text-white leading-relaxed">
|
||||
V rule: When your Pokémon V is Knocked Out, your opponent takes 2 Prize cards.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Abilities/Attacks/Moves Section */}
|
||||
{card.oracle_text && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Card Text</div>
|
||||
<div className="text-xs text-white leading-relaxed">{card.oracle_text}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quote Section */}
|
||||
{card.flavor_text && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Quote</div>
|
||||
<div className="text-xs text-white italic leading-relaxed">"{card.flavor_text}"</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Price */}
|
||||
{card.current_price && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Current Price</div>
|
||||
<div className="text-xs text-white">{formatCurrency(card.current_price)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Section - Weaknesses and Retreat */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{card.weakness && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Weaknesses</div>
|
||||
<div className="text-xs text-white flex items-center gap-1">
|
||||
{card.game === 'Pokemon' && (
|
||||
<span className="text-red-500">👊</span>
|
||||
)}
|
||||
{card.weakness}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{card.retreat_cost && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Retreat</div>
|
||||
<div className="text-xs text-white flex items-center gap-1">
|
||||
{card.game === 'Pokemon' && (
|
||||
<>
|
||||
<span className="text-gray-400">⭐</span>
|
||||
<span className="text-gray-400">⭐</span>
|
||||
</>
|
||||
)}
|
||||
{card.retreat_cost}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Decks and Collections */}
|
||||
{(card.decks && card.decks.length > 0) || (card.collections && card.collections.length > 0) && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Included In</div>
|
||||
<div className="text-xs text-white">
|
||||
{card.decks && card.decks.length > 0 && (
|
||||
<div className="mb-1">
|
||||
<span className="font-semibold">Decks:</span> {card.decks.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
{card.collections && card.collections.length > 0 && (
|
||||
<div>
|
||||
<span className="font-semibold">{VOCAB.LISTS}:</span> {card.collections.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Card Back Component for placeholders
|
||||
function CardBack({ game }) {
|
||||
const getCardBackImage = () => {
|
||||
switch (game) {
|
||||
case 'MTG':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjOEI0NTEzIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNBMDUyMkQiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiM0QTkwRTIiIHRleHQtYW5jaG9yPSJtaWRkbGUiPk1BR0lDPC90ZXh0Pgo8dGV4dCB4PSIxMDAiIHk9IjQ1IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5UaGUgR2F0aGVyaW5nPC90ZXh0Pgo8Y2lyY2xlIGN4PSI3MCIgY3k9IjcwIiByPSI0IiBmaWxsPSJ3aGl0ZSIvPgo8Y2lyY2xlIGN4PSIxMzAiIGN4PSI3MCIgcj0iNCIgZmlsbD0iI0Y1OTlFMEIiLz4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iODAiIHI9IjQiIGZpbGw9IiM0QTkwRTIiLz4KPGNpcmNsZSBjeD0iNzAiIGN5PSI5MCIgcj0iNCIgZmlsbD0iIzEwQjk4MSIvPgo8Y2lyY2xlIGN4PSIxMzAiIGN5PSI5MCIgcj0iNCIgZmlsbD0iYmxhY2siLz4KPHRleHQgeD0iMTAwIiB5PSIxMjAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiMyMEIyQUEiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkRFQ0tNQVNURVI8L3RleHQ+Cjwvc3ZnPgo=';
|
||||
case 'Pokemon':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjRkY2QjZCIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNGRjhFOEUiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGRkQ3MDAiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlBPS0VNT048L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iNDUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRyYWRpbmcgQ2FyZCBHYW1lPC90ZXh0Pgo8Y2lyY2xlIGN4PSIxMDAiIGN5PSI3MCIgcj0iMjAiIGZpbGw9IndoaXRlIi8+Cjx0ZXh0IHg9IjEwMCIgeT0iNzgiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZmlsbD0iIzFGN0Y3RiIgdGV4dC1hbmNob3I9Im1pZGRsZSI+4pePPC90ZXh0Pgo8dGV4dCB4PSIxMDAiIHk9IjEyMCIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEwIiBmb250LXdlaWdodD0iYm9sZCIgZmlsbD0iI0ZGRDcwMCIgdGV4dC1hbmNob3I9Im1pZGRsZSI+R0FNRSBGUkVBSzwvdGV4dD4KPC9zdmc+Cg==';
|
||||
case 'Lorcana':
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjRUM0ODk5Ii8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiNGNDcyQjYiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IiNGQkJGMjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiPkRJU05FWTwvdGV4dD4KPHRleHQgeD0iMTAwIiB5PSI0NSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBmaWxsPSJ3aGl0ZSIgdGV4dC1hbmNob3I9Im1pZGRsZSI+TG9yY2FuYTwvdGV4dD4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iNzAiIHI9IjIwIiBmaWxsPSJ3aGl0ZSIvPgo8dGV4dCB4PSIxMDAiIHk9Ijc4IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIGZpbGw9IiMxRjdGN0YiIHRleHQtYW5jaG9yPSJtaWRkbGUiPvCfkqQ8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iMTIwIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjRjU5RTBCIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5SQVZFTlNERVJHPzwvdGV4dD4KPC9zdmc+Cg==';
|
||||
default:
|
||||
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE0MyIgdmlld0JveD0iMCAwIDIwMCAxNDMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTQzIiBmaWxsPSIjNkI3MjgwIi8+CjxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSIxMjMiIGZpbGw9IiM5Q0EzQUYiIHJ4PSI4Ii8+Cjx0ZXh0IHg9IjEwMCIgeT0iMzAiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IndoaXRlIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5UQ0c8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iNDUiIGZvbnQtZmFtaWx5PSJBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgZmlsbD0id2hpdGUiIHRleHQtYW5jaG9yPSJtaWRkbGUiPlRyYWRpbmcgQ2FyZDwvdGV4dD4KPGNpcmNsZSBjeD0iMTAwIiBjeT0iNzAiIHI9IjIwIiBmaWxsPSJ3aGl0ZSIvPgo8dGV4dCB4PSIxMDAiIHk9Ijc4IiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjQiIGZpbGw9IiMxRjdGN0YiIHRleHQtYW5jaG9yPSJtaWRkbGUiPvCfkqQ8L3RleHQ+Cjx0ZXh0IHg9IjEwMCIgeT0iMTIwIiBmb250LWZhbWlseT0iQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTAiIGZvbnQtd2VpZ2h0PSJib2xkIiBmaWxsPSIjNkI3MjgwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIj5DQVJEPC90ZXh0Pgo8L3N2Zz4K';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full rounded-lg overflow-hidden">
|
||||
<img
|
||||
src={getCardBackImage()}
|
||||
alt={`${game} card back`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue