Ajout zones en pavage + fix dockefiles

This commit is contained in:
Sébastien Rivière
2025-06-25 14:34:29 +02:00
parent adcf6f031e
commit 8919a49513
48 changed files with 1074 additions and 714 deletions

View File

@@ -5,7 +5,7 @@ FROM node:22-alpine
WORKDIR /app WORKDIR /app
# Copy package.json and package-lock.json to the /app directory # Copy package.json and package-lock.json to the /app directory
COPY package.json package-lock.json ./ COPY package.json package-lock.json* ./
# Install dependencies # Install dependencies
RUN npm install RUN npm install

View File

@@ -8,7 +8,7 @@ WORKDIR /app
ENV NODE_ENV=development ENV NODE_ENV=development
# Copy package.json and package-lock.json to the /app directory # Copy package.json and package-lock.json to the /app directory
COPY package.json package-lock.json ./ COPY package.json package-lock.json* ./
# Install dependencies # Install dependencies
RUN npm install RUN npm install

View File

@@ -5,7 +5,7 @@ This module also exposes functions to send messages via socket to all admins
*/ */
import { io } from "./index.js"; import { io } from "./index.js";
import game from "./game.js" import game from "./game.js"
import zone from "./zone_manager.js" import zoneManager from "./zone_manager.js"
import penaltyController from "./penalty_controller.js"; import penaltyController from "./penalty_controller.js";
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { createHash } from "crypto"; import { createHash } from "crypto";
@@ -57,12 +57,11 @@ export function initAdminSocketHandler() {
// Other settings that need initialization // Other settings that need initialization
socket.emit("penalty_settings", penaltyController.settings) socket.emit("penalty_settings", penaltyController.settings)
socket.emit("game_settings", game.settings) socket.emit("game_settings", game.settings)
socket.emit("zone_settings", zone.zoneSettings) socket.emit("zone_settings", zoneManager.settings)
socket.emit("zone", zone.currentZone) socket.emit("current_zone", {
socket.emit("new_zone", { begin: zoneManager.getCurrentZone(),
begin: zone.currentStartZone, end: zoneManager.getNextZone(),
end: zone.nextZone, endDate: zoneManager.currentZoneEndDate,
endDate: zone.nextZoneDate,
}) })
} else { } else {
// Attempt unsuccessful // Attempt unsuccessful
@@ -89,11 +88,11 @@ export function initAdminSocketHandler() {
socket.emit("error", "Not logged in"); socket.emit("error", "Not logged in");
return; return;
} }
if (!game.setZoneSettings(settings)) { if (!zoneManager.changeSettings(settings)) {
socket.emit("error", "Error changing zone"); socket.emit("error", "Error changing zone");
socket.emit("zone_settings", zone.zoneSettings) // Still broadcast the old config to the client who submited an incorrect config to keep the client up to date socket.emit("zone_settings", zoneManager.settings)
} else { } else {
secureAdminBroadcast("zone_settings", zone.zoneSettings) secureAdminBroadcast("zone_settings", zoneManager.settings)
} }
}) })

View File

@@ -3,12 +3,44 @@ This module manages the main game state, the teams, the settings and the game lo
*/ */
import { secureAdminBroadcast } from "./admin_socket.js"; import { secureAdminBroadcast } from "./admin_socket.js";
import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js"; import { playersBroadcast, sendUpdatedTeamInformations } from "./team_socket.js";
import { isInCircle, getDistanceFromLatLon } from "./map_utils.js";
import timeoutHandler from "./timeoutHandler.js"; import timeoutHandler from "./timeoutHandler.js";
import penaltyController from "./penalty_controller.js"; import penaltyController from "./penalty_controller.js";
import zoneManager from "./zone_manager.js"; import zoneManager from "./zone_manager.js";
import trajectory from "./trajectory.js"; import trajectory from "./trajectory.js";
/**
* Compute the distance between two points givent their longitude and latitude
* @param {Object} pos1 The first position
* @param {Object} pos2 The second position
* @returns the distance between the two positions in meters
* @see https://gist.github.com/miguelmota/10076960
*/
function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
const degToRad = (deg) => deg * (Math.PI / 180);
var R = 6371; // Radius of the earth in km
var dLat = degToRad(lat2 - lat1);
var dLon = degToRad(lon2 - lon1);
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
;
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c; // Distance in km
return d * 1000;
}
/**
* Check if a GPS point is in a circle
* @param {Object} position The position to check, an object with lat and lng fields
* @param {Object} center The center of the circle, an object with lat and lng fields
* @param {Number} radius The radius of the circle in meters
* @returns
*/
function isInCircle(position, center, radius) {
return getDistanceFromLatLon(position, center) < radius;
}
/** /**
* The possible states of the game * The possible states of the game
*/ */
@@ -56,7 +88,7 @@ export default {
switch (newState) { switch (newState) {
case GameState.SETUP: case GameState.SETUP:
trajectory.stop(); trajectory.stop();
zoneManager.reset(); zoneManager.stop();
penaltyController.stop(); penaltyController.stop();
timeoutHandler.endAllSendPositionTimeout(); timeoutHandler.endAllSendPositionTimeout();
for (let team of this.teams) { for (let team of this.teams) {
@@ -77,28 +109,38 @@ export default {
this.updateTeamChasing(); this.updateTeamChasing();
break; break;
case GameState.PLACEMENT: case GameState.PLACEMENT:
if (this.teams.length < 3) {
secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate});
return false;
}
trajectory.stop(); trajectory.stop();
zoneManager.reset(); zoneManager.stop();
penaltyController.stop(); penaltyController.stop();
timeoutHandler.endAllSendPositionTimeout(); timeoutHandler.endAllSendPositionTimeout();
this.startDate = null; this.startDate = null;
break; break;
case GameState.PLAYING: case GameState.PLAYING:
if (!zoneManager.start()) { if (this.teams.length < 3) {
secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate});
return false; return false;
} }
trajectory.start(); trajectory.start();
zoneManager.start();
penaltyController.start(); penaltyController.start();
this.initLastSentLocations(); this.initLastSentLocations();
this.startDate = Date.now(); this.startDate = Date.now();
break; break;
case GameState.FINISHED: case GameState.FINISHED:
if (this.state != GameState.PLAYING) {
secureAdminBroadcast("game_state", {state: this.state, startDate: this.startDate});
return false;
}
for (const team of this.teams) { for (const team of this.teams) {
if (!team.finishDate) team.finishDate = Date.now(); if (!team.finishDate) team.finishDate = Date.now();
} }
trajectory.stop(); trajectory.stop();
zoneManager.stop();
penaltyController.stop(); penaltyController.stop();
zoneManager.reset();
timeoutHandler.endAllSendPositionTimeout(); timeoutHandler.endAllSendPositionTimeout();
break; break;
} }
@@ -379,28 +421,4 @@ export default {
timeoutHandler.endSendPositionTimeout(teamId); timeoutHandler.endSendPositionTimeout(teamId);
this.updateTeamChasing(); this.updateTeamChasing();
}, },
/**
* Change the settings of the Zone manager
* The game should not be in PLAYING or FINISHED state
* @param {Object} newSettings The object containing the settings to be changed
* @returns false if failed
*/
setZoneSettings(newSettings) {
if ('min' in newSettings || 'max' in newSettings) {
const min = newSettings.min ?? zoneManager.zoneSettings.min;
const max = newSettings.max ?? zoneManager.zoneSettings.max;
// The end zone must be included in the start zone
if (!isInCircle(min.center, max.center, max.radius-min.radius)) {
return false;
}
}
zoneManager.udpateSettings(newSettings);
if (this.state == GameState.PLAYING || this.state == GameState.FINISHED) {
if (!zoneManager.start()) {
return false;
}
}
return true;
}
} }

View File

@@ -1,40 +0,0 @@
/**
* Convert a angle from degree to radian
* @param {Number} deg angle in degree
* @returns angle in radian
*/
function degToRad(deg) {
return deg * (Math.PI / 180);
}
/**
* Compute the distance between two points givent their longitude and latitude
* @param {Object} pos1 The first position
* @param {Object} pos2 The second position
* @returns the distance between the two positions in meters
* @see https://gist.github.com/miguelmota/10076960
*/
export function getDistanceFromLatLon({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
var R = 6371; // Radius of the earth in km
var dLat = degToRad(lat2 - lat1);
var dLon = degToRad(lon2 - lon1);
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2)
;
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var d = R * c; // Distance in km
return d * 1000;
}
/**
* Check if a GPS point is in a circle
* @param {Object} position The position to check, an object with lat and lng fields
* @param {Object} center The center of the circle, an object with lat and lng fields
* @param {Number} radius The radius of the circle in meters
* @returns
*/
export function isInCircle(position, center, radius) {
return getDistanceFromLatLon(position, center) < radius;
}

View File

