diff --git a/src/components/CardSearchModal.tsx b/src/components/CardSearchModal.tsx new file mode 100644 index 0000000..61d3b9f --- /dev/null +++ b/src/components/CardSearchModal.tsx @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import { X, Search } from 'lucide-react'; +import { searchCards } from '../services/api'; +import { Card } from '../types'; + +interface CardSearchModalProps { + isOpen: boolean; + onClose: () => void; + onSelectCard: (card: Card) => void; +} + +export default function CardSearchModal({ isOpen, onClose, onSelectCard }: CardSearchModalProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + if (!searchQuery.trim()) return; + + setLoading(true); + setError(null); + + try { + const cards = await searchCards(`name:${searchQuery}`); + setSearchResults(cards || []); + } catch (err) { + setError('Failed to fetch cards. Please try again.'); + console.error('Error fetching cards:', err); + } finally { + setLoading(false); + } + }; + + const handleSelectCard = (card: Card) => { + onSelectCard(card); + onClose(); + setSearchQuery(''); + setSearchResults([]); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

Search Card for Background

+ +
+ + {/* Search Form */} +
+
+ setSearchQuery(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-900 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Search for a card by name..." + autoFocus + /> + +
+
+ + {/* Results */} +
+ {loading && ( +
+
+
+ )} + + {error && ( +
+ {error} +
+ )} + + {!loading && !error && searchResults.length === 0 && searchQuery && ( +
+ No cards found. Try a different search term. +
+ )} + + {!loading && !error && searchResults.length === 0 && !searchQuery && ( +
+ Search for a card to use as background +
+ )} + + {searchResults.length > 0 && ( +
+ {searchResults.map((card) => ( + + ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/LifeCounter.tsx b/src/components/LifeCounter.tsx index 08a8f94..8faba18 100644 --- a/src/components/LifeCounter.tsx +++ b/src/components/LifeCounter.tsx @@ -1,167 +1,208 @@ -import React, { useState, useEffect } from 'react'; - import { Plus, Minus } from 'lucide-react'; +import React, { useState } from 'react'; +import { Users, RotateCcw, Settings } from 'lucide-react'; +import PlayerLifeCounter from './PlayerLifeCounter'; +import CardSearchModal from './CardSearchModal'; +import { Card } from '../types'; - interface Player { - id: number; - name: string; - life: number; - color: string; +interface Player { + id: number; + name: string; + life: number; + backgroundImage?: string; +} + +const DEFAULT_STARTING_LIFE = 20; + +export default function LifeCounter() { + const [players, setPlayers] = useState([ + { id: 1, name: 'Player 1', life: DEFAULT_STARTING_LIFE }, + { id: 2, name: 'Player 2', life: DEFAULT_STARTING_LIFE }, + ]); + const [isCardSearchOpen, setIsCardSearchOpen] = useState(false); + const [selectedPlayerId, setSelectedPlayerId] = useState(null); + const [showSettings, setShowSettings] = useState(false); + const [startingLife, setStartingLife] = useState(DEFAULT_STARTING_LIFE); + + const updateLife = (playerId: number, change: number) => { + setPlayers((prevPlayers) => + prevPlayers.map((player) => + player.id === playerId ? { ...player, life: Math.max(0, player.life + change) } : player + ) + ); + }; + + const changePlayerName = (playerId: number) => { + const player = players.find((p) => p.id === playerId); + if (!player) return; + + const newName = prompt('Enter new player name:', player.name); + if (newName && newName.trim()) { + setPlayers((prevPlayers) => + prevPlayers.map((p) => (p.id === playerId ? { ...p, name: newName.trim() } : p)) + ); } + }; - const COLORS = ['white', 'blue', 'black', 'red', 'green']; + const openBackgroundSelector = (playerId: number) => { + setSelectedPlayerId(playerId); + setIsCardSearchOpen(true); + }; - export default function LifeCounter() { - const [numPlayers, setNumPlayers] = useState(null); - const [playerNames, setPlayerNames] = useState([]); - const [players, setPlayers] = useState([]); - const [setupComplete, setSetupComplete] = useState(false); + const handleCardSelect = (card: Card) => { + if (selectedPlayerId !== null && card.image_uris?.art_crop) { + setPlayers((prevPlayers) => + prevPlayers.map((p) => + p.id === selectedPlayerId ? { ...p, backgroundImage: card.image_uris!.art_crop } : p + ) + ); + } + setSelectedPlayerId(null); + }; - useEffect(() => { - if (numPlayers !== null) { - setPlayers( - Array.from({ length: numPlayers }, (_, i) => ({ - id: i + 1, - name: playerNames[i] || `Player ${i + 1}`, - life: 20, - color: COLORS[i % COLORS.length], - })) - ); - } - }, [numPlayers, playerNames]); + const changePlayerCount = (count: number) => { + const currentCount = players.length; - const handleNumPlayersChange = (e: React.ChangeEvent) => { - const newNumPlayers = parseInt(e.target.value, 10); - setNumPlayers(newNumPlayers); - setPlayerNames(Array(newNumPlayers).fill('')); - }; + if (count > currentCount) { + // Add players + const newPlayers = Array.from({ length: count - currentCount }, (_, i) => ({ + id: currentCount + i + 1, + name: `Player ${currentCount + i + 1}`, + life: startingLife, + })); + setPlayers([...players, ...newPlayers]); + } else if (count < currentCount) { + // Remove players + setPlayers(players.slice(0, count)); + } + }; - const handleNameChange = (index: number, newName: string) => { - const updatedNames = [...playerNames]; - updatedNames[index] = newName; - setPlayerNames(updatedNames); - }; + const resetAllLife = () => { + if (confirm(`Reset all players to ${startingLife} life?`)) { + setPlayers((prevPlayers) => + prevPlayers.map((p) => ({ ...p, life: startingLife })) + ); + } + }; - const updateLife = (playerId: number, change: number) => { - setPlayers((prevPlayers) => - prevPlayers.map((player) => - player.id === playerId ? { ...player, life: player.life + change } : player - ) - ); - }; + const getGridClass = () => { + const count = players.length; + if (count === 1) return 'grid-cols-1'; + if (count === 2) return 'grid-cols-1 md:grid-cols-2'; + if (count === 3) return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3'; + if (count === 4) return 'grid-cols-2 lg:grid-cols-2'; + if (count === 5) return 'grid-cols-2 lg:grid-cols-3'; + if (count === 6) return 'grid-cols-2 lg:grid-cols-3'; + if (count === 7 || count === 8) return 'grid-cols-2 lg:grid-cols-4'; + return 'grid-cols-2 lg:grid-cols-4'; + }; - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - setSetupComplete(true); - }; + return ( +
+ {/* Header */} +
+

Life Counter

+
+ + +
+
- const renderSetupForm = () => ( -
-

Setup Players

-
+ {/* Settings Panel */} + {showSettings && ( +
+
+ {/* Player Count */}
- +
- {numPlayers !== null && - Array.from({ length: numPlayers }, (_, i) => ( -
- - handleNameChange(i, 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={`Player ${i + 1} Name`} - /> -
- ))} - - {numPlayers !== null && ( - - )} - -
- ); - - const renderLifeCounters = () => ( -
-
- {players.map((player, index) => { - const angle = (index / players.length) * 360; - const rotation = 360 - angle; - const x = 50 + 40 * Math.cos((angle - 90) * Math.PI / 180); - const y = 50 + 40 * Math.sin((angle - 90) * Math.PI / 180); - - return ( -
-
+ +
+ {[20, 30, 40].map((life) => ( + - -
-
-
- ); - })} + {life} + + ))} +
+
- ); + )} - return ( -
-
-

Life Counter

- {!setupComplete ? renderSetupForm() : renderLifeCounters()} -
-
- ); - } + {/* Life Counter Grid */} +
+ {players.map((player) => ( + updateLife(player.id, change)} + onChangeName={() => changePlayerName(player.id)} + onChangeBackground={() => openBackgroundSelector(player.id)} + /> + ))} +
+ + {/* Card Search Modal */} + { + setIsCardSearchOpen(false); + setSelectedPlayerId(null); + }} + onSelectCard={handleCardSelect} + /> + + ); +} diff --git a/src/components/PlayerLifeCounter.tsx b/src/components/PlayerLifeCounter.tsx new file mode 100644 index 0000000..1a6bd95 --- /dev/null +++ b/src/components/PlayerLifeCounter.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { Plus, Minus, Image as ImageIcon } from 'lucide-react'; + +interface PlayerLifeCounterProps { + id: number; + name: string; + life: number; + backgroundImage?: string; + onLifeChange: (change: number) => void; + onChangeName: () => void; + onChangeBackground: () => void; +} + +export default function PlayerLifeCounter({ + name, + life, + backgroundImage, + onLifeChange, + onChangeName, + onChangeBackground, +}: PlayerLifeCounterProps) { + return ( +
+ {/* Background Image */} + {backgroundImage ? ( +
+
+
+ ) : ( +
+ )} + + {/* Content */} +
+ {/* Player Name */} + + + {/* Life Total */} +
+
+ {life} +
+
+ + {/* Controls */} +
+ {/* Life Buttons */} +
+ + +
+ + {/* Background Button */} + +
+
+
+ ); +}