task/134-frontend-check #100

Merged
Donat merged 6 commits from task/134-frontend-check into main 2025-11-17 19:40:56 +01:00
10 changed files with 2007 additions and 72 deletions
Showing only changes of commit 70cc18a58d - Show all commits
@@ -197,9 +197,8 @@ export class GameWebSocketService {
private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise<void> { private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise<void> {
try { try {
// Simple data extraction - let Socket.IO handle the parsing // Socket.IO automatically deserializes JSON - data is already an object
const jsdata = JSON.parse(data); const gameToken = data?.gameToken;
const gameToken = jsdata?.gameToken;
if (!gameToken) { if (!gameToken) {
logError('Game join failed: No game token provided'); logError('Game join failed: No game token provided');
@@ -243,6 +242,12 @@ export class GameWebSocketService {
const isGamemaster = game.createdby === userId; const isGamemaster = game.createdby === userId;
const needsApproval = game.logintype === LoginType.PRIVATE && !isGamemaster; 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) // Generate dynamic room names (needed for both approval and direct join)
const gameRoomName = `game_${gameCode}`; const gameRoomName = `game_${gameCode}`;
const playerRoomName = `game_${gameCode}:${playerName}`; const playerRoomName = `game_${gameCode}:${playerName}`;
@@ -275,6 +280,8 @@ export class GameWebSocketService {
await socket.join(gameRoomName); await socket.join(gameRoomName);
await socket.join(playerRoomName); 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 // Send success response to the joining player
socket.emit('game:joined', { 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); const gameState = await this.getGameState(gameCode);
socket.emit('game:state', gameState); socket.emit('game:state', gameState);
// Broadcast updated game state to all other players so they see the new player
// Update Redis with active player connection socket.to(gameRoomName).emit('game:state-update', gameState);
await this.updatePlayerConnection(gameCode, playerName, true);
} catch (error) { } catch (error) {
socket.emit('game:error', { socket.emit('game:error', {
@@ -314,7 +320,7 @@ export class GameWebSocketService {
private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise<void> { private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise<void> {
try { try {
const { gameCode } = JSON.parse(data as any); const { gameCode } = data;
const playerName = socket.playerName; const playerName = socket.playerName;
// Validate we have the required data // Validate we have the required data
@@ -354,7 +360,7 @@ export class GameWebSocketService {
private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise<void> { private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise<void> {
try { try {
const { gameCode, action, data: actionData } = JSON.parse(data as any); const { gameCode, action, data: actionData } = data;
if (!socket.gameCode || socket.gameCode !== gameCode) { if (!socket.gameCode || socket.gameCode !== gameCode) {
socket.emit('game:error', { message: 'You must be in the game to perform actions' }); 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> { private async handleGameChat(socket: AuthenticatedSocket, data: GameChatData): Promise<void> {
try { try {
const { gameCode, message } = JSON.parse(data as any); const { gameCode, message } = data;
if (!socket.gameCode || socket.gameCode !== gameCode) { if (!socket.gameCode || socket.gameCode !== gameCode) {
socket.emit('game:error', { message: 'You must be in the game to chat' }); 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> { private async handlePlayerReady(socket: AuthenticatedSocket, data: { gameCode: string; ready: boolean }): Promise<void> {
try { try {
const { gameCode, ready } = JSON.parse(data as any); const { gameCode, ready } = data;
const gameRoomName = `game_${gameCode}`; const gameRoomName = `game_${gameCode}`;
// Update player ready status in Redis // 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> { private async handleApprovePlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string }): Promise<void> {
try { try {
const { gameCode, playerName } = JSON.parse(data as any); const { gameCode, playerName } = data;
// Verify that the requesting socket is the gamemaster // Verify that the requesting socket is the gamemaster
const game = await this.gameRepository.findByGameCode(gameCode); 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> { private async handleRejectPlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string; reason?: string }): Promise<void> {
try { try {
const { gameCode, playerName, reason } = JSON.parse(data as any); const { gameCode, playerName, reason } = data;
// Verify that the requesting socket is the gamemaster // Verify that the requesting socket is the gamemaster
const game = await this.gameRepository.findByGameCode(gameCode); const game = await this.gameRepository.findByGameCode(gameCode);
@@ -561,7 +567,7 @@ export class GameWebSocketService {
private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise<void> { private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise<void> {
try { try {
const { gameToken } = JSON.parse(data as any); const { gameToken } = data;
if (!gameToken) { if (!gameToken) {
socket.emit('game:error', { message: 'Game token is required' }); socket.emit('game:error', { message: 'Game token is required' });
@@ -606,6 +612,9 @@ export class GameWebSocketService {
logOther(`Approved player ${playerName} joined game room: ${gameRoomName}`); 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 // Send success response to the joining player
socket.emit('game:joined', { socket.emit('game:joined', {
gameCode, gameCode,
@@ -624,12 +633,12 @@ export class GameWebSocketService {
timestamp: new Date().toISOString() 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); const gameState = await this.getGameState(gameCode);
socket.emit('game:state', gameState); socket.emit('game:state', gameState);
// Update Redis with active player connection // Broadcast updated game state to all other players so they see the new player
await this.updatePlayerConnection(gameCode, playerName, true); socket.to(gameRoomName).emit('game:state-update', gameState);
} catch (error) { } catch (error) {
logError('Error handling approved join', error as 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> { private async handleDiceRoll(socket: AuthenticatedSocket, data: DiceRollData): Promise<void> {
try { try {
const { gameCode, diceValue } = JSON.parse(data as any); const { gameCode, diceValue } = data;
// Validate input // Validate input
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
@@ -738,7 +747,7 @@ export class GameWebSocketService {
private async handleCardAnswer(socket: AuthenticatedSocket, data: CardAnswerData): Promise<void> { private async handleCardAnswer(socket: AuthenticatedSocket, data: CardAnswerData): Promise<void> {
try { try {
const { gameCode, answer } = JSON.parse(data as any); const { gameCode, answer } = data;
// Validate input // Validate input
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
@@ -848,7 +857,7 @@ export class GameWebSocketService {
private async handleGamemasterDecision(socket: AuthenticatedSocket, data: GamemasterDecisionData): Promise<void> { private async handleGamemasterDecision(socket: AuthenticatedSocket, data: GamemasterDecisionData): Promise<void> {
try { try {
const { gameCode, requestId, decision } = JSON.parse(data as any); const { gameCode, requestId, decision } = data;
// Validate input // Validate input
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) { if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
+1
View File
@@ -23,6 +23,7 @@ import { ToastConfig } from "./components/Toastify/toastifyServices" // ✅ font
import VerifyEmailPage from "./pages/Auth/VerifyEmailPage" import VerifyEmailPage from "./pages/Auth/VerifyEmailPage"
import ChooseDeck from "./pages/Game/ChooseDeck" import ChooseDeck from "./pages/Game/ChooseDeck"
import PlayerSetup from "./pages/Game/PlayerSetup" import PlayerSetup from "./pages/Game/PlayerSetup"
import GameModalsDemo from "./pages/Game/GameModalsDemo"
function App() { function App() {
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
@@ -5,7 +5,7 @@ import { API_CONFIG } from '../api/userApi';
const isDev = import.meta.env.DEV; const isDev = import.meta.env.DEV;
const log = (...args) => isDev && console.log(...args); const log = (...args) => isDev && console.log(...args);
const warn = (...args) => isDev && console.warn(...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 * Optimized WebSocket hook for game connection
@@ -20,29 +20,53 @@ export const useGameWebSocket = (gameToken) => {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [isGamemaster, setIsGamemaster] = useState(false); const [isGamemaster, setIsGamemaster] = useState(false);
const [gameStarted, setGameStarted] = 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()); const eventListenersRef = useRef(new Map());
// Memoized derived values - no extra state needed // Memoized derived values - no extra state needed
const players = useMemo(() => { const players = useMemo(() => {
// Backend sends different player fields depending on game state // Backend sends different player fields depending on game state
// connectedPlayers: array of player names (strings) who are connected via WebSocket // 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 connectedPlayers = gameState?.connectedPlayers || [];
const gamePlayers = gameState?.players || [];
const currentPlayers = gameState?.currentPlayers || []; const currentPlayers = gameState?.currentPlayers || [];
// If we have full player objects, use those // Debug: log what we received
if (currentPlayers.length > 0) return currentPlayers; if (import.meta.env.DEV) {
if (gamePlayers.length > 0) return gamePlayers; console.log('🎮 Computing players list:');
console.log(' - connectedPlayers:', connectedPlayers);
console.log(' - currentPlayers:', currentPlayers);
}
// Otherwise, map connected player names to basic player objects // If we have full player objects with positions, use those (during game)
return connectedPlayers.map((name, index) => ({ 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}`, id: `player-${index}`,
name: typeof name === 'string' ? name : name.playerName || `Player ${index + 1}`, name: playerName,
isOnline: true, isOnline: true,
isReady: gameState?.readyPlayers?.includes(name) || false, isReady: gameState?.readyPlayers?.includes(playerName) || false,
})); };
}, [gameState?.connectedPlayers, gameState?.players, gameState?.currentPlayers, gameState?.readyPlayers]); });
}
console.log('⚠️ No players found');
return [];
}, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]);
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]); const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
// Connect to game WebSocket - only once per token // Connect to game WebSocket - only once per token
@@ -83,44 +107,92 @@ export const useGameWebSocket = (gameToken) => {
// Game state handlers - batch updates // Game state handlers - batch updates
const handleGameState = (state) => { 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); setGameState(state);
}; };
const handleGameJoined = (data) => { const handleGameJoined = (data) => {
log('✅ Joined game:', 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 // Store if this user is the gamemaster
if (data.isGamemaster !== undefined) { if (data.isGamemaster !== undefined) {
log('✅ Setting isGamemaster from joined event:', data.isGamemaster);
setIsGamemaster(data.isGamemaster); setIsGamemaster(data.isGamemaster);
} else {
log('⚠️ No isGamemaster flag in joined event');
} }
// Backend will send game:state next // Backend will send game:state next
}; };
const handlePlayerJoined = (data) => { const handlePlayerJoined = (data) => {
log('👤 Player joined:', data.playerName); 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 // Update game state to add the new player to connectedPlayers
setGameState(prev => { setGameState(prev => {
if (!prev) return prev; if (!prev) {
log('⚠️ No previous game state, cannot add player');
return prev;
}
const currentConnected = prev.connectedPlayers || []; const currentConnected = prev.connectedPlayers || [];
// Only add if not already in the list // Only add if not already in the list
if (!currentConnected.includes(data.playerName)) { if (!currentConnected.includes(data.playerName)) {
log('✅ Adding player to connectedPlayers:', data.playerName);
log(' - Current list:', currentConnected);
log(' - New list:', [...currentConnected, data.playerName]);
return { return {
...prev, ...prev,
connectedPlayers: [...currentConnected, data.playerName] connectedPlayers: [...currentConnected, data.playerName]
}; };
} }
log('⚠️ Player already in connectedPlayers:', data.playerName);
return prev; return prev;
}); });
}; };
const handleGameStarted = (data) => { const handleGameStarted = (data) => {
log('🎮 Game started:', data); log('🎮 Game started:', data);
// Batch state updates
if (data.boardData) setBoardData(data.boardData); // EXTRA DEBUG
if (data.gameState) setGameState(data.gameState); if (import.meta.env.DEV) {
console.log('🔍 GAME STARTED EVENT:', JSON.stringify(data, null, 2));
}
// Signal that game has started // Signal that game has started
setGameStarted(true); 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) => { const handlePlayerMoved = (moveData) => {
@@ -145,10 +217,68 @@ export const useGameWebSocket = (gameToken) => {
}; };
const handleError = (err) => { const handleError = (err) => {
error('❌ Game error:', err); logError('❌ Game error:', err);
setError(err.message); 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 // Register all handlers
socket.on('connect', handleConnect); socket.on('connect', handleConnect);
socket.on('connect_error', handleConnectError); socket.on('connect_error', handleConnectError);
@@ -162,6 +292,13 @@ export const useGameWebSocket = (gameToken) => {
socket.on('game:turn-changed', handleTurnChanged); socket.on('game:turn-changed', handleTurnChanged);
socket.on('game:error', handleError); 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 // Cleanup
return () => { return () => {
log('🧹 Cleaning up WebSocket connection'); log('🧹 Cleaning up WebSocket connection');
@@ -247,6 +384,115 @@ export const useGameWebSocket = (gameToken) => {
return true; return true;
}, [isConnected, gameState?.gameCode]); }, [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 { return {
socket: socketRef.current, socket: socketRef.current,
isConnected, isConnected,
@@ -257,11 +503,19 @@ export const useGameWebSocket = (gameToken) => {
error, error,
isGamemaster, isGamemaster,
gameStarted, gameStarted,
pendingPlayers,
approvalStatus,
// Methods // Methods
rollDice, rollDice,
sendMessage, sendMessage,
setReady, setReady,
leaveGame, leaveGame,
approveJoker,
rejectJoker,
submitAnswer,
submitPositionGuess,
approvePlayer,
rejectPlayer,
addEventListener, addEventListener,
removeEventListener, 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 (/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 { getVerticalOffset } from "../../utils/randomUtils"
import Dice from "../../utils/dice/Dice" import Dice from "../../utils/dice/Dice"
import { useGameWebSocket } from "../../hooks/useGameWebSocket" 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 // Constants - outside component to prevent recreation
const PLAYER_STYLES = [ const PLAYER_STYLES = [
@@ -47,17 +51,54 @@ const GameScreen = () => {
isConnected, isConnected,
gameState, gameState,
players: backendPlayers, players: backendPlayers,
boardData, boardData: websocketBoardData,
currentTurn, currentTurn,
error, error,
rollDice, rollDice,
approveJoker,
rejectJoker,
submitAnswer,
submitPositionGuess,
addEventListener, addEventListener,
removeEventListener removeEventListener
} = useGameWebSocket(gameToken) } = 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 [path, setPath] = useState([])
const [players, setPlayers] = 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 // Memoized board dimensions
const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => { const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => {
const { rows, cols, cellSize, cellMargin, rowSpacing } = BOARD_CONFIG const { rows, cols, cellSize, cellMargin, rowSpacing } = BOARD_CONFIG
@@ -171,6 +212,178 @@ const GameScreen = () => {
return () => removeEventListener('game:player-moved') return () => removeEventListener('game:player-moved')
}, [addEventListener, removeEventListener]) }, [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 // Sorted players - memoized
const sortedPlayers = useMemo( const sortedPlayers = useMemo(
() => [...players].sort((a, b) => b.position - a.position), () => [...players].sort((a, b) => b.position - a.position),
@@ -179,7 +392,13 @@ const GameScreen = () => {
// Handle dice roll // Handle dice roll
const handleDiceRoll = useCallback((value) => { 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]) }, [rollDice])
// Get field style - memoized // Get field style - memoized
@@ -233,14 +452,12 @@ const GameScreen = () => {
{isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'} {isConnected ? '🟢 Csatlakozva' : '🔴 Kapcsolódás...'}
</span> </span>
</div> </div>
{error && ( {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"> <div className="mt-2 px-4 py-2 rounded-lg shadow-lg bg-red-600 text-white text-xs">
{error} {error}
</div> </div>
)} )}
</div> </div> {/* Game Info Bar */}
{/* Game Info Bar */}
{gameState && ( {gameState && (
<div className="fixed top-4 left-4 z-50"> <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"> <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> </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> </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
+179 -18
View File
@@ -9,6 +9,7 @@ import { startGame } from "../../api/gameApi.js"
const Lobby = () => { const Lobby = () => {
const [visible, setVisible] = useState(false) const [visible, setVisible] = useState(false)
const [isStarting, setIsStarting] = useState(false)
const sectionRef = useRef(null) const sectionRef = useRef(null)
const { goHome, goGame } = HandleNavigate() const { goHome, goGame } = HandleNavigate()
const location = useLocation() const location = useLocation()
@@ -25,19 +26,29 @@ const Lobby = () => {
players, players,
isGamemaster, isGamemaster,
gameStarted, gameStarted,
pendingPlayers,
approvalStatus,
approvePlayer,
rejectPlayer,
} = useGameWebSocket(gameToken) } = useGameWebSocket(gameToken)
const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...' const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...'
// Filter out gamemaster from player list - gamemaster is NOT a player // Players list - gamemaster is separate, don't filter
const currentPlayers = (players || []).filter(p => { // Backend should handle this correctly
// If we have userId info, filter by that const currentPlayers = players || []
if (p.userId) {
return p.userId !== gameState?.createdBy // 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) }, [isGamemaster, gameState, players, currentPlayers, pendingPlayers])
return true
})
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
@@ -56,7 +67,18 @@ const Lobby = () => {
console.log('🎮 Game started, navigating to /game') console.log('🎮 Game started, navigating to /game')
goGame() 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 = () => { const handleExit = () => {
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) { if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
@@ -66,7 +88,15 @@ const Lobby = () => {
} }
const handleStartGame = async () => { const handleStartGame = async () => {
// Prevent double-click
if (isStarting) {
console.log('⚠️ Game start already in progress, ignoring duplicate request')
return
}
try { try {
setIsStarting(true)
// Get gameId from gameState // Get gameId from gameState
const gameId = gameState?.gameId const gameId = gameState?.gameId
if (!gameId) { if (!gameId) {
@@ -78,13 +108,30 @@ const Lobby = () => {
const response = await startGame(gameId) const response = await startGame(gameId)
console.log('Game start response:', response) console.log('Game start response:', response)
// Backend will broadcast game:started event to all players // Store boardData and updated game state for GameScreen
// Navigate to game page if (response.boardData) {
goGame() 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) { } catch (error) {
console.error('Failed to start game:', error) console.error('Failed to start game:', error)
// 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}`) alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
} }
} finally {
setIsStarting(false)
}
} }
const copyGameCode = () => { const copyGameCode = () => {
@@ -92,6 +139,21 @@ const Lobby = () => {
alert('Játék kód vágólapra másolva: ' + gameCode) 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) => { const getInitials = (name) => {
return name return name
.split(" ") .split(" ")
@@ -111,7 +173,47 @@ const Lobby = () => {
<Navbar /> <Navbar />
</div> </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 <section
ref={sectionRef} 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 ${ 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}): Játékosok ({currentPlayers.length}):
</p> </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"> <div className="bg-zinc-800/90 rounded-xl shadow-lg p-6 mb-8">
<ul className="flex flex-col gap-4"> <ul className="flex flex-col gap-4">
{currentPlayers.length === 0 ? ( {currentPlayers.length === 0 ? (
@@ -215,15 +369,21 @@ const Lobby = () => {
/* Gamemaster view - can start game */ /* Gamemaster view - can start game */
<button <button
onClick={handleStartGame} 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 ${ 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-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' : '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> </button>
) : ( ) : (
/* Player view - cannot start game, just wait */ /* Player view - cannot start game, just wait */
@@ -240,7 +400,8 @@ const Lobby = () => {
</button> </button>
</div> </div>
</section> </section>
</main> </div>
)}
</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