@@ -1,11 +1,10 @@
/* /*
This module manages the verification of the game rules and the penalties. This module manages the verification of the game rules and the penalties.
*/ */
import { isInCircle } from "./map_utils.js";
import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js"; import { sendUpdatedTeamInformations, teamBroadcast } from "./team_socket.js";
import { secureAdminBroadcast } from "./admin_socket.js"; import { secureAdminBroadcast } from "./admin_socket.js";
import game, { GameState } from "./game.js"; import game, { GameState } from "./game.js";
import zone from "./zone_manager.js"; import zoneManager from "./zone_manager.js";
export default { export default {
// Object mapping team id to the date they left the zone as a UNIX millisecond timestamp // Object mapping team id to the date they left the zone as a UNIX millisecond timestamp
@@ -115,10 +114,10 @@ export default {
game.teams.forEach((team) => { game.teams.forEach((team) => {
if (team.captured) { return } if (team.captured) { return }
//All the informations are not ready yet //All the informations are not ready yet
if (team.currentLocation == null || zone.currentZone == null) { if (team.currentLocation == null || !zoneManager.isRunning) {
return; return;
} }
if (!isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] }, zone.currentZone.center, zone.currentZone.radius)) { if (!zoneManager.isInCircle({ lat: team.currentLocation[0], lng: team.currentLocation[1] })) {
//The team was not previously out of the zone //The team was not previously out of the zone
if (!this.outOfBoundsSince[team.id]) { if (!this.outOfBoundsSince[team.id]) {
this.outOfBoundsSince[team.id] = Date.now(); this.outOfBoundsSince[team.id] = Date.now();

View File

@@ -6,7 +6,7 @@ This module also exposes functions to send messages via socket to all teams
import { secureAdminBroadcast } from "./admin_socket.js"; import { secureAdminBroadcast } from "./admin_socket.js";
import { io } from "./index.js"; import { io } from "./index.js";
import game from "./game.js"; import game from "./game.js";
import zone from "./zone_manager.js"; import zoneManager from "./zone_manager.js";
/** /**
* Send a socket message to all the players of a team * Send a socket message to all the players of a team
@@ -97,11 +97,10 @@ export function initTeamSocket() {
socket.emit("login_response", true); socket.emit("login_response", true);
socket.emit("game_state", game.state); socket.emit("game_state", game.state);
socket.emit("game_settings", game.settings); socket.emit("game_settings", game.settings);
socket.emit("zone", zone.currentZone); socket.emit("zone", {
socket.emit("new_zone", { begin: zoneManager.getCurrentZone(),
begin: zone.currentStartZone, end: zoneManager.getNextZone(),
end: zone.nextZone, endDate: zoneManager.currentZoneEndDate,
endDate: zone.nextZoneDate,
}) })
callback({ isLoggedIn : true, message: "Logged in"}); callback({ isLoggedIn : true, message: "Logged in"});
}); });

View File

@@ -1,237 +1,230 @@
/*
This module manages the play area during the game, shrinking it over time based of some settings.
*/
import { randomCirclePoint } from 'random-location'
import { isInCircle } from './map_utils.js';
import { playersBroadcast } from './team_socket.js'; import { playersBroadcast } from './team_socket.js';
import { secureAdminBroadcast } from './admin_socket.js'; import { secureAdminBroadcast } from './admin_socket.js';
/**
* Scale a value that is known to be in a range to a new range /* -------------------------------- Useful functions and constants -------------------------------- */
* for instance map(50,0,100,1000,2000) will return 1500 as 50 is halfway between 0 and 100 and 1500 is halfway through 1000 and 2000
* @param {Number} value value to map const EARTH_RADIUS = 6_371_000; // Radius of the earth in m
* @param {Number} oldMin minimum value of the number
* @param {Number} oldMax maximum value of the number function haversine_distance({ lat: lat1, lng: lon1 }, { lat: lat2, lng: lon2 }) {
* @param {Number} newMin minimum value of the output const degToRad = (deg) => deg * (Math.PI / 180);
* @param {Number} newMax maximum value of the output const dLat = degToRad(lat2 - lat1);
* @returns const dLon = degToRad(lon2 - lon1);
*/ const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(degToRad(lat1)) * Math.cos(degToRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
function map(value, oldMin, oldMax, newMin, newMax) { const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return ((value - oldMin) / (oldMax - oldMin)) * (newMax - newMin) + newMin; return c * EARTH_RADIUS;
} }
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
return Math.abs(latlng1.lat - latlng2.lat) < epsilon && Math.abs(latlng1.lng - latlng2.lng) < epsilon;
}
/* -------------------------------- Polygon zones -------------------------------- */
const defaultPolygonSettings = { polygons: [], durations: [] };
function polygonZone(points, duration) {
return {
points: points,
duration: duration,
isInZone(location) {
const {lat: x, lng: y} = location;
let inside = false;
for (let i = 0, j = this.points.length - 1; i < this.points.length; j = i++) {
const {lat: xi, lng: yi} = this.points[i];
const {lat: xj, lng: yj} = this.points[j];
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
if (intersects) inside = !inside;
}
return inside;
}
}
}
function mergePolygons(poly1, poly2) {
// poly1 and poly2 are clockwise, not overlaping and touching polygons. If those two polygons were on a map, they would be
// one against each other, and the merge would make a new clockwise polygon out of the outer border of the two polygons.
// If it happens that poly1 and poly2 are not touching, poly1 would be returned untouched.
// Basically because polygons are clockwise, the alogorithm starts from a point A in poly1 not shared by poly2, and
// when a point is shared by poly1 and poly2, the algorithm continues in poly2, and so on until point A.
const getPointIndex = (point, array) => {
for (let i = 0; i < array.length; i++) {
if (latlngEqual(array[i], point)) return i;
}
return -1;
}
// Find the index of the first point of poly1 that doesn't belong to merge (it exists)
let i = 0;
while (getPointIndex(poly1[i], poly2) != -1) i++;
// Starting the merge from that point
const merge = [poly1[i]];
i = (i + 1) % poly1.length;
let currentArray = poly1;
let otherArray = poly2;
while (!latlngEqual(currentArray[i], merge[0])) {
const j = getPointIndex(currentArray[i], otherArray);
if (j != -1) {
[currentArray, otherArray] = [otherArray, currentArray];
i = j;
}
merge.push(currentArray[i]);
i = (i + 1) % currentArray.length;
}
return merge;
}
function polygonSettingsToZones(settings) {
const { polygons, durations } = settings;
const reversedPolygons = polygons.slice().reverse();
const reversedDurations = durations.slice().reverse();
const zones = [];
for (let i = 0; i < reversedPolygons.length; i++) {
const polygon =reversedPolygons[i];
const duration = reversedDurations[i];
const length = zones.length;
if (length == 0) {
zones.push(polygonZone(
polygon,
duration
));
} else {
zones.push(polygonZone(
mergePolygons(zones[length-1].points, polygon),
duration
));
}
}
return zones.slice().reverse();
}
/* -------------------------------- Circle zones -------------------------------- */
const defaultCircleSettings = { min: null, max: null, reductionCount: 4, duration: 1 };
function circleZone(center, radius, duration) {
return {
center: center,
radius: radius,
duration: duration,
isInZone(location) {
return haversine_distance(center, location) < this.radius;
}
}
}
function circleSettingsToZones(settings) {
const {min, max, reductionCount, duration} = settings;
if (haversine_distance(max.center, min.center) > max.radius - min.radius) {
return null;
}
const zones = [circleZone(max.center, max.radius, duration)];
const radiusReductionLength = (max.radius - min.radius) / reductionCount;
let center = max.center;
let radius = max.radius;
for (let i = 1; i < reductionCount; i++) {
radius -= radiusReductionLength;
let new_center = null;
while (!new_center || haversine_distance(new_center, min.center) > radius - min.radius) {
const angle = Math.random() * 2 * Math.PI;
const angularDistance = Math.sqrt(Math.random()) * radiusReductionLength / EARTH_RADIUS;
const lat0Rad = center.lat * Math.PI / 180;
const lon0Rad = center.lng * Math.PI / 180;
const latRad = Math.asin(
Math.sin(lat0Rad) * Math.cos(angularDistance) +
Math.cos(lat0Rad) * Math.sin(angularDistance) * Math.cos(angle)
);
const lonRad = lon0Rad + Math.atan2(
Math.sin(angle) * Math.sin(angularDistance) * Math.cos(lat0Rad),
Math.cos(angularDistance) - Math.sin(lat0Rad) * Math.sin(latRad)
);
new_center = {lat: latRad * 180 / Math.PI, lng: lonRad * 180 / Math.PI};
}
center = new_center;
zones.push(circleZone(center, radius, duration))
}
zones.push(circleZone(min.center, min.radius, 0));
return zones;
}
/* -------------------------------- Zone manager -------------------------------- */
export default { export default {
//Setings storing where the zone will start, end and how it should evolve isRunning: false,
//The zone will start by staying at its max value for reductionInterval minutes zones: [], // A zone has to be connected space that doesn't contain an earth pole
//and then reduce during reductionDuration minutes, then wait again... currentZone: { id: 0, timeoutId: null, endDate: null },
//The reduction factor is such that after reductionCount the zone will be the min zone settings: defaultPolygonSettings,
zoneSettings: {
min: { center: null, radius: null },
max: { center: null, radius: null },
reductionCount: 2,
reductionDuration: 1,
reductionInterval: 1,
updateIntervalSeconds: 1,
},
nextZoneDecrement: null,
//Live location of the zone
currentZone: { center: null, radius: null },
//If the zone is shrinking, this is the target of the current shrinking
//If the zone is not shrinking, this will be the target of the next shrinking
nextZone: { center: null, radius: null },
//Zone at the begining of the shrinking
currentStartZone: { center: null, radius: null },
startDate: null,
started: false,
updateIntervalId: null,
nextZoneTimeoutId: null,
nextZoneDate: null,
/**
* Test if a given configuration object is valid, i.e if all needed values are well defined
* @param {Object} settings Settings object describing a config of a zone manager
* @returns if the config is correct
*/
validateSettings(settings) {
if (settings.reductionCount && (typeof settings.reductionCount != "number" || settings.reductionCount <= 0)) { return false }
if (settings.reductionDuration && (typeof settings.reductionDuration != "number" || settings.reductionDuration < 0)) { return false }
if (settings.reductionInterval && (typeof settings.reductionInterval != "number" || settings.reductionInterval < 0)) { return false }
if (settings.updateIntervalSeconds && (typeof settings.updateIntervalSeconds != "number" || settings.updateIntervalSeconds <= 0)) { return false }
if (settings.max && (typeof settings.max.radius != "number" || typeof settings.max.center.lat != "number" || typeof settings.max.center.lng != "number")) { return false }
if (settings.min && (typeof settings.min.radius != "number" || typeof settings.min.center.lat != "number" || typeof settings.min.center.lng != "number")) { return false }
return true;
},
/**
* Test if the zone manager is ready to start
* @returns true if the zone manager is ready to be started, false otherwise
*/
ready() {
return this.validateSettings(this.zoneSettings);
},
/**
* Update the settings of the zone, this can be done by passing an object containing the settings to change.
* Unless specified, the durations are in minutes
* Default config :
* `this.zoneSettings = {
* min: {center: null, radius: null},
* max: {center: null, radius: null},
* reductionCount: 2,
* reductionDuration: 10,
* reductionInterval: 10,
* updateIntervalSeconds: 10,
* }`
* @param {Object} newSettings The fields of the settings to udpate
* @returns
*/
udpateSettings(newSettings) {
//validate settings
this.zoneSettings = { ...this.zoneSettings, ...newSettings };
this.nextZoneDecrement = (this.zoneSettings.max.radius - this.zoneSettings.min.radius) / this.zoneSettings.reductionCount;
return true;
},
/**
* Reinitialize the object and stop all the tasks
*/
reset() {
this.currentZoneCount = 0;
this.started = false;
if (this.updateIntervalId != null) {
clearInterval(this.updateIntervalId);
this.updateIntervalId = null;
}
if (this.nextZoneTimeoutId != null) {
clearTimeout(this.nextZoneTimeoutId);
this.nextZoneTimeoutId = null;
}
},
/**
* Start the zone reduction sequence
*/
start() { start() {
if (!this.ready()) return false; this.isRunning = true;
this.reset(); this.currentZone.id = -1;
this.started = true; this.goNextZone();
this.startDate = new Date();
//initialize the zone to its max value
this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.max));
this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.max));
this.currentZone = JSON.parse(JSON.stringify(this.zoneSettings.max));
return this.setNextZone();
}, },
/** stop() {
* Get the center of the next zone, this center need to satisfy two properties this.isRunning = false;
* - it needs to be included in the current zone, this means that this new point should lie in the circle of center currentZone.center and radius currentZone.radius - newRadius clearTimeout(this.currentZone.timeoutId);
* - it needs to include the last zone, which means that the center must lie in the center of center min.center and of radius newRadius - min.radius },
* @param newRadius the radius that the new zone will have
* @returns the coordinates of the new center as an object with lat and long fields goNextZone() {
*/ this.currentZone.id++;
getRandomNextCenter(newRadius) { if (this.currentZone.id >= this.zones.length) return;
let ok = false; this.currentZone.timeoutId = setTimeout(() => this.goNextZone(), this.getCurrentZone().duration * 60 * 1000);
let res = null this.currentZone.endDate = Date.now() + this.getCurrentZone().duration * 60 * 1000;
let tries = 0; this.zoneBroadcast();
const MAX_TRIES = 100000 },
//take a random point satisfying both conditions
while (tries++ < MAX_TRIES && !ok) { getCurrentZone() {
res = randomCirclePoint({ latitude: this.currentZone.center.lat, longitude: this.currentZone.center.lng }, this.currentZone.radius - newRadius); return this.zones[this.currentZone.id];
ok = (isInCircle({ lat: res.latitude, lng: res.longitude }, this.zoneSettings.min.center, newRadius - this.zoneSettings.min.radius)) },
}
if (tries >= MAX_TRIES) { getNextZone() {
return false; if (this.currentZone.id + 1 < this.zones.length) {
} return this.zones[this.currentZone.id + 1];
return { } else {
lat: res.latitude, return this.zones[this.currentZone.id];
lng: res.longitude
} }
}, },
/** isInZone(location) {
* Compute the next zone satifying the given settings, update the nextZone and currentStartZone if (this.zones.length == 0) {
* Wait for the appropriate duration before starting a new zone reduction if needed return true;
*/ } else {
setNextZone() { return this.getCurrentZone().isInZone(location);
this.nextZoneDate = Date.now() + this.zoneSettings.reductionInterval * 60 * 1000;
//At this point, nextZone == currentZone, we need to update the next zone, the raidus decrement, and start a timer before the next shrink
//last zone
if (this.currentZoneCount == this.zoneSettings.reductionCount) {
this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min))
this.currentStartZone = JSON.parse(JSON.stringify(this.zoneSettings.min))
} else if (this.currentZoneCount == this.zoneSettings.reductionCount - 1) {
this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone))
this.nextZone = JSON.parse(JSON.stringify(this.zoneSettings.min))
this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval)
this.currentZoneCount++;
} else if (this.currentZoneCount < this.zoneSettings.reductionCount) {
this.nextZone.center = this.getRandomNextCenter(this.nextZone.radius - this.nextZoneDecrement)
//Next center cannot be found
if (this.nextZone.center === false) {
console.log("no center")
return false;
}
this.nextZone.radius -= this.nextZoneDecrement;
this.currentStartZone = JSON.parse(JSON.stringify(this.currentZone))
this.nextZoneTimeoutId = setTimeout(() => this.startShrinking(), 1000 * 60 * this.zoneSettings.reductionInterval)
this.currentZoneCount++;
} }
this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentStartZone))) },
this.onNextZoneUpdate({
begin: JSON.parse(JSON.stringify(this.currentStartZone)), changeSettings(settings) {
end: JSON.parse(JSON.stringify(this.nextZone)), const zones = polygonSettingsToZones(settings);
endDate: JSON.parse(JSON.stringify(this.nextZoneDate)), if (!zones) return false;
}) this.zones = zones;
this.settings = settings;
this.zoneBroadcast();
return true; return true;
}, },
/* zoneBroadcast() {
* Start a task that will run periodically updatinng the zone size, and calling the onZoneUpdate callback const zone = {
* This will also periodically check if the reduction is over or not begin: this.getCurrentZone(),
* If the reduction is over this function will call setNextZone end: this.getNextZone(),
*/ endDate:this.currentZone.endDate,
startShrinking() { };
this.nextZoneDate = Date.now() + this.zoneSettings.reductionDuration * 60 * 1000; playersBroadcast("current_zone", zone);
this.onZoneUpdateStart(JSON.parse(JSON.stringify(this.nextZoneDate))); secureAdminBroadcast("current_zone", zone);
const startTime = new Date();
this.updateIntervalId = setInterval(() => {
const completed = ((new Date() - startTime) / (1000 * 60)) / this.zoneSettings.reductionDuration;
this.currentZone.radius = map(completed, 0, 1, this.currentStartZone.radius, this.nextZone.radius)
this.currentZone.center.lat = map(completed, 0, 1, this.currentStartZone.center.lat, this.nextZone.center.lat)
this.currentZone.center.lng = map(completed, 0, 1, this.currentStartZone.center.lng, this.nextZone.center.lng)
this.onZoneUpdate(JSON.parse(JSON.stringify(this.currentZone)))
//Zone shrinking is over
if (completed >= 1) {
clearInterval(this.updateIntervalId);
this.updateIntervalId = null;
this.setNextZone();
return;
}
}, this.zoneSettings.updateIntervalSeconds * 1000);
}, },
}
//a call to onNextZoneUpdate will be made when the zone reduction ends and a new next zone is announced
onNextZoneUpdate(newZone) {
playersBroadcast("new_zone", newZone)
secureAdminBroadcast("new_zone", newZone)
},
//a call to onZoneUpdateStart will be made when the zone reduction starts
onZoneUpdateStart(date) {
playersBroadcast("zone_start", date)
secureAdminBroadcast("zone_start", date)
},
//a call to onZoneUpdate will be made every updateIntervalSeconds when the zone is changing
onZoneUpdate(zone) {
playersBroadcast("zone", zone)
secureAdminBroadcast("zone", zone)
},
}

