csatlakozas-mukodesdemodemodemo
This commit is contained in:
Generated
+128
-1
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
|
||||||
const GameScreen = () => {
|
// Constants - outside component to prevent recreation
|
||||||
const boardRows = 5
|
const PLAYER_STYLES = [
|
||||||
const boardCols = 20
|
{ color: "bg-blue-600", emoji: "🐍" },
|
||||||
const totalCells = boardRows * boardCols
|
{ color: "bg-green-600", emoji: "🐢" },
|
||||||
const cellSize = 40
|
{ color: "bg-purple-600", emoji: "🐇" },
|
||||||
const cellMargin = 2.5
|
{ color: "bg-yellow-600", emoji: "🦊" },
|
||||||
const rowSpacing = 70 // Extra spacing between rows
|
{ color: "bg-red-600", emoji: "🦁" },
|
||||||
const topOffset = rowSpacing * 0.5 // Increase topOffset for more spacing
|
{ color: "bg-pink-600", emoji: "🐷" },
|
||||||
const bottomOffset = rowSpacing * 0.5 // Increase bottomOffset for more spacing
|
{ color: "bg-orange-600", emoji: "🐯" },
|
||||||
const boardWidthPx = boardCols * (cellSize + cellMargin * 2)
|
{ color: "bg-indigo-600", emoji: "🐺" },
|
||||||
const boardHeightPx =
|
]
|
||||||
boardRows * (cellSize + cellMargin * 2 + rowSpacing) + topOffset + bottomOffset - rowSpacing
|
|
||||||
|
|
||||||
// Generate a snake-like path with vertical spacing and vertical offsets
|
const BOARD_CONFIG = {
|
||||||
const generateWindingPath = () => {
|
rows: 5,
|
||||||
const path = []
|
cols: 20,
|
||||||
let currentNum = 1
|
cellSize: 40,
|
||||||
|
cellMargin: 2.5,
|
||||||
|
rowSpacing: 70,
|
||||||
|
}
|
||||||
|
|
||||||
for (let row = 0; row < boardRows && currentNum <= totalCells; row++) {
|
// Helper functions outside component
|
||||||
// Calculate the y position with extra row spacing
|
const mapFieldType = (backendType) => {
|
||||||
const baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing)
|
switch (backendType) {
|
||||||
|
case 'positive': return 'good'
|
||||||
// If row number is even, go right; if odd, go left
|
case 'negative': return 'bad'
|
||||||
if (row % 2 === 0) {
|
case 'luck': return 'clover'
|
||||||
// Left to right
|
default: return 'regular'
|
||||||
for (let col = 0; col < boardCols && currentNum <= totalCells; col++) {
|
|
||||||
path.push({
|
|
||||||
number: currentNum++,
|
|
||||||
x: col * (cellSize + cellMargin * 2),
|
|
||||||
y: baseYPosition + getVerticalOffset(currentNum - 1),
|
|
||||||
type: getFieldType(currentNum - 1),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Right to left
|
|
||||||
for (let col = boardCols - 1; col >= 0 && currentNum <= totalCells; col--) {
|
|
||||||
path.push({
|
|
||||||
number: currentNum++,
|
|
||||||
x: col * (cellSize + cellMargin * 2),
|
|
||||||
y: baseYPosition + getVerticalOffset(currentNum - 1),
|
|
||||||
type: getFieldType(currentNum - 1),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return path
|
const getDefaultFieldType = (count) => {
|
||||||
}
|
|
||||||
|
|
||||||
const getFieldType = (count) => {
|
|
||||||
if (count % 17 === 0) return "clover"
|
if (count % 17 === 0) return "clover"
|
||||||
if (count % 13 === 0) return "bad"
|
if (count % 13 === 0) return "bad"
|
||||||
if ((count + 5) % 13 === 0) return "good"
|
if ((count + 5) % 13 === 0) return "good"
|
||||||
return "regular"
|
return "regular"
|
||||||
|
}
|
||||||
|
|
||||||
|
const GameScreen = () => {
|
||||||
|
// WebSocket connection
|
||||||
|
const gameToken = localStorage.getItem('gameToken')
|
||||||
|
const {
|
||||||
|
isConnected,
|
||||||
|
gameState,
|
||||||
|
players: backendPlayers,
|
||||||
|
boardData,
|
||||||
|
currentTurn,
|
||||||
|
error,
|
||||||
|
rollDice,
|
||||||
|
addEventListener,
|
||||||
|
removeEventListener
|
||||||
|
} = useGameWebSocket(gameToken)
|
||||||
|
|
||||||
|
const [path, setPath] = useState([])
|
||||||
|
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 hasBackendData = backendFields && Array.isArray(backendFields)
|
||||||
|
|
||||||
|
let currentNum = 1
|
||||||
|
|
||||||
|
// Generate all 100 positions
|
||||||
|
while (currentNum <= totalCells) {
|
||||||
|
const row = Math.floor((currentNum - 1) / cols)
|
||||||
|
const posInRow = (currentNum - 1) % cols
|
||||||
|
const isLeftToRight = row % 2 === 0
|
||||||
|
|
||||||
|
// Calculate column based on direction
|
||||||
|
const col = isLeftToRight ? posInRow : (cols - 1 - posInRow)
|
||||||
|
|
||||||
|
// Base Y position for this row
|
||||||
|
let baseYPosition = topOffset + row * (cellSize + cellMargin * 2 + rowSpacing)
|
||||||
|
|
||||||
|
// Apply vertical offset for wave effect
|
||||||
|
let yOffset = getVerticalOffset(currentNum - 1)
|
||||||
|
|
||||||
|
// Special handling for turn positions (21, 41, 61, 81)
|
||||||
|
// These should be positioned between rows to show the turn
|
||||||
|
if (currentNum % cols === 1 && currentNum > 1) {
|
||||||
|
// This is the first element of a new row (21, 41, 61, 81)
|
||||||
|
// Position it halfway between the previous row and current row
|
||||||
|
baseYPosition = topOffset + (row - 0.5) * (cellSize + cellMargin * 2 + rowSpacing)
|
||||||
|
yOffset = 0 // Reset wave offset for turn positions
|
||||||
}
|
}
|
||||||
|
|
||||||
const [path, setPath] = useState(generateWindingPath())
|
const backendField = hasBackendData ? backendFields.find(f => f.position === currentNum) : null
|
||||||
const [players, setPlayers] = useState([
|
|
||||||
{ id: 1, name: "Béla", position: 34, score: 25, color: "bg-blue-600", emoji: "🐍" },
|
|
||||||
{ 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)
|
path.push({
|
||||||
const [selectedDice, setSelectedDice] = useState(null)
|
number: currentNum,
|
||||||
|
x: col * (cellSize + cellMargin * 2),
|
||||||
|
y: baseYPosition + yOffset,
|
||||||
|
type: backendField ? mapFieldType(backendField.type) : getDefaultFieldType(currentNum - 1),
|
||||||
|
stepValue: backendField?.stepValue || 0,
|
||||||
|
})
|
||||||
|
|
||||||
// Sort players by position in descending order
|
currentNum++
|
||||||
const sortedPlayers = [...players].sort((a, b) => b.position - a.position)
|
|
||||||
|
|
||||||
// Handle dice roll completion
|
|
||||||
const handleDiceRoll = (value) => {
|
|
||||||
console.log("Rolled:", value)
|
|
||||||
// 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)
|
return path
|
||||||
|
}, [rows, cols, totalCells, cellSize, cellMargin, rowSpacing, topOffset])
|
||||||
|
|
||||||
const getFieldStyle = (type) => {
|
// Update path when boardData changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (boardData?.fields) {
|
||||||
|
setPath(generateWindingPath(boardData.fields))
|
||||||
|
} else if (path.length === 0) {
|
||||||
|
setPath(generateWindingPath())
|
||||||
|
}
|
||||||
|
}, [boardData, generateWindingPath])
|
||||||
|
|
||||||
|
// Update players from backend - memoized mapping
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setPlayers(mappedPlayers)
|
||||||
|
}, [backendPlayers])
|
||||||
|
|
||||||
|
// Listen to player movement - optimized to update only moved player
|
||||||
|
useEffect(() => {
|
||||||
|
if (!addEventListener) return
|
||||||
|
|
||||||
|
const handlePlayerMoved = (moveData) => {
|
||||||
|
setPlayers(prev =>
|
||||||
|
prev.map(p =>
|
||||||
|
p.id === moveData.playerId
|
||||||
|
? { ...p, position: moveData.newPosition }
|
||||||
|
: p
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,32 +386,34 @@ 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
|
{/* Connection warning */}
|
||||||
value={selectedDice ?? ""}
|
{!isConnected && (
|
||||||
onChange={(e) => {
|
<div className="mt-3 text-xs text-red-400">
|
||||||
const v = e.target.value ? Number(e.target.value) : null
|
⚠️ Nincs kapcsolat a szerverrel
|
||||||
setSelectedDice(v)
|
</div>
|
||||||
}}
|
)}
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<Dice onRoll={handleDiceRoll} selectedValue={selectedDice} />
|
{/* 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>
|
</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;
|
||||||
@@ -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 ? (
|
||||||
|
<li className="text-center text-zinc-400 py-4">
|
||||||
|
Várakozás játékosokra...
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
currentPlayers.map((player, index) => (
|
||||||
|
<li
|
||||||
|
key={player.id || index}
|
||||||
|
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
|
<div
|
||||||
className="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold"
|
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)" }}
|
style={{ background: "rgba(34,197,94,0.12)", color: "var(--color-mint)" }}
|
||||||
>
|
>
|
||||||
{getInitials(user)}
|
{getInitials(player.name || `Player ${index + 1}`)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white text-lg">{user}</span>
|
<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>
|
</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')
|
||||||
|
|
||||||
|
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
|
||||||
})
|
})
|
||||||
// Itt küldd el az API-nak a lobby létrehozását
|
|
||||||
// navigate("/game-lobby", { state: { lobbyId: response.lobbyId } })
|
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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user