diff --git a/src/client/src/components/Modal.tsx b/src/client/src/components/Modal.tsx index 422586c..4546b15 100644 --- a/src/client/src/components/Modal.tsx +++ b/src/client/src/components/Modal.tsx @@ -5,11 +5,13 @@ interface ModalProps { isOpen: boolean; onClose?: () => void; title: string; - message: string; + message?: string; + children?: React.ReactNode; type?: 'info' | 'success' | 'warning' | 'error'; confirmLabel?: string; onConfirm?: () => void; cancelLabel?: string; + maxWidth?: string; } export const Modal: React.FC = ({ @@ -17,10 +19,12 @@ export const Modal: React.FC = ({ onClose, title, message, + children, type = 'info', confirmLabel = 'OK', onConfirm, - cancelLabel + cancelLabel, + maxWidth = 'max-w-md' }) => { if (!isOpen) return null; @@ -45,10 +49,10 @@ export const Modal: React.FC = ({ return (
-
+
{getIcon()}

{title}

@@ -60,33 +64,42 @@ export const Modal: React.FC = ({ )}
-

- {message} -

- -
- {cancelLabel && onClose && ( - +
+ {message && ( +

+ {message} +

)} - + {children}
+ + {(onConfirm || cancelLabel) && ( +
+ {cancelLabel && onClose && ( + + )} + {onConfirm && ( + + )} +
+ )}
); diff --git a/src/client/src/modules/lobby/LobbyManager.tsx b/src/client/src/modules/lobby/LobbyManager.tsx index 37b692d..f72a067 100644 --- a/src/client/src/modules/lobby/LobbyManager.tsx +++ b/src/client/src/modules/lobby/LobbyManager.tsx @@ -3,7 +3,8 @@ import React, { useState } from 'react'; import { socketService } from '../../services/SocketService'; import { GameRoom } from './GameRoom'; import { Pack } from '../../services/PackGeneratorService'; -import { Users, PlusCircle, LogIn, AlertCircle, Loader2 } from 'lucide-react'; +import { Users, PlusCircle, LogIn, AlertCircle, Loader2, Package, Check } from 'lucide-react'; +import { Modal } from '../../components/Modal'; interface LobbyManagerProps { generatedPacks: Pack[]; @@ -31,29 +32,23 @@ export const LobbyManager: React.FC = ({ generatedPacks, avai localStorage.setItem('player_name', playerName); }, [playerName]); + const [showBoxSelection, setShowBoxSelection] = useState(false); + const [availableBoxes, setAvailableBoxes] = useState<{ id: string, title: string, packs: Pack[], setCode: string, packCount: number }[]>([]); + const connect = () => { if (!socketService.socket.connected) { socketService.connect(); } }; - const handleCreateRoom = async () => { - if (!playerName) { - setError('Please enter your name'); - return; - } - if (generatedPacks.length === 0) { - setError('No packs generated! Please go to Draft Management and generate packs first.'); - return; - } - + const executeCreateRoom = async (packsToUse: Pack[]) => { setLoading(true); setError(''); connect(); try { // Collect all cards for caching (packs + basic lands) - const allCards = generatedPacks.flatMap(p => p.cards); + const allCards = packsToUse.flatMap(p => p.cards); const allCardsAndLands = [...allCards, ...availableLands]; // Deduplicate by Scryfall ID @@ -84,7 +79,7 @@ export const LobbyManager: React.FC = ({ generatedPacks, avai // Note: For multiplayer, clients need to access this URL. const baseUrl = `${window.location.protocol}//${window.location.host}/cards/images`; - const updatedPacks = generatedPacks.map(pack => ({ + const updatedPacks = packsToUse.map(pack => ({ ...pack, cards: pack.cards.map(c => ({ ...c, @@ -115,9 +110,68 @@ export const LobbyManager: React.FC = ({ generatedPacks, avai setError(err.message || 'Connection error'); } finally { setLoading(false); + setShowBoxSelection(false); } }; + const handleCreateRoom = async () => { + if (!playerName) { + setError('Please enter your name'); + return; + } + if (generatedPacks.length === 0) { + setError('No packs generated! Please go to Draft Management and generate packs first.'); + return; + } + + // Logic to detect Multiple Boxes + // 1. Group by Set Name + const packsBySet: Record = {}; + generatedPacks.forEach(p => { + const key = p.setName; + if (!packsBySet[key]) packsBySet[key] = []; + packsBySet[key].push(p); + }); + + const boxes: { id: string, title: string, packs: Pack[], setCode: string, packCount: number }[] = []; + + // Sort sets alphabetically + Object.keys(packsBySet).sort().forEach(setName => { + const setPacks = packsBySet[setName]; + const BOX_SIZE = 36; + + // Split into chunks of 36 + for (let i = 0; i < setPacks.length; i += BOX_SIZE) { + const chunk = setPacks.slice(i, i + BOX_SIZE); + const boxNum = Math.floor(i / BOX_SIZE) + 1; + const setCode = (chunk[0].cards[0]?.setCode || 'unk').toLowerCase(); + + boxes.push({ + id: `${setCode}-${boxNum}-${Date.now()}`, // Unique ID + title: `${setName} - Box ${boxNum}`, + packs: chunk, + setCode: setCode, + packCount: chunk.length + }); + } + }); + + // Strategy: If we have multiple boxes, or if we have > 36 packs but maybe not multiple "boxes" (e.g. 50 packs of mixed), + // we should interpret them. + // The prompt says: "more than 1 box has been generated". + // If I generate 2 boxes (72 packs), `boxes` array will have length 2. + // If I generate 1 box (36 packs), `boxes` array will have length 1. + + if (boxes.length > 1) { + setAvailableBoxes(boxes); + setShowBoxSelection(true); + return; + } + + // If only 1 box (or partial), just use all packs + executeCreateRoom(generatedPacks); + }; + const handleJoinRoom = async () => { if (!playerName) { setError('Please enter your name'); @@ -316,6 +370,62 @@ export const LobbyManager: React.FC = ({ generatedPacks, avai
+ {/* Box Selection Modal */} + setShowBoxSelection(false)} + title="Select Sealed Box" + message="Multiple boxes available. Please select a sealed box to open for this draft." + type="info" + maxWidth="max-w-3xl" + > +
+ {availableBoxes.map(box => ( + + ))} +
+
+ +
+
); };