diff --git a/src/components/CardSearch.tsx b/src/components/CardSearch.tsx index 567ee3c..d669b15 100644 --- a/src/components/CardSearch.tsx +++ b/src/components/CardSearch.tsx @@ -1,9 +1,12 @@ -import React, { useState } from 'react'; -import { searchCards } from '../services/api'; +import React, { useState, useEffect } from 'react'; +import { RefreshCw, PackagePlus, Loader2, CheckCircle, XCircle, Trash2 } from 'lucide-react'; +import { searchCards, getUserCollection, addCardToCollection } from '../services/api'; import { Card } from '../types'; +import { useAuth } from '../contexts/AuthContext'; import MagicCard from './MagicCard'; const CardSearch = () => { + const { user } = useAuth(); const [cardName, setCardName] = useState(''); const [text, setText] = useState(''); const [rulesText, setRulesText] = useState(''); @@ -40,6 +43,85 @@ const CardSearch = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // Collection state + const [userCollection, setUserCollection] = useState>(new Map()); + const [addingCardId, setAddingCardId] = useState(null); + const [cardFaceIndex, setCardFaceIndex] = useState>(new Map()); + const [snackbar, setSnackbar] = useState<{ message: string; type: 'success' | 'error' } | null>(null); + + // Load user collection + useEffect(() => { + const loadUserCollection = async () => { + if (!user) return; + try { + const collection = await getUserCollection(user.id); + setUserCollection(collection); + } catch (error) { + console.error('Error loading user collection:', error); + } + }; + loadUserCollection(); + }, [user]); + + // Helper function to check if a card has an actual back face + const isDoubleFaced = (card: Card) => { + const backFaceLayouts = ['transform', 'modal_dfc', 'double_faced_token', 'reversible_card']; + return card.card_faces && card.card_faces.length > 1 && backFaceLayouts.includes(card.layout); + }; + + // Get current face index for a card + const getCurrentFaceIndex = (cardId: string) => { + return cardFaceIndex.get(cardId) || 0; + }; + + // Toggle card face + const toggleCardFace = (cardId: string, totalFaces: number) => { + setCardFaceIndex(prev => { + const newMap = new Map(prev); + const currentIndex = prev.get(cardId) || 0; + const nextIndex = (currentIndex + 1) % totalFaces; + newMap.set(cardId, nextIndex); + return newMap; + }); + }; + + // Get card image for current face + const getCardImageUri = (card: Card, faceIndex: number = 0) => { + if (isDoubleFaced(card) && card.card_faces) { + return card.card_faces[faceIndex]?.image_uris?.normal || card.card_faces[faceIndex]?.image_uris?.small; + } + return card.image_uris?.normal || card.image_uris?.small || card.card_faces?.[0]?.image_uris?.normal; + }; + + // Add card to collection + const handleAddCardToCollection = async (cardId: string) => { + if (!user) { + setSnackbar({ message: 'Please log in to add cards to your collection', type: 'error' }); + setTimeout(() => setSnackbar(null), 3000); + return; + } + + try { + setAddingCardId(cardId); + await addCardToCollection(user.id, cardId, 1); + + setUserCollection(prev => { + const newMap = new Map(prev); + const currentQty = newMap.get(cardId) || 0; + newMap.set(cardId, currentQty + 1); + return newMap; + }); + + setSnackbar({ message: 'Card added to collection!', type: 'success' }); + } catch (error) { + console.error('Error adding card to collection:', error); + setSnackbar({ message: 'Failed to add card to collection', type: 'error' }); + } finally { + setAddingCardId(null); + setTimeout(() => setSnackbar(null), 3000); + } + }; + const handleSearch = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); @@ -524,20 +606,109 @@ const CardSearch = () => { {searchResults && searchResults.length > 0 && (
- {searchResults.map((card) => ( -
- -
-

{card.name}

-

{card.type_line}

+ {searchResults.map((card) => { + const currentFaceIndex = getCurrentFaceIndex(card.id); + const isMultiFaced = isDoubleFaced(card); + const inCollection = userCollection.get(card.id) || 0; + const isAddingThisCard = addingCardId === card.id; + + const displayName = isMultiFaced && card.card_faces + ? card.card_faces[currentFaceIndex]?.name || card.name + : card.name; + + return ( +
+
+ {getCardImageUri(card, currentFaceIndex) ? ( + {displayName} + ) : ( + + )} + {isMultiFaced && ( + + )} +
+
+
+

{displayName}

+ {inCollection > 0 && ( + + + x{inCollection} + + )} +
+

+ {isMultiFaced && card.card_faces + ? card.card_faces[currentFaceIndex]?.type_line || card.type_line + : card.type_line} +

+ {card.prices?.usd && ( +
${card.prices.usd}
+ )} + +
-
- ))} + ); + })}
)}
+ + {/* Snackbar */} + {snackbar && ( +
+
+
+ {snackbar.type === 'success' ? ( + + ) : ( + + )} + {snackbar.message} +
+ +
+
+ )} ); }; -export default CardSearch; +export default CardSearch; diff --git a/src/components/Collection.tsx b/src/components/Collection.tsx index c5abf54..fd12af0 100644 --- a/src/components/Collection.tsx +++ b/src/components/Collection.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Search, Loader2, Trash2, CheckCircle, XCircle } from 'lucide-react'; +import { Search, Loader2, Trash2, CheckCircle, XCircle, RefreshCw } from 'lucide-react'; import { Card } from '../types'; import { getUserCollection, getCardsByIds } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; @@ -11,8 +11,48 @@ export default function Collection() { const [filteredCollection, setFilteredCollection] = useState<{ card: Card; quantity: number }[]>([]); const [isLoadingCollection, setIsLoadingCollection] = useState(true); const [hoveredCard, setHoveredCard] = useState(null); + const [cardFaceIndex, setCardFaceIndex] = useState>(new Map()); const [snackbar, setSnackbar] = useState<{ message: string; type: 'success' | 'error' } | null>(null); + // Helper function to check if a card has an actual back face (not adventure/split/etc) + const isDoubleFaced = (card: Card) => { + // Only show flip for cards with physical back sides + const backFaceLayouts = ['transform', 'modal_dfc', 'double_faced_token', 'reversible_card']; + return card.card_faces && card.card_faces.length > 1 && backFaceLayouts.includes(card.layout); + }; + + // Helper function to get the current face index for a card + const getCurrentFaceIndex = (cardId: string) => { + return cardFaceIndex.get(cardId) || 0; + }; + + // Helper function to get the image URI for a card (handling both single and double-faced) + const getCardImageUri = (card: Card, faceIndex: number = 0) => { + if (isDoubleFaced(card) && card.card_faces) { + return card.card_faces[faceIndex]?.image_uris?.normal || card.card_faces[faceIndex]?.image_uris?.small; + } + return card.image_uris?.normal || card.image_uris?.small; + }; + + // Helper function to get the large image URI for hover preview + 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; + }; + + // Toggle card face + const toggleCardFace = (cardId: string, totalFaces: number) => { + setCardFaceIndex(prev => { + const newMap = new Map(prev); + const currentIndex = prev.get(cardId) || 0; + const nextIndex = (currentIndex + 1) % totalFaces; + newMap.set(cardId, nextIndex); + return newMap; + }); + }; + // Load user's collection from Supabase on mount useEffect(() => { const loadCollection = async () => { @@ -115,63 +155,103 @@ export default function Collection() { ) : (
- {filteredCollection.map(({ card, quantity }) => ( -
setHoveredCard(card)} - onMouseLeave={() => setHoveredCard(null)} - > - {/* Small card thumbnail */} -
- {card.name} - {/* Quantity badge */} -
- x{quantity} + {filteredCollection.map(({ card, quantity }) => { + const currentFaceIndex = getCurrentFaceIndex(card.id); + const isMultiFaced = isDoubleFaced(card); + const displayName = isMultiFaced && card.card_faces + ? card.card_faces[currentFaceIndex]?.name || card.name + : card.name; + + return ( +
setHoveredCard(card)} + onMouseLeave={() => setHoveredCard(null)} + > + {/* Small card thumbnail */} +
+ {displayName} + {/* Quantity badge */} +
+ x{quantity} +
+ {/* Flip button for double-faced cards */} + {isMultiFaced && ( + + )} +
+ + {/* Card name below thumbnail */} +
+ {displayName}
- - {/* Card name below thumbnail */} -
- {card.name} -
-
- ))} + ); + })}
)}
{/* Hover Card Preview */} - {hoveredCard && ( -
-
- {hoveredCard.name} -
-

{hoveredCard.name}

-

{hoveredCard.type_line}

- {hoveredCard.oracle_text && ( -

- {hoveredCard.oracle_text} -

- )} - {hoveredCard.prices?.usd && ( -
- ${hoveredCard.prices.usd} -
- )} + {hoveredCard && (() => { + 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 ( +
+
+
+ {displayName} + {isMultiFaced && ( +
+ Face {currentFaceIndex + 1}/{hoveredCard.card_faces!.length} +
+ )} +
+
+

{displayName}

+

{displayTypeLine}

+ {displayOracleText && ( +

+ {displayOracleText} +

+ )} + {hoveredCard.prices?.usd && ( +
+ ${hoveredCard.prices.usd} +
+ )} +
-
- )} + ); + })()} {/* Snackbar */} {snackbar && ( diff --git a/src/components/DeckManager.tsx b/src/components/DeckManager.tsx index ab1bb26..cac94fc 100644 --- a/src/components/DeckManager.tsx +++ b/src/components/DeckManager.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Plus, Search, Save, Trash2, Loader2, CheckCircle, XCircle, AlertCircle, PackagePlus } from 'lucide-react'; +import { Plus, Search, Save, Trash2, Loader2, CheckCircle, XCircle, AlertCircle, PackagePlus, RefreshCw } from 'lucide-react'; import { Card, Deck } from '../types'; import { searchCards, getUserCollection, addCardToCollection, addMultipleCardsToCollection } from '../services/api'; import { useAuth } from '../contexts/AuthContext'; @@ -120,6 +120,7 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { const [isLoadingCollection, setIsLoadingCollection] = useState(true); const [addingCardId, setAddingCardId] = useState(null); const [isAddingAll, setIsAddingAll] = useState(false); + const [cardFaceIndex, setCardFaceIndex] = useState>(new Map()); // Load user collection on component mount useEffect(() => { @@ -141,6 +142,33 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { loadUserCollection(); }, [user]); + // Helper functions for double-faced cards + const isDoubleFaced = (card: Card) => { + const backFaceLayouts = ['transform', 'modal_dfc', 'double_faced_token', 'reversible_card']; + return card.card_faces && card.card_faces.length > 1 && backFaceLayouts.includes(card.layout); + }; + + const getCurrentFaceIndex = (cardId: string) => { + return cardFaceIndex.get(cardId) || 0; + }; + + const toggleCardFace = (cardId: string, totalFaces: number) => { + setCardFaceIndex(prev => { + const newMap = new Map(prev); + const currentIndex = prev.get(cardId) || 0; + const nextIndex = (currentIndex + 1) % totalFaces; + newMap.set(cardId, nextIndex); + return newMap; + }); + }; + + const getCardImageUri = (card: Card, faceIndex: number = 0) => { + if (isDoubleFaced(card) && card.card_faces) { + return card.card_faces[faceIndex]?.image_uris?.normal || card.card_faces[faceIndex]?.image_uris?.small; + } + return card.image_uris?.normal || card.image_uris?.small || card.card_faces?.[0]?.image_uris?.normal; + }; + // Helper function to check if a card is in the collection const isCardInCollection = (cardId: string, requiredQuantity: number = 1): boolean => { const ownedQuantity = userCollection.get(cardId) || 0; @@ -491,24 +519,82 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
- {searchResults.map(card => ( -
- -
-

{card.name}

- + {searchResults.map(card => { + const currentFaceIndex = getCurrentFaceIndex(card.id); + const isMultiFaced = isDoubleFaced(card); + const inCollection = userCollection.get(card.id) || 0; + const isAddingThisCard = addingCardId === card.id; + + const displayName = isMultiFaced && card.card_faces + ? card.card_faces[currentFaceIndex]?.name || card.name + : card.name; + + return ( +
+
+ {getCardImageUri(card, currentFaceIndex) ? ( + {displayName} + ) : ( + + )} + {isMultiFaced && ( + + )} +
+
+
+

{displayName}

+ {inCollection > 0 && ( + + + x{inCollection} + + )} +
+ {card.prices?.usd && ( +
${card.prices.usd}
+ )} +
+ + +
+
-
- ))} + ); + })}
diff --git a/src/components/MagicCard.tsx b/src/components/MagicCard.tsx index 876c2ef..810f2ef 100644 --- a/src/components/MagicCard.tsx +++ b/src/components/MagicCard.tsx @@ -6,11 +6,14 @@ interface MagicCardProps { } const MagicCard = ({ card }: MagicCardProps) => { + // Handle both regular cards and double-faced cards (transform, modal_dfc, etc) + const imageUri = card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal; + return (
- {card.image_uris?.normal ? ( + {imageUri ? ( {card.name}