Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e709667482 | |||
| c41b9cb76a |
@@ -1,8 +1,8 @@
|
|||||||
# MongoDB connection for backend
|
# MongoDB connection for backend
|
||||||
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27018/ludo?authSource=admin&replicaSet=rs0
|
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27017/ludo?authSource=admin&replicaSet=rs0
|
||||||
|
|
||||||
# Backend port
|
# Backend port
|
||||||
PORT=3000
|
PORT=18081
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
NODE_ENV=development
|
NODE_ENV=production
|
||||||
|
|||||||
+5
-1
@@ -69,7 +69,11 @@ if (process.env.NODE_ENV === 'production') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { server };
|
module.exports = { server };
|
||||||
|
root@DietPi:~/mern-ludo/backend# ^C
|
||||||
|
root@DietPi:~/mern-ludo/backend# cd .
|
||||||
|
root@DietPi:~/mern-ludo/backend# cd ..
|
||||||
|
root@DietPi:~/mern-ludo# nano Dockerfile
|
||||||
|
root@DietPi:~/mern-ludo# cat Dockerfile
|
||||||
# Node 20 for Pi 5 (ARM64)
|
# Node 20 for Pi 5 (ARM64)
|
||||||
FROM node:20-bullseye-slim
|
FROM node:20-bullseye-slim
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
PORT=8080
|
PORT=8080
|
||||||
# MongoDB connection for backend
|
# MongoDB connection for backend
|
||||||
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27018/ludo?authSource=admin&replicaSet=rs0
|
CONNECTION_URI=mongodb://admin:adminpassword@192.168.0.197:27017/ludo?authSource=admin&replicaSet=rs0
|
||||||
|
|
||||||
NODE_ENV="development"
|
NODE_ENV="development"
|
||||||
@@ -12,19 +12,9 @@ module.exports = socket => {
|
|||||||
if (isMoveValid(req.session, pawn, room)) {
|
if (isMoveValid(req.session, pawn, room)) {
|
||||||
const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber);
|
const newPositionOfMovedPawn = pawn.getPositionAfterMove(room.rolledNumber);
|
||||||
room.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
room.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
||||||
const beaten = room.beatPawns(newPositionOfMovedPawn, req.session.color);
|
room.beatPawns(newPositionOfMovedPawn, req.session.color);
|
||||||
// If pawn killed any opponent pawns, attacker gets another turn.
|
room.changeMovingPlayer();
|
||||||
// Also a roll of 6 grants another turn.
|
|
||||||
// Additionally, grant an extra turn when a pawn reaches its final home position.
|
|
||||||
const FINAL_POSITIONS = { red: 73, blue: 79, green: 85, yellow: 91 };
|
|
||||||
const reachedHome = FINAL_POSITIONS[pawn.color] === newPositionOfMovedPawn;
|
|
||||||
const winner = room.getWinner();
|
const winner = room.getWinner();
|
||||||
if ((beaten > 0 || room.rolledNumber === 6 || reachedHome) && !winner) {
|
|
||||||
room.resetTurnForSamePlayer();
|
|
||||||
} else {
|
|
||||||
room.changeMovingPlayer();
|
|
||||||
}
|
|
||||||
// const winner = room.getWinner();
|
|
||||||
if (winner) {
|
if (winner) {
|
||||||
room.endGame(winner);
|
room.endGame(winner);
|
||||||
sendWinner(room._id.toString(), winner);
|
sendWinner(room._id.toString(), winner);
|
||||||
|
|||||||
@@ -19,12 +19,7 @@ const makeRandomMove = async roomId => {
|
|||||||
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
|
const randomPawn = pawnsThatCanMove[Math.floor(Math.random() * pawnsThatCanMove.length)];
|
||||||
room.movePawn(randomPawn);
|
room.movePawn(randomPawn);
|
||||||
}
|
}
|
||||||
// If player rolled a 6 they should get another turn: keep same player but reset timer
|
room.changeMovingPlayer();
|
||||||
if (room.rolledNumber !== 6) {
|
|
||||||
room.changeMovingPlayer();
|
|
||||||
} else {
|
|
||||||
room.resetTurnForSamePlayer();
|
|
||||||
}
|
|
||||||
const winner = room.getWinner();
|
const winner = room.getWinner();
|
||||||
if (winner) {
|
if (winner) {
|
||||||
room.endGame(winner);
|
room.endGame(winner);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const { getRooms, getRoom, updateRoom, createNewRoom, deleteRoom } = require('../services/roomService');
|
const { getRooms, getRoom, updateRoom, createNewRoom } = require('../services/roomService');
|
||||||
const { sendToOnePlayerRooms, sendToOnePlayerData, sendWinner } = require('../socket/emits');
|
const { sendToOnePlayerRooms, sendToOnePlayerData, sendWinner } = require('../socket/emits');
|
||||||
|
|
||||||
module.exports = socket => {
|
module.exports = socket => {
|
||||||
@@ -8,7 +8,6 @@ module.exports = socket => {
|
|||||||
const room = await getRoom(req.session.roomId);
|
const room = await getRoom(req.session.roomId);
|
||||||
// Handle the situation when the server crashes and any player reconnects after the time has expired
|
// Handle the situation when the server crashes and any player reconnects after the time has expired
|
||||||
// Typically, the responsibility for changing players is managed by gameHandler.js.
|
// Typically, the responsibility for changing players is managed by gameHandler.js.
|
||||||
if(!room) return;
|
|
||||||
if (room.nextMoveTime <= Date.now()) {
|
if (room.nextMoveTime <= Date.now()) {
|
||||||
room.changeMovingPlayer();
|
room.changeMovingPlayer();
|
||||||
await updateRoom(room);
|
await updateRoom(room);
|
||||||
@@ -27,23 +26,7 @@ module.exports = socket => {
|
|||||||
sendToOnePlayerRooms(socket.id, await getRooms());
|
sendToOnePlayerRooms(socket.id, await getRooms());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRoom = async roomId => {
|
|
||||||
try {
|
|
||||||
console.log('🗑️ Attempting to delete room:', roomId);
|
|
||||||
const result = await deleteRoom(roomId);
|
|
||||||
console.log('✅ Room deleted successfully:', result);
|
|
||||||
const updatedRooms = await getRooms();
|
|
||||||
console.log('📋 Updated room count:', updatedRooms.length);
|
|
||||||
console.log('📤 Sending updated rooms to socket:', socket.id);
|
|
||||||
sendToOnePlayerRooms(socket.id, updatedRooms);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error deleting room:', error);
|
|
||||||
socket.emit('error:deleteRoom', 'Failed to delete room');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on('room:data', handleGetData);
|
socket.on('room:data', handleGetData);
|
||||||
socket.on('room:rooms', handleGetAllRooms);
|
socket.on('room:rooms', handleGetAllRooms);
|
||||||
socket.on('room:create', handleCreateRoom);
|
socket.on('room:create', handleCreateRoom);
|
||||||
socket.on('room:delete', handleDeleteRoom);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const PawnSchema = new Schema({
|
|||||||
});
|
});
|
||||||
|
|
||||||
PawnSchema.methods.canMove = function (rolledNumber) {
|
PawnSchema.methods.canMove = function (rolledNumber) {
|
||||||
// Pawn can leave base only when a 6 is rolled
|
if (this.position === this.basePos && (rolledNumber === 6 || rolledNumber === 1)) {
|
||||||
if (this.position === this.basePos && rolledNumber === 6) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// (if player's pawn is near finish line) if the move does not go beyond the win line
|
// (if player's pawn is near finish line) if the move does not go beyond the win line
|
||||||
|
|||||||
+17
-90
@@ -5,17 +5,6 @@ const timeoutManager = require('./timeoutManager.js');
|
|||||||
const PawnSchema = require('./pawn');
|
const PawnSchema = require('./pawn');
|
||||||
const PlayerSchema = require('./player');
|
const PlayerSchema = require('./player');
|
||||||
|
|
||||||
// Safe/colored box positions in Ludo (where pawns cannot be killed)
|
|
||||||
const SAFE_POSITIONS = [16, 29, 42, 55];
|
|
||||||
// const HOME_ENTRY_POSITIONS = [66, 27, 40, 53];
|
|
||||||
const STAR_POSITIONS = [63, 24, 37, 50];
|
|
||||||
|
|
||||||
const isSafePosition = (position) => {
|
|
||||||
return SAFE_POSITIONS.includes(position)
|
|
||||||
|| STAR_POSITIONS.includes(position)
|
|
||||||
|| position > 66; // Also safe in home stretch
|
|
||||||
};
|
|
||||||
|
|
||||||
const RoomSchema = new mongoose.Schema({
|
const RoomSchema = new mongoose.Schema({
|
||||||
name: String,
|
name: String,
|
||||||
private: { type: Boolean, default: false },
|
private: { type: Boolean, default: false },
|
||||||
@@ -47,42 +36,23 @@ const RoomSchema = new mongoose.Schema({
|
|||||||
});
|
});
|
||||||
|
|
||||||
RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
|
RoomSchema.methods.beatPawns = function (position, attackingPawnColor) {
|
||||||
// Do not beat pawns on safe/colored positions
|
|
||||||
if (isSafePosition(position)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let beatenCount = 0;
|
|
||||||
const pawnsOnPosition = this.pawns.filter(pawn => pawn.position === position);
|
const pawnsOnPosition = this.pawns.filter(pawn => pawn.position === position);
|
||||||
pawnsOnPosition.forEach(pawn => {
|
pawnsOnPosition.forEach(pawn => {
|
||||||
if (pawn.color !== attackingPawnColor) {
|
if (pawn.color !== attackingPawnColor) {
|
||||||
const index = this.getPawnIndex(pawn._id);
|
const index = this.getPawnIndex(pawn._id);
|
||||||
if (index !== -1 && this.pawns[index].position !== this.pawns[index].basePos) {
|
this.pawns[index].position = this.pawns[index].basePos;
|
||||||
this.pawns[index].position = this.pawns[index].basePos;
|
|
||||||
beatenCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return beatenCount;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.changeMovingPlayer = function () {
|
RoomSchema.methods.changeMovingPlayer = function () {
|
||||||
if (this.winner) return;
|
if (this.winner) return;
|
||||||
if (!Array.isArray(this.players) || this.players.length === 0) {
|
const playerIndex = this.players.findIndex(player => player.nowMoving === true);
|
||||||
console.warn(`[room:${this._id}] changeMovingPlayer: players array is empty or null`);
|
this.players[playerIndex].nowMoving = false;
|
||||||
return;
|
if (playerIndex + 1 === this.players.length) {
|
||||||
}
|
|
||||||
const playerIndex = this.players.findIndex(player => player && player.nowMoving === true);
|
|
||||||
if (playerIndex === -1) {
|
|
||||||
console.warn(`[room:${this._id}] changeMovingPlayer: no player currently marked as nowMoving`);
|
|
||||||
// Default to first player
|
|
||||||
this.players[0].nowMoving = true;
|
this.players[0].nowMoving = true;
|
||||||
} else {
|
} else {
|
||||||
this.players[playerIndex].nowMoving = false;
|
this.players[playerIndex + 1].nowMoving = true;
|
||||||
if (playerIndex + 1 === this.players.length) {
|
|
||||||
this.players[0].nowMoving = true;
|
|
||||||
} else {
|
|
||||||
this.players[playerIndex + 1].nowMoving = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.nextMoveTime = Date.now() + MOVE_TIME;
|
this.nextMoveTime = Date.now() + MOVE_TIME;
|
||||||
this.rolledNumber = null;
|
this.rolledNumber = null;
|
||||||
@@ -90,22 +60,6 @@ RoomSchema.methods.changeMovingPlayer = function () {
|
|||||||
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
|
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.resetTurnForSamePlayer = function () {
|
|
||||||
if (this.winner) return;
|
|
||||||
// Keep the same player moving but reset the roll and move timer
|
|
||||||
const now = Date.now();
|
|
||||||
const currentPlayer = this.getCurrentlyMovingPlayer();
|
|
||||||
if (!currentPlayer) {
|
|
||||||
console.warn(`[room:${this._id}] resetTurnForSamePlayer: No current player found!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.nextMoveTime = now + MOVE_TIME;
|
|
||||||
this.rolledNumber = null;
|
|
||||||
console.log(`[room:${this._id}] resetTurnForSamePlayer: player=${currentPlayer.color}, now=${now}, nextMoveTime=${this.nextMoveTime}, MOVE_TIME=${MOVE_TIME}`);
|
|
||||||
timeoutManager.clear(this._id.toString());
|
|
||||||
timeoutManager.set(makeRandomMove, MOVE_TIME, this._id.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomSchema.methods.movePawn = function (pawn) {
|
RoomSchema.methods.movePawn = function (pawn) {
|
||||||
const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber);
|
const newPositionOfMovedPawn = pawn.getPositionAfterMove(this.rolledNumber);
|
||||||
this.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
this.changePositionOfPawn(pawn, newPositionOfMovedPawn);
|
||||||
@@ -114,9 +68,8 @@ RoomSchema.methods.movePawn = function (pawn) {
|
|||||||
|
|
||||||
RoomSchema.methods.getPawnsThatCanMove = function () {
|
RoomSchema.methods.getPawnsThatCanMove = function () {
|
||||||
const movingPlayer = this.getCurrentlyMovingPlayer();
|
const movingPlayer = this.getCurrentlyMovingPlayer();
|
||||||
if (!movingPlayer) return [];
|
|
||||||
const playerPawns = this.getPlayerPawns(movingPlayer.color);
|
const playerPawns = this.getPlayerPawns(movingPlayer.color);
|
||||||
return (playerPawns || []).filter(pawn => pawn.canMove(this.rolledNumber));
|
return playerPawns.filter(pawn => pawn.canMove(this.rolledNumber));
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) {
|
RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) {
|
||||||
@@ -125,15 +78,10 @@ RoomSchema.methods.changePositionOfPawn = function (pawn, newPosition) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.canStartGame = function () {
|
RoomSchema.methods.canStartGame = function () {
|
||||||
if (!Array.isArray(this.players)) return false;
|
return this.players.filter(player => player.ready).length >= 2;
|
||||||
return this.players.filter(player => player && player.ready).length >= 2;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.startGame = function () {
|
RoomSchema.methods.startGame = function () {
|
||||||
if (!Array.isArray(this.players) || this.players.length === 0) {
|
|
||||||
console.warn(`[room:${this._id}] startGame: players array empty`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.started = true;
|
this.started = true;
|
||||||
this.nextMoveTime = Date.now() + MOVE_TIME;
|
this.nextMoveTime = Date.now() + MOVE_TIME;
|
||||||
this.players.forEach(player => (player.ready = true));
|
this.players.forEach(player => (player.ready = true));
|
||||||
@@ -145,30 +93,28 @@ RoomSchema.methods.endGame = function (winner) {
|
|||||||
timeoutManager.clear(this._id.toString());
|
timeoutManager.clear(this._id.toString());
|
||||||
this.rolledNumber = null;
|
this.rolledNumber = null;
|
||||||
this.nextMoveTime = null;
|
this.nextMoveTime = null;
|
||||||
if (Array.isArray(this.players)) this.players.forEach(player => (player.nowMoving = false));
|
this.players.map(player => (player.nowMoving = false));
|
||||||
this.winner = winner;
|
this.winner = winner;
|
||||||
this.save();
|
this.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.getWinner = function () {
|
RoomSchema.methods.getWinner = function () {
|
||||||
if (!Array.isArray(this.pawns)) return null;
|
if (this.pawns.filter(pawn => pawn.color === 'red' && pawn.position === 73).length === 4) {
|
||||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'red' && pawn.position === 73).length === 4) {
|
|
||||||
return 'red';
|
return 'red';
|
||||||
}
|
}
|
||||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'blue' && pawn.position === 79).length === 4) {
|
if (this.pawns.filter(pawn => pawn.color === 'blue' && pawn.position === 79).length === 4) {
|
||||||
return 'blue';
|
return 'blue';
|
||||||
}
|
}
|
||||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'green' && pawn.position === 85).length === 4) {
|
if (this.pawns.filter(pawn => pawn.color === 'green' && pawn.position === 85).length === 4) {
|
||||||
return 'green';
|
return 'green';
|
||||||
}
|
}
|
||||||
if (this.pawns.filter(pawn => pawn && pawn.color === 'yellow' && pawn.position === 91).length === 4) {
|
if (this.pawns.filter(pawn => pawn.color === 'yellow' && pawn.position === 91).length === 4) {
|
||||||
return 'yellow';
|
return 'yellow';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.isFull = function () {
|
RoomSchema.methods.isFull = function () {
|
||||||
if (!Array.isArray(this.players)) return false;
|
|
||||||
if (this.players.length === 4) {
|
if (this.players.length === 4) {
|
||||||
this.full = true;
|
this.full = true;
|
||||||
}
|
}
|
||||||
@@ -176,13 +122,11 @@ RoomSchema.methods.isFull = function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.getPlayer = function (playerId) {
|
RoomSchema.methods.getPlayer = function (playerId) {
|
||||||
if (!Array.isArray(this.players)) return null;
|
return this.players.find(player => player._id.toString() === playerId.toString());
|
||||||
return this.players.find(player => player && player._id && player._id.toString() === playerId.toString()) || null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.addPlayer = function (name, id) {
|
RoomSchema.methods.addPlayer = function (name, id) {
|
||||||
if (this.full) return;
|
if (this.full) return;
|
||||||
if (!Array.isArray(this.players)) this.players = [];
|
|
||||||
this.players.push({
|
this.players.push({
|
||||||
sessionID: id,
|
sessionID: id,
|
||||||
name: name,
|
name: name,
|
||||||
@@ -192,36 +136,19 @@ RoomSchema.methods.addPlayer = function (name, id) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.getPawnIndex = function (pawnId) {
|
RoomSchema.methods.getPawnIndex = function (pawnId) {
|
||||||
if (!Array.isArray(this.pawns)) return -1;
|
return this.pawns.findIndex(pawn => pawn._id.toString() === pawnId.toString());
|
||||||
return this.pawns.findIndex(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.getPawn = function (pawnId) {
|
RoomSchema.methods.getPawn = function (pawnId) {
|
||||||
if (!Array.isArray(this.pawns)) return null;
|
return this.pawns.find(pawn => pawn._id.toString() === pawnId.toString());
|
||||||
return this.pawns.find(pawn => pawn && pawn._id && pawn._id.toString() === pawnId.toString()) || null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.getPlayerPawns = function (color) {
|
RoomSchema.methods.getPlayerPawns = function (color) {
|
||||||
if (!Array.isArray(this.pawns)) return [];
|
return this.pawns.filter(pawn => pawn.color === color);
|
||||||
return this.pawns.filter(pawn => pawn && pawn.color === color);
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomSchema.methods.getPawnsOnPosition = function (position) {
|
|
||||||
if (!Array.isArray(this.pawns)) return [];
|
|
||||||
return this.pawns.filter(pawn => pawn && pawn.position === position);
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomSchema.methods.isStacked = function (position) {
|
|
||||||
return this.getPawnsOnPosition(position).length > 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
RoomSchema.methods.getOpponentPawnsOnPosition = function (position, color) {
|
|
||||||
return this.getPawnsOnPosition(position).filter(pawn => pawn.color !== color);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomSchema.methods.getCurrentlyMovingPlayer = function () {
|
RoomSchema.methods.getCurrentlyMovingPlayer = function () {
|
||||||
if (!Array.isArray(this.players)) return null;
|
return this.players.find(player => player.nowMoving === true);
|
||||||
return this.players.find(player => player && player.nowMoving === true) || null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Room = mongoose.model('Room', RoomSchema);
|
const Room = mongoose.model('Room', RoomSchema);
|
||||||
|
|||||||
@@ -23,21 +23,8 @@ const createNewRoom = async data => {
|
|||||||
return room;
|
return room;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRoom = async roomId => {
|
|
||||||
return await Room.findByIdAndDelete(roomId).exec();
|
|
||||||
};
|
|
||||||
|
|
||||||
Room.watch().on('change', async data => {
|
Room.watch().on('change', async data => {
|
||||||
// Ignore delete operations
|
sendToPlayersData(await getRoom(data.documentKey._id));
|
||||||
if (data.operationType === 'delete') {
|
|
||||||
console.log('🗑️ Room deleted, skipping sendToPlayersData');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = await getRoom(data.documentKey._id);
|
|
||||||
if (room) {
|
|
||||||
sendToPlayersData(room);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom, deleteRoom };
|
module.exports = { getRoom, getRooms, updateRoom, getJoinableRoom, createNewRoom };
|
||||||
|
|||||||
+1
-2
@@ -9,8 +9,7 @@ services:
|
|||||||
- ludo-net
|
- ludo-net
|
||||||
volumes:
|
volumes:
|
||||||
- ludo-mongo-data:/data/db
|
- ludo-mongo-data:/data/db
|
||||||
- /opt/ludo-secrets/mongo-keyfile:/etc/mongo-keyfile:ro
|
- ./mongo-keyfile:/etc/mongo-keyfile:ro
|
||||||
# - ./mongo-keyfile:/etc/mongo-keyfile:ro
|
|
||||||
# - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
# - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
||||||
command:
|
command:
|
||||||
- bash
|
- bash
|
||||||
|
|||||||
+1
-7
@@ -13,13 +13,7 @@ function App() {
|
|||||||
const [playerSocket, setPlayerSocket] = useState();
|
const [playerSocket, setPlayerSocket] = useState();
|
||||||
const [redirect, setRedirect] = useState();
|
const [redirect, setRedirect] = useState();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let socket;
|
const socket = io(`http://${window.location.hostname}:8080`, { withCredentials: true });
|
||||||
if(process.env.NODE_ENV !== 'production') {
|
|
||||||
socket = io(`http://${window.location.hostname}:8080`, { withCredentials: true });
|
|
||||||
} else {
|
|
||||||
socket = io(`${window.location.protocol}//${window.location.host}`, { withCredentials: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on('player:data', data => {
|
socket.on('player:data', data => {
|
||||||
data = JSON.parse(data);
|
data = JSON.parse(data);
|
||||||
setPlayerData(data);
|
setPlayerData(data);
|
||||||
|
|||||||
@@ -85,11 +85,7 @@ const Gameboard = () => {
|
|||||||
<div className={styles.winnerContainer}>
|
<div className={styles.winnerContainer}>
|
||||||
<img src={trophyImage} alt='winner' />
|
<img src={trophyImage} alt='winner' />
|
||||||
<h1>
|
<h1>
|
||||||
{context.color === winner ? (
|
1st: <span style={{ color: winner }}>{winner}</span>
|
||||||
<>You Won!</>
|
|
||||||
) : (
|
|
||||||
<>1st: <span style={{ color: winner }}>{winner}</span></>
|
|
||||||
)}
|
|
||||||
</h1>
|
</h1>
|
||||||
<button onClick={() => socket.emit('player:exit')}>Play again</button>
|
<button onClick={() => socket.emit('player:exit')}>Play again</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,20 +1,82 @@
|
|||||||
.winnerContainer {
|
.winnerContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 40px;
|
padding: 30px 20px;
|
||||||
width: 300px;
|
width: 90vw;
|
||||||
height: 250px;
|
max-width: 320px;
|
||||||
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 20px;
|
margin: 20px auto;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
gap: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.winnerContainer > img {
|
||||||
|
max-width: 80px;
|
||||||
|
height: auto;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winnerContainer > h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 10px 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.winnerContainer > button {
|
.winnerContainer > button {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
width: 200px;
|
width: 100%;
|
||||||
|
max-width: 180px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winnerContainer > button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winnerContainer > button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.winnerContainer {
|
||||||
|
padding: 25px 15px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winnerContainer > h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.winnerContainer {
|
||||||
|
width: 85vw;
|
||||||
|
padding: 20px 15px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winnerContainer > h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winnerContainer > button {
|
||||||
|
max-width: 150px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import positionMapCoords from '../positions';
|
|||||||
import pawnImages from '../../../constants/pawnImages';
|
import pawnImages from '../../../constants/pawnImages';
|
||||||
import canPawnMove from './canPawnMove';
|
import canPawnMove from './canPawnMove';
|
||||||
import getPositionAfterMove from './getPositionAfterMove';
|
import getPositionAfterMove from './getPositionAfterMove';
|
||||||
import { pawnsAt } from './pawnHelpers';
|
import styles from './Map.module.css';
|
||||||
|
|
||||||
const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
||||||
const player = useContext(PlayerDataContext);
|
const player = useContext(PlayerDataContext);
|
||||||
@@ -15,26 +15,15 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
|||||||
|
|
||||||
const [hintPawn, setHintPawn] = useState();
|
const [hintPawn, setHintPawn] = useState();
|
||||||
|
|
||||||
const paintPawn = (context, pawn, x, y, pawnWidth = 28, onDraw) => {
|
const paintPawn = (context, pawn) => {
|
||||||
const pawnHeight = Math.round((pawnWidth * 30) / 35);
|
const { x, y } = positionMapCoords[pawn.position];
|
||||||
const touchableArea = new Path2D();
|
const touchableArea = new Path2D();
|
||||||
const touchRadius = Math.max(6, Math.round(pawnWidth / 2));
|
touchableArea.arc(x, y, 12, 0, 2 * Math.PI);
|
||||||
touchableArea.arc(x, y, touchRadius, 0, 2 * Math.PI);
|
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
const imgSrc = pawnImages[pawn.color] || pawnImages['red'];
|
image.src = pawnImages[pawn.color];
|
||||||
image.src = imgSrc;
|
image.onload = function () {
|
||||||
|
context.drawImage(image, x - 17, y - 15, 35, 30);
|
||||||
const drawAndCallback = () => {
|
|
||||||
context.drawImage(image, x - Math.round(pawnWidth / 2), y - Math.round(pawnHeight / 2), pawnWidth, pawnHeight);
|
|
||||||
if (typeof onDraw === 'function') onDraw();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (image.complete) {
|
|
||||||
drawAndCallback();
|
|
||||||
} else {
|
|
||||||
image.onload = drawAndCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return touchableArea;
|
return touchableArea;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,7 +34,7 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
|||||||
cursorX = event.clientX - rect.left,
|
cursorX = event.clientX - rect.left,
|
||||||
cursorY = event.clientY - rect.top;
|
cursorY = event.clientY - rect.top;
|
||||||
for (const pawn of pawns) {
|
for (const pawn of pawns) {
|
||||||
if (pawn.touchableArea && ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
|
if (ctx.isPointInPath(pawn.touchableArea, cursorX, cursorY)) {
|
||||||
if (canPawnMove(pawn, rolledNumber)) socket.emit('game:move', pawn._id);
|
if (canPawnMove(pawn, rolledNumber)) socket.emit('game:move', pawn._id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +51,6 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
|||||||
canvas.style.cursor = 'default';
|
canvas.style.cursor = 'default';
|
||||||
for (const pawn of pawns) {
|
for (const pawn of pawns) {
|
||||||
if (
|
if (
|
||||||
pawn.touchableArea &&
|
|
||||||
ctx.isPointInPath(pawn.touchableArea, x, y) &&
|
ctx.isPointInPath(pawn.touchableArea, x, y) &&
|
||||||
player.color === pawn.color &&
|
player.color === pawn.color &&
|
||||||
canPawnMove(pawn, rolledNumber)
|
canPawnMove(pawn, rolledNumber)
|
||||||
@@ -87,88 +75,11 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
|||||||
image.src = mapImage;
|
image.src = mapImage;
|
||||||
image.onload = function () {
|
image.onload = function () {
|
||||||
ctx.drawImage(image, 0, 0);
|
ctx.drawImage(image, 0, 0);
|
||||||
// draw pawns grouped by position with 2-row layout
|
|
||||||
const drawn = new Set();
|
|
||||||
pawns.forEach((pawn, index) => {
|
pawns.forEach((pawn, index) => {
|
||||||
if (drawn.has(pawn._id)) return;
|
pawns[index].touchableArea = paintPawn(ctx, pawn);
|
||||||
const group = pawnsAt(pawns, pawn.position);
|
|
||||||
const total = group.length;
|
|
||||||
const center = positionMapCoords[pawn.position];
|
|
||||||
if (!center) return;
|
|
||||||
|
|
||||||
const slotOffsetX = 14; // horizontal spacing
|
|
||||||
const slotOffsetY = 2; // minimal vertical spacing between rows
|
|
||||||
|
|
||||||
// helper to draw a single representative pawn for a color group
|
|
||||||
// and show a small badge with the count when there are multiple pawns
|
|
||||||
const drawColorGroup = (pawnGroup, x, y, baseWidth) => {
|
|
||||||
if (!pawnGroup || pawnGroup.length === 0) return null;
|
|
||||||
const rep = pawnGroup[0];
|
|
||||||
// draw pawn and then badge after image renders so badge is on top
|
|
||||||
const touch = paintPawn(ctx, rep, x, y, baseWidth, () => {
|
|
||||||
if (pawnGroup.length > 1) {
|
|
||||||
const badgeRadius = 8; // smaller badge
|
|
||||||
const badgeX = x + Math.round(baseWidth / 2) - 4; // slightly closer to pawn edge
|
|
||||||
const badgeY = y - Math.round(baseWidth / 2) + 4;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.fillStyle = 'rgba(0,0,0,0.85)';
|
|
||||||
ctx.arc(badgeX, badgeY, badgeRadius, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.fillStyle = 'white';
|
|
||||||
ctx.font = '10px Arial'; // smaller text
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(String(pawnGroup.length), badgeX, badgeY);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const foundIndex = pawns.findIndex(pp => pp._id === rep._id);
|
|
||||||
if (foundIndex !== -1) pawns[foundIndex].touchableArea = touch;
|
|
||||||
return touch;
|
|
||||||
};
|
|
||||||
|
|
||||||
// group pawns by color so same-color pawns stack vertically (tight)
|
|
||||||
const colorGroups = {};
|
|
||||||
group.forEach(g => {
|
|
||||||
if (!g) return;
|
|
||||||
if (!colorGroups[g.color]) colorGroups[g.color] = [];
|
|
||||||
colorGroups[g.color].push(g);
|
|
||||||
});
|
|
||||||
const colors = Object.keys(colorGroups);
|
|
||||||
const nColors = colors.length;
|
|
||||||
const colorSpacing = slotOffsetX; // horizontal spacing between different colors
|
|
||||||
|
|
||||||
colors.forEach((color, ci) => {
|
|
||||||
const pawnsOfColor = colorGroups[color];
|
|
||||||
const x = center.x + (ci - (nColors - 1) / 2) * colorSpacing;
|
|
||||||
const baseWidth = pawnsOfColor.length > 1 ? 20 : total === 1 ? 28 : 24;
|
|
||||||
drawColorGroup(pawnsOfColor, x, center.y, baseWidth);
|
|
||||||
pawnsOfColor.forEach(p => drawn.add(p._id));
|
|
||||||
});
|
|
||||||
|
|
||||||
// badge for >6 pawns
|
|
||||||
if (total > 6) {
|
|
||||||
const badgeX = center.x + slotOffsetX + 12;
|
|
||||||
const badgeY = center.y - slotOffsetY - 12;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.fillStyle = 'rgba(0,0,0,0.75)';
|
|
||||||
ctx.arc(badgeX, badgeY, 12, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
ctx.fillStyle = 'white';
|
|
||||||
ctx.font = '11px Arial';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(String(total), badgeX, badgeY);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (hintPawn) {
|
if (hintPawn) {
|
||||||
// draw hint as a grey circle at target position
|
paintPawn(ctx, hintPawn);
|
||||||
const pos = positionMapCoords[hintPawn.position];
|
|
||||||
if (pos) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.fillStyle = 'rgba(128,128,128,0.6)';
|
|
||||||
ctx.arc(pos.x, pos.y, 14, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -176,14 +87,16 @@ const Map = ({ pawns, nowMoving, rolledNumber }) => {
|
|||||||
}, [hintPawn, pawns]);
|
}, [hintPawn, pawns]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<div className={styles.canvasContainer}>
|
||||||
className='canvas-container'
|
<canvas
|
||||||
width={460}
|
className={styles.canvas}
|
||||||
height={460}
|
width={460}
|
||||||
ref={canvasRef}
|
height={460}
|
||||||
onClick={handleCanvasClick}
|
ref={canvasRef}
|
||||||
onMouseMove={handleMouseMove}
|
onClick={handleCanvasClick}
|
||||||
/>
|
onMouseMove={handleMouseMove}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Map;
|
export default Map;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.canvasContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;\n width: 100%;\n height: 100%;\n padding: 10px;\n box-sizing: border-box;\n}\n\ncanvas.canvas {\n border-radius: 15px;\n border: 2px solid black;\n width: 460px;\n height: 460px;\n max-width: 100%;\n height: auto;\n aspect-ratio: 1 / 1;\n touch-action: none;\n}\n\n/* Tablet (768px - 1024px) */\n@media (max-width: 1024px) {\n .canvasContainer {\n padding: 8px;\n }\n\n canvas.canvas {\n width: 400px;\n max-width: 90vw;\n }\n}\n\n/* Mobile (less than 768px) */\n@media (max-width: 767px) {\n .canvasContainer {\n padding: 5px;\n }\n\n canvas.canvas {\n width: 90vw;\n max-width: 460px;\n }\n}\n\n/* Small Mobile (less than 480px) */\n@media (max-width: 479px) {\n canvas.canvas {\n width: 95vw;\n max-width: 100%;\n }\n}\n
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const canPawnMove = (pawn, rolledNumber) => {
|
const canPawnMove = (pawn, rolledNumber) => {
|
||||||
// If is in base
|
// If is in base
|
||||||
if ((rolledNumber === 6) && pawn.position === pawn.basePos) {
|
if ((rolledNumber === 1 || rolledNumber === 6) && pawn.position === pawn.basePos) {
|
||||||
return true;
|
return true;
|
||||||
// Other situations: pawn is on map or pawn is in end positions
|
// Other situations: pawn is on map or pawn is in end positions
|
||||||
} else if (pawn.position !== pawn.basePos) {
|
} else if (pawn.position !== pawn.basePos) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const getPositionAfterMove = (pawn, rolledNumber) => {
|
|||||||
return position;
|
return position;
|
||||||
}
|
}
|
||||||
case 'yellow':
|
case 'yellow':
|
||||||
if (pawn.position + rolledNumber <= 91) {
|
if (pawn.position + rolledNumber <= 85) {
|
||||||
if (position >= 12 && position <= 15) {
|
if (position >= 12 && position <= 15) {
|
||||||
return 29;
|
return 29;
|
||||||
} else if (position <= 67 && position + rolledNumber > 67) {
|
} else if (position <= 67 && position + rolledNumber > 67) {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export const pawnsAt = (pawns, position) => {
|
|
||||||
return Array.isArray(pawns) ? pawns.filter(p => p && p.position === position) : [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const countPawnsAt = (pawns, position) => pawnsAt(pawns, position).length;
|
|
||||||
|
|
||||||
export const opponentsAt = (pawns, position, myColor) =>
|
|
||||||
pawnsAt(pawns, position).filter(p => p.color !== myColor);
|
|
||||||
|
|
||||||
export default { pawnsAt, countPawnsAt, opponentsAt };
|
|
||||||
@@ -4,12 +4,91 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > input,
|
||||||
|
.formContainer > button {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 350px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid white;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > input:focus,
|
||||||
|
.formContainer > button:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > button {
|
||||||
|
background-color: #007bff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.privateContainer {
|
.privateContainer {
|
||||||
margin-left: 10px;
|
width: 90%;
|
||||||
|
max-width: 350px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
justify-content: space-between;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.privateContainer > label {
|
||||||
|
color: white;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.formContainer > input,
|
||||||
|
.formContainer > button,
|
||||||
|
.privateContainer {
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.formContainer {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > input,
|
||||||
|
.formContainer > button,
|
||||||
|
.privateContainer {
|
||||||
|
max-width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formContainer > input,
|
||||||
|
.formContainer > button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,23 +20,12 @@ const JoinServer = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket.emit('room:rooms');
|
socket.emit('room:rooms');
|
||||||
socket.on('room:rooms', () => {
|
socket.on('room:rooms', () => {
|
||||||
console.log('✅ Room list updated');
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
socket.on('error:deleteRoom', (error) => {
|
|
||||||
console.error('❌ Delete error:', error);
|
|
||||||
alert('Failed to delete server: ' + error);
|
|
||||||
getRooms();
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
socket.off('error:deleteRoom');
|
|
||||||
socket.off('room:rooms');
|
|
||||||
};
|
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
const getRooms = () => {
|
const getRooms = () => {
|
||||||
setRooms([]);
|
setRooms([]);
|
||||||
setIsLoading(true);
|
|
||||||
socket.emit('room:rooms');
|
socket.emit('room:rooms');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,15 +34,6 @@ const JoinServer = () => {
|
|||||||
setJoining(true);
|
setJoining(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = roomId => {
|
|
||||||
if (window.confirm('Are you sure you want to delete this server?')) {
|
|
||||||
console.log('🗑️ Deleting room:', roomId);
|
|
||||||
setIsLoading(true);
|
|
||||||
setRooms([]); // Clear the list immediately
|
|
||||||
socket.emit('room:delete', roomId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ServersTableWithLoading = withLoading(ServersTable);
|
const ServersTableWithLoading = withLoading(ServersTable);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,7 +51,6 @@ const JoinServer = () => {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
rooms={rooms}
|
rooms={rooms}
|
||||||
handleJoinClick={handleJoinClick}
|
handleJoinClick={handleJoinClick}
|
||||||
handleDeleteClick={handleDeleteClick}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,50 @@
|
|||||||
.serversTableContainer {
|
.serversTableContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
height: 500px;
|
max-height: 500px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refresh {
|
.refresh {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 40px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
|
padding: 0 5px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refresh > img {
|
.refresh > img {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh > img:hover {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh > img:active {
|
||||||
|
transform: rotate(180deg) scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.serversTableContainer {
|
||||||
|
max-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.serversTableContainer {
|
||||||
|
max-height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
import lock from '../../../../images/login-page/lock.png';
|
import lock from '../../../../images/login-page/lock.png';
|
||||||
import styles from './ServersTable.module.css';
|
import styles from './ServersTable.module.css';
|
||||||
|
|
||||||
const ServerListTable = ({ rooms, handleJoinClick, handleDeleteClick }) => {
|
const ServerListTable = ({ rooms, handleJoinClick }) => {
|
||||||
const safeRooms = Array.isArray(rooms) ? rooms : [];
|
|
||||||
return (
|
return (
|
||||||
<table className={styles.rooms}>
|
<div className={styles.tableContainer}>
|
||||||
<thead>
|
<table className={styles.rooms}>
|
||||||
<tr>
|
<thead>
|
||||||
<th className={styles.firstColumn}></th>
|
<tr>
|
||||||
<th>Server</th>
|
<th className={styles.firstColumn} aria-label="Private"></th>
|
||||||
<th>#/#</th>
|
<th>Server</th>
|
||||||
<th>Status</th>
|
<th>Players</th>
|
||||||
<th className={styles.lastColumn}></th>
|
<th>Status</th>
|
||||||
</tr>
|
<th className={styles.lastColumn} aria-label="Action"></th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{safeRooms.map((room, index) => {
|
<tbody>
|
||||||
if (!room) return null;
|
{rooms.map((room, index) => {
|
||||||
return room.started ? null : (
|
return room.started ? null : (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{room.private ? <img src={lock} alt='private' /> : null}</td>
|
<td data-label="Private">{room.private ? <img src={lock} alt='private' /> : null}</td>
|
||||||
<td className={styles.roomName}>{room.name}</td>
|
<td className={styles.roomName} data-label="Server">{room.name}</td>
|
||||||
<td>{`${(room.players && room.players.length) || 0}/4`}</td>
|
<td data-label="Players">{`${room.players.length}/4`}</td>
|
||||||
<td>{room.isStarted ? 'started' : 'waiting'}</td>
|
<td data-label="Status">{room.isStarted ? 'started' : 'waiting'}</td>
|
||||||
<td className={styles.lastColumn}>
|
<td className={styles.lastColumn} data-label="Action">
|
||||||
<button onClick={() => handleJoinClick(room)}>Join</button>
|
<button onClick={() => handleJoinClick(room)}>Join</button>
|
||||||
<button onClick={() => handleDeleteClick(room._id)} className={styles.deleteBtn}>
|
</td>
|
||||||
Delete
|
</tr>
|
||||||
</button>
|
);
|
||||||
</td>
|
})}
|
||||||
</tr>
|
</tbody>
|
||||||
);
|
</table>
|
||||||
})}
|
</div>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,48 @@
|
|||||||
|
.tableContainer {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
height: fit-content;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > thead > tr {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > th {
|
||||||
|
padding: 10px 8px;
|
||||||
|
text-align: center;
|
||||||
|
height: auto;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > thead > tr :nth-child(2) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr > td {
|
||||||
|
padding: 10px 8px;
|
||||||
|
text-align: center;
|
||||||
|
height: auto;
|
||||||
|
border-bottom: 1px solid #e9e9e9;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr:hover {
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
.roomName {
|
.roomName {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -5,49 +50,162 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
}
|
}
|
||||||
.rooms > thead > tr :nth-child(2) {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.rooms > tbody > tr > td > img {
|
.rooms > tbody > tr > td > img {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
width: 20px;
|
width: 18px;
|
||||||
height: 20px;
|
height: 18px;
|
||||||
}
|
|
||||||
|
|
||||||
.rooms > th {
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
.rooms > tbody > tr > td {
|
|
||||||
padding: 4px;
|
|
||||||
text-align: center;
|
|
||||||
height: 50px;
|
|
||||||
}
|
|
||||||
.rooms > tbody > tr > td {
|
|
||||||
max-height: 50px;
|
|
||||||
height: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rooms {
|
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.lastColumn {
|
.lastColumn {
|
||||||
width: 130px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.firstColumn {
|
.firstColumn {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.deleteBtn {
|
.rooms button {
|
||||||
margin-left: 5px;
|
padding: 6px 12px;
|
||||||
background-color: #ff4444;
|
background-color: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 4px 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet (768px - 1024px) */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.rooms > th,
|
||||||
|
.rooms > tbody > tr > td {
|
||||||
|
padding: 8px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastColumn {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomName {
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile (less than 768px) */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.tableContainer {
|
||||||
|
margin: 0 -5px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr {
|
||||||
|
display: block;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr > td {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 90px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
text-align: right;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr > td:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr > td:first-child {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
grid-template-columns: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr > td:before {
|
||||||
|
content: attr(data-label);
|
||||||
|
font-weight: 600;
|
||||||
|
color: #666;
|
||||||
|
min-width: 85px;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomName {
|
||||||
|
max-width: 100%;
|
||||||
|
text-align: right !important;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.roomName:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr > td:first-child:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastColumn {
|
||||||
|
width: 100%;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lastColumn button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.firstColumn {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rooms > tbody > tr > td > img {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,27 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
height: 50%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 20px 10px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
gap: 15px;
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 5px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,90 @@
|
|||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 10px 20px 60px 20px;
|
padding: 20px 15px;
|
||||||
width: 300px;
|
width: 90vw;
|
||||||
|
max-width: 340px;
|
||||||
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
background: radial-gradient(circle, rgba(0, 138, 255, 1) 5%, rgba(9, 9, 121, 1) 81%);
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 20px;
|
margin: 20px auto;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container > input {
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid white;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
color: white;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
.container > button {
|
.container > button {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
|
padding: 10px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
.container > input {
|
|
||||||
margin-top: 10px;
|
.container > button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > button:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
max-width: 300px;
|
||||||
|
padding: 18px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > input {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > button {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
width: 85vw;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 15px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > button {
|
||||||
|
width: 90px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
.container {
|
.container {
|
||||||
margin: 50px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 500px;
|
width: 90vw;
|
||||||
|
max-width: 500px;
|
||||||
color: white;
|
color: white;
|
||||||
|
margin: 10px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -14,19 +15,22 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
min-height: 40px;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transform: scaleX(1.02);
|
transform: scaleX(1.02);
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
padding-left: 10px;
|
padding: 0 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title > h1 {
|
.title > h1 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
@@ -35,12 +39,47 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: 5px;
|
padding: 10px;
|
||||||
padding-right: 5px;
|
|
||||||
padding-top: 10px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
border-left: 1px solid black;
|
border-left: 1px solid black;
|
||||||
border-right: 1px solid black;
|
border-right: 1px solid black;
|
||||||
border-bottom: 1px solid black;
|
border-bottom: 1px solid black;
|
||||||
|
border-radius: 0 0 2px 2px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
max-width: 450px;
|
||||||
|
margin: 8px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title > h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
width: 95vw;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 5px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
min-height: 35px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title > h1 {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 8px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,47 @@
|
|||||||
.container {
|
.container {
|
||||||
margin-left: 20px;
|
display: flex;
|
||||||
margin-right: 20px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px 10px;
|
||||||
width: 50px;
|
width: 50px;
|
||||||
|
min-width: 50px;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
min-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container > img {
|
.container > img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: transform 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > img:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > img:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
margin: 4px 8px;
|
||||||
|
width: 45px;
|
||||||
|
min-width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
min-height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
margin: 3px 6px;
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,17 @@
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
animation: timerAnimation 15s linear infinite;
|
animation: timerAnimation 15s linear infinite;
|
||||||
transition-duration: 15s;
|
transition-duration: 15s;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes timerAnimation {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.8);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px rgba(255, 215, 0, 1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 10px rgba(255, 215, 0, 0.8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,46 @@
|
|||||||
.container {
|
.container {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 100px;
|
|
||||||
min-height: 50px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 80px;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: 5px 10px;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > p {
|
||||||
|
margin: 0;
|
||||||
|
white-space: normal;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
min-width: 70px;
|
||||||
|
min-height: 45px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-width: 1.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
min-width: 60px;
|
||||||
|
min-height: 40px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,47 @@
|
|||||||
.playerContainer {
|
.playerContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 70px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
.red {
|
||||||
margin-bottom: 50px;
|
order: 1;
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yellow {
|
.yellow {
|
||||||
margin-bottom: 50px;
|
order: 2;
|
||||||
flex-flow: row-reverse;
|
flex-direction: row-reverse;
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue {
|
.blue {
|
||||||
margin-top: 50px;
|
order: 3;
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.green {
|
.green {
|
||||||
margin-top: 50px;
|
order: 4;
|
||||||
flex-flow: row-reverse;
|
flex-direction: row-reverse;
|
||||||
grid-column: 2;
|
}
|
||||||
grid-row: 4;
|
|
||||||
|
/* Tablet (768px - 1024px) */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.playerContainer {
|
||||||
|
padding: 8px;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile (less than 768px) */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.playerContainer {
|
||||||
|
padding: 6px;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,54 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 10px;
|
margin: 8px 5px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-flow: row-reverse;
|
flex-flow: row-reverse;
|
||||||
background-color: grey;
|
background-color: #666;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
border: 2px solid white;
|
border: 2px solid white;
|
||||||
|
padding: 6px 10px;
|
||||||
|
gap: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.container:hover {
|
||||||
|
background-color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
.container > label {
|
.container > label {
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 100px;
|
|
||||||
color: white;
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
margin: 6px 4px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > label {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
margin: 4px 3px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
gap: 3px;
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > label {
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.container {
|
.container {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -8,6 +8,14 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
z-index: 1;
|
z-index: 100;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 39 KiB |
+38
-6
@@ -1,3 +1,9 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
circle,
|
circle,
|
||||||
@@ -7,36 +13,53 @@ body {
|
|||||||
rgba(9, 9, 121, 1) 81%
|
rgba(9, 9, 121, 1) 81%
|
||||||
);
|
);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
border: 2px solid black;
|
border: 2px solid black;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-container {
|
.navbar-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
background-color: darkblue;
|
background-color: darkblue;
|
||||||
color: white;
|
color: white;
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 60px;
|
|
||||||
left: 82px;
|
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
padding: 1px;
|
padding: 4px 8px;
|
||||||
width: 30px;
|
min-width: 40px;
|
||||||
height: 20px;
|
min-height: 24px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -47,7 +70,16 @@ canvas {
|
|||||||
animation: timerAnimation 15s linear infinite;
|
animation: timerAnimation 15s linear infinite;
|
||||||
transition-duration: 15s;
|
transition-duration: 15s;
|
||||||
}
|
}
|
||||||
#root {
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user