From 20ccd2333e922bc755d3111241c13bf4862adad9 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Sat, 26 Jul 2025 00:22:35 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=AF=20Implemented=20Collection=20Selec?= =?UTF-8?q?tion=20Modal=20System?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ New CollectionSelectionModal Component: - Multi-select collection interface with search functionality - Visual card previews showing selected cards to add - Select all/deselect all functionality for bulk operations - Real-time collection filtering and search - Professional modal design with dark mode support 🔗 Integration with Cards Page: - Bulk selection toolbar now opens collection modal - Individual card actions trigger collection modal - Success feedback with detailed results - Automatic selection clearing after successful additions 🎮 Enhanced Card Detail Page: - Replaced old single-select dropdown with new modal - Multi-collection support for single cards - Automatic refresh of card collections after additions - Consistent UI across all card interaction points 🎨 User Experience Features: - Visual card thumbnails in modal header - Collection metadata display (card count, public status) - Loading states and error handling - Responsive design for all screen sizes Ready for seamless card-to-collection workflow! 📦✨ --- components/CollectionSelectionModal.js | 346 +++++++++++++++++++++++++ pages/card/[id].js | 85 ++---- pages/cards.js | 34 ++- 3 files changed, 402 insertions(+), 63 deletions(-) create mode 100644 components/CollectionSelectionModal.js diff --git a/components/CollectionSelectionModal.js b/components/CollectionSelectionModal.js new file mode 100644 index 0000000..eb7380a --- /dev/null +++ b/components/CollectionSelectionModal.js @@ -0,0 +1,346 @@ +import { useState, useEffect } from 'react'; + +export default function CollectionSelectionModal({ + isOpen, + onClose, + cards, // Array of cards to add + onAddToCollections +}) { + const [collections, setCollections] = useState([]); + const [selectedCollections, setSelectedCollections] = useState([]); + const [loading, setLoading] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + useEffect(() => { + if (isOpen) { + fetchCollections(); + setSelectedCollections([]); + setSearchQuery(''); + } + }, [isOpen]); + + const fetchCollections = async () => { + setLoading(true); + try { + const response = await fetch('/api/collections', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth_token')}` + } + }); + + if (response.ok) { + const data = await response.json(); + setCollections(data.collections || []); + } else { + console.error('Failed to fetch collections'); + } + } catch (error) { + console.error('Error fetching collections:', error); + } finally { + setLoading(false); + } + }; + + const handleCollectionToggle = (collectionId) => { + setSelectedCollections(prev => { + if (prev.includes(collectionId)) { + return prev.filter(id => id !== collectionId); + } else { + return [...prev, collectionId]; + } + }); + }; + + const handleSelectAll = () => { + const filteredCollections = getFilteredCollections(); + const allSelected = filteredCollections.every(collection => + selectedCollections.includes(collection.id) + ); + + if (allSelected) { + // Deselect all filtered collections + setSelectedCollections(prev => + prev.filter(id => !filteredCollections.some(c => c.id === id)) + ); + } else { + // Select all filtered collections + const newSelections = filteredCollections + .filter(collection => !selectedCollections.includes(collection.id)) + .map(collection => collection.id); + setSelectedCollections(prev => [...prev, ...newSelections]); + } + }; + + const getFilteredCollections = () => { + if (!searchQuery.trim()) return collections; + + return collections.filter(collection => + collection.name.toLowerCase().includes(searchQuery.toLowerCase()) || + collection.description?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + }; + + const handleSubmit = async () => { + if (selectedCollections.length === 0) return; + + setSubmitting(true); + try { + const results = []; + + // Add cards to each selected collection + for (const collectionId of selectedCollections) { + for (const card of cards) { + const response = await fetch(`/api/collections/${collectionId}/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) { + results.push({ collectionId, cardId: card.id, success: true }); + } else { + results.push({ collectionId, cardId: card.id, success: false }); + } + } + } + + // Call callback with results + onAddToCollections(results, selectedCollections, cards); + onClose(); + } catch (error) { + console.error('Error adding cards to collections:', error); + } finally { + setSubmitting(false); + } + }; + + const filteredCollections = getFilteredCollections(); + const allFilteredSelected = filteredCollections.length > 0 && + filteredCollections.every(collection => selectedCollections.includes(collection.id)); + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+

+ Add to Collections +

+ +
+ + {/* Card Summary */} +
+
+ {cards.slice(0, 3).map((card, index) => ( +
+ {card.image_url ? ( + {card.name} + ) : ( +
+ No Image +
+ )} +
+ ))} + {cards.length > 3 && ( +
+ + +{cards.length - 3} + +
+ )} +
+
+

+ {cards.length} card{cards.length !== 1 ? 's' : ''} selected +

+

+ Choose collections to add {cards.length === 1 ? 'this card' : 'these cards'} to +

+
+
+ + {/* Search */} +
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-2 pl-10 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white" + /> + + + +
+
+ + {/* Collections List */} +
+ {loading ? ( +
+
+

Loading collections...

+
+ ) : filteredCollections.length === 0 ? ( +
+ + + +

+ {searchQuery ? 'No collections match your search' : 'No collections found'} +

+

+ {searchQuery ? 'Try a different search term' : 'Create your first collection to get started'} +

+
+ ) : ( +
+ {/* Select All */} + {filteredCollections.length > 1 && ( +
+ +
+ )} + + {/* Collection Items */} +
+ {filteredCollections.map((collection) => ( +
handleCollectionToggle(collection.id)} + > +
+ handleCollectionToggle(collection.id)} + className="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 focus:ring-2" + /> + + {/* Collection Image */} +
+ {collection.image ? ( + {collection.name} + ) : ( +
+ + + +
+ )} +
+ + {/* Collection Info */} +
+

+ {collection.name} +

+ {collection.description && ( +

+ {collection.description} +

+ )} +
+ + {collection.card_count || 0} cards + + {collection.is_public && ( + + Public + + )} +
+
+
+
+ ))} +
+
+ )} +
+ + {/* Footer */} +
+
+
+ {selectedCollections.length > 0 && ( + + {selectedCollections.length} collection{selectedCollections.length !== 1 ? 's' : ''} selected + + )} +
+ +
+ + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/pages/card/[id].js b/pages/card/[id].js index 4367e9a..aefb234 100644 --- a/pages/card/[id].js +++ b/pages/card/[id].js @@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import Layout from '../../components/Layout'; import { useIsAdmin } from '../../lib/admin-auth'; +import CollectionSelectionModal from '../../components/CollectionSelectionModal'; export default function CardDetail() { const router = useRouter(); @@ -693,66 +694,30 @@ export default function CardDetail() { )} - {/* Collection Modal */} - {showCollectionModal && ( -
-
-

- Add to Collection -

-
- - -
-
- - -
-
-
- )} + {/* Collection Selection Modal */} + setShowCollectionModal(false)} + cards={card ? [card] : []} + onAddToCollections={(results, selectedCollectionIds, cards) => { + const successCount = results.filter(r => r.success).length; + if (successCount > 0) { + // Refresh card collections + const fetchCardCollections = async () => { + try { + const response = await fetch(`/api/cards/${id}/collections`); + if (response.ok) { + const data = await response.json(); + setCardCollections(data); + } + } catch (error) { + console.error('Error refreshing card collections:', error); + } + }; + fetchCardCollections(); + } + }} + /> {/* Deck Modal */} {showDeckModal && ( diff --git a/pages/cards.js b/pages/cards.js index 8f39d26..a8478ea 100644 --- a/pages/cards.js +++ b/pages/cards.js @@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; import Layout from '../components/Layout'; import CardItem from '../components/CardItem'; import BulkSelectionToolbar from '../components/BulkSelectionToolbar'; +import CollectionSelectionModal from '../components/CollectionSelectionModal'; export default function Cards() { const router = useRouter(); @@ -38,6 +39,10 @@ export default function Cards() { // Bulk selection state const [selectedCards, setSelectedCards] = useState([]); const [favoritedCards, setFavoritedCards] = useState(new Set()); + + // Modal states + const [showCollectionModal, setShowCollectionModal] = useState(false); + const [cardsToAdd, setCardsToAdd] = useState([]); // Fetch cards from database const fetchCards = async (isLoadMore = false) => { @@ -355,8 +360,8 @@ export default function Cards() { // Bulk action handlers const handleBulkAddToCollection = (cards) => { - console.log('Adding to collection:', cards); - alert(`Adding ${cards.length} cards to collection (functionality coming soon)`); + setCardsToAdd(cards); + setShowCollectionModal(true); }; const handleBulkAddToDeck = (cards) => { @@ -409,6 +414,21 @@ export default function 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} collection${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 ( @@ -707,7 +727,7 @@ export default function Cards() { viewMode={viewMode} isSelected={selectedCards.some(c => c.id === card.id)} onToggleSelect={handleToggleSelect} - onAddToCollection={handleBulkAddToCollection} + onAddToCollection={(card) => handleBulkAddToCollection([card])} onAddToDeck={handleBulkAddToDeck} onToggleFavorite={handleToggleFavorite} isFavorited={favoritedCards.has(card.id)} @@ -763,6 +783,14 @@ export default function Cards() { onBulkFavorite={handleBulkFavorite} onBulkDelete={handleBulkDelete} /> + + {/* Collection Selection Modal */} + setShowCollectionModal(false)} + cards={cardsToAdd} + onAddToCollections={handleAddToCollections} + /> ); }