✨ Redesigned Card Interaction with Sliding Information Panel
🎨 New Card-Focused Design: - Cards now display as clean images without permanent overlays - Information and actions slide out from behind the card on hover - Selected cards scale up slightly and show persistent info panel 🚀 Enhanced User Experience: - Card images are the primary focus, no visual clutter - Smooth 300ms transitions with easing for professional feel - Sliding panel appears 2px below card for depth effect - Backdrop blur effect for better contrast and readability ⚡ Improved Interactions: - Primary actions (Collection, Deck) prominently displayed - Secondary actions (Own, Favorite, View) in compact grid - Proper z-index management prevents panel conflicts - Increased grid spacing (gap-8, p-8) for sliding panels 🎯 Technical Improvements: - CSS classes for consistent styling and performance - Prevented text selection during interactions - Transform origin optimization for smooth animations - Proper pointer event handling for accessibility Cards are now the star of the show! 🌟✨
This commit is contained in:
parent
e2e5e5bcab
commit
a6602eab87
3 changed files with 202 additions and 96 deletions
|
|
@ -127,108 +127,174 @@ export default function CardItem({
|
|||
|
||||
// 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'
|
||||
<div className={`card-item-container relative group bg-transparent rounded-xl transition-all duration-300 overflow-visible ${
|
||||
isSelected ? 'selected scale-105' : ''
|
||||
}`}>
|
||||
{/* 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}
|
||||
{/* Main Card Container */}
|
||||
<div className={`relative bg-white rounded-xl border transition-all duration-300 overflow-hidden ${
|
||||
isSelected
|
||||
? 'border-purple-500 shadow-xl ring-2 ring-purple-200'
|
||||
: 'border-gray-200 group-hover:border-gray-300'
|
||||
}`}>
|
||||
{/* Selection Checkbox */}
|
||||
<div className="absolute top-3 left-3 z-20">
|
||||
<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 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>
|
||||
{/* Favorite Button */}
|
||||
<div className="absolute top-3 right-3 z-20">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleFavorite(card);
|
||||
}}
|
||||
className={`p-2 rounded-full transition-all duration-300 ${
|
||||
isFavorited
|
||||
? 'text-red-500 bg-white shadow-sm'
|
||||
: 'text-gray-400 bg-white/90 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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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-300 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>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
{/* Sliding Information Panel */}
|
||||
<div className={`card-slide-panel absolute inset-0 bg-white rounded-xl border border-gray-200 shadow-xl transition-all duration-300 ease-out z-10 ${
|
||||
isSelected
|
||||
? 'translate-y-2 opacity-100'
|
||||
: 'translate-y-0 opacity-0 group-hover:translate-y-2 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto'
|
||||
}`}>
|
||||
{/* Card Info Content */}
|
||||
<div className="p-4 h-full flex flex-col justify-between">
|
||||
{/* Top Section - Card Details */}
|
||||
<div>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<h3 className="font-bold text-gray-900 text-sm leading-tight line-clamp-2 flex-1 mr-2">
|
||||
{card.name}
|
||||
</h3>
|
||||
<span className="text-xl font-bold text-green-600 flex-shrink-0">
|
||||
${card.market_price}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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 className="space-y-1 mb-4">
|
||||
<p className="text-sm text-gray-700 font-medium">{card.set_name}</p>
|
||||
<p className="text-sm text-gray-600">{card.rarity} • {card.card_type}</p>
|
||||
</div>
|
||||
|
||||
{/* Game Badge */}
|
||||
<div className="mb-4">
|
||||
<span className="px-3 py-1 text-sm font-medium rounded-full bg-blue-100 text-blue-800">
|
||||
{card.game}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Section - Actions */}
|
||||
<div className="space-y-3">
|
||||
{/* Primary Actions */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddToCollection([card]);
|
||||
}}
|
||||
className="flex items-center justify-center space-x-2 px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium"
|
||||
>
|
||||
<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>
|
||||
<span>Collection</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAddToDeck([card]);
|
||||
}}
|
||||
className="flex items-center justify-center space-x-2 px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
|
||||
>
|
||||
<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>
|
||||
<span>Deck</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Secondary Actions */}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Mark as owned functionality
|
||||
alert('Mark as owned (coming soon)');
|
||||
}}
|
||||
className="flex items-center justify-center px-2 py-2 bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors text-xs font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggleFavorite(card);
|
||||
}}
|
||||
className={`flex items-center justify-center px-2 py-2 rounded-lg transition-colors text-xs font-medium ${
|
||||
isFavorited
|
||||
? 'bg-red-100 text-red-700 hover:bg-red-200'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// View details functionality
|
||||
window.open(`/card/${card.id}`, '_blank');
|
||||
}}
|
||||
className="flex items-center justify-center px-2 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors text-xs font-medium"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -699,7 +699,7 @@ export default function Cards() {
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={viewMode === 'grid' ? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 gap-6 p-6' : 'space-y-4 p-6'}>
|
||||
<div className={viewMode === 'grid' ? '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-8 gap-8 p-8' : 'space-y-4 p-6'}>
|
||||
{cards.map(card => (
|
||||
<CardItem
|
||||
key={card.id}
|
||||
|
|
|
|||
|
|
@ -333,4 +333,44 @@ body {
|
|||
0 0 20px rgba(168, 85, 247, 0.4),
|
||||
0 0 40px rgba(168, 85, 247, 0.2),
|
||||
0 0 60px rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
/* Card Sliding Effect Styles */
|
||||
.card-item-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-item-container:hover {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.card-item-container.selected {
|
||||
z-index: 60;
|
||||
}
|
||||
|
||||
/* Smooth transitions for card interactions */
|
||||
.card-slide-panel {
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
/* Ensure proper layering during animations */
|
||||
.card-grid-container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Prevent text selection during card interactions */
|
||||
.card-item-container * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Add subtle backdrop blur effect for sliding panels */
|
||||
.card-slide-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(2px);
|
||||
border-radius: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
Loading…
Reference in a new issue