From 33e03d8f47c512d045b0a30df110210ceb44fd8c Mon Sep 17 00:00:00 2001 From: varutasu <104105839+varutasu@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:57:49 -0500 Subject: [PATCH] Extract Card3D and CardBack from pages/cards.js (Brief 1). (#77) 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 --- components/Card3D.js | 530 ++++++++++++++++++++++++++++++++++++++++++ pages/cards.js | 531 ------------------------------------------- 2 files changed, 530 insertions(+), 531 deletions(-) create mode 100644 components/Card3D.js diff --git a/components/Card3D.js b/components/Card3D.js new file mode 100644 index 0000000..47fe808 --- /dev/null +++ b/components/Card3D.js @@ -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 ( +
+
+
+
+ {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} +
+ ); +} diff --git a/pages/cards.js b/pages/cards.js index ee5e878..57aa942 100644 --- a/pages/cards.js +++ b/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() { ); } - -// 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 ( -
-
-
-
- {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} -
- ); -} \ No newline at end of file