import React, { useState, useEffect } from 'react'; import { Search, Loader2, Trash2, CheckCircle, XCircle, RefreshCw, Plus, Minus, X } from 'lucide-react'; import { Card } from '../types'; import { getUserCollection, getCardsByIds, addCardToCollection } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; import { supabase } from '../lib/supabase'; import ConfirmModal from './ConfirmModal'; export default function Collection() { const { user } = useAuth(); const [searchQuery, setSearchQuery] = useState(''); const [collection, setCollection] = useState<{ card: Card; quantity: number }[]>([]); const [filteredCollection, setFilteredCollection] = useState<{ card: Card; quantity: number }[]>([]); const [isLoadingCollection, setIsLoadingCollection] = useState(true); const [hoveredCard, setHoveredCard] = useState(null); const [selectedCard, setSelectedCard] = useState<{ card: Card; quantity: number } | null>(null); const [cardFaceIndex, setCardFaceIndex] = useState>(new Map()); const [snackbar, setSnackbar] = useState<{ message: string; type: 'success' | 'error' } | null>(null); const [isUpdating, setIsUpdating] = useState(false); const [confirmModal, setConfirmModal] = useState<{ isOpen: boolean; cardId: string; cardName: string; }>({ isOpen: false, cardId: '', cardName: '' }); // Helper function to check if a card has an actual back face (not adventure/split/etc) const isDoubleFaced = (card: Card) => { // Only show flip for cards with physical back sides const backFaceLayouts = ['transform', 'modal_dfc', 'double_faced_token', 'reversible_card']; return card.card_faces && card.card_faces.length > 1 && backFaceLayouts.includes(card.layout); }; // Helper function to get the current face index for a card const getCurrentFaceIndex = (cardId: string) => { return cardFaceIndex.get(cardId) || 0; }; // Helper function to get the image URI for a card (handling both single and double-faced) const getCardImageUri = (card: Card, faceIndex: number = 0) => { if (isDoubleFaced(card) && card.card_faces) { return card.card_faces[faceIndex]?.image_uris?.normal || card.card_faces[faceIndex]?.image_uris?.small; } return card.image_uris?.normal || card.image_uris?.small; }; // Helper function to get the large image URI for hover preview const getCardLargeImageUri = (card: Card, faceIndex: number = 0) => { if (isDoubleFaced(card) && card.card_faces) { return card.card_faces[faceIndex]?.image_uris?.large || card.card_faces[faceIndex]?.image_uris?.normal; } return card.image_uris?.large || card.image_uris?.normal; }; // Toggle card face const toggleCardFace = (cardId: string, totalFaces: number) => { setCardFaceIndex(prev => { const newMap = new Map(prev); const currentIndex = prev.get(cardId) || 0; const nextIndex = (currentIndex + 1) % totalFaces; newMap.set(cardId, nextIndex); return newMap; }); }; // Load user's collection from Supabase on mount useEffect(() => { const loadCollection = async () => { if (!user) { setIsLoadingCollection(false); return; } try { setIsLoadingCollection(true); // Get collection from Supabase (returns Map) const collectionMap = await getUserCollection(user.id); if (collectionMap.size === 0) { setCollection([]); return; } // Get the actual card data from Scryfall for all cards in collection const cardIds = Array.from(collectionMap.keys()); const cards = await getCardsByIds(cardIds); // Combine card data with quantities const collectionWithCards = cards.map(card => ({ card, quantity: collectionMap.get(card.id) || 0, })); setCollection(collectionWithCards); setFilteredCollection(collectionWithCards); } catch (error) { console.error('Error loading collection:', error); setSnackbar({ message: 'Failed to load collection', type: 'error' }); } finally { setIsLoadingCollection(false); } }; loadCollection(); }, [user]); // Filter collection based on search query useEffect(() => { if (!searchQuery.trim()) { setFilteredCollection(collection); return; } const query = searchQuery.toLowerCase(); const filtered = collection.filter(({ card }) => { return ( card.name.toLowerCase().includes(query) || card.type_line?.toLowerCase().includes(query) || card.oracle_text?.toLowerCase().includes(query) || card.colors?.some(color => color.toLowerCase().includes(query)) ); }); setFilteredCollection(filtered); }, [searchQuery, collection]); // Update card quantity in collection const updateCardQuantity = async (cardId: string, newQuantity: number) => { if (!user || newQuantity < 0) return; try { setIsUpdating(true); if (newQuantity === 0) { // Remove card from collection const { error } = await supabase .from('collections') .delete() .eq('user_id', user.id) .eq('card_id', cardId); if (error) throw error; // Update local state setCollection(prev => prev.filter(item => item.card.id !== cardId)); setSelectedCard(null); setSnackbar({ message: 'Card removed from collection', type: 'success' }); } else { // Update quantity const { error } = await supabase .from('collections') .update({ quantity: newQuantity, updated_at: new Date().toISOString() }) .eq('user_id', user.id) .eq('card_id', cardId); if (error) throw error; // Update local state setCollection(prev => prev.map(item => item.card.id === cardId ? { ...item, quantity: newQuantity } : item ) ); if (selectedCard && selectedCard.card.id === cardId) { setSelectedCard({ ...selectedCard, quantity: newQuantity }); } setSnackbar({ message: 'Quantity updated', type: 'success' }); } } catch (error) { console.error('Error updating card quantity:', error); setSnackbar({ message: 'Failed to update quantity', type: 'error' }); } finally { setIsUpdating(false); setTimeout(() => setSnackbar(null), 3000); } }; // Add one to quantity const incrementQuantity = async (cardId: string, currentQuantity: number) => { await updateCardQuantity(cardId, currentQuantity + 1); }; // Remove one from quantity const decrementQuantity = async (cardId: string, currentQuantity: number) => { if (currentQuantity > 0) { await updateCardQuantity(cardId, currentQuantity - 1); } }; return (

My Collection

{/* Search within collection */}
setSearchQuery(e.target.value)} className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="Search your collection by name, type, or text..." />
{/* Collection */}

