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"
+ >
+

{
+ 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..6df3979
--- /dev/null
+++ b/lib/use-collection-view.js
@@ -0,0 +1,502 @@
+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() {
+ 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 = loading;
+
+ return {
+ cards,
+ collection,
+ copySuccess,
+ editForm,
+ favoritedCards,
+ gameStats,
+ groupBy,
+ groupedCards,
+ handleAddCard,
+ handleAddToCollection,
+ handleAddToDeck,
+ handleDeleteCollection,
+ handleDownloadCSV,
+ handleEditCollection,
+ handleImageUpload,
+ handleSearchCards,
+ handleToggleFavorite,
+ handleToggleSelect,
+ id,
+ 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..82fb542 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();
- 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
-