Enhance DeckCard and DeckList components for improved layout and responsiveness
This commit is contained in:
@@ -22,42 +22,38 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-gray-800 rounded-xl overflow-hidden shadow-lg card-hover cursor-pointer animate-scale-in"
|
className="bg-gray-800 rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-all cursor-pointer group"
|
||||||
onClick={() => onEdit?.(deck.id)}
|
onClick={() => onEdit?.(deck.id)}
|
||||||
>
|
>
|
||||||
<div className="relative h-32 sm:h-40 md:h-48 overflow-hidden">
|
{/* Full Card Art */}
|
||||||
|
<div className="relative aspect-[5/7] overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src={commander?.image_uris?.normal || deck.cards[0]?.card.image_uris?.normal}
|
src={commander?.image_uris?.normal || deck.cards[0]?.card.image_uris?.normal}
|
||||||
alt={commander?.name || deck.cards[0]?.card.name}
|
alt={commander?.name || deck.cards[0]?.card.name}
|
||||||
className="w-full object-cover object-top transform translate-y-[-12%]"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-gray-900 to-transparent" />
|
{/* Overlay for text readability */}
|
||||||
</div>
|
<div className="absolute inset-0 bg-gradient-to-t from-gray-900 via-gray-900/60 to-transparent" />
|
||||||
|
|
||||||
<div className="p-4">
|
{/* Deck info overlay */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="absolute bottom-0 left-0 right-0 p-3">
|
||||||
<h3 className="text-xl font-bold text-white">{deck.name}</h3>
|
<div className="flex items-start justify-between mb-1">
|
||||||
|
<h3 className="text-base sm:text-lg font-bold text-white line-clamp-2 flex-1">{deck.name}</h3>
|
||||||
{validation.isValid ? (
|
{validation.isValid ? (
|
||||||
<div className="flex items-center text-green-400">
|
<Check size={16} className="text-green-400 ml-2 flex-shrink-0" />
|
||||||
<Check size={16} className="mr-1" />
|
|
||||||
<span className="text-sm">Legal</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center text-yellow-400" title={validation.errors.join(', ')}>
|
<AlertTriangle size={16} className="text-yellow-400 ml-2 flex-shrink-0" title={validation.errors.join(', ')} />
|
||||||
<AlertTriangle size={16} className="mr-1" />
|
|
||||||
<span className="text-sm">Issues</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm text-gray-400">
|
<div className="flex items-center justify-between text-xs text-gray-300 mb-2">
|
||||||
<span className="capitalize">{deck.format}</span>
|
<span className="capitalize">{deck.format}</span>
|
||||||
<span>{deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards</span>
|
<span>{deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{commander && (
|
{commander && (
|
||||||
<div className="mt-2 text-sm text-gray-300">
|
<div className="text-xs text-blue-300 mb-2 truncate">
|
||||||
<span className="text-blue-400">Commander:</span> {commander.name}
|
<span className="font-semibold">Commander:</span> {commander.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -66,12 +62,13 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onEdit?.(deck.id);
|
onEdit?.(deck.id);
|
||||||
}}
|
}}
|
||||||
className="mt-4 w-full min-h-[44px] px-4 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2 text-white btn-ripple transition-smooth glow-on-hover"
|
className="w-full min-h-[36px] px-3 py-2 bg-blue-600/90 hover:bg-blue-600 rounded-md flex items-center justify-center gap-2 text-white text-sm font-medium transition-colors backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
<Edit size={20} />
|
<Edit size={16} />
|
||||||
Edit Deck
|
<span>Edit</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ const DeckList = ({ onDeckEdit, onCreateDeck }: DeckListProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 sm:gap-4">
|
||||||
{decks.map((deck) => (
|
{decks.map((deck) => (
|
||||||
<DeckCard key={deck.id} deck={deck} onEdit={onDeckEdit} />
|
<DeckCard key={deck.id} deck={deck} onEdit={onDeckEdit} />
|
||||||
))}
|
))}
|
||||||
@@ -98,15 +98,15 @@ const DeckList = ({ onDeckEdit, onCreateDeck }: DeckListProps) => {
|
|||||||
{/* Create New Deck Card */}
|
{/* Create New Deck Card */}
|
||||||
<button
|
<button
|
||||||
onClick={onCreateDeck}
|
onClick={onCreateDeck}
|
||||||
className="bg-gray-800 rounded-xl overflow-hidden shadow-lg hover:shadow-2xl border-2 border-dashed border-gray-600 hover:border-blue-500 transition-all duration-300 hover:scale-105 cursor-pointer group min-h-[300px] flex flex-col items-center justify-center gap-4 p-8"
|
className="bg-gray-800 rounded-lg overflow-hidden shadow-lg hover:shadow-xl border-2 border-dashed border-gray-600 hover:border-blue-500 transition-all duration-300 hover:scale-105 cursor-pointer group aspect-[5/7] flex flex-col items-center justify-center gap-3 p-4"
|
||||||
>
|
>
|
||||||
<PlusCircle size={64} className="text-gray-600 group-hover:text-blue-500 transition-colors" />
|
<PlusCircle size={48} className="text-gray-600 group-hover:text-blue-500 transition-colors" />
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-xl font-bold text-gray-400 group-hover:text-blue-400 transition-colors">
|
<h3 className="text-sm sm:text-base font-bold text-gray-400 group-hover:text-blue-400 transition-colors">
|
||||||
Create New Deck
|
Create New Deck
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
<p className="text-xs text-gray-500 mt-1 hidden sm:block">
|
||||||
Start building your collection
|
Start building
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ const suggestLandCountAndDistribution = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
||||||
|
const [currentDeckId, setCurrentDeckId] = useState<string | null>(initialDeck?.id || null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [searchResults, setSearchResults] = useState<Card[]>([]);
|
const [searchResults, setSearchResults] = useState<Card[]>([]);
|
||||||
const [selectedCards, setSelectedCards] = useState<{
|
const [selectedCards, setSelectedCards] = useState<{
|
||||||
@@ -310,8 +311,9 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
|
const deckId = currentDeckId || crypto.randomUUID();
|
||||||
const deckToSave: Deck = {
|
const deckToSave: Deck = {
|
||||||
id: initialDeck?.id || crypto.randomUUID(),
|
id: deckId,
|
||||||
name: deckName,
|
name: deckName,
|
||||||
format: deckFormat,
|
format: deckFormat,
|
||||||
cards: selectedCards,
|
cards: selectedCards,
|
||||||
@@ -337,9 +339,14 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
|
|
||||||
if (deckError) throw deckError;
|
if (deckError) throw deckError;
|
||||||
|
|
||||||
|
// Update current deck ID if this was a new deck
|
||||||
|
if (!currentDeckId) {
|
||||||
|
setCurrentDeckId(deckId);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete existing cards if updating
|
// Delete existing cards if updating
|
||||||
if (initialDeck) {
|
if (currentDeckId) {
|
||||||
await supabase.from('deck_cards').delete().eq('deck_id', initialDeck.id);
|
await supabase.from('deck_cards').delete().eq('deck_id', currentDeckId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the deck cards
|
// Save the deck cards
|
||||||
@@ -495,30 +502,38 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||||
{/* Card Search Section */}
|
{/* Card Search Section */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
<form onSubmit={handleSearch} className="flex gap-2">
|
{/* Mobile-First Search Bar */}
|
||||||
<div className="relative flex-1">
|
<form onSubmit={handleSearch} className="relative">
|
||||||
<Search
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
|
||||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={e => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white"
|
className="w-full pl-10 pr-24 py-3 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 text-white"
|
||||||
placeholder="Search for cards..."
|
placeholder="Rechercher une carte..."
|
||||||
/>
|
/>
|
||||||
</div>
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setSearchResults([]);
|
||||||
|
}}
|
||||||
|
className="absolute right-14 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<XCircle size={20} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="min-h-[44px] px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center gap-2"
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 bg-blue-600 hover:bg-blue-700 rounded-md"
|
||||||
>
|
>
|
||||||
<Search size={20} />
|
<Search size={20} />
|
||||||
<span className="hidden sm:inline">Search</span>
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
{/* Vertical Card List for Mobile */}
|
||||||
|
<div className="space-y-2">
|
||||||
{searchResults.map(card => {
|
{searchResults.map(card => {
|
||||||
const currentFaceIndex = getCurrentFaceIndex(card.id);
|
const currentFaceIndex = getCurrentFaceIndex(card.id);
|
||||||
const isMultiFaced = isDoubleFaced(card);
|
const isMultiFaced = isDoubleFaced(card);
|
||||||
@@ -532,17 +547,18 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={card.id}
|
key={card.id}
|
||||||
className="bg-gray-800 rounded-lg overflow-hidden hover:ring-2 hover:ring-blue-500 transition-all"
|
className="bg-gray-800 rounded-lg p-3 flex items-center gap-3 hover:bg-gray-750 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
{/* Card Thumbnail */}
|
||||||
|
<div className="relative flex-shrink-0 w-16 h-22 rounded overflow-hidden">
|
||||||
{getCardImageUri(card, currentFaceIndex) ? (
|
{getCardImageUri(card, currentFaceIndex) ? (
|
||||||
<img
|
<img
|
||||||
src={getCardImageUri(card, currentFaceIndex)}
|
src={getCardImageUri(card, currentFaceIndex)}
|
||||||
alt={displayName}
|
alt={displayName}
|
||||||
className="w-full h-auto"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MagicCard card={card} />
|
<div className="w-full h-full bg-gray-700" />
|
||||||
)}
|
)}
|
||||||
{isMultiFaced && (
|
{isMultiFaced && (
|
||||||
<button
|
<button
|
||||||
@@ -550,38 +566,45 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleCardFace(card.id, card.card_faces!.length);
|
toggleCardFace(card.id, card.card_faces!.length);
|
||||||
}}
|
}}
|
||||||
className="absolute bottom-2 right-2 bg-purple-600 hover:bg-purple-700 text-white p-2 rounded-full shadow-lg transition-all"
|
className="absolute bottom-0 right-0 bg-purple-600 text-white p-1 rounded-tl"
|
||||||
title="Flip card"
|
|
||||||
>
|
>
|
||||||
<RefreshCw size={16} />
|
<RefreshCw size={10} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
{/* Card Info */}
|
||||||
<h3 className="font-bold">{displayName}</h3>
|
<div className="flex-1 min-w-0">
|
||||||
{inCollection > 0 && (
|
<h3 className="font-medium text-sm truncate">{displayName}</h3>
|
||||||
<span className="text-xs bg-green-600 px-2 py-0.5 rounded-full flex items-center gap-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<CheckCircle size={12} />
|
{card.mana_cost && (
|
||||||
x{inCollection}
|
<div className="text-xs text-gray-400">{card.mana_cost}</div>
|
||||||
</span>
|
)}
|
||||||
|
{card.prices?.usd && (
|
||||||
|
<div className="text-xs text-gray-400">${card.prices.usd}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{card.prices?.usd && (
|
{inCollection > 0 && (
|
||||||
<div className="text-sm text-gray-400 mb-2">${card.prices.usd}</div>
|
<div className="text-xs text-green-400 mt-1">
|
||||||
|
<CheckCircle size={12} className="inline mr-1" />
|
||||||
|
x{inCollection} in collection
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2">
|
</div>
|
||||||
|
|
||||||
|
{/* Add Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => addCardToDeck(card)}
|
onClick={() => addCardToDeck(card)}
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2"
|
className="flex-shrink-0 w-10 h-10 bg-blue-600 hover:bg-blue-700 rounded-full flex items-center justify-center transition-colors"
|
||||||
>
|
>
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
Add to Deck
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Add to Collection Button (hidden on mobile by default) */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAddCardToCollection(card.id, 1)}
|
onClick={() => handleAddCardToCollection(card.id, 1)}
|
||||||
disabled={isAddingThisCard}
|
disabled={isAddingThisCard}
|
||||||
className="px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg flex items-center justify-center gap-2"
|
className="hidden sm:flex flex-shrink-0 w-10 h-10 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-full items-center justify-center transition-colors"
|
||||||
title="Add to collection"
|
title="Add to collection"
|
||||||
>
|
>
|
||||||
{isAddingThisCard ? (
|
{isAddingThisCard ? (
|
||||||
@@ -591,8 +614,6 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user