✨ New Components: - BulkSelectionToolbar: Floating bottom toolbar with selection count and actions - CardItem: Modular card component with selection checkboxes and individual actions 🎯 Features: - Multi-select cards with checkboxes (grid and list view) - Floating toolbar shows selected count and quick actions - Primary actions: Add to Collection, Add to Deck, Mark as Owned - Secondary actions: Add to Favorites, Remove from Owned, Bulk Delete - Individual card actions: Favorite, Add to Collection, Add to Deck - Visual feedback: Selected cards get purple border and ring - Hover effects: Quick actions appear on card hover 🎨 UX Improvements: - Professional selection states with purple theming - Smooth animations and transitions - Responsive design for both grid and list views - Clear selection button and action confirmations - Tooltips and visual indicators Ready for bulk card management! 📦✨
237 lines
No EOL
9.3 KiB
JavaScript
237 lines
No EOL
9.3 KiB
JavaScript
import { useState } from 'react';
|
|
|
|
export default function CardItem({
|
|
card,
|
|
viewMode = 'grid',
|
|
isSelected = false,
|
|
onToggleSelect,
|
|
onAddToCollection,
|
|
onAddToDeck,
|
|
onToggleFavorite,
|
|
isFavorited = false
|
|
}) {
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
const handleSelectChange = (e) => {
|
|
e.stopPropagation();
|
|
onToggleSelect(card);
|
|
};
|
|
|
|
const handleImageError = () => {
|
|
setImageError(true);
|
|
};
|
|
|
|
if (viewMode === 'list') {
|
|
return (
|
|
<div className={`flex items-center p-4 border rounded-lg transition-all duration-200 ${
|
|
isSelected
|
|
? 'border-purple-500 bg-purple-50 shadow-md'
|
|
: 'border-gray-200 hover:border-gray-300 hover:shadow-sm'
|
|
}`}>
|
|
{/* Selection Checkbox */}
|
|
<div className="flex-shrink-0 mr-4">
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={handleSelectChange}
|
|
className="w-4 h-4 text-purple-600 bg-gray-100 border-gray-300 rounded focus:ring-purple-500 focus:ring-2"
|
|
/>
|
|
</div>
|
|
|
|
{/* Card Image */}
|
|
<div className="flex-shrink-0 mr-4">
|
|
<div className="w-16 h-22 bg-gray-200 rounded-lg overflow-hidden">
|
|
{!imageError ? (
|
|
<img
|
|
src={card.image_url}
|
|
alt={card.name}
|
|
className="w-full h-full object-cover"
|
|
onError={handleImageError}
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center text-gray-400 text-xs">
|
|
No Image
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-gray-900 truncate">{card.name}</h3>
|
|
<p className="text-sm text-gray-600">{card.set_name} • {card.rarity}</p>
|
|
<p className="text-sm text-gray-500">{card.card_type}</p>
|
|
</div>
|
|
|
|
{/* Game Badge */}
|
|
<div className="flex-shrink-0 mx-4">
|
|
<span className="px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
|
{card.game}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Price */}
|
|
<div className="flex-shrink-0 mx-4">
|
|
<span className="text-lg font-bold text-green-600">
|
|
${card.market_price}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex-shrink-0 flex items-center space-x-2">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleFavorite(card);
|
|
}}
|
|
className={`p-2 rounded-lg transition-colors ${
|
|
isFavorited
|
|
? 'text-red-500 hover:text-red-600'
|
|
: 'text-gray-400 hover:text-red-500'
|
|
}`}
|
|
>
|
|
<svg className="w-5 h-5" fill={isFavorited ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
</button>
|
|
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onAddToCollection(card);
|
|
}}
|
|
className="p-2 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-lg transition-colors"
|
|
title="Add to Collection"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
</button>
|
|
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onAddToDeck(card);
|
|
}}
|
|
className="p-2 text-green-600 hover:text-green-700 hover:bg-green-50 rounded-lg transition-colors"
|
|
title="Add to Deck"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Grid view
|
|
return (
|
|
<div className={`relative group bg-white rounded-xl border transition-all duration-200 overflow-hidden ${
|
|
isSelected
|
|
? 'border-purple-500 shadow-lg ring-2 ring-purple-200'
|
|
: 'border-gray-200 hover:border-gray-300 hover:shadow-md'
|
|
}`}>
|
|
{/* Selection Checkbox */}
|
|
<div className="absolute top-3 left-3 z-10">
|
|
<input
|
|
type="checkbox"
|
|
checked={isSelected}
|
|
onChange={handleSelectChange}
|
|
className="w-4 h-4 text-purple-600 bg-white border-gray-300 rounded focus:ring-purple-500 focus:ring-2 shadow-sm"
|
|
/>
|
|
</div>
|
|
|
|
{/* Favorite Button */}
|
|
<div className="absolute top-3 right-3 z-10">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onToggleFavorite(card);
|
|
}}
|
|
className={`p-2 rounded-full transition-all duration-200 ${
|
|
isFavorited
|
|
? 'text-red-500 bg-white shadow-sm'
|
|
: 'text-gray-400 bg-white/80 hover:text-red-500 hover:bg-white shadow-sm opacity-0 group-hover:opacity-100'
|
|
}`}
|
|
>
|
|
<svg className="w-4 h-4" fill={isFavorited ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Card Image */}
|
|
<div className="aspect-[2.5/3.5] bg-gray-200 overflow-hidden">
|
|
{!imageError ? (
|
|
<img
|
|
src={card.image_url}
|
|
alt={card.name}
|
|
className="w-full h-full object-cover transition-transform duration-200 group-hover:scale-105"
|
|
onError={handleImageError}
|
|
/>
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center text-gray-400">
|
|
<div className="text-center">
|
|
<svg className="w-12 h-12 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
<p className="text-xs">No Image</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Card Info */}
|
|
<div className="p-4">
|
|
<div className="flex items-start justify-between mb-2">
|
|
<h3 className="font-semibold text-gray-900 text-sm leading-tight line-clamp-2 flex-1 mr-2">
|
|
{card.name}
|
|
</h3>
|
|
<span className="text-lg font-bold text-green-600 flex-shrink-0">
|
|
${card.market_price}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-xs text-gray-600 mb-1">{card.set_name}</p>
|
|
<p className="text-xs text-gray-500 mb-3">{card.rarity} • {card.card_type}</p>
|
|
|
|
{/* Game Badge */}
|
|
<div className="flex items-center justify-between">
|
|
<span className="px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
|
{card.game}
|
|
</span>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onAddToCollection(card);
|
|
}}
|
|
className="p-1.5 text-blue-600 hover:text-blue-700 hover:bg-blue-50 rounded-md transition-colors"
|
|
title="Add to Collection"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
</button>
|
|
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onAddToDeck(card);
|
|
}}
|
|
className="p-1.5 text-green-600 hover:text-green-700 hover:bg-green-50 rounded-md transition-colors"
|
|
title="Add to Deck"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|