Merge pull request 'final POC' (#103) from Backend_Fix into main
Reviewed-on: #103
This commit was merged in pull request #103.
This commit is contained in:
@@ -99,7 +99,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
|
||||
if (data.type === 'QUESTION') {
|
||||
// Quiz típus validálás
|
||||
if (data.subType === 'quiz') {
|
||||
if (!data.question || !data.question.trim()) {
|
||||
if (!data.text || !data.text.trim()) {
|
||||
notifyError("Kérdés megadása kötelező!")
|
||||
return false
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
|
||||
}
|
||||
// Igaz/Hamis típus validálás
|
||||
else if (data.subType === 'truefalse') {
|
||||
if (!data.statement || !data.statement.trim()) {
|
||||
if (!data.text || !data.text.trim()) {
|
||||
notifyError("Állítás megadása kötelező!")
|
||||
return false
|
||||
}
|
||||
@@ -121,7 +121,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
|
||||
}
|
||||
// Párosítás típus validálás
|
||||
else if (data.subType === 'matching') {
|
||||
if (!data.taskDescription || !data.taskDescription.trim()) {
|
||||
if (!data.text || !data.text.trim()) {
|
||||
notifyError("Feladat leírása kötelező!")
|
||||
return false
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
|
||||
}
|
||||
// Szöveges válasz típus validálás
|
||||
else if (data.subType === 'text') {
|
||||
if (!data.question || !data.question.trim()) {
|
||||
if (!data.text || !data.text.trim()) {
|
||||
notifyError("Kérdés megadása kötelező!")
|
||||
return false
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export default function CardEditor({ card, isCreating, cardType, onSave, onCance
|
||||
}
|
||||
// Általános validálás (ha nincs subType megadva)
|
||||
else {
|
||||
if (!data.question && !data.statement) {
|
||||
if (!data.text || !data.text.trim()) {
|
||||
notifyError("Kérdés vagy állítás megadása kötelező!")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -129,8 +129,8 @@ export default function TaskCardEditor({ card, onChange }) {
|
||||
Kérdés
|
||||
</label>
|
||||
<textarea
|
||||
value={card.question || ''}
|
||||
onChange={(e) => updateField('question', e.target.value)}
|
||||
value={card.text || ''}
|
||||
onChange={(e) => updateField('text', e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 resize-none"
|
||||
rows="3"
|
||||
placeholder="Írd be a kérdést..."
|
||||
@@ -190,8 +190,8 @@ export default function TaskCardEditor({ card, onChange }) {
|
||||
Állítás
|
||||
</label>
|
||||
<textarea
|
||||
value={card.statement || ''}
|
||||
onChange={(e) => updateField('statement', e.target.value)}
|
||||
value={card.text || ''}
|
||||
onChange={(e) => updateField('text', e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 resize-none"
|
||||
rows="3"
|
||||
placeholder="Írd be az állítást..."
|
||||
@@ -251,8 +251,8 @@ export default function TaskCardEditor({ card, onChange }) {
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={card.taskDescription || ''}
|
||||
onChange={(e) => updateField('taskDescription', e.target.value)}
|
||||
value={card.text || ''}
|
||||
onChange={(e) => updateField('text', e.target.value)}
|
||||
className="w-full px-4 py-2 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200"
|
||||
placeholder="Pl.: Párosítsd a országokat a fővárosukkal"
|
||||
/>
|
||||
@@ -326,8 +326,8 @@ export default function TaskCardEditor({ card, onChange }) {
|
||||
Kérdés
|
||||
</label>
|
||||
<textarea
|
||||
value={card.question || ''}
|
||||
onChange={(e) => updateField('question', e.target.value)}
|
||||
value={card.text || ''}
|
||||
onChange={(e) => updateField('text', e.target.value)}
|
||||
className="w-full px-4 py-3 rounded-xl bg-[color:var(--color-background)] border border-[color:var(--color-surface-selected)] text-[color:var(--color-text)] focus:ring-2 focus:ring-[color:var(--color-success)] focus:border-transparent outline-none transition-all duration-200 resize-none"
|
||||
rows="3"
|
||||
placeholder="Írd be a kérdést..."
|
||||
|
||||
@@ -20,8 +20,17 @@ const PlayMenu = ({ onJoinGame, onCreateGame, user, setUser }) => {
|
||||
setError("Add meg a játék kódját!")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user has a name (logged in or guest)
|
||||
const nameToSend = username ?? guestName?.trim()
|
||||
if (!nameToSend) {
|
||||
setGuestError("Adj meg egy nevet a csatlakozáshoz!")
|
||||
return
|
||||
}
|
||||
|
||||
setError("")
|
||||
onJoinGame(joinCode)
|
||||
setGuestError("")
|
||||
onJoinGame(joinCode, nameToSend)
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Card Type Constants
|
||||
* Must match backend CardType enum from DeckAggregate.ts
|
||||
*/
|
||||
export const CardType = {
|
||||
QUIZ: 0, // Multiple choice (A, B, C, D)
|
||||
SENTENCE_PAIRING: 1, // Match left to right parts
|
||||
OWN_ANSWER: 2, // Free text answer
|
||||
TRUE_FALSE: 3, // True or False question
|
||||
CLOSER: 4 // Guess the closest number
|
||||
};
|
||||
|
||||
/**
|
||||
* Get card type name for display
|
||||
*/
|
||||
export const getCardTypeName = (type) => {
|
||||
switch (type) {
|
||||
case CardType.QUIZ:
|
||||
return 'Kvíz';
|
||||
case CardType.SENTENCE_PAIRING:
|
||||
return 'Mondatpárosítás';
|
||||
case CardType.OWN_ANSWER:
|
||||
return 'Saját válasz';
|
||||
case CardType.TRUE_FALSE:
|
||||
return 'Igaz vagy Hamis';
|
||||
case CardType.CLOSER:
|
||||
return 'Közelebb';
|
||||
default:
|
||||
return 'Kérdés';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if card type requires text input
|
||||
*/
|
||||
export const requiresTextInput = (type) => {
|
||||
return type === CardType.OWN_ANSWER || type === CardType.TRUE_FALSE || type === CardType.CLOSER;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if card type has multiple choice options
|
||||
*/
|
||||
export const hasMultipleChoice = (type) => {
|
||||
return type === CardType.QUIZ;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if card type has sentence pairing
|
||||
*/
|
||||
export const hasSentencePairing = (type) => {
|
||||
return type === CardType.SENTENCE_PAIRING;
|
||||
};
|
||||
@@ -16,42 +16,48 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
const socketRef = useRef(null);
|
||||
const gameTokenRef = useRef(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
|
||||
// Single game object containing all game data
|
||||
const [gameState, setGameState] = useState(null);
|
||||
const [boardData, setBoardData] = useState(null);
|
||||
// Structure: {
|
||||
// gameCode: string,
|
||||
// boardData: object,
|
||||
// turnInfo: { currentPlayer, currentPlayerName, turnNumber },
|
||||
// players: [{ playerId, playerName, isOnline, isReady }],
|
||||
// playerPositions: { playerName: position },
|
||||
// connectedPlayers: [],
|
||||
// readyPlayers: [],
|
||||
// state: string,
|
||||
// isGamemaster: boolean
|
||||
// }
|
||||
|
||||
const [error, setError] = useState(null);
|
||||
const [isGamemaster, setIsGamemaster] = useState(false);
|
||||
const [gameStarted, setGameStarted] = useState(false);
|
||||
const [pendingPlayers, setPendingPlayers] = useState([]);
|
||||
const [approvalStatus, setApprovalStatus] = useState(null);
|
||||
const [playerIdentifier, setPlayerIdentifier] = useState(null);
|
||||
const [isMyTurnFlag, setIsMyTurnFlag] = useState(false); // Directly controlled by game:your-turn
|
||||
const [playerDiceRolls, setPlayerDiceRolls] = useState({});
|
||||
|
||||
// Memoized derived values
|
||||
// Memoized derived values - extract from single game object
|
||||
const players = useMemo(() => {
|
||||
const connectedPlayers = gameState?.connectedPlayers || [];
|
||||
const currentPlayers = gameState?.currentPlayers || [];
|
||||
|
||||
if (currentPlayers.length > 0) {
|
||||
return currentPlayers;
|
||||
}
|
||||
|
||||
if (connectedPlayers.length > 0) {
|
||||
return connectedPlayers.map((nameOrObj, index) => {
|
||||
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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [gameState?.connectedPlayers, gameState?.currentPlayers, gameState?.readyPlayers]);
|
||||
return gameState?.players || [];
|
||||
}, [gameState?.players]);
|
||||
|
||||
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
|
||||
const playerPositions = useMemo(() => {
|
||||
return gameState?.playerPositions || {};
|
||||
}, [gameState?.playerPositions]);
|
||||
|
||||
const boardData = useMemo(() => {
|
||||
return gameState?.boardData || null;
|
||||
}, [gameState?.boardData]);
|
||||
|
||||
const currentTurn = useMemo(() => gameState?.turnInfo?.currentPlayer || null, [gameState?.turnInfo?.currentPlayer]);
|
||||
const currentTurnName = useMemo(() => gameState?.turnInfo?.currentPlayerName || null, [gameState?.turnInfo?.currentPlayerName]);
|
||||
|
||||
// isMyTurn is simply the flag set by game:your-turn event
|
||||
const isMyTurn = isMyTurnFlag;
|
||||
|
||||
/**
|
||||
* Connect to game WebSocket with a game token
|
||||
@@ -79,6 +85,16 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
log('🔌 Connecting to game WebSocket...');
|
||||
gameTokenRef.current = gameToken;
|
||||
|
||||
// Decode token to get player identifier
|
||||
try {
|
||||
const payload = JSON.parse(atob(gameToken.split('.')[1]));
|
||||
const identifier = payload.userId || payload.playerName;
|
||||
setPlayerIdentifier(identifier);
|
||||
log('🎮 Player identifier:', identifier);
|
||||
} catch (err) {
|
||||
logError('Failed to decode game token:', err);
|
||||
}
|
||||
|
||||
// Connect to /game namespace
|
||||
socketRef.current = io(`${API_CONFIG.wsURL}/game`, {
|
||||
transports: ['websocket'],
|
||||
@@ -102,11 +118,22 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
logError('❌ Connection error:', err);
|
||||
setIsConnected(false);
|
||||
setError(err.message);
|
||||
|
||||
// If reconnection fails completely, navigate to home
|
||||
if (err.message?.includes('timeout') || err.message?.includes('xhr poll error')) {
|
||||
setTimeout(() => {
|
||||
if (!socketRef.current?.connected) {
|
||||
logError('⚠️ Connection failed - redirecting to home');
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, 10000); // Give 10 seconds for reconnection attempts
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', (reason) => {
|
||||
log('🔌 Disconnected:', reason);
|
||||
setIsConnected(false);
|
||||
setIsMyTurnFlag(false); // Clear turn flag on disconnect
|
||||
});
|
||||
|
||||
// Game state handlers
|
||||
@@ -115,7 +142,72 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
if (state?.isGamemaster !== undefined) {
|
||||
setIsGamemaster(state.isGamemaster);
|
||||
}
|
||||
setGameState(state);
|
||||
// Merge state into single game object
|
||||
setGameState(prev => {
|
||||
// Build players array from various sources
|
||||
let playersArray = prev?.players || [];
|
||||
|
||||
// If we have currentPlayers or players from backend, use them (already have proper structure)
|
||||
if (state.currentPlayers && Array.isArray(state.currentPlayers) && state.currentPlayers.length > 0) {
|
||||
playersArray = state.currentPlayers.map(p => ({
|
||||
playerId: p.playerId || p.id,
|
||||
playerName: p.playerName || p.name,
|
||||
name: p.playerName || p.name, // Add name for compatibility
|
||||
isOnline: p.isOnline !== undefined ? p.isOnline : true,
|
||||
isReady: p.isReady || false
|
||||
}));
|
||||
} else if (state.players && Array.isArray(state.players) && state.players.length > 0) {
|
||||
playersArray = state.players.map(p => ({
|
||||
playerId: p.playerId || p.id,
|
||||
playerName: p.playerName || p.name,
|
||||
name: p.playerName || p.name, // Add name for compatibility
|
||||
isOnline: p.isOnline !== undefined ? p.isOnline : true,
|
||||
isReady: p.isReady || false
|
||||
}));
|
||||
}
|
||||
// If players array is still empty but we have connectedPlayers (array of strings), convert them
|
||||
else if (playersArray.length === 0 && state.connectedPlayers && Array.isArray(state.connectedPlayers) && state.connectedPlayers.length > 0) {
|
||||
playersArray = state.connectedPlayers.map((playerName, index) => ({
|
||||
playerId: `player-${index}`, // Temporary ID until we get the real one
|
||||
playerName: playerName,
|
||||
name: playerName, // Add name for compatibility
|
||||
isOnline: true,
|
||||
isReady: false
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialize playerPositions from backend data if joining game in progress
|
||||
let playerPositions = prev?.playerPositions || {};
|
||||
// If we don't have positions yet but backend sent players with positions, initialize from them
|
||||
if (Object.keys(playerPositions).length === 0 && state.players && Array.isArray(state.players)) {
|
||||
const positionsFromBackend = {};
|
||||
state.players.forEach(p => {
|
||||
const playerName = p.playerName || p.name;
|
||||
if (playerName && p.position !== undefined) {
|
||||
positionsFromBackend[playerName] = p.position;
|
||||
}
|
||||
});
|
||||
if (Object.keys(positionsFromBackend).length > 0) {
|
||||
playerPositions = positionsFromBackend;
|
||||
}
|
||||
}
|
||||
|
||||
const merged = {
|
||||
...prev,
|
||||
gameCode: state.gameCode || prev?.gameCode,
|
||||
boardData: state.boardData || prev?.boardData,
|
||||
state: state.state || prev?.state,
|
||||
connectedPlayers: state.connectedPlayers || prev?.connectedPlayers || [],
|
||||
readyPlayers: state.readyPlayers || prev?.readyPlayers || [],
|
||||
// Preserve turn info - only turn-changed updates it
|
||||
turnInfo: prev?.turnInfo || { currentPlayer: null, currentPlayerName: null, turnNumber: 0 },
|
||||
// Use the built players array
|
||||
players: playersArray,
|
||||
// Initialize playerPositions from backend if joining in progress, otherwise preserve
|
||||
playerPositions: playerPositions
|
||||
};
|
||||
return merged;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('game:state-update', (state) => {
|
||||
@@ -123,7 +215,19 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
if (state?.isGamemaster !== undefined) {
|
||||
setIsGamemaster(state.isGamemaster);
|
||||
}
|
||||
setGameState(state);
|
||||
// Merge state update into single game object
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
gameCode: state.gameCode || prev?.gameCode,
|
||||
boardData: state.boardData || prev?.boardData,
|
||||
state: state.state || prev?.state,
|
||||
connectedPlayers: state.connectedPlayers || prev?.connectedPlayers,
|
||||
readyPlayers: state.readyPlayers || prev?.readyPlayers,
|
||||
// Preserve turn info and positions
|
||||
turnInfo: prev?.turnInfo,
|
||||
players: prev?.players,
|
||||
playerPositions: prev?.playerPositions
|
||||
}));
|
||||
});
|
||||
|
||||
socket.on('game:joined', (data) => {
|
||||
@@ -131,6 +235,26 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
if (data.isGamemaster !== undefined) {
|
||||
setIsGamemaster(data.isGamemaster);
|
||||
}
|
||||
// Initialize or update gameState with gameCode and gameId from join confirmation
|
||||
setGameState(prev => {
|
||||
if (prev && prev.gameCode) {
|
||||
// If already has gameCode, just add gameId if missing
|
||||
return {
|
||||
...prev,
|
||||
gameId: data.gameId || prev.gameId,
|
||||
gameCode: data.gameCode,
|
||||
playerName: data.playerName,
|
||||
isAuthenticated: data.isAuthenticated
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
gameId: data.gameId, // Store gameId from backend
|
||||
gameCode: data.gameCode,
|
||||
playerName: data.playerName,
|
||||
isAuthenticated: data.isAuthenticated
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('game:player-joined', (data) => {
|
||||
@@ -138,60 +262,128 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
const currentConnected = prev.connectedPlayers || [];
|
||||
if (!currentConnected.includes(data.playerName)) {
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: [...currentConnected, data.playerName]
|
||||
};
|
||||
const currentPlayers = prev.players || [];
|
||||
|
||||
// Add to connectedPlayers if not already there
|
||||
const updatedConnected = currentConnected.includes(data.playerName)
|
||||
? currentConnected
|
||||
: [...currentConnected, data.playerName];
|
||||
|
||||
// Add to players array if not already there (without position - that's in playerPositions)
|
||||
const playerExists = currentPlayers.some(p =>
|
||||
p.playerName === data.playerName || p.name === data.playerName
|
||||
);
|
||||
|
||||
const updatedPlayers = playerExists
|
||||
? currentPlayers
|
||||
: [...currentPlayers, {
|
||||
playerId: data.playerId,
|
||||
playerName: data.playerName,
|
||||
name: data.playerName, // Add name for compatibility with Lobby display
|
||||
isOnline: true,
|
||||
isReady: false
|
||||
}];
|
||||
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: updatedConnected,
|
||||
players: updatedPlayers
|
||||
};
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('game:player-joined', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-left', (data) => {
|
||||
log('👋 Player left:', data.playerName);
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: (prev.connectedPlayers || []).filter(p => p !== data.playerName)
|
||||
};
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('game:player-left', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-ready', (data) => {
|
||||
log('✅ Player ready:', data.playerName);
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
const readyPlayers = prev.readyPlayers || [];
|
||||
if (data.ready && !readyPlayers.includes(data.playerName)) {
|
||||
return { ...prev, readyPlayers: [...readyPlayers, data.playerName] };
|
||||
} else if (!data.ready) {
|
||||
return { ...prev, readyPlayers: readyPlayers.filter(p => p !== data.playerName) };
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('game:player-ready', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:all-ready', (data) => {
|
||||
log('✅ All players ready! Game can start.');
|
||||
window.dispatchEvent(new CustomEvent('game:all-ready', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:start', (data) => {
|
||||
log('🎮 Game started:', data);
|
||||
setGameStarted(true);
|
||||
|
||||
// Store board data if provided
|
||||
if (data.boardData) {
|
||||
setBoardData(data.boardData);
|
||||
log('✅ Board data stored from game:start event');
|
||||
}
|
||||
|
||||
// Update game state with turn info
|
||||
if (data.playerOrder) {
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
playerOrder: data.playerOrder,
|
||||
currentPlayer: data.currentPlayer,
|
||||
turnSequence: data.playerOrder
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('game:started', (data) => {
|
||||
log('🎮 Game started (legacy event):', data);
|
||||
setGameStarted(true);
|
||||
});
|
||||
|
||||
socket.on('game:player-moved', (moveData) => {
|
||||
log('🏃 Player moved:', moveData.playerName);
|
||||
// Update game state with position initialization
|
||||
setGameState(prev => {
|
||||
if (!prev?.currentPlayers) return prev;
|
||||
return {
|
||||
// Initialize all player positions to 0 at game start
|
||||
const initialPositions = {};
|
||||
|
||||
// Use existing players from prev (already set by game:joined/game:state)
|
||||
const existingPlayers = prev?.players || [];
|
||||
|
||||
// Initialize positions for all existing players
|
||||
existingPlayers.forEach((player) => {
|
||||
const playerName = player.playerName || player.name;
|
||||
if (playerName) {
|
||||
// Initialize ALL positions to 0 - only game:player-arrived will change them
|
||||
initialPositions[playerName] = 0;
|
||||
}
|
||||
});
|
||||
|
||||
const updated = {
|
||||
...prev,
|
||||
currentPlayers: prev.currentPlayers.map(p =>
|
||||
p.playerId === moveData.playerId
|
||||
? { ...p, boardPosition: moveData.newPosition }
|
||||
: p
|
||||
),
|
||||
gameCode: data.gameCode || prev?.gameCode,
|
||||
boardData: data.boardData || prev?.boardData,
|
||||
state: data.gamePhase || data.state || 'playing',
|
||||
boardSize: data.boardSize || prev?.boardSize,
|
||||
// Keep existing players list, initialize positions to 0
|
||||
players: existingPlayers,
|
||||
playerPositions: initialPositions,
|
||||
// Preserve turn info and other data
|
||||
turnInfo: prev?.turnInfo || { currentPlayer: null, currentPlayerName: null, turnNumber: 0 },
|
||||
connectedPlayers: prev?.connectedPlayers || [],
|
||||
readyPlayers: prev?.readyPlayers || []
|
||||
};
|
||||
return updated;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('game:turn-changed', (data) => {
|
||||
log('🔄 Turn changed to:', data.currentPlayerName);
|
||||
setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev);
|
||||
|
||||
// Turn changed means it's NOT my turn anymore
|
||||
setIsMyTurnFlag(false);
|
||||
|
||||
// This is the ONLY place turnInfo should be set
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
turnInfo: {
|
||||
currentPlayer: data.currentPlayer,
|
||||
currentPlayerName: data.currentPlayerName,
|
||||
turnNumber: data.turnNumber || prev?.turnInfo?.turnNumber || 0
|
||||
}
|
||||
}));
|
||||
|
||||
// Force a re-render by logging after state update
|
||||
setTimeout(() => {
|
||||
console.log('🔄 [game:turn-changed] State should be updated now');
|
||||
}, 100);
|
||||
});
|
||||
|
||||
socket.on('game:error', (err) => {
|
||||
@@ -246,6 +438,199 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
return prev;
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('game:your-turn', (data) => {
|
||||
log('🎯 Your turn!', data);
|
||||
console.log('🎯 [game:your-turn] Received - enabling dice');
|
||||
|
||||
// Set flag to true - dice is now enabled
|
||||
setIsMyTurnFlag(true);
|
||||
|
||||
// Emit custom event for GameScreen to display turn notification
|
||||
window.dispatchEvent(new CustomEvent('game:your-turn', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:dice-rolled', (data) => {
|
||||
log('🎲 Dice rolled:', data.diceValue, 'by', data.playerName);
|
||||
// Track the dice roll for this player
|
||||
setPlayerDiceRolls(prev => ({
|
||||
...prev,
|
||||
[data.playerName]: data.diceValue
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent('game:dice-rolled', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-moving', (data) => {
|
||||
log('🚶 Player moving:', data.playerName, 'from', data.fromPosition, 'to', data.toPosition);
|
||||
window.dispatchEvent(new CustomEvent('game:player-moving', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-arrived', (data) => {
|
||||
log('🎯 Player arrived:', data.playerName, 'at position', data.position, '(' + data.fieldType + ')');
|
||||
|
||||
// Update playerPositions in game object - THIS IS THE ONLY PLACE POSITIONS ARE MODIFIED
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
const updatedPositions = { ...prev.playerPositions, [data.playerName]: data.position };
|
||||
|
||||
return {
|
||||
...prev,
|
||||
playerPositions: updatedPositions
|
||||
};
|
||||
});
|
||||
|
||||
window.dispatchEvent(new CustomEvent('game:player-arrived', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:card-drawn', (data) => {
|
||||
log('🃏 Card drawn:', data.cardType, 'by', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:card-drawn', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:card-result', (data) => {
|
||||
log('🎴 Card result:', data.playerName, data.description);
|
||||
window.dispatchEvent(new CustomEvent('game:card-result', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:answer-timeout', (data) => {
|
||||
log('⏰ Answer timeout:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:answer-timeout', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:answer-result', (data) => {
|
||||
log('📝 Answer result:', data.correct ? '✅ Correct' : '❌ Wrong', '-', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:answer-result', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:gamemaster-decision-request', (data) => {
|
||||
log('👨⚖️ Gamemaster decision request:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:gamemaster-decision-request', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:gamemaster-decision-result', (data) => {
|
||||
log('⚖️ Gamemaster decision result:', data.decision, '-', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:gamemaster-decision-result', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:card-drawn-self', (data) => {
|
||||
log('🃏 You drew a card:', data.cardType);
|
||||
window.dispatchEvent(new CustomEvent('game:card-drawn-self', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:joker-activated', (data) => {
|
||||
log('🃏 Joker activated:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:joker-activated', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:extra-turn', (data) => {
|
||||
log('⭐ Extra turn:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:extra-turn', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:turn-lost', (data) => {
|
||||
log('😞 Turn lost:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:turn-lost', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:no-movement', (data) => {
|
||||
log('⛔ No movement:', data.playerName, '-', data.reason);
|
||||
window.dispatchEvent(new CustomEvent('game:no-movement', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:penalty-avoided', (data) => {
|
||||
log('🛡️ Penalty avoided:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:penalty-avoided', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:guess-timeout', (data) => {
|
||||
log('⏰ Guess timeout:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:guess-timeout', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-guessing', (data) => {
|
||||
log('🤔 Player guessing:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:player-guessing', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:secondary-landing', (data) => {
|
||||
log('🎯 Secondary landing:', data.playerName, 'on', data.fieldType);
|
||||
window.dispatchEvent(new CustomEvent('game:secondary-landing', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-disconnected', (data) => {
|
||||
log('⚠️ Player disconnected:', data.playerName);
|
||||
setGameState(prev => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
connectedPlayers: (prev.connectedPlayers || []).filter(p => p !== data.playerName)
|
||||
};
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('game:player-disconnected', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-disconnected-during-turn', (data) => {
|
||||
log('⚠️ Player disconnected during turn:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:player-disconnected-during-turn', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:player-disconnected-during-card', (data) => {
|
||||
log('⚠️ Player disconnected during card:', data.playerName);
|
||||
window.dispatchEvent(new CustomEvent('game:player-disconnected-during-card', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:guess-result', (data) => {
|
||||
log('🎯 Guess result:', data);
|
||||
// Position update will come from game:player-arrived event
|
||||
window.dispatchEvent(new CustomEvent('game:guess-result', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:joker-complete', (data) => {
|
||||
log('🃏 Joker complete:', data);
|
||||
// Position update will come from game:player-arrived event
|
||||
window.dispatchEvent(new CustomEvent('game:joker-complete', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:luck-consequence', (data) => {
|
||||
log('🍀 Luck consequence:', data);
|
||||
// Position update will come from game:player-arrived event
|
||||
window.dispatchEvent(new CustomEvent('game:luck-consequence', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:ended', (data) => {
|
||||
log('🏁 Game ended! Winner:', data.winner);
|
||||
setGameState(prev => ({
|
||||
...prev,
|
||||
status: 'finished',
|
||||
winner: data.winner,
|
||||
finalScores: data.scores
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent('game:ended', { detail: data }));
|
||||
|
||||
// Don't auto-navigate - let the player close the winner modal manually
|
||||
// They can use the "Vissza a főoldalra" button when ready
|
||||
});
|
||||
|
||||
socket.on('game:extra-turn-remaining', (data) => {
|
||||
log('⭐ Extra turn remaining:', data);
|
||||
window.dispatchEvent(new CustomEvent('game:extra-turn-remaining', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:players-skipped', (data) => {
|
||||
log('⏭️ Players skipped:', data.skippedPlayers);
|
||||
window.dispatchEvent(new CustomEvent('game:players-skipped', { detail: data }));
|
||||
});
|
||||
|
||||
socket.on('game:cleanup-complete', (data) => {
|
||||
log('🧹 Cleanup complete:', data);
|
||||
window.dispatchEvent(new CustomEvent('game:cleanup-complete', { detail: data }));
|
||||
|
||||
// Navigate to home after cleanup
|
||||
setTimeout(() => {
|
||||
log('🏠 Navigating to home after cleanup');
|
||||
window.location.href = '/';
|
||||
}, 2000);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@@ -259,8 +644,7 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
socketRef.current = null;
|
||||
gameTokenRef.current = null;
|
||||
setIsConnected(false);
|
||||
setGameState(null);
|
||||
setBoardData(null);
|
||||
setGameState(null); // Clear entire game object
|
||||
setError(null);
|
||||
setIsGamemaster(false);
|
||||
setGameStarted(false);
|
||||
@@ -316,9 +700,79 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
return true;
|
||||
}, [isConnected, isGamemaster, gameState?.gameCode]);
|
||||
|
||||
const submitAnswer = useCallback((answer, cardId = null) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot submit answer: not connected');
|
||||
return false;
|
||||
}
|
||||
log('📝 Submitting answer:', answer, 'for card:', cardId);
|
||||
socket.emit('game:card-answer', {
|
||||
gameCode: gameState?.gameCode,
|
||||
answer,
|
||||
cardId
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
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]);
|
||||
|
||||
const submitJokerPositionGuess = useCallback((guessedPosition) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected) {
|
||||
warn('⚠️ Cannot submit joker position guess: not connected');
|
||||
return false;
|
||||
}
|
||||
log('🃏🎯 Submitting joker position guess:', guessedPosition);
|
||||
socket.emit('game:joker-position-guess', { gameCode: gameState?.gameCode, guessedPosition });
|
||||
return true;
|
||||
}, [isConnected, gameState?.gameCode]);
|
||||
|
||||
const approveJoker = useCallback((requestId) => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected || !isGamemaster) {
|
||||
warn('⚠️ Cannot approve joker: not gamemaster or not connected');
|
||||
return false;
|
||||
}
|
||||
log('✅ Approving joker request:', requestId);
|
||||
socket.emit('game:gamemaster-decision', {
|
||||
gameCode: gameState?.gameCode,
|
||||
requestId,
|
||||
decision: 'approve'
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, isGamemaster, gameState?.gameCode]);
|
||||
|
||||
const rejectJoker = useCallback((requestId, reason = 'Joker answer rejected') => {
|
||||
const socket = socketRef.current;
|
||||
if (!socket || !isConnected || !isGamemaster) {
|
||||
warn('⚠️ Cannot reject joker: not gamemaster or not connected');
|
||||
return false;
|
||||
}
|
||||
log('❌ Rejecting joker request:', requestId, 'Reason:', reason);
|
||||
socket.emit('game:gamemaster-decision', {
|
||||
gameCode: gameState?.gameCode,
|
||||
requestId,
|
||||
decision: 'reject',
|
||||
reason
|
||||
});
|
||||
return true;
|
||||
}, [isConnected, isGamemaster, gameState?.gameCode]);
|
||||
|
||||
const addEventListener = useCallback((event, handler) => {
|
||||
const socket = socketRef.current;
|
||||
if (socket) socket.on(event, handler);
|
||||
if (socket) {
|
||||
socket.on(event, handler);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const removeEventListener = useCallback((event, handler) => {
|
||||
@@ -329,15 +783,20 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
const value = {
|
||||
socket: socketRef.current,
|
||||
isConnected,
|
||||
gameState,
|
||||
players,
|
||||
boardData,
|
||||
currentTurn,
|
||||
gameState, // Single game object with all data
|
||||
players, // Memoized from gameState.players
|
||||
playerPositions, // Memoized from gameState.playerPositions (SINGLE SOURCE OF TRUTH for positions)
|
||||
boardData, // Memoized from gameState.boardData
|
||||
currentTurn, // Memoized from gameState.turnInfo.currentPlayer
|
||||
currentTurnName, // Memoized from gameState.turnInfo.currentPlayerName
|
||||
isMyTurn,
|
||||
playerIdentifier,
|
||||
error,
|
||||
isGamemaster,
|
||||
gameStarted,
|
||||
pendingPlayers,
|
||||
approvalStatus,
|
||||
playerDiceRolls,
|
||||
// Connection management
|
||||
connect,
|
||||
disconnect,
|
||||
@@ -348,6 +807,11 @@ export const GameWebSocketProvider = ({ children }) => {
|
||||
leaveGame,
|
||||
approvePlayer,
|
||||
rejectPlayer,
|
||||
submitAnswer,
|
||||
submitPositionGuess,
|
||||
submitJokerPositionGuess,
|
||||
approveJoker,
|
||||
rejectJoker,
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
};
|
||||
|
||||
@@ -68,14 +68,71 @@ export default function DeckCreator() {
|
||||
2: 'organization'
|
||||
}
|
||||
|
||||
// Process cards: convert type field from number to string
|
||||
const processedCards = (deckData.cards || []).map(card => {
|
||||
// A kártya type mezője a deck type-ját tükrözi (backend így küldi)
|
||||
// Ezért a deck type alapján állítjuk be
|
||||
return {
|
||||
...card,
|
||||
type: typeMapping[deckData.type] || 'QUESTION'
|
||||
// Process cards: convert backend Card structure to frontend format
|
||||
const processedCards = (deckData.cards || []).map((card, index) => {
|
||||
const deckType = typeMapping[deckData.type] || 'QUESTION'
|
||||
|
||||
// Base card structure
|
||||
const processedCard = {
|
||||
id: card.id || Date.now() + index,
|
||||
type: deckType,
|
||||
text: card.text || ""
|
||||
}
|
||||
|
||||
// For QUESTION deck, determine subType from CardType enum
|
||||
if (deckType === 'QUESTION' && card.type !== undefined) {
|
||||
const cardTypeMapping = {
|
||||
0: 'quiz', // QUIZ
|
||||
1: 'matching', // SENTENCE_PAIRING (editor uses 'matching')
|
||||
2: 'text', // OWN_ANSWER
|
||||
3: 'truefalse', // TRUE_FALSE
|
||||
4: 'closer' // CLOSER
|
||||
}
|
||||
processedCard.subType = cardTypeMapping[card.type] || 'text'
|
||||
|
||||
// Parse answer based on CardType
|
||||
if (card.type === 0 && Array.isArray(card.answer)) {
|
||||
// QUIZ: answer is array of {answer, text, correct}
|
||||
processedCard.options = card.answer.map(opt => opt.text || opt) // Extract text or use whole object
|
||||
const correctOption = card.answer.find(opt => opt.correct)
|
||||
processedCard.correctAnswer = card.answer.indexOf(correctOption)
|
||||
} else if (card.type === 1 && Array.isArray(card.answer)) {
|
||||
// SENTENCE_PAIRING: answer is array of {left, right}
|
||||
// Convert to editor format: leftItems[], rightItems[], correctPairs{leftIdx: rightIdx}
|
||||
processedCard.leftItems = card.answer.map(p => p.left)
|
||||
processedCard.rightItems = card.answer.map(p => p.right)
|
||||
// Create correctPairs mapping: each left item at index i maps to right item at index i
|
||||
processedCard.correctPairs = card.answer.reduce((acc, _, idx) => {
|
||||
acc[idx] = idx
|
||||
return acc
|
||||
}, {})
|
||||
} else if (card.type === 2 && card.answer) {
|
||||
// OWN_ANSWER: answer is array of acceptable strings
|
||||
processedCard.acceptedAnswers = Array.isArray(card.answer) ? card.answer : [card.answer]
|
||||
} else if (card.type === 3 && card.answer) {
|
||||
// TRUE_FALSE: answer is "true" or "false"
|
||||
const answerStr = String(card.answer).toLowerCase()
|
||||
processedCard.isTrue = answerStr === "true" || answerStr === "1"
|
||||
processedCard.correctAnswer = card.answer
|
||||
} else if (card.type === 4 && typeof card.answer === 'object') {
|
||||
// CLOSER: answer is {correct: number, percent: number}
|
||||
processedCard.correctAnswer = card.answer.correct
|
||||
processedCard.percent = card.answer.percent || 10
|
||||
}
|
||||
}
|
||||
|
||||
// For LUCK deck, include consequence
|
||||
if (deckType === 'LUCK' && card.consequence) {
|
||||
processedCard.consequence = card.consequence
|
||||
}
|
||||
|
||||
// Copy question field if exists (for backward compatibility)
|
||||
if (card.question) {
|
||||
processedCard.question = card.question
|
||||
}
|
||||
|
||||
console.log('Card loaded:', { backend: card, frontend: processedCard })
|
||||
return processedCard
|
||||
})
|
||||
|
||||
setDeck({
|
||||
@@ -120,53 +177,103 @@ export default function DeckCreator() {
|
||||
notifyWarning(`${invalidCardsCount} db nem megfelelő típusú kártya törölve a mentés előtt.`)
|
||||
}
|
||||
|
||||
// Tisztítsuk meg a kártyákat - konvertáljuk a backend által várt formátumra
|
||||
// Tisztítsük meg a kártyákat - konvertáljuk a backend által várt formátumra
|
||||
// Backend Card interface: { text: string, type?: CardType, answer?: any, consequence?: Consequence }
|
||||
const cleanedCards = validCards.map(card => {
|
||||
// Card subType mapping to backend CardType enum
|
||||
const cardTypeMapping = {
|
||||
'quiz': 0, // QUIZ
|
||||
'pairing': 1, // SENTENCE_PAIRING
|
||||
'matching': 1, // SENTENCE_PAIRING (editor uses 'matching')
|
||||
'text': 2, // OWN_ANSWER
|
||||
'truefalse': 3, // TRUE_FALSE
|
||||
'closer': 4 // CLOSER
|
||||
}
|
||||
|
||||
// Kezdjük az ID-val (ha van)
|
||||
const cleanedCard = {}
|
||||
|
||||
if (card.id) {
|
||||
cleanedCard.id = card.id
|
||||
}
|
||||
// TEXT field - required (question text)
|
||||
cleanedCard.text = card.text || ""
|
||||
|
||||
// Ha van subType (QUESTION típusú kártyáknál), akkor add hozzá a type mezőt
|
||||
// TYPE field - CardType enum (0-4) for QUESTION cards only
|
||||
if (card.subType && cardTypeMapping[card.subType] !== undefined) {
|
||||
cleanedCard.type = cardTypeMapping[card.subType]
|
||||
}
|
||||
|
||||
// Text mező - kötelező, különböző forrásokból jöhet
|
||||
cleanedCard.text = card.text || card.question || card.statement || ""
|
||||
|
||||
// Egyéb frontend mezők, amiket a backend is elfogad
|
||||
if (card.question !== undefined) cleanedCard.question = card.question
|
||||
if (card.statement !== undefined) cleanedCard.statement = card.statement
|
||||
if (card.options !== undefined) cleanedCard.options = card.options
|
||||
if (card.correctAnswer !== undefined) cleanedCard.correctAnswer = card.correctAnswer
|
||||
if (card.leftItems !== undefined) cleanedCard.leftItems = card.leftItems
|
||||
if (card.rightItems !== undefined) cleanedCard.rightItems = card.rightItems
|
||||
if (card.correctPairs !== undefined) cleanedCard.correctPairs = card.correctPairs
|
||||
if (card.acceptedAnswers !== undefined) cleanedCard.acceptedAnswers = card.acceptedAnswers
|
||||
if (card.hint !== undefined) cleanedCard.hint = card.hint
|
||||
|
||||
// Answer mező (ha van)
|
||||
if (card.answer !== undefined && card.answer !== null) {
|
||||
// ANSWER field - structure depends on CardType
|
||||
if (card.subType === 'quiz' && card.options) {
|
||||
// QUIZ (type 0): answer = array of { answer: "A", text: "...", correct: boolean }
|
||||
// TaskCardEditor stores options as strings, need to convert to object format
|
||||
cleanedCard.answer = card.options.map((opt, idx) => {
|
||||
const letter = String.fromCharCode(65 + idx) // A, B, C, D
|
||||
// If option is a string, convert it
|
||||
if (typeof opt === 'string') {
|
||||
return {
|
||||
answer: letter,
|
||||
text: opt,
|
||||
correct: card.correctAnswer === idx
|
||||
}
|
||||
}
|
||||
// If option is already an object, use its values
|
||||
return {
|
||||
answer: opt.answer || letter,
|
||||
text: opt.text || opt.label || "",
|
||||
correct: opt.correct || opt.answer === card.correctAnswer || false
|
||||
}
|
||||
})
|
||||
} else if (card.subType === 'matching' && (card.correctPairs || card.leftItems || card.rightItems)) {
|
||||
// SENTENCE_PAIRING (type 1): answer = array of { left: "...", right: "..." }
|
||||
// TaskCardEditor stores: leftItems[], rightItems[], correctPairs{leftIdx: rightIdx}
|
||||
// Backend expects: array of {left, right} pairs
|
||||
|
||||
if (Array.isArray(card.correctPairs)) {
|
||||
// Already in correct format (backward compatibility)
|
||||
cleanedCard.answer = card.correctPairs.map(pair => ({
|
||||
left: pair.left || pair.leftText || "",
|
||||
right: pair.right || pair.rightText || ""
|
||||
}))
|
||||
} else if (card.leftItems && card.rightItems && typeof card.correctPairs === 'object') {
|
||||
// Convert from editor format: {leftIdx: rightIdx} -> [{left, right}]
|
||||
cleanedCard.answer = Object.entries(card.correctPairs || {}).map(([leftIdx, rightIdx]) => ({
|
||||
left: card.leftItems[parseInt(leftIdx)] || "",
|
||||
right: card.rightItems[parseInt(rightIdx)] || ""
|
||||
}))
|
||||
} else {
|
||||
cleanedCard.answer = []
|
||||
}
|
||||
} else if (card.subType === 'text' && card.acceptedAnswers) {
|
||||
// OWN_ANSWER (type 2): answer = array of acceptable answer strings
|
||||
cleanedCard.answer = Array.isArray(card.acceptedAnswers)
|
||||
? card.acceptedAnswers
|
||||
: [card.acceptedAnswers]
|
||||
} else if (card.subType === 'truefalse') {
|
||||
// TRUE_FALSE (type 3): answer = "true" or "false"
|
||||
// Use isTrue boolean field from editor, fallback to correctAnswer or answer
|
||||
if (card.isTrue !== undefined) {
|
||||
cleanedCard.answer = card.isTrue ? "true" : "false"
|
||||
} else if (card.correctAnswer !== undefined) {
|
||||
cleanedCard.answer = String(card.correctAnswer)
|
||||
} else if (card.answer !== undefined) {
|
||||
cleanedCard.answer = String(card.answer)
|
||||
} else {
|
||||
cleanedCard.answer = "true" // default
|
||||
}
|
||||
} else if (card.subType === 'closer' && card.correctAnswer !== undefined) {
|
||||
// CLOSER (type 4): answer = { correct: number, percent: number }
|
||||
cleanedCard.answer = {
|
||||
correct: Number(card.correctAnswer),
|
||||
percent: Number(card.percent || 10)
|
||||
}
|
||||
} else if (card.answer !== undefined && card.answer !== null) {
|
||||
// Fallback: use existing answer field
|
||||
cleanedCard.answer = card.answer
|
||||
}
|
||||
|
||||
// Csak LUCK típusú kártyáknál add hozzá a consequence-t
|
||||
// CONSEQUENCE field - only for LUCK cards
|
||||
if (deck.type === 'LUCK' && card.consequence) {
|
||||
cleanedCard.consequence = card.consequence
|
||||
}
|
||||
|
||||
console.log('Card mapping:', { original: card, cleaned: cleanedCard })
|
||||
return cleanedCard
|
||||
})
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ const Card_display = () => {
|
||||
const result = await getDeckById(deckId)
|
||||
if (!mounted) return
|
||||
|
||||
console.log('Loaded deck:', result)
|
||||
setDeck(result)
|
||||
|
||||
// Parse cards from JSON if it's a string
|
||||
@@ -60,8 +59,6 @@ const Card_display = () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Parsed cards:', parsedCards)
|
||||
console.log('First card structure:', parsedCards[0])
|
||||
setCards(parsedCards)
|
||||
} catch (err) {
|
||||
console.error('Failed to load deck', err)
|
||||
@@ -79,8 +76,8 @@ const Card_display = () => {
|
||||
let filteredCards = cards.filter((card) => {
|
||||
if (!search) return true
|
||||
const searchLower = search.toLowerCase()
|
||||
// Check question, statement, and options
|
||||
const questionText = card.question || card.statement || ''
|
||||
// Check text field (or fallback to question/statement for old data)
|
||||
const questionText = card.text || card.question || card.statement || ''
|
||||
const questionMatch = questionText.toLowerCase().includes(searchLower)
|
||||
const answersMatch = Array.isArray(card.options)
|
||||
? card.options.some(opt => opt && opt.toLowerCase().includes(searchLower))
|
||||
@@ -96,12 +93,12 @@ const Card_display = () => {
|
||||
// Keep original order
|
||||
return 0
|
||||
} else if (sortBy === "question-asc") {
|
||||
const aText = a.question || a.statement || ''
|
||||
const bText = b.question || b.statement || ''
|
||||
const aText = a.text || a.question || a.statement || ''
|
||||
const bText = b.text || b.question || b.statement || ''
|
||||
return aText.localeCompare(bText)
|
||||
} else if (sortBy === "question-desc") {
|
||||
const aText = a.question || a.statement || ''
|
||||
const bText = b.question || b.statement || ''
|
||||
const aText = a.text || a.question || a.statement || ''
|
||||
const bText = b.text || b.question || b.statement || ''
|
||||
return bText.localeCompare(aText)
|
||||
} else if (sortBy === "answers-asc") {
|
||||
const aCount = Array.isArray(a.options) ? a.options.length : Array.isArray(a.answers) ? a.answers.length : 0
|
||||
@@ -136,33 +133,26 @@ const Card_display = () => {
|
||||
// Card subtype Hungarian labels - UPDATED based on actual data
|
||||
const cardSubTypeLabels = {
|
||||
// String types (from DeckCreator)
|
||||
"quiz": "Quiz ",
|
||||
"truefalse": "Igaz/Hamis",
|
||||
"multiplechoice": "Feleletválasztós",
|
||||
"text": "Szöveges válasz",
|
||||
"number": "Számos válasz",
|
||||
"order": "Sorbarendezés",
|
||||
"matching": "Párosítás",
|
||||
"fill": "Kiegészítés",
|
||||
"QUESTION": "Kérdés",
|
||||
"LUCK": "Szerencse",
|
||||
"JOKER": "Joker",
|
||||
"joker": "Joker",
|
||||
"luck": "Szerencse",
|
||||
// If backend converts to different numbers, map them:
|
||||
"0": "Igaz/Hamis", // truefalse = 0
|
||||
"1": "Feleletválasztós", // multiplechoice = 1
|
||||
"2": "Szöveges válasz", // text = 2
|
||||
"3": "Igaz/Hamis", // type 3 = truefalse (alternate encoding)
|
||||
"4": "Sorbarendezés", // order = 4
|
||||
"5": "Párosítás", // matching = 5
|
||||
"6": "Kiegészítés", // fill = 6
|
||||
0: "Igaz/Hamis",
|
||||
1: "Feleletválasztós",
|
||||
// Backend CardType enum (numeric):
|
||||
"0": "Quiz", // CardType.QUIZ = 0
|
||||
"1": "Párosítás", // CardType.SENTENCE_PAIRING = 1
|
||||
"2": "Szöveges válasz", // CardType.OWN_ANSWER = 2
|
||||
"3": "Igaz/Hamis", // CardType.TRUE_FALSE = 3
|
||||
"4": "Közelítés", // CardType.CLOSER = 4
|
||||
0: "Quiz",
|
||||
1: "Párosítás",
|
||||
2: "Szöveges válasz",
|
||||
3: "Igaz/Hamis", // type 3 detected
|
||||
4: "Sorbarendezés",
|
||||
5: "Párosítás",
|
||||
6: "Kiegészítés"
|
||||
3: "Igaz/Hamis",
|
||||
4: "Közelítés"
|
||||
}
|
||||
|
||||
const currentDeckType = deck ? (deckTypes[deck.type] || { label: "Ismeretlen", color: "var(--color-success)" }) : null
|
||||
@@ -365,48 +355,51 @@ const Card_display = () => {
|
||||
let answerOptions = []
|
||||
let correctAnswerIndex = card.correctAnswer
|
||||
|
||||
// Normalize subType (can be string or number or undefined)
|
||||
const subType = card.subType ? String(card.subType).toLowerCase() : 'undefined'
|
||||
// Detect card type - prioritize numeric card.type over subType
|
||||
let detectedType = 'undefined'
|
||||
|
||||
// Detect card type by fields if subType is missing
|
||||
let detectedType = subType
|
||||
if (subType === 'undefined' || subType === 'null') {
|
||||
// First check deck type - if deck is JOKER or LUCK type, cards inherit that
|
||||
if (deck.type === 1) {
|
||||
// Deck type 1 = Joker deck
|
||||
// First check deck type - if deck is JOKER or LUCK type, cards inherit that
|
||||
if (deck.type === 1) {
|
||||
// Deck type 1 = Joker deck
|
||||
detectedType = 'joker'
|
||||
} else if (deck.type === 0) {
|
||||
// Deck type 0 = Luck deck
|
||||
detectedType = 'luck'
|
||||
} else if (card.type !== undefined && card.type !== null) {
|
||||
// Check by card.type field (numeric CardType enum)
|
||||
const cardType = typeof card.type === 'string' ? card.type.toLowerCase() : card.type
|
||||
|
||||
if (cardType === 'joker' || card.type === 'JOKER') {
|
||||
detectedType = 'joker'
|
||||
} else if (deck.type === 0) {
|
||||
// Deck type 0 = Luck deck
|
||||
} else if (cardType === 'luck' || card.type === 'LUCK') {
|
||||
detectedType = 'luck'
|
||||
} else if (card.type !== undefined) {
|
||||
// Check by card.type field (string or numeric)
|
||||
const cardType = typeof card.type === 'string' ? card.type.toLowerCase() : card.type
|
||||
|
||||
if (cardType === 'joker' || card.type === 'JOKER') {
|
||||
// Joker card
|
||||
detectedType = 'joker'
|
||||
} else if (cardType === 'luck' || card.type === 'LUCK') {
|
||||
// Luck card
|
||||
detectedType = 'luck'
|
||||
} else if (card.type === 3) {
|
||||
// type 3 = True/False
|
||||
detectedType = 'truefalse'
|
||||
} else if (card.type === 2) {
|
||||
// type 2 = Text answer
|
||||
detectedType = 'text'
|
||||
}
|
||||
} else if (card.leftItems && card.rightItems && card.correctPairs) {
|
||||
// Has leftItems, rightItems AND correctPairs = matching
|
||||
} else if (card.type === 0) {
|
||||
// type 0 = Quiz (multiple choice)
|
||||
detectedType = 'quiz'
|
||||
} else if (card.type === 1) {
|
||||
// type 1 = Matching/Pairing
|
||||
detectedType = 'matching'
|
||||
} else if (card.acceptedAnswers && card.acceptedAnswers.length > 0 && card.acceptedAnswers[0] && card.acceptedAnswers[0].trim()) {
|
||||
// Only detect as text if acceptedAnswers has non-empty values
|
||||
} else if (card.type === 2) {
|
||||
// type 2 = Text answer
|
||||
detectedType = 'text'
|
||||
} else if (card.isTrue !== undefined) {
|
||||
} else if (card.type === 3) {
|
||||
// type 3 = True/False
|
||||
detectedType = 'truefalse'
|
||||
} else if (card.options && Array.isArray(card.options) && card.options.some(opt => opt && opt.trim())) {
|
||||
// Has non-empty options - must be multiple choice
|
||||
detectedType = 'multiplechoice'
|
||||
}
|
||||
} else if (card.subType) {
|
||||
// Fallback to subType if card.type is missing
|
||||
detectedType = String(card.subType).toLowerCase()
|
||||
} else if (card.leftItems && card.rightItems && card.correctPairs) {
|
||||
// Has leftItems, rightItems AND correctPairs = matching
|
||||
detectedType = 'matching'
|
||||
} else if (card.acceptedAnswers && card.acceptedAnswers.length > 0 && card.acceptedAnswers[0] && card.acceptedAnswers[0].trim()) {
|
||||
// Only detect as text if acceptedAnswers has non-empty values
|
||||
detectedType = 'text'
|
||||
} else if (card.isTrue !== undefined) {
|
||||
detectedType = 'truefalse'
|
||||
} else if (card.options && Array.isArray(card.options) && card.options.some(opt => opt && opt.trim())) {
|
||||
// Has non-empty options - must be multiple choice
|
||||
detectedType = 'multiplechoice'
|
||||
}
|
||||
|
||||
// Extract consequence info for JOKER and LUCK cards
|
||||
@@ -431,19 +424,40 @@ const Card_display = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (detectedType === 'truefalse' || detectedType === '0') {
|
||||
if (detectedType === 'truefalse' || detectedType === '3') {
|
||||
// True/False cards
|
||||
answerOptions = ['Igaz', 'Hamis']
|
||||
// correctAnswer: 0 = Igaz, 1 = Hamis (based on user feedback)
|
||||
correctAnswerIndex = card.correctAnswer !== undefined ? card.correctAnswer : (card.isTrue ? 0 : 1)
|
||||
} else if ((detectedType === 'text' || detectedType === '2') && card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
|
||||
// Text-based cards with accepted answers
|
||||
answerOptions = card.acceptedAnswers
|
||||
correctAnswerIndex = -1 // All accepted answers are correct
|
||||
} else if (detectedType === 'matching' || detectedType === '5') {
|
||||
// Matching cards - pairs
|
||||
if (card.leftItems && card.rightItems && card.correctPairs) {
|
||||
// Build pairs from correctPairs object
|
||||
// Parse answer from various sources
|
||||
let isCorrectTrue = false
|
||||
if (card.isTrue !== undefined) {
|
||||
isCorrectTrue = card.isTrue
|
||||
} else if (card.answer !== undefined) {
|
||||
const answerStr = String(card.answer).toLowerCase()
|
||||
isCorrectTrue = answerStr === 'true' || answerStr === '1' || answerStr === 'igaz'
|
||||
} else if (card.correctAnswer !== undefined) {
|
||||
isCorrectTrue = card.correctAnswer === 0 || card.correctAnswer === true || card.correctAnswer === 'true'
|
||||
}
|
||||
correctAnswerIndex = isCorrectTrue ? 0 : 1
|
||||
} else if (detectedType === 'quiz' || detectedType === 'multiplechoice' || detectedType === '0') {
|
||||
// Quiz/Multiple choice - parse from backend answer array or frontend options
|
||||
if (card.answer && Array.isArray(card.answer) && card.answer.length > 0 && typeof card.answer[0] === 'object') {
|
||||
// Backend format: [{answer: "A", text: "...", correct: boolean}]
|
||||
answerOptions = card.answer.map(opt => opt.text || opt.label || '')
|
||||
const correctOption = card.answer.find(opt => opt.correct)
|
||||
correctAnswerIndex = card.answer.indexOf(correctOption)
|
||||
} else if (card.options && Array.isArray(card.options)) {
|
||||
// Frontend format: ["option1", "option2"]
|
||||
answerOptions = card.options.filter(opt => opt && opt.trim())
|
||||
correctAnswerIndex = card.correctAnswer
|
||||
}
|
||||
} else if (detectedType === 'matching' || detectedType === '1') {
|
||||
// Matching cards - parse from backend answer array or frontend fields
|
||||
if (card.answer && Array.isArray(card.answer) && card.answer.length > 0 && typeof card.answer[0] === 'object') {
|
||||
// Backend format: [{left: "...", right: "..."}]
|
||||
answerOptions = card.answer.map(pair => `${pair.left} → ${pair.right}`)
|
||||
correctAnswerIndex = -1 // All pairs are correct
|
||||
} else if (card.leftItems && card.rightItems && card.correctPairs) {
|
||||
// Frontend format with leftItems, rightItems, correctPairs
|
||||
const pairs = []
|
||||
for (const [leftIdx, rightIdx] of Object.entries(card.correctPairs)) {
|
||||
const left = card.leftItems[parseInt(leftIdx)]
|
||||
@@ -455,10 +469,17 @@ const Card_display = () => {
|
||||
answerOptions = pairs
|
||||
correctAnswerIndex = -1 // All pairs are correct
|
||||
}
|
||||
} else if ((detectedType === 'multiplechoice' || detectedType === '1') && card.options && Array.isArray(card.options)) {
|
||||
// Multiple choice - filter out empty options
|
||||
answerOptions = card.options.filter(opt => opt && opt.trim())
|
||||
correctAnswerIndex = card.correctAnswer
|
||||
} else if (detectedType === 'text' || detectedType === '2') {
|
||||
// OWN_ANSWER - parse from backend answer array or frontend acceptedAnswers
|
||||
if (card.answer) {
|
||||
// Backend format: ["answer1", "answer2"] or single value
|
||||
answerOptions = Array.isArray(card.answer) ? card.answer : [card.answer]
|
||||
correctAnswerIndex = -1 // All answers are correct
|
||||
} else if (card.acceptedAnswers && Array.isArray(card.acceptedAnswers)) {
|
||||
// Frontend format
|
||||
answerOptions = card.acceptedAnswers
|
||||
correctAnswerIndex = -1 // All accepted answers are correct
|
||||
}
|
||||
} else if (card.options && Array.isArray(card.options)) {
|
||||
// Other types with options
|
||||
answerOptions = card.options.filter(opt => opt && opt.trim())
|
||||
@@ -682,12 +703,26 @@ const Card_display = () => {
|
||||
<div className="text-[color:var(--color-text-muted)] text-sm font-medium mb-2">
|
||||
Helyes válasz:
|
||||
</div>
|
||||
{detectedType === 'truefalse' || detectedType === '0' ? (
|
||||
{detectedType === 'truefalse' || detectedType === '3' ? (
|
||||
// True/False - show only the correct answer
|
||||
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
|
||||
✓ {card.isTrue ? 'Igaz' : 'Hamis'}
|
||||
</div>
|
||||
) : detectedType === 'matching' || detectedType === '5' ? (
|
||||
(() => {
|
||||
// Determine if correct answer is true
|
||||
let isCorrectTrue = false
|
||||
if (card.isTrue !== undefined) {
|
||||
isCorrectTrue = card.isTrue
|
||||
} else if (card.answer !== undefined) {
|
||||
const answerStr = String(card.answer).toLowerCase()
|
||||
isCorrectTrue = answerStr === 'true' || answerStr === '1' || answerStr === 'igaz'
|
||||
} else if (card.correctAnswer !== undefined) {
|
||||
isCorrectTrue = card.correctAnswer === 0 || card.correctAnswer === true || card.correctAnswer === 'true'
|
||||
}
|
||||
return (
|
||||
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
|
||||
✓ {isCorrectTrue ? 'Igaz' : 'Hamis'}
|
||||
</div>
|
||||
)
|
||||
})()
|
||||
) : detectedType === 'matching' || detectedType === '1' ? (
|
||||
// Matching - show all correct pairs
|
||||
<ul className="space-y-2">
|
||||
{answerOptions.map((pair, idx) => (
|
||||
@@ -699,7 +734,7 @@ const Card_display = () => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (detectedType === 'text' || detectedType === '2') && card.acceptedAnswers && Array.isArray(card.acceptedAnswers) ? (
|
||||
) : (detectedType === 'text' || detectedType === '2') && answerOptions.length > 0 ? (
|
||||
// Text answers - show all accepted answers
|
||||
<ul className="space-y-1">
|
||||
{answerOptions.map((answer, ansIdx) => (
|
||||
@@ -711,8 +746,24 @@ const Card_display = () => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (detectedType === 'quiz' || detectedType === 'multiplechoice' || detectedType === '0') && answerOptions.length > 0 ? (
|
||||
// Quiz/Multiple choice - show all options with correct one highlighted
|
||||
<ul className="space-y-2">
|
||||
{answerOptions.map((option, ansIdx) => (
|
||||
<li
|
||||
key={ansIdx}
|
||||
className={`text-[color:var(--color-text)] text-sm rounded-lg px-3 py-2 border-l-2 font-semibold ${
|
||||
ansIdx === correctAnswerIndex
|
||||
? 'bg-[color:var(--color-success)]/20 border-[color:var(--color-success)]'
|
||||
: 'bg-[color:var(--color-surface)] border-[color:var(--color-surface-selected)]'
|
||||
}`}
|
||||
>
|
||||
{ansIdx === correctAnswerIndex ? '✓ ' : ''}{String.fromCharCode(65 + ansIdx)}. {option}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
// Multiple choice - show only the correct answer
|
||||
// Other types - show only the correct answer
|
||||
correctAnswerIndex !== undefined && correctAnswerIndex !== -1 && answerOptions[correctAnswerIndex] ? (
|
||||
<div className="text-[color:var(--color-text)] text-lg font-bold bg-[color:var(--color-success)]/20 rounded-lg px-4 py-3 border-l-4 border-[color:var(--color-success)]">
|
||||
✓ {answerOptions[correctAnswerIndex]}
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
import { CardType } from "../../constants/CardTypes"
|
||||
import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
|
||||
/**
|
||||
* Draggable item for sentence pairing
|
||||
*/
|
||||
const DraggableItem = ({ id, text, disabled }) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id, disabled })
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`bg-gray-800 border-2 border-purple-500 rounded-lg p-3 text-white ${
|
||||
disabled ? 'cursor-default' : 'cursor-grab active:cursor-grabbing'
|
||||
} ${isDragging ? 'shadow-lg' : ''}`}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* CardDisplayModal - Kártya megjelenítése a játékos számára
|
||||
@@ -11,6 +49,8 @@ import { motion, AnimatePresence } from "framer-motion"
|
||||
* @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)
|
||||
* @param {boolean} props.isMyTurn - Whether this is the active player (can answer) or spectator (read-only)
|
||||
* @param {string} props.submittedAnswer - For spectators, shows what the active player answered
|
||||
*/
|
||||
const CardDisplayModal = ({
|
||||
isOpen,
|
||||
@@ -18,16 +58,22 @@ const CardDisplayModal = ({
|
||||
card,
|
||||
cardType = "QUESTION",
|
||||
onSubmitAnswer,
|
||||
timeLimit = 60
|
||||
timeLimit = 60,
|
||||
isMyTurn = true,
|
||||
submittedAnswer = null
|
||||
}) => {
|
||||
const [playerAnswer, setPlayerAnswer] = useState("")
|
||||
const [selectedOption, setSelectedOption] = useState(null)
|
||||
const [timeLeft, setTimeLeft] = useState(timeLimit)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
||||
// For sentence pairing drag and drop
|
||||
const [rightItems, setRightItems] = useState([])
|
||||
const sensors = useSensors(useSensor(PointerSensor))
|
||||
|
||||
// Timer countdown
|
||||
// Timer countdown (only for active player)
|
||||
useEffect(() => {
|
||||
if (!isOpen || cardType !== "QUESTION") return
|
||||
if (!isOpen || cardType !== "QUESTION" || !isMyTurn) return
|
||||
|
||||
setTimeLeft(timeLimit)
|
||||
const timer = setInterval(() => {
|
||||
@@ -42,7 +88,7 @@ const CardDisplayModal = ({
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [isOpen, timeLimit])
|
||||
}, [isOpen, timeLimit, isMyTurn])
|
||||
|
||||
// Reset state when modal opens
|
||||
useEffect(() => {
|
||||
@@ -50,8 +96,13 @@ const CardDisplayModal = ({
|
||||
setPlayerAnswer("")
|
||||
setSelectedOption(null)
|
||||
setIsProcessing(false)
|
||||
|
||||
// Initialize sentence pairing right items
|
||||
if (card?.sentencePairs && card.sentencePairs.length > 0) {
|
||||
setRightItems(card.sentencePairs.map(p => ({ id: p.id, text: p.right })))
|
||||
}
|
||||
}
|
||||
}, [isOpen])
|
||||
}, [isOpen, card])
|
||||
|
||||
const handleTimeout = () => {
|
||||
if (onSubmitAnswer) {
|
||||
@@ -60,27 +111,49 @@ const CardDisplayModal = ({
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (isProcessing) return
|
||||
if (isProcessing || !isMyTurn) return
|
||||
|
||||
let answer = null
|
||||
|
||||
// Quiz típus - A, B, C, D
|
||||
if (card?.type === 0 || card?.answerOptions) {
|
||||
// Different answer formats based on card type
|
||||
if (card?.type === CardType.SENTENCE_PAIRING && card?.sentencePairs) {
|
||||
// Answer is array of pairs
|
||||
answer = card.sentencePairs.map((leftPair, index) => ({
|
||||
pairId: leftPair.id,
|
||||
leftText: leftPair.left,
|
||||
rightText: rightItems[index].text
|
||||
}))
|
||||
} else if (selectedOption !== null) {
|
||||
answer = selectedOption
|
||||
}
|
||||
// Szöveges válasz
|
||||
else {
|
||||
} else {
|
||||
answer = playerAnswer.trim()
|
||||
}
|
||||
|
||||
if (!answer) return
|
||||
if (!answer || (Array.isArray(answer) && answer.length === 0)) {
|
||||
console.warn('⚠️ No answer provided')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('📝 Submitting answer:', answer)
|
||||
setIsProcessing(true)
|
||||
|
||||
try {
|
||||
await onSubmitAnswer(answer)
|
||||
} catch (error) {
|
||||
console.error("Válasz küldési hiba:", error)
|
||||
console.error("❌ Válasz küldési hiba:", error)
|
||||
setIsProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event
|
||||
|
||||
if (active.id !== over.id) {
|
||||
setRightItems((items) => {
|
||||
const oldIndex = items.findIndex(item => item.id === active.id)
|
||||
const newIndex = items.findIndex(item => item.id === over.id)
|
||||
return arrayMove(items, oldIndex, newIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +170,7 @@ const CardDisplayModal = ({
|
||||
switch (cardType) {
|
||||
case "QUESTION": return "Feladat Kártya"
|
||||
case "LUCK": return "Szerencse Kártya"
|
||||
case "JOKER": return "Joker Kártya"
|
||||
case "JOKER": return "Joker Kártya Feladat"
|
||||
default: return "Kártya"
|
||||
}
|
||||
}
|
||||
@@ -143,7 +216,7 @@ const CardDisplayModal = ({
|
||||
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"
|
||||
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 max-h-[90vh] overflow-y-auto"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className={`bg-gradient-to-r ${getCardBgGradient()} p-6 relative overflow-hidden`}>
|
||||
@@ -154,13 +227,20 @@ const CardDisplayModal = ({
|
||||
<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>
|
||||
<p className="text-white/80 text-sm">
|
||||
{isMyTurn ? "Válaszolj a kérdésre!" : "Néző mód - Várakozás a játékosra"}
|
||||
</p>
|
||||
)}
|
||||
{cardType === "JOKER" && (
|
||||
<p className="text-purple-100 text-sm">
|
||||
{isMyTurn ? "Gamemaster jóváhagyás szükséges" : "Várakozás a Gamemaster döntésére"}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timer - csak QUESTION típusnál */}
|
||||
{cardType === "QUESTION" && (
|
||||
{/* Timer - csak QUESTION típusnál és aktív játékosnál */}
|
||||
{cardType === "QUESTION" && isMyTurn && (
|
||||
<div className="bg-black/30 rounded-lg px-4 py-2">
|
||||
<div className={`text-2xl font-bold ${getTimeColor()}`}>
|
||||
⏱️ {formatTime(timeLeft)}
|
||||
@@ -172,57 +252,261 @@ const CardDisplayModal = ({
|
||||
|
||||
{/* 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>
|
||||
{/* JOKER CARD SPECIFIC LAYOUT */}
|
||||
{cardType === "JOKER" ? (
|
||||
<>
|
||||
{/* 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">{card.playerEmoji || "🎭"}</div>
|
||||
<div>
|
||||
<p className="text-gray-400 text-sm">Játékos</p>
|
||||
<p className="text-white font-semibold text-lg">{card.playerName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Answer Options - Quiz típus (type: 0) */}
|
||||
{cardType === "QUESTION" && (card.type === 0 || card.answerOptions) && (
|
||||
{/* 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">
|
||||
{card.question || "Joker Kártya Feladat"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{card.consequence && (
|
||||
<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">
|
||||
{card.consequence}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Points Info (if available) */}
|
||||
{card.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">
|
||||
{card.points} pont
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm">járható érte</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Waiting for gamemaster message */}
|
||||
{card.waitingForGamemaster && (
|
||||
<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>Várakozás:</strong> A Gamemaster döntése szükséges a folytatáshoz.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* REGULAR CARD LAYOUT */}
|
||||
{/* Spectator Notice */}
|
||||
{!isMyTurn && (
|
||||
<div className="bg-blue-900/30 rounded-xl p-4 border border-blue-500/50 text-center">
|
||||
<p className="text-blue-300 font-semibold">👀 Néző módban vagy</p>
|
||||
<p className="text-gray-300 text-sm mt-1">Várakozás a játékos válaszára...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* QUIZ TYPE - Four Buttons (A, B, C, D) */}
|
||||
{cardType === "QUESTION" && card.type === CardType.QUIZ && card.answerOptions && card.answerOptions.length > 0 && (
|
||||
<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>
|
||||
))}
|
||||
<h3 className="text-purple-300 font-semibold">
|
||||
{isMyTurn ? "Válaszd ki a helyes választ:" : "Válasz lehetőségek:"}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{card.answerOptions.map((option, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => isMyTurn && setSelectedOption(option.answer)}
|
||||
disabled={!isMyTurn || 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 scale-105"
|
||||
: submittedAnswer === option.answer
|
||||
? "bg-blue-600 border-blue-400 text-white"
|
||||
: "bg-gray-800 border-gray-600 text-gray-300"
|
||||
} ${isMyTurn && !isProcessing ? "hover:border-purple-500 cursor-pointer" : "cursor-default"} ${
|
||||
!isMyTurn ? "opacity-75" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl font-bold bg-gray-900/50 rounded-full w-10 h-10 flex items-center justify-center">
|
||||
{option.answer}
|
||||
</span>
|
||||
<span className="flex-1">{option.text}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!isMyTurn && submittedAnswer && (
|
||||
<p className="text-blue-300 text-center text-sm">
|
||||
ℹ️ A játékos választása: <span className="font-bold">{submittedAnswer}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text Input - egyéb kérdés típusok */}
|
||||
{cardType === "QUESTION" && card.type !== 0 && !card.answerOptions && (
|
||||
{/* TRUE/FALSE TYPE - Two Buttons */}
|
||||
{cardType === "QUESTION" && card.type === CardType.TRUE_FALSE && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-purple-300 font-semibold">Írd be a választ:</h3>
|
||||
<h3 className="text-purple-300 font-semibold text-center">
|
||||
{isMyTurn ? "Igaz vagy Hamis?" : "Válasz lehetőségek:"}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => isMyTurn && setSelectedOption("Igaz")}
|
||||
disabled={!isMyTurn || isProcessing}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
selectedOption === "Igaz"
|
||||
? "bg-green-600 border-green-400 text-white scale-105"
|
||||
: submittedAnswer === "Igaz"
|
||||
? "bg-blue-600 border-blue-400 text-white"
|
||||
: "bg-gray-800 border-gray-600 text-gray-300"
|
||||
} ${isMyTurn && !isProcessing ? "hover:border-green-500 cursor-pointer" : "cursor-default"}`}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">✅</div>
|
||||
<div className="text-xl font-bold">IGAZ</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => isMyTurn && setSelectedOption("Hamis")}
|
||||
disabled={!isMyTurn || isProcessing}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
selectedOption === "Hamis"
|
||||
? "bg-red-600 border-red-400 text-white scale-105"
|
||||
: submittedAnswer === "Hamis"
|
||||
? "bg-blue-600 border-blue-400 text-white"
|
||||
: "bg-gray-800 border-gray-600 text-gray-300"
|
||||
} ${isMyTurn && !isProcessing ? "hover:border-red-500 cursor-pointer" : "cursor-default"}`}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl mb-2">❌</div>
|
||||
<div className="text-xl font-bold">HAMIS</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{!isMyTurn && submittedAnswer && (
|
||||
<p className="text-blue-300 text-center text-sm">
|
||||
ℹ️ A játékos választása: <span className="font-bold">{submittedAnswer}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SENTENCE_PAIRING TYPE - Drag and Drop */}
|
||||
{cardType === "QUESTION" && card.type === CardType.SENTENCE_PAIRING && card.sentencePairs && card.sentencePairs.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-purple-300 font-semibold text-center">
|
||||
{isMyTurn ? "Párosítsd a mondatokat! (Húzd a jobb oldali elemeket)" : "Mondatpárosítás:"}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Left column - fixed */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-gray-400 text-sm text-center font-semibold">Bal oldal</p>
|
||||
{card.sentencePairs.map((pair, index) => (
|
||||
<div
|
||||
key={`left-${pair.id}`}
|
||||
className="bg-gray-800 border-2 border-blue-500 rounded-lg p-3 text-white"
|
||||
>
|
||||
{pair.left}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right column - draggable */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-gray-400 text-sm text-center font-semibold">
|
||||
{isMyTurn ? "Jobb oldal (húzható)" : "Jobb oldal"}
|
||||
</p>
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={rightItems.map(item => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
disabled={!isMyTurn}
|
||||
>
|
||||
{rightItems.map((item) => (
|
||||
<DraggableItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
text={item.text}
|
||||
disabled={!isMyTurn}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
{!isMyTurn && (
|
||||
<p className="text-blue-300 text-center text-sm">
|
||||
ℹ️ Várakozás a játékos párosítására...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OWN_ANSWER and CLOSER - Text Input */}
|
||||
{cardType === "QUESTION" && (card.type === CardType.OWN_ANSWER || card.type === CardType.CLOSER) && (
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-purple-300 font-semibold">
|
||||
{isMyTurn ? "Írd be a választ:" : "A játékos válasza:"}
|
||||
</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"
|
||||
value={isMyTurn ? playerAnswer : submittedAnswer || "Várakozás..."}
|
||||
onChange={(e) => isMyTurn && setPlayerAnswer(e.target.value)}
|
||||
onKeyPress={(e) => isMyTurn && e.key === 'Enter' && !isProcessing && playerAnswer.trim() && handleSubmit()}
|
||||
disabled={!isMyTurn || isProcessing}
|
||||
placeholder={card.type === CardType.CLOSER ? "Számot adj meg" : "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 ${
|
||||
!isMyTurn ? "cursor-default" : ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hint (if available) */}
|
||||
{card.hint && (
|
||||
{card.hint && isMyTurn && (
|
||||
<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>
|
||||
@@ -234,11 +518,11 @@ const CardDisplayModal = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Submit Button - csak QUESTION típusnál */}
|
||||
{cardType === "QUESTION" && (
|
||||
{/* Submit Button - csak QUESTION típusnál és aktív játékosnál */}
|
||||
{cardType === "QUESTION" && isMyTurn && (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={isProcessing || (!playerAnswer && !selectedOption)}
|
||||
disabled={isProcessing || (!playerAnswer.trim() && selectedOption === null && card.type !== CardType.SENTENCE_PAIRING)}
|
||||
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
|
||||
@@ -254,8 +538,8 @@ const CardDisplayModal = ({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Close Button - LUCK és JOKER típusnál */}
|
||||
{(cardType === "LUCK" || cardType === "JOKER") && (
|
||||
{/* Close Button - LUCK és JOKER típusnál vagy nézőknél */}
|
||||
{((cardType === "LUCK" || cardType === "JOKER") || !isMyTurn) && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full bg-gradient-to-r from-green-600 to-teal-600 hover:from-green-500 hover:to-teal-500
|
||||
|
||||
@@ -133,7 +133,21 @@ const ConsequenceModal = ({
|
||||
<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>
|
||||
{Array.isArray(correctAnswer) ? (
|
||||
<div className="space-y-1">
|
||||
{correctAnswer
|
||||
.filter(answer => answer.correct)
|
||||
.map((answer, idx) => (
|
||||
<p key={idx} className="text-white font-semibold">
|
||||
{answer.answer ? `${answer.answer}) ` : ''}{answer.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-white font-semibold">
|
||||
{typeof correctAnswer === 'object' ? JSON.stringify(correctAnswer) : correctAnswer}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,14 +50,22 @@ const GameScreen = () => {
|
||||
isConnected,
|
||||
gameState,
|
||||
players: backendPlayers,
|
||||
playerPositions, // NEW: Get dedicated position tracking state
|
||||
boardData: websocketBoardData,
|
||||
currentTurn,
|
||||
currentTurnName,
|
||||
isMyTurn,
|
||||
playerIdentifier,
|
||||
isGamemaster,
|
||||
error,
|
||||
playerDiceRolls,
|
||||
rollDice,
|
||||
approveJoker,
|
||||
rejectJoker,
|
||||
submitAnswer,
|
||||
submitPositionGuess,
|
||||
submitJokerPositionGuess,
|
||||
leaveGame,
|
||||
addEventListener,
|
||||
removeEventListener
|
||||
} = useGameWebSocketContext()
|
||||
@@ -89,6 +97,7 @@ const GameScreen = () => {
|
||||
// Card display modal state
|
||||
const [isCardModalOpen, setIsCardModalOpen] = useState(false)
|
||||
const [currentCard, setCurrentCard] = useState(null)
|
||||
const [isMyCardTurn, setIsMyCardTurn] = useState(false) // Track if I'm the one answering
|
||||
|
||||
// Consequence modal state
|
||||
const [isConsequenceModalOpen, setIsConsequenceModalOpen] = useState(false)
|
||||
@@ -98,6 +107,14 @@ const GameScreen = () => {
|
||||
const [isPredictionModalOpen, setIsPredictionModalOpen] = useState(false)
|
||||
const [currentPredictionData, setCurrentPredictionData] = useState(null)
|
||||
|
||||
// End game modal state
|
||||
const [isEndGameModalOpen, setIsEndGameModalOpen] = useState(false)
|
||||
const [endGameData, setEndGameData] = useState(null)
|
||||
|
||||
// Animation state management
|
||||
const [animatingPlayers, setAnimatingPlayers] = useState({}) // { playerId: { from, to, startTime, duration } }
|
||||
const [animatedPositions, setAnimatedPositions] = useState({}) // { playerId: currentAnimatedPosition }
|
||||
|
||||
// Memoized board dimensions
|
||||
const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => {
|
||||
const { rows, cols, cellSize, cellMargin, rowSpacing } = BOARD_CONFIG
|
||||
@@ -175,61 +192,210 @@ const GameScreen = () => {
|
||||
}
|
||||
}, [boardData, generateWindingPath])
|
||||
|
||||
// Update players from backend - memoized mapping
|
||||
// Update players from backend - memoized mapping (UI properties only, no position)
|
||||
useEffect(() => {
|
||||
if (!backendPlayers?.length) return
|
||||
|
||||
const mappedPlayers = backendPlayers.map((player, index) => ({
|
||||
id: player.playerId || player.id || index,
|
||||
name: player.playerName || player.name || `Player ${index + 1}`,
|
||||
position: player.boardPosition || 0,
|
||||
score: player.score || 0,
|
||||
color: PLAYER_STYLES[index % PLAYER_STYLES.length].color,
|
||||
emoji: PLAYER_STYLES[index % PLAYER_STYLES.length].emoji,
|
||||
isOnline: player.isOnline !== undefined ? player.isOnline : true,
|
||||
isReady: player.isReady || false,
|
||||
}))
|
||||
const mappedPlayers = backendPlayers.map((player, index) => {
|
||||
const playerName = player.playerName || player.name || `Player ${index + 1}`;
|
||||
|
||||
return {
|
||||
id: player.playerId || player.id || index,
|
||||
name: playerName,
|
||||
// NO position stored here - always read from context playerPositions
|
||||
score: player.score || 0,
|
||||
color: PLAYER_STYLES[index % PLAYER_STYLES.length].color,
|
||||
emoji: PLAYER_STYLES[index % PLAYER_STYLES.length].emoji,
|
||||
isOnline: player.isOnline !== undefined ? player.isOnline : true,
|
||||
isReady: player.isReady || false,
|
||||
};
|
||||
})
|
||||
|
||||
setPlayers(mappedPlayers)
|
||||
}, [backendPlayers])
|
||||
|
||||
// Listen to player movement - optimized to update only moved player
|
||||
// Debug: Log playerPositions changes
|
||||
useEffect(() => {
|
||||
console.log('🔍 [GameScreen] playerPositions changed:', JSON.stringify(playerPositions));
|
||||
players.forEach(p => {
|
||||
const pos = playerPositions?.[p.name];
|
||||
console.log(`🔍 [GameScreen] Player ${p.name} position from context: ${pos}`);
|
||||
});
|
||||
}, [playerPositions, players]);
|
||||
|
||||
// Animation loop using requestAnimationFrame
|
||||
useEffect(() => {
|
||||
let animationFrameId
|
||||
|
||||
const animate = () => {
|
||||
const now = Date.now()
|
||||
const updates = {}
|
||||
let hasActiveAnimations = false
|
||||
|
||||
Object.entries(animatingPlayers).forEach(([playerId, animation]) => {
|
||||
const elapsed = now - animation.startTime
|
||||
const progress = Math.min(elapsed / animation.duration, 1)
|
||||
|
||||
// Easing function (ease-in-out)
|
||||
const eased = progress < 0.5
|
||||
? 2 * progress * progress
|
||||
: 1 - Math.pow(-2 * progress + 2, 2) / 2
|
||||
|
||||
// Interpolate position
|
||||
const currentPos = Math.round(
|
||||
animation.from + (animation.to - animation.from) * eased
|
||||
)
|
||||
|
||||
updates[playerId] = currentPos
|
||||
|
||||
if (progress < 1) {
|
||||
hasActiveAnimations = true
|
||||
}
|
||||
// Animation complete - no local position update needed
|
||||
// Position always comes from context playerPositions
|
||||
})
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
setAnimatedPositions(updates)
|
||||
}
|
||||
|
||||
// Clean up completed animations
|
||||
if (!hasActiveAnimations && Object.keys(animatingPlayers).length > 0) {
|
||||
setAnimatingPlayers({})
|
||||
setAnimatedPositions({})
|
||||
} else if (hasActiveAnimations) {
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(animatingPlayers).length > 0) {
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
}
|
||||
}
|
||||
}, [animatingPlayers])
|
||||
|
||||
// Listen to player-moving event to start animation
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handlePlayerMoved = (moveData) => {
|
||||
setPlayers(prev =>
|
||||
prev.map(p =>
|
||||
p.id === moveData.playerId
|
||||
? { ...p, position: moveData.newPosition }
|
||||
: p
|
||||
)
|
||||
)
|
||||
const handlePlayerMoving = (moveData) => {
|
||||
const duration = Math.abs(moveData.toPosition - moveData.fromPosition) * 50 // 50ms per position
|
||||
const clampedDuration = Math.max(500, Math.min(duration, 2000)) // Between 0.5s and 2s
|
||||
|
||||
// Backend sends 1-based positions (1-100), use directly
|
||||
setAnimatingPlayers(prev => ({
|
||||
...prev,
|
||||
[moveData.playerId]: {
|
||||
from: moveData.fromPosition,
|
||||
to: moveData.toPosition,
|
||||
startTime: Date.now(),
|
||||
duration: clampedDuration
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
addEventListener('game:player-moved', handlePlayerMoved)
|
||||
return () => removeEventListener('game:player-moved')
|
||||
addEventListener('game:player-moving', handlePlayerMoving)
|
||||
return () => removeEventListener('game:player-moving')
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Listen to Joker card events (csak Gamemaster számára)
|
||||
// Listen to errors and close modals
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handleGameError = (errorData) => {
|
||||
console.error('❌ Game error:', errorData)
|
||||
// Close any open modals on error
|
||||
setIsCardModalOpen(false)
|
||||
setIsPredictionModalOpen(false)
|
||||
setIsJokerModalOpen(false)
|
||||
// Show error in consequence modal if severe
|
||||
if (errorData.message && errorData.message.includes('card')) {
|
||||
setCurrentConsequence({
|
||||
isCorrect: false,
|
||||
explanation: errorData.message || 'An error occurred',
|
||||
consequenceType: null,
|
||||
consequenceValue: 0
|
||||
})
|
||||
setIsConsequenceModalOpen(true)
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener('game:error', handleGameError)
|
||||
return () => removeEventListener('game:error')
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Listen to player-arrived event (trigger animation for position changes)
|
||||
useEffect(() => {
|
||||
const handlePlayerArrived = (event) => {
|
||||
const arrivalData = event.detail
|
||||
|
||||
// Context manager already updated playerPositions
|
||||
// Just set animation flag for visual animation
|
||||
const player = players.find(p => p.id === arrivalData.playerId || p.name === arrivalData.playerName)
|
||||
if (player) {
|
||||
setAnimatingPlayers(prevAnimating => ({
|
||||
...prevAnimating,
|
||||
[player.id]: true
|
||||
}))
|
||||
|
||||
// Clear animation flag after animation completes (2 seconds)
|
||||
setTimeout(() => {
|
||||
setAnimatingPlayers(prevAnimating => {
|
||||
const newAnimating = { ...prevAnimating }
|
||||
delete newAnimating[player.id]
|
||||
return newAnimating
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to window CustomEvent instead of socket event (context already handles socket)
|
||||
window.addEventListener('game:player-arrived', handlePlayerArrived)
|
||||
return () => window.removeEventListener('game:player-arrived', handlePlayerArrived)
|
||||
}, [players])
|
||||
|
||||
// Listen to Joker card events
|
||||
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)
|
||||
|
||||
if (isGamemaster) {
|
||||
// Gamemaster sees approval modal with approve/deny buttons
|
||||
console.log('👑 Gamemaster látja a jóváhagyási modal-t')
|
||||
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)
|
||||
} else {
|
||||
// Other players see the joker card as a read-only card display
|
||||
console.log('👥 Játékosok látják a joker kártyát (csak olvasás)')
|
||||
setCurrentCard({
|
||||
type: 'JOKER',
|
||||
question: jokerData.jokerCard?.question || jokerData.cardTitle || 'Joker Kártya Feladat',
|
||||
consequence: jokerData.jokerCard?.consequence?.description || jokerData.cardDescription,
|
||||
playerName: jokerData.playerName,
|
||||
playerEmoji: jokerData.playerEmoji || "🎭",
|
||||
isReadOnly: true,
|
||||
waitingForGamemaster: true
|
||||
})
|
||||
setIsCardModalOpen(true)
|
||||
setIsMyCardTurn(false) // Not my turn to answer
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for gamemaster decision request (correct event name per docs)
|
||||
@@ -240,33 +406,57 @@ const GameScreen = () => {
|
||||
removeEventListener('game:joker-drawn')
|
||||
removeEventListener('game:gamemaster-decision-request')
|
||||
}
|
||||
}, [addEventListener, removeEventListener])
|
||||
}, [addEventListener, removeEventListener, isGamemaster])
|
||||
|
||||
// Listen to card drawn events (kártya megjelenítés)
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handleCardDrawn = (cardData) => {
|
||||
console.log('🎴 Kártya húzva:', cardData)
|
||||
// Handle card drawn FOR ME (I need to answer)
|
||||
const handleCardDrawnSelf = (data) => {
|
||||
console.log('🎴 Kártya húzva NEKEM:', data)
|
||||
const cardData = data.cardData || data;
|
||||
console.log('📦 Card data structure:', cardData)
|
||||
setCurrentCard({
|
||||
id: cardData.cardId || cardData.id,
|
||||
type: cardData.cardType || cardData.type,
|
||||
question: cardData.question || cardData.text,
|
||||
answerOptions: cardData.answerOptions || cardData.options || [],
|
||||
id: cardData.cardid || cardData.id,
|
||||
type: cardData.type,
|
||||
question: cardData.question || cardData.text || cardData.statement,
|
||||
answerOptions: cardData.answerOptions || [],
|
||||
sentencePairs: cardData.sentencePairs || [],
|
||||
words: cardData.words || [],
|
||||
acceptableAnswers: cardData.acceptableAnswers || [],
|
||||
correctAnswer: cardData.correctAnswer,
|
||||
hint: cardData.hint,
|
||||
points: cardData.points || 0,
|
||||
timeLimit: cardData.timeLimit || 60
|
||||
timeLimit: data.timeLimit || cardData.timeLimit || 60
|
||||
})
|
||||
setIsMyCardTurn(true) // I need to answer
|
||||
setIsCardModalOpen(true)
|
||||
}
|
||||
|
||||
// Listen for both generic and self-specific events
|
||||
// Handle card drawn by ANOTHER PLAYER (spectator mode)
|
||||
const handleCardDrawn = (data) => {
|
||||
console.log('👀 Kártya húzva más játékos által:', data)
|
||||
const cardData = data.cardData || data;
|
||||
setCurrentCard({
|
||||
id: cardData.cardid || cardData.id,
|
||||
type: cardData.type,
|
||||
question: cardData.question || cardData.text || cardData.statement,
|
||||
answerOptions: cardData.answerOptions || [],
|
||||
sentencePairs: cardData.sentencePairs || [],
|
||||
words: cardData.words || [],
|
||||
timeLimit: data.timeLimit || cardData.timeLimit || 60
|
||||
})
|
||||
setIsMyCardTurn(false) // Spectator mode
|
||||
setIsCardModalOpen(true)
|
||||
}
|
||||
|
||||
addEventListener('game:card-drawn-self', handleCardDrawnSelf)
|
||||
addEventListener('game:card-drawn', handleCardDrawn)
|
||||
addEventListener('game:card-drawn-self', handleCardDrawn)
|
||||
|
||||
return () => {
|
||||
removeEventListener('game:card-drawn')
|
||||
removeEventListener('game:card-drawn-self')
|
||||
removeEventListener('game:card-drawn')
|
||||
}
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
@@ -297,11 +487,30 @@ const GameScreen = () => {
|
||||
const handleLuckConsequence = (luckData) => {
|
||||
console.log('🍀 Szerencse kártya következménye:', luckData)
|
||||
|
||||
// Close card modal if it's open (shouldn't be for luck, but just in case)
|
||||
setIsCardModalOpen(false)
|
||||
|
||||
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!',
|
||||
explanation: luckData.description || luckData.message || 'Szerencse kártya!',
|
||||
playerAnswer: null,
|
||||
correctAnswer: null
|
||||
})
|
||||
setIsConsequenceModalOpen(true)
|
||||
}
|
||||
|
||||
const handleCardResult = (resultData) => {
|
||||
console.log('🎴 Card result (luck):', resultData)
|
||||
// This is for luck cards that use game:card-result event
|
||||
setIsCardModalOpen(false)
|
||||
|
||||
setCurrentConsequence({
|
||||
isCorrect: true,
|
||||
consequenceType: resultData.consequence?.type,
|
||||
consequenceValue: resultData.consequence?.value || 0,
|
||||
explanation: resultData.description || 'Szerencse kártya!',
|
||||
playerAnswer: null,
|
||||
correctAnswer: null
|
||||
})
|
||||
@@ -310,10 +519,12 @@ const GameScreen = () => {
|
||||
|
||||
addEventListener('game:answer-validated', handleAnswerValidated)
|
||||
addEventListener('game:luck-consequence', handleLuckConsequence)
|
||||
addEventListener('game:card-result', handleCardResult)
|
||||
|
||||
return () => {
|
||||
removeEventListener('game:answer-validated')
|
||||
removeEventListener('game:luck-consequence')
|
||||
removeEventListener('game:card-result')
|
||||
}
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
@@ -334,16 +545,105 @@ const GameScreen = () => {
|
||||
setIsPredictionModalOpen(true)
|
||||
}
|
||||
|
||||
const handleJokerPositionGuessRequest = (predictionData) => {
|
||||
console.log('🃏🎯 Joker után 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.message || 'Tippeld meg a végső pozíciódat joker után!',
|
||||
timeLimit: predictionData.timeLimit || 30,
|
||||
isJoker: true // Mark this as joker guess
|
||||
})
|
||||
setIsPredictionModalOpen(true)
|
||||
}
|
||||
|
||||
const handleGuessResult = (resultData) => {
|
||||
console.log('✅ Tippelés eredménye:', resultData)
|
||||
// Close prediction modal
|
||||
setIsPredictionModalOpen(false)
|
||||
|
||||
// Backend already emits game:player-arrived before this event
|
||||
// Position is handled by context manager, no need for pendingPositionUpdate
|
||||
setCurrentConsequence({
|
||||
isCorrect: resultData.guessCorrect,
|
||||
playerAnswer: resultData.guessedPosition,
|
||||
correctAnswer: resultData.actualPosition,
|
||||
explanation: resultData.message,
|
||||
consequenceType: resultData.penaltyApplied ? 'penalty' : 'success',
|
||||
consequenceValue: resultData.penaltyApplied ? -2 : 0
|
||||
})
|
||||
setIsConsequenceModalOpen(true)
|
||||
}
|
||||
|
||||
const handleJokerComplete = (resultData) => {
|
||||
console.log('🃏✅ Joker tippelés eredménye:', resultData)
|
||||
// Close prediction modal
|
||||
setIsPredictionModalOpen(false)
|
||||
|
||||
// Backend already emits game:player-arrived before this event (if moved)
|
||||
// Position is handled by context manager, no need for pendingPositionUpdate
|
||||
setCurrentConsequence({
|
||||
isCorrect: resultData.guessCorrect,
|
||||
playerAnswer: resultData.guessedPosition,
|
||||
correctAnswer: resultData.actualPosition,
|
||||
explanation: resultData.message,
|
||||
consequenceType: resultData.penaltyApplied ? 'penalty' : (resultData.moved ? 'success' : 'neutral'),
|
||||
consequenceValue: resultData.penaltyApplied ? -2 : 0
|
||||
})
|
||||
setIsConsequenceModalOpen(true)
|
||||
}
|
||||
|
||||
addEventListener('game:position-guess-request', handlePositionGuessRequest)
|
||||
return () => removeEventListener('game:position-guess-request')
|
||||
addEventListener('game:joker-position-guess-request', handleJokerPositionGuessRequest)
|
||||
addEventListener('game:guess-result', handleGuessResult)
|
||||
addEventListener('game:joker-complete', handleJokerComplete)
|
||||
return () => {
|
||||
removeEventListener('game:position-guess-request')
|
||||
removeEventListener('game:joker-position-guess-request')
|
||||
removeEventListener('game:guess-result')
|
||||
removeEventListener('game:joker-complete')
|
||||
}
|
||||
}, [addEventListener, removeEventListener])
|
||||
|
||||
// Listen to game end event
|
||||
useEffect(() => {
|
||||
if (!addEventListener) return
|
||||
|
||||
const handleGameEnded = (endData) => {
|
||||
console.log('🏆 Játék vége:', endData)
|
||||
setEndGameData({
|
||||
winnerName: endData.winnerName,
|
||||
winnerId: endData.winner,
|
||||
finalPositions: endData.finalPositions || [],
|
||||
message: endData.message
|
||||
})
|
||||
setIsEndGameModalOpen(true)
|
||||
}
|
||||
|
||||
const handleGamemasterDecision = (decisionData) => {
|
||||
console.log('👑 Gamemaster döntés:', decisionData)
|
||||
// Close joker card modal for non-gamemaster players when decision is made
|
||||
if (!isGamemaster) {
|
||||
setIsCardModalOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener('game:ended', handleGameEnded)
|
||||
addEventListener('game:gamemaster-decision-result', handleGamemasterDecision)
|
||||
return () => {
|
||||
removeEventListener('game:ended')
|
||||
removeEventListener('game:gamemaster-decision-result')
|
||||
}
|
||||
}, [addEventListener, removeEventListener, isGamemaster])
|
||||
|
||||
// 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)
|
||||
// WebSocket üzenet a backend felé - csak requestId kell
|
||||
approveJoker(jokerRequest.requestId)
|
||||
|
||||
// Modal bezárása
|
||||
setIsJokerModalOpen(false)
|
||||
@@ -353,8 +653,8 @@ const GameScreen = () => {
|
||||
const handleRejectJoker = useCallback(async (jokerRequest) => {
|
||||
console.log('❌ Joker feladat elutasítva:', jokerRequest)
|
||||
|
||||
// WebSocket üzenet a backend felé
|
||||
rejectJoker(jokerRequest.playerId, jokerRequest.cardId, jokerRequest.requestId)
|
||||
// WebSocket üzenet a backend felé - csak requestId kell
|
||||
rejectJoker(jokerRequest.requestId, 'Joker rejected by gamemaster')
|
||||
|
||||
// Modal bezárása
|
||||
setIsJokerModalOpen(false)
|
||||
@@ -362,31 +662,45 @@ const GameScreen = () => {
|
||||
|
||||
// Kártya válasz beküldése
|
||||
const handleSubmitAnswer = useCallback((answer) => {
|
||||
console.log('📝 Válasz beküldve:', answer)
|
||||
console.log('📝 Válasz beküldve:', answer, 'Card ID:', currentCard?.id)
|
||||
|
||||
// WebSocket emit a backend felé
|
||||
if (currentCard?.id) {
|
||||
submitAnswer(currentCard.id, answer)
|
||||
}
|
||||
// WebSocket emit a backend felé - uses context method with card ID
|
||||
submitAnswer(answer, currentCard?.id)
|
||||
|
||||
// A consequence modal automatikusan megnyílik a 'game:answer-validated' event hatására
|
||||
}, [currentCard?.id, submitAnswer])
|
||||
}, [submitAnswer, currentCard])
|
||||
|
||||
// Consequence modal bezárása
|
||||
const handleConsequenceClose = useCallback(() => {
|
||||
// Position updates are handled by game:player-arrived event in context
|
||||
// No need to manually update positions here
|
||||
setIsConsequenceModalOpen(false)
|
||||
}, [])
|
||||
|
||||
// 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)
|
||||
// WebSocket emit a backend felé (különböző event joker-nél)
|
||||
if (currentPredictionData?.isJoker) {
|
||||
console.log('🃏 Joker pozíció tipp beküldése')
|
||||
submitJokerPositionGuess(predictedPosition)
|
||||
} else {
|
||||
submitPositionGuess(predictedPosition)
|
||||
}
|
||||
|
||||
// Modal bezárása
|
||||
setIsPredictionModalOpen(false)
|
||||
}, [submitPositionGuess])
|
||||
}, [submitPositionGuess, submitJokerPositionGuess, currentPredictionData])
|
||||
|
||||
// Sorted players - memoized
|
||||
// Sorted players - memoized (sort by context position)
|
||||
const sortedPlayers = useMemo(
|
||||
() => [...players].sort((a, b) => b.position - a.position),
|
||||
[players]
|
||||
() => [...players].sort((a, b) => {
|
||||
const posA = playerPositions?.[a.name] || 0
|
||||
const posB = playerPositions?.[b.name] || 0
|
||||
return posB - posA
|
||||
}),
|
||||
[players, playerPositions]
|
||||
)
|
||||
|
||||
// Handle dice roll
|
||||
@@ -437,35 +751,45 @@ const GameScreen = () => {
|
||||
return (
|
||||
<div className="p-4 bg-gradient-to-br from-gray-900 via-gray-800 to-teal-900 min-h-screen flex items-center justify-center">
|
||||
<div className="w-full">
|
||||
{/* Connection Status Indicator */}
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<div className={`px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 ${
|
||||
isConnected
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-red-600 text-white'
|
||||
}`}>
|
||||
<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 && !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 */}
|
||||
{/* Exit Game Button - Top Right Corner */}
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (window.confirm('Biztosan ki szeretnél lépni a játékból?')) {
|
||||
leaveGame()
|
||||
window.location.href = '/'
|
||||
}
|
||||
}}
|
||||
className="bg-red-600 hover:bg-red-700 text-white font-semibold py-2 px-4 rounded-lg shadow-lg transition-colors duration-200 flex items-center gap-2 cursor-pointer"
|
||||
title="Kilépés a játékból"
|
||||
>
|
||||
🚪 Kilépés
|
||||
</button>
|
||||
</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">
|
||||
<div className="text-teal-300 text-sm font-medium">
|
||||
🎮 Játék kód: <span className="font-bold text-white">{gameState.gameCode || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
isConnected ? 'bg-green-400 animate-pulse' : 'bg-red-400'
|
||||
}`}></div>
|
||||
<span className={`text-xs ${
|
||||
isConnected ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{isConnected ? 'Csatlakozva' : 'Kapcsolódás...'}
|
||||
</span>
|
||||
</div>
|
||||
{currentTurn && (
|
||||
<div className="text-gray-400 text-xs mt-1">
|
||||
🎯 Köron: <span className="text-white">{players.find(p => p.id === currentTurn)?.name || 'Betöltés...'}</span>
|
||||
🎯 Köron: <span className={`font-bold ${isMyTurn ? 'text-green-400' : 'text-white'}`}>
|
||||
{currentTurnName || players.find(p => p.id === currentTurn || p.playerName === currentTurn || p.name === currentTurn)?.name || currentTurn || 'Betöltés...'}
|
||||
</span>
|
||||
{isMyTurn && <span className="ml-2 text-green-400 animate-pulse">← Te vagy!</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -513,18 +837,28 @@ const GameScreen = () => {
|
||||
))}
|
||||
|
||||
{/* Player tokens */}
|
||||
{players.map((player) => (
|
||||
<div
|
||||
key={player.id}
|
||||
className={`absolute w-6 h-6 ${player.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white text-xs font-bold z-10 animate-bounce`}
|
||||
style={{
|
||||
...getPlayerPosition(player.position),
|
||||
transform: "translate(17px, 17px)",
|
||||
}}
|
||||
>
|
||||
{player.emoji}
|
||||
</div>
|
||||
))}
|
||||
{players.map((player) => {
|
||||
// ALWAYS read position from context playerPositions, not local state
|
||||
// Backend uses 1-based indexing (1-100)
|
||||
const contextPosition = playerPositions?.[player.name] ?? 1
|
||||
// Use animated position if player is currently animating, otherwise use context position
|
||||
const displayPosition = animatedPositions[player.id] ?? contextPosition
|
||||
const isAnimating = animatingPlayers[player.id] !== undefined
|
||||
|
||||
return (
|
||||
<div
|
||||
key={player.id}
|
||||
className={`absolute w-6 h-6 ${player.color} rounded-full border-2 border-white shadow-lg flex items-center justify-center text-white text-xs font-bold z-10 transition-transform ${isAnimating ? 'scale-110' : ''}`}
|
||||
style={{
|
||||
...getPlayerPosition(displayPosition),
|
||||
transform: "translate(17px, 17px)",
|
||||
transition: isAnimating ? 'none' : 'all 0.3s ease'
|
||||
}}
|
||||
>
|
||||
{player.emoji}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -591,7 +925,7 @@ const GameScreen = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
Pozíció: {player.position} • Pontszám: {player.score}
|
||||
Pozíció: {playerPositions?.[player.name] ?? 1} • Pontszám: {player.score}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,11 +935,23 @@ const GameScreen = () => {
|
||||
{/* Dice Container */}
|
||||
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center">
|
||||
<h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
|
||||
<p className="text-gray-300 text-sm mb-4">
|
||||
Kattints a kockára dobáshoz!
|
||||
</p>
|
||||
|
||||
<Dice onRoll={handleDiceRoll} />
|
||||
{isMyTurn ? (
|
||||
<>
|
||||
<p className="text-green-400 text-sm mb-4 font-bold animate-pulse">
|
||||
🎯 A te köröd! Kattints a kockára dobáshoz!
|
||||
</p>
|
||||
<Dice onRoll={handleDiceRoll} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-gray-500 text-sm mb-4">
|
||||
⏳ Várd meg a köröd...
|
||||
</p>
|
||||
<div className="opacity-50 pointer-events-none">
|
||||
<Dice onRoll={handleDiceRoll} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Connection warning */}
|
||||
{!isConnected && (
|
||||
@@ -624,7 +970,9 @@ const GameScreen = () => {
|
||||
<div>🎮 Game Code: {gameState?.gameCode || 'N/A'}</div>
|
||||
<div>👥 Players: {backendPlayers?.length || 0}</div>
|
||||
<div>🎲 Board Fields: {boardData?.fields?.length || 0}</div>
|
||||
<div>🏁 Current Turn: {currentTurn || 'N/A'}</div>
|
||||
<div>🏁 Current Turn: {currentTurnName || currentTurn || 'N/A'}</div>
|
||||
<div>🆔 My ID: {playerIdentifier || 'N/A'}</div>
|
||||
<div>✅ Is My Turn: {isMyTurn ? 'YES' : 'NO'}</div>
|
||||
{/* <div>🔑 Token: {gameToken ? '✅' : '❌'}</div> */}
|
||||
</div>
|
||||
</div>
|
||||
@@ -650,20 +998,85 @@ const GameScreen = () => {
|
||||
onClose={() => setIsCardModalOpen(false)}
|
||||
card={currentCard}
|
||||
onSubmitAnswer={handleSubmitAnswer}
|
||||
isMyTurn={isMyCardTurn}
|
||||
/>
|
||||
|
||||
{/* Consequence Modal - következmények megjelenítése */}
|
||||
<ConsequenceModal
|
||||
isOpen={isConsequenceModalOpen}
|
||||
onClose={() => setIsConsequenceModalOpen(false)}
|
||||
consequence={currentConsequence}
|
||||
onClose={handleConsequenceClose}
|
||||
isCorrect={currentConsequence?.isCorrect}
|
||||
consequenceType={currentConsequence?.consequenceType}
|
||||
consequenceValue={currentConsequence?.consequenceValue}
|
||||
playerAnswer={currentConsequence?.playerAnswer}
|
||||
correctAnswer={currentConsequence?.correctAnswer}
|
||||
explanation={currentConsequence?.explanation}
|
||||
/>
|
||||
|
||||
{/* End Game Modal */}
|
||||
{isEndGameModalOpen && endGameData && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/90">
|
||||
<div className="relative bg-gradient-to-br from-yellow-900 via-yellow-800 to-yellow-900 rounded-2xl shadow-2xl border-4 border-yellow-500 max-w-2xl w-full p-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-pulse" />
|
||||
|
||||
<div className="relative">
|
||||
<div className="text-8xl mb-4 animate-bounce">🏆</div>
|
||||
<h1 className="text-5xl font-bold text-white mb-4">
|
||||
Játék vége!
|
||||
</h1>
|
||||
<div className="bg-black/30 rounded-xl p-6 mb-6">
|
||||
<p className="text-6xl font-bold text-yellow-300 mb-2">
|
||||
{endGameData.winnerName}
|
||||
</p>
|
||||
<p className="text-2xl text-yellow-100">
|
||||
🎉 Nyert! 🎉
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{endGameData.finalPositions && endGameData.finalPositions.length > 0 && (
|
||||
<div className="bg-black/30 rounded-xl p-4 mb-6">
|
||||
<h3 className="text-xl font-semibold text-yellow-300 mb-3">Végső állás:</h3>
|
||||
<div className="space-y-2">
|
||||
{endGameData.finalPositions
|
||||
.sort((a, b) => b.boardPosition - a.boardPosition)
|
||||
.map((player, index) => (
|
||||
<div key={player.playerId} className="flex items-center justify-between bg-yellow-900/30 rounded-lg p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">
|
||||
{index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎮'}
|
||||
</span>
|
||||
<span className="text-white font-semibold">{player.playerName}</span>
|
||||
</div>
|
||||
<span className="text-yellow-300 font-bold">Pozíció: {player.boardPosition}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className="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-8 rounded-xl shadow-lg transform transition-all duration-200 hover:scale-105 text-xl"
|
||||
>
|
||||
Vissza a főoldalra
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step Prediction Modal - pozíció tippelés */}
|
||||
<StepPredictionModal
|
||||
isOpen={isPredictionModalOpen}
|
||||
onClose={() => setIsPredictionModalOpen(false)}
|
||||
predictionData={currentPredictionData}
|
||||
currentPosition={currentPredictionData?.currentPosition || 0}
|
||||
diceRoll={currentPredictionData?.diceRoll || 0}
|
||||
fieldStepValue={currentPredictionData?.fieldStepValue || 0}
|
||||
patternModifier={currentPredictionData?.patternModifier || 0}
|
||||
cardText={currentPredictionData?.cardText || "Tippeld meg, melyik pozícióra fogsz lépni!"}
|
||||
hints={currentPredictionData?.hints || []}
|
||||
timeLimit={currentPredictionData?.timeLimit || 30}
|
||||
isJoker={currentPredictionData?.isJoker || false}
|
||||
onSubmitPrediction={handleSubmitPrediction}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react"
|
||||
import React, { useState, useEffect } from "react"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
/**
|
||||
@@ -12,6 +12,7 @@ import { motion, AnimatePresence } from "framer-motion"
|
||||
* @param {Function} props.onReject - Elutasítás callback
|
||||
* @param {string} props.playerName - Játékos neve
|
||||
* @param {string} props.playerEmoji - Játékos emoji
|
||||
* @param {number} props.timeLimit - Időkorlát másodpercben (default: 120)
|
||||
*/
|
||||
const JokerApprovalModal = ({
|
||||
isOpen,
|
||||
@@ -20,9 +21,49 @@ const JokerApprovalModal = ({
|
||||
onApprove,
|
||||
onReject,
|
||||
playerName,
|
||||
playerEmoji = "🎭"
|
||||
playerEmoji = "🎭",
|
||||
timeLimit = 120
|
||||
}) => {
|
||||
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 = () => {
|
||||
// Auto-reject on timeout
|
||||
if (onReject && !isProcessing) {
|
||||
handleReject()
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const getTimeColor = () => {
|
||||
if (timeLeft > 60) return "text-green-400"
|
||||
if (timeLeft > 30) return "text-yellow-400"
|
||||
return "text-red-400 animate-pulse"
|
||||
}
|
||||
|
||||
const handleApprove = async () => {
|
||||
setIsProcessing(true)
|
||||
@@ -82,13 +123,21 @@ const JokerApprovalModal = ({
|
||||
<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 className="flex items-center gap-3">
|
||||
{/* Timer */}
|
||||
<div className="bg-black/30 rounded-lg px-4 py-2">
|
||||
<div className={`text-2xl font-bold ${getTimeColor()}`}>
|
||||
⏱️ {formatTime(timeLeft)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/80 hover:text-white transition-colors text-2xl"
|
||||
disabled={isProcessing}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ const Lobby = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
|
||||
const [user, setUser] = useRequireAuth()
|
||||
const [user, setUser] = useRequireAuth({ redirect: false })
|
||||
|
||||
// Get game code from location state or WebSocket
|
||||
const gameCodeFromState = location.state?.gameCode
|
||||
@@ -75,6 +75,7 @@ const Lobby = () => {
|
||||
|
||||
// Auto-navigate when game starts
|
||||
useEffect(() => {
|
||||
console.log("🎮 Lobby: gameStarted changed to:", gameStarted)
|
||||
if (gameStarted) {
|
||||
console.log("🎮 Game started, navigating to /game")
|
||||
goGame()
|
||||
|
||||
@@ -42,7 +42,9 @@ const StepPredictionModal = ({
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
// Reset time when modal opens
|
||||
setTimeLeft(timeLimit)
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft(prev => {
|
||||
if (prev <= 1) {
|
||||
@@ -55,10 +57,10 @@ const StepPredictionModal = ({
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [isOpen, timeLimit])
|
||||
}, [isOpen]) // Remove timeLimit from dependencies to prevent timer reset
|
||||
|
||||
const handleTimeout = () => {
|
||||
if (onSubmitPrediction) {
|
||||
if (onSubmitPrediction && !isProcessing) {
|
||||
onSubmitPrediction(null) // null = timeout
|
||||
}
|
||||
}
|
||||
@@ -89,7 +91,9 @@ const StepPredictionModal = ({
|
||||
}, [isOpen])
|
||||
|
||||
// Számított végső pozíció (helyes válasz)
|
||||
const calculatedPosition = currentPosition + diceRoll + fieldStepValue + patternModifier
|
||||
// Backend formula: currentPosition + (stepValue × dice) + patternModifier
|
||||
const movement = fieldStepValue * diceRoll
|
||||
const calculatedPosition = currentPosition + movement + patternModifier
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
return `0:${seconds.toString().padStart(2, '0')}`
|
||||
@@ -158,40 +162,58 @@ const StepPredictionModal = ({
|
||||
</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>
|
||||
{/* Step-by-Step Calculation Helper */}
|
||||
<div className="bg-gradient-to-br from-blue-900/30 to-purple-900/30 rounded-xl p-4 border border-blue-500/30">
|
||||
<h3 className="text-blue-300 font-semibold mb-3 text-center">🧮 Számítási Adatok</h3>
|
||||
|
||||
{/* Visual calculation steps */}
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="flex items-center justify-between bg-black/40 rounded-lg p-3">
|
||||
<span className="text-gray-300">Kezdő pozíció:</span>
|
||||
<span className="text-white font-bold text-xl">{currentPosition}</span>
|
||||
</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 className="flex items-center justify-between bg-black/40 rounded-lg p-3">
|
||||
<span className="text-gray-300">Mező érték:</span>
|
||||
<span className="text-yellow-400 font-bold text-xl">{fieldStepValue}</span>
|
||||
</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 className="flex items-center justify-between bg-black/40 rounded-lg p-3">
|
||||
<span className="text-gray-300">Dobás:</span>
|
||||
<span className="text-blue-400 font-bold text-xl">{diceRoll}</span>
|
||||
</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} = ?
|
||||
|
||||
{/* Pattern Modifier Info Box */}
|
||||
<div className="bg-indigo-900/30 rounded-lg p-4 border border-indigo-500/30">
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<span className="text-2xl">ℹ️</span>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-indigo-300 font-semibold mb-1">Zóna módosító szabályok:</h4>
|
||||
<ul className="text-gray-300 text-sm space-y-1">
|
||||
<li>• <strong>0-ra végződik</strong> (10, 20...): nincs módosító</li>
|
||||
<li>• <strong>5-re végződik</strong> (15, 25...): ±3 módosító</li>
|
||||
<li>• <strong>3-mal osztható</strong> (9, 12, 21...): ±2 módosító</li>
|
||||
<li>• <strong>Páratlan</strong> (1, 7, 11...): ±1 módosító</li>
|
||||
<li>• <strong>Egyéb páros</strong>: nincs módosító</li>
|
||||
</ul>
|
||||
<p className="text-indigo-200 text-xs mt-2">A módosító előjele a mező típusától függ (+ pozitív, - negatív mező)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formula hint without answer */}
|
||||
<div className="bg-yellow-900/20 rounded-lg p-3 border border-yellow-500/30 mt-3">
|
||||
<p className="text-yellow-200 text-xs text-center">
|
||||
💡 <strong>Képlet:</strong> Kezdő + (Mező × Dobás) + Zóna módosító
|
||||
</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 className="text-yellow-300 font-semibold text-center">
|
||||
✍️ Írd be a tippelt pozíciót:
|
||||
</h3>
|
||||
<input
|
||||
type="number"
|
||||
@@ -199,19 +221,19 @@ const StepPredictionModal = ({
|
||||
onChange={(e) => setPrediction(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSubmit()}
|
||||
disabled={isProcessing}
|
||||
placeholder="Pl: 28"
|
||||
placeholder={`Pl.: ${Math.floor(currentPosition + diceRoll * 1.5)}`}
|
||||
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}
|
||||
min={0}
|
||||
max={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Prediction Info */}
|
||||
{/* Show entered prediction */}
|
||||
{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"
|
||||
className="bg-yellow-900/20 rounded-xl p-3 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ó
|
||||
|
||||
@@ -17,30 +17,21 @@ export default function Home() {
|
||||
const [isJoining, setIsJoining] = useState(false)
|
||||
|
||||
// Join game handler - csatlakozás játékhoz kóddal
|
||||
const handleJoinGame = async (code) => {
|
||||
if (!user) {
|
||||
alert('Kérlek először jelentkezz be!')
|
||||
goLogin()
|
||||
return
|
||||
const handleJoinGame = async (code, playerName) => {
|
||||
// playerName is now passed from PlayMenu (either logged in user or guest name)
|
||||
if (!playerName) {
|
||||
alert('Név megadása kötelező a játékhoz való csatlakozáshoz!');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== JOIN GAME DEBUG ===')
|
||||
console.log('Current user:', user)
|
||||
console.log('Game code:', code)
|
||||
console.log('LocalStorage username:', localStorage.getItem('username'))
|
||||
console.log('LocalStorage authLevel:', localStorage.getItem('authLevel'))
|
||||
console.log('======================')
|
||||
|
||||
setIsJoining(true)
|
||||
try {
|
||||
const joinData = {
|
||||
gameCode: code.toUpperCase(),
|
||||
playerName: user || 'Player',
|
||||
playerName: playerName,
|
||||
}
|
||||
|
||||
console.log('Sending join request with:', joinData)
|
||||
const response = await joinGame(joinData)
|
||||
console.log('Joined game:', response)
|
||||
|
||||
// Backend returns game object directly
|
||||
if (response.gameToken) {
|
||||
@@ -51,8 +42,6 @@ export default function Home() {
|
||||
} catch (err) {
|
||||
const errorMsg = err.response?.data?.error || err.response?.data?.message || 'Nem sikerült csatlakozni a játékhoz'
|
||||
alert(errorMsg)
|
||||
console.error('Join game error:', err)
|
||||
console.error('Error details:', err.response?.data)
|
||||
} finally {
|
||||
setIsJoining(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user