created the tournament ui and fixed the turns sequence
This commit is contained in:
@@ -82,7 +82,7 @@ define(['./workbox-5a5d9309'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.gg4oatbh7is"
|
||||
"revision": "0.ca9afac9bpo"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Layers, Box, Trophy, Users, Play } from 'lucide-react';
|
||||
import { CubeManager } from './modules/cube/CubeManager';
|
||||
import { TournamentManager } from './modules/tournament/TournamentManager';
|
||||
import { LobbyManager } from './modules/lobby/LobbyManager';
|
||||
import { DeckTester } from './modules/tester/DeckTester';
|
||||
import { Pack } from './services/PackGeneratorService';
|
||||
@@ -130,7 +129,13 @@ export const App: React.FC = () => {
|
||||
)}
|
||||
{activeTab === 'lobby' && <LobbyManager generatedPacks={generatedPacks} availableLands={availableLands} />}
|
||||
{activeTab === 'tester' && <DeckTester />}
|
||||
{activeTab === 'bracket' && <TournamentManager />}
|
||||
{activeTab === 'bracket' && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-400">
|
||||
<Trophy className="w-16 h-16 mb-4 opacity-50" />
|
||||
<h2 className="text-xl font-bold">Tournament Manager</h2>
|
||||
<p>Tournaments are now managed within the Online Lobby.</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="bg-slate-900 border-t border-slate-800 p-2 text-center text-xs text-slate-500 shrink-0">
|
||||
|
||||
@@ -11,6 +11,7 @@ interface SidePanelPreviewProps {
|
||||
onToggleCollapse: (collapsed: boolean) => void;
|
||||
onResizeStart?: (e: React.MouseEvent | React.TouchEvent) => void;
|
||||
className?: string; // For additional styling (positioning, z-index, etc)
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SidePanelPreview: React.FC<SidePanelPreviewProps> = ({
|
||||
|
||||
@@ -17,7 +17,10 @@ interface DeckBuilderViewProps {
|
||||
roomId: string;
|
||||
currentPlayerId: string;
|
||||
initialPool: any[];
|
||||
initialDeck?: any[];
|
||||
availableBasicLands?: any[];
|
||||
onSubmit?: (deck: any[]) => void;
|
||||
submitLabel?: string;
|
||||
}
|
||||
|
||||
const ManaCurve = ({ deck }: { deck: any[] }) => {
|
||||
@@ -176,6 +179,40 @@ const ListItem: React.FC<{ card: DraftCard; onClick?: () => void; onHover?: (c:
|
||||
);
|
||||
};
|
||||
|
||||
const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) => {
|
||||
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
||||
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover, () => {
|
||||
if (window.matchMedia('(pointer: coarse)').matches) {
|
||||
onHover(card);
|
||||
} else {
|
||||
onCardClick(card);
|
||||
}
|
||||
}, card);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => onHover(card)}
|
||||
onMouseLeave={() => onHover(null)}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onTouchMove={onTouchMove}
|
||||
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
|
||||
>
|
||||
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
||||
{isFoil && <FoilOverlay />}
|
||||
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
|
||||
{displayImage ? (
|
||||
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" draggable={false} />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
|
||||
)}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Extracted Component to avoid re-mounting issues
|
||||
const CardsDisplay: React.FC<{
|
||||
cards: any[];
|
||||
@@ -273,7 +310,7 @@ const CardsDisplay: React.FC<{
|
||||
)
|
||||
};
|
||||
|
||||
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, availableBasicLands = [] }) => {
|
||||
export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, initialDeck = [], availableBasicLands = [], onSubmit, submitLabel }) => {
|
||||
// Unlimited Timer (Static for now)
|
||||
const [timer] = useState<string>("Unlimited");
|
||||
/* --- Hooks --- */
|
||||
@@ -359,8 +396,17 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
useEffect(() => localStorage.setItem('deck_groupBy', groupBy), [groupBy]);
|
||||
useEffect(() => localStorage.setItem('deck_cardWidth', cardWidth.toString()), [cardWidth]);
|
||||
|
||||
const [pool, setPool] = useState<any[]>(initialPool);
|
||||
const [deck, setDeck] = useState<any[]>([]);
|
||||
const [deck, setDeck] = useState<any[]>(initialDeck);
|
||||
const [pool, setPool] = useState<any[]>(() => {
|
||||
if (initialDeck && initialDeck.length > 0) {
|
||||
// Need to be careful about IDs.
|
||||
// If initialDeck cards are from the pool, they share IDs?
|
||||
// Usually yes.
|
||||
const deckIds = new Set(initialDeck.map(c => c.id));
|
||||
return initialPool.filter(c => !deckIds.has(c.id));
|
||||
}
|
||||
return initialPool;
|
||||
});
|
||||
// const [lands, setLands] = useState(...); // REMOVED: Managed directly in deck now
|
||||
const [hoveredCard, setHoveredCard] = useState<any>(null);
|
||||
const [displayCard, setDisplayCard] = useState<any>(null);
|
||||
@@ -498,7 +544,13 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
return cardWithDefinition;
|
||||
});
|
||||
|
||||
socketService.socket.emit('player_ready', { deck: preparedDeck });
|
||||
|
||||
|
||||
if (onSubmit) {
|
||||
onSubmit(preparedDeck);
|
||||
} else {
|
||||
socketService.socket.emit('player_ready', { deck: preparedDeck });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoBuild = async () => {
|
||||
@@ -876,7 +928,7 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
onClick={submitDeck}
|
||||
className="bg-emerald-600 hover:bg-emerald-500 text-white px-4 py-2 rounded-lg font-bold shadow-lg flex items-center gap-2 transition-transform hover:scale-105 text-sm"
|
||||
>
|
||||
<Save className="w-4 h-4" /> <span className="hidden sm:inline">Submit Deck</span><span className="sm:hidden">Save</span>
|
||||
<Save className="w-4 h-4" /> <span className="hidden sm:inline">{submitLabel || 'Submit Deck'}</span><span className="sm:hidden">Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1004,36 +1056,4 @@ export const DeckBuilderView: React.FC<DeckBuilderViewProps> = ({ initialPool, a
|
||||
);
|
||||
};
|
||||
|
||||
const DeckCardItem = ({ card, useArtCrop, isFoil, onCardClick, onHover }: any) => {
|
||||
const displayImage = useArtCrop ? card.imageArtCrop : card.image;
|
||||
const { onTouchStart, onTouchEnd, onTouchMove, onClick } = useCardTouch(onHover, () => {
|
||||
if (window.matchMedia('(pointer: coarse)').matches) {
|
||||
onHover(card);
|
||||
} else {
|
||||
onCardClick(card);
|
||||
}
|
||||
}, card);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
onMouseEnter={() => onHover(card)}
|
||||
onMouseLeave={() => onHover(null)}
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchEnd={onTouchEnd}
|
||||
onTouchMove={onTouchMove}
|
||||
className="relative group bg-slate-900 rounded-lg shrink-0 cursor-pointer hover:scale-105 transition-transform"
|
||||
>
|
||||
<div className={`relative ${useArtCrop ? 'aspect-square' : 'aspect-[2.5/3.5]'} overflow-hidden rounded-lg shadow-xl border transition-all duration-200 group-hover:ring-2 group-hover:ring-purple-400 group-hover:shadow-purple-500/30 ${isFoil ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
|
||||
{isFoil && <FoilOverlay />}
|
||||
{isFoil && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1.5 rounded backdrop-blur-sm">FOIL</div>}
|
||||
{displayImage ? (
|
||||
<img src={displayImage} alt={card.name} className="w-full h-full object-cover" draggable={false} />
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-xs text-center p-1 text-slate-500 font-bold border-2 border-slate-700 m-1 rounded">{card.name}</div>
|
||||
)}
|
||||
<div className={`absolute bottom-0 left-0 right-0 h-1.5 ${card.rarity === 'mythic' ? 'bg-gradient-to-r from-orange-500 to-red-600' : card.rarity === 'rare' ? 'bg-gradient-to-r from-yellow-400 to-yellow-600' : card.rarity === 'uncommon' ? 'bg-gradient-to-r from-gray-300 to-gray-500' : 'bg-black'}`} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -77,7 +77,10 @@ export const MulliganView: React.FC<MulliganViewProps> = ({ hand, mulliganCount,
|
||||
{/* Controls */}
|
||||
<div className="flex gap-8">
|
||||
<button
|
||||
onClick={() => onDecision(false, [])}
|
||||
onClick={() => {
|
||||
console.log("Mulligan Clicked");
|
||||
onDecision(false, []);
|
||||
}}
|
||||
className="px-8 py-4 bg-red-600/20 hover:bg-red-600/40 border border-red-500 text-red-100 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 group"
|
||||
>
|
||||
<span>Mulligan</span>
|
||||
@@ -85,7 +88,12 @@ export const MulliganView: React.FC<MulliganViewProps> = ({ hand, mulliganCount,
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => isSelectionValid && onDecision(true, Array.from(selectedToBottom))}
|
||||
onClick={() => {
|
||||
if (isSelectionValid) {
|
||||
console.log("Keep Hand Clicked", Array.from(selectedToBottom));
|
||||
onDecision(true, Array.from(selectedToBottom));
|
||||
}
|
||||
}}
|
||||
disabled={!isSelectionValid}
|
||||
className={`px-8 py-4 rounded-xl font-bold text-lg transition-all flex flex-col items-center gap-1 min-w-[200px] ${isSelectionValid
|
||||
? 'bg-emerald-600 hover:bg-emerald-500 text-white shadow-[0_0_20px_rgba(16,185,129,0.4)]'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { GameState, Phase, Step } from '../../types/game';
|
||||
import { ManaIcon } from '../../components/ManaIcon';
|
||||
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Play, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react';
|
||||
import { Shield, Swords, Hourglass, Zap, Hand, ChevronRight, XCircle, Clock, Files, Crosshair, Skull, Flag, Moon, Trash2 } from 'lucide-react';
|
||||
|
||||
interface PhaseStripProps {
|
||||
gameState: GameState;
|
||||
@@ -75,7 +75,7 @@ export const PhaseStrip: React.FC<PhaseStripProps> = ({
|
||||
else actionLabel = "Pass";
|
||||
} else {
|
||||
// Resolve
|
||||
const topItem = gameState.stack![gameState.stack!.length - 1];
|
||||
// const topItem = gameState.stack![gameState.stack!.length - 1]; // Unused
|
||||
actionLabel = "Resolve";
|
||||
actionType = 'PASS_PRIORITY';
|
||||
ActionIcon = Zap;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Modal } from '../../components/Modal';
|
||||
import { useToast } from '../../components/Toast';
|
||||
import { GameView } from '../game/GameView';
|
||||
import { DraftView } from '../draft/DraftView';
|
||||
import { TournamentManager as TournamentView } from '../tournament/TournamentManager';
|
||||
import { DeckBuilderView } from '../draft/DeckBuilderView';
|
||||
|
||||
interface Player {
|
||||
@@ -71,6 +72,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const [gameState, setGameState] = useState<any>(initialGameState || null);
|
||||
const [draftState, setDraftState] = useState<any>(initialDraftState || null);
|
||||
const [tournamentState, setTournamentState] = useState<any>((initialRoom as any).tournament || null);
|
||||
const [preparingMatchId, setPreparingMatchId] = useState<string | null>(null);
|
||||
const [mobileTab, setMobileTab] = useState<'game' | 'chat'>('game'); // Keep for mobile
|
||||
|
||||
// Derived State
|
||||
@@ -180,14 +183,32 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
setGameState(data);
|
||||
};
|
||||
|
||||
const handleTournamentUpdate = (data: any) => {
|
||||
setTournamentState(data);
|
||||
};
|
||||
|
||||
// Also handle finish
|
||||
const handleTournamentFinished = (data: any) => {
|
||||
showToast(`Tournament Winner: ${data.winner.name}!`, 'success');
|
||||
};
|
||||
|
||||
socket.on('draft_update', handleDraftUpdate);
|
||||
socket.on('draft_error', handleDraftError);
|
||||
socket.on('game_update', handleGameUpdate);
|
||||
socket.on('tournament_update', handleTournamentUpdate);
|
||||
socket.on('tournament_finished', handleTournamentFinished);
|
||||
|
||||
socket.on('match_start', () => {
|
||||
setPreparingMatchId(null);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off('draft_update', handleDraftUpdate);
|
||||
socket.off('draft_error', handleDraftError);
|
||||
socket.off('game_update', handleGameUpdate);
|
||||
socket.off('tournament_update', handleTournamentUpdate);
|
||||
socket.off('tournament_finished', handleTournamentFinished);
|
||||
socket.off('match_start');
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -271,6 +292,29 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
||||
return <DeckBuilderView roomId={room.id} currentPlayerId={currentPlayerId} initialPool={myPool} availableBasicLands={room.basicLands} />;
|
||||
}
|
||||
|
||||
if (room.status === 'tournament' && tournamentState) {
|
||||
if (preparingMatchId) {
|
||||
const myTournamentPlayer = tournamentState.players.find((p: any) => p.id === currentPlayerId);
|
||||
const myPool = draftState?.players[currentPlayerId]?.pool || [];
|
||||
const myDeck = myTournamentPlayer?.deck || [];
|
||||
|
||||
return <DeckBuilderView
|
||||
roomId={room.id}
|
||||
currentPlayerId={currentPlayerId}
|
||||
initialPool={myPool}
|
||||
initialDeck={myDeck}
|
||||
availableBasicLands={room.basicLands}
|
||||
onSubmit={(deck) => {
|
||||
socketService.socket.emit('match_ready', { matchId: preparingMatchId, deck });
|
||||
setPreparingMatchId(null);
|
||||
showToast("Deck ready! Waiting for game to start...", 'success');
|
||||
}}
|
||||
submitLabel="Ready for Match"
|
||||
/>;
|
||||
}
|
||||
return <TournamentView tournament={tournamentState} currentPlayerId={currentPlayerId} onJoinMatch={setPreparingMatchId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl flex flex-col items-center justify-center">
|
||||
<h2 className="text-3xl font-bold text-white mb-4">Waiting for Players...</h2>
|
||||
|
||||
@@ -1,97 +1,119 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Users } from 'lucide-react';
|
||||
import { useToast } from '../../components/Toast';
|
||||
import React from 'react';
|
||||
import { Trophy, Play } from 'lucide-react';
|
||||
import { socketService } from '../../services/SocketService';
|
||||
|
||||
interface TournamentPlayer {
|
||||
id: string;
|
||||
name: string;
|
||||
isBot: boolean;
|
||||
}
|
||||
|
||||
interface Match {
|
||||
id: number;
|
||||
p1: string;
|
||||
p2: string;
|
||||
id: string;
|
||||
round: number;
|
||||
matchIndex: number;
|
||||
player1: TournamentPlayer | null;
|
||||
player2: TournamentPlayer | null;
|
||||
winnerId?: string;
|
||||
status: 'pending' | 'ready' | 'in_progress' | 'finished';
|
||||
}
|
||||
|
||||
interface Bracket {
|
||||
round1: Match[];
|
||||
totalPlayers: number;
|
||||
interface Tournament {
|
||||
id: string;
|
||||
players: TournamentPlayer[];
|
||||
rounds: Match[][];
|
||||
currentRound: number;
|
||||
status: 'setup' | 'active' | 'finished';
|
||||
winner?: TournamentPlayer;
|
||||
}
|
||||
|
||||
export const TournamentManager: React.FC = () => {
|
||||
const [playerInput, setPlayerInput] = useState('');
|
||||
const [bracket, setBracket] = useState<Bracket | null>(null);
|
||||
const { showToast } = useToast();
|
||||
interface TournamentManagerProps {
|
||||
tournament: Tournament;
|
||||
currentPlayerId: string;
|
||||
onJoinMatch: (matchId: string) => void;
|
||||
}
|
||||
|
||||
const shuffleArray = (array: any[]) => {
|
||||
let currentIndex = array.length, randomIndex;
|
||||
const newArray = [...array];
|
||||
while (currentIndex !== 0) {
|
||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
||||
currentIndex--;
|
||||
[newArray[currentIndex], newArray[randomIndex]] = [newArray[randomIndex], newArray[currentIndex]];
|
||||
}
|
||||
return newArray;
|
||||
};
|
||||
export const TournamentManager: React.FC<TournamentManagerProps> = ({ tournament, currentPlayerId, onJoinMatch }) => {
|
||||
const { rounds, winner } = tournament;
|
||||
|
||||
const generateBracket = () => {
|
||||
if (!playerInput.trim()) return;
|
||||
const names = playerInput.split('\n').filter(n => n.trim() !== '').map(n => n.trim());
|
||||
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)));
|
||||
const byesNeeded = nextPowerOf2 - shuffled.length;
|
||||
|
||||
const fullRoster = [...shuffled];
|
||||
for (let i = 0; i < byesNeeded; i++) fullRoster.push("BYE");
|
||||
|
||||
const pairings: Match[] = [];
|
||||
for (let i = 0; i < fullRoster.length; i += 2) {
|
||||
pairings.push({ id: i, p1: fullRoster[i], p2: fullRoster[i + 1] });
|
||||
}
|
||||
|
||||
setBracket({ round1: pairings, totalPlayers: names.length });
|
||||
const handleJoinMatch = (matchId: string) => {
|
||||
socketService.socket.emit('join_match', { matchId }, (response: any) => {
|
||||
if (!response.success) {
|
||||
console.error(response.message);
|
||||
// Ideally show toast
|
||||
alert(response.message); // Fallback
|
||||
} else {
|
||||
onJoinMatch(matchId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto max-w-4xl mx-auto p-4 md:p-6">
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8">
|
||||
<h2 className="text-xl font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-blue-400" /> Players
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400 mb-2">Enter one name per line</p>
|
||||
<textarea
|
||||
className="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-slate-300 focus:ring-2 focus:ring-blue-500 outline-none resize-none mb-4"
|
||||
placeholder={`Player 1\nPlayer 2...`}
|
||||
value={playerInput}
|
||||
onChange={(e) => setPlayerInput(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={generateBracket}
|
||||
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded-lg font-bold w-full md:w-auto transition-colors"
|
||||
>
|
||||
Generate Bracket
|
||||
</button>
|
||||
<div className="h-full overflow-y-auto max-w-6xl mx-auto p-4 md:p-6 text-slate-100">
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center gap-2">
|
||||
<Trophy className="w-6 h-6 text-yellow-500" /> Tournament Bracket
|
||||
</h2>
|
||||
<p className="text-slate-400 text-sm mt-1">Round {tournament.currentRound}</p>
|
||||
</div>
|
||||
{winner && (
|
||||
<div className="bg-yellow-500/20 border border-yellow-500 text-yellow-200 px-6 py-3 rounded-xl flex items-center gap-3 animate-pulse">
|
||||
<Trophy className="w-8 h-8" />
|
||||
<div>
|
||||
<div className="text-xs uppercase font-bold tracking-wider">Winner</div>
|
||||
<div className="text-xl font-bold">{winner.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{bracket && (
|
||||
<div className="bg-slate-800 rounded-xl p-6 border border-slate-700 shadow-xl overflow-x-auto">
|
||||
<h3 className="text-lg font-bold text-white mb-6 border-b border-slate-700 pb-2">Round 1 (Single Elimination)</h3>
|
||||
<div className="flex flex-col gap-4 min-w-[300px]">
|
||||
{bracket.round1.map((match, i) => (
|
||||
<div key={i} className="bg-slate-900 border border-slate-700 rounded-lg p-4 flex flex-col gap-2 relative">
|
||||
<div className="absolute -left-3 top-1/2 w-3 h-px bg-slate-600"></div>
|
||||
<div className="flex justify-between items-center bg-slate-800/50 p-2 rounded border border-slate-700/50">
|
||||
<span className={match.p1 === 'BYE' ? 'text-slate-500 italic' : 'font-bold text-white'}>{match.p1}</span>
|
||||
</div>
|
||||
<div className="text-xs text-center text-slate-500">VS</div>
|
||||
<div className="flex justify-between items-center bg-slate-800/50 p-2 rounded border border-slate-700/50">
|
||||
<span className={match.p2 === 'BYE' ? 'text-slate-500 italic' : 'font-bold text-white'}>{match.p2}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-8 overflow-x-auto pb-8 snap-x">
|
||||
{rounds.map((roundMatches, roundIndex) => (
|
||||
<div key={roundIndex} className="flex flex-col justify-center gap-16 min-w-[280px] snap-center">
|
||||
<h3 className="text-center font-bold text-slate-500 uppercase tracking-widest text-sm mb-4">
|
||||
{roundIndex === rounds.length - 1 ? "Finals" : `Round ${roundIndex + 1}`}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-8 justify-center flex-1">
|
||||
{roundMatches.map((match) => {
|
||||
const isMyMatch = (match.player1?.id === currentPlayerId || match.player2?.id === currentPlayerId);
|
||||
const isPlayable = isMyMatch && match.status === 'ready' && !match.winnerId;
|
||||
|
||||
return (
|
||||
<div key={match.id} className={`bg-slate-900 border ${isMyMatch ? 'border-blue-500 ring-1 ring-blue-500/50' : 'border-slate-700'} rounded-lg p-0 overflow-hidden relative shadow-lg`}>
|
||||
{/* Status Indicator */}
|
||||
{match.status === 'in_progress' && <div className="absolute top-0 right-0 bg-green-500 text-xs text-black font-bold px-2 py-0.5">LIVE</div>}
|
||||
|
||||
<div className={`p-3 border-b border-slate-800 flex justify-between items-center ${match.winnerId === match.player1?.id ? 'bg-emerald-900/30' : ''}`}>
|
||||
<span className={match.player1 ? 'font-bold' : 'text-slate-600 italic'}>
|
||||
{match.player1 ? match.player1.name : 'Waiting...'}
|
||||
</span>
|
||||
{match.winnerId === match.player1?.id && <Trophy className="w-4 h-4 text-emerald-500" />}
|
||||
</div>
|
||||
<div className={`p-3 flex justify-between items-center ${match.winnerId === match.player2?.id ? 'bg-emerald-900/30' : ''}`}>
|
||||
<span className={match.player2 ? 'font-bold' : 'text-slate-600 italic'}>
|
||||
{match.player2 ? match.player2.name : 'Waiting...'}
|
||||
</span>
|
||||
{match.winnerId === match.player2?.id && <Trophy className="w-4 h-4 text-emerald-500" />}
|
||||
</div>
|
||||
|
||||
{isPlayable && (
|
||||
<button
|
||||
onClick={() => handleJoinMatch(match.id)}
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-2 flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Play className="w-4 h-4" /> Play Match
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,13 @@ class SocketService {
|
||||
this.socket = io(URL, {
|
||||
autoConnect: false
|
||||
});
|
||||
|
||||
// Debug Wrapper
|
||||
const originalEmit = this.socket.emit;
|
||||
this.socket.emit = (event: string, ...args: any[]) => {
|
||||
console.log(`[Socket] 📤 Emitting: ${event}`, args);
|
||||
return originalEmit.apply(this.socket, [event, ...args]);
|
||||
};
|
||||
}
|
||||
|
||||
connect() {
|
||||
|
||||
Reference in New Issue
Block a user