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,72 +72,74 @@ export const App: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<GlobalContextMenu />
|
<ConfirmDialogProvider>
|
||||||
<PWAInstallPrompt />
|
<GlobalContextMenu />
|
||||||
<div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
|
<PWAInstallPrompt />
|
||||||
<header className="bg-slate-800 border-b border-slate-700 p-4 shrink-0 z-50 shadow-lg">
|
<div className="h-screen flex flex-col bg-slate-900 text-slate-100 font-sans overflow-hidden">
|
||||||
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
|
<header className="bg-slate-800 border-b border-slate-700 p-4 shrink-0 z-50 shadow-lg">
|
||||||
<div className="flex items-center gap-3">
|
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-4">
|
||||||
<div className="bg-purple-600 p-2 rounded-lg"><Layers className="w-6 h-6 text-white" /></div>
|
<div className="flex items-center gap-3">
|
||||||
<div>
|
<div className="bg-purple-600 p-2 rounded-lg"><Layers className="w-6 h-6 text-white" /></div>
|
||||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent flex items-center gap-2">
|
<div>
|
||||||
MTG Peasant Drafter
|
<h1 className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent flex items-center gap-2">
|
||||||
<span className="px-1.5 py-0.5 rounded-md bg-purple-500/10 border border-purple-500/20 text-[10px] font-bold text-purple-400 tracking-wider shadow-[0_0_10px_rgba(168,85,247,0.1)]">ALPHA</span>
|
MTG Peasant Drafter
|
||||||
</h1>
|
<span className="px-1.5 py-0.5 rounded-md bg-purple-500/10 border border-purple-500/20 text-[10px] font-bold text-purple-400 tracking-wider shadow-[0_0_10px_rgba(168,85,247,0.1)]">ALPHA</span>
|
||||||
<p className="text-slate-400 text-xs uppercase tracking-wider">Pack Generator & Tournament Manager</p>
|
</h1>
|
||||||
|
<p className="text-slate-400 text-xs uppercase tracking-wider">Pack Generator & Tournament Manager</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('draft')}
|
||||||
|
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'draft' ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-4 h-4" /> <span className="hidden md:inline">Draft Management</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('lobby')}
|
||||||
|
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'lobby' ? 'bg-emerald-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
||||||
|
>
|
||||||
|
<Users className="w-4 h-4" /> <span className="hidden md:inline">Online Lobby</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('tester')}
|
||||||
|
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'tester' ? 'bg-teal-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4" /> <span className="hidden md:inline">Deck Tester</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('bracket')}
|
||||||
|
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'bracket' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
||||||
|
>
|
||||||
|
<Trophy className="w-4 h-4" /> <span className="hidden md:inline">Tournament</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div className="flex bg-slate-900 rounded-lg p-1 border border-slate-700">
|
<main className="flex-1 overflow-hidden relative">
|
||||||
<button
|
{activeTab === 'draft' && (
|
||||||
onClick={() => setActiveTab('draft')}
|
<CubeManager
|
||||||
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'draft' ? 'bg-purple-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
packs={generatedPacks}
|
||||||
>
|
setPacks={setGeneratedPacks}
|
||||||
<Box className="w-4 h-4" /> <span className="hidden md:inline">Draft Management</span>
|
availableLands={availableLands}
|
||||||
</button>
|
setAvailableLands={setAvailableLands}
|
||||||
<button
|
onGoToLobby={() => setActiveTab('lobby')}
|
||||||
onClick={() => setActiveTab('lobby')}
|
/>
|
||||||
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'lobby' ? 'bg-emerald-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
)}
|
||||||
>
|
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} availableLands={availableLands} />}
|
||||||
<Users className="w-4 h-4" /> <span className="hidden md:inline">Online Lobby</span>
|
{activeTab === 'tester' && <DeckTester />}
|
||||||
</button>
|
{activeTab === 'bracket' && <TournamentManager />}
|
||||||
<button
|
</main>
|
||||||
onClick={() => setActiveTab('tester')}
|
|
||||||
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'tester' ? 'bg-teal-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
|
||||||
>
|
|
||||||
<Play className="w-4 h-4" /> <span className="hidden md:inline">Deck Tester</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('bracket')}
|
|
||||||
className={`px-3 md:px-4 py-2 rounded-md text-sm font-bold flex items-center gap-2 transition-all ${activeTab === 'bracket' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white'}`}
|
|
||||||
>
|
|
||||||
<Trophy className="w-4 h-4" /> <span className="hidden md:inline">Tournament</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="flex-1 overflow-hidden relative">
|
<footer className="bg-slate-900 border-t border-slate-800 p-2 text-center text-xs text-slate-500 shrink-0">
|
||||||
{activeTab === 'draft' && (
|
<p>
|
||||||
<CubeManager
|
Entire code generated by <span className="text-purple-400 font-medium">Antigravity</span> and <span className="text-sky-400 font-medium">Gemini Pro</span>
|
||||||
packs={generatedPacks}
|
</p>
|
||||||
setPacks={setGeneratedPacks}
|
</footer>
|
||||||
availableLands={availableLands}
|
</div>
|
||||||
setAvailableLands={setAvailableLands}
|
</ConfirmDialogProvider>
|
||||||
onGoToLobby={() => setActiveTab('lobby')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} availableLands={availableLands} />}
|
|
||||||
{activeTab === 'tester' && <DeckTester />}
|
|
||||||
{activeTab === 'bracket' && <TournamentManager />}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<footer className="bg-slate-900 border-t border-slate-800 p-2 text-center text-xs text-slate-500 shrink-0">
|
|
||||||
<p>
|
|
||||||
Entire code generated by <span className="text-purple-400 font-medium">Antigravity</span> and <span className="text-sky-400 font-medium">Gemini Pro</span>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</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); };
|
||||||
@@ -238,8 +256,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
{room.players.filter(p => p.role === 'player').map(p => {
|
{room.players.filter(p => p.role === 'player').map(p => {
|
||||||
const isReady = (p as any).ready;
|
const isReady = (p as any).ready;
|
||||||
return (
|
return (
|
||||||
<div key={p.id} className={`flex items-center gap-2 px-4 py-2 rounded-lg border ${isReady ? 'bg-emerald-900/30 border-emerald-500/50' : 'bg-slate-700/30 border-slate-700'}`}>
|
<div key={p.id} className={`flex items - center gap - 2 px - 4 py - 2 rounded - lg border ${isReady ? 'bg-emerald-900/30 border-emerald-500/50' : 'bg-slate-700/30 border-slate-700'} `}>
|
||||||
<div className={`w-2 h-2 rounded-full ${isReady ? 'bg-emerald-500' : 'bg-slate-600'}`}></div>
|
<div className={`w - 2 h - 2 rounded - full ${isReady ? 'bg-emerald-500' : 'bg-slate-600'} `}></div>
|
||||||
<span className={isReady ? 'text-emerald-200' : 'text-slate-500'}>{p.name}</span>
|
<span className={isReady ? 'text-emerald-200' : 'text-slate-500'}>{p.name}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -305,13 +323,13 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
<div className="shrink-0 flex items-center bg-slate-800 border-b border-slate-700">
|
<div className="shrink-0 flex items-center bg-slate-800 border-b border-slate-700">
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileTab('game')}
|
onClick={() => setMobileTab('game')}
|
||||||
className={`flex-1 p-3 flex items-center justify-center gap-2 text-sm font-bold transition-colors ${mobileTab === 'game' ? 'text-emerald-400 bg-slate-700/50 border-b-2 border-emerald-500' : 'text-slate-400 hover:text-slate-200'}`}
|
className={`flex - 1 p - 3 flex items - center justify - center gap - 2 text - sm font - bold transition - colors ${mobileTab === 'game' ? 'text-emerald-400 bg-slate-700/50 border-b-2 border-emerald-500' : 'text-slate-400 hover:text-slate-200'} `}
|
||||||
>
|
>
|
||||||
<Layers className="w-4 h-4" /> Game
|
<Layers className="w-4 h-4" /> Game
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileTab('chat')}
|
onClick={() => setMobileTab('chat')}
|
||||||
className={`flex-1 p-3 flex items-center justify-center gap-2 text-sm font-bold transition-colors ${mobileTab === 'chat' ? 'text-purple-400 bg-slate-700/50 border-b-2 border-purple-500' : 'text-slate-400 hover:text-slate-200'}`}
|
className={`flex - 1 p - 3 flex items - center justify - center gap - 2 text - sm font - bold transition - colors ${mobileTab === 'chat' ? 'text-purple-400 bg-slate-700/50 border-b-2 border-purple-500' : 'text-slate-400 hover:text-slate-200'} `}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Users className="w-4 h-4" />
|
<Users className="w-4 h-4" />
|
||||||
@@ -369,7 +387,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
<div className="hidden lg:flex w-14 shrink-0 flex-col items-center gap-4 py-4 bg-slate-900 border-l border-slate-800 z-30 relative">
|
<div className="hidden lg:flex w-14 shrink-0 flex-col items-center gap-4 py-4 bg-slate-900 border-l border-slate-800 z-30 relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActivePanel(activePanel === 'lobby' ? null : 'lobby')}
|
onClick={() => setActivePanel(activePanel === 'lobby' ? null : 'lobby')}
|
||||||
className={`p-3 rounded-xl transition-all duration-200 group relative ${activePanel === 'lobby' ? 'bg-purple-600 text-white shadow-lg shadow-purple-900/50' : 'text-slate-500 hover:text-purple-400 hover:bg-slate-800'}`}
|
className={`p - 3 rounded - xl transition - all duration - 200 group relative ${activePanel === 'lobby' ? 'bg-purple-600 text-white shadow-lg shadow-purple-900/50' : 'text-slate-500 hover:text-purple-400 hover:bg-slate-800'} `}
|
||||||
title="Lobby & Players"
|
title="Lobby & Players"
|
||||||
>
|
>
|
||||||
<Users className="w-6 h-6" />
|
<Users className="w-6 h-6" />
|
||||||
@@ -380,7 +398,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setActivePanel(activePanel === 'chat' ? null : 'chat')}
|
onClick={() => setActivePanel(activePanel === 'chat' ? null : 'chat')}
|
||||||
className={`p-3 rounded-xl transition-all duration-200 group relative ${activePanel === 'chat' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/50' : 'text-slate-500 hover:text-blue-400 hover:bg-slate-800'}`}
|
className={`p - 3 rounded - xl transition - all duration - 200 group relative ${activePanel === 'chat' ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/50' : 'text-slate-500 hover:text-blue-400 hover:bg-slate-800'} `}
|
||||||
title="Chat"
|
title="Chat"
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -415,7 +433,7 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">{room.players.length} Connected</span>
|
<span className="text-xs font-bold text-slate-500 uppercase tracking-wider">{room.players.length} Connected</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setNotificationsEnabled(!notificationsEnabled)}
|
onClick={() => setNotificationsEnabled(!notificationsEnabled)}
|
||||||
className={`flex items-center gap-2 text-xs font-bold px-2 py-1 rounded-lg transition-colors border ${notificationsEnabled ? 'bg-slate-800 border-slate-600 text-slate-300 hover:text-white' : 'bg-red-900/20 border-red-900/50 text-red-400'}`}
|
className={`flex items - center gap - 2 text - xs font - bold px - 2 py - 1 rounded - lg transition - colors border ${notificationsEnabled ? 'bg-slate-800 border-slate-600 text-slate-300 hover:text-white' : 'bg-red-900/20 border-red-900/50 text-red-400'} `}
|
||||||
title={notificationsEnabled ? "Disable Notifications" : "Enable Notifications"}
|
title={notificationsEnabled ? "Disable Notifications" : "Enable Notifications"}
|
||||||
>
|
>
|
||||||
{notificationsEnabled ? <Bell className="w-3 h-3" /> : <BellOff className="w-3 h-3" />}
|
{notificationsEnabled ? <Bell className="w-3 h-3" /> : <BellOff className="w-3 h-3" />}
|
||||||
@@ -433,11 +451,11 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
return (
|
return (
|
||||||
<div key={p.id} className="flex items-center justify-between bg-slate-900/80 p-3 rounded-xl border border-slate-700/50 hover:border-slate-600 transition-colors group">
|
<div key={p.id} className="flex items-center justify-between bg-slate-900/80 p-3 rounded-xl border border-slate-700/50 hover:border-slate-600 transition-colors group">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm shadow-inner ${p.isBot ? 'bg-indigo-900 text-indigo-200 border border-indigo-500' : p.role === 'spectator' ? 'bg-slate-800 text-slate-500' : 'bg-gradient-to-br from-purple-600 to-blue-600 text-white shadow-purple-900/30'}`}>
|
<div className={`w - 10 h - 10 rounded - full flex items - center justify - center font - bold text - sm shadow - inner ${p.isBot ? 'bg-indigo-900 text-indigo-200 border border-indigo-500' : p.role === 'spectator' ? 'bg-slate-800 text-slate-500' : 'bg-gradient-to-br from-purple-600 to-blue-600 text-white shadow-purple-900/30'} `}>
|
||||||
{p.isBot ? <Bot className="w-5 h-5" /> : p.name.substring(0, 2).toUpperCase()}
|
{p.isBot ? <Bot className="w-5 h-5" /> : p.name.substring(0, 2).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className={`text-sm font-bold ${isMe ? 'text-white' : 'text-slate-200'}`}>
|
<span className={`text - sm font - bold ${isMe ? 'text-white' : 'text-slate-200'} `}>
|
||||||
{p.name} {isMe && <span className="text-slate-500 font-normal">(You)</span>}
|
{p.name} {isMe && <span className="text-slate-500 font-normal">(You)</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-500 flex items-center gap-1">
|
<span className="text-[10px] uppercase font-bold tracking-wider text-slate-500 flex items-center gap-1">
|
||||||
@@ -450,11 +468,16 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -498,8 +521,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{messages.map(msg => (
|
{messages.map(msg => (
|
||||||
<div key={msg.id} className={`flex flex-col ${msg.sender === (room.players.find(p => p.id === currentPlayerId)?.name) ? 'items-end' : 'items-start'}`}>
|
<div key={msg.id} className={`flex flex - col ${msg.sender === (room.players.find(p => p.id === currentPlayerId)?.name) ? 'items-end' : 'items-start'} `}>
|
||||||
<div className={`max-w-[85%] px-3 py-2 rounded-xl text-sm ${msg.sender === (room.players.find(p => p.id === currentPlayerId)?.name) ? 'bg-blue-600 text-white rounded-br-none shadow-blue-900/20' : 'bg-slate-700 text-slate-200 rounded-bl-none'}`}>
|
<div className={`max - w - [85 %] px - 3 py - 2 rounded - xl text - sm ${msg.sender === (room.players.find(p => p.id === currentPlayerId)?.name) ? 'bg-blue-600 text-white rounded-br-none shadow-blue-900/20' : 'bg-slate-700 text-slate-200 rounded-bl-none'} `}>
|
||||||
{msg.text}
|
{msg.text}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[10px] text-slate-500 mt-1 font-medium">{msg.sender}</span>
|
<span className="text-[10px] text-slate-500 mt-1 font-medium">{msg.sender}</span>
|
||||||
@@ -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