2025-07-21 14:53:06 -04:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { useQuery } from '@tanstack/react-query';
|
2025-07-22 20:51:38 -04:00
|
|
|
import { tcgApi } from '../services/tcgApi';
|
|
|
|
|
import CardSearch from '../components/cards/CardSearch';
|
2025-07-22 21:00:10 -04:00
|
|
|
import CardDatabaseBrowser from '../components/cards/CardDatabaseBrowser';
|
2025-07-22 20:51:38 -04:00
|
|
|
import CardManager from '../components/cards/CardManager';
|
2025-07-21 14:53:06 -04:00
|
|
|
import CardImageDisplay from '../components/CardImageDisplay';
|
|
|
|
|
import GlowingCard from '../components/GlowingCard';
|
2025-07-22 21:00:10 -04:00
|
|
|
import type { UserCard, CardFilters, Card } from '../types';
|
2025-07-21 14:53:06 -04:00
|
|
|
|
|
|
|
|
const Cards: React.FC = () => {
|
|
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
2025-07-22 20:51:38 -04:00
|
|
|
const [filters, setFilters] = useState<CardFilters>({
|
|
|
|
|
game: '',
|
|
|
|
|
rarity: '',
|
|
|
|
|
status: 'all',
|
|
|
|
|
search: '',
|
|
|
|
|
});
|
|
|
|
|
const [viewMode, setViewMode] = useState<'card' | 'list'>('card');
|
|
|
|
|
const [showCardSearch, setShowCardSearch] = useState(false);
|
2025-07-22 21:00:10 -04:00
|
|
|
const [showDatabaseBrowser, setShowDatabaseBrowser] = useState(false);
|
2025-07-22 20:51:38 -04:00
|
|
|
const [editingCard, setEditingCard] = useState<string | null>(null);
|
2025-07-21 14:53:06 -04:00
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
const { data: userCards = [], isLoading, error } = useQuery({
|
|
|
|
|
queryKey: ['user-cards', { ...filters, search: searchTerm }],
|
|
|
|
|
queryFn: () => tcgApi.cards.getUserCards({ ...filters, search: searchTerm }),
|
2025-07-21 14:53:06 -04:00
|
|
|
});
|
|
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
// Filter cards based on current filters
|
|
|
|
|
const filteredCards = userCards.filter((userCard: UserCard) => {
|
|
|
|
|
const card = userCard.card;
|
|
|
|
|
if (!card) return false;
|
|
|
|
|
|
|
|
|
|
if (filters.rarity && card.rarity !== filters.rarity) return false;
|
|
|
|
|
if (filters.game && card.game !== filters.game) return false;
|
|
|
|
|
if (filters.status && filters.status !== 'all' && userCard.status !== filters.status) return false;
|
|
|
|
|
|
2025-07-21 14:53:06 -04:00
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const getGameBadgeColor = (game: string) => {
|
|
|
|
|
switch (game) {
|
2025-07-22 20:51:38 -04:00
|
|
|
case 'MTG': return 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300';
|
|
|
|
|
case 'POKEMON': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
|
|
|
|
|
case 'LORCANA': return 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300';
|
|
|
|
|
case 'YUGIOH': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300';
|
|
|
|
|
default: return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300';
|
2025-07-21 14:53:06 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRarityBadgeColor = (rarity: string) => {
|
|
|
|
|
switch (rarity.toLowerCase()) {
|
2025-07-22 20:51:38 -04:00
|
|
|
case 'common': return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300';
|
|
|
|
|
case 'uncommon': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
|
|
|
|
|
case 'rare': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300';
|
|
|
|
|
case 'super rare': return 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300';
|
|
|
|
|
case 'legendary': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
|
|
|
|
|
case 'mythic': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
|
|
|
|
|
default: return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300';
|
2025-07-21 14:53:06 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
const getStatusBadgeColor = (status: string) => {
|
|
|
|
|
switch (status) {
|
|
|
|
|
case 'owned': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
|
|
|
|
|
case 'wanted': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
|
|
|
|
|
default: return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300';
|
2025-07-21 14:53:06 -04:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
const getConditionBadgeColor = (condition: string) => {
|
|
|
|
|
switch (condition) {
|
|
|
|
|
case 'mint': return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-300';
|
|
|
|
|
case 'near_mint': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
|
|
|
|
|
case 'excellent': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300';
|
|
|
|
|
case 'good': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
|
|
|
|
|
case 'light_played': return 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300';
|
|
|
|
|
case 'played': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300';
|
|
|
|
|
case 'poor': return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-300';
|
|
|
|
|
default: return 'bg-surface-100 text-surface-800 dark:bg-surface-700 dark:text-surface-300';
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-07-21 14:53:06 -04:00
|
|
|
|
|
|
|
|
return (
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
2025-07-21 14:53:06 -04:00
|
|
|
<div>
|
2025-07-22 20:51:38 -04:00
|
|
|
<h1 className="text-2xl font-bold text-surface-900 dark:text-white">My Cards</h1>
|
|
|
|
|
<p className="text-surface-600 dark:text-surface-400 mt-1">
|
|
|
|
|
{filteredCards.length} cards in your collection
|
2025-07-21 14:53:06 -04:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-07-22 20:51:38 -04:00
|
|
|
|
2025-07-22 21:00:10 -04:00
|
|
|
<div className="flex space-x-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowCardSearch(true)}
|
|
|
|
|
className="bg-gradient-to-r from-primary-500 to-accent-500 hover:from-primary-600 hover:to-accent-600 text-white px-4 py-2 rounded-xl font-medium transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4 mr-2 inline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
|
|
|
</svg>
|
|
|
|
|
Add Cards
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowDatabaseBrowser(true)}
|
|
|
|
|
className="bg-gradient-to-r from-accent-500 to-primary-500 hover:from-accent-600 hover:to-primary-600 text-white px-4 py-2 rounded-xl font-medium transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4 mr-2 inline" 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>
|
|
|
|
|
Browse Database
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search and Filter Bar */}
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-4">
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* Search Bar */}
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
|
|
|
<svg className="h-5 w-5 text-surface-400" 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>
|
|
|
|
|
</div>
|
2025-07-21 14:53:06 -04:00
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
2025-07-22 20:51:38 -04:00
|
|
|
placeholder="Search your cards..."
|
|
|
|
|
className="w-full pl-10 pr-4 py-3 bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded-xl text-surface-900 dark:text-white placeholder-surface-500 dark:placeholder-surface-400 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors"
|
2025-07-21 14:53:06 -04:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-07-22 20:51:38 -04:00
|
|
|
|
|
|
|
|
{/* Filters and View Toggle */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex space-x-2 overflow-x-auto pb-2">
|
|
|
|
|
<select
|
|
|
|
|
value={filters.game}
|
|
|
|
|
onChange={(e) => setFilters(prev => ({ ...prev, game: e.target.value }))}
|
|
|
|
|
className="px-3 py-2 bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors whitespace-nowrap"
|
|
|
|
|
>
|
|
|
|
|
<option value="">All Games</option>
|
|
|
|
|
<option value="MTG">Magic: The Gathering</option>
|
|
|
|
|
<option value="POKEMON">Pokémon</option>
|
|
|
|
|
<option value="LORCANA">Disney Lorcana</option>
|
|
|
|
|
<option value="YUGIOH">Yu-Gi-Oh!</option>
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
value={filters.rarity}
|
|
|
|
|
onChange={(e) => setFilters(prev => ({ ...prev, rarity: e.target.value }))}
|
|
|
|
|
className="px-3 py-2 bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors whitespace-nowrap"
|
|
|
|
|
>
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
value={filters.status}
|
|
|
|
|
onChange={(e) => setFilters(prev => ({ ...prev, status: e.target.value as any }))}
|
|
|
|
|
className="px-3 py-2 bg-surface-50 dark:bg-surface-700 border border-surface-300 dark:border-surface-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors whitespace-nowrap"
|
|
|
|
|
>
|
|
|
|
|
<option value="all">All Cards</option>
|
|
|
|
|
<option value="owned">Owned</option>
|
|
|
|
|
<option value="wanted">Wanted</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* View Toggle */}
|
|
|
|
|
<div className="flex bg-surface-100 dark:bg-surface-700 rounded-lg p-1">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setViewMode('card')}
|
|
|
|
|
className={`p-2 rounded-md transition-colors ${
|
|
|
|
|
viewMode === 'card'
|
|
|
|
|
? 'bg-white dark:bg-surface-600 text-surface-900 dark:text-white shadow-sm'
|
|
|
|
|
: 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4" 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-md transition-colors ${
|
|
|
|
|
viewMode === 'list'
|
|
|
|
|
? 'bg-white dark:bg-surface-600 text-surface-900 dark:text-white shadow-sm'
|
|
|
|
|
: 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4" 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>
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Loading State */}
|
|
|
|
|
{isLoading && (
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="flex items-center justify-center py-12">
|
|
|
|
|
<div className="flex flex-col items-center">
|
|
|
|
|
<div className="w-8 h-8 bg-gradient-to-r from-primary-500 to-accent-500 rounded-full flex items-center justify-center animate-bounce-subtle mb-3">
|
|
|
|
|
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
<path 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 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-surface-600 dark:text-surface-400 text-sm">Loading your cards...</p>
|
|
|
|
|
</div>
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Error State */}
|
|
|
|
|
{error && (
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-4 mb-6">
|
|
|
|
|
<p className="text-red-800 dark:text-red-400">Error loading cards. Please try again.</p>
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
{/* Cards Display */}
|
2025-07-21 14:53:06 -04:00
|
|
|
{!isLoading && !error && (
|
|
|
|
|
<>
|
|
|
|
|
{filteredCards.length === 0 ? (
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 p-8">
|
2025-07-21 14:53:06 -04:00
|
|
|
<div className="text-center">
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="w-16 h-16 bg-surface-100 dark:bg-surface-700 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
|
|
|
|
<svg className="w-8 h-8 text-surface-400" 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 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<h3 className="text-xl font-semibold text-surface-900 dark:text-white mb-2">
|
|
|
|
|
{userCards.length === 0 ? 'No cards yet' : 'No matching cards'}
|
2025-07-21 14:53:06 -04:00
|
|
|
</h3>
|
2025-07-22 20:51:38 -04:00
|
|
|
<p className="text-surface-600 dark:text-surface-400 mb-6">
|
|
|
|
|
{userCards.length === 0
|
|
|
|
|
? 'Start building your collection by adding cards'
|
|
|
|
|
: 'Try adjusting your search or filters'
|
|
|
|
|
}
|
2025-07-21 14:53:06 -04:00
|
|
|
</p>
|
2025-07-22 20:51:38 -04:00
|
|
|
{userCards.length === 0 && (
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-3 items-center justify-center">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowCardSearch(true)}
|
|
|
|
|
className="bg-gradient-to-r from-primary-500 to-accent-500 hover:from-primary-600 hover:to-accent-600 text-white px-6 py-3 rounded-xl font-medium transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
Browse Cards
|
|
|
|
|
</button>
|
|
|
|
|
<button className="bg-surface-100 dark:bg-surface-700 hover:bg-surface-200 dark:hover:bg-surface-600 text-surface-800 dark:text-surface-300 px-6 py-3 rounded-xl font-medium transition-colors">
|
|
|
|
|
Scan Cards
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : viewMode === 'card' ? (
|
|
|
|
|
/* Card View */
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
|
|
|
{filteredCards.map((userCard: UserCard) => {
|
|
|
|
|
const card = userCard.card;
|
|
|
|
|
if (!card) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<GlowingCard key={userCard.id} rarity={card.rarity} className="bg-white dark:bg-surface-800 rounded-xl shadow overflow-hidden hover:shadow-lg transition-all duration-200">
|
|
|
|
|
{/* Card Image */}
|
|
|
|
|
<div className="flex justify-center p-4 bg-surface-50 dark:bg-surface-700/50">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<CardImageDisplay
|
|
|
|
|
card={card}
|
|
|
|
|
size="large"
|
|
|
|
|
className="mx-auto"
|
|
|
|
|
/>
|
|
|
|
|
{/* Status Badge */}
|
|
|
|
|
<div className="absolute -top-2 -right-2">
|
|
|
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusBadgeColor(userCard.status)}`}>
|
|
|
|
|
{userCard.status === 'owned' ? '✅' : '❤️'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Quantity Badge */}
|
|
|
|
|
{userCard.quantity > 1 && (
|
|
|
|
|
<div className="absolute -bottom-2 -right-2">
|
|
|
|
|
<span className="bg-surface-800 dark:bg-surface-200 text-white dark:text-surface-900 text-xs font-bold px-2 py-1 rounded-full">
|
|
|
|
|
{userCard.quantity}x
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-22 20:51:38 -04:00
|
|
|
|
|
|
|
|
{/* 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-surface-900 dark:text-white truncate" title={card.name}>
|
|
|
|
|
{card.name}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-surface-600 dark:text-surface-400 truncate">{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>
|
2025-07-21 14:53:06 -04:00
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
{/* Quick Stats */}
|
|
|
|
|
<div className="space-y-1 mb-3">
|
|
|
|
|
{card.card_type && (
|
|
|
|
|
<p className="text-xs text-surface-600 dark:text-surface-400 truncate" title={card.card_type}>
|
|
|
|
|
{card.card_type}
|
|
|
|
|
</p>
|
2025-07-21 14:53:06 -04:00
|
|
|
)}
|
|
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
<div className="flex justify-between items-center">
|
|
|
|
|
{card.mana_cost && (
|
|
|
|
|
<span className="text-xs text-surface-600 dark:text-surface-400">
|
|
|
|
|
Cost: {card.mana_cost}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(card.power && card.toughness) && (
|
|
|
|
|
<span className="text-xs text-surface-600 dark:text-surface-400">
|
|
|
|
|
{card.power}/{card.toughness}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
<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 dark:text-green-400">
|
|
|
|
|
${card.current_price.toFixed(2)}
|
2025-07-21 14:53:06 -04:00
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-22 20:51:38 -04:00
|
|
|
{/* Condition Badge */}
|
|
|
|
|
{userCard.condition && userCard.status === 'owned' && (
|
|
|
|
|
<div className="mb-3">
|
|
|
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getConditionBadgeColor(userCard.condition)}`}>
|
|
|
|
|
{userCard.condition.replace('_', ' ').toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-07-21 14:53:06 -04:00
|
|
|
)}
|
2025-07-22 20:51:38 -04:00
|
|
|
|
|
|
|
|
{/* Edit Button */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setEditingCard(userCard.id)}
|
|
|
|
|
className="w-full bg-surface-100 dark:bg-surface-700 hover:bg-surface-200 dark:hover:bg-surface-600 text-surface-800 dark:text-surface-300 py-2 px-3 rounded-lg text-sm font-medium transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Edit Card
|
|
|
|
|
</button>
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
2025-07-22 20:51:38 -04:00
|
|
|
</GlowingCard>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
) : (
|
2025-07-22 20:51:38 -04:00
|
|
|
/* List View */
|
|
|
|
|
<div className="bg-white dark:bg-surface-800 rounded-xl border border-surface-200 dark:border-surface-700 overflow-hidden">
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{filteredCards.map((userCard: UserCard) => {
|
|
|
|
|
const card = userCard.card;
|
|
|
|
|
if (!card) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={userCard.id} className="flex items-center p-4 hover:bg-surface-50 dark:hover:bg-surface-700/50 transition-colors">
|
|
|
|
|
{/* Card Image */}
|
|
|
|
|
<div className="w-12 h-16 bg-surface-200 dark:bg-surface-700 rounded-lg flex items-center justify-center flex-shrink-0 mr-4">
|
|
|
|
|
{card.image_url ? (
|
|
|
|
|
<img
|
|
|
|
|
src={card.image_url}
|
|
|
|
|
alt={card.name}
|
|
|
|
|
className="w-full h-full object-cover rounded-lg"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<svg className="w-4 h-4 text-surface-400" 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 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Card Info */}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<h3 className="font-semibold text-surface-900 dark:text-white truncate">
|
|
|
|
|
{card.name}
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm text-surface-600 dark:text-surface-400 truncate">
|
|
|
|
|
{card.set_name}
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center space-x-2 mt-1">
|
|
|
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getGameBadgeColor(card.game)}`}>
|
|
|
|
|
{card.game}
|
|
|
|
|
</span>
|
|
|
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getRarityBadgeColor(card.rarity)}`}>
|
2025-07-21 14:53:06 -04:00
|
|
|
{card.rarity}
|
|
|
|
|
</span>
|
2025-07-22 20:51:38 -04:00
|
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusBadgeColor(userCard.status)}`}>
|
|
|
|
|
{userCard.status}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Quantity and Price */}
|
|
|
|
|
<div className="text-right mr-4">
|
|
|
|
|
<div className="text-sm font-medium text-surface-900 dark:text-white">
|
|
|
|
|
{userCard.quantity}x
|
|
|
|
|
</div>
|
|
|
|
|
{card.current_price && (
|
|
|
|
|
<div className="text-sm text-green-600 dark:text-green-400">
|
|
|
|
|
${(card.current_price * userCard.quantity).toFixed(2)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Edit Button */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setEditingCard(userCard.id)}
|
|
|
|
|
className="p-2 text-surface-400 hover:text-surface-600 dark:hover:text-surface-300 rounded-lg transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2025-07-22 20:51:38 -04:00
|
|
|
|
|
|
|
|
{/* Card Search Modal */}
|
|
|
|
|
<CardSearch
|
|
|
|
|
isOpen={showCardSearch}
|
|
|
|
|
onClose={() => setShowCardSearch(false)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Card Manager Modal */}
|
|
|
|
|
{editingCard && (
|
|
|
|
|
<CardManager
|
|
|
|
|
isOpen={!!editingCard}
|
|
|
|
|
onClose={() => setEditingCard(null)}
|
|
|
|
|
cardId={editingCard}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
2025-07-22 21:00:10 -04:00
|
|
|
|
|
|
|
|
{/* Database Browser Modal */}
|
|
|
|
|
<CardDatabaseBrowser
|
|
|
|
|
isOpen={showDatabaseBrowser}
|
|
|
|
|
onClose={() => setShowDatabaseBrowser(false)}
|
|
|
|
|
onCardSelect={(card: Card) => {
|
|
|
|
|
// Handle card selection from database browser
|
|
|
|
|
console.log('Selected card from database:', card);
|
|
|
|
|
setShowDatabaseBrowser(false);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2025-07-21 14:53:06 -04:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default Cards;
|