From 418e9e4507d14c78c7c18d4432311db700b599bf Mon Sep 17 00:00:00 2001 From: dnviti Date: Sat, 20 Dec 2025 17:21:11 +0100 Subject: [PATCH] feat: Introduce a global confirmation dialog and integrate it for various actions across game rooms, tournament, cube, and deck management, while also adding new UI controls and actions to the game room. --- src/client/dev-dist/sw.js | 2 +- src/client/src/App.tsx | 127 +++++++++--------- src/client/src/components/ConfirmDialog.tsx | 77 +++++++++++ src/client/src/modules/cube/CubeManager.tsx | 23 ++-- .../src/modules/draft/DeckBuilderView.tsx | 14 +- src/client/src/modules/game/GameView.tsx | 24 ++-- src/client/src/modules/lobby/GameRoom.tsx | 70 +++++++--- .../modules/tournament/TournamentManager.tsx | 7 +- 8 files changed, 242 insertions(+), 102 deletions(-) create mode 100644 src/client/src/components/ConfirmDialog.tsx diff --git a/src/client/dev-dist/sw.js b/src/client/dev-dist/sw.js index ef5d02e..5036f75 100644 --- a/src/client/dev-dist/sw.js +++ b/src/client/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.08qtrue2dho" + "revision": "0.rc445urejpk" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/src/client/src/App.tsx b/src/client/src/App.tsx index c566360..322bd80 100644 --- a/src/client/src/App.tsx +++ b/src/client/src/App.tsx @@ -7,6 +7,7 @@ import { DeckTester } from './modules/tester/DeckTester'; import { Pack } from './services/PackGeneratorService'; import { ToastProvider } from './components/Toast'; import { GlobalContextMenu } from './components/GlobalContextMenu'; +import { ConfirmDialogProvider } from './components/ConfirmDialog'; import { PWAInstallPrompt } from './components/PWAInstallPrompt'; @@ -71,72 +72,74 @@ export const App: React.FC = () => { return ( - - -
-
-
-
-
-
-

- MTG Peasant Drafter - ALPHA -

-

Pack Generator & Tournament Manager

+ + + +
+
+
+
+
+
+

+ MTG Peasant Drafter + ALPHA +

+

Pack Generator & Tournament Manager

+
+
+ +
+ + + +
+
-
- - - - -
-
-
+
+ {activeTab === 'draft' && ( + setActiveTab('lobby')} + /> + )} + {activeTab === 'lobby' && } + {activeTab === 'tester' && } + {activeTab === 'bracket' && } +
-
- {activeTab === 'draft' && ( - setActiveTab('lobby')} - /> - )} - {activeTab === 'lobby' && } - {activeTab === 'tester' && } - {activeTab === 'bracket' && } -
- -
-

- Entire code generated by Antigravity and Gemini Pro -

-
-
+
+

+ Entire code generated by Antigravity and Gemini Pro +

