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.
Some checks failed
Build and Deploy / build (push) Failing after 15m40s
Some checks failed
Build and Deploy / build (push) Failing after 15m40s
This commit is contained in:
@@ -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"), {
|
||||
|
||||
@@ -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,6 +72,7 @@ export const App: React.FC = () => {
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<ConfirmDialogProvider>
|
||||
<GlobalContextMenu />
|
||||
<PWAInstallPrompt />
|
||||
<div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
|
||||
@@ -137,6 +139,7 @@ export const App: React.FC = () => {
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
</ConfirmDialogProvider>
|
||||
</ToastProvider>
|
||||
);
|
||||
};
|
||||
|
||||
77
src/client/src/components/ConfirmDialog.tsx
Normal file
77
src/client/src/components/ConfirmDialog.tsx
Normal file
@@ -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<boolean>;
|
||||
}
|
||||
|
||||
const ConfirmDialogContext = createContext<ConfirmDialogContextType | undefined>(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<ConfirmOptions>({
|
||||
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<boolean>((resolve) => {
|
||||
resolveRef.current = resolve;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
resolveRef.current(true);
|
||||
}, []);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
resolveRef.current(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ConfirmDialogContext.Provider value={{ confirm }}>
|
||||
{children}
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleCancel}
|
||||
title={options.title}
|
||||
message={options.message}
|
||||
type={options.type}
|
||||
confirmLabel={options.confirmLabel}
|
||||
cancelLabel={options.cancelLabel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</ConfirmDialogContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -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<CubeManagerProps> = ({ 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<CubeManagerProps> = ({ 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<CubeManagerProps> = ({ 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<CubeManagerProps> = ({ 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<CubeManagerProps> = ({ 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<CubeManagerProps> = ({ 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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
|
||||
// Unlimited Timer (Static for now)
|
||||
const [timer] = useState<string>("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<DeckBuilderViewProps> = ({ 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'));
|
||||
|
||||
@@ -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<GameViewProps> = ({ 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<GameViewProps> = ({ 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<GameViewProps> = ({ 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<GameViewProps> = ({ 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<GameViewProps> = ({ gameState, currentPlayerId }
|
||||
<button
|
||||
className="absolute top-0 right-0 p-1 text-slate-600 hover:text-white transition-colors"
|
||||
title="Restart Game (Dev)"
|
||||
onClick={() => {
|
||||
if (window.confirm('Restart game? Deck will remain, state will reset.')) {
|
||||
onClick={async () => {
|
||||
if (await confirm({
|
||||
title: 'Restart Game?',
|
||||
message: 'Are you sure you want to restart the game? The deck will remain, but the game state will reset.',
|
||||
confirmLabel: 'Restart',
|
||||
type: 'warning'
|
||||
})) {
|
||||
socketService.socket.emit('game_action', { action: { type: 'RESTART_GAME' } });
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { socketService } from '../../services/SocketService';
|
||||
import { Users, MessageSquare, Send, Copy, Check, Layers, LogOut, Bell, BellOff, X, Bot } from 'lucide-react';
|
||||
import { Share2, Users, Play, LogOut, Copy, Check, Hash, Crown, XCircle, MessageSquare, Send, Bell, BellOff, X, Bot, Layers } from 'lucide-react';
|
||||
import { useConfirm } from '../../components/ConfirmDialog';
|
||||
import { Modal } from '../../components/Modal';
|
||||
import { useToast } from '../../components/Toast';
|
||||
import { GameView } from '../game/GameView';
|
||||
@@ -45,7 +45,15 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
// State
|
||||
const [room, setRoom] = useState<Room>(initialRoom);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalConfig, setModalConfig] = useState({ title: '', message: '', type: 'info' as 'info' | 'error' | 'warning' | 'success' });
|
||||
const [modalConfig, setModalConfig] = useState<{
|
||||
title: string;
|
||||
message: string;
|
||||
type: 'info' | 'error' | 'warning' | 'success';
|
||||
confirmLabel?: string;
|
||||
onConfirm?: () => void;
|
||||
cancelLabel?: string;
|
||||
onClose?: () => void;
|
||||
}>({ title: '', message: '', type: 'info' });
|
||||
|
||||
// Side Panel State
|
||||
const [activePanel, setActivePanel] = useState<'lobby' | 'chat' | null>(null);
|
||||
@@ -55,6 +63,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
|
||||
// Services
|
||||
const { showToast } = useToast();
|
||||
const { confirm } = useConfirm();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Restored States
|
||||
const [message, setMessage] = useState('');
|
||||
@@ -132,8 +142,16 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
useEffect(() => {
|
||||
const socket = socketService.socket;
|
||||
const onKicked = () => {
|
||||
alert("You have been kicked from the room.");
|
||||
onExit();
|
||||
// alert("You have been kicked from the room.");
|
||||
// onExit();
|
||||
setModalConfig({
|
||||
title: 'Kicked',
|
||||
message: 'You have been kicked from the room.',
|
||||
type: 'error',
|
||||
confirmLabel: 'Back to Lobby',
|
||||
onConfirm: () => onExit()
|
||||
});
|
||||
setModalOpen(true);
|
||||
};
|
||||
socket.on('kicked', onKicked);
|
||||
return () => { socket.off('kicked', onKicked); };
|
||||
@@ -453,8 +471,13 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
<div className={`flex gap - 1 ${isSolo ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition - opacity`}>
|
||||
{isMeHost && !isMe && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`Kick ${p.name}?`)) {
|
||||
onClick={async () => {
|
||||
if (await confirm({
|
||||
title: 'Kick Player?',
|
||||
message: `Are you sure you want to kick ${p.name}?`,
|
||||
confirmLabel: 'Kick',
|
||||
type: 'error'
|
||||
})) {
|
||||
socketService.socket.emit('kick_player', { roomId: room.id, targetId: p.id });
|
||||
}
|
||||
}}
|
||||
@@ -548,8 +571,13 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.confirm("Are you sure you want to leave the game?")) {
|
||||
onClick={async () => {
|
||||
if (await confirm({
|
||||
title: 'Leave Game?',
|
||||
message: "Are you sure you want to leave the game? You can rejoin later.",
|
||||
confirmLabel: 'Leave',
|
||||
type: 'warning'
|
||||
})) {
|
||||
onExit();
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Users } from 'lucide-react';
|
||||
import { useToast } from '../../components/Toast';
|
||||
|
||||
interface Match {
|
||||
id: number;
|
||||
@@ -15,6 +16,7 @@ interface Bracket {
|
||||
export const TournamentManager: React.FC = () => {
|
||||
const [playerInput, setPlayerInput] = useState('');
|
||||
const [bracket, setBracket] = useState<Bracket | null>(null);
|
||||
const { showToast } = useToast();
|
||||
|
||||
const shuffleArray = (array: any[]) => {
|
||||
let currentIndex = array.length, randomIndex;
|
||||
@@ -30,7 +32,10 @@ export const TournamentManager: React.FC = () => {
|
||||
const generateBracket = () => {
|
||||
if (!playerInput.trim()) return;
|
||||
const names = playerInput.split('\n').filter(n => n.trim() !== '').map(n => n.trim());
|
||||
if (names.length < 2) { alert("Enter at least 2 players."); return; }
|
||||
if (names.length < 2) {
|
||||
showToast("Enter at least 2 players.", 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const shuffled = shuffleArray(names);
|
||||
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(shuffled.length)));
|
||||
|
||||
Reference in New Issue
Block a user