View File

@@ -1,8 +1,8 @@
import { AdminConnexionProvider} from "@/context/adminConnexionContext"; import { AdminConnexionProvider } from "@/context/adminConnexionContext";
import { AdminProvider } from "@/context/adminContext"; import { AdminProvider } from "@/context/adminContext";
import Link from "next/link"; import Link from "next/link";
export default function AdminLayout({ children}) { export default function AdminLayout({ children }) {
return ( return (
<AdminConnexionProvider> <AdminConnexionProvider>
<AdminProvider> <AdminProvider>

View File

@@ -1,15 +1,14 @@
"use client"; "use client";
import { TeamReady } from "@/components/admin/teamReady"; import TeamReady from "@/components/admin/teamReady";
import BlueButton, { GreenButton, RedButton } from "@/components/util/button"; import { BlueButton, GreenButton, RedButton } from "@/components/util/button";
import { useAdminConnexion } from "@/context/adminConnexionContext"; import { useAdminConnexion } from "@/context/adminConnexionContext";
import useAdmin from "@/hook/useAdmin"; import useAdmin from "@/hook/useAdmin";
import { GameState } from "@/util/gameState"; import { GameState } from "@/util/gameState";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { TeamListFixed } from '@/components/admin/teamList';
const LiveMap = dynamic(() => import('@/components/admin/mapPicker').then((mod) => mod.LiveMap), { // Imported at runtime and not at compile time
ssr: false const LiveMap = dynamic(() => import('@/components/admin/liveMap'), { ssr: false });
});
export default function AdminPage() { export default function AdminPage() {
const { useProtect } = useAdminConnexion(); const { useProtect } = useAdminConnexion();
const { gameState, changeState } = useAdmin(); const { gameState, changeState } = useAdmin();

View File

@@ -1,12 +1,12 @@
"use client"; "use client";
import { GameSettings } from "@/components/admin/gameSettings"; import GameSettings from "@/components/admin/gameSettings";
import { PenaltySettings } from "@/components/admin/penaltySettings"; import PenaltySettings from "@/components/admin/penaltySettings";
import { useAdminConnexion } from "@/context/adminConnexionContext"; import { useAdminConnexion } from "@/context/adminConnexionContext";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
const ZoneSelector = dynamic(() => import('@/components/admin/zoneSelector').then((mod) => mod.ZoneSelector), { // Imported at runtime and not at compile time
ssr: false const ZoneSelector = dynamic(() => import('@/components/admin/polygonZoneMap'), { ssr: false });
});
export default function AdminPage() { export default function AdminPage() {
const { useProtect } = useAdminConnexion(); const { useProtect } = useAdminConnexion();
useProtect(); useProtect();

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import ActionDrawer from '@/components/team/actionDrawer'; import ActionDrawer from '@/components/team/actionDrawer';
import { Notification } from '@/components/team/notification'; import Notification from '@/components/team/notification';
import PlacementOverlay from '@/components/team/placementOverlay'; import PlacementOverlay from '@/components/team/placementOverlay';
import { WaitingScreen } from '@/components/team/waitingScreen'; import WaitingScreen from '@/components/team/waitingScreen';
import { LogoutButton } from '@/components/util/button'; import { LogoutButton } from '@/components/util/button';
import { useSocket } from '@/context/socketContext'; import { useSocket } from '@/context/socketContext';
import { useTeamConnexion } from '@/context/teamConnexionContext'; import { useTeamConnexion } from '@/context/teamConnexionContext';

View File

@@ -0,0 +1,110 @@
import { useEffect, useState } from "react";
import { BlueButton, GreenButton, RedButton } from "../util/button";
import { TextInput } from "../util/textInput";
import useAdmin from "@/hook/useAdmin";
import useLocation from "@/hook/useLocation";
import "leaflet/dist/leaflet.css";
import { Circle, MapContainer, TileLayer } from "react-leaflet";
import useMapCircleDraw from "@/hook/useMapCircleDraw";
import { MapPan, MapEventListener } from "./mapUtils";
const DEFAULT_ZOOM = 14;
const EditMode = {
MIN: 0,
MAX: 1
}
function CircleDrawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
const { handleClick: maxClick, handleMouseMove: maxHover, center: maxCenter, radius: maxRadius } = useMapCircleDraw(minZone, setMinZone);
const { handleClick: minClick, handleMouseMove: minHover, center: minCenter, radius: minRadius } = useMapCircleDraw(maxZone, setMaxZone);
function handleLeftClick(e) {
if (editMode == EditMode.MAX) {
maxClick(e);
} else {
minClick(e);
}
}
function handleMouseMove(e) {
if (editMode == EditMode.MAX) {
maxHover(e);
} else {
minHover(e);
}
}
return (
<div>
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="blue" fillColor="blue" />}
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="red" fillColor="red" />}
<MapEventListener onLeftClick={handleLeftClick} onRightClick={() => {}} onMouseMove={handleMouseMove} />
</div>
);
}
export function CircleZonePicker({ minZone, maxZone, editMode, setMinZone, setMaxZone, ...props }) {
const location = useLocation(Infinity);
return (
<div className='h-96'>
<MapContainer {...props} className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapPan center={location} zoom={DEFAULT_ZOOM} />
<CircleDrawings minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
</MapContainer>
</div>
);
}
export default function CircleZoneMap() {
const [editMode, setEditMode] = useState(EditMode.MIN);
const [minZone, setMinZone] = useState(null);
const [maxZone, setMaxZone] = useState(null);
const [reductionCount, setReductionCount] = useState("");
const [duration, setDuration] = useState("");
const {zoneSettings, changeZoneSettings} = useAdmin();
useEffect(() => {
if (zoneSettings) {
setMinZone(zoneSettings.min);
setMaxZone(zoneSettings.max);
setReductionCount(zoneSettings.reductionCount.toString());
setDuration(zoneSettings.duration.toString());
}
}, [zoneSettings]);
function handleSettingsSubmit() {
const newSettings = {min:minZone, max:maxZone, reductionCount: Number(reductionCount), duration: Number(duration)};
changeZoneSettings(newSettings);
}
// When the user set one zone, switch to the other
useEffect(() => {
if(editMode == EditMode.MIN) {
setEditMode(EditMode.MAX);
}else {
setEditMode(EditMode.MIN);
}
}, [minZone, maxZone]);
return <div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
<h2 className="text-2xl">Edit zones</h2>
{editMode == EditMode.MIN && <BlueButton onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</BlueButton>}
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>}
<CircleZonePicker minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
<div>
<p>Number of zones</p>
<TextInput value={reductionCount} onChange={(e) => setReductionCount(e.target.value)}></TextInput>
</div>
<div>
<p>Duration of a zone</p>
<TextInput value={duration} onChange={(e) => setDuration(e.target.value)}></TextInput>
</div>
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton>
</div>
}

View File

@@ -3,7 +3,7 @@ import { TextArea } from "../util/textInput";
import { GreenButton } from "../util/button"; import { GreenButton } from "../util/button";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const GameSettings = () => { export default function GameSettings() {
const {gameSettings, changeGameSettings} = useAdmin(); const {gameSettings, changeGameSettings} = useAdmin();
const [capturedMessage, setCapturedMessage] = useState(""); const [capturedMessage, setCapturedMessage] = useState("");
const [winnerEndMessage, setWinnerEndMessage] = useState(""); const [winnerEndMessage, setWinnerEndMessage] = useState("");
@@ -11,7 +11,6 @@ export const GameSettings = () => {
const [waitingMessage, setWaitingMessage] = useState(""); const [waitingMessage, setWaitingMessage] = useState("");
useEffect(() => { useEffect(() => {
console.log({gameSettings})
if (gameSettings) { if (gameSettings) {
setCapturedMessage(gameSettings.capturedMessage); setCapturedMessage(gameSettings.capturedMessage);
setWinnerEndMessage(gameSettings.winnerEndGameMessage); setWinnerEndMessage(gameSettings.winnerEndGameMessage);
@@ -46,4 +45,4 @@ export const GameSettings = () => {
<GreenButton onClick={applySettings}>Apply</GreenButton> <GreenButton onClick={applySettings}>Apply</GreenButton>
</div> </div>
) )
} }

View File

@@ -0,0 +1,75 @@
import useLocation from "@/hook/useLocation";
import { useEffect, useState } from "react";
import "leaflet/dist/leaflet.css";
import { MapContainer, Marker, TileLayer, Tooltip, Polyline, Polygon } from "react-leaflet";
import useAdmin from "@/hook/useAdmin";
import { GameState } from "@/util/gameState";
import { MapPan } from "./mapUtils";
const DEFAULT_ZOOM = 14;
const positionIcon = new L.Icon({
iconUrl: '/icons/location.png',
iconSize: [30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -15],
shadowSize: [30, 30],
});
export default function LiveMap() {
const location = useLocation(Infinity);
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
const { zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
// Remaining time before sending position
useEffect(() => {
if (nextZoneDate) {
const updateTime = () => {
setTimeLeftNextZone(Math.max(0, Math.floor((nextZoneDate - Date.now()) / 1000)));
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}
}, [nextZoneDate]);
function formatTime(time) {
// time is in seconds
if (time < 0) return "00:00";
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
}
function Arrow({pos1, pos2}) {
if (pos1 && pos2) {
return (
<Polyline positions={[pos1, pos2]} pathOptions={{ color: 'black', weight: 3 }}/>
)
} else {
return null;
}
}
return (
<div className='min-h-full w-full'>
{gameState == GameState.PLAYING && timeLeftNextZone && <p>{`Next zone in : ${formatTime(timeLeftNextZone)}`}</p>}
<MapContainer className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapPan center={location} zoom={DEFAULT_ZOOM} />
{gameState == GameState.PLAYING && zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'blue', fillColor: 'blue', fillOpacity: '0.2', weight: 3 }} />}
{gameState == GameState.PLAYING && zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0', weight: 3 }} />}
{teams.map((team) => team.currentLocation && !team.captured &&
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
<Tooltip permanent direction="top" offset={[0, -5]} className="custom-tooltip">{team.name}</Tooltip>
<Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>
</Marker>
)}
</MapContainer>
</div>
)
}

View File

@@ -1,166 +0,0 @@
"use client";
import { useLocation } from "@/hook/useLocation";
import { useEffect, useState } from "react";
import "leaflet/dist/leaflet.css";
import { Circle, MapContainer, Marker, TileLayer, useMap, Tooltip, Polyline } from "react-leaflet";
import { useMapCircleDraw } from "@/hook/mapDrawing";
import useAdmin from "@/hook/useAdmin";
import { GameState } from "@/util/gameState";
const positionIcon = new L.Icon({
iconUrl: '/icons/location.png',
iconSize: [30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -15],
shadowSize: [30, 30],
});
function MapPan(props) {
const map = useMap();
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (!initialized && props.center) {
map.flyTo(props.center, props.zoom, { animate: false });
setInitialized(true)
}
}, [props.center]);
return null;
}
function MapEventListener({ onClick, onMouseMove }) {
const map = useMap();
useEffect(() => {
map.on('click', onClick);
return () => {
map.off('click', onClick);
}
}, [onClick]);
useEffect(() => {
map.on('mousemove', onMouseMove);
return () => {
map.off('mousemove', onMouseMove);
}
});
return null;
}
const DEFAULT_ZOOM = 14;
export function CircularAreaPicker({ area, setArea, markerPosition, ...props }) {
const location = useLocation(Infinity);
const { handleClick, handleMouseMove, center, radius } = useMapCircleDraw(area, setArea);
return (
<MapContainer {...props} className='min-h-full w-full ' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{center && radius && <Circle center={center} radius={radius} fillColor="blue" />}
{markerPosition && <Marker position={markerPosition} icon={positionIcon}>
</Marker>}
<MapPan center={location} zoom={DEFAULT_ZOOM} />
<MapEventListener onClick={handleClick} onMouseMove={handleMouseMove} />
</MapContainer>)
}
export const EditMode = {
MIN: 0,
MAX: 1
}
export function ZonePicker({ minZone, setMinZone, maxZone, setMaxZone, editMode, ...props }) {
const location = useLocation(Infinity);
const { handleClick: maxClick, handleMouseMove: maxHover, center: maxCenter, radius: maxRadius } = useMapCircleDraw(minZone, setMinZone);
const { handleClick: minClick, handleMouseMove: minHover, center: minCenter, radius: minRadius } = useMapCircleDraw(maxZone, setMaxZone);
function handleClick(e) {
if (editMode == EditMode.MAX) {
maxClick(e);
} else {
minClick(e);
}
}
function handleMouseMove(e) {
if (editMode == EditMode.MAX) {
maxHover(e);
} else {
minHover(e);
}
}
return (
<div>
<div className='h-96'>
<MapContainer {...props} className='min-h-full w-full ' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{minCenter && minRadius && <Circle center={minCenter} radius={minRadius} color="blue" fillColor="blue" />}
{maxCenter && maxRadius && <Circle center={maxCenter} radius={maxRadius} color="red" fillColor="red" />}
<MapPan center={location} zoom={DEFAULT_ZOOM} />
<MapEventListener onClick={handleClick} onMouseMove={handleMouseMove} />
</MapContainer>
</div>
{ maxCenter && minCenter && typeof maxCenter.distanceTo === 'function'
&& maxRadius + maxCenter.distanceTo(minCenter) >= minRadius
&& <p className="text-red-500">La zone de fin doit être incluse dans celle de départ</p>}
</div>
)
}
export function LiveMap() {
const location = useLocation(Infinity);
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
const { zone, zoneExtremities, teams, nextZoneDate, isShrinking , getTeam, gameState } = useAdmin();
// Remaining time before sending position
useEffect(() => {
const updateTime = () => {
setTimeLeftNextZone(Math.max(0, Math.floor((nextZoneDate - Date.now()) / 1000)));
};
updateTime();
const interval = setInterval(updateTime, 1000);
return () => clearInterval(interval);
}, [nextZoneDate]);
function formatTime(time) {
// time is in seconds
if (time < 0) return "00:00";
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
}
function Arrow({pos1, pos2}) {
if (pos1 && pos2) {
return (
<Polyline positions={[pos1, pos2]} pathOptions={{ color: 'black', weight: 3 }}/>
)
} else {
return null;
}
}
return (
<div className='min-h-full w-full'>
{gameState == GameState.PLAYING && <p>{`${isShrinking ? "Fin" : "Début"} du rétrécissement de la zone dans : ${formatTime(timeLeftNextZone)}`}</p>}
<MapContainer className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapPan center={location} zoom={DEFAULT_ZOOM} />
{gameState == GameState.PLAYING && zone && <Circle center={zone.center} radius={zone.radius} color="blue" />}
{gameState == GameState.PLAYING && zoneExtremities && <Circle center={zoneExtremities.begin.center} radius={zoneExtremities.begin.radius} color='black' fill={false} />}
{gameState == GameState.PLAYING && zoneExtremities && <Circle center={zoneExtremities.end.center} radius={zoneExtremities.end.radius} color='red' fill={false} />}
{teams.map((team) => team.currentLocation && !team.captured &&
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
<Tooltip permanent direction="top" offset={[0, -5]} className="custom-tooltip">{team.name}</Tooltip>
<Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>
</Marker>
)}
</MapContainer>
</div>
)
}

View File

@@ -0,0 +1,88 @@
import { useEffect, useState } from "react";
import "leaflet/dist/leaflet.css";
import { useMap } from "react-leaflet";
export function MapPan(props) {
const map = useMap();
const [initialized, setInitialized] = useState(false);
useEffect(() => {
if (!initialized && props.center) {
map.flyTo(props.center, props.zoom, { animate: false });
setInitialized(true)
}
}, [props.center]);
return null;
}
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
const map = useMap();
// Handle the mouse click left
useEffect(() => {
let moved = false;
let downButton = null;
const handleMouseDown = (e) => {
moved = false;
downButton = e.originalEvent.button;
};
const handleMouseMove = () => {
moved = true;
};
const handleMouseUp = (e) => {
if (!moved) {
if (downButton == 0) {
onLeftClick(e);
}
}
downButton = null;
};
map.on('mousedown', handleMouseDown);
map.on('mousemove', handleMouseMove);
map.on('mouseup', handleMouseUp);
return () => {
map.off('mousedown', handleMouseDown);
map.off('mousemove', handleMouseMove);
map.off('mouseup', handleMouseUp);
};
}, [onLeftClick, onRightClick]);
// Handle the right click
useEffect(() => {
const handleMouseDown = (e) => {
if (e.originalEvent.button == 2) {
onRightClick(e);
}
};
map.on('mousedown', handleMouseDown);
return () => {
map.off('mousedown', handleMouseDown);
}
}, [onRightClick]);
// Handle the mouse move
useEffect(() => {
map.on('mousemove', onMouseMove);
return () => {
map.off('mousemove', onMouseMove);
}
}, [onMouseMove]);
// Prevent right click context menu
useEffect(() => {
const container = map.getContainer();
const preventContextMenu = (e) => e.preventDefault();
container.addEventListener('contextmenu', preventContextMenu);
return () => container.removeEventListener('contextmenu', preventContextMenu);
}, []);
}

View File

@@ -1,9 +1,9 @@
import useAdmin from "@/hook/useAdmin"; import useAdmin from "@/hook/useAdmin";
import TextInput from "../util/textInput"; import { TextInput } from "../util/textInput";
import { GreenButton } from "../util/button"; import { GreenButton } from "../util/button";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const PenaltySettings = () => { export default function PenaltySettings() {
const {penaltySettings, changePenaltySettings} = useAdmin(); const {penaltySettings, changePenaltySettings} = useAdmin();
const [maxPenalties, setMaxPenalties] = useState(""); const [maxPenalties, setMaxPenalties] = useState("");
const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState(""); const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState("");
@@ -46,4 +46,4 @@ export const PenaltySettings = () => {
<GreenButton onClick={applySettings}>Apply</GreenButton> <GreenButton onClick={applySettings}>Apply</GreenButton>
</div> </div>
) )
} }

View File

@@ -0,0 +1,33 @@
import useLocation from "@/hook/useLocation";
import "leaflet/dist/leaflet.css";
import { Circle, MapContainer, Marker, TileLayer } from "react-leaflet";
import useMapCircleDraw from "@/hook/useMapCircleDraw";
import { MapPan, MapEventListener } from "./mapUtils";
const DEFAULT_ZOOM = 14;
const positionIcon = new L.Icon({
iconUrl: '/icons/location.png',
iconSize: [30, 30],
iconAnchor: [15, 15],
popupAnchor: [0, -15],
shadowSize: [30, 30],
});
export default function CircularAreaPicker({ area, setArea, markerPosition, ...props }) {
const location = useLocation(Infinity);
const { handleClick, handleMouseMove, center, radius } = useMapCircleDraw(area, setArea);
return (
<MapContainer {...props} className='min-h-full w-full ' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{center && radius && <Circle center={center} radius={radius} fillColor="blue" />}
{markerPosition && <Marker position={markerPosition} icon={positionIcon}>
</Marker>}
<MapPan center={location} zoom={DEFAULT_ZOOM} />
<MapEventListener onLeftClick={handleClick} onRightClick={() => {}} onMouseMove={handleMouseMove} />
</MapContainer>
);
}

View File

@@ -0,0 +1,142 @@
import { useEffect, useState } from "react";
import { GreenButton } from "../util/button";
import { TextInput } from "../util/textInput";
import useAdmin from "@/hook/useAdmin";
import useLocation from "@/hook/useLocation";
import "leaflet/dist/leaflet.css";
import { MapContainer, TileLayer, Polyline, Polygon, CircleMarker } from "react-leaflet";
import useMapPolygonDraw from "@/hook/useMapPolygonDraw";
import { MapPan, MapEventListener } from "./mapUtils";
const DEFAULT_ZOOM = 14;
function PolygonDrawings({ polygons, addPolygon, removePolygon }) {
const { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove } = useMapPolygonDraw(polygons, addPolygon, removePolygon);
const nodeSize = 5; // px
const lineThickness = 3; // px
function DrawNode({pos, color}) {
return (
<CircleMarker center={pos} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} />
);
}
function DrawLine({pos1, pos2, color}) {
return (
<Polyline positions={[pos1, pos2]} pathOptions={{ color: color, weight: lineThickness }} />
);
}
function DrawUnfinishedPolygon({polygon}) {
const length = polygon.length;
if (length > 0) {
return (
<div>
<DrawNode pos={polygon[0]} color={"red"} zIndexOffset={1000} />
{polygon.map((_, i) => {
if (i < length-1) {
return <DrawLine key={i} pos1={polygon[i]} pos2={polygon[i+1]} color={"red"} />;
} else {
return null;
}
})}
</div>
);
}
}
function DrawPolygon({polygon}) {
const length = polygon.length;
if (length > 2) {
return (
<Polygon positions={polygon} pathOptions={{ color: 'black', fillColor: 'black', fillOpacity: '0.5', weight: lineThickness }} />
);
}
}
return (
<div>
<MapEventListener onLeftClick={handleLeftClick} onRightClick={handleRightClick} onMouseMove={handleMouseMove} />
{polygons.map((polygon, i) => <DrawPolygon key={i} polygon={polygon} />)}
<DrawUnfinishedPolygon polygon={currentPolygon} />
{highlightNodes.map((node, i) => <DrawNode key={i} pos={node} color={"black"} />)}
</div>
);
}
function PolygonZonePicker({ polygons, addPolygon, removePolygon, ...props }) {
const location = useLocation(Infinity);
return (
<div className='h-96'>
<MapContainer {...props} className='min-h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<MapPan center={location} zoom={DEFAULT_ZOOM} />
<PolygonDrawings polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
</MapContainer>
</div>
);
}
export default function PolygonZoneMap() {
const defaultDuration = 10;
const [polygons, setPolygons] = useState([]);
const [durations, setDurations] = useState([]);
const {zoneSettings, changeZoneSettings} = useAdmin();
useEffect(() => {
if (zoneSettings) {
setPolygons(zoneSettings.polygons);
setDurations(zoneSettings.durations);
}
}, [zoneSettings]);
function addPolygon(polygon) {
// Polygons
setPolygons([...polygons, polygon]);
// Durations
setDurations([...durations, defaultDuration]);
}
function removePolygon(i) {
// Polygons
const newPolygons = [...polygons];
newPolygons.splice(i, 1);
setPolygons(newPolygons);
// Durations
const newDurations = [...durations];
newDurations.splice(i, 1);
setDurations(newDurations);
}
function updateDuration(i, duration) {
const newDurations = [...durations];
newDurations[i] = duration;
setDurations(newDurations);
}
function handleSettingsSubmit() {
const newSettings = {polygons: polygons, durations: durations};
changeZoneSettings(newSettings);
}
return (
<div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
<h2 className="text-2xl">Edit zones</h2>
<PolygonZonePicker polygons={polygons} addPolygon={addPolygon} removePolygon={removePolygon} />
<ul>
{durations.map((duration, i) => (
<li key={i}>
<p>Zone {i+1}</p>
<TextInput value={duration} onChange={(e) => updateDuration(i, e.target.value)}/>
</li>
))}
</ul>
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import TextInput from '../util/textInput' import { TextInput } from '../util/textInput'
import BlueButton from '../util/button' import { BlueButton } from '../util/button'
export default function TeamAddForm({onAddTeam}) { export default function TeamAddForm({onAddTeam}) {
const [teamName, setTeamName] = React.useState(''); const [teamName, setTeamName] = React.useState('');

View File

@@ -1,15 +1,13 @@
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import TextInput from '../util/textInput' import { TextInput } from '../util/textInput'
import BlueButton, { RedButton } from '../util/button'; import { BlueButton, RedButton } from '../util/button';
import useAdmin from '@/hook/useAdmin'; import useAdmin from '@/hook/useAdmin';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
import { GameState } from '@/util/gameState'; import { GameState } from '@/util/gameState';
const CircularAreaPicker = dynamic(() => import('./mapPicker').then((mod) => mod.CircularAreaPicker), { // Imported at runtime and not at compile time
ssr: false const PlacementMap = dynamic(() => import('./placementMap'), { ssr: false });
});
export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) { export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
const teamImage = useRef(null); const teamImage = useRef(null);
@@ -84,7 +82,7 @@ export default function TeamEdit({ selectedTeamId, setSelectedTeamId }) {
<RedButton onClick={handleRemove}>Remove</RedButton> <RedButton onClick={handleRemove}>Remove</RedButton>
</div> </div>
<p className='text-2xl text-center w-full'>Starting zone</p> <p className='text-2xl text-center w-full'>Starting zone</p>
<CircularAreaPicker area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} /> <PlacementMap area={team.startingArea} setArea={(startingArea) => updateTeam(team.id, { startingArea })} markerPosition={team?.currentLocation} />
</div> </div>
<div className='flex w-1/2 flex-col h-min gap-2 items-center'> <div className='flex w-1/2 flex-col h-min gap-2 items-center'>
<h2 className='text-2xl text-center'>Team details</h2> <h2 className='text-2xl text-center'>Team details</h2>

View File

@@ -1,14 +1,12 @@
"use client";
import useAdmin from '@/hook/useAdmin'; import useAdmin from '@/hook/useAdmin';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import React from 'react' import React from 'react'
const reorder = (list, startIndex, endIndex) => { function reorder(list, startIndex, endIndex) {
const result = Array.from(list); const result = Array.from(list);
const [removed] = result.splice(startIndex, 1); const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed); result.splice(endIndex, 0, removed);
return result;
return result;
}; };
function TeamListItem({ team, index, onSelected, itemSelected }) { function TeamListItem({ team, index, onSelected, itemSelected }) {
@@ -24,11 +22,12 @@ function TeamListItem({ team, index, onSelected, itemSelected }) {
)} )}
</Draggable> </Draggable>
) );
} }
export default function TeamList({selectedTeamId, onSelected}) { export default function TeamList({selectedTeamId, onSelected}) {
const {teams, reorderTeams} = useAdmin(); const {teams, reorderTeams} = useAdmin();
function onDragEnd(result) { function onDragEnd(result) {
if (!result.destination) { if (!result.destination) {
return; return;
@@ -46,6 +45,7 @@ export default function TeamList({selectedTeamId, onSelected}) {
reorderTeams(newTeams); reorderTeams(newTeams);
} }
return ( return (
<DragDropContext onDragEnd={onDragEnd} > <DragDropContext onDragEnd={onDragEnd} >
<Droppable droppableId='team-list'> <Droppable droppableId='team-list'>
@@ -61,5 +61,5 @@ export default function TeamList({selectedTeamId, onSelected}) {
)} )}
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
) );
} }

View File

@@ -1,16 +1,18 @@
import useAdmin from "@/hook/useAdmin" import useAdmin from "@/hook/useAdmin"
export function TeamReady() { export default function TeamReady() {
const {teams} = useAdmin(); const {teams} = useAdmin();
return <div className='w-full h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'> return (
<h2 className="text-2xl">Teams ready status</h2> <div className='w-full h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
{teams.map((team) => team.ready ? ( <h2 className="text-2xl">Teams ready status</h2>
<div key={team.id} className="p-2 text-white bg-green-500 shadow-md text-xl rounded flex flex-row"> {teams.map((team) => team.ready ? (
<div>{team.name} : Ready</div> <div key={team.id} className="p-2 text-white bg-green-500 shadow-md text-xl rounded flex flex-row">
</div>) : ( <div>{team.name} : Ready</div>
<div key={team.id} className="p-2 text-white bg-red-500 shadow-md text-xl rounded flex flex-row"> </div>) : (
<div>{team.name} : Not ready</div> <div key={team.id} className="p-2 text-white bg-red-500 shadow-md text-xl rounded flex flex-row">
</div> <div>{team.name} : Not ready</div>
</div>
))} ))}
</div> </div>
} );
}

View File

@@ -1,66 +0,0 @@
import { useEffect, useState } from "react";
import BlueButton, { GreenButton, RedButton } from "../util/button";
import { EditMode, ZonePicker } from "./mapPicker";
import TextInput from "../util/textInput";
import useAdmin from "@/hook/useAdmin";
export function ZoneSelector() {
const [editMode, setEditMode] = useState(EditMode.MIN);
const [minZone, setMinZone] = useState(null);
const [maxZone, setMaxZone] = useState(null);
const [reductionCount, setReductionCount] = useState("");
const [reductionDuration, setReductionDuration] = useState("");
const [reductionInterval, setReductionInterval] = useState("");
const {zoneSettings, changeZoneSettings} = useAdmin();
useEffect(() => {
if (zoneSettings) {
setMinZone(zoneSettings.min);
setMaxZone(zoneSettings.max);
setReductionCount(zoneSettings.reductionCount.toString());
setReductionDuration(zoneSettings.reductionDuration.toString());
setReductionInterval(zoneSettings.reductionInterval.toString());
}
}, [zoneSettings]);
function handleSettingsSubmit() {
const newSettings = {min:minZone, max:maxZone, reductionCount: Number(reductionCount), reductionDuration: Number(reductionDuration), reductionInterval: Number(reductionInterval)};
const changingSettings = {};
for (const key in newSettings) {
if (newSettings[key] != zoneSettings[key]) {
changingSettings[key] = newSettings[key];
}
}
changeZoneSettings(changingSettings);
}
//When the user set one zone, switch to the other
useEffect(() => {
if(editMode == EditMode.MIN) {
setEditMode(EditMode.MAX);
}else {
setEditMode(EditMode.MIN);
}
}, [minZone, maxZone]);
return <div className='w-2/5 h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
<h2 className="text-2xl">Edit zones</h2>
{editMode == EditMode.MIN && <BlueButton onClick={() => setEditMode(EditMode.MAX)}>Click to edit first zone</BlueButton>}
{editMode == EditMode.MAX && <RedButton onClick={() => setEditMode(EditMode.MIN)}>Click to edit last zone</RedButton>}
<ZonePicker minZone={minZone} maxZone={maxZone} editMode={editMode} setMinZone={setMinZone} setMaxZone={setMaxZone} />
<div>
<p>Number of reductions</p>
<TextInput value={reductionCount} onChange={(e) => setReductionCount(e.target.value)}></TextInput>
</div>
<div>
<p>Duration of each reduction</p>
<TextInput value={reductionDuration} onChange={(e) => setReductionDuration(e.target.value)}></TextInput>
</div>
<div>
<p>Interval between reductions</p>
<TextInput value={reductionInterval} onChange={(e) => setReductionInterval(e.target.value)}></TextInput>
</div>
<GreenButton onClick={handleSettingsSubmit}>Apply</GreenButton>
</div>
}

View File

@@ -1,10 +1,9 @@
import useGame from "@/hook/useGame"; import useGame from "@/hook/useGame";
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import BlueButton, { GreenButton, RedButton } from "../util/button"; import { BlueButton, GreenButton } from "../util/button";
import TextInput from "../util/textInput"; import { TextInput } from "../util/textInput";
import { useTeamConnexion } from "@/context/teamConnexionContext"; import useTeamConnexion from "@/context/teamConnexionContext";
import { EnemyTeamModal } from "./enemyTeamModal"; import EnemyTeamModal from "./enemyTeamModal";
import Image from "next/image";
export default function ActionDrawer() { export default function ActionDrawer() {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@@ -73,4 +72,4 @@ export default function ActionDrawer() {
<EnemyTeamModal visible={enemyModalVisible} onClose={() => setEnemyModalVisible(false)} /> <EnemyTeamModal visible={enemyModalVisible} onClose={() => setEnemyModalVisible(false)} />
</div> </div>
) )
} }

View File

@@ -1,11 +1,9 @@
import useGame from "@/hook/useGame"; import useGame from "@/hook/useGame";
import { RedButton } from "../util/button"; import { RedButton } from "../util/button";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import Image from "next/image";
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
export function EnemyTeamModal({ visible, onClose }) { export default function EnemyTeamModal({ visible, onClose }) {
const { teamId, enemyName } = useGame(); const { teamId, enemyName } = useGame();
const imageRef = useRef(null); const imageRef = useRef(null);
@@ -38,4 +36,4 @@ export function EnemyTeamModal({ visible, onClose }) {
</div> </div>
</> </>
) )
} }

View File

@@ -1,7 +1,6 @@
"use client";
import { useState } from "react"; import { useState } from "react";
import BlueButton from "../util/button"; import { BlueButton } from "../util/button";
import TextInput from "../util/textInput"; import { TextInput } from "../util/textInput";
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) { export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
@@ -17,4 +16,4 @@ export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
<BlueButton type="submit">{buttonText}</BlueButton> <BlueButton type="submit">{buttonText}</BlueButton>
</form> </form>
) )
} }

