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 ShareModal from '../../components/ShareModal';
import CollaboratorFacepile from '../../components/CollaboratorFacepile';
import CardItem from '../../components/CardItem';
import Layout from '../../components/Layout';
import { useAuth } from '../../lib/use-auth';
@ -43,9 +44,14 @@ export default function CollectionView() {
const [showSearchResults, setShowSearchResults] = useState(false);
const [showUploadModal, setShowUploadModal] = useState(false);
// Card interaction states
const [selectedCards, setSelectedCards] = useState([]);
const [favoritedCards, setFavoritedCards] = useState(new Set());
useEffect(() => {
if (identifier && user) {
fetchCollectionData();
loadFavoritedCards();
}
}, [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 = () => {
if (cards.length === 0) {
alert('No cards to download');
@ -453,8 +536,8 @@ export default function CollectionView() {
{/* Action buttons */}
<div className="flex items-center space-x-3">
{/* Edit and Delete buttons - only show for owner */}
{collection.userRole === 'owner' && (
{/* Edit and Delete buttons - only show for owner and non-system collections */}
{collection.userRole === 'owner' && !collection.isSystemCollection && (
<>
<button
onClick={() => setShowEditModal(true)}
@ -494,9 +577,28 @@ export default function CollectionView() {
{/* Collection Info */}
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
{collection.name}
</h1>
<div className="flex items-center space-x-3 mb-2">
<h1 className="text-3xl font-bold" style={{ color: 'var(--text-primary)' }}>
{collection.name}
</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)' }}>
{collection.description}
</p>
@ -712,26 +814,19 @@ export default function CollectionView() {
</h2>
</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) => (
<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">
{card.image_url ? (
<img
src={card.image_url}
alt={card.name}
className="w-full h-full object-cover rounded-lg"
/>
) : (
'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>
<CardItem
key={card.id || index}
card={card}
viewMode={viewMode}
isSelected={selectedCards.some(c => c.id === card.id)}
onToggleSelect={handleToggleSelect}
onAddToCollection={handleAddToCollection}
onAddToDeck={handleAddToDeck}
onToggleFavorite={handleToggleFavorite}
isFavorited={favoritedCards.has(card.id)}
/>
))}
</div>
</div>