deckhearth/pages/cards.js
Randall Stillwell afb79c57d9 Major Scanner Improvements
🔧 Gemini AI Integration:
- Added Google Gemini API as default OCR service
- Auto-configures from GEMINI_AI_API_KEY environment variable
- Fixed Puter.js authentication issues
- Enhanced OCR settings with connection testing

🎨 Redesigned Scanner Queue:
- New thumbnail + content layout with checkbox overlay
- Smart quantity management (duplicates increment quantity)
- Complete card information display from database
- Two-row action layout (primary/secondary actions)
- Floating bottom toolbar for bulk actions
- Real card images from database

�� Enhanced User Experience:
- Fixed Canvas2D performance warnings
- Better error handling and fallbacks
- Improved responsive design
- Database confirmation indicators
- Professional card scanning workflow

📱 Mobile Ready:
- Optimized layouts for mobile scanning
- Touch-friendly controls and interactions
- Improved visual feedback and status indicators
2025-07-29 14:19:48 -05:00

1499 lines
No EOL
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef } from 'react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import Layout from '../components/Layout';
import CardItem from '../components/CardItem';
import BulkSelectionToolbar from '../components/BulkSelectionToolbar';
import CollectionSelectionModal from '../components/CollectionSelectionModal';
import ProtectedRoute from '../components/ProtectedRoute';
import { ManaCost, ColorFilterSymbol } from '../components/ManaSymbols';
import ManaSymbolSettings from '../components/ManaSymbolSettings';
import { useAuth } from '../lib/use-auth';
// Public fallback component for non-authenticated users
function PublicCardsView() {
return (
<Layout user={null}>
<div className="p-6">
<div className="max-w-7xl mx-auto">
{/* Header with login prompt */}
<div className="mb-8 text-center">
<h1 className="text-4xl font-bold mb-4 gradient-text-flame">Browse Trading Cards</h1>
<p className="text-xl mb-6" style={{ color: 'var(--text-secondary)' }}>
Discover thousands of trading cards from popular TCGs
</p>
<div className="bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 rounded-2xl p-6 border border-orange-200 dark:border-orange-800">
<h3 className="text-lg font-semibold mb-2 gradient-text-ember">Sign Up to Unlock Full Features</h3>
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
Create collections, mark favorites, add cards to your inventory, and more!
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link
href="/signup"
className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90"
style={{
backgroundColor: 'var(--accent-ember)',
color: 'white'
}}
>
Create Free Account
</Link>
<Link
href="/login"
className="px-6 py-3 font-medium rounded-xl border-2 transition-all duration-200 hover:opacity-80"
style={{
color: 'var(--text-primary)',
borderColor: 'var(--accent-ember)'
}}
>
Sign In
</Link>
</div>
</div>
</div>
{/* Basic card browser would go here - simplified version */}
<div className="text-center py-20">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--accent-ember)' }}>
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<h3 className="text-xl font-semibold mb-2 gradient-text-flame">Full Card Browser Coming Soon</h3>
<p style={{ color: 'var(--text-secondary)' }}>
Sign up now to get early access to our complete card database and collection tools.
</p>
</div>
</div>
</div>
</Layout>
);
}
function AuthenticatedCards() {
const router = useRouter();
const { user, loading: authLoading } = useAuth();
const [cards, setCards] = useState([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedTCG, setSelectedTCG] = useState('all');
const [selectedRarity, setSelectedRarity] = useState('all');
const [selectedSet, setSelectedSet] = useState('all');
const [selectedValueRange, setSelectedValueRange] = useState('all');
const [viewMode, setViewMode] = useState('grid'); // grid or list
const [pagination, setPagination] = useState({
page: 1,
limit: 50,
total: 0,
pages: 0
});
const [filters, setFilters] = useState({
games: [],
rarities: [],
sets: []
});
const [hasMore, setHasMore] = useState(true);
const loadingMoreRef = useRef(false);
const hasMoreRef = useRef(true);
// Bulk selection state
const [selectedCards, setSelectedCards] = useState([]);
const [favoritedCards, setFavoritedCards] = useState(new Set());
// Modal states
const [showCollectionModal, setShowCollectionModal] = useState(false);
const [cardsToAdd, setCardsToAdd] = useState([]);
// Mana symbol settings
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
// Fetch cards from database
const fetchCards = async (isLoadMore = false) => {
try {
if (isLoadMore) {
setLoadingMore(true);
loadingMoreRef.current = true;
} else {
setLoading(true);
}
const params = new URLSearchParams({
query: searchQuery,
game: selectedTCG,
rarity: selectedRarity,
set: selectedSet,
minPrice: selectedValueRange === 'under-50' ? '0' :
selectedValueRange === '50-100' ? '50' :
selectedValueRange === '100-500' ? '100' :
selectedValueRange === '500-1000' ? '500' :
selectedValueRange === 'over-1000' ? '1000' : '',
maxPrice: selectedValueRange === '50-100' ? '100' :
selectedValueRange === '100-500' ? '500' :
selectedValueRange === '500-1000' ? '1000' : '',
page: pagination.page.toString(),
limit: pagination.limit.toString()
});
const response = await fetch(`/api/cards/search?${params}`);
const data = await response.json();
if (data.success) {
if (isLoadMore) {
setCards(prevCards => [...prevCards, ...data.cards]);
} else {
setCards(data.cards);
}
setPagination(data.pagination);
setFilters(data.filters);
const hasMoreCards = data.pagination.page < data.pagination.pages;
setHasMore(hasMoreCards);
hasMoreRef.current = hasMoreCards;
} else {
console.error('Failed to fetch cards:', data.error);
if (!isLoadMore) {
setCards([]);
}
}
} catch (error) {
console.error('Error fetching cards:', error);
if (!isLoadMore) {
setCards([]);
}
} finally {
setLoading(false);
setLoadingMore(false);
loadingMoreRef.current = false;
}
};
// Load user's favorited cards
const loadFavoritedCards = async () => {
try {
const token = localStorage.getItem('auth_token');
if (!token) return;
const response = await fetch('/api/favorites?type=card', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
const favoriteIds = new Set(data.favorites.map(fav => parseInt(fav.item_id)));
setFavoritedCards(favoriteIds);
}
} catch (error) {
console.error('Error loading favorited cards:', error);
}
};
// Initial load
useEffect(() => {
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
loadFavoritedCards(); // Load user's favorites
}, [selectedTCG, selectedRarity, selectedSet, selectedValueRange]);
// Handle search with debounce
const [searchTimeout, setSearchTimeout] = useState(null);
const handleSearchChange = (value) => {
setSearchQuery(value);
// Clear existing timeout
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Set new timeout for search
const newTimeout = setTimeout(() => {
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
}, 500); // 500ms delay
setSearchTimeout(newTimeout);
};
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
};
}, [searchTimeout]);
// Load more cards
const loadMoreCards = async () => {
if (!loadingMoreRef.current && hasMoreRef.current) {
const nextPage = pagination.page + 1;
setPagination(prev => ({ ...prev, page: nextPage }));
// Use the next page number directly in the fetch
try {
setLoadingMore(true);
loadingMoreRef.current = true;
const params = new URLSearchParams({
query: searchQuery,
game: selectedTCG,
rarity: selectedRarity,
set: selectedSet,
minPrice: selectedValueRange === 'under-50' ? '0' :
selectedValueRange === '50-100' ? '50' :
selectedValueRange === '100-500' ? '100' :
selectedValueRange === '500-1000' ? '500' :
selectedValueRange === 'over-1000' ? '1000' : '',
maxPrice: selectedValueRange === '50-100' ? '100' :
selectedValueRange === '100-500' ? '500' :
selectedValueRange === '500-1000' ? '1000' : '',
page: nextPage.toString(),
limit: pagination.limit.toString()
});
const response = await fetch(`/api/cards/search?${params}`);
const data = await response.json();
if (data.success) {
setCards(prevCards => [...prevCards, ...data.cards]);
setPagination(data.pagination);
setFilters(data.filters);
const hasMoreCards = data.pagination.page < data.pagination.pages;
setHasMore(hasMoreCards);
hasMoreRef.current = hasMoreCards;
} else {
console.error('Failed to fetch more cards:', data.error);
}
} catch (error) {
console.error('Error fetching more cards:', error);
} finally {
setLoadingMore(false);
loadingMoreRef.current = false;
}
}
};
// Update refs when state changes
useEffect(() => {
hasMoreRef.current = hasMore;
loadingMoreRef.current = loadingMore;
}, [hasMore, loadingMore]);
// Intersection Observer for infinite scroll
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && hasMoreRef.current && !loadingMoreRef.current) {
loadMoreCards();
}
});
},
{
rootMargin: '50px',
threshold: 0.1
}
);
const setupObserver = () => {
const sentinel = document.getElementById('infinite-scroll-sentinel');
if (sentinel) {
observer.observe(sentinel);
} else {
setTimeout(setupObserver, 100);
}
};
setupObserver();
return () => {
const sentinel = document.getElementById('infinite-scroll-sentinel');
if (sentinel) {
observer.unobserve(sentinel);
}
};
}, []);
const getRarityColor = (rarity) => {
// Map database rarity values to colors
const rarityColorMap = {
'common': '#6B7280',
'uncommon': '#10B981',
'rare': '#F59E0B',
'mythic': '#FFD700',
'holographic': '#FF6B6B',
'enchanted': '#A855F7',
'secret rare': '#FF6B6B',
'ultra rare': '#A855F7'
};
return rarityColorMap[rarity?.toLowerCase()] || '#6B7280';
};
const tcgOptions = [
{ value: 'MTG', label: 'Magic: The Gathering', color: 'purple', icon: '🔮' },
{ value: 'Pokemon', label: 'Pokemon', color: 'blue', icon: '⚡' },
{ value: 'Lorcana', label: 'Disney Lorcana', color: 'pink', icon: '✨' }
];
const rarityOptions = [
{ value: 'all', label: 'All Rarities' },
{ value: 'common', label: 'Common' },
{ value: 'uncommon', label: 'Uncommon' },
{ value: 'rare', label: 'Rare' },
{ value: 'mythic', label: 'Mythic' },
{ value: 'enchanted', label: 'Enchanted' },
...filters.rarities.filter(rarity =>
!['all', 'common', 'uncommon', 'rare', 'mythic', 'enchanted'].includes(rarity)
).map(rarity => ({
value: rarity,
label: rarity,
color: getRarityColor(rarity)
}))
];
const setOptions = [
{ value: 'all', label: 'All Sets' },
...filters.sets.map(set => ({
value: set,
label: set
}))
];
const valueRangeOptions = [
{ value: 'all', label: 'All Values' },
{ value: 'under-50', label: 'Under $50' },
{ value: '50-100', label: '$50 - $100' },
{ value: '100-500', label: '$100 - $500' },
{ value: '500-1000', label: '$500 - $1,000' },
{ value: 'over-1000', label: 'Over $1,000' }
];
const formatCurrency = (amount) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
const getRarityLabel = (rarity) => {
const rarityOption = rarityOptions.find(option => option.value === rarity);
return rarityOption ? rarityOption.label : rarity;
};
// Bulk selection handlers
const handleToggleSelect = (card) => {
setSelectedCards(prev => {
const isSelected = prev.some(c => c.id === card.id);
if (isSelected) {
return prev.filter(c => c.id !== card.id);
} else {
return [...prev, card];
}
});
};
const handleClearSelection = () => {
setSelectedCards([]);
};
const handleToggleFavorite = async (card) => {
try {
const isFavorited = favoritedCards.has(card.id);
const method = isFavorited ? 'DELETE' : 'POST';
const response = await fetch('/api/favorites', {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
itemType: 'card',
itemId: card.id
})
});
if (response.ok) {
setFavoritedCards(prev => {
const newSet = new Set(prev);
if (isFavorited) {
newSet.delete(card.id);
} else {
newSet.add(card.id);
}
return newSet;
});
}
} catch (error) {
console.error('Error toggling favorite:', error);
}
};
// Bulk action handlers
const handleBulkAddToCollection = (cards) => {
setCardsToAdd(cards);
setShowCollectionModal(true);
};
const handleBulkAddToDeck = (cards) => {
console.log('Adding to deck:', cards);
alert(`Adding ${cards.length} cards to deck (functionality coming soon)`);
};
const handleBulkMarkAsOwned = async (cards) => {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
alert('Please log in to mark cards as owned');
return;
}
let successCount = 0;
for (const card of cards) {
try {
const response = await fetch(`/api/cards/${card.id}/ownership`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ quantity: 1 })
});
if (response.ok) {
successCount++;
}
} catch (error) {
console.error(`Error marking card ${card.id} as owned:`, error);
}
}
if (successCount === cards.length) {
alert(`Successfully marked ${cards.length} cards as owned!`);
} else {
alert(`Marked ${successCount} out of ${cards.length} cards as owned. Some operations may have failed.`);
}
// Clear selection after successful operation
setSelectedCards([]);
} catch (error) {
console.error('Error in bulk mark as owned:', error);
alert('Failed to mark cards as owned. Please try again.');
}
};
const handleBulkRemoveFromOwned = async (cards) => {
try {
const token = localStorage.getItem('auth_token');
if (!token) {
alert('Please log in to remove cards from owned');
return;
}
let successCount = 0;
for (const card of cards) {
try {
const response = await fetch(`/api/cards/${card.id}/ownership`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ quantity: 0 })
});
if (response.ok) {
successCount++;
}
} catch (error) {
console.error(`Error removing card ${card.id} from owned:`, error);
}
}
if (successCount === cards.length) {
alert(`Successfully removed ${cards.length} cards from owned!`);
} else {
alert(`Removed ${successCount} out of ${cards.length} cards from owned. Some operations may have failed.`);
}
// Clear selection after successful operation
setSelectedCards([]);
} catch (error) {
console.error('Error in bulk remove from owned:', error);
alert('Failed to remove cards from owned. Please try again.');
}
};
const handleBulkFavorite = async (cards) => {
try {
for (const card of cards) {
if (!favoritedCards.has(card.id)) {
await fetch('/api/favorites', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
itemType: 'card',
itemId: card.id
})
});
}
}
setFavoritedCards(prev => {
const newSet = new Set(prev);
cards.forEach(card => newSet.add(card.id));
return newSet;
});
alert(`Added ${cards.length} cards to favorites`);
} catch (error) {
console.error('Error bulk favoriting:', error);
}
};
const handleBulkDelete = (cards) => {
console.log('Bulk delete:', cards);
alert(`Bulk delete functionality coming soon for ${cards.length} cards`);
};
// Collection modal handlers
const handleAddToCollections = (results, selectedCollectionIds, cards) => {
const successCount = results.filter(r => r.success).length;
const totalAttempts = results.length;
if (successCount === totalAttempts) {
alert(`Successfully added ${cards.length} card${cards.length !== 1 ? 's' : ''} to ${selectedCollectionIds.length} collection${selectedCollectionIds.length !== 1 ? 's' : ''}!`);
} else {
alert(`Added ${successCount} out of ${totalAttempts} cards. Some additions may have failed.`);
}
// Clear selection after successful addition
setSelectedCards([]);
};
if (loading && cards.length === 0) {
return (
<Layout user={user}>
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2" style={{ borderColor: 'var(--text-accent)' }}></div>
</div>
</Layout>
);
}
return (
<Layout user={user}>
{/* Header */}
<div className="p-4 sm:p-6" style={{ backgroundColor: 'var(--bg-secondary)', borderBottom: '1px solid var(--border)' }}>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
Cards
</h1>
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
Browse and manage your card collection
</p>
</div>
<div className="flex space-x-2 sm:space-x-4">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-xl transition-all duration-200 ${
viewMode === 'grid'
? 'shadow-lg'
: 'hover:shadow-md'
}`}
style={{
backgroundColor: viewMode === 'grid' ? 'var(--bg-tertiary)' : 'transparent',
color: viewMode === 'grid' ? 'var(--text-primary)' : 'var(--text-secondary)'
}}
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
</svg>
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-xl transition-all duration-200 ${
viewMode === 'list'
? 'shadow-lg'
: 'hover:shadow-md'
}`}
style={{
backgroundColor: viewMode === 'list' ? 'var(--bg-tertiary)' : 'transparent',
color: viewMode === 'list' ? 'var(--text-primary)' : 'var(--text-secondary)'
}}
>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
</button>
</div>
</div>
</div>
{/* Quick Filters */}
<div className="p-4 sm:p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
<div className="mb-4">
<h3 className="text-base sm:text-lg font-semibold mb-3" style={{ color: 'var(--text-primary)' }}>
Quick Filters
</h3>
<div className="flex flex-wrap gap-2 sm:gap-3">
<button
onClick={() => setSelectedTCG('all')}
className={`px-4 py-2 rounded-xl font-medium transition-all duration-200 ${
selectedTCG === 'all'
? 'shadow-lg'
: 'hover:shadow-md'
}`}
style={{
backgroundColor: selectedTCG === 'all' ? 'var(--bg-tertiary)' : 'transparent',
color: selectedTCG === 'all' ? 'var(--text-primary)' : 'var(--text-secondary)',
border: '1px solid var(--border)'
}}
>
All Games
</button>
{tcgOptions.map(option => (
<button
key={option.value}
onClick={() => setSelectedTCG(option.value)}
className={`px-4 py-2 rounded-xl font-medium transition-all duration-200 flex items-center gap-2 ${
selectedTCG === option.value
? 'shadow-lg'
: 'hover:shadow-md'
}`}
style={{
backgroundColor: selectedTCG === option.value ? 'var(--bg-tertiary)' : 'transparent',
color: selectedTCG === option.value ? 'var(--text-primary)' : 'var(--text-secondary)',
border: '1px solid var(--border)'
}}
>
<span>{option.icon}</span>
{option.label}
</button>
))}
</div>
</div>
{/* Advanced Filters */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Search
</label>
<div className="relative">
<input
type="text"
placeholder="Search cards..."
className="search-bar w-full pr-12"
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
}
}}
/>
<button
onClick={() => {
setPagination(prev => ({ ...prev, page: 1 }));
setCards([]);
setHasMore(true);
hasMoreRef.current = true;
fetchCards(false);
}}
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 rounded-lg hover:bg-opacity-20 transition-all duration-200"
style={{ backgroundColor: 'var(--bg-tertiary)' }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Rarity
</label>
<select
value={selectedRarity}
onChange={(e) => setSelectedRarity(e.target.value)}
className="input-field w-full"
>
{rarityOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Set
</label>
<select
value={selectedSet}
onChange={(e) => setSelectedSet(e.target.value)}
className="input-field w-full"
>
{setOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--text-primary)' }}>
Value Range
</label>
<select
value={selectedValueRange}
onChange={(e) => setSelectedValueRange(e.target.value)}
className="input-field w-full"
>
{valueRangeOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
{/* Active Filters Display */}
{(selectedTCG !== 'all' || selectedRarity !== 'all' || selectedSet !== 'all' || selectedValueRange !== 'all') && (
<div className="mt-4">
<div className="flex flex-wrap gap-2">
{selectedTCG !== 'all' && (
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)'
}}>
{tcgOptions.find(opt => opt.value === selectedTCG)?.label}
<button
onClick={() => setSelectedTCG('all')}
className="ml-2 hover:opacity-70"
>
×
</button>
</span>
)}
{selectedRarity !== 'all' && (
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)'
}}>
{rarityOptions.find(opt => opt.value === selectedRarity)?.label}
<button
onClick={() => setSelectedRarity('all')}
className="ml-2 hover:opacity-70"
>
×
</button>
</span>
)}
{selectedSet !== 'all' && (
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)'
}}>
{setOptions.find(opt => opt.value === selectedSet)?.label}
<button
onClick={() => setSelectedSet('all')}
className="ml-2 hover:opacity-70"
>
×
</button>
</span>
)}
{selectedValueRange !== 'all' && (
<span className="px-3 py-1 rounded-full text-sm font-medium" style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
border: '1px solid var(--border)'
}}>
{valueRangeOptions.find(opt => opt.value === selectedValueRange)?.label}
<button
onClick={() => setSelectedValueRange('all')}
className="ml-2 hover:opacity-70"
>
×
</button>
</span>
)}
<button
onClick={() => {
setSelectedTCG('all');
setSelectedRarity('all');
setSelectedSet('all');
setSelectedValueRange('all');
}}
className="px-3 py-1 rounded-full text-sm font-medium hover:opacity-70" style={{
color: 'var(--text-accent)',
border: '1px solid var(--border)'
}}>
Clear All
</button>
</div>
</div>
)}
</div>
{/* Cards Display */}
<div className="p-6 pb-64">
{cards.length === 0 ? (
<div className="text-center py-12">
<div className="text-6xl mb-4">🃏</div>
<h3 className="text-xl font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>
No cards found
</h3>
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
Try adjusting your search or filters
</p>
</div>
) : (
<>
<div className={viewMode === 'grid' ? 'card-grid-container grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3 sm:gap-4 lg:gap-6 p-3 sm:p-4 lg:p-6 xl:p-8' : 'space-y-4 p-6'}>
{cards.map((card) => (
<CardItem
key={card.id}
card={card}
viewMode={viewMode}
isSelected={selectedCards.some(c => c.id === card.id)}
onToggleSelect={handleToggleSelect}
onAddToCollection={(card) => handleBulkAddToCollection([card])}
onAddToDeck={handleBulkAddToDeck}
onToggleFavorite={handleToggleFavorite}
isFavorited={favoritedCards.has(card.id)}
/>
))}
</div>
{/* Infinite Scroll Loading Indicator */}
{loadingMore && (
<div className="flex justify-center py-8">
<div className="flex items-center space-x-3">
<div className="animate-spin rounded-full h-8 w-8 border-b-2" style={{ borderColor: 'var(--text-accent)' }}></div>
<span style={{ color: 'var(--text-secondary)' }}>Loading more cards...</span>
</div>
</div>
)}
{/* Infinite Scroll Sentinel */}
<div id="infinite-scroll-sentinel" className="h-4 w-full" />
{/* Load More Button */}
{hasMore && !loadingMore && (
<div className="flex justify-center py-8">
<button
onClick={loadMoreCards}
className="px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg hover:from-blue-600 hover:to-purple-700 transition-all duration-200 shadow-lg hover:shadow-xl"
>
Load More Cards
</button>
</div>
)}
{/* End of results indicator */}
{!hasMore && cards.length > 0 && (
<div className="flex justify-center py-8">
<span style={{ color: 'var(--text-secondary)' }}>No more cards to load</span>
</div>
)}
</>
)}
</div>
{/* Bulk Selection Toolbar */}
<BulkSelectionToolbar
selectedCards={selectedCards}
onClearSelection={handleClearSelection}
onAddToCollection={handleBulkAddToCollection}
onAddToDeck={handleBulkAddToDeck}
onMarkAsOwned={handleBulkMarkAsOwned}
onRemoveFromOwned={handleBulkRemoveFromOwned}
onBulkFavorite={handleBulkFavorite}
onBulkDelete={handleBulkDelete}
/>
{/* Collection Selection Modal */}
<CollectionSelectionModal
isOpen={showCollectionModal}
onClose={() => setShowCollectionModal(false)}
cards={cardsToAdd}
onAddToCollections={handleAddToCollections}
/>
</Layout>
);
}
export default function Cards() {
return (
<ProtectedRoute
allowPublic={true}
publicFallback={<PublicCardsView />}
>
<AuthenticatedCards />
</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">Collections:</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>
);
}