/* 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 (
{card.game}

{card.name}

{card.set_name} • {getRarityLabel(card.rarity)}

{card.oracle_text || card.card_type}

{formatCurrency(card.current_price || 0)}

{getRarityLabel(card.rarity)}
); } return (
{/* Card Container with proper ratio and rarity glow */}
{/* Particle Effects for All Rarities */} {card.rarity !== 'common' && (
{/* 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 */}
{[...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 (
); })}
{/* Sparkle Effects - Corner highlights */}
{[...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 (
); })}
{/* Rarity Aura - Blend with overall glow */}
); })()}
)} {/* Card Image */}
{card.image_url ? ( {card.name} { e.target.style.display = 'none'; e.target.nextSibling.style.display = 'flex'; }} /> ) : null} {/* Card Back placeholder when no image */}
{/* Owned Quantity Chip - Only show if owned */} {card.quantity > 0 && (
{card.quantity}
)} {/* Favorite Button */}
{/* Hover Details Panel */} {isHovered && (
{/* Card Title */}
{card.name}
{/* Top Section - TCG, Set, Type, HP in 2x2 grid */}
TCG
{card.game}
Set
{card.set_name}
{card.type && (
Type
{card.game === 'Pokemon' && card.type === 'Lightning' && ( )} {card.type}
)} {card.hp && (
HP
{card.hp}
)}
{/* Card Type and Form */}
Card type
{card.card_type || 'Card'}
{card.form && (
Form
{card.form}
)}
{/* Cost to Play */} {card.mana_cost && (
Cost to Play
)} {/* Card Rule Section */} {card.game === 'Pokemon' && card.form && card.form.includes('V') && (
Card rule
V rule: When your Pokémon V is Knocked Out, your opponent takes 2 Prize cards.
)} {/* Abilities/Attacks/Moves Section */} {card.oracle_text && (
Card Text
{card.oracle_text}
)} {/* Quote Section */} {card.flavor_text && (
Quote
"{card.flavor_text}"
)} {/* Current Price */} {card.current_price && (
Current Price
{formatCurrency(card.current_price)}
)} {/* Bottom Section - Weaknesses and Retreat */}
{card.weakness && (
Weaknesses
{card.game === 'Pokemon' && ( 👊 )} {card.weakness}
)} {card.retreat_cost && (
Retreat
{card.game === 'Pokemon' && ( <> )} {card.retreat_cost}
)}
{/* Decks and Collections */} {(card.decks && card.decks.length > 0) || (card.collections && card.collections.length > 0) && (
Included In
{card.decks && card.decks.length > 0 && (
Decks: {card.decks.join(', ')}
)} {card.collections && card.collections.length > 0 && (
{VOCAB.LISTS}: {card.collections.join(', ')}
)}
)}
)}
); } // 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 (
{`${game}
); }