View File

@@ -1,15 +1,13 @@
'use client';
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Circle, MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet' import { Circle, MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet'
import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css' import 'leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css'
import "leaflet-defaulticon-compatibility"; import "leaflet-defaulticon-compatibility";
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import useGame from '@/hook/useGame'; import useGame from '@/hook/useGame';
import { useTeamContext } from '@/context/teamContext'; import useTeamContext from '@/context/teamContext';
const DEFAULT_ZOOM = 14; const DEFAULT_ZOOM = 14;
// Pan to the center of the map when the position of the user is updated for the first time // Pan to the center of the map when the position of the user is updated for the first time
function MapPan(props) { function MapPan(props) {
const map = useMap(); const map = useMap();

View File

@@ -1,26 +1,29 @@
import { useSocketListener } from "@/hook/useSocketListener"; import { useSocketListener } from "@/hook/useSocketListener";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function Notification({ socket }) { export default function Notification({ socket }) {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [timeoutId, setTimeoutId] = useState(null); const [timeoutId, setTimeoutId] = useState(null);
const [notification, setNotification] = useState(null); const [notification, setNotification] = useState(null);
useSocketListener(socket, "error", (notification) => { useSocketListener(socket, "error", (notification) => {
console.log("error", notification); console.log("error", notification);
setNotification({ type: "error", text: notification }); setNotification({ type: "error", text: notification });
setVisible(true); setVisible(true);
}); });
useSocketListener(socket, "success", (notification) => { useSocketListener(socket, "success", (notification) => {
console.log("success", notification); console.log("success", notification);
setNotification({ type: "success", text: notification }); setNotification({ type: "success", text: notification });
setVisible(true); setVisible(true);
}); });
useSocketListener(socket, "warning", (notification) => { useSocketListener(socket, "warning", (notification) => {
console.log("warning", notification); console.log("warning", notification);
setNotification({ type: "warning", text: notification }); setNotification({ type: "warning", text: notification });
setVisible(true); setVisible(true);
}); });
// Hide the notification after 5 seconds // Hide the notification after 5 seconds
useEffect(() => { useEffect(() => {
console.log({ visible }); console.log({ visible });
@@ -34,12 +37,14 @@ export function Notification({ socket }) {
} }
}, [visible]); }, [visible]);
let bgColorMap = { const bgColorMap = {
error: "bg-red-500 text-white", error: "bg-red-500 text-white",
success: "bg-green-500", success: "bg-green-500",
warning: "bg-yellow-500" warning: "bg-yellow-500"
} }
const classNames = 'fixed relative w-11/12 p-5 z-30 mx-auto inset-x-0 flex justify-center rounded-xl transition-all shadow-xl ' + (visible ? "top-5 " : "-translate-y-full "); const classNames = 'fixed relative w-11/12 p-5 z-30 mx-auto inset-x-0 flex justify-center rounded-xl transition-all shadow-xl ' + (visible ? "top-5 " : "-translate-y-full ");
return ( return (
Object.keys(bgColorMap).map((key) => Object.keys(bgColorMap).map((key) =>
notification?.type == key && notification?.type == key &&
@@ -47,5 +52,5 @@ export function Notification({ socket }) {
<p className="absolute top-2 right-2 p-2 rounded-l text-3xl bg-white">x</p> <p className="absolute top-2 right-2 p-2 rounded-l text-3xl bg-white">x</p>
<p className='text-center text-xl'>{notification?.text}</p> <p className='text-center text-xl'>{notification?.text}</p>
</div> </div>
)); ));
} }

View File

@@ -1,6 +1,5 @@
import { useTeamConnexion } from "@/context/teamConnexionContext"; import useTeamConnexion from "@/context/teamConnexionContext";
import useGame from "@/hook/useGame" import useGame from "@/hook/useGame"
import Image from "next/image";
export default function PlacementOverlay() { export default function PlacementOverlay() {
const { name, ready } = useGame(); const { name, ready } = useGame();

View File

@@ -1,12 +1,10 @@
import useGame from "@/hook/useGame" import useGame from "@/hook/useGame"
import { GreenButton, LogoutButton } from "../util/button"; import { GreenButton, LogoutButton } from "../util/button";
import { useRef } from "react"; import { useRef } from "react";
import Image from "next/image"; import useTeamContext from "@/context/teamContext";
import { useTeamContext } from "@/context/teamContext";
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
export function WaitingScreen() { export default function WaitingScreen() {
const { name, teamId } = useGame(); const { name, teamId } = useGame();
const { gameSettings } = useTeamContext(); const { gameSettings } = useTeamContext();
const imageRef = useRef(null); const imageRef = useRef(null);

View File

@@ -1,7 +1,6 @@
import { useTeamConnexion } from "@/context/teamConnexionContext"; import { useTeamConnexion } from "@/context/teamConnexionContext";
import Image from "next/image";
export default function BlueButton({ children, ...props }) { export function BlueButton({ children, ...props }) {
return (<button {...props} className="bg-blue-600 hover:bg-blue-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded"> return (<button {...props} className="bg-blue-600 hover:bg-blue-500 text-lg ease-out duration-200 text-white w-full h-full p-4 shadow-sm rounded">
{children} {children}
</button>) </button>)
@@ -20,6 +19,6 @@ export function GreenButton({ children, ...props }) {
} }
export function LogoutButton() { export function LogoutButton() {
const { logout } = useTeamConnexion(); const { logout } = useTeamConnexion();
return <img src="/icons/logout.png" onClick={logout} className='w-12 h-12 bg-red-500 p-2 top-1 right-1 rounded-lg cursor-pointer bg-red fixed z-20' /> return <img src="/icons/logout.png" onClick={logout} className='w-12 h-12 bg-red-500 p-2 top-1 right-1 rounded-lg cursor-pointer bg-red fixed z-20' />
} }

View File

@@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
export default function TextInput({...props}) { export function TextInput({...props}) {
return ( return (
<input {...props} type="text" className="block w-full h-full p-4 rounded text-center ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600" /> <input {...props} type="text" className="block w-full h-full p-4 rounded text-center ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600" />
) )

View File

@@ -1,11 +1,12 @@
"use client"; "use client";
import { createContext, useContext, useMemo, } from "react"; import { createContext, useContext, useMemo } from "react";
import { useSocket } from "./socketContext"; import { useSocket } from "./socketContext";
import { useSocketAuth } from "@/hook/useSocketAuth"; import useSocketAuth from "@/hook/useSocketAuth";
import { usePasswordProtect } from "@/hook/usePasswordProtect"; import usePasswordProtect from "@/hook/usePasswordProtect";
const adminConnexionContext = createContext(); const adminConnexionContext = createContext();
const AdminConnexionProvider = ({ children }) => {
export function AdminConnexionProvider({ children }) {
const { adminSocket } = useSocket(); const { adminSocket } = useSocket();
const { login, loggedIn, loading } = useSocketAuth(adminSocket, "admin_password"); const { login, loggedIn, loading } = useSocketAuth(adminSocket, "admin_password");
const useProtect = () => usePasswordProtect("/admin/login", "/admin", loading, loggedIn); const useProtect = () => usePasswordProtect("/admin/login", "/admin", loading, loggedIn);
@@ -19,9 +20,6 @@ const AdminConnexionProvider = ({ children }) => {
); );
} }
function useAdminConnexion() { export function useAdminConnexion() {
return useContext(adminConnexionContext); return useContext(adminConnexionContext);
} }
export { AdminConnexionProvider, useAdminConnexion };

View File

@@ -1,53 +1,43 @@
"use client"; "use client";
import { createContext, useContext, useEffect, useMemo, useState } from "react"; import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { useSocket } from "./socketContext"; import { useSocket } from "./socketContext";
import { useSocketListener } from "@/hook/useSocketListener"; import useSocketListener from "@/hook/useSocketListener";
import { useAdminConnexion } from "./adminConnexionContext"; import { useAdminConnexion } from "./adminConnexionContext";
import { GameState } from "@/util/gameState"; import { GameState } from "@/util/gameState";
const adminContext = createContext(); const adminContext = createContext();
function AdminProvider({ children }) { export function AdminProvider({ children }) {
const [teams, setTeams] = useState([]); const [teams, setTeams] = useState([]);
const [zoneSettings, setZoneSettings] = useState(null) const [zoneSettings, setZoneSettings] = useState(null)
const [penaltySettings, setPenaltySettings] = useState(null); const [penaltySettings, setPenaltySettings] = useState(null);
const [gameSettings, setGameSettings] = useState(null); const [gameSettings, setGameSettings] = useState(null);
const [zone, setZone] = useState(null);
const [zoneExtremities, setZoneExtremities] = useState(null); const [zoneExtremities, setZoneExtremities] = useState(null);
const [nextZoneDate, setNextZoneDate] = useState(null); const [nextZoneDate, setNextZoneDate] = useState(null);
const [isShrinking, setIsShrinking] = useState(false);
const { adminSocket } = useSocket(); const { adminSocket } = useSocket();
const { loggedIn } = useAdminConnexion(); const { loggedIn } = useAdminConnexion();
const [gameState, setGameState] = useState(GameState.SETUP); const [gameState, setGameState] = useState(GameState.SETUP);
const [startDate, setStartDate] = useState(null); const [startDate, setStartDate] = useState(null);
useSocketListener(adminSocket, "game_state", (data) => {setGameState(data.state); setStartDate(data.startDate)}); useSocketListener(adminSocket, "game_state", (data) => {setGameState(data.state); setStartDate(data.startDate)});
//Send a request to get the teams when the user logs in // Send a request to get the teams when the user logs in
useEffect(() => { useEffect(() => {
adminSocket.emit("get_teams"); adminSocket.emit("get_teams");
}, [loggedIn]); }, [loggedIn]);
function waiting(data) { function setCurrent_zone(data) {
setIsShrinking(false);
setZoneExtremities({begin: data.begin, end: data.end}); setZoneExtremities({begin: data.begin, end: data.end});
setNextZoneDate(data.endDate); setNextZoneDate(data.endDate);
} }
function shrinking(data) { // Bind listeners to update the team list and the game status on socket message
setIsShrinking(true);
setNextZoneDate(data);
}
//Bind listeners to update the team list and the game status on socket message
useSocketListener(adminSocket, "teams", setTeams); useSocketListener(adminSocket, "teams", setTeams);
useSocketListener(adminSocket, "zone_settings", setZoneSettings); useSocketListener(adminSocket, "zone_settings", setZoneSettings);
useSocketListener(adminSocket, "game_settings", setGameSettings); useSocketListener(adminSocket, "game_settings", setGameSettings);
useSocketListener(adminSocket, "penalty_settings", setPenaltySettings); useSocketListener(adminSocket, "penalty_settings", setPenaltySettings);
useSocketListener(adminSocket, "zone", setZone); useSocketListener(adminSocket, "current_zone", setCurrent_zone);
useSocketListener(adminSocket, "zone_start", shrinking);
useSocketListener(adminSocket, "new_zone", waiting);
const value = useMemo(() => ({ zone, zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, isShrinking, startDate }), [zoneSettings, teams, gameState, zone, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, isShrinking, startDate]); const value = useMemo(() => ({ zoneExtremities, teams, zoneSettings, penaltySettings, gameSettings, gameState, nextZoneDate, startDate }), [zoneSettings, teams, gameState, zoneExtremities, penaltySettings, gameSettings, nextZoneDate, startDate]);
return ( return (
<adminContext.Provider value={value}> <adminContext.Provider value={value}>
{children} {children}
@@ -55,8 +45,6 @@ function AdminProvider({ children }) {
); );
} }
function useAdminContext() { export function useAdminContext() {
return useContext(adminContext); return useContext(adminContext);
} }
export { AdminProvider, useAdminContext };

View File

@@ -1,22 +1,17 @@
"use client"; "use client";
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo } from "react";
import { env } from 'next-runtime-env'; import { env } from 'next-runtime-env';
import { io } from 'socket.io-client';
const { io } = require("socket.io-client");
var proto = "wss://";
const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST"); const NEXT_PUBLIC_SOCKET_HOST = env("NEXT_PUBLIC_SOCKET_HOST");
if (NEXT_PUBLIC_SOCKET_HOST == "localhost") { const SOCKET_URL = (NEXT_PUBLIC_SOCKET_HOST == "localhost" ? "ws://" : "wss://") + NEXT_PUBLIC_SOCKET_HOST;
proto = "ws://";
}
const SOCKET_URL = proto + NEXT_PUBLIC_SOCKET_HOST;
const USER_SOCKET_URL = SOCKET_URL + "/player"; const USER_SOCKET_URL = SOCKET_URL + "/player";
const ADMIN_SOCKET_URL = SOCKET_URL + "/admin"; const ADMIN_SOCKET_URL = SOCKET_URL + "/admin";
export const teamSocket = io(USER_SOCKET_URL, { export const teamSocket = io(USER_SOCKET_URL, {
path: "/back/socket.io", path: "/back/socket.io",
}); });
export const adminSocket = io(ADMIN_SOCKET_URL, { export const adminSocket = io(ADMIN_SOCKET_URL, {
path: "/back/socket.io", path: "/back/socket.io",
}); });

View File

@@ -1,13 +1,14 @@
"use client"; "use client";
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo } from "react";
import { useSocket } from "./socketContext"; import { useSocket } from "./socketContext";
import { useSocketAuth } from "@/hook/useSocketAuth"; import useSocketAuth from "@/hook/useSocketAuth";
import { usePasswordProtect } from "@/hook/usePasswordProtect"; import usePasswordProtect from "@/hook/usePasswordProtect";
const teamConnexionContext = createContext(); const teamConnexionContext = createContext();
const TeamConnexionProvider = ({ children }) => {
export function TeamConnexionProvider({ children }) {
const { teamSocket } = useSocket(); const { teamSocket } = useSocket();
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password"); const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
const useProtect = () => usePasswordProtect("/team", "/team/track", loading, loggedIn); const useProtect = () => usePasswordProtect("/team", "/team/track", loading, loggedIn);
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading, useProtect}), [teamId, login, loggedIn, loading]); const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading, useProtect}), [teamId, login, loggedIn, loading]);
@@ -19,9 +20,6 @@ const TeamConnexionProvider = ({ children }) => {
); );
} }
function useTeamConnexion() { export function useTeamConnexion() {
return useContext(teamConnexionContext); return useContext(teamConnexionContext);
} }
export { TeamConnexionProvider, useTeamConnexion };

