/* eslint-disable @next/next/no-img-element -- Binder/card images use external URLs; next/image migration is out of scope. */ 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 CardItem from '../../components/CardItem'; import Layout from '../../components/Layout'; import LoginCTA from '../../components/LoginCTA'; import { ManaCost, ColorFilterSymbol } from '../../components/ManaSymbols'; import ManaSymbolSettings from '../../components/ManaSymbolSettings'; import { useAuth } from '../../lib/use-auth'; import { VOCAB, collectionDisplayName } from '../../lib/collection-vocabulary.js'; export default function CollectionView() { const router = useRouter(); const { identifier } = router.query; const { user, loading: authLoading } = useAuth(); 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 [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [copySuccess, setCopySuccess] = useState(false); const [selectedTCG, setSelectedTCG] = useState('MTG'); // Edit form state const [editForm, setEditForm] = useState({ name: '', description: '', isPublic: false, image: '', tags: [] }); // 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); // Card interaction states const [selectedCards, setSelectedCards] = useState([]); const [favoritedCards, setFavoritedCards] = useState(new Set()); 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); } }; 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 === collection?.id); setIsFavorited(isFav); } else { console.error('Failed to check favorites:', response.status); } } catch (error) { console.error('Error checking favorites:', error); } }; const fetchCollectionData = async () => { try { // First try to fetch without authentication (for public collections) let response = await fetch(`/api/collections/${identifier}`); // If that fails and we have a user, try with authentication if (!response.ok && user) { response = await fetch(`/api/collections/${identifier}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` } }); } if (response.ok) { const data = await response.json(); // Check if we accessed via numeric ID and need to redirect to slug if (data.slug && identifier !== data.slug && !isNaN(parseInt(identifier))) { // Redirect to slug URL router.replace(`/collection/${data.slug}`, undefined, { shallow: false }); return; } setCollection(data); setEditForm({ name: data.name || '', description: data.description || '', isPublic: data.isPublic || false, image: data.image || '', tags: Array.isArray(data.tags) ? data.tags : (data.tags ? data.tags.split(',') : []) }); // Fetch collection cards (try public first, then authenticated) let cardsResponse = await fetch(`/api/collections/${identifier}/cards`); if (!cardsResponse.ok && user) { cardsResponse = await fetch(`/api/collections/${identifier}/cards`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` } }); } if (cardsResponse.ok) { const cardsData = await cardsResponse.json(); setCards(cardsData.cards || []); } // Check if collection is favorited checkIfFavorited(); } else if (response.status === 401 || response.status === 403) { // Collection is private and user is not authenticated/authorized if (!user) { router.push('/login'); } else { // User is authenticated but doesn't have access router.push('/collections'); } } } catch (error) { console.error('Error fetching collection:', error); } finally { setLoading(false); } }; useEffect(() => { if (identifier) { // eslint-disable-next-line react-hooks/set-state-in-effect -- load list when slug or auth changes fetchCollectionData(); if (user) { loadFavoritedCards(); } } // eslint-disable-next-line react-hooks/exhaustive-deps -- reload when list slug or auth changes }, [identifier, user]); const handleEditCollection = async () => { try { const response = await fetch(`/api/collections/${identifier}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }, body: JSON.stringify({ name: editForm.name, description: editForm.description, isPublic: editForm.isPublic, image: editForm.image, tags: editForm.tags }) }); if (response.ok) { const updatedCollection = await response.json(); // If the name changed and we got a new slug, redirect if (updatedCollection.slug && updatedCollection.slug !== identifier) { router.push(`/collection/${updatedCollection.slug}`); } else { // Just refresh the data fetchCollectionData(); } setShowEditModal(false); } else { const error = await response.json(); alert(error.error || 'Failed to update list'); } } catch (error) { console.error('Error updating collection:', error); alert('Network error. Please try again.'); } }; const handleDeleteCollection = async () => { try { const response = await fetch(`/api/collections/${identifier}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` } }); if (response.ok) { router.push('/collections'); } else { const error = await response.json(); alert(error.error || 'Failed to delete list'); } } catch (error) { console.error('Error deleting collection:', error); alert('Network error. Please try again.'); } }; 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/${identifier}/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: collection.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: collection.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/${identifier}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` }, body: JSON.stringify({ isPublic: !collection.isPublic }) }); if (response.ok) { setCollection(prev => ({ ...prev, isPublic: !prev.isPublic })); } } catch (error) { console.error('Error updating collection:', error); } }; const handleImageUpload = async (imageUrl) => { try { const response = await fetch(`/api/collections/${identifier}`, { 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); } }; // Card interaction 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 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); } }; const handleAddToCollection = (card) => { // This would open a collection selection modal console.log('Add to collection:', card); }; const handleAddToDeck = (card) => { // This would open a deck selection modal console.log('Add to deck:', card); }; 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; }, {}); // Get game display names and counts const gameStats = {}; Object.keys(groupedCards).forEach(game => { if (game && game !== 'Other') { gameStats[game] = groupedCards[game].length; } }); // Show loading spinner while auth is loading or data is loading if (authLoading || loading) { return (
); } if (!collection) { return (

List not found

); } return (
{/* Header */}
{/* Back button */}
{/* Action buttons */}
{/* Edit and Delete buttons - only show for owner and non-system collections */} {collection.userRole === 'owner' && !collection.isSystemCollection && ( <> )}
{/* Collection Info */}

{collectionDisplayName(collection)}

{/* System collection indicator */} {collection.isSystemCollection && (
🔒 SYSTEM
{VOCAB.SYSTEM_COLLECTION_SYNC_HINT}
)}

{collection.description}

{/* TCG Tags */}
{Object.entries(gameStats).map(([game, count]) => count > 0 ? ( {game} ) : null )}
{/* Creator and Stats */}
{collection.creator ? ( ) : ( Crafted by Unknown User )}
Cards: {cards.length}
Cost: ${collection.value || '0'}
Created {new Date(collection.createdAt).toLocaleDateString()} Last updated {new Date(collection.lastViewed).toLocaleDateString()}
{/* Action Bar */}
Activity 123
{/* Content */}
{/* Search and Filters */}
{ 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)' }} />
{/* Game Sections */} {Object.entries(groupedCards).map(([game, gameCards]) => (

{game === 'MTG' ? 'Magic The Gathering' : game} {gameCards.length}

{gameCards.map((card, index) => ( c.id === card.id)} onToggleSelect={handleToggleSelect} onAddToCollection={handleAddToCollection} onAddToDeck={handleAddToDeck} onToggleFavorite={handleToggleFavorite} isFavorited={favoritedCards.has(card.id)} /> ))}
))} {cards.length === 0 && (
🃏

Start Building Your List

Add cards to get started with your collection

)}
{/* Search Results Dropdown */} {showSearchResults && searchResults.length > 0 && (
{searchResults.map(card => (
handleAddCard(card)} className="flex items-center p-3 hover:bg-gray-50 cursor-pointer" > {card.name} { e.target.src = 'https://via.placeholder.com/48x64/6366f1/ffffff?text=No+Image'; }} />
{card.name}
{card.set_name} • ${card.market_price}
))}
)} {/* Edit Collection Modal */} {showEditModal && (

Edit List

setEditForm({...editForm, name: e.target.value})} placeholder="Enter list name" />