2025-07-21 14:53:06 -04:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
|
import { cardService } from '../services/api';
|
|
|
|
|
import CardImageDisplay from '../components/CardImageDisplay';
|
|
|
|
|
import GlowingCard from '../components/GlowingCard';
|
|
|
|
|
|
|
|
|
|
interface Card {
|
|
|
|
|
id: number;
|
|
|
|
|
name: string;
|
|
|
|
|
set_name: string;
|
|
|
|
|
set_code: string;
|
|
|
|
|
card_number: string;
|
|
|
|
|
rarity: string;
|
|
|
|
|
game: string;
|
|
|
|
|
mana_cost?: string;
|
|
|
|
|
cmc?: number;
|
|
|
|
|
card_type: string;
|
|
|
|
|
colors?: string[];
|
|
|
|
|
oracle_text?: string;
|
|
|
|
|
power?: string;
|
|
|
|
|
toughness?: string;
|
|
|
|
|
current_price?: number;
|
|
|
|
|
market_price?: number;
|
|
|
|
|
verified: boolean;
|
|
|
|
|
stock_image_url?: string;
|
|
|
|
|
image_url?: string;
|
|
|
|
|
artwork_crop_coords?: {
|
|
|
|
|
x: number;
|
|
|
|
|
y: number;
|
|
|
|
|
width: number;
|
|
|
|
|
height: number;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const Cards: React.FC = () => {
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
const [selectedGame, setSelectedGame] = useState('');
|
|
|
|
|
const [selectedRarity, setSelectedRarity] = useState('');
|
|
|
|
|
const [viewMode, setViewMode] = useState<'card' | 'table'>('card');
|
|
|
|
|
|
|
|
|
|
const { data: cards = [], isLoading, error } = useQuery({
|
|
|
|
|
queryKey: ['cards', selectedGame, searchTerm],
|
|
|
|
|
queryFn: () => cardService.getAllCards({
|
|
|
|
|
game: selectedGame || undefined,
|
|
|
|
|
search: searchTerm || undefined
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Filter cards based on rarity (client-side for now)
|
|
|
|
|
const filteredCards = cards.filter((card: Card) => {
|
|
|
|
|
if (selectedRarity && card.rarity !== selectedRarity) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getGameBadgeColor = (game: string) => {
|
|
|
|
|
switch (game) {
|
|
|
|
|
case 'MTG': return 'bg-orange-100 text-orange-800';
|
|
|
|
|
case 'POKEMON': return 'bg-yellow-100 text-yellow-800';
|
|
|
|
|
case 'LORCANA': return 'bg-purple-100 text-purple-800';
|
|
|
|
|
default: return 'bg-gray-100 text-gray-800';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRarityBadgeColor = (rarity: string) => {
|
|
|
|
|
switch (rarity.toLowerCase()) {
|
|
|
|
|
case 'common': return 'bg-gray-100 text-gray-800';
|
|
|
|
|
case 'uncommon': return 'bg-green-100 text-green-800';
|
|
|
|
|
case 'rare': return 'bg-blue-100 text-blue-800';
|
|
|
|
|
case 'super rare': return 'bg-purple-100 text-purple-800';
|
|
|
|
|
case 'legendary': return 'bg-yellow-100 text-yellow-800';
|
|
|
|
|
case 'mythic': return 'bg-red-100 text-red-800';
|
|
|
|
|
default: return 'bg-gray-100 text-gray-800';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRarityColor = (rarity: string) => {
|
|
|
|
|
switch (rarity.toLowerCase()) {
|
|
|
|
|
case 'common': return '#6B7280';
|
|
|
|
|
case 'uncommon': return '#22C55E';
|
|
|
|
|
case 'rare': return '#3B82F6';
|
|
|
|
|
case 'super rare': return '#9333EA';
|
|
|
|
|
case 'legendary': return '#F59E0B';
|
|
|
|
|
case 'mythic': return '#EF4444';
|
|
|
|
|
default: return '#6B7280';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex justify-between items-center mb-8">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-bold text-gray-900">Cards Database</h1>
|
|
|
|
|
<p className="text-gray-600 mt-2">
|
|
|
|
|
Browse and search all trading cards - {filteredCards.length} cards found
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
{/* View Toggle */}
|
|
|
|
|
<div className="bg-gray-100 rounded-lg p-1 flex">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setViewMode('card')}
|
|
|
|
|
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
|
|
|
viewMode === 'card'
|
|
|
|
|
? 'bg-white text-gray-900 shadow-sm'
|
|
|
|
|
: 'text-gray-600 hover:text-gray-900'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
🃏 Cards
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setViewMode('table')}
|
|
|
|
|
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
|
|
|
|
viewMode === 'table'
|
|
|
|
|
? 'bg-white text-gray-900 shadow-sm'
|
|
|
|
|
: 'text-gray-600 hover:text-gray-900'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
📊 Table
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-07-21 22:06:38 -04:00
|
|
|
<button className="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md font-medium transition-colors">
|
2025-07-21 14:53:06 -04:00
|
|
|
Add Card
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search and Filter Bar */}
|
|
|
|
|
<div className="bg-white rounded-lg shadow border border-gray-200 p-6 mb-6">
|
|
|
|
|
<div className="flex flex-col md:flex-row gap-4">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Search cards..."
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
2025-07-21 22:06:38 -04:00
|
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
2025-07-21 14:53:06 -04:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<select
|
|
|
|
|
value={selectedGame}
|
|
|
|
|
onChange={(e) => setSelectedGame(e.target.value)}
|
2025-07-21 22:06:38 -04:00
|
|
|
className="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
2025-07-21 14:53:06 -04:00
|
|
|
>
|
|
|
|
|
<option value="">All Games</option>
|
|
|
|
|
<option value="MTG">Magic: The Gathering</option>
|
|
|
|
|
<option value="POKEMON">Pokémon</option>
|
|
|
|
|
<option value="LORCANA">Disney Lorcana</option>
|
|
|
|
|
</select>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedRarity}
|
|
|
|
|
onChange={(e) => setSelectedRarity(e.target.value)}
|
2025-07-21 22:06:38 -04:00
|
|
|
className="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
2025-07-21 14:53:06 -04:00
|
|
|
>
|
|
|
|
|
<option value="">All Rarities</option>
|
|
|
|
|
<option value="Common">Common</option>
|
|
|
|
|
<option value="Uncommon">Uncommon</option>
|
|
|
|
|
<option value="Rare">Rare</option>
|
|
|
|
|
<option value="Super Rare">Super Rare</option>
|
|
|
|
|
<option value="Legendary">Legendary</option>
|
|
|
|
|
<option value="Mythic">Mythic</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Loading State */}
|
|
|
|
|
{isLoading && (
|
|
|
|
|
<div className="bg-white rounded-lg shadow border border-gray-200 p-8 text-center">
|
2025-07-21 22:06:38 -04:00
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto mb-4"></div>
|
2025-07-21 14:53:06 -04:00
|
|
|
<p className="text-gray-600">Loading cards...</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Error State */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
|
|
|
|
<p className="text-red-800">Error loading cards. Please try again.</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Cards Grid */}
|
|
|
|
|
{!isLoading && !error && (
|
|
|
|
|
<>
|
|
|
|
|
{filteredCards.length === 0 ? (
|
|
|
|
|
<div className="bg-white rounded-lg shadow border border-gray-200 p-8">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<span className="text-6xl mb-4 block">🃏</span>
|
|
|
|
|
<h3 className="text-xl font-semibold text-gray-900 mb-2">
|
|
|
|
|
No cards found
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-gray-600 mb-6">
|
|
|
|
|
Try adjusting your search filters or add some cards to your collection
|
|
|
|
|
</p>
|
2025-07-21 22:06:38 -04:00
|
|
|
<button className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-3 rounded-md font-medium transition-colors mr-4">
|
2025-07-21 14:53:06 -04:00
|
|
|
Scan Cards
|
|
|
|
|
</button>
|
|
|
|
|
<button className="bg-gray-100 hover:bg-gray-200 text-gray-800 px-6 py-3 rounded-md font-medium transition-colors">
|
|
|
|
|
Add Manually
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : viewMode === 'card' ? (
|
|
|
|
|
/* Card View */
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
|
|
|
|
{filteredCards.map((card: Card) => (
|
|
|
|
|
<GlowingCard key={card.id} rarity={card.rarity} className="bg-white rounded-lg shadow overflow-hidden hover:shadow-lg">
|
|
|
|
|
{/* Card Image */}
|
|
|
|
|
<div className="flex justify-center p-4 bg-gray-50">
|
|
|
|
|
<CardImageDisplay
|
|
|
|
|
card={card}
|
|
|
|
|
size="large"
|
|
|
|
|
className="mx-auto"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Card Info */}
|
|
|
|
|
<div className="p-4">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex justify-between items-start mb-3">
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<h3 className="font-semibold text-gray-900 truncate" title={card.name}>
|
|
|
|
|
{card.name}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-gray-600">{card.set_name}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getGameBadgeColor(card.game)}`}>
|
|
|
|
|
{card.game}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Quick Stats */}
|
|
|
|
|
<div className="space-y-1 mb-3">
|
|
|
|
|
{card.card_type && (
|
|
|
|
|
<p className="text-xs text-gray-600 truncate" title={card.card_type}>
|
|
|
|
|
{card.card_type}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
{card.mana_cost && (
|
|
|
|
|
<span className="text-xs text-gray-600">
|
|
|
|
|
Cost: {card.mana_cost}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(card.power && card.toughness) && (
|
|
|
|
|
<span className="text-xs text-gray-600">
|
|
|
|
|
{card.power}/{card.toughness}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getRarityBadgeColor(card.rarity)}`}>
|
|
|
|
|
{card.rarity}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{card.current_price && (
|
|
|
|
|
<span className="text-sm font-medium text-green-600">
|
|
|
|
|
${card.current_price.toFixed(2)}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</GlowingCard>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
/* Table View */
|
|
|
|
|
<div className="bg-white rounded-lg shadow border border-gray-200 overflow-hidden">
|
|
|
|
|
<div className="overflow-x-auto">
|
|
|
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
|
|
|
<thead className="bg-gray-50">
|
|
|
|
|
<tr>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
Card
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
Type
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
Cost
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
P/T
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
Rarity
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
Price
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
|
Actions
|
|
|
|
|
</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
|
|
|
{filteredCards.map((card: Card) => (
|
|
|
|
|
<tr key={card.id} className="hover:bg-gray-50 transition-colors border-l-4" style={{borderLeftColor: getRarityColor(card.rarity)}}>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<CardImageDisplay
|
|
|
|
|
card={card}
|
|
|
|
|
size="small"
|
|
|
|
|
className="mr-3"
|
|
|
|
|
/>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="text-sm font-medium text-gray-900">
|
|
|
|
|
{card.name}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm text-gray-500">
|
|
|
|
|
{card.set_name}
|
|
|
|
|
</div>
|
|
|
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getGameBadgeColor(card.game)}`}>
|
|
|
|
|
{card.game}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
|
|
{card.card_type}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
|
|
{card.mana_cost || '—'}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
|
|
|
{(card.power && card.toughness) ? `${card.power}/${card.toughness}` : '—'}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
|
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getRarityBadgeColor(card.rarity)}`}>
|
|
|
|
|
{card.rarity}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-green-600">
|
|
|
|
|
{card.current_price ? `$${card.current_price.toFixed(2)}` : '—'}
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
2025-07-21 22:06:38 -04:00
|
|
|
<button className="text-indigo-600 hover:text-indigo-900">
|
2025-07-21 14:53:06 -04:00
|
|
|
View
|
|
|
|
|
</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default Cards;
|