View File

@@ -1,14 +1,14 @@
"use client"; "use client";
import { useLocation } from "@/hook/useLocation"; import useLocation from "@/hook/useLocation";
import { useSocketListener } from "@/hook/useSocketListener"; import useSocketListener from "@/hook/useSocketListener";
import { createContext, use, useContext, useEffect, useMemo, useRef, useState } from "react"; import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
import { useSocket } from "./socketContext"; import { useSocket } from "./socketContext";
import { useTeamConnexion } from "./teamConnexionContext"; import { useTeamConnexion } from "./teamConnexionContext";
import { GameState } from "@/util/gameState"; import { GameState } from "@/util/gameState";
const teamContext = createContext();
const teamContext = createContext() export function TeamProvider({children}) {
function TeamProvider({children}) {
const [teamInfos, setTeamInfos] = useState({}); const [teamInfos, setTeamInfos] = useState({});
const [gameState, setGameState] = useState(GameState.SETUP); const [gameState, setGameState] = useState(GameState.SETUP);
const [gameSettings, setGameSettings] = useState(null); const [gameSettings, setGameSettings] = useState(null);
@@ -21,17 +21,12 @@ function TeamProvider({children}) {
teamInfosRef.current = teamInfos; teamInfosRef.current = teamInfos;
useSocketListener(teamSocket, "update_team", (newTeamInfos) => { useSocketListener(teamSocket, "update_team", (newTeamInfos) => setTeamInfos({...teamInfosRef.current, ...newTeamInfos}) );
setTeamInfos({...teamInfosRef.current, ...newTeamInfos});
});
useSocketListener(teamSocket, "game_state", setGameState); useSocketListener(teamSocket, "game_state", setGameState);
useSocketListener(teamSocket, "zone", setZone); useSocketListener(teamSocket, "zone", setZone);
useSocketListener(teamSocket, "new_zone", setZoneExtremities); useSocketListener(teamSocket, "new_zone", setZoneExtremities);
useSocketListener(teamSocket, "game_settings", setGameSettings); useSocketListener(teamSocket, "game_settings", setGameSettings);
//Send the current position to the server when the user is logged in //Send the current position to the server when the user is logged in
useEffect(() => { useEffect(() => {
console.log("sending position", measuredLocation); console.log("sending position", measuredLocation);
@@ -48,8 +43,6 @@ function TeamProvider({children}) {
); );
} }
function useTeamContext() { export function useTeamContext() {
return useContext(teamContext); return useContext(teamContext);
} }
export { TeamProvider, useTeamContext };

View File

@@ -1,3 +1,4 @@
"use client";
import { useAdminContext } from "@/context/adminContext"; import { useAdminContext } from "@/context/adminContext";
import { useSocket } from "@/context/socketContext"; import { useSocket } from "@/context/socketContext";
@@ -52,4 +53,4 @@ export default function useAdmin() {
} }
return { ...adminContext, changeGameSettings, changeZoneSettings, changePenaltySettings, pollTeams, getTeam, getTeamName, reorderTeams, addTeam, removeTeam, changeState, updateTeam }; return { ...adminContext, changeGameSettings, changeZoneSettings, changePenaltySettings, pollTeams, getTeam, getTeamName, reorderTeams, addTeam, removeTeam, changeState, updateTeam };
} }

View File

@@ -1,5 +1,4 @@
"use client"; "use client";
import { useSocket } from "@/context/socketContext"; import { useSocket } from "@/context/socketContext";
import { useTeamConnexion } from "@/context/teamConnexionContext"; import { useTeamConnexion } from "@/context/teamConnexionContext";
import { useTeamContext } from "@/context/teamContext"; import { useTeamContext } from "@/context/teamContext";
@@ -33,4 +32,4 @@ export default function useGame() {
teamId, teamId,
gameState, gameState,
}; };
} }

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useLocalStorage(key, initialValue) { export default function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(initialValue); const [storedValue, setStoredValue] = useState(initialValue);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);

