feat: Add exit functionality and confirmation modal to DraftView, and include draft state in join room callback.
This commit is contained in:
93
src/client/src/components/Modal.tsx
Normal file
93
src/client/src/components/Modal.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { X, AlertTriangle, CheckCircle, Info } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
type?: 'info' | 'success' | 'warning' | 'error';
|
||||||
|
confirmLabel?: string;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
cancelLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Modal: React.FC<ModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
type = 'info',
|
||||||
|
confirmLabel = 'OK',
|
||||||
|
onConfirm,
|
||||||
|
cancelLabel
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return <CheckCircle className="w-6 h-6 text-emerald-500" />;
|
||||||
|
case 'warning': return <AlertTriangle className="w-6 h-6 text-amber-500" />;
|
||||||
|
case 'error': return <AlertTriangle className="w-6 h-6 text-red-500" />;
|
||||||
|
default: return <Info className="w-6 h-6 text-blue-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBorderColor = () => {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return 'border-emerald-500/50';
|
||||||
|
case 'warning': return 'border-amber-500/50';
|
||||||
|
case 'error': return 'border-red-500/50';
|
||||||
|
default: return 'border-slate-700';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-200">
|
||||||
|
<div
|
||||||
|
className={`bg-slate-900 border ${getBorderColor()} rounded-xl shadow-2xl max-w-md w-full p-6 animate-in zoom-in-95 duration-200`}
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{getIcon()}
|
||||||
|
<h3 className="text-xl font-bold text-white">{title}</h3>
|
||||||
|
</div>
|
||||||
|
{onClose && !cancelLabel && (
|
||||||
|
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-slate-300 mb-8 leading-relaxed">
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
{cancelLabel && onClose && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 font-medium transition-colors border border-slate-700"
|
||||||
|
>
|
||||||
|
{cancelLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (onConfirm) onConfirm();
|
||||||
|
if (onClose) onClose();
|
||||||
|
}}
|
||||||
|
className={`px-6 py-2 rounded-lg font-bold text-white shadow-lg transition-transform hover:scale-105 ${type === 'error' ? 'bg-red-600 hover:bg-red-500' :
|
||||||
|
type === 'warning' ? 'bg-amber-600 hover:bg-amber-500' :
|
||||||
|
type === 'success' ? 'bg-emerald-600 hover:bg-emerald-500' :
|
||||||
|
'bg-blue-600 hover:bg-blue-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{confirmLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { socketService } from '../../services/SocketService';
|
import { socketService } from '../../services/SocketService';
|
||||||
|
import { LogOut } from 'lucide-react';
|
||||||
|
import { Modal } from '../../components/Modal';
|
||||||
|
|
||||||
interface DraftViewProps {
|
interface DraftViewProps {
|
||||||
draftState: any;
|
draftState: any;
|
||||||
roomId: string; // Passed from parent
|
roomId: string; // Passed from parent
|
||||||
currentPlayerId: string;
|
currentPlayerId: string;
|
||||||
|
onExit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, currentPlayerId }) => {
|
export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, currentPlayerId, onExit }) => {
|
||||||
const [timer, setTimer] = useState(60);
|
const [timer, setTimer] = useState(60);
|
||||||
|
const [confirmExitOpen, setConfirmExitOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@@ -78,14 +82,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, curren
|
|||||||
socketService.socket.emit('pick_card', { roomId, playerId: currentPlayerId, cardId });
|
socketService.socket.emit('pick_card', { roomId, playerId: currentPlayerId, cardId });
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!activePack) {
|
// ... inside DraftView return ...
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center h-full bg-slate-900 text-white">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Waiting for next pack...</h2>
|
|
||||||
<div className="animate-pulse bg-slate-700 w-64 h-8 rounded"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-slate-950 text-white overflow-hidden relative select-none" onContextMenu={(e) => e.preventDefault()}>
|
<div className="flex flex-col h-full bg-slate-950 text-white overflow-hidden relative select-none" onContextMenu={(e) => e.preventDefault()}>
|
||||||
@@ -117,8 +114,23 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, curren
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-4xl font-mono text-emerald-400 font-bold drop-shadow-[0_0_10px_rgba(52,211,153,0.5)]">
|
<div className="flex items-center gap-6">
|
||||||
00:{timer < 10 ? `0${timer}` : timer}
|
{!activePack ? (
|
||||||
|
<div className="text-sm font-bold text-amber-500 animate-pulse uppercase tracking-wider">Waiting...</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-4xl font-mono text-emerald-400 font-bold drop-shadow-[0_0_10px_rgba(52,211,153,0.5)]">
|
||||||
|
00:{timer < 10 ? `0${timer}` : timer}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{onExit && (
|
||||||
|
<button
|
||||||
|
onClick={() => setConfirmExitOpen(true)}
|
||||||
|
className="p-3 bg-slate-800 hover:bg-red-500/20 text-slate-400 hover:text-red-500 border border-slate-700 hover:border-red-500/50 rounded-xl transition-all shadow-lg group"
|
||||||
|
title="Exit to Lobby"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +139,7 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, curren
|
|||||||
<div className="flex-1 flex overflow-hidden">
|
<div className="flex-1 flex overflow-hidden">
|
||||||
|
|
||||||
{/* Dedicated Zoom Zone (Left Sidebar) */}
|
{/* Dedicated Zoom Zone (Left Sidebar) */}
|
||||||
<div className="hidden lg:flex w-80 shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800/50 bg-slate-900/20 backdrop-blur-sm z-10">
|
<div className="hidden lg:flex w-80 shrink-0 flex-col items-center justify-start pt-8 border-r border-slate-800/50 bg-slate-900/20 backdrop-blur-sm z-10 transition-all">
|
||||||
{hoveredCard ? (
|
{hoveredCard ? (
|
||||||
<div className="animate-in fade-in slide-in-from-left-4 duration-300 p-4 sticky top-4">
|
<div className="animate-in fade-in slide-in-from-left-4 duration-300 p-4 sticky top-4">
|
||||||
<img
|
<img
|
||||||
@@ -150,30 +162,49 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, curren
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Area: Current Pack */}
|
{/* Main Area: Current Pack OR Waiting State */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 z-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
|
<div className="flex-1 overflow-y-auto p-4 z-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
|
||||||
<div className="flex flex-col items-center justify-center min-h-full pb-10">
|
{!activePack ? (
|
||||||
<h3 className="text-center text-slate-500 uppercase tracking-[0.2em] text-xs font-bold mb-8">Select a Card</h3>
|
<div className="flex flex-col items-center justify-center min-h-full pb-10 fade-in animate-in duration-500">
|
||||||
<div className="flex flex-wrap justify-center gap-6 [perspective:1000px]">
|
<div className="w-24 h-24 mb-6 relative">
|
||||||
{activePack.cards.map((card: any) => (
|
<div className="absolute inset-0 rounded-full border-4 border-slate-800"></div>
|
||||||
<div
|
<div className="absolute inset-0 rounded-full border-4 border-t-emerald-500 animate-spin"></div>
|
||||||
key={card.id}
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
|
<LogOut className="w-8 h-8 text-emerald-500 rotate-180" /> {/* Just a placeholder icon or similar */}
|
||||||
style={{ width: `${14 * cardScale}rem` }}
|
|
||||||
onClick={() => handlePick(card.id)}
|
|
||||||
onMouseEnter={() => setHoveredCard(card)}
|
|
||||||
onMouseLeave={() => setHoveredCard(null)}
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 rounded-xl bg-emerald-500 blur-xl opacity-0 group-hover:opacity-40 transition-opacity duration-300"></div>
|
|
||||||
<img
|
|
||||||
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
|
||||||
alt={card.name}
|
|
||||||
className="w-full rounded-xl shadow-2xl shadow-black group-hover:ring-2 ring-emerald-400/50 relative z-10"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
<h2 className="text-3xl font-bold text-white mb-2">Waiting for next pack...</h2>
|
||||||
|
<p className="text-slate-400">Your neighbor is selecting a card.</p>
|
||||||
|
<div className="mt-8 flex gap-2">
|
||||||
|
<div className="w-3 h-3 bg-emerald-500 rounded-full animate-bounce [animation-delay:-0.3s]"></div>
|
||||||
|
<div className="w-3 h-3 bg-emerald-500 rounded-full animate-bounce [animation-delay:-0.15s]"></div>
|
||||||
|
<div className="w-3 h-3 bg-emerald-500 rounded-full animate-bounce"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-full pb-10">
|
||||||
|
<h3 className="text-center text-slate-500 uppercase tracking-[0.2em] text-xs font-bold mb-8">Select a Card</h3>
|
||||||
|
<div className="flex flex-wrap justify-center gap-6 [perspective:1000px]">
|
||||||
|
{activePack.cards.map((card: any) => (
|
||||||
|
<div
|
||||||
|
key={card.id}
|
||||||
|
className="group relative transition-all duration-300 hover:scale-110 hover:-translate-y-4 hover:z-50 cursor-pointer"
|
||||||
|
style={{ width: `${14 * cardScale}rem` }}
|
||||||
|
onClick={() => handlePick(card.id)}
|
||||||
|
onMouseEnter={() => setHoveredCard(card)}
|
||||||
|
onMouseLeave={() => setHoveredCard(null)}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 rounded-xl bg-emerald-500 blur-xl opacity-0 group-hover:opacity-40 transition-opacity duration-300"></div>
|
||||||
|
<img
|
||||||
|
src={card.image || card.image_uris?.normal || card.card_faces?.[0]?.image_uris?.normal}
|
||||||
|
alt={card.name}
|
||||||
|
className="w-full rounded-xl shadow-2xl shadow-black group-hover:ring-2 ring-emerald-400/50 relative z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -214,6 +245,16 @@ export const DraftView: React.FC<DraftViewProps> = ({ draftState, roomId, curren
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Modal
|
||||||
|
isOpen={confirmExitOpen}
|
||||||
|
onClose={() => setConfirmExitOpen(false)}
|
||||||
|
title="Exit Draft?"
|
||||||
|
message="Are you sure you want to exit the draft? You can rejoin later."
|
||||||
|
type="warning"
|
||||||
|
confirmLabel="Exit Draft"
|
||||||
|
cancelLabel="Stay"
|
||||||
|
onConfirm={onExit}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
|
||||||
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, Play, Copy, Check, Layers } from 'lucide-react';
|
import { Users, MessageSquare, Send, Play, Copy, Check, Layers, LogOut } from 'lucide-react';
|
||||||
|
import { Modal } from '../../components/Modal';
|
||||||
import { GameView } from '../game/GameView';
|
import { GameView } from '../game/GameView';
|
||||||
import { DraftView } from '../draft/DraftView';
|
import { DraftView } from '../draft/DraftView';
|
||||||
import { DeckBuilderView } from '../draft/DeckBuilderView';
|
import { DeckBuilderView } from '../draft/DeckBuilderView';
|
||||||
@@ -11,6 +12,7 @@ interface Player {
|
|||||||
name: string;
|
name: string;
|
||||||
isHost: boolean;
|
isHost: boolean;
|
||||||
role: 'player' | 'spectator';
|
role: 'player' | 'spectator';
|
||||||
|
isOffline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
@@ -32,54 +34,39 @@ interface GameRoomProps {
|
|||||||
room: Room;
|
room: Room;
|
||||||
currentPlayerId: string;
|
currentPlayerId: string;
|
||||||
initialGameState?: any;
|
initialGameState?: any;
|
||||||
|
initialDraftState?: any;
|
||||||
|
onExit: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPlayerId, initialGameState }) => {
|
export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPlayerId, initialGameState, initialDraftState, onExit }) => {
|
||||||
|
// State
|
||||||
const [room, setRoom] = useState<Room>(initialRoom);
|
const [room, setRoom] = useState<Room>(initialRoom);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [modalConfig, setModalConfig] = useState({ title: '', message: '', type: 'info' as 'info' | 'error' | 'warning' | 'success' });
|
||||||
|
|
||||||
|
// Restored States
|
||||||
const [message, setMessage] = useState('');
|
const [message, setMessage] = useState('');
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>(initialRoom.messages || []);
|
const [messages, setMessages] = useState<ChatMessage[]>(initialRoom.messages || []);
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const [gameState, setGameState] = useState<any>(initialGameState || null);
|
const [gameState, setGameState] = useState<any>(initialGameState || null);
|
||||||
|
const [draftState, setDraftState] = useState<any>(initialDraftState || null);
|
||||||
|
|
||||||
|
// Derived State
|
||||||
|
const host = room.players.find(p => p.isHost);
|
||||||
|
const isHostOffline = host?.isOffline;
|
||||||
|
const isMeHost = currentPlayerId === host?.id;
|
||||||
|
|
||||||
|
// Effects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRoom(initialRoom);
|
setRoom(initialRoom);
|
||||||
setMessages(initialRoom.messages || []);
|
setMessages(initialRoom.messages || []);
|
||||||
}, [initialRoom]);
|
}, [initialRoom]);
|
||||||
|
|
||||||
useEffect(() => {
|
// Scroll to bottom of chat
|
||||||
const socket = socketService.socket;
|
|
||||||
|
|
||||||
const handleRoomUpdate = (updatedRoom: Room) => {
|
|
||||||
console.log('Room updated:', updatedRoom);
|
|
||||||
setRoom(updatedRoom);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNewMessage = (msg: ChatMessage) => {
|
|
||||||
setMessages(prev => [...prev, msg]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGameUpdate = (game: any) => {
|
|
||||||
setGameState(game);
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on('room_update', handleRoomUpdate);
|
|
||||||
socket.on('new_message', handleNewMessage);
|
|
||||||
socket.on('game_update', handleGameUpdate);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
socket.off('room_update', handleRoomUpdate);
|
|
||||||
socket.off('new_message', handleNewMessage);
|
|
||||||
socket.off('game_update', handleGameUpdate);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
// New States
|
|
||||||
const [draftState, setDraftState] = useState<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = socketService.socket;
|
const socket = socketService.socket;
|
||||||
const handleDraftUpdate = (data: any) => {
|
const handleDraftUpdate = (data: any) => {
|
||||||
@@ -87,7 +74,12 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDraftError = (error: { message: string }) => {
|
const handleDraftError = (error: { message: string }) => {
|
||||||
alert(error.message); // Simple alert for now
|
setModalConfig({
|
||||||
|
title: 'Error',
|
||||||
|
message: error.message,
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.on('draft_update', handleDraftUpdate);
|
socket.on('draft_update', handleDraftUpdate);
|
||||||
@@ -116,10 +108,8 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(room.id).catch(err => {
|
navigator.clipboard.writeText(room.id).catch(err => {
|
||||||
console.error('Failed to copy: ', err);
|
console.error('Failed to copy: ', err);
|
||||||
// Fallback could go here
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback for non-secure context or older browsers
|
|
||||||
console.warn('Clipboard API not available');
|
console.warn('Clipboard API not available');
|
||||||
const textArea = document.createElement("textarea");
|
const textArea = document.createElement("textarea");
|
||||||
textArea.value = room.id;
|
textArea.value = room.id;
|
||||||
@@ -135,19 +125,17 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleStartGame = () => {
|
const handleStartGame = () => {
|
||||||
// Create a test deck for each player for now
|
|
||||||
const testDeck = Array.from({ length: 40 }).map((_, i) => ({
|
const testDeck = Array.from({ length: 40 }).map((_, i) => ({
|
||||||
id: `card-${i}`,
|
id: `card-${i}`,
|
||||||
name: i % 2 === 0 ? "Mountain" : "Lightning Bolt",
|
name: i % 2 === 0 ? "Mountain" : "Lightning Bolt",
|
||||||
image_uris: {
|
image_uris: {
|
||||||
normal: i % 2 === 0
|
normal: i % 2 === 0
|
||||||
? "https://cards.scryfall.io/normal/front/1/9/194459f0-2586-444a-be7d-786d5e7e9bc4.jpg" // Mountain
|
? "https://cards.scryfall.io/normal/front/1/9/194459f0-2586-444a-be7d-786d5e7e9bc4.jpg"
|
||||||
: "https://cards.scryfall.io/normal/front/f/2/f29ba16f-c8fb-42fe-aabf-87089cb211a7.jpg" // Bolt
|
: "https://cards.scryfall.io/normal/front/f/2/f29ba16f-c8fb-42fe-aabf-87089cb211a7.jpg"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const decks = room.players.reduce((acc, p) => ({ ...acc, [p.id]: testDeck }), {});
|
const decks = room.players.reduce((acc, p) => ({ ...acc, [p.id]: testDeck }), {});
|
||||||
|
|
||||||
socketService.socket.emit('start_game', { roomId: room.id, decks });
|
socketService.socket.emit('start_game', { roomId: room.id, decks });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,21 +143,16 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
socketService.socket.emit('start_draft', { roomId: room.id });
|
socketService.socket.emit('start_draft', { roomId: room.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to determine view
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (gameState) {
|
if (gameState) {
|
||||||
return <GameView gameState={gameState} currentPlayerId={currentPlayerId} />;
|
return <GameView gameState={gameState} currentPlayerId={currentPlayerId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.status === 'drafting' && draftState) {
|
if (room.status === 'drafting' && draftState) {
|
||||||
return <DraftView draftState={draftState} roomId={room.id} currentPlayerId={currentPlayerId} />;
|
return <DraftView draftState={draftState} roomId={room.id} currentPlayerId={currentPlayerId} onExit={onExit} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.status === 'deck_building' && draftState) {
|
if (room.status === 'deck_building' && draftState) {
|
||||||
// Check if I am ready
|
|
||||||
// Type casting needed because 'ready' was added to interface only in server side so far?
|
|
||||||
// Need to update client Player interface too in this file if not already consistent.
|
|
||||||
// But let's assume raw object has it.
|
|
||||||
const me = room.players.find(p => p.id === currentPlayerId) as any;
|
const me = room.players.find(p => p.id === currentPlayerId) as any;
|
||||||
if (me?.ready) {
|
if (me?.ready) {
|
||||||
return (
|
return (
|
||||||
@@ -201,7 +184,6 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
return <DeckBuilderView roomId={room.id} currentPlayerId={currentPlayerId} initialPool={myPool} />;
|
return <DeckBuilderView roomId={room.id} currentPlayerId={currentPlayerId} initialPool={myPool} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default Waiting Lobby
|
|
||||||
return (
|
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">
|
<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>
|
<h2 className="text-3xl font-bold text-white mb-4">Waiting for Players...</h2>
|
||||||
@@ -250,16 +232,13 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
<div className="flex h-full gap-4">
|
<div className="flex h-full gap-4">
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
|
|
||||||
{/* Sidebar: Players & Chat */}
|
|
||||||
<div className="w-80 flex flex-col gap-4">
|
<div className="w-80 flex flex-col gap-4">
|
||||||
{/* Players List */}
|
|
||||||
<div className="flex-1 bg-slate-800 rounded-xl p-4 border border-slate-700 shadow-xl overflow-hidden flex flex-col">
|
<div className="flex-1 bg-slate-800 rounded-xl p-4 border border-slate-700 shadow-xl overflow-hidden flex flex-col">
|
||||||
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2">
|
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2">
|
||||||
<Users className="w-4 h-4" /> Lobby
|
<Users className="w-4 h-4" /> Lobby
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
<div className="flex-1 overflow-y-auto space-y-2 pr-1">
|
||||||
{room.players.map(p => {
|
{room.players.map(p => {
|
||||||
// Cast to any to access ready state without full interface update for now
|
|
||||||
const isReady = (p as any).ready;
|
const isReady = (p as any).ready;
|
||||||
return (
|
return (
|
||||||
<div key={p.id} className="flex items-center justify-between bg-slate-900/50 p-2 rounded-lg border border-slate-700/50">
|
<div key={p.id} className="flex items-center justify-between bg-slate-900/50 p-2 rounded-lg border border-slate-700/50">
|
||||||
@@ -283,7 +262,6 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat */}
|
|
||||||
<div className="h-1/2 bg-slate-800 rounded-xl p-4 border border-slate-700 shadow-xl flex flex-col">
|
<div className="h-1/2 bg-slate-800 rounded-xl p-4 border border-slate-700 shadow-xl flex flex-col">
|
||||||
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2">
|
<h3 className="text-sm font-bold text-slate-400 uppercase mb-3 flex items-center gap-2">
|
||||||
<MessageSquare className="w-4 h-4" /> Chat
|
<MessageSquare className="w-4 h-4" /> Chat
|
||||||
@@ -311,6 +289,48 @@ export const GameRoom: React.FC<GameRoomProps> = ({ room: initialRoom, currentPl
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Host Disconnected Overlay */}
|
||||||
|
{isHostOffline && !isMeHost && (
|
||||||
|
<div className="absolute inset-0 z-50 bg-black/80 backdrop-blur-md flex flex-col items-center justify-center p-8 animate-in fade-in duration-500">
|
||||||
|
<div className="bg-slate-900 border border-red-500/50 p-8 rounded-2xl shadow-2xl max-w-lg text-center">
|
||||||
|
<div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<Users className="w-8 h-8 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-2">Game Paused</h2>
|
||||||
|
<p className="text-slate-300 mb-6">
|
||||||
|
The host <span className="text-white font-bold">{host?.name}</span> has disconnected.
|
||||||
|
The game is paused until they reconnect.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col gap-6 items-center">
|
||||||
|
<div className="flex items-center justify-center gap-2 text-xs text-slate-500 uppercase tracking-wider font-bold animate-pulse">
|
||||||
|
<span className="w-2 h-2 bg-red-500 rounded-full"></span>
|
||||||
|
Waiting for host...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (window.confirm("Are you sure you want to leave the game?")) {
|
||||||
|
onExit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="px-6 py-2 bg-slate-800 hover:bg-red-900/30 text-slate-400 hover:text-red-400 border border-slate-700 hover:border-red-500/50 rounded-lg flex items-center gap-2 transition-all"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" /> Leave Game
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Global Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={modalOpen}
|
||||||
|
onClose={() => setModalOpen(false)}
|
||||||
|
title={modalConfig.title}
|
||||||
|
message={modalConfig.message}
|
||||||
|
type={modalConfig.type}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) =>
|
|||||||
const [joinRoomId, setJoinRoomId] = useState('');
|
const [joinRoomId, setJoinRoomId] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [initialDraftState, setInitialDraftState] = useState<any>(null);
|
||||||
|
|
||||||
const [playerId] = useState(() => {
|
const [playerId] = useState(() => {
|
||||||
const saved = localStorage.getItem('player_id');
|
const saved = localStorage.getItem('player_id');
|
||||||
if (saved) return saved;
|
if (saved) return saved;
|
||||||
@@ -128,6 +130,7 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
setInitialDraftState(response.draftState || null);
|
||||||
setActiveRoom(response.room);
|
setActiveRoom(response.room);
|
||||||
} else {
|
} else {
|
||||||
setError(response.message || 'Failed to join room');
|
setError(response.message || 'Failed to join room');
|
||||||
@@ -154,12 +157,7 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) =>
|
|||||||
connect();
|
connect();
|
||||||
socketService.emitPromise('rejoin_room', { roomId: savedRoomId })
|
socketService.emitPromise('rejoin_room', { roomId: savedRoomId })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// We don't get the room back directly in this event usually, but let's assume socket events 'room_update' handles it?
|
// Rejoin logic mostly handled by onRoomUpdate via socket
|
||||||
// The backend 'rejoin_room' doesn't return a callback with room data in the current implementation, it emits updates.
|
|
||||||
// However, let's try to invoke 'join_room' logic as a fallback or assume room_update catches it.
|
|
||||||
// Actually, backend 'rejoin_room' DOES emit 'room_update'.
|
|
||||||
// Let's rely on the socket listener in GameRoom... wait, GameRoom is not mounted yet!
|
|
||||||
// We need to listen to 'room_update' HERE to switch state.
|
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.warn("Reconnection failed", err);
|
console.warn("Reconnection failed", err);
|
||||||
@@ -183,8 +181,17 @@ export const LobbyManager: React.FC<LobbyManagerProps> = ({ generatedPacks }) =>
|
|||||||
}, [playerId]);
|
}, [playerId]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleExitRoom = () => {
|
||||||
|
setActiveRoom(null);
|
||||||
|
setInitialDraftState(null);
|
||||||
|
localStorage.removeItem('active_room_id');
|
||||||
|
// Also likely want to disconnect socket or leave room specifically if needed,
|
||||||
|
// but just clearing local state allows creating new rooms.
|
||||||
|
// Ideally: socketService.emit('leave_room', { roomId: activeRoom.id, playerId });
|
||||||
|
};
|
||||||
|
|
||||||
if (activeRoom) {
|
if (activeRoom) {
|
||||||
return <GameRoom room={activeRoom} currentPlayerId={playerId} />;
|
return <GameRoom room={activeRoom} currentPlayerId={playerId} onExit={handleExitRoom} initialDraftState={initialDraftState} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -115,8 +115,13 @@ export const DeckTester: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExitTester = () => {
|
||||||
|
setActiveRoom(null);
|
||||||
|
setInitialGame(null);
|
||||||
|
};
|
||||||
|
|
||||||
if (activeRoom) {
|
if (activeRoom) {
|
||||||
return <GameRoom room={activeRoom} currentPlayerId={playerId} initialGameState={initialGame} />;
|
return <GameRoom room={activeRoom} currentPlayerId={playerId} initialGameState={initialGame} onExit={handleExitTester} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -88,13 +88,14 @@ io.on('connection', (socket) => {
|
|||||||
console.log(`Player ${playerName} joined room ${roomId}`);
|
console.log(`Player ${playerName} joined room ${roomId}`);
|
||||||
io.to(room.id).emit('room_update', room); // Broadcast update
|
io.to(room.id).emit('room_update', room); // Broadcast update
|
||||||
|
|
||||||
// If drafting, send state immediately
|
// If drafting, send state immediately and include in callback
|
||||||
|
let currentDraft = null;
|
||||||
if (room.status === 'drafting') {
|
if (room.status === 'drafting') {
|
||||||
const draft = draftManager.getDraft(roomId);
|
currentDraft = draftManager.getDraft(roomId);
|
||||||
if (draft) socket.emit('draft_update', draft);
|
if (currentDraft) socket.emit('draft_update', currentDraft);
|
||||||
}
|
}
|
||||||
|
|
||||||
callback({ success: true, room });
|
callback({ success: true, room, draftState: currentDraft });
|
||||||
} else {
|
} else {
|
||||||
callback({ success: false, message: 'Room not found or full' });
|
callback({ success: false, message: 'Room not found or full' });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user