csatlakozas-mukodesdemodemodemo (HIVJ FEL DONAT EMIATT) #94

Merged
Donat merged 1 commits from zsola into main 2025-11-06 20:32:29 +01:00
11 changed files with 1251 additions and 145 deletions
Showing only changes of commit 2b1217192c - Show all commits
+128 -1
View File
@@ -16,6 +16,7 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.7" "tailwindcss": "^4.1.7"
}, },
"devDependencies": { "devDependencies": {
@@ -1259,6 +1260,11 @@
"win32" "win32"
] ]
}, },
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.1.7", "version": "4.1.7",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.7.tgz",
@@ -1957,6 +1963,42 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.1", "version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -3097,7 +3139,6 @@
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
@@ -3478,6 +3519,64 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3729,6 +3828,34 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+1
View File
@@ -18,6 +18,7 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5", "react-toastify": "^11.0.5",
"socket.io-client": "^4.8.1",
"tailwindcss": "^4.1.7" "tailwindcss": "^4.1.7"
}, },
"devDependencies": { "devDependencies": {
+2
View File
@@ -14,6 +14,7 @@ import CompanyHub from "./pages/Contacts/Contacts"
import About from "./pages/About/About" import About from "./pages/About/About"
import ScrollToTop from "./components/ScrollToTop" import ScrollToTop from "./components/ScrollToTop"
import GameScreen from "./pages/Game/GameScreen" import GameScreen from "./pages/Game/GameScreen"
import GameTest from "./pages/Game/GameTest"
import Reports from "./pages/Report/Reports" import Reports from "./pages/Report/Reports"
import Lobby from "./pages/Game/Lobby" import Lobby from "./pages/Game/Lobby"
import ProfileCard from "./components/Userdetails/Userdetails" import ProfileCard from "./components/Userdetails/Userdetails"
@@ -68,6 +69,7 @@ function App() {
<Route path="/deck-creator" element={<DeckCreator />} /> <Route path="/deck-creator" element={<DeckCreator />} />
<Route path="/deck-creator/:deckId" element={<DeckCreator />} /> <Route path="/deck-creator/:deckId" element={<DeckCreator />} />
<Route path="/game" element={<GameScreen />} /> <Route path="/game" element={<GameScreen />} />
<Route path="/game-test" element={<GameTest />} />
{/* <Route path="/contacts" element={<CompanyHub />} /> */} {/* <Route path="/contacts" element={<CompanyHub />} /> */}
<Route path="/report" element={<Reports />} /> <Route path="/report" element={<Reports />} />
<Route path="/choosedeck" element={<ChooseDeck />} /> <Route path="/choosedeck" element={<ChooseDeck />} />
+80
View File
@@ -0,0 +1,80 @@
import { apiClient } from './userApi';
/**
* Create a new game
* @param {Object} gameData - Game creation data
* @param {string[]} gameData.deckids - Array of deck UUIDs
* @param {number} gameData.maxplayers - Maximum players (2-8)
* @param {number} gameData.logintype - 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
* @returns {Promise<Object>} Game data with gameCode
*/
export const createGame = async (gameData) => {
try {
const response = await apiClient.post('/games/start', gameData);
return response.data;
} catch (error) {
console.error('Error creating game:', error);
throw error;
}
};
/**
* Join an existing game
* @param {Object} joinData - Join game data
* @param {string} joinData.gameCode - 6-character game code
* @param {string} [joinData.playerName] - Player name (required for public games)
* @returns {Promise<Object>} Game data with gameToken
*/
export const joinGame = async (joinData) => {
try {
const response = await apiClient.post('/games/join', joinData);
return response.data;
} catch (error) {
console.error('Error joining game:', error);
console.error('Join game error response:', error.response?.data);
throw error;
}
};
/**
* Start the game (gamemaster only)
* @param {string} gameId - Game UUID
* @returns {Promise<Object>} Game data with board
*/
export const startGame = async (gameId) => {
try {
const response = await apiClient.post(`/games/${gameId}/start`);
return response.data;
} catch (error) {
console.error('Error starting game:', error);
throw error;
}
};
/**
* Get user's games
* @returns {Promise<Array>} Array of games
*/
export const getMyGames = async () => {
try {
const response = await apiClient.get('/games/my-games');
return response.data;
} catch (error) {
console.error('Error fetching games:', error);
throw error;
}
};
/**
* Get active public games
* @returns {Promise<Array>} Array of active games
*/
export const getActiveGames = async () => {
try {
const response = await apiClient.get('/games/active');
return response.data;
} catch (error) {
console.error('Error fetching active games:', error);
throw error;
}
};
@@ -0,0 +1,268 @@
import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { io } from 'socket.io-client';
import { API_CONFIG } from '../api/userApi';
const isDev = import.meta.env.DEV;
const log = (...args) => isDev && console.log(...args);
const warn = (...args) => isDev && console.warn(...args);
const error = (...args) => console.error(...args);
/**
* Optimized WebSocket hook for game connection
* @param {string} gameToken - JWT token from game join
* @returns {Object} WebSocket state and methods
*/
export const useGameWebSocket = (gameToken) => {
const socketRef = useRef(null);
const [isConnected, setIsConnected] = useState(false);
const [gameState, setGameState] = useState(null);
const [boardData, setBoardData] = useState(null);
const [error, setError] = useState(null);
const [isGamemaster, setIsGamemaster] = useState(false);
const [gameStarted, setGameStarted] = useState(false);
const eventListenersRef = useRef(new Map());
// Memoized derived values - no extra state needed
const players = useMemo(() => {
// Backend sends different player fields depending on game state
// connectedPlayers: array of player names (strings) who are connected via WebSocket
// players: full player objects with game data (positions, etc.)
const connectedPlayers = gameState?.connectedPlayers || [];
const gamePlayers = gameState?.players || [];
const currentPlayers = gameState?.currentPlayers || [];
// If we have full player objects, use those
if (currentPlayers.length > 0) return currentPlayers;
if (gamePlayers.length > 0) return gamePlayers;
// Otherwise, map connected player names to basic player objects
return connectedPlayers.map((name, index) => ({
id: `player-${index}`,
name: typeof name === 'string' ? name : name.playerName || `Player ${index + 1}`,
isOnline: true,
isReady: gameState?.readyPlayers?.includes(name) || false,
}));
}, [gameState?.connectedPlayers, gameState?.players, gameState?.currentPlayers, gameState?.readyPlayers]);
const currentTurn = useMemo(() => gameState?.currentPlayer || null, [gameState?.currentPlayer]);
// Connect to game WebSocket - only once per token
useEffect(() => {
if (!gameToken) return;
log('🔌 Connecting to game WebSocket...');
// Connect to /game namespace
socketRef.current = io(`${API_CONFIG.wsURL}/game`, {
transports: ['websocket'],
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
timeout: 5000,
});
const socket = socketRef.current;
// Connection handlers
const handleConnect = () => {
log('✅ Connected to game WebSocket');
setIsConnected(true);
setError(null);
socket.emit('game:join', { gameToken });
};
const handleConnectError = (err) => {
error('❌ Connection error:', err);
setIsConnected(false);
setError(err.message);
};
const handleDisconnect = (reason) => {
log('🔌 Disconnected:', reason);
setIsConnected(false);
};
// Game state handlers - batch updates
const handleGameState = (state) => {
log('📊 Game state:', state);
setGameState(state);
};
const handleGameJoined = (data) => {
log('✅ Joined game:', data);
// Store if this user is the gamemaster
if (data.isGamemaster !== undefined) {
setIsGamemaster(data.isGamemaster);
}
// Backend will send game:state next
};
const handlePlayerJoined = (data) => {
log('👤 Player joined:', data.playerName);
// Update game state to add the new player to connectedPlayers
setGameState(prev => {
if (!prev) return prev;
const currentConnected = prev.connectedPlayers || [];
// Only add if not already in the list
if (!currentConnected.includes(data.playerName)) {
return {
...prev,
connectedPlayers: [...currentConnected, data.playerName]
};
}
return prev;
});
};
const handleGameStarted = (data) => {
log('🎮 Game started:', data);
// Batch state updates
if (data.boardData) setBoardData(data.boardData);
if (data.gameState) setGameState(data.gameState);
// Signal that game has started
setGameStarted(true);
};
const handlePlayerMoved = (moveData) => {
log('🏃 Player moved:', moveData.playerName);
// Update only the moved player
setGameState(prev => {
if (!prev?.currentPlayers) return prev;
return {
...prev,
currentPlayers: prev.currentPlayers.map(p =>
p.playerId === moveData.playerId
? { ...p, boardPosition: moveData.newPosition }
: p
),
};
});
};
const handleTurnChanged = (data) => {
log('🔄 Turn changed to:', data.currentPlayerName);
setGameState(prev => prev ? { ...prev, currentPlayer: data.currentPlayer } : prev);
};
const handleError = (err) => {
error('❌ Game error:', err);
setError(err.message);
};
// Register all handlers
socket.on('connect', handleConnect);
socket.on('connect_error', handleConnectError);
socket.on('disconnect', handleDisconnect);
socket.on('game:state', handleGameState);
socket.on('game:state-update', handleGameState);
socket.on('game:joined', handleGameJoined);
socket.on('game:player-joined', handlePlayerJoined);
socket.on('game:started', handleGameStarted);
socket.on('game:player-moved', handlePlayerMoved);
socket.on('game:turn-changed', handleTurnChanged);
socket.on('game:error', handleError);
// Cleanup
return () => {
log('🧹 Cleaning up WebSocket connection');
socket.removeAllListeners();
socket.disconnect();
};
}, [gameToken]);
// Optimized event listener management
const addEventListener = useCallback((event, handler) => {
const socket = socketRef.current;
if (!socket) return;
socket.on(event, handler);
eventListenersRef.current.set(event, handler);
}, []);
const removeEventListener = useCallback((event) => {
const socket = socketRef.current;
if (!socket) return;
const handler = eventListenersRef.current.get(event);
if (handler) {
socket.off(event, handler);
eventListenersRef.current.delete(event);
}
}, []);
// Memoized action methods - stable references
const rollDice = useCallback((diceValue) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot roll dice: not connected');
return false;
}
log('🎲 Rolling dice:', diceValue);
socket.emit('game:dice-roll', {
gameCode: gameState?.gameCode,
diceValue,
});
return true;
}, [isConnected, gameState?.gameCode]);
const sendMessage = useCallback((message) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot send message: not connected');
return false;
}
socket.emit('game:chat', {
gameCode: gameState?.gameCode,
message,
});
return true;
}, [isConnected, gameState?.gameCode]);
const setReady = useCallback((ready = true) => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot set ready: not connected');
return false;
}
socket.emit('game:ready', {
gameCode: gameState?.gameCode,
ready,
});
return true;
}, [isConnected, gameState?.gameCode]);
const leaveGame = useCallback(() => {
const socket = socketRef.current;
if (!socket || !isConnected) {
warn('⚠️ Cannot leave game: not connected');
return false;
}
socket.emit('game:leave', {
gameCode: gameState?.gameCode,
});
return true;
}, [isConnected, gameState?.gameCode]);
return {
socket: socketRef.current,
isConnected,
gameState,
players,
boardData,
currentTurn,
error,
isGamemaster,
gameStarted,
// Methods
rollDice,
sendMessage,
setReady,
leaveGame,
addEventListener,
removeEventListener,
};
};
+273 -110
View File
@@ -1,88 +1,189 @@
import React, { useState } from "react" import React, { useState, useEffect, useMemo, useCallback } from "react"
import { getVerticalOffset } from "../../utils/randomUtils" import { getVerticalOffset } from "../../utils/randomUtils"
import Dice from "../../utils/dice/Dice" import Dice from "../../utils/dice/Dice"
import { useGameWebSocket } from "../../hooks/useGameWebSocket"
// Constants - outside component to prevent recreation
const PLAYER_STYLES = [
{ color: "bg-blue-600", emoji: "🐍" },
{ color: "bg-green-600", emoji: "🐢" },
{ color: "bg-purple-600", emoji: "🐇" },
{ color: "bg-yellow-600", emoji: "🦊" },
{ color: "bg-red-600", emoji: "🦁" },
{ color: "bg-pink-600", emoji: "🐷" },
{ color: "bg-orange-600", emoji: "🐯" },
{ color: "bg-indigo-600", emoji: "🐺" },
]
const BOARD_CONFIG = {
rows: 5,
cols: 20,
cellSize: 40,
cellMargin: 2.5,
rowSpacing: 70,
}
// Helper functions outside component
const mapFieldType = (backendType) => {
switch (backendType) {
case 'positive': return 'good'
case 'negative': return 'bad'
case 'luck': return 'clover'
default: return 'regular'
}
}
const getDefaultFieldType = (count) => {
if (count % 17 === 0) return "clover"
if (count % 13 === 0) return "bad"
if ((count + 5) % 13 === 0) return "good"
return "regular"
}
const GameScreen = () => { const GameScreen = () => {
const boardRows = 5 // WebSocket connection
const boardCols = 20 const gameToken = localStorage.getItem('gameToken')
const totalCells = boardRows * boardCols const {
const cellSize = 40 isConnected,
const cellMargin = 2.5 gameState,
const rowSpacing = 70 // Extra spacing between rows players: backendPlayers,
const topOffset = rowSpacing * 0.5 // Increase topOffset for more spacing boardData,
const bottomOffset = rowSpacing * 0.5 // Increase bottomOffset for more spacing currentTurn,
const boardWidthPx = boardCols * (cellSize + cellMargin * 2) error,
const boardHeightPx = rollDice,
boardRows * (cellSize + cellMargin * 2 + rowSpacing) + topOffset + bottomOffset - rowSpacing addEventListener,
removeEventListener
} = useGameWebSocket(gameToken)
// Generate a snake-like path with vertical spacing and vertical offsets const [path, setPath] = useState([])
const generateWindingPath = () => { const [players, setPlayers] = useState([])
// Memoized board dimensions
const { rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset, width, height } = useMemo(() => {
const { rows, cols, cellSize, cellMargin, rowSpacing } = BOARD_CONFIG
const topOffset = rowSpacing * 0.5
const bottomOffset = rowSpacing * 0.5
const totalCells = rows * cols
return {
rows,
cols,
totalCells,
cellSize,
cellMargin,
rowSpacing,
topOffset,
bottomOffset,
width: cols * (cellSize + cellMargin * 2),
height: rows * (cellSize + cellMargin * 2 + rowSpacing) + topOffset + bottomOffset - rowSpacing,
}
}, [])
// Memoized path generator - Snake pattern with proper turn handling
const generateWindingPath = useCallback((backendFields = null) => {
const path = [] const path = []
const hasBackendData = backendFields && Array.isArray(backendFields)
let currentNum = 1 let currentNum = 1
for (let row = 0; row < boardRows && currentNum <= totalCells; row++) { // Generate all 100 positions
// Calculate the y position with extra row spacing while (currentNum <= totalCells) {
const baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing) const row = Math.floor((currentNum - 1) / cols)
const posInRow = (currentNum - 1) % cols
const isLeftToRight = row % 2 === 0
// If row number is even, go right; if odd, go left // Calculate column based on direction
if (row % 2 === 0) { const col = isLeftToRight ? posInRow : (cols - 1 - posInRow)
// Left to right
for (let col = 0; col < boardCols && currentNum <= totalCells; col++) { // Base Y position for this row
path.push({ let baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing)
number: currentNum++,
x: col * (cellSize + cellMargin * 2), // Apply vertical offset for wave effect
y: baseYPosition + getVerticalOffset(currentNum - 1), let yOffset = getVerticalOffset(currentNum - 1)
type: getFieldType(currentNum - 1),
}) // Special handling for turn positions (21, 41, 61, 81)
} // These should be positioned between rows to show the turn
} else { if (currentNum % cols === 1 && currentNum > 1) {
// Right to left // This is the first element of a new row (21, 41, 61, 81)
for (let col = boardCols - 1; col >= 0 && currentNum <= totalCells; col--) { // Position it halfway between the previous row and current row
path.push({ baseYPosition = topOffset + (row - 0.5) * (cellSize + cellMargin * 2 + rowSpacing)
number: currentNum++, yOffset = 0 // Reset wave offset for turn positions
x: col * (cellSize + cellMargin * 2),
y: baseYPosition + getVerticalOffset(currentNum - 1),
type: getFieldType(currentNum - 1),
})
}
} }
const backendField = hasBackendData ? backendFields.find(f => f.position === currentNum) : null
path.push({
number: currentNum,
x: col * (cellSize + cellMargin * 2),
y: baseYPosition + yOffset,
type: backendField ? mapFieldType(backendField.type) : getDefaultFieldType(currentNum - 1),
stepValue: backendField?.stepValue || 0,
})
currentNum++
} }
return path return path
} }, [rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset])
const getFieldType = (count) => { // Update path when boardData changes
if (count % 17 === 0) return "clover" useEffect(() => {
if (count % 13 === 0) return "bad" if (boardData?.fields) {
if ((count + 5) % 13 === 0) return "good" setPath(generateWindingPath(boardData.fields))
return "regular" } else if (path.length === 0) {
} setPath(generateWindingPath())
}
}, [boardData, generateWindingPath])
const [path, setPath] = useState(generateWindingPath()) // Update players from backend - memoized mapping
const [players, setPlayers] = useState([ useEffect(() => {
{ id: 1, name: "Béla", position: 34, score: 25, color: "bg-blue-600", emoji: "🐍" }, if (!backendPlayers?.length) return
{ id: 2, name: "Juci", position: 50, score: 30, color: "bg-green-600", emoji: "🐢" },
{ id: 3, name: "Kati", position: 70, score: 15, color: "bg-purple-600", emoji: "🐇" },
{ id: 3, name: "Fürtös", position: 68, score: 14, color: "bg-yellow-600", emoji: "😂" },
])
// New: selected dice value from dropdown (null = none) const mappedPlayers = backendPlayers.map((player, index) => ({
const [selectedDice, setSelectedDice] = useState(null) 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,
}))
// Sort players by position in descending order setPlayers(mappedPlayers)
const sortedPlayers = [...players].sort((a, b) => b.position - a.position) }, [backendPlayers])
// Handle dice roll completion // Listen to player movement - optimized to update only moved player
const handleDiceRoll = (value) => { useEffect(() => {
console.log("Rolled:", value) if (!addEventListener) return
// reset dropdown selection after roll
setSelectedDice(null)
// You can add logic here to move the current player based on the dice value
}
console.log("Generated path length:", path.length) const handlePlayerMoved = (moveData) => {
setPlayers(prev =>
prev.map(p =>
p.id === moveData.playerId
? { ...p, position: moveData.newPosition }
: p
)
)
}
const getFieldStyle = (type) => { addEventListener('game:player-moved', handlePlayerMoved)
return () => removeEventListener('game:player-moved')
}, [addEventListener, removeEventListener])
// Sorted players - memoized
const sortedPlayers = useMemo(
() => [...players].sort((a, b) => b.position - a.position),
[players]
)
// Handle dice roll
const handleDiceRoll = useCallback((value) => {
rollDice(value)
}, [rollDice])
// Get field style - memoized
const getFieldStyle = useCallback((type) => {
switch (type) { switch (type) {
case "clover": case "clover":
return "bg-teal-700 border-teal-500 shadow-teal-800" return "bg-teal-700 border-teal-500 shadow-teal-800"
@@ -93,15 +194,16 @@ const GameScreen = () => {
default: default:
return "bg-gray-800 border-gray-600 shadow-gray-900" return "bg-gray-800 border-gray-600 shadow-gray-900"
} }
} }, [])
const getPlayerPosition = (playerPosition) => { // Get player position - memoized
const getPlayerPosition = useCallback((playerPosition) => {
const field = path.find((p) => p.number === playerPosition) const field = path.find((p) => p.number === playerPosition)
return field ? { top: `${field.y}px`, left: `${field.x}px` } : { top: 0, left: 0 } return field ? { top: `${field.y}px`, left: `${field.x}px` } : { top: 0, left: 0 }
} }, [path])
// Function to get medal style based on rank // Get medal style - memoized
const getMedalStyle = (rank) => { const getMedalStyle = useCallback((rank) => {
switch (rank) { switch (rank) {
case 1: case 1:
return "bg-yellow-400 text-yellow-900 border-yellow-500 shadow-yellow-600" return "bg-yellow-400 text-yellow-900 border-yellow-500 shadow-yellow-600"
@@ -112,20 +214,57 @@ const GameScreen = () => {
default: default:
return "bg-gray-700 text-gray-300 border-gray-600 shadow-gray-800" return "bg-gray-700 text-gray-300 border-gray-600 shadow-gray-800"
} }
} }, [])
return ( 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="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"> <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 && (
<div className="mt-2 px-4 py-2 rounded-lg shadow-lg bg-red-600 text-white text-xs">
{error}
</div>
)}
</div>
{/* Game Info Bar */}
{gameState && (
<div className="fixed top-4 left-4 z-50">
<div className="bg-gray-800 border border-teal-700 px-4 py-2 rounded-lg shadow-lg">
<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>
{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>
</div>
)}
</div>
</div>
)}
<div className="flex flex-col md:flex-row gap-6 justify-center"> <div className="flex flex-col md:flex-row gap-6 justify-center">
{/* Game Board */} {/* Game Board */}
<div className="relative bg-gray-800 p-6 rounded-2xl shadow-xl border border-teal-700 flex flex-col items-center justify-center overflow-hidden"> <div className="relative bg-gray-800 p-6 rounded-2xl shadow-xl border border-teal-700 flex flex-col items-center justify-center overflow-hidden">
{/* Háttér */} {/* Background decoration */}
<div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden"> <div className="absolute w-full h-full opacity-10 pointer-events-none overflow-hidden">
{[...Array(35)].map((_, i) => ( {[...Array(35)].map((_, i) => (
<div <div
key={i} key={i}
className="absolute rounded-full bg-teal-600 animate-pulse8" className="absolute rounded-full bg-teal-600 animate-pulse"
style={{ style={{
width: Math.random() * 120 + 40 + "px", width: Math.random() * 120 + 40 + "px",
height: Math.random() * 120 + 40 + "px", height: Math.random() * 120 + 40 + "px",
@@ -136,8 +275,9 @@ const GameScreen = () => {
></div> ></div>
))} ))}
</div> </div>
<div className="relative" style={{ height: `${boardHeightPx}px`, width: `${boardWidthPx}px` }}>
{/* Mezők */} <div className="relative" style={{ height: `${height}px`, width: `${width}px` }}>
{/* Fields */}
{path.map((field) => ( {path.map((field) => (
<div <div
key={field.number} key={field.number}
@@ -163,44 +303,65 @@ const GameScreen = () => {
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`} 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={{ style={{
...getPlayerPosition(player.position), ...getPlayerPosition(player.position),
transform: "translate(18px, 18px)", transform: "translate(17px, 17px)",
}} }}
> >
{player.emoji} {player.emoji}
</div> </div>
))} ))}
</div> </div>
{/* Game information */}
{/* <div className="bg-white rounded-xl p-2 shadow-lg border border-indigo-100 max-w-3xl mx-auto mt-4 z-10">
<p className="text-gray-600 text-sm text-center">
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-white border border-gray-300 rounded-full mr-1"></span> Sima</span>
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-green-200 border border-green-500 rounded-full mr-1"></span> Lóhere</span>
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-red-200 border border-red-500 rounded-full mr-1"></span> Rossz</span>
<span className="inline-flex items-center mx-2"><span className="w-3 h-3 bg-blue-200 border border-blue-500 rounded-full mr-1"></span> Jó</span>
</p>
</div> */}
</div> </div>
{/* Right sidebar */} {/* Right sidebar */}
<div className="flex-1 max-w-md"> <div className="flex-1 max-w-md">
<div className="bg-gray-800 rounded-xl p-4 shadow-lg mb-4 border border-teal-700"> <div className="bg-gray-800 rounded-xl p-4 shadow-lg mb-4 border border-teal-700">
<h2 className="text-xl font-semibold mb-3 text-teal-300">Játékosok</h2> <h2 className="text-xl font-semibold mb-3 text-teal-300">Játékosok</h2>
{/* Empty state */}
{players.length === 0 && (
<div className="text-center py-8 text-gray-400">
<div className="text-4xl mb-2">👥</div>
<p className="text-sm">Várakozás játékosokra...</p>
</div>
)}
{/* Players list */}
{sortedPlayers.map((player, index) => ( {sortedPlayers.map((player, index) => (
<div <div
key={player.id} key={player.id}
className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors" className="flex items-center mb-3 p-2 bg-gray-900 rounded-lg hover:bg-gray-700 transition-colors relative"
> >
{/* Online indicator */}
{player.isOnline && (
<div className="absolute top-1 right-1 w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
)}
<div <div
className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`} className={`w-8 h-8 ${player.color} rounded-full mr-3 flex items-center justify-center text-white text-sm font-bold shadow-md`}
> >
{player.emoji} {player.emoji}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="font-medium text-sm text-gray-300 flex items-center"> <div className="font-medium text-sm text-gray-300 flex items-center gap-2 flex-wrap">
{player.name} {player.name}
{/* Ready indicator */}
{player.isReady && (
<span className="px-2 py-0.5 bg-green-600 text-white text-xs rounded-full">
Kész
</span>
)}
{/* Current turn indicator */}
{currentTurn === player.id && (
<span className="px-2 py-0.5 bg-yellow-500 text-gray-900 text-xs rounded-full font-bold animate-pulse">
Köre
</span>
)}
{/* Rank medal */}
<span <span
className={`ml-2 px-2 py-1 rounded-full border text-xs font-bold shadow-md ${getMedalStyle( className={`ml-auto px-2 py-1 rounded-full border text-xs font-bold shadow-md ${getMedalStyle(
index + 1 index + 1
)}`} )}`}
> >
@@ -225,31 +386,33 @@ const GameScreen = () => {
<div className="bg-gray-800 rounded-xl p-4 shadow-lg border border-teal-700 text-center"> <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> <h2 className="text-xl font-semibold mb-3 text-teal-300">Dobókocka</h2>
<p className="text-gray-300 text-sm mb-4"> <p className="text-gray-300 text-sm mb-4">
Kattints a kockára dobáshoz vagy válassz egy számot az alábbiból! Kattints a kockára dobáshoz!
</p> </p>
{/* Dropdown to select number 1-6 (triggers animated roll to that number) */} <Dice onRoll={handleDiceRoll} />
<div className="mb-3">
<select
value={selectedDice ?? ""}
onChange={(e) => {
const v = e.target.value ? Number(e.target.value) : null
setSelectedDice(v)
}}
className="bg-gray-900 text-gray-200 rounded-md p-2 border border-gray-700"
>
<option value="">Válassz számot...</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
</select>
</div>
<Dice onRoll={handleDiceRoll} selectedValue={selectedDice} /> {/* Connection warning */}
{!isConnected && (
<div className="mt-3 text-xs text-red-400">
Nincs kapcsolat a szerverrel
</div>
)}
</div> </div>
{/* Debug Info Panel (Development only) */}
{import.meta.env.DEV && (
<div className="bg-gray-900 rounded-xl p-4 shadow-lg border border-gray-700 text-left mt-4">
<h3 className="text-sm font-semibold mb-2 text-gray-400">🔧 Debug Info</h3>
<div className="text-xs text-gray-500 space-y-1">
<div>📡 Connected: {isConnected ? '✅' : '❌'}</div>
<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>🔑 Token: {gameToken ? '✅' : '❌'}</div>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -0,0 +1,160 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { createGame, joinGame } from '../../api/gameApi';
const GameTest = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [gameCode, setGameCode] = useState('');
const [createdGameCode, setCreatedGameCode] = useState('');
const [showSuccess, setShowSuccess] = useState(false);
const handleCreateGame = async () => {
setLoading(true);
setError(null);
setShowSuccess(false);
try {
const token = localStorage.getItem('token');
if (!token) {
setError('Please login first at /login');
return;
}
const gameData = {
deckids: ['99333c9a-5928-4788-b852-fa482d34ce56'], // Test deck ID as array
maxplayers: 4,
logintype: 0, // 0=PUBLIC
};
const response = await createGame(gameData);
console.log('Game created:', response);
// Backend returns game object directly
const code = response.gamecode || response.gameCode;
if (code) {
setCreatedGameCode(code);
setShowSuccess(true);
}
// Store game token if provided
if (response.gameToken) {
localStorage.setItem('gameToken', response.gameToken);
}
// Wait 3 seconds to show code, then navigate
setTimeout(() => {
navigate('/lobby', { state: { gameCode: code } });
}, 3000);
} catch (err) {
setError(err.response?.data?.message || 'Failed to create game');
console.error('Create game error:', err);
} finally {
setLoading(false);
}
};
const handleJoinGame = async () => {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('token');
if (!token) {
setError('Kérlek jelentkezz be először a /login oldalon');
return;
}
const joinData = {
gameCode: gameCode.toUpperCase(),
playerName: localStorage.getItem('username') || 'Test Player',
};
const response = await joinGame(joinData);
console.log('Joined game:', response);
// Store game token
if (response.data?.gameToken) {
localStorage.setItem('gameToken', response.data.gameToken);
navigate('/lobby', { state: { gameCode: gameCode.toUpperCase() } });
}
} catch (err) {
setError(err.response?.data?.message || 'Nem sikerült csatlakozni a játékhoz');
console.error('Join game error:', err);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-900 text-white flex items-center justify-center p-8">
<div className="bg-gray-800 rounded-lg p-8 max-w-md w-full">
<h1 className="text-3xl font-bold mb-6 text-center">Game Test</h1>
{error && (
<div className="bg-red-500/20 border border-red-500 rounded p-3 mb-4">
{error}
</div>
)}
{showSuccess && createdGameCode && (
<div className="bg-green-500/20 border border-green-500 rounded p-4 mb-4">
<p className="font-bold text-lg mb-2">Game Created!</p>
<p className="text-2xl font-mono tracking-wider text-green-400 mb-2">
{createdGameCode}
</p>
<p className="text-sm text-gray-300">
Share this code with other players so they can join!
</p>
<p className="text-sm text-gray-400 mt-2">
Redirecting to game in 3 seconds...
</p>
</div>
)}
<div className="space-y-4">
<button
onClick={handleCreateGame}
disabled={loading}
className="w-full bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white font-bold py-3 px-4 rounded transition"
>
{loading ? 'Creating...' : 'Create New Game'}
</button>
<div className="text-center text-gray-400">OR</div>
<div>
<input
type="text"
value={gameCode}
onChange={(e) => setGameCode(e.target.value)}
placeholder="Enter Game Code"
className="w-full bg-gray-700 text-white px-4 py-2 rounded mb-2"
/>
<button
onClick={handleJoinGame}
disabled={loading || !gameCode}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-bold py-3 px-4 rounded transition"
>
{loading ? 'Joining...' : 'Join Game'}
</button>
</div>
</div>
<div className="mt-6 pt-6 border-t border-gray-700">
<p className="text-sm text-gray-400 mb-2">Quick Access (Dev Only):</p>
<button
onClick={() => {
localStorage.setItem('gameToken', 'test-token-123');
navigate('/game');
}}
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-2 px-4 rounded text-sm"
>
Go to Game (with test token)
</button>
</div>
</div>
</div>
);
};
export default GameTest;
+166 -14
View File
@@ -3,6 +3,8 @@ import { useNavigate, useLocation } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar.jsx" import Navbar from "../../components/Navbar/Navbar.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import useRequireAuth from "../../hooks/useRequireAuth.jsx" import useRequireAuth from "../../hooks/useRequireAuth.jsx"
import { useGameWebSocket } from "../../hooks/useGameWebSocket.js"
import { startGame } from "../../api/gameApi.js"
const Lobby = () => { const Lobby = () => {
const [visible, setVisible] = useState(false) const [visible, setVisible] = useState(false)
@@ -12,6 +14,30 @@ const Lobby = () => {
const [user, setUser] = useRequireAuth() const [user, setUser] = useRequireAuth()
// Get game code from location state or WebSocket
const gameCodeFromState = location.state?.gameCode
const gameToken = localStorage.getItem('gameToken')
const {
isConnected,
gameState,
players,
isGamemaster,
gameStarted,
} = useGameWebSocket(gameToken)
const gameCode = gameCodeFromState || gameState?.gameCode || 'Loading...'
// Filter out gamemaster from player list - gamemaster is NOT a player
const currentPlayers = (players || []).filter(p => {
// If we have userId info, filter by that
if (p.userId) {
return p.userId !== gameState?.createdBy
}
// Otherwise filter by name (less reliable but works for now)
return true
})
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
([entry]) => { ([entry]) => {
@@ -23,12 +49,48 @@ const Lobby = () => {
return () => observer.disconnect() return () => observer.disconnect()
}, []) }, [])
// Auto-navigate when game starts
useEffect(() => {
if (gameStarted) {
console.log('🎮 Game started, navigating to /game')
navigate("/game")
}
}, [gameStarted, navigate])
const handleExit = () => { const handleExit = () => {
if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) { if (window.confirm("Biztosan ki szeretnél lépni a lobbyból?")) {
localStorage.removeItem('gameToken')
navigate("/home") navigate("/home")
} }
} }
const handleStartGame = async () => {
try {
// Get gameId from gameState
const gameId = gameState?.gameId
if (!gameId) {
alert('Hiba: Játék azonosító nem található')
return
}
console.log('Starting game with ID:', gameId)
const response = await startGame(gameId)
console.log('Game start response:', response)
// Backend will broadcast game:started event to all players
// Navigate to game page
navigate("/game")
} catch (error) {
console.error('Failed to start game:', error)
alert(`Nem sikerült elindítani a játékot: ${error.response?.data?.error || error.message}`)
}
}
const copyGameCode = () => {
navigator.clipboard.writeText(gameCode)
alert('Játék kód vágólapra másolva: ' + gameCode)
}
const getInitials = (name) => { const getInitials = (name) => {
return name return name
.split(" ") .split(" ")
@@ -57,31 +119,121 @@ const Lobby = () => {
style={{ background: "rgba(0,0,0,0.25)" }} style={{ background: "rgba(0,0,0,0.25)" }}
> >
<h1 className="text-4xl md:text-5xl font-extrabold text-green-300 mb-4 text-center tracking-wide drop-shadow-lg"> <h1 className="text-4xl md:text-5xl font-extrabold text-green-300 mb-4 text-center tracking-wide drop-shadow-lg">
{user} Lobby-ja Játék Lobby
</h1> </h1>
<p className="text-lg text-zinc-300 mb-8 text-center"> {/* Game Code Display */}
Játékosok, akik csatlakoztak ehhez a szobához: <div className="bg-gradient-to-r from-green-600/20 to-teal-600/20 rounded-xl p-6 mb-6 border-2 border-green-400/50">
<p className="text-lg text-zinc-300 mb-2 text-center font-semibold">
Játék Kód:
</p>
<div className="flex items-center justify-center gap-3">
<p className="text-5xl font-mono font-extrabold text-green-300 tracking-widest drop-shadow-lg">
{gameCode}
</p>
<button
onClick={copyGameCode}
className="bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded-lg font-semibold transition-all duration-200 hover:scale-105"
title="Másolás vágólapra"
>
📋 Másolás
</button>
</div>
<p className="text-sm text-zinc-400 mt-3 text-center">
Oszd meg ezt a kódot másokkal, hogy csatlakozhassanak a játékhoz!
</p>
</div>
{/* Connection Status */}
<div className="mb-4 text-center">
<span className={`inline-block px-4 py-2 rounded-full text-sm font-semibold ${
isConnected
? 'bg-green-600/20 text-green-300 border border-green-400'
: 'bg-red-600/20 text-red-300 border border-red-400'
}`}>
{isConnected ? '🟢 Kapcsolódva' : '🔴 Kapcsolat megszakadt'}
</span>
</div>
<p className="text-lg text-zinc-300 mb-6 text-center">
Játékosok ({currentPlayers.length}):
</p> </p>
<div className="bg-zinc-800/90 rounded-xl shadow-lg p-6 mb-8"> <div className="bg-zinc-800/90 rounded-xl shadow-lg p-6 mb-8">
<ul className="flex flex-col gap-4"> <ul className="flex flex-col gap-4">
<li className="bg-zinc-700 py-3 px-4 rounded-xl text-green-400 font-semibold flex items-center gap-4 shadow hover:shadow-green-500/20 transition"> {currentPlayers.length === 0 ? (
<div <li className="text-center text-zinc-400 py-4">
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold" Várakozás játékosokra...
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }} </li>
> ) : (
{getInitials(user)} currentPlayers.map((player, index) => (
</div> <li
<span className="text-white text-lg">{user}</span> key={player.id || index}
</li> className="bg-zinc-700 py-3 px-4 rounded-xl text-green-400 font-semibold flex items-center gap-4 shadow hover:shadow-green-500/20 transition"
>
<div
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
>
{getInitials(player.name || `Player ${index + 1}`)}
</div>
<span className="text-white text-lg flex-1">
{player.name || `Player ${index + 1}`}
</span>
{player.isReady && (
<span className="bg-green-600 text-white text-xs px-2 py-1 rounded-full">
Kész
</span>
)}
{player.isOnline && (
<span className="text-green-400 text-xs">🟢</span>
)}
</li>
))
)}
</ul> </ul>
</div> </div>
<div className="flex justify-center"> {/* Role indicator */}
<div className="mb-6 text-center">
{isGamemaster ? (
<div className="bg-yellow-600/20 text-yellow-300 px-4 py-3 rounded-lg border border-yellow-400/50">
<p className="font-semibold">👑 Te vagy a Gamemaster!</p>
<p className="text-sm mt-1">Te nem játszol, csak indítod és moderálod a játékot.</p>
</div>
) : (
<div className="bg-blue-600/20 text-blue-300 px-4 py-3 rounded-lg border border-blue-400/50">
<p className="font-semibold">🎮 Te vagy egy Játékos!</p>
<p className="text-sm mt-1">Várj, amíg a gamemaster elindítja a játékot.</p>
</div>
)}
</div>
<div className="flex justify-center gap-4">
{isGamemaster ? (
/* Gamemaster view - can start game */
<button
onClick={handleStartGame}
disabled={currentPlayers.length < 2}
className={`px-8 py-3 rounded-xl font-semibold shadow-lg transition-transform transform hover:scale-105 ${
currentPlayers.length >= 2
? 'bg-gradient-to-r from-green-700 to-green-500 hover:from-green-600 hover:to-green-400 text-white hover:shadow-green-400/30'
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
}`}
title={currentPlayers.length < 2 ? 'Minimum 2 játékos szükséges' : 'Játék indítása'}
>
Játék Indítása
</button>
) : (
/* Player view - cannot start game, just wait */
<div className="text-center text-zinc-400">
<p className="text-lg">Várakozás a gamemaster-re...</p>
<p className="text-sm mt-2">Csak a gamemaster indíthatja el a játékot</p>
</div>
)}
<button <button
onClick={handleExit} onClick={handleExit}
className="bg-gradient-to-r from-green-700 to-green-500 hover:from-green-600 hover:to-green-400 text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-green-400/30 transition-transform transform hover:scale-105" className="bg-gradient-to-r from-red-700 to-red-500 hover:from-red-600 hover:to-red-400 text-white px-8 py-3 rounded-xl font-semibold shadow-lg hover:shadow-red-400/30 transition-transform transform hover:scale-105"
> >
Kilépés Kilépés
</button> </button>
@@ -1,4 +1,4 @@
import React, { useState } from "react" import React, { useState, useEffect } from "react"
import { useNavigate, useLocation } from "react-router-dom" import { useNavigate, useLocation } from "react-router-dom"
import Navbar from "../../components/Navbar/Navbar.jsx" import Navbar from "../../components/Navbar/Navbar.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
@@ -6,6 +6,7 @@ import Footer from "../../components/Footer/Footer.jsx"
import useRequireAuth from "../../hooks/useRequireAuth.jsx" import useRequireAuth from "../../hooks/useRequireAuth.jsx"
import ButtonGreen from "../../components/Buttons/ButtonGreen.jsx" import ButtonGreen from "../../components/Buttons/ButtonGreen.jsx"
import { motion } from "framer-motion" import { motion } from "framer-motion"
import { createGame, joinGame } from "../../api/gameApi.js"
const GameLobbySetup = () => { const GameLobbySetup = () => {
const [username] = useRequireAuth({ key: "username", redirectTo: "/login" }) const [username] = useRequireAuth({ key: "username", redirectTo: "/login" })
@@ -16,19 +17,82 @@ const GameLobbySetup = () => {
const [maxPlayers, setMaxPlayers] = useState(4) const [maxPlayers, setMaxPlayers] = useState(4)
const [isPublic, setIsPublic] = useState(true) const [isPublic, setIsPublic] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [createdGameCode, setCreatedGameCode] = useState('')
const [showSuccess, setShowSuccess] = useState(false)
const handleCreateLobby = () => { const handleCreateLobby = async () => {
console.log({ setLoading(true)
deckIds, setError(null)
maxPlayers,
isPublic, try {
}) const username = localStorage.getItem('username')
// Itt küldd el az API-nak a lobby létrehozását
// navigate("/game-lobby", { state: { lobbyId: response.lobbyId } }) console.log('Creating game - username:', username)
if (!username) {
setError('Kérlek jelentkezz be először!')
setLoading(false)
return
}
// Backend expects deckids (array), maxplayers (number), logintype (0=PUBLIC, 1=PRIVATE)
const gameData = {
deckids: deckIds, // Array of deck UUIDs
maxplayers: maxPlayers, // Number
logintype: isPublic ? 0 : 1, // 0=PUBLIC, 1=PRIVATE, 2=ORGANIZATION
}
console.log('Creating game with data:', gameData)
const response = await createGame(gameData)
console.log('Game created:', response)
// Verify localStorage still has username
console.log('After create - username:', localStorage.getItem('username'))
// Backend returns game object directly
const code = response.gamecode || response.gameCode
if (code) {
setCreatedGameCode(code)
setShowSuccess(true)
}
// Creator needs to join their own game to get a gameToken
// This allows the WebSocket to recognize them as the gamemaster
try {
const username = localStorage.getItem('username')
const joinResponse = await joinGame({
gameCode: code,
playerName: username
})
if (joinResponse.gameToken) {
localStorage.setItem('gameToken', joinResponse.gameToken)
console.log('Creator joined game as gamemaster, token stored')
}
} catch (joinError) {
console.error('Failed to join game as creator:', joinError)
// Continue anyway - the creator can still try to join manually
}
// Wait 3 seconds to show code, then navigate to lobby
setTimeout(() => {
console.log('Navigating to lobby with code:', code)
navigate('/lobby', { state: { gameCode: code } })
}, 3000)
} catch (err) {
console.error('Create game error:', err)
console.error('Error response:', err.response?.data)
console.error('Error status:', err.response?.status)
setError(err.response?.data?.message || err.response?.data?.error || 'Nem sikerült létrehozni a játékot')
} finally {
setLoading(false)
}
} }
if (deckIds.length === 0) { if (deckIds.length === 0) {
navigate("/choose-deck") navigate("/choosedeck")
return null return null
} }
@@ -67,6 +131,27 @@ const GameLobbySetup = () => {
{deckIds.length} pakli kiválasztva. Add meg a játék részleteit. {deckIds.length} pakli kiválasztva. Add meg a játék részleteit.
</motion.p> </motion.p>
{error && (
<div className="bg-red-500/20 border border-red-500 rounded-lg p-4 mb-6">
{error}
</div>
)}
{createdGameCode && (
<div className="bg-green-500/20 border border-green-500 rounded-lg p-6 mb-6">
<p className="font-bold text-xl mb-2">Játék Létrehozva! 🎉</p>
<p className="text-3xl font-mono tracking-wider text-green-400 mb-2">
{createdGameCode}
</p>
<p className="text-sm text-gray-300">
Oszd meg ezt a kódot más játékosokkal, hogy csatlakozhassanak!
</p>
<p className="text-sm text-gray-400 mt-2">
Átirányítás a lobby-hoz 3 másodperc múlva...
</p>
</div>
)}
<div className="bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl p-8 shadow-lg space-y-6"> <div className="bg-[color:var(--color-surface)]/80 backdrop-blur-lg rounded-2xl p-8 shadow-lg space-y-6">
{/* Max Players */} {/* Max Players */}
<div> <div>
@@ -115,11 +200,17 @@ const GameLobbySetup = () => {
<div className="flex justify-center gap-4 mt-8"> <div className="flex justify-center gap-4 mt-8">
<ButtonGreen <ButtonGreen
text="Vissza" text="Vissza"
onClick={() => navigate("/choose-deck")} onClick={() => navigate("/choosedeck")}
width="w-auto px-8" width="w-auto px-8"
className="bg-gray-600 hover:bg-gray-700" className="bg-gray-600 hover:bg-gray-700"
disabled={loading}
/>
<ButtonGreen
text={loading ? "Létrehozás..." : "Lobby Létrehozása"}
onClick={handleCreateLobby}
width="w-auto px-8"
disabled={loading}
/> />
<ButtonGreen text="Lobby Létrehozása" onClick={handleCreateLobby} width="w-auto px-8" />
</div> </div>
</motion.section> </motion.section>
</main> </main>
@@ -1,24 +1,75 @@
// src/pages/Home/Home.jsx // src/pages/Home/Home.jsx
// Régi PlayMenu-s oldal, "Home" néven // Régi PlayMenu-s oldal, "Home" néven
import { useEffect } from "react" import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import useRequireAuth from "../../hooks/useRequireAuth" import useRequireAuth from "../../hooks/useRequireAuth"
import Navbar from "../../components/Navbar/Navbar" import Navbar from "../../components/Navbar/Navbar"
import Footer from "../../components/Footer/Footer.jsx" import Footer from "../../components/Footer/Footer.jsx"
import Background from "../../assets/backgrounds/Background.jsx" import Background from "../../assets/backgrounds/Background.jsx"
import PlayMenu from "../../components/Landingpage/PlayMenu.jsx" import PlayMenu from "../../components/Landingpage/PlayMenu.jsx"
import { joinGame } from "../../api/gameApi.js"
export default function Home() { export default function Home() {
const navigate = useNavigate()
// a hook inicializálja a user-t a localStorage-ból és visszaadja a state-et + settert // a hook inicializálja a user-t a localStorage-ból és visszaadja a state-et + settert
const [user, setUser] = useRequireAuth({ redirect: false }) // no redirect on unauthenticated visitors const [user, setUser] = useRequireAuth({ redirect: false }) // no redirect on unauthenticated visitors
const [isJoining, setIsJoining] = useState(false)
// Dummy callbackok és user példa // Join game handler - csatlakozás játékhoz kóddal
const handleJoinGame = (code) => { const handleJoinGame = async (code) => {
alert(`Csatlakozás játékhoz: ${code}`) if (!user) {
alert('Kérlek először jelentkezz be!')
navigate('/login')
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',
}
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) {
localStorage.setItem('gameToken', response.gameToken)
}
navigate('/lobby', { state: { gameCode: code.toUpperCase() } })
} 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)
}
} }
// Create game handler - új játék létrehozása
const handleCreateGame = () => { const handleCreateGame = () => {
alert("Új játék létrehozása") if (!user) {
alert('Kérlek először jelentkezz be!')
navigate('/login')
return
}
// Navigate to choose deck page to start game creation flow
navigate('/choosedeck')
} }
const userObj = { name: user } const userObj = { name: user }
// ha szükséges a user módosítása máshol: setUser("újnév") automatikusan menti localStorage-be // ha szükséges a user módosítása máshol: setUser("újnév") automatikusan menti localStorage-be
+11
View File
@@ -13,6 +13,17 @@ export default defineConfig({
}, },
hmr: { hmr: {
clientPort: 5173, clientPort: 5173,
},
proxy: {
'/api': {
target: 'http://backend:3000',
changeOrigin: true,
},
'/socket.io': {
target: 'http://backend:3000',
changeOrigin: true,
ws: true,
}
} }
}, },
preview: { preview: {