View File

@@ -5,11 +5,10 @@ import { useEffect, useState } from "react";
* A hook that returns the location of the user and updates it periodically * A hook that returns the location of the user and updates it periodically
* @returns {Object} The location of the user * @returns {Object} The location of the user
*/ */
export function useLocation(interval) { export default function useLocation(interval) {
const [location, setLocation] = useState(); const [location, setLocation] = useState();
useEffect(() => { useEffect(() => {
function update() { function update() {
console.log('Updating location');
navigator.geolocation.getCurrentPosition((position) => { navigator.geolocation.getCurrentPosition((position) => {
setLocation([position.coords.latitude, position.coords.longitude]); setLocation([position.coords.latitude, position.coords.longitude]);
if(interval != Infinity) { if(interval != Infinity) {
@@ -21,4 +20,4 @@ export function useLocation(interval) {
}, []); }, []);
return location; return location;
} }

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useMapCircleDraw(area, setArea) { export default function useMapCircleDraw(area, setArea) {
const [drawing, setDrawing] = useState(false); const [drawing, setDrawing] = useState(false);
const [center, setCenter] = useState(area?.center || null); const [center, setCenter] = useState(area?.center || null);
const [radius, setRadius] = useState(area?.radius || null); const [radius, setRadius] = useState(area?.radius || null);
@@ -12,7 +13,7 @@ export function useMapCircleDraw(area, setArea) {
}, [area]) }, [area])
function handleClick(e) { function handleClick(e) {
if(!drawing) { if (!drawing) {
setCenter(e.latlng); setCenter(e.latlng);
setRadius(null); setRadius(null);
setDrawing(true); setDrawing(true);
@@ -23,14 +24,10 @@ export function useMapCircleDraw(area, setArea) {
} }
function handleMouseMove(e) { function handleMouseMove(e) {
if(drawing) { if (drawing) {
setRadius(e.latlng.distanceTo(center)); setRadius(e.latlng.distanceTo(center));
} }
} }
return {
handleClick, return { handleClick, handleMouseMove, center, radius };
handleMouseMove, }
center,
radius,
}
}

View File

@@ -0,0 +1,214 @@
"use client";
import { useState } from "react";
import { useMap } from "react-leaflet";
export default function useMapPolygonDraw(polygons, addPolygon, removePolygon) {
const map = useMap();
const nodeCatchDistance = 30; // px
const nodeHighlightDistance = 30; // px
const [currentPolygon, setCurrentPolygon] = useState([]);
const [highlightNodes, setHighlightNodes] = useState([]);
function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
return Math.abs(latlng1.lat - latlng2.lat) < epsilon && Math.abs(latlng1.lng - latlng2.lng) < epsilon;
}
function layerDistance(latlng1, latlng2) {
// Return the pixel distance between latlng1 and latlng2 as they appear on the map
const {x: x1, y: y1} = map.latLngToLayerPoint(latlng1);
const {x: x2, y: y2} = map.latLngToLayerPoint(latlng2);
return Math.sqrt((x1 - x2)**2 + (y1 - y2)**2);
}
function isDrawing() {
return currentPolygon.length > 0;
}
function areSegmentsIntersecting(p1, p2, p3, p4) {
// Return true if the segments (p1, p2) and (p3, p4) are strictly intersecting, else false
const direction = (a, b, c) => {
return (c.lng - a.lng) * (b.lat - a.lat) - (b.lng - a.lng) * (c.lat - a.lat);
};
const d1 = direction(p3, p4, p1);
const d2 = direction(p3, p4, p2);
const d3 = direction(p1, p2, p3);
const d4 = direction(p1, p2, p4);
return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
}
function isIntersecting(segment, pointArray, isPolygon) {
// Return true if segment intersects one of the pointArray segments according to areSegmentsIntersecting
// Moreover if isPolygon, then it verifies if segment intersects the segment closing pointArray
const length = pointArray.length;
for (let i = 0; i < length-1; i++) {
if (areSegmentsIntersecting(segment[0], segment[1], pointArray[i], pointArray[i+1])) {
return true;
}
}
if (isPolygon && length > 2) {
return areSegmentsIntersecting(segment[0], segment[1], pointArray[length-1], pointArray[0]);
} else {
return false;
}
}
function isInPolygon(latlng, polygon) {
// Return true if latlng is strictly inside polygon
// Return false if latlng is outside polygon or on a vertex of the polygon
// Return true or false if latlng is on the border
if (latlngEqual(latlng, polygon[0])) return false;
const length = polygon.length;
const {lat: x, lng: y} = latlng;
let inside = false;
for (let i = 0, j = length - 1; i < length; j = i++) {
if (latlngEqual(latlng, polygon[j])) return false;
const {lat: xi, lng: yi} = polygon[i];
const {lat: xj, lng: yj} = polygon[j];
const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);
if (intersects) inside = !inside;
}
return inside;
}
function isClockwise(points) {
// Return true if the tab describes a clockwise polygon (Shoelace formula)
let sum = 0;
for (let i = 0; i < points.length; i++) {
const curr = points[i];
const next = points[(i + 1) % points.length];
sum += (next.lng - curr.lng) * (next.lat + curr.lat);
}
return sum > 0;
};
function getZoneIndex(latlng) {
// Return the index of the polygon where latlng is according to isInPolygon
for (let iPolygon = 0; iPolygon < polygons.length; iPolygon++) {
if (isInPolygon(latlng, polygons[iPolygon])) {
return iPolygon;
}
}
return -1;
}
function getEventLatLng(e) {
// Return the closest latlng to e.latlng among the existing nodes including the first node of currentPolygon
// If the closest distance is superior to nodeCatchDistance, then e.latlng is returned
const closeNodes = [];
// Existing nodes
for (const polygon of polygons) {
for (const node of polygon) {
const d = layerDistance(e.latlng, node);
if (d < nodeCatchDistance) {
closeNodes.push([d, node]);
}
}
}
// First node of currentPolygon
if (isDrawing()) {
const d = layerDistance(e.latlng, currentPolygon[0]);
if (d < nodeCatchDistance) {
closeNodes.push([d, currentPolygon[0]]);
}
}
// If there is no close node
if (closeNodes.length == 0) {
return e.latlng;
// Else return the closest close node
} else {
return closeNodes.reduce( (min, current) => { return current[0] < min[0] ? current : min } )[1];
}
}
function handleLeftClick(e) {
setHighlightNodes([]);
const latlng = getEventLatLng(e);
const length = currentPolygon.length;
// If it is the first node
if (!isDrawing()) {
// If the point is not in an existing polygon
if (getZoneIndex(latlng) == -1) {
setCurrentPolygon([latlng]);
}
// If it is the last node
} else if (latlngEqual(latlng, currentPolygon[0])) {
// If the current polygon is a polygon (at least 3 points)
if (length >= 3) {
// If the current polygon is not circling an existing polygon
for (const polygon of polygons) {
// meanPoint exists and is strictly inside polygon
const meanPoint = {
lat: (polygon[0].lat + polygon[1].lat + polygon[2].lat) / 3,
lng: (polygon[0].lng + polygon[1].lng + polygon[2].lng) / 3
};
if (isInPolygon(meanPoint, currentPolygon)) return;
}
// Making the new polygon clockwise to simplify some algorithms
if (!isClockwise(currentPolygon)) currentPolygon.reverse();
addPolygon(currentPolygon);
setCurrentPolygon([]);
}
// If it is an intermediate node
} else {
// Is the polygon closing to early ?
for (const point of currentPolygon) if (latlngEqual(point, latlng)) return;
// Is the new point making the current polygon intersect with itself ?
if (isIntersecting([latlng, currentPolygon[length-1]], currentPolygon, false)) return;
// Is the new point inside a polygon ?
if (getZoneIndex(latlng) != -1) return;
// Is the new point making the current polygon intersect with another polygon ?
for (const polygon of polygons) {
// Strict intersection
if (isIntersecting([latlng, currentPolygon[length-1]], polygon, true)) return;
// Intersection by joining two non adjacent nodes of polygon
let tab = [-1, -1];
for (let i = 0; i < polygon.length; i++) {
if (latlngEqual(latlng, polygon[i])) tab[0] = i;
if (latlngEqual(currentPolygon[length-1], polygon[i])) tab[1] = i;
}
if (
tab[0] != -1 && tab[1] != -1 &&
(tab[0] != (tab[1] + 1) % polygon.length) &&
(tab[1] != (tab[0] + 1) % polygon.length)
) return;
}
setCurrentPolygon([...currentPolygon, latlng]);
}
}
function handleRightClick(e) {
setHighlightNodes([]);
// If isDrawing, cancel the currentPolygon
if (isDrawing()) {
setCurrentPolygon([]);
// If not isDrawing, remove the clicked polygon
} else {
const i = getZoneIndex(e.latlng);
if (i != -1) removePolygon(i);
}
}
function handleMouseMove(e) {
const nodes = [];
for (const polygon of polygons) {
for (const node of polygon) {
if (layerDistance(node, e.latlng) < nodeHighlightDistance && node != currentPolygon[0]) nodes.push(node);
}
}
setHighlightNodes(nodes);
}
return { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove };
}

View File

@@ -2,7 +2,7 @@
import { redirect, usePathname } from "next/navigation"; import { redirect, usePathname } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
export function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) { export default function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
const path = usePathname(); const path = usePathname();
useEffect(() => { useEffect(() => {
if (!loggedIn && !loading && path !== loginPath) { if (!loggedIn && !loading && path !== loginPath) {
@@ -12,4 +12,4 @@ export function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
redirect(redirectPath) redirect(redirectPath)
} }
}, [loggedIn, loading, path]); }, [loggedIn, loading, path]);
} }

View File

@@ -1,13 +1,13 @@
import {useEffect, useState} from 'react'; "use client";
import { useSocketListener } from './useSocketListener'; import { useEffect, useState } from 'react';
import { useLocalStorage } from './useLocalStorage'; import useSocketListener from './useSocketListener';
import { usePathname } from 'next/navigation'; import useLocalStorage from './useLocalStorage';
const LOGIN_MESSAGE = "login"; const LOGIN_MESSAGE = "login";
const LOGOUT_MESSAGE = "logout"; const LOGOUT_MESSAGE = "logout";
const LOGIN_RESPONSE_MESSAGE = "login_response"; const LOGIN_RESPONSE_MESSAGE = "login_response";
export function useSocketAuth(socket, passwordName) { export default function useSocketAuth(socket, passwordName) {
const [loggedIn, setLoggedIn] = useState(false); const [loggedIn, setLoggedIn] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [waitingForResponse, setWaitingForResponse] = useState(true); const [waitingForResponse, setWaitingForResponse] = useState(true);
@@ -50,4 +50,4 @@ export function useSocketAuth(socket, passwordName) {
return {login,logout,password: savedPassword, loggedIn, loading}; return {login,logout,password: savedPassword, loggedIn, loading};
} }

View File

@@ -1,10 +1,11 @@
import { useEffect} from "react"; "use client";
import { useEffect } from "react";
export function useSocketListener(socket, event, callback) { export default function useSocketListener(socket, event, callback) {
useEffect(() => { useEffect(() => {
socket.on(event,callback); socket.on(event,callback);
return () => { return () => {
socket.off(event, callback); socket.off(event, callback);
} }
}, []); }, []);
} }