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"
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.08qtrue2dho"
|
"revision": "0.rc445urejpk"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
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 { Pack } from './services/PackGeneratorService';
|
||||||
import { ToastProvider } from './components/Toast';
|
import { ToastProvider } from './components/Toast';
|
||||||
import { GlobalContextMenu } from './components/GlobalContextMenu';
|
import { GlobalContextMenu } from './components/GlobalContextMenu';
|
||||||
|
import { ConfirmDialogProvider } from './components/ConfirmDialog';
|
||||||
|
|
||||||
import { PWAInstallPrompt } from './components/PWAInstallPrompt';
|
import { PWAInstallPrompt } from './components/PWAInstallPrompt';
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ export const App: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
|
<ConfirmDialogProvider>
|
||||||
<GlobalContextMenu />
|
<GlobalContextMenu />
|
||||||
<PWAInstallPrompt />
|
<PWAInstallPrompt />
|
||||||
<div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
|
<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>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
</ConfirmDialogProvider>
|
||||||
</ToastProvider>
|
</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 { PackCard } from '../../components/PackCard';
|
||||||
import { socketService } from '../../services/SocketService';
|
import { socketService } from '../../services/SocketService';
|
||||||
import { useToast } from '../../components/Toast';
|
import { useToast } from '../../components/Toast';
|
||||||
|
import { useConfirm } from '../../components/ConfirmDialog';
|
||||||
|
|
||||||
interface CubeManagerProps {
|
interface CubeManagerProps {
|
||||||
packs: Pack[];
|
packs: Pack[];
|
||||||
@@ -16,6 +17,7 @@ interface CubeManagerProps {
|
|||||||
|
|
||||||
export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, availableLands, setAvailableLands, onGoToLobby }) => {
|
export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, availableLands, setAvailableLands, onGoToLobby }) => {
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
|
||||||
// --- Services ---
|
// --- Services ---
|
||||||
// Memoize services to persist cache across renders
|
// Memoize services to persist cache across renders
|
||||||
@@ -288,14 +290,14 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newPacks.length === 0) {
|
if (newPacks.length === 0) {
|
||||||
alert(`No packs generated. Check your card pool settings.`);
|
showToast(`No packs generated. Check your card pool settings.`, 'warning');
|
||||||
} else {
|
} else {
|
||||||
setPacks(newPacks);
|
setPacks(newPacks);
|
||||||
setAvailableLands(newLands);
|
setAvailableLands(newLands);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Process failed", err);
|
console.error("Process failed", err);
|
||||||
alert(err.message || "Error during process.");
|
showToast(err.message || "Error during process.", 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setProgress('');
|
setProgress('');
|
||||||
@@ -306,8 +308,13 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
|||||||
if (packs.length === 0) return;
|
if (packs.length === 0) return;
|
||||||
|
|
||||||
// Validate Lands - Warn but allow proceed (server will handle it or deck builder will be landless)
|
// Validate Lands - Warn but allow proceed (server will handle it or deck builder will be landless)
|
||||||
if (!availableLands || availableLands.length === 0) {
|
if (availableLands.length === 0) {
|
||||||
if (!confirm("No basic lands detected in the current pool. Decks might be invalid. Continue?")) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,12 +345,12 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, avail
|
|||||||
onGoToLobby();
|
onGoToLobby();
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to start solo draft: " + response.message);
|
showToast("Failed to start solo draft: " + response.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Error: " + e.message);
|
showToast("Error: " + e.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
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
|
3,Banishing Light,Normal,Bloomburrow,25a06f82-ebdb-4dd6-bfe8-958018ce557c
|
||||||
4,Barkform Harvester,Normal,Bloomburrow,f77049a6-0f22-415b-bc89-20bcb32accf6
|
4,Barkform Harvester,Normal,Bloomburrow,f77049a6-0f22-415b-bc89-20bcb32accf6
|
||||||
1,Bark-Knuckle Boxer,Normal,Bloomburrow,582637a9-6aa0-4824-bed7-d5fc91bda35e
|
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
|
3,Bellowing Crier,Normal,Bloomburrow,ca2215dd-6300-49cf-b9b2-3a840b786c31
|
||||||
1,Blacksmith's Talent,Normal,Bloomburrow,4bb318fa-481d-40a7-978e-f01b49101ae0
|
1,Blacksmith's Talent,Normal,Bloomburrow,4bb318fa-481d-40a7-978e-f01b49101ae0
|
||||||
1,Blooming Blast,Normal,Bloomburrow,0cd92a83-cec3-4085-a929-3f204e3e0140
|
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);
|
setTimeout(() => setCopySuccess(false), 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy: ', 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 { CSS } from '@dnd-kit/utilities';
|
||||||
import { AutoDeckBuilder } from '../../utils/AutoDeckBuilder';
|
import { AutoDeckBuilder } from '../../utils/AutoDeckBuilder';
|
||||||
import { Wand2 } from 'lucide-react'; // Import Wand icon
|
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 {
|
interface DeckBuilderViewProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -273,6 +276,10 @@ const CardsDisplay: React.FC<{
|
|||||||
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
|
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
|
||||||
// Unlimited Timer (Static for now)
|
// Unlimited Timer (Static for now)
|
||||||
const [timer] = useState<string>("Unlimited");
|
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 [layout, setLayout] = useState<'vertical' | 'horizontal'>(() => {
|
||||||
const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_layout') : null;
|
const saved = typeof window !== 'undefined' ? localStorage.getItem('deck_layout') : null;
|
||||||
return (saved as 'vertical' | 'horizontal') || 'vertical';
|
return (saved as 'vertical' | 'horizontal') || 'vertical';
|
||||||
@@ -495,7 +502,12 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAutoBuild = async () => {
|
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");
|
console.log("Auto-Build: Started");
|
||||||
// 1. Merge current deck back into pool (excluding basic lands generated)
|
// 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'));
|
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 { ChevronLeft, Eye, RotateCcw } from 'lucide-react';
|
||||||
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
|
import { DndContext, DragOverlay, useSensor, useSensors, MouseSensor, TouchSensor, DragStartEvent, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
@@ -160,7 +161,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
document.body.style.cursor = 'col-resize';
|
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 (!resizingState.current.active || !sidebarRef.current) return;
|
||||||
if (e.cancelable) e.preventDefault();
|
if (e.cancelable) e.preventDefault();
|
||||||
|
|
||||||
@@ -168,9 +169,9 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
const delta = clientX - resizingState.current.startX;
|
const delta = clientX - resizingState.current.startX;
|
||||||
const newWidth = Math.max(200, Math.min(600, resizingState.current.startWidth + delta));
|
const newWidth = Math.max(200, Math.min(600, resizingState.current.startWidth + delta));
|
||||||
sidebarRef.current.style.width = `${newWidth}px`;
|
sidebarRef.current.style.width = `${newWidth}px`;
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const onResizeEnd = useCallback(() => {
|
const onResizeEnd = () => {
|
||||||
if (resizingState.current.active && sidebarRef.current) {
|
if (resizingState.current.active && sidebarRef.current) {
|
||||||
setSidebarWidth(parseInt(sidebarRef.current.style.width));
|
setSidebarWidth(parseInt(sidebarRef.current.style.width));
|
||||||
}
|
}
|
||||||
@@ -180,7 +181,7 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
document.removeEventListener('mouseup', onResizeEnd);
|
document.removeEventListener('mouseup', onResizeEnd);
|
||||||
document.removeEventListener('touchend', onResizeEnd);
|
document.removeEventListener('touchend', onResizeEnd);
|
||||||
document.body.style.cursor = 'default';
|
document.body.style.cursor = 'default';
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Disable default context menu
|
// 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(
|
const sensors = useSensors(
|
||||||
useSensor(MouseSensor, { activationConstraint: { distance: 10 } }),
|
useSensor(MouseSensor, { activationConstraint: { distance: 10 } }),
|
||||||
useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } })
|
useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } })
|
||||||
@@ -884,8 +887,13 @@ export const GameView: React.FC<GameViewProps> = ({ gameState, currentPlayerId }
|
|||||||
<button
|
<button
|
||||||
className="absolute top-0 right-0 p-1 text-slate-600 hover:text-white transition-colors"
|
className="absolute top-0 right-0 p-1 text-slate-600 hover:text-white transition-colors"
|
||||||
title="Restart Game (Dev)"
|
title="Restart Game (Dev)"
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (window.confirm('Restart game? Deck will remain, state will reset.')) {
|
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' } });
|
socketService.socket.emit('game_action', { action: { type: 'RESTART_GAME' } });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { socketService } from '../../services/SocketService';
|
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 { Modal } from '../../components/Modal';
|
||||||
import { useToast } from '../../components/Toast';
|
import { useToast } from '../../components/Toast';
|
||||||
import { GameView } from '../game/GameView';
|
import { GameView } from '../game/GameView';
|
||||||
@@ -45,7 +45,15 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
// State
|
// State
|
||||||
const [room, setRoom] = useState<Room>(initialRoom);
|
const [room, setRoom] = useState<Room>(initialRoom);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
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
|
// Side Panel State
|
||||||
const [activePanel, setActivePanel] = useState<'lobby' | 'chat' | null>(null);
|
const [activePanel, setActivePanel] = useState<'lobby' | 'chat' | null>(null);
|
||||||
@@ -55,6 +63,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { confirm } = useConfirm();
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
// Restored States
|
// Restored States
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
@@ -132,8 +142,16 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = socketService.socket;
|
const socket = socketService.socket;
|
||||||
const onKicked = () => {
|
const onKicked = () => {
|
||||||
alert("You have been kicked from the room.");
|
// alert("You have been kicked from the room.");
|
||||||
onExit();
|
// 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);
|
socket.on('kicked', onKicked);
|
||||||
return () => { socket.off('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`}>
|
<div className={`flex gap - 1 ${isSolo ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition - opacity`}>
|
||||||
{isMeHost && !isMe && (
|
{isMeHost && !isMe && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (confirm(`Kick ${p.name}?`)) {
|
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 });
|
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>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (window.confirm("Are you sure you want to leave the game?")) {
|
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();
|
onExit();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
|
import { useToast } from '../../components/Toast';
|
||||||
|
|
||||||
interface Match {
|
interface Match {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -15,6 +16,7 @@ interface Bracket {
|
|||||||
export const TournamentManager: React.FC = () => {
|
export const TournamentManager: React.FC = () => {
|
||||||
const [playerInput, setPlayerInput] = useState('');
|
const [playerInput, setPlayerInput] = useState('');
|
||||||
const [bracket, setBracket] = useState<Bracket | null>(null);
|
const [bracket, setBracket] = useState<Bracket | null>(null);
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
const shuffleArray = (array: any[]) => {
|
const shuffleArray = (array: any[]) => {
|
||||||
let currentIndex = array.length, randomIndex;
|
let currentIndex = array.length, randomIndex;
|
||||||
@@ -30,7 +32,10 @@ export const TournamentManager: React.FC = () => {
|
|||||||
const generateBracket = () => {
|
const generateBracket = () => {
|
||||||
if (!playerInput.trim()) return;
|
if (!playerInput.trim()) return;
|
||||||
const names = playerInput.split('\n').filter(n => n.trim() !== '').map(n => n.trim());
|
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 shuffled = shuffleArray(names);
|
||||||
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(shuffled.length)));
|
const nextPowerOf2 = Math.pow(2, Math.ceil(Math.log2(shuffled.length)));
|
||||||
|
|||||||
Reference in New Issue
Block a user