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

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
"use client";
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 { WaitingScreen } from '@/components/team/waitingScreen';
import WaitingScreen from '@/components/team/waitingScreen';
import { LogoutButton } from '@/components/util/button';
import { useSocket } from '@/context/socketContext';
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 { useEffect, useState } from "react";
export const GameSettings = () => {
export default function GameSettings() {
const {gameSettings, changeGameSettings} = useAdmin();
const [capturedMessage, setCapturedMessage] = useState("");
const [winnerEndMessage, setWinnerEndMessage] = useState("");
@@ -11,7 +11,6 @@ export const GameSettings = () => {
const [waitingMessage, setWaitingMessage] = useState("");
useEffect(() => {
console.log({gameSettings})
if (gameSettings) {
setCapturedMessage(gameSettings.capturedMessage);
setWinnerEndMessage(gameSettings.winnerEndGameMessage);
@@ -46,4 +45,4 @@ export const GameSettings = () => {
<GreenButton onClick={applySettings}>Apply</GreenButton>
</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 TextInput from "../util/textInput";
import { TextInput } from "../util/textInput";
import { GreenButton } from "../util/button";
import { useEffect, useState } from "react";
export const PenaltySettings = () => {
export default function PenaltySettings() {
const {penaltySettings, changePenaltySettings} = useAdmin();
const [maxPenalties, setMaxPenalties] = useState("");
const [allowedTimeOutOfZone, setAllowedTimeOutOfZone] = useState("");
@@ -46,4 +46,4 @@ export const PenaltySettings = () => {
<GreenButton onClick={applySettings}>Apply</GreenButton>
</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 TextInput from '../util/textInput'
import BlueButton from '../util/button'
import { TextInput } from '../util/textInput'
import { BlueButton } from '../util/button'
export default function TeamAddForm({onAddTeam}) {
const [teamName, setTeamName] = React.useState('');

View File

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

View File

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

View File

@@ -1,16 +1,18 @@
import useAdmin from "@/hook/useAdmin"
export function TeamReady() {
export default function TeamReady() {
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'>
<h2 className="text-2xl">Teams ready status</h2>
{teams.map((team) => team.ready ? (
<div key={team.id} className="p-2 text-white bg-green-500 shadow-md text-xl rounded flex flex-row">
<div>{team.name} : Ready</div>
</div>) : (
<div key={team.id} className="p-2 text-white bg-red-500 shadow-md text-xl rounded flex flex-row">
<div>{team.name} : Not ready</div>
</div>
return (
<div className='w-full h-full gap-1 bg-white p-10 flex flex-col text-center shadow-2xl overflow-y-scroll'>
<h2 className="text-2xl">Teams ready status</h2>
{teams.map((team) => team.ready ? (
<div key={team.id} className="p-2 text-white bg-green-500 shadow-md text-xl rounded flex flex-row">
<div>{team.name} : Ready</div>
</div>) : (
<div key={team.id} className="p-2 text-white bg-red-500 shadow-md text-xl rounded flex flex-row">
<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 { useEffect, useState } from "react"
import BlueButton, { GreenButton, RedButton } from "../util/button";
import TextInput from "../util/textInput";
import { useTeamConnexion } from "@/context/teamConnexionContext";
import { EnemyTeamModal } from "./enemyTeamModal";
import Image from "next/image";
import { BlueButton, GreenButton } from "../util/button";
import { TextInput } from "../util/textInput";
import useTeamConnexion from "@/context/teamConnexionContext";
import EnemyTeamModal from "./enemyTeamModal";
export default function ActionDrawer() {
const [visible, setVisible] = useState(false);
@@ -73,4 +72,4 @@ export default function ActionDrawer() {
<EnemyTeamModal visible={enemyModalVisible} onClose={() => setEnemyModalVisible(false)} />
</div>
)
}
}

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,29 @@
import { useSocketListener } from "@/hook/useSocketListener";
import { useEffect, useState } from "react";
export function Notification({ socket }) {
export default function Notification({ socket }) {
const [visible, setVisible] = useState(false);
const [timeoutId, setTimeoutId] = useState(null);
const [notification, setNotification] = useState(null);
useSocketListener(socket, "error", (notification) => {
console.log("error", notification);
setNotification({ type: "error", text: notification });
setVisible(true);
});
useSocketListener(socket, "success", (notification) => {
console.log("success", notification);
setNotification({ type: "success", text: notification });
setVisible(true);
});
useSocketListener(socket, "warning", (notification) => {
console.log("warning", notification);
setNotification({ type: "warning", text: notification });
setVisible(true);
});
// Hide the notification after 5 seconds
useEffect(() => {
console.log({ visible });
@@ -34,12 +37,14 @@ export function Notification({ socket }) {
}
}, [visible]);
let bgColorMap = {
const bgColorMap = {
error: "bg-red-500 text-white",
success: "bg-green-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 ");
return (
Object.keys(bgColorMap).map((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='text-center text-xl'>{notification?.text}</p>
</div>
));
}
));
}

View File

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

View File

@@ -1,12 +1,10 @@
import useGame from "@/hook/useGame"
import { GreenButton, LogoutButton } from "../util/button";
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';
export function WaitingScreen() {
export default function WaitingScreen() {
const { name, teamId } = useGame();
const { gameSettings } = useTeamContext();
const imageRef = useRef(null);

View File

@@ -1,7 +1,6 @@
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">
{children}
</button>)
@@ -20,6 +19,6 @@ export function GreenButton({ children, ...props }) {
}
export function LogoutButton() {
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' />
}
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' />
}

View File

@@ -1,6 +1,6 @@
import React from 'react'
export default function TextInput({...props}) {
export function TextInput({...props}) {
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" />
)

View File

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

View File

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

View File

@@ -1,22 +1,17 @@
"use client";
import { createContext, useContext, useMemo } from "react";
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");
if (NEXT_PUBLIC_SOCKET_HOST == "localhost") {
proto = "ws://";
}
const SOCKET_URL = proto + NEXT_PUBLIC_SOCKET_HOST;
const SOCKET_URL = (NEXT_PUBLIC_SOCKET_HOST == "localhost" ? "ws://" : "wss://") + NEXT_PUBLIC_SOCKET_HOST;
const USER_SOCKET_URL = SOCKET_URL + "/player";
const ADMIN_SOCKET_URL = SOCKET_URL + "/admin";
export const teamSocket = io(USER_SOCKET_URL, {
path: "/back/socket.io",
});
export const adminSocket = io(ADMIN_SOCKET_URL, {
path: "/back/socket.io",
});

View File

@@ -1,13 +1,14 @@
"use client";
import { createContext, useContext, useMemo } from "react";
import { useSocket } from "./socketContext";
import { useSocketAuth } from "@/hook/useSocketAuth";
import { usePasswordProtect } from "@/hook/usePasswordProtect";
import useSocketAuth from "@/hook/useSocketAuth";
import usePasswordProtect from "@/hook/usePasswordProtect";
const teamConnexionContext = createContext();
const TeamConnexionProvider = ({ children }) => {
export function TeamConnexionProvider({ children }) {
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 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);
}
export { TeamConnexionProvider, useTeamConnexion };

View File

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

View File

@@ -1,3 +1,4 @@
"use client";
import { useAdminContext } from "@/context/adminContext";
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 };
}
}

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
export function useLocalStorage(key, initialValue) {
export default function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(initialValue);
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
* @returns {Object} The location of the user
*/
export function useLocation(interval) {
export default function useLocation(interval) {
const [location, setLocation] = useState();
useEffect(() => {
function update() {
console.log('Updating location');
navigator.geolocation.getCurrentPosition((position) => {
setLocation([position.coords.latitude, position.coords.longitude]);
if(interval != Infinity) {
@@ -21,4 +20,4 @@ export function useLocation(interval) {
}, []);
return location;
}
}

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
export function useMapCircleDraw(area, setArea) {
export default function useMapCircleDraw(area, setArea) {
const [drawing, setDrawing] = useState(false);
const [center, setCenter] = useState(area?.center || null);
const [radius, setRadius] = useState(area?.radius || null);
@@ -12,7 +13,7 @@ export function useMapCircleDraw(area, setArea) {
}, [area])
function handleClick(e) {
if(!drawing) {
if (!drawing) {
setCenter(e.latlng);
setRadius(null);
setDrawing(true);
@@ -23,14 +24,10 @@ export function useMapCircleDraw(area, setArea) {
}
function handleMouseMove(e) {
if(drawing) {
if (drawing) {
setRadius(e.latlng.distanceTo(center));
}
}
return {
handleClick,
handleMouseMove,
center,
radius,
}
}
return { handleClick, 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 { useEffect } from "react";
export function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
export default function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
const path = usePathname();
useEffect(() => {
if (!loggedIn && !loading && path !== loginPath) {
@@ -12,4 +12,4 @@ export function usePasswordProtect(loginPath, redirectPath, loading, loggedIn) {
redirect(redirectPath)
}
}, [loggedIn, loading, path]);
}
}

View File

@@ -1,13 +1,13 @@
import {useEffect, useState} from 'react';
import { useSocketListener } from './useSocketListener';
import { useLocalStorage } from './useLocalStorage';
import { usePathname } from 'next/navigation';
"use client";
import { useEffect, useState } from 'react';
import useSocketListener from './useSocketListener';
import useLocalStorage from './useLocalStorage';
const LOGIN_MESSAGE = "login";
const LOGOUT_MESSAGE = "logout";
const LOGIN_RESPONSE_MESSAGE = "login_response";
export function useSocketAuth(socket, passwordName) {
export default function useSocketAuth(socket, passwordName) {
const [loggedIn, setLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
const [waitingForResponse, setWaitingForResponse] = useState(true);
@@ -50,4 +50,4 @@ export function useSocketAuth(socket, passwordName) {
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(() => {
socket.on(event,callback);
return () => {
socket.off(event, callback);
}
}, []);
}
}