diff --git a/src/App.tsx b/src/App.tsx index d5d0bfd..4c44de4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,85 +1,88 @@ import React, { useState } from 'react'; -import DeckManager from './components/DeckManager'; -import DeckList from './components/DeckList'; -import LoginForm from './components/LoginForm'; -import Navigation from './components/Navigation'; -import Collection from './components/Collection'; -import DeckEditor from './components/DeckEditor'; -import Profile from './components/Profile'; -import { AuthProvider, useAuth } from './contexts/AuthContext'; + import DeckManager from './components/DeckManager'; + import DeckList from './components/DeckList'; + import LoginForm from './components/LoginForm'; + import Navigation from './components/Navigation'; + import Collection from './components/Collection'; + import DeckEditor from './components/DeckEditor'; + import Profile from './components/Profile'; + import CardSearch from './components/CardSearch'; + import { AuthProvider, useAuth } from './contexts/AuthContext'; -type Page = 'home' | 'deck' | 'login' | 'collection' | 'edit-deck' | 'profile'; + type Page = 'home' | 'deck' | 'login' | 'collection' | 'edit-deck' | 'profile' | 'search'; -function AppContent() { - const [currentPage, setCurrentPage] = useState('home'); - const [selectedDeckId, setSelectedDeckId] = useState(null); - const { user, loading } = useAuth(); + function AppContent() { + const [currentPage, setCurrentPage] = useState('home'); + const [selectedDeckId, setSelectedDeckId] = useState(null); + const { user, loading } = useAuth(); - if (loading) { - return ( -
-
-
- ); - } - - if (!user && currentPage !== 'login') { - return ; - } - - const handleDeckEdit = (deckId: string) => { - setSelectedDeckId(deckId); - setCurrentPage('edit-deck'); - }; - - const renderPage = () => { - switch (currentPage) { - case 'home': + if (loading) { return ( -
-
-

My Decks

- -
+
+
); - case 'deck': - return ; - case 'edit-deck': - return selectedDeckId ? ( - { - setSelectedDeckId(null); - setCurrentPage('home'); - }} - /> - ) : null; - case 'collection': - return ; - case 'profile': - return ; - case 'login': + } + + if (!user && currentPage !== 'login') { return ; - default: - return null; + } + + const handleDeckEdit = (deckId: string) => { + setSelectedDeckId(deckId); + setCurrentPage('edit-deck'); + }; + + const renderPage = () => { + switch (currentPage) { + case 'home': + return ( +
+
+

My Decks

+ +
+
+ ); + case 'deck': + return ; + case 'edit-deck': + return selectedDeckId ? ( + { + setSelectedDeckId(null); + setCurrentPage('home'); + }} + /> + ) : null; + case 'collection': + return ; + case 'profile': + return ; + case 'search': + return ; + case 'login': + return ; + default: + return null; + } + }; + + return ( +
+ + {renderPage()} +
+ ); } - }; - return ( -
- - {renderPage()} -
- ); -} + function App() { + return ( + + + + ); + } -function App() { - return ( - - - - ); -} - -export default App; + export default App; diff --git a/src/components/CardSearch.tsx b/src/components/CardSearch.tsx new file mode 100644 index 0000000..e3dd0bf --- /dev/null +++ b/src/components/CardSearch.tsx @@ -0,0 +1,528 @@ +import React, { useState } from 'react'; + import { searchCards } from '../services/api'; + import { Card } from '../types'; + + const CardSearch = () => { + const [cardName, setCardName] = useState(''); + const [text, setText] = useState(''); + const [rulesText, setRulesText] = useState(''); + const [typeLine, setTypeLine] = useState(''); + const [typeMatch, setTypeMatch] = useState('partial'); + const [typeInclude, setTypeInclude] = useState(true); + const [colors, setColors] = useState({ W: false, U: false, B: false, R: false, G: false, C: false }); + const [colorMode, setColorMode] = useState('exactly'); + const [commanderColors, setCommanderColors] = useState({ W: false, U: false, B: false, R: false, G: false, C: false }); + const [manaCost, setManaCost] = useState(''); + const [manaValue, setManaValue] = useState(''); + const [manaValueComparison, setManaValueComparison] = useState('='); + const [games, setGames] = useState({ paper: false, arena: false, mtgo: false }); + const [format, setFormat] = useState(''); + const [formatStatus, setFormatStatus] = useState(''); + const [set, setSet] = useState(''); + const [block, setBlock] = useState(''); + const [rarity, setRarity] = useState({ common: false, uncommon: false, rare: false, mythic: false }); + const [criteria, setCriteria] = useState(''); + const [criteriaMatch, setCriteriaMatch] = useState('partial'); + const [criteriaInclude, setCriteriaInclude] = useState(true); + const [price, setPrice] = useState(''); + const [currency, setCurrency] = useState('usd'); + const [priceComparison, setPriceComparison] = useState('='); + const [artist, setArtist] = useState(''); + const [flavorText, setFlavorText] = useState(''); + const [loreFinder, setLoreFinder] = useState(''); + const [language, setLanguage] = useState('en'); + const [displayImages, setDisplayImages] = useState(false); + const [order, setOrder] = useState('name'); + const [showAllPrints, setShowAllPrints] = useState(false); + const [includeExtras, setIncludeExtras] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + let query = ''; + + if (cardName) query += `name:${cardName} `; + if (text) query += `o:${text} `; + if (rulesText) query += `o:"${rulesText.replace('~', cardName)}" `; + if (typeLine) { + const typeQuery = typeMatch === 'partial' ? typeLine : `"${typeLine}"`; + query += `${typeInclude ? '' : '-'}t:${typeQuery} `; + } + if (Object.values(colors).some(Boolean)) { + const activeColors = Object.keys(colors).filter((key) => colors[key as keyof typeof colors]).join(''); + const colorQuery = colorMode === 'exactly' ? `c:${activeColors}` : `color<=${activeColors}`; + query += `${colorQuery} `; + } + if (Object.values(commanderColors).some(Boolean)) { + const activeColors = Object.keys(commanderColors).filter((key) => commanderColors[key as keyof typeof commanderColors]).join(''); + query += `id:${activeColors} `; + } + if (manaCost) query += `m:${manaCost} `; + if (manaValue) query += `mv${manaValueComparison}${manaValue} `; + if (Object.values(games).some(Boolean)) { + const activeGames = Object.keys(games).filter((key) => games[key as keyof typeof games]).join(','); + query += `game:${activeGames} `; + } + if (format) query += `f:${format} `; + if (formatStatus) query += `${formatStatus}:${format} `; + if (set) query += `e:${set} `; + if (block) query += `b:${block} `; + if (Object.values(rarity).some(Boolean)) { + const activeRarities = Object.keys(rarity).filter((key) => rarity[key as keyof typeof rarity]).join(','); + query += `r:${activeRarities} `; + } + if (criteria) { + const criteriaQuery = criteriaMatch === 'partial' ? criteria : `"${criteria}"`; + query += `${criteriaInclude ? '' : '-'}o:${criteriaQuery} `; + } + if (price) query += `${currency}${priceComparison}${price} `; + if (artist) query += `a:${artist} `; + if (flavorText) query += `ft:${flavorText} `; + if (loreFinder) query += `${loreFinder} `; + if (language) query += `lang:${language} `; + if (displayImages) query += `display:grid `; + if (order) query += `order:${order} `; + if (showAllPrints) query += `unique:prints `; + if (includeExtras) query += `include:extras `; + + try { + const cards = await searchCards(query.trim()); + setSearchResults(cards); + } catch (err) { + setError('Failed to fetch cards.'); + console.error('Error fetching cards:', err); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Card Search

+
+ {/* Card Details */} +
+ setCardName(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Card Name" + /> + setText(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Text" + /> + setRulesText(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Rules Text (~ for card name)" + /> +
+ setTypeLine(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Type Line" + /> + + +
+
+ + {/* Colors */} +
+
+

Card Colors

+
+ {['W', 'U', 'B', 'R', 'G', 'C'].map((color) => ( + + ))} +
+ +
+
+

Commander Colors

+
+ {['W', 'U', 'B', 'R', 'G', 'C'].map((color) => ( + + ))} +
+
+
+ + {/* Mana Cost */} + setManaCost(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Mana Cost (e.g., {W}{W})" + /> + + {/* Stats */} +
+ + setManaValue(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Mana Value" + /> +
+ + {/* Games */} +
+

Games

+
+ {['paper', 'arena', 'mtgo'].map((game) => ( + + ))} +
+
+ + {/* Formats */} +
+ + +
+ + {/* Sets */} +
+ setSet(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Set Code" + /> + setBlock(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Block Code" + /> +
+ + {/* Rarity */} +
+

Rarity

+
+ {['common', 'uncommon', 'rare', 'mythic'].map((r) => ( + + ))} +
+
+ + {/* Criteria */} +
+ setCriteria(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Criteria" + /> + + +
+ + {/* Prices */} +
+ + + setPrice(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Price" + /> +
+ + {/* Additional Filters */} +
+ setArtist(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Artist" + /> + setFlavorText(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Flavor Text" + /> + setLoreFinder(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Lore Finderâ„¢" + /> + +
+ + {/* Preferences */} +
+ + + + +
+ + +
+ + {loading && ( +
+
+
+ )} + + {error && ( +
+ {error} +
+ )} + + {searchResults.length > 0 && ( +
+ {searchResults.map((card) => ( +
+ {card.image_uris?.normal && ( + {card.name} + )} +
+

{card.name}

+

{card.type_line}

+
+
+ ))} +
+ )} +
+
+ ); + }; + + export default CardSearch; diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 8313bc0..3f94673 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,9 +1,9 @@ import React, { useState, useRef, useEffect } from 'react'; - import { Home, PlusSquare, Library, LogOut, Settings, ChevronDown } from 'lucide-react'; + import { Home, PlusSquare, Library, LogOut, Settings, ChevronDown, Search } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { supabase } from '../lib/supabase'; - type Page = 'home' | 'deck' | 'login' | 'collection' | 'profile'; + type Page = 'home' | 'deck' | 'login' | 'collection' | 'profile' | 'search'; interface NavigationProps { currentPage: Page; @@ -49,6 +49,7 @@ import React, { useState, useRef, useEffect } from 'react'; { id: 'home' as const, label: 'Home', icon: Home }, { id: 'deck' as const, label: 'New Deck', icon: PlusSquare }, { id: 'collection' as const, label: 'Collection', icon: Library }, + { id: 'search' as const, label: 'Search', icon: Search }, ]; const handleSignOut = async () => { @@ -134,7 +135,7 @@ import React, { useState, useRef, useEffect } from 'react'; {/* Mobile Navigation - Bottom */}