mirror of
https://git.rezel.net/LudoTech/traque.git
synced 2026-02-09 02:10:18 +01:00
Ajout zones en pavage + fix dockefiles
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
110
traque-front/components/admin/circleZoneMap.jsx
Normal file
110
traque-front/components/admin/circleZoneMap.jsx
Normal 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='© <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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
75
traque-front/components/admin/liveMap.jsx
Normal file
75
traque-front/components/admin/liveMap.jsx
Normal 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='© <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>
|
||||
)
|
||||
}
|
||||
@@ -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='© <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='© <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='© <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>
|
||||
)
|
||||
}
|
||||
88
traque-front/components/admin/mapUtils.jsx
Normal file
88
traque-front/components/admin/mapUtils.jsx
Normal 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);
|
||||
}, []);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
33
traque-front/components/admin/placementMap.jsx
Normal file
33
traque-front/components/admin/placementMap.jsx
Normal 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='© <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>
|
||||
);
|
||||
}
|
||||
142
traque-front/components/admin/polygonZoneMap.jsx
Normal file
142
traque-front/components/admin/polygonZoneMap.jsx
Normal 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='© <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>
|
||||
);
|
||||
}
|
||||
@@ -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('');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' />
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
)
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
214
traque-front/hook/useMapPolygonDraw.jsx
Normal file
214
traque-front/hook/useMapPolygonDraw.jsx
Normal 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 };
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user