Merge branch 'main' of https://git.mdnd-it.cc/Donat/SerpentRace
This commit is contained in:
@@ -197,9 +197,8 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise<void> {
|
||||
try {
|
||||
// Simple data extraction - let Socket.IO handle the parsing
|
||||
const jsdata = JSON.parse(data);
|
||||
const gameToken = jsdata?.gameToken;
|
||||
// Socket.IO automatically deserializes JSON - data is already an object
|
||||
const gameToken = data?.gameToken;
|
||||
|
||||
if (!gameToken) {
|
||||
logError('Game join failed: No game token provided');
|
||||
@@ -243,6 +242,12 @@ export class GameWebSocketService {
|
||||
const isGamemaster = game.createdby === userId;
|
||||
const needsApproval = game.logintype === LoginType.PRIVATE && !isGamemaster;
|
||||
|
||||
logOther(`Player joining game: ${playerName}`);
|
||||
logOther(` - userId: ${userId}`);
|
||||
logOther(` - game.createdby: ${game.createdby}`);
|
||||
logOther(` - isGamemaster: ${isGamemaster}`);
|
||||
logOther(` - needsApproval: ${needsApproval}`);
|
||||
|
||||
// Generate dynamic room names (needed for both approval and direct join)
|
||||
const gameRoomName = `game_${gameCode}`;
|
||||
const playerRoomName = `game_${gameCode}:${playerName}`;
|
||||
@@ -275,6 +280,8 @@ export class GameWebSocketService {
|
||||
await socket.join(gameRoomName);
|
||||
await socket.join(playerRoomName);
|
||||
|
||||
// Update Redis with active player connection FIRST (before getting state)
|
||||
await this.updatePlayerConnection(gameCode, playerName, true);
|
||||
|
||||
// Send success response to the joining player
|
||||
socket.emit('game:joined', {
|
||||
@@ -296,13 +303,12 @@ export class GameWebSocketService {
|
||||
});
|
||||
|
||||
|
||||
// Send current game state to the joining player
|
||||
// Send current game state to the joining player (now includes this player)
|
||||
const gameState = await this.getGameState(gameCode);
|
||||
socket.emit('game:state', gameState);
|
||||
|
||||
|
||||
// Update Redis with active player connection
|
||||
await this.updatePlayerConnection(gameCode, playerName, true);
|
||||
// Broadcast updated game state to all other players so they see the new player
|
||||
socket.to(gameRoomName).emit('game:state-update', gameState);
|
||||
|
||||
} catch (error) {
|
||||
socket.emit('game:error', {
|
||||
@@ -314,7 +320,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise<void> {
|
||||
try {
|
||||
const { gameCode } = JSON.parse(data as any);
|
||||
const { gameCode } = data;
|
||||
const playerName = socket.playerName;
|
||||
|
||||
// Validate we have the required data
|
||||
@@ -354,7 +360,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, action, data: actionData } = JSON.parse(data as any);
|
||||
const { gameCode, action, data: actionData } = data;
|
||||
|
||||
if (!socket.gameCode || socket.gameCode !== gameCode) {
|
||||
socket.emit('game:error', { message: 'You must be in the game to perform actions' });
|
||||
@@ -396,7 +402,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleGameChat(socket: AuthenticatedSocket, data: GameChatData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, message } = JSON.parse(data as any);
|
||||
const { gameCode, message } = data;
|
||||
|
||||
if (!socket.gameCode || socket.gameCode !== gameCode) {
|
||||
socket.emit('game:error', { message: 'You must be in the game to chat' });
|
||||
@@ -422,7 +428,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handlePlayerReady(socket: AuthenticatedSocket, data: { gameCode: string; ready: boolean }): Promise<void> {
|
||||
try {
|
||||
const { gameCode, ready } = JSON.parse(data as any);
|
||||
const { gameCode, ready } = data;
|
||||
const gameRoomName = `game_${gameCode}`;
|
||||
|
||||
// Update player ready status in Redis
|
||||
@@ -452,7 +458,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleApprovePlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string }): Promise<void> {
|
||||
try {
|
||||
const { gameCode, playerName } = JSON.parse(data as any);
|
||||
const { gameCode, playerName } = data;
|
||||
|
||||
// Verify that the requesting socket is the gamemaster
|
||||
const game = await this.gameRepository.findByGameCode(gameCode);
|
||||
@@ -513,7 +519,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleRejectPlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string; reason?: string }): Promise<void> {
|
||||
try {
|
||||
const { gameCode, playerName, reason } = JSON.parse(data as any);
|
||||
const { gameCode, playerName, reason } = data;
|
||||
|
||||
// Verify that the requesting socket is the gamemaster
|
||||
const game = await this.gameRepository.findByGameCode(gameCode);
|
||||
@@ -561,7 +567,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise<void> {
|
||||
try {
|
||||
const { gameToken } = JSON.parse(data as any);
|
||||
const { gameToken } = data;
|
||||
|
||||
if (!gameToken) {
|
||||
socket.emit('game:error', { message: 'Game token is required' });
|
||||
@@ -606,6 +612,9 @@ export class GameWebSocketService {
|
||||
|
||||
logOther(`Approved player ${playerName} joined game room: ${gameRoomName}`);
|
||||
|
||||
// Update Redis with active player connection FIRST (before getting state)
|
||||
await this.updatePlayerConnection(gameCode, playerName, true);
|
||||
|
||||
// Send success response to the joining player
|
||||
socket.emit('game:joined', {
|
||||
gameCode,
|
||||
@@ -624,12 +633,12 @@ export class GameWebSocketService {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Send current game state to the joining player
|
||||
// Send current game state to the joining player (now includes this player)
|
||||
const gameState = await this.getGameState(gameCode);
|
||||
socket.emit('game:state', gameState);
|
||||
|
||||
// Update Redis with active player connection
|
||||
await this.updatePlayerConnection(gameCode, playerName, true);
|
||||
// Broadcast updated game state to all other players so they see the new player
|
||||
socket.to(gameRoomName).emit('game:state-update', gameState);
|
||||
|
||||
} catch (error) {
|
||||
logError('Error handling approved join', error as Error);
|
||||
@@ -639,7 +648,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleDiceRoll(socket: AuthenticatedSocket, data: DiceRollData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, diceValue } = JSON.parse(data as any);
|
||||
const { gameCode, diceValue } = data;
|
||||
|
||||
// Validate input
|
||||
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
|
||||
@@ -738,7 +747,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleCardAnswer(socket: AuthenticatedSocket, data: CardAnswerData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, answer } = JSON.parse(data as any);
|
||||
const { gameCode, answer } = data;
|
||||
|
||||
// Validate input
|
||||
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
|
||||
@@ -848,7 +857,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleGamemasterDecision(socket: AuthenticatedSocket, data: GamemasterDecisionData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, requestId, decision } = JSON.parse(data as any);
|
||||
const { gameCode, requestId, decision } = data;
|
||||
|
||||
// Validate input
|
||||
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ToastConfig } from "./components/Toastify/toastifyServices" // ✅ font
|
||||
import VerifyEmailPage from "./pages/Auth/VerifyEmailPage"
|
||||
import ChooseDeck from "./pages/Game/ChooseDeck"
|
||||
import PlayerSetup from "./pages/Game/PlayerSetup"
|
||||
import GameModalsDemo from "./pages/Game/GameModalsDemo"
|
||||
|
||||
function App() {
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { API_CONFIG } from '../api/userApi';
|
||||
const isDev = import.meta.env.DEV;
|
||||
const log = (...args) => isDev && console.log(...args);
|
||||
const warn = (...args) => isDev && console.warn(...args);
|
||||
const error = (...args) => console.error(...args);
|
||||
const logError = (...args) => console.error(...args);
|
||||
|
||||
/**
|
||||
* Optimized WebSocket hook for game connection
|
||||
@@ -20,29 +20,53 @@ export const useGameWebSocket = (gameToken) => {
|
||||
const [error, setError] = useState(null);
|
||||
const [isGamemaster, setIsGamemaster] = useState(false);
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const [pendingPlayers, setPendingPlayers] = useState([]); // Players waiting for approval
|
||||
const [approvalStatus, setApprovalStatus] = useState(null); // 'pending' | 'approved' | 'denied' | null
|
||||
const eventListenersRef = useRef(new Map());
|
||||
|
||||
// Memoized derived values - no extra state needed
|
||||
const players = useMemo(() => {
|
||||
// Backend sends different player fields depending on game state
|
||||
// connectedPlayers: array of player names (strings) who are connected via WebSocket
|
||||
// players: full player objects with game data (positions, etc.)
|
||||
// players: array of player IDs (UUIDs) - NOT USEFUL for display
|
||||
// currentPlayers: full player objects with game data (positions, etc.)
|
||||
const connectedPlayers = gameState?.connectedPlayers || [];
|
||||
const gamePlayers = gameState?.players || [];
|
||||
const currentPlayers = gameState?.currentPlayers || [];
|
||||
|
||||
// If we have full player objects, use those
|
||||
if (currentPlayers.length > 0) return currentPlayers;
|
||||
if (gamePlayers.length > 0) return gamePlayers;
|
||||
// Debug: log what we received
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🎮 Computing players list:');
|
||||
console.log(' - connectedPlayers:', connectedPlayers);
|
||||
console.log(' - currentPlayers:', currentPlayers);
|
||||
}
|
||||
|
||||
// Otherwise, map connected player names to basic player objects
|
||||
return connectedPlayers.map((name, index) => ({
|
||||
id: `player-${index}`,
|
||||
name: typeof name === 'string' ? name : name.playerName || `Player ${index + 1}`,
|
||||
isOnline: true,
|
||||
isReady: gameState?.readyPlayers?.includes(name) || false,
|
||||
}));
|
||||
}, [gameState?.connectedPlayers, gameState?.players, gameState?.currentPlayers, gameState?.readyPlayers]);
|
||||
// If we have full player objects with positions, use those (during game)
|
||||
if (currentPlayers.length > 0) {
|
||||
console.log('✅ Using currentPlayers');
|
||||
return currentPlayers;
|
||||
}
|
||||
|
||||
// Otherwise, use connectedPlayers (player names from Redis)
|
||||
if (connectedPlayers.length > 0) {
|
||||
console.log('✅ Mapping connectedPlayers to player objects');
|
||||
return connectedPlayers.map((nameOrObj, index) => {
|
||||
// Handle both string names and objects
|
||||
const playerName = typeof nameOrObj === 'string'
|
||||
? nameOrObj
|
||||
: (nameOrObj.playerName || nameOrObj.name || `Player ${index + 1}`);
|
||||
|
||||
return {
|
||||
id: `player-${index}`,
|
||||
name: playerName,
|
||||
isOnline: true,
|
||||
isReady: gameState?.readyPlayers?.includes(playerName) || false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
console.log('⚠️ No players found');
|
||||
return [];
|
||||
}, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]);
|
||||
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
|
||||
|
||||
// Connect to game WebSocket - only once per token
|
||||
@@ -83,44 +107,92 @@ export const useGameWebSocket = (gameToken) => {
|
||||
|
||||
// Game state handlers - batch updates
|
||||
const handleGameState = (state) => {
|
||||
log('📊 Game state:', state);
|
||||
log('📊 Game state received:', state);
|
||||
log(' - connectedPlayers:', state?.connectedPlayers);
|
||||
log(' - players:', state?.players);
|
||||
log(' - currentPlayers:', state?.currentPlayers);
|
||||
log(' - isGamemaster in state:', state?.isGamemaster);
|
||||
|
||||
// EXTRA DEBUG: Show full state structure
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🔍 FULL STATE OBJECT:', JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
// If state contains isGamemaster flag, update it
|
||||
if (state?.isGamemaster !== undefined) {
|
||||
log('✅ Setting isGamemaster from state:', state.isGamemaster);
|
||||
setIsGamemaster(state.isGamemaster);
|
||||
}
|
||||
|
||||
setGameState(state);
|
||||
};
|
||||
|
||||
const handleGameJoined = (data) => {
|
||||
log('✅ Joined game:', data);
|
||||
|
||||
// EXTRA DEBUG: Show full joined data
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🔍 FULL JOINED DATA:', JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Store if this user is the gamemaster
|
||||
if (data.isGamemaster !== undefined) {
|
||||
log('✅ Setting isGamemaster from joined event:', data.isGamemaster);
|
||||
setIsGamemaster(data.isGamemaster);
|
||||
} else {
|
||||
log('⚠️ No isGamemaster flag in joined event');
|
||||
}
|
||||
// Backend will send game:state next
|
||||
};
|
||||
|
||||
const handlePlayerJoined = (data) => {
|
||||
log('👤 Player joined:', data.playerName);
|
||||
|
||||
// EXTRA DEBUG
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🔍 PLAYER JOINED EVENT:', JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Update game state to add the new player to connectedPlayers
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
if (!prev) {
|
||||
log('⚠️ No previous game state, cannot add player');
|
||||
return prev;
|
||||
}
|
||||
|
||||
const currentConnected = prev.connectedPlayers || [];
|
||||
// Only add if not already in the list
|
||||
if (!currentConnected.includes(data.playerName)) {
|
||||
log('✅ Adding player to connectedPlayers:', data.playerName);
|
||||
log(' - Current list:', currentConnected);
|
||||
log(' - New list:', [...currentConnected, data.playerName]);
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: [...currentConnected, data.playerName]
|
||||
};
|
||||
}
|
||||
log('⚠️ Player already in connectedPlayers:', data.playerName);
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
const handleGameStarted = (data) => {
|
||||
log('🎮 Game started:', data);
|
||||
// Batch state updates
|
||||
if (data.boardData) setBoardData(data.boardData);
|
||||
if (data.gameState) setGameState(data.gameState);
|
||||
|
||||
// EXTRA DEBUG
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🔍 GAME STARTED EVENT:', JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
// Signal that game has started
|
||||
setGameStarted(true);
|
||||
|
||||
// Request updated game state from server (includes boardData and currentPlayers)
|
||||
const socket = socketRef.current;
|
||||
if (socket && socket.connected) {
|
||||
log('📡 Requesting updated game state after game start');
|
||||
// The server will send game:state event with full data
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlayerMoved = (moveData) => {
|
||||
@@ -145,10 +217,68 @@ export const useGameWebSocket = (gameToken) => {
|
||||
};
|
||||
|
||||
const handleError = (err) => {
|
||||
error('❌ Game error:', err);
|
||||
logError('❌ Game error:', err);
|
||||
setError(err.message);
|
||||
};
|
||||
|
||||
// Approval system handlers (PRIVATE games only)
|
||||
const handlePendingApproval = (data) => {
|
||||
log('⏳ Pending gamemaster approval:', data);
|
||||
setApprovalStatus('pending');
|
||||
setError('Waiting for gamemaster approval...');
|
||||
};
|
||||
|
||||
const handlePlayerRequestingJoin = (data) => {
|
||||
log('🔔 Player requesting to join:', data.playerName);
|
||||
// Add to pending players list (for gamemaster)
|
||||
setPendingPlayers(prev => {
|
||||
if (prev.some(p => p.playerName === data.playerName)) return prev;
|
||||
return [...prev, {
|
||||
playerName: data.playerName,
|
||||
isAuthenticated: data.isAuthenticated,
|
||||
timestamp: data.timestamp
|
||||
}];
|
||||
});
|
||||
};
|
||||
|
||||
const handleApprovalGranted = (data) => {
|
||||
log('✅ Join request approved:', data);
|
||||
setApprovalStatus('approved');
|
||||
setError(null);
|
||||
|
||||
// Player should now join the game rooms
|
||||
const socket = socketRef.current;
|
||||
if (socket && data.gameRoomName) {
|
||||
// Emit join-approved to notify backend
|
||||
socket.emit('game:join-approved', { gameToken });
|
||||
}
|
||||
};
|
||||
|
||||
const handleApprovalDenied = (data) => {
|
||||
error('❌ Join request denied:', data.reason || data.message);
|
||||
setApprovalStatus('denied');
|
||||
setError(data.reason || 'Your request to join was denied');
|
||||
};
|
||||
|
||||
const handlePlayerApproved = (data) => {
|
||||
log('✅ Player approved by gamemaster:', data.playerName);
|
||||
// Remove from pending players list
|
||||
setPendingPlayers(prev => prev.filter(p => p.playerName !== data.playerName));
|
||||
|
||||
// Add to connected players
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
const currentConnected = prev.connectedPlayers || [];
|
||||
if (!currentConnected.includes(data.playerName)) {
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: [...currentConnected, data.playerName]
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
};
|
||||
|
||||
// Register all handlers
|
||||
socket.on('connect', handleConnect);
|
||||
socket.on('connect_error', handleConnectError);
|
||||
@@ -161,6 +291,13 @@ export const useGameWebSocket = (gameToken) => {
|
||||
socket.on('game:player-moved', handlePlayerMoved);
|
||||
socket.on('game:turn-changed', handleTurnChanged);
|
||||
socket.on('game:error', handleError);
|
||||
|
||||
// Approval system events (PRIVATE games)
|
||||
socket.on('game:pending-approval', handlePendingApproval);
|
||||
socket.on('game:player-requesting-join', handlePlayerRequestingJoin);
|
||||
socket.on('game:approval-granted', handleApprovalGranted);
|
||||
socket.on('game:approval-denied', handleApprovalDenied);
|
||||
socket.on('game:player-approved', handlePlayerApproved);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
@@ -247,6 +384,115 @@ export const useGameWebSocket = (gameToken) => {
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
// Joker approval methods
|
||||
const approveJoker = useCallback((playerId, cardId, requestId) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot approve joker: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('✅ Approving joker for player:', playerId);
|
||||
socket.emit('game:gamemaster-decision', {
|
||||
gameCode: gameState?.gameCode,
|
||||
requestId: requestId || `joker-${playerId}-${cardId}`,
|
||||
decision: 'approve',
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
const rejectJoker = useCallback((playerId, cardId, requestId) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot reject joker: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('❌ Rejecting joker for player:', playerId);
|
||||
socket.emit('game:gamemaster-decision', {
|
||||
gameCode: gameState?.gameCode,
|
||||
requestId: requestId || `joker-${playerId}-${cardId}`,
|
||||
decision: 'reject',
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
// Card answer submission
|
||||
const submitAnswer = useCallback((cardId, answer) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot submit answer: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('📝 Submitting answer:', answer);
|
||||
socket.emit('game:card-answer', {
|
||||
gameCode: gameState?.gameCode,
|
||||
cardId,
|
||||
answer,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
// Position guess submission
|
||||
const submitPositionGuess = useCallback((guessedPosition) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot submit position guess: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('🎯 Submitting position guess:', guessedPosition);
|
||||
socket.emit('game:position-guess', {
|
||||
gameCode: gameState?.gameCode,
|
||||
guessedPosition,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
// Approve player (gamemaster only, PRIVATE games)
|
||||
const approvePlayer = useCallback((playerName) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot approve player: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isGamemaster) {
|
||||
warn('⚠️ Only gamemaster can approve players');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('✅ Approving player:', playerName);
|
||||
socket.emit('game:approve-player', {
|
||||
gameCode: gameState?.gameCode,
|
||||
playerName,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, isGamemaster, gameState?.gameCode]);
|
||||
|
||||
// Reject player (gamemaster only, PRIVATE games)
|
||||
const rejectPlayer = useCallback((playerName, reason = 'Join request denied') => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot reject player: not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isGamemaster) {
|
||||
warn('⚠️ Only gamemaster can reject players');
|
||||
return false;
|
||||
}
|
||||
|
||||
log('❌ Rejecting player:', playerName);
|
||||
socket.emit('game:reject-player', {
|
||||
gameCode: gameState?.gameCode,
|
||||
playerName,
|
||||
reason,
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, isGamemaster, gameState?.gameCode]);
|
||||
|
||||
return {
|
||||
socket: socketRef.current,
|
||||
isConnected,
|
||||
@@ -257,11 +503,19 @@ export const useGameWebSocket = (gameToken) => {
|
||||
error,
|
||||
isGamemaster,
|
||||
gameStarted,
|
||||
pendingPlayers,
|
||||
approvalStatus,
|
||||
// Methods
|
||||
rollDice,
|
||||
sendMessage,
|
||||
setReady,
|
||||
leaveGame,
|
||||
approveJoker,
|
||||
rejectJoker,
|
||||
submitAnswer,
|
||||
submitPositionGuess,
|
||||
approvePlayer,
|
||||
rejectPlayer,
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
/**
|
||||
* CardDisplayModal - Kártya megjelenítése a játékos számára
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - Modal megjelenítése
|
||||
* @param {Function} props.onClose - Modal bezárása
|
||||
* @param {Object} props.card - Kártya adatok
|
||||
* @param {string} props.cardType - Kártya típusa (QUESTION, LUCK, JOKER)
|
||||
* @param {Function} props.onSubmitAnswer - Válasz beküldése (csak QUESTION típusnál)
|
||||
* @param {number} props.timeLimit - Időkorlát másodpercben (default: 60)
|
||||
*/
|
||||
const CardDisplayModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
card,
|
||||
cardType = "QUESTION",
|
||||
onSubmitAnswer,
|
||||
timeLimit = 60
|
||||
}) => {
|
||||
const [playerAnswer, setPlayerAnswer] = useState("")
|
||||
const [selectedOption, setSelectedOption] = useState(null)
|
||||
const [timeLeft, setTimeLeft] = useState(timeLimit)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
||||
// Timer countdown
|
||||
useEffect(() => {
|
||||
if (!isOpen || cardType !== "QUESTION") return
|
||||
|
||||
setTimeLeft(timeLimit)
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer)
|
||||
handleTimeout()
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [isOpen, timeLimit])
|
||||
|
||||
// Reset state when modal opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setPlayerAnswer("")
|
||||
setSelectedOption(null)
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleTimeout = () => {
|
||||
if (onSubmitAnswer) {
|
||||
onSubmitAnswer(null) // null = timeout
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isProcessing) return
|
||||
|
||||
let answer = null
|
||||
|
||||
// Quiz típus - A, B, C, D
|
||||
if (card?.type === 0 || card?.answerOptions) {
|
||||
answer = selectedOption
|
||||
}
|
||||
// Szöveges válasz
|
||||
else {
|
||||
answer = playerAnswer.trim()
|
||||
}
|
||||
|
||||
if (!answer) return
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
await onSubmitAnswer(answer)
|
||||
} catch (error) {
|
||||
console.error("Válasz küldési hiba:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const getCardIcon = () => {
|
||||
switch (cardType) {
|
||||
case "QUESTION": return "❓"
|
||||
case "LUCK": return "🍀"
|
||||
case "JOKER": return "🃏"
|
||||
default: return "📝"
|
||||
}
|
||||
}
|
||||
|
||||
const getCardTitle = () => {
|
||||
switch (cardType) {
|
||||
case "QUESTION": return "Feladat Kártya"
|
||||
case "LUCK": return "Szerencse Kártya"
|
||||
case "JOKER": return "Joker Kártya"
|
||||
default: return "Kártya"
|
||||
}
|
||||
}
|
||||
|
||||
const getCardBgGradient = () => {
|
||||
switch (cardType) {
|
||||
case "QUESTION": return "from-blue-600 via-purple-600 to-blue-600"
|
||||
case "LUCK": return "from-green-600 via-teal-600 to-green-600"
|
||||
case "JOKER": return "from-purple-600 via-pink-600 to-purple-600"
|
||||
default: return "from-gray-600 via-gray-700 to-gray-600"
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const getTimeColor = () => {
|
||||
if (timeLeft > 30) return "text-green-400"
|
||||
if (timeLeft > 10) return "text-yellow-400"
|
||||
return "text-red-400 animate-pulse"
|
||||
}
|
||||
|
||||
if (!isOpen || !card) return null
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.5 }}
|
||||
className="relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 border-purple-500/30 max-w-2xl w-full overflow-hidden"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={`bg-gradient-to-r ${getCardBgGradient()} p-6 relative overflow-hidden`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
|
||||
<div className="relative flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-5xl animate-bounce">{getCardIcon()}</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">{getCardTitle()}</h2>
|
||||
{cardType === "QUESTION" && (
|
||||
<p className="text-white/80 text-sm">Válaszolj a kérdésre!</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timer - csak QUESTION típusnál */}
|
||||
{cardType === "QUESTION" && (
|
||||
<div className="bg-black/30 rounded-lg px-4 py-2">
|
||||
<div className={`text-2xl font-bold ${getTimeColor()}`}>
|
||||
⏱️ {formatTime(timeLeft)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Question/Text */}
|
||||
<div className="bg-gray-800/50 rounded-xl p-5 border border-gray-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-3xl">📝</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white text-lg leading-relaxed">
|
||||
{card.question || card.text || card.statement}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Answer Options - Quiz típus (type: 0) */}
|
||||
{cardType === "QUESTION" && (card.type === 0 || card.answerOptions) && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-purple-300 font-semibold">Válaszd ki a helyes választ:</h3>
|
||||
{card.answerOptions?.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setSelectedOption(option.answer)}
|
||||
disabled={isProcessing}
|
||||
className={`w-full text-left p-4 rounded-lg border-2 transition-all ${
|
||||
selectedOption === option.answer
|
||||
? "bg-purple-600 border-purple-400 text-white"
|
||||
: "bg-gray-800 border-gray-600 text-gray-300 hover:border-purple-500"
|
||||
} ${isProcessing ? "opacity-50 cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<span className="font-bold">{option.answer})</span> {option.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Input - egyéb kérdés típusok */}
|
||||
{cardType === "QUESTION" && card.type !== 0 && !card.answerOptions && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-purple-300 font-semibold">Írd be a választ:</h3>
|
||||
<input
|
||||
type="text"
|
||||
value={playerAnswer}
|
||||
onChange={(e) => setPlayerAnswer(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSubmit()}
|
||||
disabled={isProcessing}
|
||||
placeholder="Válaszod..."
|
||||
className="w-full bg-gray-800 border-2 border-gray-600 rounded-lg px-4 py-3 text-white focus:border-purple-500 focus:outline-none disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hint (if available) */}
|
||||
{card.hint && (
|
||||
<div className="bg-yellow-900/20 rounded-xl p-4 border border-yellow-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-2xl">💡</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-yellow-300 font-semibold mb-2">Segítség</h3>
|
||||
<p className="text-gray-300 text-sm">{card.hint}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button - csak QUESTION típusnál */}
|
||||
{cardType === "QUESTION" && (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={isProcessing || (!playerAnswer && !selectedOption)}
|
||||
className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500
|
||||
text-white font-bold py-4 px-6 rounded-xl shadow-lg
|
||||
transform transition-all duration-200 hover:scale-105 active:scale-95
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||||
border border-purple-500/50"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-2xl">✅</span>
|
||||
<span className="text-lg">
|
||||
{isProcessing ? "Feldolgozás..." : "Válasz beküldése"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Close Button - LUCK és JOKER típusnál */}
|
||||
{(cardType === "LUCK" || cardType === "JOKER") && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-500 hover:to-teal-500
|
||||
text-white font-bold py-4 px-6 rounded-xl shadow-lg
|
||||
transform transition-all duration-200 hover:scale-105 active:scale-95
|
||||
border border-green-500/50"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-2xl">👍</span>
|
||||
<span className="text-lg">Rendben</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default CardDisplayModal
|
||||
@@ -0,0 +1,202 @@
|
||||
import React from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
/**
|
||||
* ConsequenceModal - Következmények megjelenítése (jó/rossz válasz után)
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - Modal megjelenítése
|
||||
* @param {Function} props.onClose - Modal bezárása
|
||||
* @param {boolean} props.isCorrect - Helyes volt-e a válasz
|
||||
* @param {string} props.consequence - Következmény szövege
|
||||
* @param {number} props.consequenceType - Következmény típusa:
|
||||
* 0 = MOVE_FORWARD (előre lépés)
|
||||
* 1 = MOVE_BACKWARD (hátra lépés)
|
||||
* 2 = LOSE_TURN (körkihagyás)
|
||||
* 3 = EXTRA_TURN (extra kör)
|
||||
* 5 = GO_TO_START (vissza a starthoz)
|
||||
* @param {number} props.consequenceValue - Következmény értéke (hány mező/kör)
|
||||
* @param {string} props.playerAnswer - Játékos válasza
|
||||
* @param {string} props.correctAnswer - Helyes válasz
|
||||
* @param {string} props.explanation - Magyarázat
|
||||
*/
|
||||
const ConsequenceModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
isCorrect,
|
||||
consequence,
|
||||
consequenceType,
|
||||
consequenceValue,
|
||||
playerAnswer,
|
||||
correctAnswer,
|
||||
explanation
|
||||
}) => {
|
||||
|
||||
const getConsequenceIcon = (type) => {
|
||||
switch(type) {
|
||||
case 0: return "🚀" // MOVE_FORWARD
|
||||
case 1: return "⬅️" // MOVE_BACKWARD
|
||||
case 2: return "😴" // LOSE_TURN
|
||||
case 3: return "🎉" // EXTRA_TURN
|
||||
case 5: return "🏁" // GO_TO_START
|
||||
default: return "📢"
|
||||
}
|
||||
}
|
||||
|
||||
const getConsequenceText = (type, value) => {
|
||||
switch(type) {
|
||||
case 0: return `${value} mezőt léphetsz előre! 🚀`
|
||||
case 1: return `${value} mezőt lépsz vissza! �`
|
||||
case 2: return `${value} kört ki kell hagyni! �`
|
||||
case 3: return `${value} extra kör jár neked! 🎉`
|
||||
case 5: return "Vissza a starthoz! 🏁"
|
||||
default: return consequence || "Következmény"
|
||||
}
|
||||
}
|
||||
|
||||
const getBgGradient = () => {
|
||||
if (isCorrect) {
|
||||
return "from-green-600 via-teal-600 to-green-600"
|
||||
}
|
||||
return "from-red-600 via-orange-600 to-red-600"
|
||||
}
|
||||
|
||||
const getBorderColor = () => {
|
||||
if (isCorrect) return "border-green-500/50"
|
||||
return "border-red-500/50"
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.5, opacity: 0, rotate: -10 }}
|
||||
animate={{ scale: 1, opacity: 1, rotate: 0 }}
|
||||
exit={{ scale: 0.5, opacity: 0, rotate: 10 }}
|
||||
transition={{ type: "spring", duration: 0.6 }}
|
||||
className={`relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 ${getBorderColor()} max-w-2xl w-full overflow-hidden`}
|
||||
>
|
||||
{/* Header with result */}
|
||||
<div className={`bg-gradient-to-r ${getBgGradient()} p-6 relative overflow-hidden`}>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent animate-pulse" />
|
||||
|
||||
<div className="relative text-center">
|
||||
<motion.div
|
||||
initial={{ scale: 0 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ delay: 0.2, type: "spring" }}
|
||||
className="text-8xl mb-2"
|
||||
>
|
||||
{isCorrect ? "✅" : "❌"}
|
||||
</motion.div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">
|
||||
{isCorrect ? "Helyes válasz!" : "Helytelen válasz!"}
|
||||
</h2>
|
||||
<p className="text-white/90 text-lg">
|
||||
{isCorrect ? "Gratulálunk! 🎉" : "Ne add fel! 💪"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Player Answer */}
|
||||
{playerAnswer && (
|
||||
<div className="bg-gray-800/50 rounded-xl p-4 border border-gray-700">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-2xl">💭</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-gray-400 text-sm mb-1">A te válaszod:</p>
|
||||
<p className="text-white font-semibold">{playerAnswer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Correct Answer - ha helytelen volt */}
|
||||
{!isCorrect && correctAnswer && (
|
||||
<div className="bg-green-900/20 rounded-xl p-4 border border-green-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-2xl">✔️</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-green-300 text-sm mb-1">A helyes válasz:</p>
|
||||
<p className="text-white font-semibold">{correctAnswer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Explanation */}
|
||||
{explanation && (
|
||||
<div className="bg-blue-900/20 rounded-xl p-4 border border-blue-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-2xl">💡</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-blue-300 text-sm mb-1">Magyarázat:</p>
|
||||
<p className="text-gray-300">{explanation}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Consequence */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ delay: 0.4 }}
|
||||
className={`${isCorrect ? 'bg-gradient-to-br from-green-900/30 to-teal-900/30 border-green-500/40' : 'bg-gradient-to-br from-red-900/30 to-orange-900/30 border-red-500/40'} rounded-xl p-6 border-2`}
|
||||
>
|
||||
<div className="text-center">
|
||||
<motion.div
|
||||
animate={{ rotate: [0, 10, -10, 10, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
className="text-6xl mb-3"
|
||||
>
|
||||
{getConsequenceIcon(consequenceType)}
|
||||
</motion.div>
|
||||
<h3 className={`text-xl font-bold mb-2 ${isCorrect ? 'text-green-300' : 'text-red-300'}`}>
|
||||
Következmény:
|
||||
</h3>
|
||||
<p className="text-white text-2xl font-bold">
|
||||
{getConsequenceText(consequenceType, consequenceValue)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`w-full bg-gradient-to-r ${
|
||||
isCorrect
|
||||
? 'from-green-600 to-teal-600 hover:from-green-500 hover:to-teal-500 border-green-500/50'
|
||||
: 'from-red-600 to-orange-600 hover:from-red-500 hover:to-orange-500 border-red-500/50'
|
||||
} text-white font-bold py-4 px-6 rounded-xl shadow-lg
|
||||
transform transition-all duration-200 hover:scale-105 active:scale-95 border`}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-2xl">👍</span>
|
||||
<span className="text-lg">Rendben, folytatom!</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConsequenceModal
|
||||
@@ -0,0 +1,250 @@
|
||||
import React, { useState } from "react"
|
||||
import CardDisplayModal from "./CardDisplayModal"
|
||||
import ConsequenceModal from "./ConsequenceModal"
|
||||
import StepPredictionModal from "./StepPredictionModal"
|
||||
|
||||
/**
|
||||
* Demo oldal a játék modal-ok tesztelésére
|
||||
*/
|
||||
const GameModalsDemo = () => {
|
||||
// Card Display Modal
|
||||
const [isCardModalOpen, setIsCardModalOpen] = useState(false)
|
||||
const [cardType, setCardType] = useState("QUESTION")
|
||||
|
||||
// Consequence Modal
|
||||
const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false)
|
||||
const [isCorrect, setIsCorrect] = useState(true)
|
||||
|
||||
// Step Prediction Modal
|
||||
const [isStepModalOpen, setIsStepModalOpen] = useState(false)
|
||||
|
||||
// Example cards
|
||||
const quizCard = {
|
||||
type: 0,
|
||||
question: "Mi Magyarország fővárosa?",
|
||||
answerOptions: [
|
||||
{ answer: "A", text: "Debrecen", correct: false },
|
||||
{ answer: "B", text: "Budapest", correct: true },
|
||||
{ answer: "C", text: "Szeged", correct: false },
|
||||
{ answer: "D", text: "Pécs", correct: false }
|
||||
],
|
||||
hint: "A Duna partján fekszik"
|
||||
}
|
||||
|
||||
const textCard = {
|
||||
type: 2,
|
||||
question: "Hány éves vagy?",
|
||||
hint: "Számmal válaszolj"
|
||||
}
|
||||
|
||||
const luckCard = {
|
||||
text: "Szerencsés vagy! +2 lépés előre! 🍀",
|
||||
consequence: { type: 3, value: 2 }
|
||||
}
|
||||
|
||||
const handleCardAnswer = (answer) => {
|
||||
console.log("Válasz:", answer)
|
||||
setIsCardModalOpen(false)
|
||||
// Következmény megjelenítése
|
||||
setTimeout(() => {
|
||||
setIsCorrect(answer === "B")
|
||||
setIsConsequenceModalOpen(true)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleStepPrediction = (prediction) => {
|
||||
console.log("Tipp:", prediction)
|
||||
setIsStepModalOpen(false)
|
||||
|
||||
// Példa: Számított pozíció = 20 + 4 + 2 + 2 = 28
|
||||
const actualPosition = 28
|
||||
const isCorrect = prediction === actualPosition
|
||||
|
||||
// Következmény megjelenítése
|
||||
setTimeout(() => {
|
||||
setIsCorrect(isCorrect)
|
||||
setIsConsequenceModalOpen(true)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h1 className="text-4xl font-bold text-white mb-8 text-center">
|
||||
🎮 Játék Modal-ok Demo
|
||||
</h1>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Card Display Modal Demos */}
|
||||
<div className="bg-gray-800 rounded-xl p-6 border border-purple-500">
|
||||
<h2 className="text-2xl font-bold text-purple-300 mb-4">
|
||||
📝 Kártya Megjelenítés
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setCardType("QUESTION")
|
||||
setIsCardModalOpen(true)
|
||||
}}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 rounded-lg transition-all hover:scale-105"
|
||||
>
|
||||
❓ Quiz Kártya
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCardType("QUESTION")
|
||||
setIsCardModalOpen(true)
|
||||
}}
|
||||
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 rounded-lg transition-all hover:scale-105"
|
||||
>
|
||||
📝 Szöveges Kártya
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCardType("LUCK")
|
||||
setIsCardModalOpen(true)
|
||||
}}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 rounded-lg transition-all hover:scale-105"
|
||||
>
|
||||
🍀 Szerencse Kártya
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Consequence Modal Demos */}
|
||||
<div className="bg-gray-800 rounded-xl p-6 border border-green-500">
|
||||
<h2 className="text-2xl font-bold text-green-300 mb-4">
|
||||
🎯 Következmények
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsCorrect(true)
|
||||
setIsConsequenceModalOpen(true)
|
||||
}}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 rounded-lg transition-all hover:scale-105"
|
||||
>
|
||||
✅ Helyes Válasz
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsCorrect(false)
|
||||
setIsConsequenceModalOpen(true)
|
||||
}}
|
||||
className="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 rounded-lg transition-all hover:scale-105"
|
||||
>
|
||||
❌ Helytelen Válasz
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Prediction Modal Demo */}
|
||||
<div className="bg-gray-800 rounded-xl p-6 border border-yellow-500">
|
||||
<h2 className="text-2xl font-bold text-yellow-300 mb-4">
|
||||
🎲 Pozíció Tippelés
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIsStepModalOpen(true)}
|
||||
className="w-full bg-yellow-600 hover:bg-yellow-700 text-white font-bold py-3 rounded-lg transition-all hover:scale-105"
|
||||
>
|
||||
🎯 Pozíció Tippelés
|
||||
</button>
|
||||
<p className="text-gray-400 text-sm mt-3">
|
||||
Tipppeld meg a végleges pozíciót a számítás alapján!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Panel */}
|
||||
<div className="mt-8 bg-gray-800 rounded-xl p-6 border border-gray-700">
|
||||
<h3 className="text-xl font-bold text-white mb-4">ℹ️ Használat a GameScreen-ben</h3>
|
||||
<div className="bg-gray-900 rounded-lg p-4 text-sm">
|
||||
<pre className="text-green-400 overflow-x-auto">
|
||||
{`// Import-ok
|
||||
import CardDisplayModal from "./CardDisplayModal"
|
||||
import ConsequenceModal from "./ConsequenceModal"
|
||||
import StepPredictionModal from "./StepPredictionModal"
|
||||
|
||||
// State-ek
|
||||
const [isCardModalOpen, setIsCardModalOpen] = useState(false)
|
||||
const [currentCard, setCurrentCard] = useState(null)
|
||||
const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false)
|
||||
const [consequenceData, setConsequenceData] = useState(null)
|
||||
|
||||
// WebSocket event
|
||||
useEffect(() => {
|
||||
if (socket) {
|
||||
socket.on('game:card-drawn', (data) => {
|
||||
setCurrentCard(data.card)
|
||||
setIsCardModalOpen(true)
|
||||
})
|
||||
|
||||
socket.on('game:answer-result', (data) => {
|
||||
setConsequenceData(data)
|
||||
setIsConsequenceModalOpen(true)
|
||||
})
|
||||
}
|
||||
}, [socket])
|
||||
|
||||
// Render
|
||||
<CardDisplayModal
|
||||
isOpen={isCardModalOpen}
|
||||
onClose={() => setIsCardModalOpen(false)}
|
||||
card={currentCard}
|
||||
cardType="QUESTION"
|
||||
onSubmitAnswer={handleSubmitAnswer}
|
||||
/>
|
||||
|
||||
<ConsequenceModal
|
||||
isOpen={isConsequenceModalOpen}
|
||||
onClose={() => setIsConsequenceModalOpen(false)}
|
||||
{...consequenceData}
|
||||
/>`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<CardDisplayModal
|
||||
isOpen={isCardModalOpen}
|
||||
onClose={() => setIsCardModalOpen(false)}
|
||||
card={cardType === "LUCK" ? luckCard : quizCard}
|
||||
cardType={cardType}
|
||||
onSubmitAnswer={handleCardAnswer}
|
||||
/>
|
||||
|
||||
<ConsequenceModal
|
||||
isOpen={isConsequenceModalOpen}
|
||||
onClose={() => setIsConsequenceModalOpen(false)}
|
||||
isCorrect={isCorrect}
|
||||
consequenceType={isCorrect ? 3 : 4}
|
||||
consequenceValue={isCorrect ? 2 : 1}
|
||||
playerAnswer={isCorrect ? "Budapest" : "Debrecen"}
|
||||
correctAnswer="Budapest"
|
||||
explanation={isCorrect
|
||||
? "Budapest a magyar főváros, 1873-ban egyesült Buda, Pest és Óbuda városa."
|
||||
: "A helyes válasz Budapest. Debrecen Magyarország második legnagyobb városa."
|
||||
}
|
||||
/>
|
||||
|
||||
<StepPredictionModal
|
||||
isOpen={isStepModalOpen}
|
||||
onClose={() => setIsStepModalOpen(false)}
|
||||
onSubmitPrediction={handleStepPrediction}
|
||||
currentPosition={20}
|
||||
diceRoll={4}
|
||||
fieldStepValue={2}
|
||||
patternModifier={2}
|
||||
cardText="Tippeld meg, melyik pozícióra fogsz lépni a számítás alapján!"
|
||||
hints={[
|
||||
"A végső pozíció = jelenlegi pozíció + dobás + mező lépés + zóna módosító",
|
||||
"Ebben a példában: 20 + 4 + 2 + 2 = 28",
|
||||
"A zóna módosító a pozíció alapján változik!"
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default GameModalsDemo
|
||||
@@ -2,6 +2,10 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"
|
||||
import { getVerticalOffset } from "../../utils/randomUtils"
|
||||
import Dice from "../../utils/dice/Dice"
|
||||
import { useGameWebSocket } from "../../hooks/useGameWebSocket"
|
||||
import JokerApprovalModal from "./JokerApprovalModal"
|
||||
import CardDisplayModal from "./CardDisplayModal"
|
||||
import ConsequenceModal from "./ConsequenceModal"
|
||||
import StepPredictionModal from "./StepPredictionModal"
|
||||
|
||||
// Constants - outside component to prevent recreation
|
||||
const PLAYER_STYLES = [
|
||||
@@ -47,16 +51,53 @@ const GameScreen = () => {
|
||||
isConnected,
|
||||
gameState,
|
||||
players: backendPlayers,
|
||||
boardData,
|
||||
boardData: websocketBoardData,
|
||||
currentTurn,
|
||||
error,
|
||||
rollDice,
|
||||
approveJoker,
|
||||
rejectJoker,
|
||||
submitAnswer,
|
||||
submitPositionGuess,
|
||||
addEventListener,
|
||||
removeEventListener
|
||||
} = useGameWebSocket(gameToken)
|
||||
|
||||
// Try to get boardData from WebSocket, fallback to localStorage
|
||||
const boardData = useMemo(() => {
|
||||
if (websocketBoardData) return websocketBoardData
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem('boardData')
|
||||
if (stored) {
|
||||
console.log('📦 Loading boardData from localStorage')
|
||||
return JSON.parse(stored)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to parse boardData from localStorage:', err)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [websocketBoardData])
|
||||
|
||||
const [path, setPath] = useState([])
|
||||
const [players, setPlayers] = useState([])
|
||||
|
||||
// Joker approval modal state
|
||||
const [isJokerModalOpen, setIsJokerModalOpen] = useState(false)
|
||||
const [currentJokerRequest, setCurrentJokerRequest] = useState(null)
|
||||
|
||||
// Card display modal state
|
||||
const [isCardModalOpen, setIsCardModalOpen] = useState(false)
|
||||
const [currentCard, setCurrentCard] = useState(null)
|
||||
|
||||
// Consequence modal state
|
||||
const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false)
|
||||
const [currentConsequence, setCurrentConsequence] = useState(null)
|
||||
|
||||
// Step prediction modal state
|
||||
const [isPredictionModalOpen, setIsPredictionModalOpen] = useState(false)
|
||||
const [currentPredictionData, setCurrentPredictionData] = useState(null)
|
||||
|
||||
// Memoized board dimensions
|
||||
const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => {
|
||||
@@ -171,6 +212,178 @@ const GameScreen = () => {
|
||||
return () => removeEventListener('game:player-moved')
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Listen to Joker card events (csak Gamemaster számára)
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handleJokerDrawn = (jokerData) => {
|
||||
console.log('🃏 Joker kártya húzva:', jokerData)
|
||||
// Joker approval modal megjelenítése
|
||||
setCurrentJokerRequest({
|
||||
playerId: jokerData.playerId,
|
||||
playerName: jokerData.playerName,
|
||||
playerEmoji: jokerData.playerEmoji || "🎭",
|
||||
cardTitle: jokerData.cardTitle || jokerData.jokerCard?.question,
|
||||
cardDescription: jokerData.cardDescription || jokerData.jokerCard?.consequence?.description,
|
||||
points: jokerData.points || jokerData.jokerCard?.consequence?.value,
|
||||
cardId: jokerData.cardId || jokerData.jokerCard?.id,
|
||||
requestId: jokerData.requestId, // Important: requestId from backend
|
||||
timestamp: Date.now()
|
||||
})
|
||||
setIsJokerModalOpen(true)
|
||||
}
|
||||
|
||||
// Listen for gamemaster decision request (correct event name per docs)
|
||||
addEventListener('game:joker-drawn', handleJokerDrawn)
|
||||
addEventListener('game:gamemaster-decision-request', handleJokerDrawn)
|
||||
|
||||
return () => {
|
||||
removeEventListener('game:joker-drawn')
|
||||
removeEventListener('game:gamemaster-decision-request')
|
||||
}
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Listen to card drawn events (kártya megjelenítés)
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handleCardDrawn = (cardData) => {
|
||||
console.log('🎴 Kártya húzva:', cardData)
|
||||
setCurrentCard({
|
||||
id: cardData.cardId || cardData.id,
|
||||
type: cardData.cardType || cardData.type,
|
||||
question: cardData.question || cardData.text,
|
||||
answerOptions: cardData.answerOptions || cardData.options || [],
|
||||
correctAnswer: cardData.correctAnswer,
|
||||
points: cardData.points || 0,
|
||||
timeLimit: cardData.timeLimit || 60
|
||||
})
|
||||
setIsCardModalOpen(true)
|
||||
}
|
||||
|
||||
// Listen for both generic and self-specific events
|
||||
addEventListener('game:card-drawn', handleCardDrawn)
|
||||
addEventListener('game:card-drawn-self', handleCardDrawn)
|
||||
|
||||
return () => {
|
||||
removeEventListener('game:card-drawn')
|
||||
removeEventListener('game:card-drawn-self')
|
||||
}
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Listen to answer validation (következmény megjelenítés)
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handleAnswerValidated = (resultData) => {
|
||||
console.log('✅ Válasz kiértékelve:', resultData)
|
||||
|
||||
// Close card modal first
|
||||
setIsCardModalOpen(false)
|
||||
|
||||
// Show consequence modal
|
||||
setCurrentConsequence({
|
||||
isCorrect: resultData.isCorrect || resultData.correct,
|
||||
playerAnswer: resultData.playerAnswer || resultData.answer,
|
||||
correctAnswer: resultData.correctAnswer,
|
||||
explanation: resultData.explanation || '',
|
||||
consequenceType: resultData.consequenceType || resultData.consequence?.type,
|
||||
consequenceValue: resultData.consequenceValue || resultData.consequence?.value || 0,
|
||||
points: resultData.pointsEarned || resultData.points || 0
|
||||
})
|
||||
setIsConsequenceModalOpen(true)
|
||||
}
|
||||
|
||||
// Also listen for luck consequences (instant consequences from luck cards)
|
||||
const handleLuckConsequence = (luckData) => {
|
||||
console.log('🍀 Szerencse kártya következménye:', luckData)
|
||||
|
||||
setCurrentConsequence({
|
||||
isCorrect: true, // Luck cards don't have right/wrong answers
|
||||
consequenceType: luckData.consequenceType,
|
||||
consequenceValue: luckData.value || luckData.consequenceValue || 0,
|
||||
explanation: luckData.message || 'Szerencse kártya!',
|
||||
playerAnswer: null,
|
||||
correctAnswer: null
|
||||
})
|
||||
setIsConsequenceModalOpen(true)
|
||||
}
|
||||
|
||||
addEventListener('game:answer-validated', handleAnswerValidated)
|
||||
addEventListener('game:luck-consequence', handleLuckConsequence)
|
||||
|
||||
return () => {
|
||||
removeEventListener('game:answer-validated')
|
||||
removeEventListener('game:luck-consequence')
|
||||
}
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Listen to position guess requests (lépés tippelés)
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handlePositionGuessRequest = (predictionData) => {
|
||||
console.log('🎯 Pozíció tippelés kérés:', predictionData)
|
||||
setCurrentPredictionData({
|
||||
currentPosition: predictionData.currentPosition,
|
||||
diceRoll: predictionData.diceRoll || predictionData.dice,
|
||||
fieldStepValue: predictionData.fieldStepValue || predictionData.fieldStep || 0,
|
||||
patternModifier: predictionData.patternModifier || predictionData.zoneModifier || 0,
|
||||
cardText: predictionData.cardText || predictionData.text || 'Tippeld meg, hova fogsz lépni!',
|
||||
timeLimit: predictionData.timeLimit || 30
|
||||
})
|
||||
setIsPredictionModalOpen(true)
|
||||
}
|
||||
|
||||
addEventListener('game:position-guess-request', handlePositionGuessRequest)
|
||||
return () => removeEventListener('game:position-guess-request')
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Joker jóváhagyás
|
||||
const handleApproveJoker = useCallback(async (jokerRequest) => {
|
||||
console.log('✅ Joker feladat jóváhagyva:', jokerRequest)
|
||||
|
||||
// WebSocket üzenet a backend felé
|
||||
approveJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId)
|
||||
|
||||
// Modal bezárása
|
||||
setIsJokerModalOpen(false)
|
||||
}, [approveJoker])
|
||||
|
||||
// Joker elutasítás
|
||||
const handleRejectJoker = useCallback(async (jokerRequest) => {
|
||||
console.log('❌ Joker feladat elutasítva:', jokerRequest)
|
||||
|
||||
// WebSocket üzenet a backend felé
|
||||
rejectJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId)
|
||||
|
||||
// Modal bezárása
|
||||
setIsJokerModalOpen(false)
|
||||
}, [rejectJoker])
|
||||
|
||||
// Kártya válasz beküldése
|
||||
const handleSubmitAnswer = useCallback((answer) => {
|
||||
console.log('📝 Válasz beküldve:', answer)
|
||||
|
||||
// WebSocket emit a backend felé
|
||||
if (currentCard?.id) {
|
||||
submitAnswer(currentCard.id, answer)
|
||||
}
|
||||
|
||||
// A consequence modal automatikusan megnyílik a 'game:answer-validated' event hatására
|
||||
}, [currentCard?.id, submitAnswer])
|
||||
|
||||
// Pozíció tippelés beküldése
|
||||
const handleSubmitPrediction = useCallback((predictedPosition) => {
|
||||
console.log('🎯 Pozíció tippelés beküldve:', predictedPosition)
|
||||
|
||||
// WebSocket emit a backend felé
|
||||
submitPositionGuess(predictedPosition)
|
||||
|
||||
// Modal bezárása
|
||||
setIsPredictionModalOpen(false)
|
||||
}, [submitPositionGuess])
|
||||
|
||||
// Sorted players - memoized
|
||||
const sortedPlayers = useMemo(
|
||||
() => [...players].sort((a, b) => b.position - a.position),
|
||||
@@ -179,7 +392,13 @@ const GameScreen = () => {
|
||||
|
||||
// Handle dice roll
|
||||
const handleDiceRoll = useCallback((value) => {
|
||||
rollDice(value)
|
||||
console.log('🎲 Dobás:', value)
|
||||
const success = rollDice(value)
|
||||
if (success) {
|
||||
console.log('✅ Kockadobás elküldve a szervernek')
|
||||
} else {
|
||||
console.warn('⚠️ Kockadobás sikertelen - nincs kapcsolat vagy nem te következel')
|
||||
}
|
||||
}, [rollDice])
|
||||
|
||||
// Get field style - memoized
|
||||
@@ -229,18 +448,16 @@ const GameScreen = () => {
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
isConnected ? 'bg-green-300 animate-pulse' : 'bg-red-300'
|
||||
}`}></div>
|
||||
<span className="text-sm font-medium">
|
||||
{isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'}
|
||||
</span>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="mt-2 px-4 py-2 rounded-lg shadow-lg bg-red-600 text-white text-xs">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-sm font-medium">
|
||||
{isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Game Info Bar */}
|
||||
{error && !error.includes('Game not found') && !error.includes('token invalid') && (
|
||||
<div className="mt-2 px-4 py-2 rounded-lg shadow-lg bg-red-600 text-white text-xs">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
)}
|
||||
</div> {/* Game Info Bar */}
|
||||
{gameState && (
|
||||
<div className="fixed top-4 left-4 z-50">
|
||||
<div className="bg-gray-800 border border-teal-700 px-4 py-2 rounded-lg shadow-lg">
|
||||
@@ -416,6 +633,40 @@ const GameScreen = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Joker Approval Modal - csak Gamemaster számára */}
|
||||
<JokerApprovalModal
|
||||
isOpen={isJokerModalOpen}
|
||||
onClose={() => setIsJokerModalOpen(false)}
|
||||
jokerRequest={currentJokerRequest}
|
||||
onApprove={handleApproveJoker}
|
||||
onReject={handleRejectJoker}
|
||||
playerName={currentJokerRequest?.playerName}
|
||||
playerEmoji={currentJokerRequest?.playerEmoji}
|
||||
/>
|
||||
|
||||
{/* Card Display Modal - kártya megjelenítés */}
|
||||
<CardDisplayModal
|
||||
isOpen={isCardModalOpen}
|
||||
onClose={() => setIsCardModalOpen(false)}
|
||||
card={currentCard}
|
||||
onSubmitAnswer={handleSubmitAnswer}
|
||||
/>
|
||||
|
||||
{/* Consequence Modal - következmények megjelenítése */}
|
||||
<ConsequenceModal
|
||||
isOpen={isConsequenceModalOpen}
|
||||
onClose={() => setIsConsequenceModalOpen(false)}
|
||||
consequence={currentConsequence}
|
||||
/>
|
||||
|
||||
{/* Step Prediction Modal - pozíció tippelés */}
|
||||
<StepPredictionModal
|
||||
isOpen={isPredictionModalOpen}
|
||||
onClose={() => setIsPredictionModalOpen(false)}
|
||||
predictionData={currentPredictionData}
|
||||
onSubmitPrediction={handleSubmitPrediction}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
import React, { useState } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
/**
|
||||
* JokerApprovalModal - Gamemaster felület a Joker kártya feladatok jóváhagyására
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - Modal megjelenítése
|
||||
* @param {Function} props.onClose - Modal bezárása
|
||||
* @param {Object} props.jokerRequest - Joker kártya adatok
|
||||
* @param {Function} props.onApprove - Jóváhagyás callback
|
||||
* @param {Function} props.onReject - Elutasítás callback
|
||||
* @param {string} props.playerName - Játékos neve
|
||||
* @param {string} props.playerEmoji - Játékos emoji
|
||||
*/
|
||||
const JokerApprovalModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
jokerRequest,
|
||||
onApprove,
|
||||
onReject,
|
||||
playerName,
|
||||
playerEmoji = "🎭"
|
||||
}) => {
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
||||
const handleApprove = async () => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await onApprove(jokerRequest)
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error("Jóváhagyási hiba:", error)
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await onReject(jokerRequest)
|
||||
onClose()
|
||||
} catch (error) {
|
||||
console.error("Elutasítási hiba:", error)
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.5 }}
|
||||
className="relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 border-purple-500/30 max-w-2xl w-full overflow-hidden"
|
||||
>
|
||||
{/* Header with Joker theme */}
|
||||
<div className="bg-gradient-to-r from-purple-600 via-pink-600 to-purple-600 p-6 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
|
||||
<div className="relative flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-5xl animate-bounce">🃏</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Joker Kártya Feladat</h2>
|
||||
<p className="text-purple-100 text-sm">Gamemaster jóváhagyás szükséges</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/80 hover:text-white transition-colors text-2xl"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Player Info */}
|
||||
<div className="bg-gray-800/50 rounded-xl p-4 border border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-4xl">{playerEmoji}</div>
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">Játékos</p>
|
||||
<p className="text-white font-semibold text-lg">{playerName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Joker Card Details */}
|
||||
<div className="bg-gradient-to-br from-purple-900/30 to-pink-900/30 rounded-xl p-5 border border-purple-500/30">
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div className="text-3xl">🎯</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-purple-300 font-semibold mb-2">Feladat címe</h3>
|
||||
<p className="text-white text-lg font-medium">
|
||||
{jokerRequest?.cardTitle || "Joker Kártya Feladat"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-2xl">📝</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-purple-300 font-semibold mb-2">Feladat leírása</h3>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
{jokerRequest?.cardDescription || "A játékosnak teljesítenie kell a Joker kártya feladatát."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Points Info */}
|
||||
{jokerRequest?.points && (
|
||||
<div className="mt-4 pt-4 border-t border-purple-500/20">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">⭐</span>
|
||||
<span className="text-yellow-400 font-bold text-lg">
|
||||
{jokerRequest.points} pont
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm">járható érte</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Player's Claim (Optional - ha később hozzáadod) */}
|
||||
{jokerRequest?.playerMessage && (
|
||||
<div className="bg-blue-900/20 rounded-xl p-4 border border-blue-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-2xl">💬</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-blue-300 font-semibold mb-2">Játékos üzenete</h3>
|
||||
<p className="text-gray-300 italic">"{jokerRequest.playerMessage}"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="bg-yellow-900/20 rounded-xl p-4 border border-yellow-500/30">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="text-2xl">ℹ️</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-yellow-200 text-sm">
|
||||
<strong>Gamemaster döntés:</strong> Nézd meg, hogy a játékos teljesítette-e a feladatot,
|
||||
majd hagyd jóvá vagy utasítsd el.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-4 pt-2">
|
||||
<button
|
||||
onClick={handleReject}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600
|
||||
text-white font-bold py-4 px-6 rounded-xl shadow-lg
|
||||
transform transition-all duration-200 hover:scale-105 active:scale-95
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||||
border border-red-500/50"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-2xl">❌</span>
|
||||
<span className="text-lg">Elutasítás</span>
|
||||
</div>
|
||||
<div className="text-xs text-red-200 mt-1">Nem teljesítette</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleApprove}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-500 hover:to-green-600
|
||||
text-white font-bold py-4 px-6 rounded-xl shadow-lg
|
||||
transform transition-all duration-200 hover:scale-105 active:scale-95
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||||
border border-green-500/50"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-2xl">✅</span>
|
||||
<span className="text-lg">Jóváhagyás</span>
|
||||
</div>
|
||||
<div className="text-xs text-green-200 mt-1">Sikeresen teljesítette</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Processing indicator */}
|
||||
{isProcessing && (
|
||||
<div className="text-center py-2">
|
||||
<div className="inline-flex items-center gap-2 text-purple-400">
|
||||
<div className="animate-spin text-2xl">⚙️</div>
|
||||
<span>Feldolgozás...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default JokerApprovalModal
|
||||
@@ -9,6 +9,7 @@ import { startGame } from "../../api/gameApi.js"
|
||||
|
||||
const Lobby = () => {
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [isStarting, setIsStarting] = useState(false)
|
||||
const sectionRef = useRef(null)
|
||||
const { goHome, goGame } = HandleNavigate()
|
||||
const location = useLocation()
|
||||
@@ -25,19 +26,29 @@ const Lobby = () => {
|
||||
players,
|
||||
isGamemaster,
|
||||
gameStarted,
|
||||
pendingPlayers,
|
||||
approvalStatus,
|
||||
approvePlayer,
|
||||
rejectPlayer,
|
||||
} = useGameWebSocket(gameToken)
|
||||
|
||||
const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...'
|
||||
|
||||
// Filter out gamemaster from player list - gamemaster is NOT a player
|
||||
const currentPlayers = (players || []).filter(p => {
|
||||
// If we have userId info, filter by that
|
||||
if (p.userId) {
|
||||
return p.userId !== gameState?.createdBy
|
||||
// Players list - gamemaster is separate, don't filter
|
||||
// Backend should handle this correctly
|
||||
const currentPlayers = players || []
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('🎮 Lobby state update:')
|
||||
console.log(' - isGamemaster:', isGamemaster)
|
||||
console.log(' - gameState:', gameState)
|
||||
console.log(' - players:', players)
|
||||
console.log(' - currentPlayers:', currentPlayers)
|
||||
console.log(' - pendingPlayers:', pendingPlayers)
|
||||
}
|
||||
// Otherwise filter by name (less reliable but works for now)
|
||||
return true
|
||||
})
|
||||
}, [isGamemaster, gameState, players, currentPlayers, pendingPlayers])
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
@@ -56,7 +67,18 @@ const Lobby = () => {
|
||||
console.log('🎮 Game started, navigating to /game')
|
||||
goGame()
|
||||
}
|
||||
}, [gameStarted, goGame])
|
||||
}, [gameStarted, navigate])
|
||||
|
||||
// Handle approval status changes
|
||||
useEffect(() => {
|
||||
if (approvalStatus === 'denied') {
|
||||
alert('A gamemaster elutasította a csatlakozási kérelmedet.')
|
||||
localStorage.removeItem('gameToken')
|
||||
navigate("/home")
|
||||
} else if (approvalStatus === 'approved') {
|
||||
console.log('✅ Join approved, you can now see the lobby')
|
||||
}
|
||||
}, [approvalStatus, navigate])
|
||||
|
||||
const handleExit = () => {
|
||||
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
|
||||
@@ -66,7 +88,15 @@ const Lobby = () => {
|
||||
}
|
||||
|
||||
const handleStartGame = async () => {
|
||||
// Prevent double-click
|
||||
if (isStarting) {
|
||||
console.log('⚠️ Game start already in progress, ignoring duplicate request')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsStarting(true)
|
||||
|
||||
// Get gameId from gameState
|
||||
const gameId = gameState?.gameId
|
||||
if (!gameId) {
|
||||
@@ -78,12 +108,29 @@ const Lobby = () => {
|
||||
const response = await startGame(gameId)
|
||||
console.log('Game start response:', response)
|
||||
|
||||
// Backend will broadcast game:started event to all players
|
||||
// Navigate to game page
|
||||
goGame()
|
||||
// Store boardData and updated game state for GameScreen
|
||||
if (response.boardData) {
|
||||
localStorage.setItem('boardData', JSON.stringify(response.boardData))
|
||||
console.log('✅ boardData stored in localStorage')
|
||||
}
|
||||
|
||||
// Navigate immediately after successful start (don't wait for WebSocket)
|
||||
console.log('🎮 Navigating to /game...')
|
||||
navigate('/game')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to start game:', error)
|
||||
alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
|
||||
|
||||
// Check if game already started
|
||||
if (error.response?.status === 409) {
|
||||
console.log('Game already started, navigating to /game...')
|
||||
// Navigate anyway - game is already running
|
||||
navigate('/game')
|
||||
} else {
|
||||
alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
|
||||
}
|
||||
} finally {
|
||||
setIsStarting(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +139,21 @@ const Lobby = () => {
|
||||
alert('Játék kód vágólapra másolva: ' + gameCode)
|
||||
}
|
||||
|
||||
const handleApprovePlayer = (playerName) => {
|
||||
if (approvePlayer(playerName)) {
|
||||
console.log(`✅ Player ${playerName} approved`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRejectPlayer = (playerName) => {
|
||||
const reason = prompt(`Miért utasítod el ${playerName}-t?`, 'Nincs hely a játékban')
|
||||
if (reason !== null) { // User didn't cancel
|
||||
if (rejectPlayer(playerName, reason)) {
|
||||
console.log(`❌ Player ${playerName} rejected`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getInitials = (name) => {
|
||||
return name
|
||||
.split(" ")
|
||||
@@ -111,7 +173,47 @@ const Lobby = () => {
|
||||
<Navbar />
|
||||
</div>
|
||||
|
||||
<main className="flex-grow text-white px-6 pt-16 mt-0 mb-20 flex items-center justify-center">
|
||||
{/* Waiting for Approval Screen (Non-gamemaster, PRIVATE games) */}
|
||||
{!isGamemaster && approvalStatus === 'pending' && (
|
||||
<div className="flex-1 flex items-center justify-center px-4 py-24">
|
||||
<div className="bg-zinc-900/95 backdrop-blur-sm rounded-3xl p-8 max-w-md w-full border border-yellow-500/50 shadow-2xl">
|
||||
<div className="text-center">
|
||||
<div className="mb-6">
|
||||
<div className="inline-flex items-center justify-center w-20 h-20 rounded-full bg-yellow-900/30 border-4 border-yellow-500/50 animate-pulse">
|
||||
<span className="text-4xl">⏳</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-yellow-300 mb-4">
|
||||
Várakozás jóváhagyásra
|
||||
</h2>
|
||||
<p className="text-zinc-300 text-lg mb-6">
|
||||
A gamemaster még nem hagyta jóvá a csatlakozásodat.
|
||||
</p>
|
||||
<p className="text-zinc-400 text-sm mb-8">
|
||||
Kérjük, várj türelemmel, amíg a gamemaster elfogadja a kérelmedet.
|
||||
</p>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="bg-zinc-800 rounded-lg p-4 border border-zinc-700">
|
||||
<p className="text-zinc-400 text-xs mb-1">Játék kód:</p>
|
||||
<p className="text-2xl font-mono font-bold text-green-300 tracking-widest">
|
||||
{gameCode}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleExit}
|
||||
className="bg-red-600 hover:bg-red-500 text-white px-6 py-3 rounded-lg font-semibold transition-all duration-200 hover:scale-105"
|
||||
>
|
||||
Mégse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Normal Lobby View (Gamemaster or approved players) */}
|
||||
{(isGamemaster || approvalStatus !== 'pending') && (
|
||||
<div className="flex-1 flex items-center justify-center px-4 py-24">
|
||||
<section
|
||||
ref={sectionRef}
|
||||
className={`w-full max-w-3xl rounded-2xl p-8 md:p-10 transition-all duration-1000 ease-out backdrop-blur-md shadow-2xl ${
|
||||
@@ -160,6 +262,58 @@ const Lobby = () => {
|
||||
Játékosok ({currentPlayers.length}):
|
||||
</p>
|
||||
|
||||
{/* Pending Players Section (Gamemaster only, PRIVATE games) */}
|
||||
{isGamemaster && pendingPlayers && pendingPlayers.length > 0 && (
|
||||
<div className="bg-yellow-900/20 border-2 border-yellow-500/50 rounded-xl shadow-lg p-6 mb-6">
|
||||
<h3 className="text-xl font-bold text-yellow-300 mb-4 flex items-center gap-2">
|
||||
<span>⏳</span>
|
||||
Jóváhagyásra váró játékosok ({pendingPlayers.length})
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-3">
|
||||
{pendingPlayers.map((player, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="bg-zinc-700 py-3 px-4 rounded-xl flex items-center gap-4"
|
||||
>
|
||||
<div
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold bg-yellow-900/30 text-yellow-300 border border-yellow-500/50"
|
||||
>
|
||||
{getInitials(player.playerName)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<span className="text-white text-lg font-semibold">
|
||||
{player.playerName}
|
||||
</span>
|
||||
{player.isAuthenticated && (
|
||||
<span className="ml-2 text-xs bg-green-600/30 text-green-300 px-2 py-0.5 rounded-full border border-green-500/50">
|
||||
✓ Bejelentkezve
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleApprovePlayer(player.playerName)}
|
||||
className="bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded-lg font-semibold transition-all duration-200 hover:scale-105 flex items-center gap-1"
|
||||
title="Jóváhagyás"
|
||||
>
|
||||
<span>✓</span>
|
||||
<span className="hidden sm:inline">Jóváhagy</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRejectPlayer(player.playerName)}
|
||||
className="bg-red-600 hover:bg-red-500 text-white px-4 py-2 rounded-lg font-semibold transition-all duration-200 hover:scale-105 flex items-center gap-1"
|
||||
title="Elutasítás"
|
||||
>
|
||||
<span>✕</span>
|
||||
<span className="hidden sm:inline">Elutasít</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-zinc-800/90 rounded-xl shadow-lg p-6 mb-8">
|
||||
<ul className="flex flex-col gap-4">
|
||||
{currentPlayers.length === 0 ? (
|
||||
@@ -215,15 +369,21 @@ const Lobby = () => {
|
||||
/* Gamemaster view - can start game */
|
||||
<button
|
||||
onClick={handleStartGame}
|
||||
disabled={currentPlayers.length < 2}
|
||||
disabled={currentPlayers.length < 2 || isStarting}
|
||||
className={`px-8 py-3 rounded-xl font-semibold shadow-lg transition-transform transform hover:scale-105 ${
|
||||
currentPlayers.length >= 2
|
||||
currentPlayers.length >= 2 && !isStarting
|
||||
? 'bg-gradient-to-r from-green-700 to-green-500 hover:from-green-600 hover:to-green-400 text-white hover:shadow-green-400/30'
|
||||
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
title={currentPlayers.length < 2 ? 'Minimum 2 játékos szükséges' : 'Játék indítása'}
|
||||
title={
|
||||
isStarting
|
||||
? 'Játék indítása folyamatban...'
|
||||
: currentPlayers.length < 2
|
||||
? 'Minimum 2 játékos szükséges'
|
||||
: 'Játék indítása'
|
||||
}
|
||||
>
|
||||
Játék Indítása
|
||||
{isStarting ? '⏳ Indítás...' : 'Játék Indítása'}
|
||||
</button>
|
||||
) : (
|
||||
/* Player view - cannot start game, just wait */
|
||||
@@ -240,7 +400,8 @@ const Lobby = () => {
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
/**
|
||||
* StepPredictionModal - Pozíció tippelés (Position Guessing)
|
||||
*
|
||||
* A dokumentáció szerint: A játékosnak meg kell tippelnie a VÉGLEGES POZÍCIÓT,
|
||||
* nem a lépésszámot!
|
||||
*
|
||||
* Számítás: finalPosition = currentPosition + diceRoll + fieldStepValue + patternModifier
|
||||
*
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.isOpen - Modal megjelenítése
|
||||
* @param {Function} props.onClose - Modal bezárása
|
||||
* @param {Function} props.onSubmitPrediction - Tipp beküldése
|
||||
* @param {number} props.currentPosition - Jelenlegi pozíció
|
||||
* @param {number} props.diceRoll - Dobás értéke
|
||||
* @param {number} props.fieldStepValue - Mező lépés értéke
|
||||
* @param {number} props.patternModifier - Zóna alapú módosító
|
||||
* @param {string} props.cardText - Kártya szövege
|
||||
* @param {Array} props.hints - Segédletek tömbje
|
||||
* @param {number} props.timeLimit - Időkorlát másodpercben (default: 30)
|
||||
*/
|
||||
const StepPredictionModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmitPrediction,
|
||||
currentPosition = 0,
|
||||
diceRoll = 0,
|
||||
fieldStepValue = 0,
|
||||
patternModifier = 0,
|
||||
cardText = "Tippeld meg, melyik pozícióra fogsz lépni!",
|
||||
hints = [],
|
||||
timeLimit = 30
|
||||
}) => {
|
||||
const [prediction, setPrediction] = useState("")
|
||||
const [showHints, setShowHints] = useState(false)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [timeLeft, setTimeLeft] = useState(timeLimit)
|
||||
|
||||
// Timer countdown
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
setTimeLeft(timeLimit)
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(timer)
|
||||
handleTimeout()
|
||||
return 0
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [isOpen, timeLimit])
|
||||
|
||||
const handleTimeout = () => {
|
||||
if (onSubmitPrediction) {
|
||||
onSubmitPrediction(null) // null = timeout
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!prediction || prediction === "" || isProcessing) return
|
||||
|
||||
const guessedPosition = parseInt(prediction)
|
||||
if (isNaN(guessedPosition)) return
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
await onSubmitPrediction(guessedPosition)
|
||||
} catch (error) {
|
||||
console.error("Tipp küldési hiba:", error)
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset amikor megnyílik
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setPrediction("")
|
||||
setShowHints(false)
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
// Számított végső pozíció (helyes válasz)
|
||||
const calculatedPosition = currentPosition + diceRoll + fieldStepValue + patternModifier
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
return `0:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const getTimeColor = () => {
|
||||
if (timeLeft > 15) return "text-green-400"
|
||||
if (timeLeft > 5) return "text-yellow-400"
|
||||
return "text-red-400 animate-pulse"
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<motion.div
|
||||
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
||||
transition={{ type: "spring", duration: 0.5 }}
|
||||
className="relative bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 rounded-2xl shadow-2xl border-2 border-yellow-500/30 max-w-xl w-full overflow-hidden max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-yellow-600 via-orange-600 to-yellow-600 p-4 relative overflow-hidden sticky top-0 z-10">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
|
||||
<div className="relative flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-3xl animate-bounce">🎯</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Pozíció Tippelés</h2>
|
||||
<p className="text-white/80 text-xs">Melyik pozícióra lépsz?</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timer */}
|
||||
<div className="bg-black/30 rounded-lg px-3 py-1">
|
||||
<div className={`text-lg font-bold ${getTimeColor()}`}>
|
||||
⏱️ {formatTime(timeLeft)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Card Text / Instructions */}
|
||||
<div className="bg-gray-800/50 rounded-xl p-3 border border-gray-700">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="text-2xl">📝</div>
|
||||
<div className="flex-1">
|
||||
<p className="text-white text-sm leading-relaxed">
|
||||
{cardText}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calculation Info */}
|
||||
<div className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 rounded-xl p-3 border border-blue-500/30">
|
||||
<h3 className="text-blue-300 font-semibold mb-2 text-center text-sm">📊 Számítási Adatok</h3>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div className="bg-black/30 rounded-lg p-2">
|
||||
<p className="text-gray-400 text-xs mb-1">Jelenlegi pozíció</p>
|
||||
<p className="text-white font-bold text-lg">{currentPosition}</p>
|
||||
</div>
|
||||
<div className="bg-black/30 rounded-lg p-2">
|
||||
<p className="text-gray-400 text-xs mb-1">Dobás (kocka)</p>
|
||||
<p className="text-white font-bold text-lg">+{diceRoll}</p>
|
||||
</div>
|
||||
<div className="bg-black/30 rounded-lg p-2">
|
||||
<p className="text-gray-400 text-xs mb-1">Mező lépés</p>
|
||||
<p className="text-white font-bold text-lg">+{fieldStepValue}</p>
|
||||
</div>
|
||||
<div className="bg-black/30 rounded-lg p-2">
|
||||
<p className="text-gray-400 text-xs mb-1">Zóna módosító</p>
|
||||
<p className={`font-bold text-lg ${patternModifier >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{patternModifier >= 0 ? '+' : ''}{patternModifier}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 bg-yellow-900/30 rounded-lg p-2 border border-yellow-500/30">
|
||||
<p className="text-yellow-300 text-center text-xs">
|
||||
<span className="font-semibold">Számítsd ki:</span> {currentPosition} + {diceRoll} + {fieldStepValue} {patternModifier >= 0 ? '+' : ''} {patternModifier} = ?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Position Input */}
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-yellow-300 font-semibold text-center text-sm">
|
||||
Írd be a tippelt pozíciót:
|
||||
</h3>
|
||||
<input
|
||||
type="number"
|
||||
value={prediction}
|
||||
onChange={(e) => setPrediction(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSubmit()}
|
||||
disabled={isProcessing}
|
||||
placeholder="Pl: 28"
|
||||
className="w-full bg-gray-800 border-2 border-yellow-600 rounded-xl px-4 py-3 text-white text-center text-2xl font-bold focus:border-yellow-400 focus:outline-none disabled:opacity-50"
|
||||
min={currentPosition}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Prediction Info */}
|
||||
{prediction && prediction !== "" && (
|
||||
<motion.div
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="bg-yellow-900/20 rounded-xl p-2 border border-yellow-500/30 text-center"
|
||||
>
|
||||
<p className="text-yellow-300 text-sm">
|
||||
A tipped: <span className="font-bold text-xl text-white">{prediction}</span> pozíció
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Hints Section */}
|
||||
{hints && hints.length > 0 && (
|
||||
<div className="bg-blue-900/20 rounded-xl p-4 border border-blue-500/30">
|
||||
<button
|
||||
onClick={() => setShowHints(!showHints)}
|
||||
className="w-full flex items-center justify-between text-blue-300 hover:text-blue-200 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">💡</span>
|
||||
<span className="font-semibold">Segédlet</span>
|
||||
</div>
|
||||
<span className="text-xl">{showHints ? "▼" : "▶"}</span>
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showHints && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="mt-3 space-y-2"
|
||||
>
|
||||
{hints.map((hint, index) => (
|
||||
<div key={index} className="bg-blue-900/30 rounded-lg p-3">
|
||||
<p className="text-gray-300 text-sm">
|
||||
<span className="font-bold text-blue-300">#{index + 1}:</span> {hint}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Risk/Reward Info */}
|
||||
<div className="bg-gradient-to-br from-green-900/20 to-red-900/20 rounded-xl p-4 border border-gray-600">
|
||||
<div className="grid grid-cols-2 gap-4 text-center">
|
||||
<div>
|
||||
<div className="text-3xl mb-2">✅</div>
|
||||
<p className="text-green-300 font-semibold text-sm">Ha eltalálod</p>
|
||||
<p className="text-white text-xs">Lépsz az új pozícióra!</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-3xl mb-2">❌</div>
|
||||
<p className="text-red-300 font-semibold text-sm">Ha nem találod el</p>
|
||||
<p className="text-white text-xs">-2 büntetés + nem lépsz!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!prediction || prediction === "" || isProcessing}
|
||||
className="w-full bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-500 hover:to-orange-500
|
||||
text-white font-bold py-4 px-6 rounded-xl shadow-lg
|
||||
transform transition-all duration-200 hover:scale-105 active:scale-95
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100
|
||||
border border-yellow-500/50"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-2xl">🎲</span>
|
||||
<span className="text-lg">
|
||||
{isProcessing ? "Feldolgozás..." : "Tipp beküldése"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Warning */}
|
||||
{(!prediction || prediction === "") && (
|
||||
<p className="text-center text-gray-400 text-sm">
|
||||
⚠️ Írd be a tippelt pozíciót a beküldéshez!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
export default StepPredictionModal
|
||||
Reference in New Issue
Block a user