- Added missing 'game' field to collection cards API query - Added onClick handler to 'Add Cards' button (redirects to /cards) - Enhanced debug logging to see actual card data - Ran sample cards script to ensure cards exist in database This should fix the missing TCG tags issue by including the game field in the API response.
686 lines
No EOL
27 KiB
JavaScript
686 lines
No EOL
27 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import Link from 'next/link';
|
|
import UploadImageModal from '../../components/UploadImageModal';
|
|
import ShareModal from '../../components/ShareModal';
|
|
import CollaboratorFacepile from '../../components/CollaboratorFacepile';
|
|
import Layout from '../../components/Layout';
|
|
|
|
export default function CollectionView() {
|
|
const router = useRouter();
|
|
const { id } = router.query;
|
|
|
|
// Get user from auth context - for now using admin user
|
|
const user = {
|
|
email: 'admin@tcgvault.com',
|
|
role: 'admin'
|
|
};
|
|
|
|
const [collection, setCollection] = useState(null);
|
|
const [cards, setCards] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isFavorited, setIsFavorited] = useState(false);
|
|
const [showShareModal, setShowShareModal] = useState(false);
|
|
const [copySuccess, setCopySuccess] = useState(false);
|
|
const [selectedTCG, setSelectedTCG] = useState('MTG');
|
|
|
|
// Filter states
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedRarity, setSelectedRarity] = useState('All Rarities');
|
|
const [selectedType, setSelectedType] = useState('All Types');
|
|
const [groupBy, setGroupBy] = useState('Group by Game');
|
|
const [sortBy, setSortBy] = useState('Sort by Name');
|
|
const [viewMode, setViewMode] = useState('grid');
|
|
const [searchCards, setSearchCards] = useState('');
|
|
const [searchResults, setSearchResults] = useState([]);
|
|
const [showSearchResults, setShowSearchResults] = useState(false);
|
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (id) {
|
|
fetchCollectionData();
|
|
}
|
|
}, [id]);
|
|
|
|
const fetchCollectionData = async () => {
|
|
try {
|
|
const response = await fetch(`/api/collections/${id}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setCollection(data.collection);
|
|
setCards(data.cards || []);
|
|
|
|
// Check if collection is favorited
|
|
checkIfFavorited();
|
|
} else {
|
|
console.error('Failed to fetch collection');
|
|
setCollection(null);
|
|
setCards([]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching collection:', error);
|
|
setCollection(null);
|
|
setCards([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const checkIfFavorited = async () => {
|
|
try {
|
|
const response = await fetch(`/api/favorites?type=collection`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
|
}
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const isFav = data.favorites.some(fav => fav.item_id === parseInt(id));
|
|
setIsFavorited(isFav);
|
|
} else {
|
|
console.error('Failed to check favorites:', response.status);
|
|
// Keep default false state
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking favorites:', error);
|
|
// Keep default false state
|
|
}
|
|
};
|
|
|
|
const handleSearchCards = async (query) => {
|
|
if (query.length < 2) {
|
|
setSearchResults([]);
|
|
setShowSearchResults(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/cards/search?q=${encodeURIComponent(query)}&limit=10`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
setSearchResults(data.cards || []);
|
|
setShowSearchResults(true);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error searching cards:', error);
|
|
}
|
|
};
|
|
|
|
const handleAddCard = async (card) => {
|
|
try {
|
|
const response = await fetch(`/api/collections/${id}/cards`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
|
},
|
|
body: JSON.stringify({
|
|
cardId: card.id,
|
|
quantity: 1
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setSearchCards('');
|
|
setShowSearchResults(false);
|
|
fetchCollectionData(); // Refresh the collection data
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding card:', error);
|
|
}
|
|
};
|
|
|
|
|
|
|
|
const toggleFavorite = async () => {
|
|
try {
|
|
if (isFavorited) {
|
|
// Remove from favorites
|
|
const response = await fetch('/api/favorites', {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
|
},
|
|
body: JSON.stringify({
|
|
itemType: 'collection',
|
|
itemId: parseInt(id)
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setIsFavorited(false);
|
|
} else {
|
|
console.error('Failed to remove favorite:', response.status);
|
|
}
|
|
} else {
|
|
// Add to favorites
|
|
const response = await fetch('/api/favorites', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
|
},
|
|
body: JSON.stringify({
|
|
itemType: 'collection',
|
|
itemId: parseInt(id)
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setIsFavorited(true);
|
|
} else {
|
|
console.error('Failed to add favorite:', response.status);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error toggling favorite:', error);
|
|
}
|
|
};
|
|
|
|
const togglePublic = async () => {
|
|
try {
|
|
const response = await fetch(`/api/collections/${id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
|
},
|
|
body: JSON.stringify({
|
|
is_public: !collection.is_public
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setCollection(prev => ({
|
|
...prev,
|
|
is_public: !prev.is_public
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating collection:', error);
|
|
}
|
|
};
|
|
|
|
const handleImageUpload = async (imageUrl) => {
|
|
try {
|
|
const response = await fetch(`/api/collections/${id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
|
},
|
|
body: JSON.stringify({
|
|
image: imageUrl
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
setCollection(prev => ({
|
|
...prev,
|
|
image: imageUrl
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating collection image:', error);
|
|
}
|
|
};
|
|
|
|
const handleDownloadCSV = () => {
|
|
if (cards.length === 0) {
|
|
alert('No cards to download');
|
|
return;
|
|
}
|
|
|
|
// Create CSV headers
|
|
const headers = [
|
|
'Name',
|
|
'Set Name',
|
|
'Set Code',
|
|
'Card Number',
|
|
'Rarity',
|
|
'Game',
|
|
'Mana Cost',
|
|
'CMC',
|
|
'Type',
|
|
'Colors',
|
|
'Oracle Text',
|
|
'Power',
|
|
'Toughness',
|
|
'Market Price',
|
|
'Quantity',
|
|
'Added Date'
|
|
];
|
|
|
|
// Create CSV rows
|
|
const csvRows = [
|
|
headers.join(','), // Header row
|
|
...cards.map(card => [
|
|
`"${(card.name || '').replace(/"/g, '""')}"`,
|
|
`"${(card.set_name || '').replace(/"/g, '""')}"`,
|
|
`"${card.set_code || ''}"`,
|
|
`"${card.card_number || ''}"`,
|
|
`"${card.rarity || ''}"`,
|
|
`"${card.game || ''}"`,
|
|
`"${card.mana_cost || ''}"`,
|
|
`"${card.cmc || ''}"`,
|
|
`"${card.card_type || ''}"`,
|
|
`"${Array.isArray(card.colors) ? card.colors.join(', ') : (card.colors || '')}"`,
|
|
`"${(card.oracle_text || '').replace(/"/g, '""')}"`,
|
|
`"${card.power || ''}"`,
|
|
`"${card.toughness || ''}"`,
|
|
`"${card.market_price || ''}"`,
|
|
`"${card.quantity || 1}"`,
|
|
`"${card.created_at ? new Date(card.created_at).toLocaleDateString() : ''}"`
|
|
].join(','))
|
|
];
|
|
|
|
// Create and download the CSV file
|
|
const csvContent = csvRows.join('\n');
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
|
|
if (link.download !== undefined) {
|
|
const url = URL.createObjectURL(blob);
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', `${collection.name || 'collection'}_cards.csv`);
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
};
|
|
|
|
// Group cards by game
|
|
const groupedCards = cards.reduce((acc, card) => {
|
|
const game = card.game || 'Other';
|
|
if (!acc[game]) acc[game] = [];
|
|
acc[game].push(card);
|
|
return acc;
|
|
}, {});
|
|
|
|
// Debug: Log the grouped cards to see what games we have
|
|
console.log('Grouped cards:', groupedCards);
|
|
console.log('Available games:', Object.keys(groupedCards));
|
|
console.log('Sample card data:', cards[0]); // Check what fields the card actually has
|
|
|
|
// Get game display names and counts - make it more flexible
|
|
const gameStats = {};
|
|
Object.keys(groupedCards).forEach(game => {
|
|
if (game && game !== 'Other') {
|
|
gameStats[game] = groupedCards[game].length;
|
|
}
|
|
});
|
|
|
|
if (loading) {
|
|
return (
|
|
<Layout user={user}>
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600"></div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|
|
|
|
if (!collection) {
|
|
return (
|
|
<Layout user={user}>
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-center">
|
|
<h2 className="text-2xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>
|
|
Collection not found
|
|
</h2>
|
|
<Link href="/collections">
|
|
<button className="px-4 py-2 rounded-lg gradient-bg-purple text-white">
|
|
Back to Collections
|
|
</button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Layout user={user}>
|
|
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
|
{/* Header */}
|
|
<div className="p-6 border-b" style={{ backgroundColor: 'var(--bg-secondary)', borderColor: 'var(--border)' }}>
|
|
<div className="flex items-center justify-between mb-6">
|
|
{/* Back button */}
|
|
<div className="flex items-center">
|
|
<Link href="/collections">
|
|
<button className="flex items-center text-sm font-medium hover:underline" style={{ color: 'var(--text-secondary)' }}>
|
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back to collections
|
|
</button>
|
|
</Link>
|
|
</div>
|
|
|
|
{/* Action buttons */}
|
|
<div className="flex items-center space-x-3">
|
|
<button
|
|
onClick={() => setShowUploadModal(true)}
|
|
className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
Upload Image
|
|
</button>
|
|
<button className="px-4 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50" style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}>
|
|
🪄 Generate AI Image
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Collection Info */}
|
|
<div className="mb-6">
|
|
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
|
{collection.name}
|
|
</h1>
|
|
<p className="text-lg mb-4" style={{ color: 'var(--text-secondary)' }}>
|
|
{collection.description}
|
|
</p>
|
|
|
|
{/* TCG Tags */}
|
|
<div className="flex items-center space-x-2 mb-4">
|
|
{Object.entries(gameStats).map(([game, count]) =>
|
|
count > 0 ? (
|
|
<span
|
|
key={game}
|
|
className="px-3 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800"
|
|
>
|
|
{game}
|
|
</span>
|
|
) : null
|
|
)}
|
|
</div>
|
|
|
|
{/* Creator and Stats */}
|
|
<div className="flex items-center space-x-6 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
{console.log('Collection data for facepile:', { id, creator_email: collection.creator_email, collection })}
|
|
{collection.creator_email ? (
|
|
<CollaboratorFacepile
|
|
collectionId={id}
|
|
creatorEmail={collection.creator_email}
|
|
/>
|
|
) : (
|
|
<span className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
Crafted by Unknown User
|
|
</span>
|
|
)}
|
|
<div>Cards: <span className="font-medium">{cards.length}</span></div>
|
|
<div>Cost: <span className="font-medium">${collection.totalValue || '0'}</span></div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4 mt-2 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
<span>Created {new Date(collection.created_at).toLocaleDateString()}</span>
|
|
<span>Last updated {new Date(collection.updated_at).toLocaleDateString()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Bar */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={() => setShowShareModal(true)}
|
|
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.367 2.684 3 3 0 00-5.367-2.684z" />
|
|
</svg>
|
|
<span>Share</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={toggleFavorite}
|
|
className={`flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg ${
|
|
isFavorited ? 'bg-red-50 border-red-200 text-red-600' : 'hover:bg-gray-50'
|
|
}`}
|
|
style={!isFavorited ? { borderColor: 'var(--border)', color: 'var(--text-primary)' } : {}}
|
|
>
|
|
<svg className="w-4 h-4" fill={isFavorited ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
<span>Favorite</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleDownloadCSV}
|
|
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium border rounded-lg hover:bg-gray-50"
|
|
style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<span>Download List</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-1 text-sm" style={{ color: 'var(--text-secondary)' }}>
|
|
<span>Activity</span>
|
|
<span className="px-2 py-1 bg-purple-100 text-purple-800 rounded-full text-xs font-medium">123</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6">
|
|
{/* Search and Filters */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
placeholder="Search cards..."
|
|
value={searchCards}
|
|
onChange={(e) => {
|
|
setSearchCards(e.target.value);
|
|
handleSearchCards(e.target.value);
|
|
}}
|
|
className="w-64 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
|
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
/>
|
|
<svg className="absolute right-3 top-2.5 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--text-secondary)' }}>
|
|
<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>
|
|
|
|
<select
|
|
value={selectedRarity}
|
|
onChange={(e) => setSelectedRarity(e.target.value)}
|
|
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
>
|
|
<option>All Rarities</option>
|
|
<option>Common</option>
|
|
<option>Uncommon</option>
|
|
<option>Rare</option>
|
|
<option>Mythic</option>
|
|
<option>Legendary</option>
|
|
</select>
|
|
|
|
<select
|
|
value={selectedType}
|
|
onChange={(e) => setSelectedType(e.target.value)}
|
|
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
>
|
|
<option>All Types</option>
|
|
<option>Creature</option>
|
|
<option>Instant</option>
|
|
<option>Sorcery</option>
|
|
<option>Artifact</option>
|
|
<option>Enchantment</option>
|
|
</select>
|
|
|
|
<select
|
|
value={groupBy}
|
|
onChange={(e) => setGroupBy(e.target.value)}
|
|
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
>
|
|
<option>Group by Game</option>
|
|
<option>Group by Rarity</option>
|
|
<option>Group by Type</option>
|
|
</select>
|
|
|
|
<select
|
|
value={sortBy}
|
|
onChange={(e) => setSortBy(e.target.value)}
|
|
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-purple-500"
|
|
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg-secondary)' }}
|
|
>
|
|
<option>Sort by Name</option>
|
|
<option>Sort by Price</option>
|
|
<option>Sort by Rarity</option>
|
|
<option>Sort by Date Added</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-3">
|
|
<div className="flex rounded-lg border" style={{ borderColor: 'var(--border)' }}>
|
|
<button
|
|
onClick={() => setViewMode('grid')}
|
|
className={`p-2 rounded-l-lg ${viewMode === 'grid' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`}
|
|
style={viewMode !== 'grid' ? { color: 'var(--text-secondary)' } : {}}
|
|
>
|
|
<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-r-lg ${viewMode === 'list' ? 'bg-white text-gray-900 shadow-sm' : 'bg-transparent hover:bg-gray-50'}`}
|
|
style={viewMode !== 'list' ? { color: 'var(--text-secondary)' } : {}}
|
|
>
|
|
<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>
|
|
|
|
<button
|
|
onClick={() => router.push('/cards')}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center space-x-2"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
</svg>
|
|
<span>Add Cards</span>
|
|
</button>
|
|
|
|
<button className="px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center space-x-2" style={{ borderColor: 'var(--border)', color: 'var(--text-primary)' }}>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
<span>Scan Cards</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Game Sections */}
|
|
{Object.entries(groupedCards).map(([game, gameCards]) => (
|
|
<div key={game} className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-xl font-bold flex items-center space-x-3" style={{ color: 'var(--text-primary)' }}>
|
|
<span>{game === 'MTG' ? 'Magic The Gathering' : game}</span>
|
|
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-sm font-medium">
|
|
{gameCards.length}
|
|
</span>
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-4">
|
|
{gameCards.map((card, index) => (
|
|
<div key={card.id || index} className="aspect-[2.5/3.5] bg-gray-200 rounded-lg flex items-center justify-center text-gray-500 text-sm font-medium hover:bg-gray-300 transition-colors cursor-pointer">
|
|
{card.image_url ? (
|
|
<img
|
|
src={card.image_url}
|
|
alt={card.name}
|
|
className="w-full h-full object-cover rounded-lg"
|
|
/>
|
|
) : (
|
|
'Card'
|
|
)}
|
|
</div>
|
|
))}
|
|
|
|
{/* Add empty card slots to fill the row */}
|
|
{Array.from({ length: Math.max(0, 7 - (gameCards.length % 7)) }, (_, index) => (
|
|
<div key={`empty-${index}`} className="aspect-[2.5/3.5] bg-gray-200 rounded-lg flex items-center justify-center text-gray-500 text-sm font-medium hover:bg-gray-300 transition-colors cursor-pointer">
|
|
Card
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{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)' }}>
|
|
Start Building Your Collection
|
|
</h3>
|
|
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
|
Add cards to get started with your collection
|
|
</p>
|
|
<button className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700">
|
|
Browse Cards to Add
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Search Results Dropdown */}
|
|
{showSearchResults && searchResults.length > 0 && (
|
|
<div className="absolute top-full left-0 right-0 bg-white border border-gray-200 rounded-lg shadow-lg max-h-64 overflow-y-auto z-50">
|
|
{searchResults.map(card => (
|
|
<div
|
|
key={card.id}
|
|
onClick={() => handleAddCard(card)}
|
|
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
|
|
>
|
|
<img
|
|
src={card.image_url}
|
|
alt={card.name}
|
|
className="w-12 h-16 object-cover rounded mr-3"
|
|
onError={(e) => {
|
|
e.target.src = 'https://via.placeholder.com/48x64/6366f1/ffffff?text=No+Image';
|
|
}}
|
|
/>
|
|
<div>
|
|
<div className="font-medium">{card.name}</div>
|
|
<div className="text-sm text-gray-500">{card.set_name} • ${card.market_price}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
|
|
|
|
{/* Upload Image Modal */}
|
|
<UploadImageModal
|
|
isOpen={showUploadModal}
|
|
onClose={() => setShowUploadModal(false)}
|
|
onUpload={handleImageUpload}
|
|
currentImage={collection.image}
|
|
/>
|
|
|
|
{/* Share Modal */}
|
|
<ShareModal
|
|
isOpen={showShareModal}
|
|
onClose={() => setShowShareModal(false)}
|
|
collectionId={id}
|
|
isPublic={collection.is_public}
|
|
onTogglePublic={togglePublic}
|
|
onInviteUser={(email) => console.log('Invited:', email)}
|
|
/>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|