improve collection and deck manager
This commit is contained in:
@@ -82,7 +82,7 @@ define(['./workbox-ca84f546'], (function (workbox) { 'use strict';
|
|||||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.obrcsn1e2cs"
|
"revision": "0.8gqbvnk5km4"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ export default function Collection() {
|
|||||||
<div className="p-4 sm:p-6">
|
<div className="p-4 sm:p-6">
|
||||||
|
|
||||||
{/* Card Image */}
|
{/* Card Image */}
|
||||||
<div className="relative mb-4">
|
<div className="relative mb-4 max-w-sm mx-auto">
|
||||||
<img
|
<img
|
||||||
src={getCardLargeImageUri(selectedCard.card, currentFaceIndex)}
|
src={getCardLargeImageUri(selectedCard.card, currentFaceIndex)}
|
||||||
alt={displayName}
|
alt={displayName}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Plus, Minus, Search, Save, Trash2, Loader2, CheckCircle, XCircle, AlertCircle, PackagePlus, RefreshCw } from 'lucide-react';
|
import { Plus, Minus, Search, Save, Trash2, Loader2, CheckCircle, XCircle, AlertCircle, PackagePlus, RefreshCw, X } from 'lucide-react';
|
||||||
import { Card, Deck } from '../types';
|
import { Card, Deck } from '../types';
|
||||||
import { searchCards, getUserCollection, addCardToCollection, addMultipleCardsToCollection } from '../services/api';
|
import { searchCards, getUserCollection, addCardToCollection, addMultipleCardsToCollection } from '../services/api';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
@@ -123,6 +123,8 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
const [addingCardId, setAddingCardId] = useState<string | null>(null);
|
const [addingCardId, setAddingCardId] = useState<string | null>(null);
|
||||||
const [isAddingAll, setIsAddingAll] = useState(false);
|
const [isAddingAll, setIsAddingAll] = useState(false);
|
||||||
const [cardFaceIndex, setCardFaceIndex] = useState<Map<string, number>>(new Map());
|
const [cardFaceIndex, setCardFaceIndex] = useState<Map<string, number>>(new Map());
|
||||||
|
const [hoveredCard, setHoveredCard] = useState<Card | null>(null);
|
||||||
|
const [selectedCard, setSelectedCard] = useState<Card | null>(null);
|
||||||
|
|
||||||
// Load user collection on component mount
|
// Load user collection on component mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -171,6 +173,13 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
return card.image_uris?.normal || card.image_uris?.small || card.card_faces?.[0]?.image_uris?.normal;
|
return card.image_uris?.normal || card.image_uris?.small || card.card_faces?.[0]?.image_uris?.normal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getCardLargeImageUri = (card: Card, faceIndex: number = 0) => {
|
||||||
|
if (isDoubleFaced(card) && card.card_faces) {
|
||||||
|
return card.card_faces[faceIndex]?.image_uris?.large || card.card_faces[faceIndex]?.image_uris?.normal;
|
||||||
|
}
|
||||||
|
return card.image_uris?.large || card.image_uris?.normal;
|
||||||
|
};
|
||||||
|
|
||||||
// Helper function to check if a card is in the collection
|
// Helper function to check if a card is in the collection
|
||||||
const isCardInCollection = (cardId: string, requiredQuantity: number = 1): boolean => {
|
const isCardInCollection = (cardId: string, requiredQuantity: number = 1): boolean => {
|
||||||
const ownedQuantity = userCollection.get(cardId) || 0;
|
const ownedQuantity = userCollection.get(cardId) || 0;
|
||||||
@@ -550,10 +559,14 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={card.id}
|
key={card.id}
|
||||||
className="bg-gray-800 rounded-lg p-3 flex items-center gap-3 hover:bg-gray-750 transition-colors"
|
className="bg-gray-800 rounded-lg p-3 flex items-center gap-3 hover:bg-gray-750 transition-colors cursor-pointer"
|
||||||
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
onClick={() => setSelectedCard(card)}
|
||||||
>
|
>
|
||||||
{/* Card Thumbnail */}
|
{/* Card Thumbnail */}
|
||||||
<div className="relative flex-shrink-0 w-16 h-22 rounded overflow-hidden">
|
<div className="relative flex-shrink-0 w-16 h-22 rounded overflow-hidden"
|
||||||
|
onClick={(e) => e.stopPropagation()}>
|
||||||
{getCardImageUri(card, currentFaceIndex) ? (
|
{getCardImageUri(card, currentFaceIndex) ? (
|
||||||
<img
|
<img
|
||||||
src={getCardImageUri(card, currentFaceIndex)}
|
src={getCardImageUri(card, currentFaceIndex)}
|
||||||
@@ -597,7 +610,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
|
|
||||||
{/* Add/Quantity Controls */}
|
{/* Add/Quantity Controls */}
|
||||||
{quantityInDeck > 0 ? (
|
{quantityInDeck > 0 ? (
|
||||||
<div className="flex-shrink-0 flex items-center gap-1">
|
<div className="flex-shrink-0 flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (quantityInDeck === 1) {
|
if (quantityInDeck === 1) {
|
||||||
@@ -620,7 +633,10 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => addCardToDeck(card)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
addCardToDeck(card);
|
||||||
|
}}
|
||||||
className="flex-shrink-0 w-10 h-10 bg-blue-600 hover:bg-blue-700 rounded-full flex items-center justify-center transition-colors"
|
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} />
|
||||||
@@ -629,7 +645,10 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
|
|
||||||
{/* Add to Collection Button (hidden on mobile by default) */}
|
{/* Add to Collection Button (hidden on mobile by default) */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleAddCardToCollection(card.id, 1)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleAddCardToCollection(card.id, 1);
|
||||||
|
}}
|
||||||
disabled={isAddingThisCard}
|
disabled={isAddingThisCard}
|
||||||
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"
|
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"
|
||||||
@@ -735,7 +754,10 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
{selectedCards.map(({ card, quantity }) => (
|
{selectedCards.map(({ card, quantity }) => (
|
||||||
<div
|
<div
|
||||||
key={card.id}
|
key={card.id}
|
||||||
className="flex items-center gap-3 p-2 rounded-lg bg-gray-700"
|
className="flex items-center gap-3 p-2 rounded-lg bg-gray-700 cursor-pointer hover:bg-gray-650 transition-colors"
|
||||||
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
onClick={() => setSelectedCard(card)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={card.image_uris?.art_crop}
|
src={card.image_uris?.art_crop}
|
||||||
@@ -748,7 +770,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
<div className="text-xs text-gray-400">${card.prices.usd}</div>
|
<div className="text-xs text-gray-400">${card.prices.usd}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={quantity}
|
value={quantity}
|
||||||
@@ -836,6 +858,181 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Hover Card Preview - only show if no card is selected */}
|
||||||
|
{hoveredCard && !selectedCard && (() => {
|
||||||
|
const currentFaceIndex = getCurrentFaceIndex(hoveredCard.id);
|
||||||
|
const isMultiFaced = isDoubleFaced(hoveredCard);
|
||||||
|
const currentFace = isMultiFaced && hoveredCard.card_faces
|
||||||
|
? hoveredCard.card_faces[currentFaceIndex]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const displayName = currentFace?.name || hoveredCard.name;
|
||||||
|
const displayTypeLine = currentFace?.type_line || hoveredCard.type_line;
|
||||||
|
const displayOracleText = currentFace?.oracle_text || hoveredCard.oracle_text;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hidden lg:block fixed top-1/2 right-8 transform -translate-y-1/2 z-40 pointer-events-none">
|
||||||
|
<div className="bg-gray-800 rounded-lg shadow-2xl p-4 max-w-md">
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src={getCardLargeImageUri(hoveredCard, currentFaceIndex)}
|
||||||
|
alt={displayName}
|
||||||
|
className="w-full h-auto rounded-lg shadow-lg"
|
||||||
|
/>
|
||||||
|
{isMultiFaced && (
|
||||||
|
<div className="absolute top-2 right-2 bg-purple-600 text-white text-xs font-bold px-2 py-1 rounded-full shadow-lg">
|
||||||
|
Face {currentFaceIndex + 1}/{hoveredCard.card_faces!.length}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<h3 className="text-xl font-bold">{displayName}</h3>
|
||||||
|
<p className="text-sm text-gray-400">{displayTypeLine}</p>
|
||||||
|
{displayOracleText && (
|
||||||
|
<p className="text-sm text-gray-300 border-t border-gray-700 pt-2">
|
||||||
|
{displayOracleText}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{hoveredCard.prices?.usd && (
|
||||||
|
<div className="text-sm text-green-400 font-semibold border-t border-gray-700 pt-2">
|
||||||
|
${hoveredCard.prices.usd}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Card Detail Panel - slides in from right */}
|
||||||
|
{selectedCard && (() => {
|
||||||
|
const currentFaceIndex = getCurrentFaceIndex(selectedCard.id);
|
||||||
|
const isMultiFaced = isDoubleFaced(selectedCard);
|
||||||
|
const currentFace = isMultiFaced && selectedCard.card_faces
|
||||||
|
? selectedCard.card_faces[currentFaceIndex]
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const displayName = currentFace?.name || selectedCard.name;
|
||||||
|
const displayTypeLine = currentFace?.type_line || selectedCard.type_line;
|
||||||
|
const displayOracleText = currentFace?.oracle_text || selectedCard.oracle_text;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300"
|
||||||
|
onClick={() => setSelectedCard(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Sliding Panel */}
|
||||||
|
<div className="fixed top-0 right-0 h-full w-full md:w-96 bg-gray-800 shadow-2xl z-50 overflow-y-auto animate-slide-in-right">
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedCard(null)}
|
||||||
|
className="fixed top-4 right-4 bg-gray-700 hover:bg-gray-600 text-white p-2 md:p-1.5 rounded-full transition-colors z-[60] shadow-lg"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<X size={24} className="md:w-5 md:h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="p-4 sm:p-6">
|
||||||
|
{/* Card Image */}
|
||||||
|
<div className="relative mb-4 max-w-sm mx-auto">
|
||||||
|
<img
|
||||||
|
src={getCardLargeImageUri(selectedCard, currentFaceIndex)}
|
||||||
|
alt={displayName}
|
||||||
|
className="w-full h-auto rounded-lg shadow-lg"
|
||||||
|
/>
|
||||||
|
{isMultiFaced && (
|
||||||
|
<>
|
||||||
|
<div className="absolute top-2 right-2 bg-purple-600 text-white text-xs font-bold px-2 py-1 rounded-full shadow-lg">
|
||||||
|
Face {currentFaceIndex + 1}/{selectedCard.card_faces!.length}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleCardFace(selectedCard.id, selectedCard.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"
|
||||||
|
title="Flip card"
|
||||||
|
>
|
||||||
|
<RefreshCw size={20} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl md:text-2xl font-bold text-white mb-2">{displayName}</h2>
|
||||||
|
<p className="text-xs sm:text-sm text-gray-400">{displayTypeLine}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{displayOracleText && (
|
||||||
|
<div className="border-t border-gray-700 pt-3">
|
||||||
|
<p className="text-sm text-gray-300">{displayOracleText}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedCard.prices?.usd && (
|
||||||
|
<div className="border-t border-gray-700 pt-3">
|
||||||
|
<div className="text-lg text-green-400 font-semibold">
|
||||||
|
${selectedCard.prices.usd} each
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Collection Status */}
|
||||||
|
{userCollection.has(selectedCard.id) && (
|
||||||
|
<div className="border-t border-gray-700 pt-3">
|
||||||
|
<div className="text-sm text-green-400">
|
||||||
|
<CheckCircle size={16} className="inline mr-1" />
|
||||||
|
x{userCollection.get(selectedCard.id)} in your collection
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Deck Quantity Management */}
|
||||||
|
<div className="border-t border-gray-700 pt-3">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">Quantity in Deck</h3>
|
||||||
|
<div className="flex items-center justify-between bg-gray-900 rounded-lg p-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const cardInDeck = selectedCards.find(c => c.card.id === selectedCard.id);
|
||||||
|
const currentQuantity = cardInDeck?.quantity || 0;
|
||||||
|
if (currentQuantity === 1) {
|
||||||
|
removeCardFromDeck(selectedCard.id);
|
||||||
|
} else if (currentQuantity > 1) {
|
||||||
|
updateCardQuantity(selectedCard.id, currentQuantity - 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!selectedCards.find(c => c.card.id === selectedCard.id)}
|
||||||
|
className="bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white p-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Minus size={20} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-bold">
|
||||||
|
{selectedCards.find(c => c.card.id === selectedCard.id)?.quantity || 0}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-400">copies</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => addCardToDeck(selectedCard)}
|
||||||
|
className="bg-green-600 hover:bg-green-700 text-white p-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<Plus size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{snackbar && (
|
{snackbar && (
|
||||||
<div
|
<div
|
||||||
className={`fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg transition-all duration-300 ${
|
className={`fixed bottom-4 right-4 bg-green-500 text-white p-4 rounded-lg shadow-lg transition-all duration-300 ${
|
||||||
|
|||||||
Reference in New Issue
Block a user