{searchQuery ? `Found ${filteredCollection.length} card(s)` : `My Cards (${collection.length} unique, ${collection.reduce((acc, c) => acc + c.quantity, 0)} total)`}

{isLoadingCollection ? (
) : collection.length === 0 ? (

Your collection is empty

Add cards from the Deck Manager to build your collection

) : filteredCollection.length === 0 ? (

No cards found

Try a different search term

) : (
{filteredCollection.map(({ card, quantity }) => { const currentFaceIndex = getCurrentFaceIndex(card.id); const isMultiFaced = isDoubleFaced(card); const displayName = isMultiFaced && card.card_faces ? card.card_faces[currentFaceIndex]?.name || card.name : card.name; return (
setHoveredCard(card)} onMouseLeave={() => setHoveredCard(null)} onClick={() => setSelectedCard({ card, quantity })} > {/* Small card thumbnail */}
{displayName} {/* Quantity badge */}
x{quantity}
{/* Flip button for double-faced cards */} {isMultiFaced && ( )}
{/* Card name below thumbnail */}
{displayName}
); })}
)}
{/* Hover Card Preview - only show if no card is selected */} {hoveredCard && !selectedCard && (() => { const currentFaceIndex = getCurrentFaceIndex(hoveredCard.id); const isMultiFaced = isDoubleFaced(hoveredCard); const currentFace = isMultiFaced && hoveredCard.card_faces ? hoveredCard.card_faces[currentFaceIndex] : null; const displayName = currentFace?.name || hoveredCard.name; const displayTypeLine = currentFace?.type_line || hoveredCard.type_line; const displayOracleText = currentFace?.oracle_text || hoveredCard.oracle_text; return (
{displayName} {isMultiFaced && (
Face {currentFaceIndex + 1}/{hoveredCard.card_faces!.length}
)}

{displayName}

{displayTypeLine}

{displayOracleText && (

{displayOracleText}

)} {hoveredCard.prices?.usd && (
${hoveredCard.prices.usd}
)}
); })()} {/* Card Detail Panel - slides in from right */} {selectedCard && (() => { const currentFaceIndex = getCurrentFaceIndex(selectedCard.card.id); const isMultiFaced = isDoubleFaced(selectedCard.card); const currentFace = isMultiFaced && selectedCard.card.card_faces ? selectedCard.card.card_faces[currentFaceIndex] : null; const displayName = currentFace?.name || selectedCard.card.name; const displayTypeLine = currentFace?.type_line || selectedCard.card.type_line; const displayOracleText = currentFace?.oracle_text || selectedCard.card.oracle_text; return ( <> {/* Backdrop */}
setSelectedCard(null)} /> {/* Sliding Panel */}
{/* Close button - fixed position, stays visible when scrolling */}
{/* Card Image */}
{displayName} {isMultiFaced && ( <>
Face {currentFaceIndex + 1}/{selectedCard.card.card_faces!.length}
)}
{/* Card Info */}

{displayName}

{displayTypeLine}

{displayOracleText && (

{displayOracleText}

)} {selectedCard.card.prices?.usd && (
${selectedCard.card.prices.usd} each
Total value: ${(parseFloat(selectedCard.card.prices.usd) * selectedCard.quantity).toFixed(2)}
)} {/* Quantity Management */}

Quantity in Collection

{selectedCard.quantity}
copies
{/* Remove from collection button */}
); })()} {/* Confirm Modal */} setConfirmModal({ isOpen: false, cardId: '', cardName: '' })} onConfirm={() => { updateCardQuantity(confirmModal.cardId, 0); setConfirmModal({ isOpen: false, cardId: '', cardName: '' }); }} title="Remove from Collection" message={`Are you sure you want to remove "${confirmModal.cardName}" from your collection? This action cannot be undone.`} confirmText="Remove" cancelText="Cancel" variant="danger" isLoading={isUpdating} /> {/* Snackbar */} {snackbar && (
{snackbar.type === 'success' ? ( ) : ( )} {snackbar.message}
)}
); }