diff --git a/components/CollectionPageView.js b/components/CollectionPageView.js new file mode 100644 index 0000000..f5eef0a --- /dev/null +++ b/components/CollectionPageView.js @@ -0,0 +1,475 @@ +/* eslint-disable @next/next/no-img-element -- Binder/card images use external URLs; next/image migration is out of scope. */ +import Link from 'next/link'; +import UploadImageModal from './UploadImageModal'; +import ShareModal from './ShareModal'; +import CollaboratorFacepile from './CollaboratorFacepile'; +import CardItem from './CardItem'; +import LoginCTA from './LoginCTA'; +import { ManaCost, ColorFilterSymbol } from './ManaSymbols'; +import ManaSymbolSettings from './ManaSymbolSettings'; +import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js'; +import CollectionEditModal from './CollectionEditModal'; +import CollectionDeleteModal from './CollectionDeleteModal'; +export default function CollectionPageView(props) { + const { + cards, + collection, + copySuccess, + editForm, + favoritedCards, + gameStats, + groupBy, + groupedCards, + handleAddCard, + handleAddToCollection, + handleAddToDeck, + handleDeleteCollection, + handleDownloadCSV, + handleEditCollection, + handleImageUpload, + handleSearchCards, + handleToggleFavorite, + handleToggleSelect, + identifier, + isFavorited, + loading, + router, + searchCards, + searchQuery, + searchResults, + selectedCards, + selectedRarity, + selectedTCG, + selectedType, + setCards, + setCollection, + setCopySuccess, + setEditForm, + setFavoritedCards, + setGroupBy, + setIsFavorited, + setLoading, + setSearchCards, + setSearchQuery, + setSearchResults, + setSelectedCards, + setSelectedRarity, + setSelectedTCG, + setSelectedType, + setShowDeleteModal, + setShowEditModal, + setShowSearchResults, + setShowShareModal, + setShowUploadModal, + setSortBy, + setViewMode, + showDeleteModal, + showEditModal, + showInitialLoading, + showSearchResults, + showShareModal, + showUploadModal, + sortBy, + user, + viewMode + } = props; + + 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}
+
+
+ ))} +
+ )} + + setShowEditModal(false)} + onSave={handleEditCollection} + /> + + setShowDeleteModal(false)} + onConfirm={handleDeleteCollection} + /> + + {/* Upload Image Modal */} + setShowUploadModal(false)} + onUpload={handleImageUpload} + currentImage={collection.image} + /> + + {/* Share Modal */} + setShowShareModal(false)} + collectionId={identifier} + isPublic={collection.isPublic} + onTogglePublic={togglePublic} + onInviteUser={(email) => console.log('Invited:', email)} + /> +
+ + {/* Show login CTA for non-authenticated users */} + {!user && collection?.isPublic && ( + + )} + + ); +} diff --git a/lib/use-collection-view.js b/lib/use-collection-view.js new file mode 100644 index 0000000..36880f8 --- /dev/null +++ b/lib/use-collection-view.js @@ -0,0 +1,501 @@ +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { VOCAB, collectionDisplayName } from './collection-vocabulary.js'; +import { downloadCollectionCardsCsv } from './collection-cards-csv.js'; + +/** + * Card/collection page state and handlers (god-component split). + */ +export function useCollectionView({ user = null, authLoading = true } = {}) { + const router = useRouter(); + const { identifier } = router.query; + + 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 = () => { + downloadCollectionCardsCsv(cards, collection?.name); + }; + + // 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; + } + }); + + const showInitialLoading = authLoading || loading; + + return { + cards, + collection, + copySuccess, + editForm, + favoritedCards, + gameStats, + groupBy, + groupedCards, + handleAddCard, + handleAddToCollection, + handleAddToDeck, + handleDeleteCollection, + handleDownloadCSV, + handleEditCollection, + handleImageUpload, + handleSearchCards, + handleToggleFavorite, + handleToggleSelect, + identifier, + isFavorited, + loading, + router, + searchCards, + searchQuery, + searchResults, + selectedCards, + selectedRarity, + selectedTCG, + selectedType, + setCards, + setCollection, + setCopySuccess, + setEditForm, + setFavoritedCards, + setGroupBy, + setIsFavorited, + setLoading, + setSearchCards, + setSearchQuery, + setSearchResults, + setSelectedCards, + setSelectedRarity, + setSelectedTCG, + setSelectedType, + setShowDeleteModal, + setShowEditModal, + setShowSearchResults, + setShowShareModal, + setShowUploadModal, + setSortBy, + setViewMode, + showDeleteModal, + showEditModal, + showInitialLoading, + showSearchResults, + showShareModal, + showUploadModal, + sortBy, + viewMode + }; +} diff --git a/pages/collection/[identifier].js b/pages/collection/[identifier].js index 9477200..f40f7cf 100644 --- a/pages/collection/[identifier].js +++ b/pages/collection/[identifier].js @@ -1,463 +1,24 @@ -/* 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 CollectionPageView from '../../components/CollectionPageView'; import { useAuth } from '../../lib/use-auth'; -import { VOCAB, collectionDisplayName } from '../../lib/collection-vocabulary.js'; -import { downloadCollectionCardsCsv } from '../../lib/collection-cards-csv.js'; -import CollectionEditModal from '../../components/CollectionEditModal'; -import CollectionDeleteModal from '../../components/CollectionDeleteModal'; +import { useCollectionView } from '../../lib/use-collection-view.js'; export default function CollectionView() { - const router = useRouter(); - const { identifier } = router.query; const { user, loading: authLoading } = useAuth(); + const collectionPage = useCollectionView({ user, authLoading }); - 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 = () => { - downloadCollectionCardsCsv(cards, collection?.name); - }; - - // 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) { + if (collectionPage.showInitialLoading) { return (
-
+
); } - if (!collection) { + if (!collectionPage.collection) { return (
@@ -466,7 +27,7 @@ export default function CollectionView() { List not found - @@ -478,400 +39,7 @@ export default function CollectionView() { 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}
-
-
- ))} -
- )} - - setShowEditModal(false)} - onSave={handleEditCollection} - /> - - setShowDeleteModal(false)} - onConfirm={handleDeleteCollection} - /> - - {/* Upload Image Modal */} - setShowUploadModal(false)} - onUpload={handleImageUpload} - currentImage={collection.image} - /> - - {/* Share Modal */} - setShowShareModal(false)} - collectionId={identifier} - isPublic={collection.isPublic} - onTogglePublic={togglePublic} - onInviteUser={(email) => console.log('Invited:', email)} - /> -
- - {/* Show login CTA for non-authenticated users */} - {!user && collection?.isPublic && ( - - )} +
); -} \ No newline at end of file +}