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 PublicCardsView from '../components/PublicCardsView'; 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'; import { VOCAB } from '../lib/collection-vocabulary.js'; 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(() => { /* eslint-disable react-hooks/set-state-in-effect -- reset list state when filters change, then fetch */ setPagination(prev => ({ ...prev, page: 1 })); setCards([]); setHasMore(true); hasMoreRef.current = true; fetchCards(false); loadFavoritedCards(); // Load user's favorites /* eslint-enable react-hooks/set-state-in-effect */ // eslint-disable-next-line react-hooks/exhaustive-deps -- filter-driven reload; fetchCards closes over latest search state }, [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); } }; // eslint-disable-next-line react-hooks/exhaustive-deps -- observer setup once; loadMoreCards uses refs for latest state }, []); 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 ${VOCAB.ADD_TO_MY_COLLECTION.toLowerCase()}`); 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 added ${cards.length} cards to ${VOCAB.MY_COLLECTION}!`); } else { alert(`Added ${successCount} out of ${cards.length} cards to ${VOCAB.MY_COLLECTION}. 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 add cards to ${VOCAB.MY_COLLECTION}. Please try again.`); } }; const handleBulkRemoveFromOwned = async (cards) => { try { const token = localStorage.getItem('auth_token'); if (!token) { alert(`Please log in to ${VOCAB.REMOVE_FROM_MY_COLLECTION.toLowerCase()}`); 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 ${VOCAB.MY_COLLECTION}!`); } else { alert(`Removed ${successCount} out of ${cards.length} cards from ${VOCAB.MY_COLLECTION}. 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 ${VOCAB.MY_COLLECTION}. 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} list${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 (
); } return ( {/* Header */}

Cards

Browse and manage your card collection

{/* Quick Filters */}

Quick Filters

{tcgOptions.map(option => ( ))}
{/* Advanced Filters */}
handleSearchChange(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') { setPagination(prev => ({ ...prev, page: 1 })); setCards([]); setHasMore(true); hasMoreRef.current = true; fetchCards(false); } }} />
{/* Active Filters Display */} {(selectedTCG !== 'all' || selectedRarity !== 'all' || selectedSet !== 'all' || selectedValueRange !== 'all') && (
{selectedTCG !== 'all' && ( {tcgOptions.find(opt => opt.value === selectedTCG)?.label} )} {selectedRarity !== 'all' && ( {rarityOptions.find(opt => opt.value === selectedRarity)?.label} )} {selectedSet !== 'all' && ( {setOptions.find(opt => opt.value === selectedSet)?.label} )} {selectedValueRange !== 'all' && ( {valueRangeOptions.find(opt => opt.value === selectedValueRange)?.label} )}
)}
{/* Cards Display */}
{cards.length === 0 ? (
🃏

No cards found

Try adjusting your search or filters

) : ( <>
{cards.map((card) => ( c.id === card.id)} onToggleSelect={handleToggleSelect} onAddToCollection={(card) => handleBulkAddToCollection([card])} onAddToDeck={handleBulkAddToDeck} onToggleFavorite={handleToggleFavorite} isFavorited={favoritedCards.has(card.id)} /> ))}
{/* Infinite Scroll Loading Indicator */} {loadingMore && (
Loading more cards...
)} {/* Infinite Scroll Sentinel */}
{/* Load More Button */} {hasMore && !loadingMore && (
)} {/* End of results indicator */} {!hasMore && cards.length > 0 && (
No more cards to load
)} )}
{/* Bulk Selection Toolbar */} {/* Collection Selection Modal */} setShowCollectionModal(false)} cards={cardsToAdd} onAddToCollections={handleAddToCollections} /> ); } export default function Cards() { return ( } > ); }