+
+ +
); }; diff --git a/src/client/src/components/ConfirmDialog.tsx b/src/client/src/components/ConfirmDialog.tsx new file mode 100644 index 0000000..494b9e1 --- /dev/null +++ b/src/client/src/components/ConfirmDialog.tsx @@ -0,0 +1,77 @@ +import React, { createContext, useContext, useState, useCallback, useRef } from 'react'; +import { Modal } from './Modal'; + +interface ConfirmOptions { + title: string; + message: string; + confirmLabel?: string; + cancelLabel?: string; + type?: 'info' | 'success' | 'warning' | 'error'; +} + +interface ConfirmDialogContextType { + confirm: (options: ConfirmOptions) => Promise; +} + +const ConfirmDialogContext = createContext(undefined); + +export const useConfirm = () => { + const context = useContext(ConfirmDialogContext); + if (!context) { + throw new Error('useConfirm must be used within a ConfirmDialogProvider'); + } + return context; +}; + +export const ConfirmDialogProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [isOpen, setIsOpen] = useState(false); + const [options, setOptions] = useState({ + title: '', + message: '', + confirmLabel: 'Confirm', + cancelLabel: 'Cancel', + type: 'warning', + }); + + const resolveRef = useRef<(value: boolean) => void>(() => { }); + + const confirm = useCallback((opts: ConfirmOptions) => { + setOptions({ + confirmLabel: 'Confirm', + cancelLabel: 'Cancel', + type: 'warning', + ...opts, + }); + setIsOpen(true); + + return new Promise((resolve) => { + resolveRef.current = resolve; + }); + }, []); + + const handleConfirm = useCallback(() => { + setIsOpen(false); + resolveRef.current(true); + }, []); + + const handleCancel = useCallback(() => { + setIsOpen(false); + resolveRef.current(false); + }, []); + + return ( + + {children} + + + ); +}; diff --git a/src/client/src/modules/cube/CubeManager.tsx b/src/client/src/modules/cube/CubeManager.tsx index 7ad8c05..167075a 100644 --- a/src/client/src/modules/cube/CubeManager.tsx +++ b/src/client/src/modules/cube/CubeManager.tsx @@ -5,6 +5,7 @@ import { PackGeneratorService, ProcessedPools, SetsMap, Pack, PackGenerationSett import { PackCard } from '../../components/PackCard'; import { socketService } from '../../services/SocketService'; import { useToast } from '../../components/Toast'; +import { useConfirm } from '../../components/ConfirmDialog'; interface CubeManagerProps { packs: Pack[]; @@ -16,6 +17,7 @@ interface CubeManagerProps { export const CubeManager: React.FC = ({ packs, setPacks, availableLands, setAvailableLands, onGoToLobby }) => { const { showToast } = useToast(); + const { confirm } = useConfirm(); // --- Services --- // Memoize services to persist cache across renders @@ -288,14 +290,14 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail } if (newPacks.length === 0) { - alert(`No packs generated. Check your card pool settings.`); + showToast(`No packs generated. Check your card pool settings.`, 'warning'); } else { setPacks(newPacks); setAvailableLands(newLands); } } catch (err: any) { console.error("Process failed", err); - alert(err.message || "Error during process."); + showToast(err.message || "Error during process.", 'error'); } finally { setLoading(false); setProgress(''); @@ -306,8 +308,13 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail if (packs.length === 0) return; // Validate Lands - Warn but allow proceed (server will handle it or deck builder will be landless) - if (!availableLands || availableLands.length === 0) { - if (!confirm("No basic lands detected in the current pool. Decks might be invalid. Continue?")) { + if (availableLands.length === 0) { + if (!await confirm({ + title: "No Basic Lands", + message: "No basic lands detected in the current pool. Decks might be invalid. Continue?", + confirmLabel: "Continue", + type: "warning" + })) { return; } } @@ -338,12 +345,12 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail onGoToLobby(); }, 100); } else { - alert("Failed to start solo draft: " + response.message); + showToast("Failed to start solo draft: " + response.message, 'error'); } } catch (e: any) { console.error(e); - alert("Error: " + e.message); + showToast("Error: " + e.message, 'error'); } finally { setLoading(false); } @@ -376,7 +383,7 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail 3,Banishing Light,Normal,Bloomburrow,25a06f82-ebdb-4dd6-bfe8-958018ce557c 4,Barkform Harvester,Normal,Bloomburrow,f77049a6-0f22-415b-bc89-20bcb32accf6 1,Bark-Knuckle Boxer,Normal,Bloomburrow,582637a9-6aa0-4824-bed7-d5fc91bda35e -1,"Baylen, the Haymaker",Normal,Bloomburrow,00e93be2-e06b-4774-8ba5-ccf82a6da1d8 +,"Baylen, the Haymaker",Normal,Bloomburrow,00e93be2-e06b-4774-8ba5-ccf82a6da1d8 3,Bellowing Crier,Normal,Bloomburrow,ca2215dd-6300-49cf-b9b2-3a840b786c31 1,Blacksmith's Talent,Normal,Bloomburrow,4bb318fa-481d-40a7-978e-f01b49101ae0 1,Blooming Blast,Normal,Bloomburrow,0cd92a83-cec3-4085-a929-3f204e3e0140 @@ -403,7 +410,7 @@ export const CubeManager: React.FC = ({ packs, setPacks, avail setTimeout(() => setCopySuccess(false), 2000); } catch (err) { console.error('Failed to copy: ', err); - alert('Failed to copy CSV to clipboard'); + showToast('Failed to copy CSV to clipboard', 'error'); } }; diff --git a/src/client/src/modules/draft/DeckBuilderView.tsx b/src/client/src/modules/draft/DeckBuilderView.tsx index e83744a..4b65e7b 100644 --- a/src/client/src/modules/draft/DeckBuilderView.tsx +++ b/src/client/src/modules/draft/DeckBuilderView.tsx @@ -9,6 +9,9 @@ import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSenso import { CSS } from '@dnd-kit/utilities'; import { AutoDeckBuilder } from '../../utils/AutoDeckBuilder'; import { Wand2 } from 'lucide-react'; // Import Wand icon +import { useToast } from '../../components/Toast'; +import { useConfirm } from '../../components/ConfirmDialog'; +import { CardComponent } from '../game/CardComponent'; interface DeckBuilderViewProps { roomId: string; @@ -273,6 +276,10 @@ const CardsDisplay: React.FC<{ export const DeckBuilderView: React.FC = ({ initialPool, availableBasicLands = [] }) => { // Unlimited Timer (Static for now) const [timer] = useState("Unlimited"); + /* --- Hooks --- */ + const { showToast } = useToast(); + const { confirm } = useConfirm(); + const [deckName, setDeckName] = useState('New Deck'); const [layout, setLayout] = useState<'vertical' | 'horizontal'>(() => { const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_layout') : null; return (saved as 'vertical' | 'horizontal') || 'vertical'; @@ -495,7 +502,12 @@ export const DeckBuilderView: React.FC = ({ initialPool, a }; const handleAutoBuild = async () => { - if (confirm("This will replace your current deck with an auto-generated one. Continue?")) { + if (await confirm({ + title: "Auto-Build Deck", + message: "This will replace your current deck with an auto-generated one. Continue?", + confirmLabel: "Auto-Build", + type: "warning" + })) { console.log("Auto-Build: Started"); // 1. Merge current deck back into pool (excluding basic lands generated) const currentDeckSpells = deck.filter(c => !c.isLandSource && !(c.typeLine || c.type_line || '').includes('Basic')); diff --git a/src/client/src/modules/game/GameView.tsx b/src/client/src/modules/game/GameView.tsx index 9cd1a18..045e792 100644 --- a/src/client/src/modules/game/GameView.tsx +++ b/src/client/src/modules/game/GameView.tsx @@ -1,4 +1,5 @@ -import { useRef, useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; +import { useConfirm } from '../../components/ConfirmDialog'; import { ChevronLeft, Eye, RotateCcw } from 'lucide-react'; import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core'; import { CSS } from '@dnd-kit/utilities'; @@ -160,7 +161,7 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } document.body.style.cursor = 'col-resize'; }; - const onResizeMove = useCallback((e: MouseEvent | TouchEvent) => { + const onResizeMove = (e: MouseEvent | TouchEvent) => { if (!resizingState.current.active || !sidebarRef.current) return; if (e.cancelable) e.preventDefault(); @@ -168,9 +169,9 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } const delta = clientX - resizingState.current.startX; const newWidth = Math.max(200, Math.min(600, resizingState.current.startWidth + delta)); sidebarRef.current.style.width = `${newWidth}px`; - }, []); + }; - const onResizeEnd = useCallback(() => { + const onResizeEnd = () => { if (resizingState.current.active && sidebarRef.current) { setSidebarWidth(parseInt(sidebarRef.current.style.width)); } @@ -180,7 +181,7 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } document.removeEventListener('mouseup', onResizeEnd); document.removeEventListener('touchend', onResizeEnd); document.body.style.cursor = 'default'; - }, []); + }; useEffect(() => { // Disable default context menu @@ -299,7 +300,9 @@ export const GameView: React.FC = ({ gameState, currentPlayerId } } }; - // --- DnD Sensors & Logic --- + // --- Hooks & Services --- + // const { showToast } = useToast(); // Assuming useToast is defined elsewhere if needed + const { confirm } = useConfirm(); const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 10 } }), useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }) @@ -884,8 +887,13 @@ export const GameView: React.FC = ({ gameState, currentPlayerId }