Enhanced Collection Detail Page

🎯 Edit/Delete Functionality:
- Edit/Delete buttons now visible in collection header
- Hidden for system collections (All My Cards)
- Only shown for collection owners
- Proper permission checks in place

🃏 Consistent Card Display:
- Replaced basic card tiles with full CardItem components
- Same hover effects and interactions as /cards page
- Selection, favorites, and action buttons work
- Responsive grid layout (2-7 columns based on screen size)
- Proper card interactions (favorite, select, add to collection/deck)

🔒 System Collection Styling:
- Added prominent SYSTEM badge in collection header
- Informative tooltip explaining auto-sync behavior
- Consistent styling with collections list page
- Clear visual distinction from regular collections

🎨 UI/UX Improvements:
- Better responsive grid layout for cards
- Proper state management for card interactions
- Consistent theming and styling
- Enhanced user feedback and visual hierarchy

Cards in collections now have the same rich interactions as the main cards page! 🚀
This commit is contained in:
Randall Stillwell 2025-07-27 20:58:18 -05:00
parent fba8af1fe1
commit 7d385d1fa7

View file

@ -4,6 +4,7 @@ import Link from 'next/link';
import UploadImageModal from '../../components/UploadImageModal'; import UploadImageModal from '../../components/UploadImageModal';
import ShareModal from '../../components/ShareModal'; import ShareModal from '../../components/ShareModal';
import CollaboratorFacepile from '../../components/CollaboratorFacepile'; import CollaboratorFacepile from '../../components/CollaboratorFacepile';
import CardItem from '../../components/CardItem';
import Layout from '../../components/Layout'; import Layout from '../../components/Layout';
import { useAuth } from '../../lib/use-auth'; import { useAuth } from '../../lib/use-auth';
@ -43,9 +44,14 @@ export default function CollectionView() {
const [showSearchResults, setShowSearchResults] = useState(false); const [showSearchResults, setShowSearchResults] = useState(false);
const [showUploadModal, setShowUploadModal] = useState(false); const [showUploadModal, setShowUploadModal] = useState(false);
// Card interaction states
const [selectedCards, setSelectedCards] = useState([]);
const [favoritedCards, setFavoritedCards] = useState(new Set());
useEffect(() => { useEffect(() => {
if (identifier && user) { if (identifier && user) {
fetchCollectionData(); fetchCollectionData();
loadFavoritedCards();
} }
}, [identifier, user]); }, [identifier, user]);
@ -317,6 +323,83 @@ export default function CollectionView() {
} }
}; };
// 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);
};
// 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);
}
};
const handleDownloadCSV = () => { const handleDownloadCSV = () => {
if (cards.length === 0) { if (cards.length === 0) {
alert('No cards to download'); alert('No cards to download');
@ -453,8 +536,8 @@ export default function CollectionView() {
{/* Action buttons */} {/* Action buttons */}
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{/* Edit and Delete buttons - only show for owner */} {/* Edit and Delete buttons - only show for owner and non-system collections */}
{collection.userRole === 'owner' && ( {collection.userRole === 'owner' && !collection.isSystemCollection && (
<> <>
<button <button
onClick={() => setShowEditModal(true)} onClick={() => setShowEditModal(true)}
@ -494,9 +577,28 @@ export default function CollectionView() {
{/* Collection Info */} {/* Collection Info */}
<div className="mb-6"> <div className="mb-6">
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}> <div className="flex items-center space-x-3 mb-2">
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
{collection.name} {collection.name}
</h1> </h1>
{/* System collection indicator */}
{collection.isSystemCollection && (
<div className="flex items-center space-x-1">
<span className="px-2.5 py-1 text-xs font-bold rounded-full bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm border-2 border-blue-200">
🔒 SYSTEM
</span>
<div className="group relative">
<svg className="h-4 w-4 text-blue-500 cursor-help" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-2 text-xs bg-gray-900 text-white rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap">
Automatically syncs with your owned cards
<div className="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900"></div>
</div>
</div>
</div>
)}
</div>
<p className="text-lg mb-4" style={{ color: 'var(--text-secondary)' }}> <p className="text-lg mb-4" style={{ color: 'var(--text-secondary)' }}>
{collection.description} {collection.description}
</p> </p>
@ -712,26 +814,19 @@ export default function CollectionView() {
</h2> </h2>
</div> </div>
<div className="grid grid-cols-7 gap-4"> <div className="card-grid-container grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 gap-3 sm:gap-4 lg:gap-6">
{gameCards.map((card, index) => ( {gameCards.map((card, index) => (
<div key={card.id || index} className="aspect-[2.5/3.5] bg-gray-200 rounded-lg flex items-center justify-center text-gray-500 text-sm font-medium hover:bg-gray-300 transition-colors cursor-pointer"> <CardItem
{card.image_url ? ( key={card.id || index}
<img card={card}
src={card.image_url} viewMode={viewMode}
alt={card.name} isSelected={selectedCards.some(c => c.id === card.id)}
className="w-full h-full object-cover rounded-lg" onToggleSelect={handleToggleSelect}
onAddToCollection={handleAddToCollection}
onAddToDeck={handleAddToDeck}
onToggleFavorite={handleToggleFavorite}
isFavorited={favoritedCards.has(card.id)}
/> />
) : (
'Card'
)}
</div>
))}
{/* Add empty card slots to fill the row */}
{Array.from({ length: Math.max(0, 7 - (gameCards.length % 7)) }, (_, index) => (
<div key={`empty-${index}`} className="aspect-[2.5/3.5] bg-gray-200 rounded-lg flex items-center justify-center text-gray-500 text-sm font-medium hover:bg-gray-300 transition-colors cursor-pointer">
Card
</div>
))} ))}
</div> </div>
</div> </div>