diff --git a/doc/TODO.md b/doc/TODO.md
index afb5ca7..1bf8700 100644
--- a/doc/TODO.md
+++ b/doc/TODO.md
@@ -33,11 +33,11 @@
- [x] Ajouter timer du rétrécissement des zones.
- [x] Pouvoir changer les paramètres du jeu pendant une partie.
- [x] Implémenter les wireframes
-- [ ] Ajouter une région par défaut si pas de position
+- [x] Ajouter une région par défaut si pas de position
- [ ] Pouvoir faire pause dans la partie
- [ ] Voir les traces et évènements des teams
- [ ] Voir l'incertitude de position des teams
-- [ ] Focus une team cliquée
+- [x] Focus une team cliquée
- [ ] Refaire les flèches de chasse sur la map
- [ ] Mettre en évidence le menu paramètre (configuration)
- [ ] Afficher un feedback quand un paramètre est sauvegardé
diff --git a/traque-front/app/admin/components/liveMap.jsx b/traque-front/app/admin/components/liveMap.jsx
index 4a6bff0..0c9bbf8 100644
--- a/traque-front/app/admin/components/liveMap.jsx
+++ b/traque-front/app/admin/components/liveMap.jsx
@@ -1,9 +1,10 @@
import { useEffect, useState } from "react";
import { Marker, Tooltip, Polyline, Polygon, Circle } from "react-leaflet";
import "leaflet/dist/leaflet.css";
-import { CustomMapContainer } from "@/components/map";
+import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
import useAdmin from "@/hook/useAdmin";
-import { GameState } from "@/util/gameState";
+import { GameState, ZoneTypes } from "@/util/types";
+import { mapZooms } from "@/util/configurations";
const positionIcon = new L.Icon({
iconUrl: '/icons/marker/blue.png',
@@ -13,12 +14,7 @@ const positionIcon = new L.Icon({
shadowSize: [30, 30],
});
-const zoneTypes = {
- circle: "circle",
- polygon: "polygon"
-}
-
-export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
+export default function LiveMap({ selectedTeamId, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows}) {
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
@@ -58,14 +54,14 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
if (!(showZones && gameState == GameState.PLAYING && zoneType)) return null;
switch (zoneType) {
- case zoneTypes.circle:
+ case ZoneTypes.CIRCLE:
return (
{ zoneExtremities.begin &&
}
@@ -81,14 +77,16 @@ export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
{gameState == GameState.PLAYING &&
{`Next zone in : ${formatTime(timeLeftNextZone)}`}
}
+ {isFocusing && }
+ setIsFocusing(false)}/>
{teams.map((team) => team.currentLocation && !team.captured &&
-
+ <>
{showNames && {team.name}}
{showArrows &&
}
-
+ >
)}
diff --git a/traque-front/app/admin/components/teamSidePanel.jsx b/traque-front/app/admin/components/teamSidePanel.jsx
index 97d94bc..a099c4d 100644
--- a/traque-front/app/admin/components/teamSidePanel.jsx
+++ b/traque-front/app/admin/components/teamSidePanel.jsx
@@ -1,7 +1,7 @@
import { env } from 'next-runtime-env';
import { useEffect, useState } from "react";
import useAdmin from "@/hook/useAdmin";
-import { GameState } from '@/util/gameState';
+import { getStatus } from '@/util/functions';
function DotLine({ label, value }) {
return (
@@ -27,28 +27,6 @@ function IconValue({ color, icon, value }) {
);
}
-const TEAM_STATUS = {
- playing: { label: "En jeu", color: "text-custom-green" },
- captured: { label: "Capturée", color: "text-custom-red" },
- outofzone: { label: "Hors zone", color: "text-custom-orange" },
- ready: { label: "Placée", color: "text-custom-green" },
- notready: { label: "Non placée", color: "text-custom-red" },
- waiting: { label: "En attente", color: "text-custom-grey" },
-};
-
-function getStatus(team, gamestate) {
- switch (gamestate) {
- case GameState.SETUP:
- return TEAM_STATUS.waiting;
- case GameState.PLACEMENT:
- return team.ready ? TEAM_STATUS.ready : TEAM_STATUS.notready;
- case GameState.PLAYING:
- return team.captured ? TEAM_STATUS.captured : team.outofzone ? TEAM_STATUS.outofzone : TEAM_STATUS.playing;
- case GameState.FINISHED:
- return team.captured ? TEAM_STATUS.captured : TEAM_STATUS.playing;
- }
-}
-
export default function TeamSidePanel({ selectedTeamId, onClose }) {
const { getTeam, startDate, gameState } = useAdmin();
const [imgSrc, setImgSrc] = useState("");
diff --git a/traque-front/app/admin/components/teamViewer.jsx b/traque-front/app/admin/components/teamViewer.jsx
index 8eb5872..f22cd97 100644
--- a/traque-front/app/admin/components/teamViewer.jsx
+++ b/traque-front/app/admin/components/teamViewer.jsx
@@ -1,28 +1,6 @@
import { List } from '@/components/list';
import useAdmin from '@/hook/useAdmin';
-import { GameState } from '@/util/gameState';
-
-const TEAM_STATUS = {
- playing: { label: "En jeu", color: "text-custom-green" },
- captured: { label: "Capturée", color: "text-custom-red" },
- outofzone: { label: "Hors zone", color: "text-custom-orange" },
- ready: { label: "Placée", color: "text-custom-green" },
- notready: { label: "Non placée", color: "text-custom-red" },
- waiting: { label: "En attente", color: "text-custom-grey" },
-};
-
-function getStatus(team, gamestate) {
- switch (gamestate) {
- case GameState.SETUP:
- return TEAM_STATUS.waiting;
- case GameState.PLACEMENT:
- return team.ready ? TEAM_STATUS.ready : TEAM_STATUS.notready;
- case GameState.PLAYING:
- return team.captured ? TEAM_STATUS.captured : team.outofzone ? TEAM_STATUS.outofzone : TEAM_STATUS.playing;
- case GameState.FINISHED:
- return team.captured ? TEAM_STATUS.captured : TEAM_STATUS.playing;
- }
-}
+import { getStatus } from '@/util/functions';
function TeamViewerItem({ team, itemSelected, onSelected }) {
const { gameState } = useAdmin();
diff --git a/traque-front/app/admin/login/components/loginForm.jsx b/traque-front/app/admin/login/components/loginForm.jsx
index 6688c8a..6642e88 100644
--- a/traque-front/app/admin/login/components/loginForm.jsx
+++ b/traque-front/app/admin/login/components/loginForm.jsx
@@ -4,16 +4,18 @@ import { TextInput } from "@/components/input";
export default function LoginForm({ onSubmit, title, placeholder, buttonText}) {
const [value, setValue] = useState("");
+
function handleSubmit(e) {
e.preventDefault();
setValue("");
onSubmit(value);
}
+
return (
- )
+ );
}
diff --git a/traque-front/app/admin/page.js b/traque-front/app/admin/page.js
index 6c5db05..37bf127 100644
--- a/traque-front/app/admin/page.js
+++ b/traque-front/app/admin/page.js
@@ -1,11 +1,12 @@
"use client";
-import React, { useState } from 'react';
+import { useState } from 'react';
import dynamic from "next/dynamic";
import Link from "next/link";
import { Section } from "@/components/section";
import { useAdminConnexion } from "@/context/adminConnexionContext";
import useAdmin from "@/hook/useAdmin";
-import { GameState } from "@/util/gameState";
+import { GameState } from "@/util/types";
+import { mapStyles } from '@/util/configurations';
import TeamSidePanel from "./components/teamSidePanel";
import TeamViewer from './components/teamViewer';
import { MapButton, ControlButton } from './components/buttons';
@@ -13,33 +14,25 @@ import { MapButton, ControlButton } from './components/buttons';
// Imported at runtime and not at compile time
const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
-const mapStyles = {
- default: {
- url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
- attribution: '©
OpenStreetMap'
- },
- satellite: {
- url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
- attribution: 'Tiles © Esri'
- },
-}
-
export default function AdminPage() {
const { useProtect } = useAdminConnexion();
const [selectedTeamId, setSelectedTeamId] = useState(null);
- const { changeState } = useAdmin();
+ const { changeState, getTeam } = useAdmin();
const [mapStyle, setMapStyle] = useState(mapStyles.default);
const [showZones, setShowZones] = useState(true);
const [showNames, setShowNames] = useState(true);
const [showArrows, setShowArrows] = useState(false);
+ const [isFocusing, setIsFocusing] = useState(true);
useProtect();
function onSelected(id) {
- if (selectedTeamId === id) {
+ if (selectedTeamId == id && (!getTeam(id)?.currentLocation || isFocusing)) {
setSelectedTeamId(null);
+ setIsFocusing(false);
} else {
setSelectedTeamId(id);
+ setIsFocusing(true);
}
}
@@ -82,7 +75,15 @@ export default function AdminPage() {
-
+
{selectedTeamId &&
diff --git a/traque-front/app/admin/parameters/page.js b/traque-front/app/admin/parameters/page.js
index 92af4c1..e64e5c0 100644
--- a/traque-front/app/admin/parameters/page.js
+++ b/traque-front/app/admin/parameters/page.js
@@ -10,18 +10,12 @@ import useAdmin from '@/hook/useAdmin';
import Messages from "./components/messages";
import TeamManager from './components/teamManager';
import useLocalVariable from "@/hook/useLocalVariable";
+import { ZoneTypes } from "@/util/types";
+import { defaultZoneSettings } from "@/util/configurations";
// Imported at runtime and not at compile time
-const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false });
const CircleZoneSelector = dynamic(() => import('./components/circleZoneSelector'), { ssr: false });
-
-const zoneTypes = {
- circle: "circle",
- polygon: "polygon"
-}
-
-const defaultCircleSettings = {type: zoneTypes.circle, min: null, max: null, reductionCount: 4, duration: 10}
-const defaultPolygonSettings = {type: zoneTypes.polygon, polygons: []}
+const PolygonZoneSelector = dynamic(() => import('./components/polygonZoneSelector'), { ssr: false });
export default function ConfigurationPage() {
const { useProtect } = useAdminConnexion();
@@ -34,10 +28,10 @@ export default function ConfigurationPage() {
function modifyLocalZoneSettings(key, value) {
setLocalZoneSettings(prev => ({...prev, [key]: value}));
- };
+ }
function handleChangeZoneType() {
- setLocalZoneSettings(localZoneSettings.type == zoneTypes.circle ? defaultPolygonSettings : defaultCircleSettings)
+ setLocalZoneSettings(localZoneSettings.type == ZoneTypes.CIRCLE ? defaultZoneSettings.polygon : defaultZoneSettings.circle)
}
function handleTeamSubmit(e) {
@@ -83,10 +77,10 @@ export default function ConfigurationPage() {
{localZoneSettings && Change zone type}
- {localZoneSettings && localZoneSettings.type == zoneTypes.circle &&
+ {localZoneSettings && localZoneSettings.type == ZoneTypes.CIRCLE &&
}
- {localZoneSettings && localZoneSettings.type == zoneTypes.polygon &&
+ {localZoneSettings && localZoneSettings.type == ZoneTypes.POLYGON &&
}
diff --git a/traque-front/components/map.jsx b/traque-front/components/map.jsx
index 21130da..f9724be 100644
--- a/traque-front/components/map.jsx
+++ b/traque-front/components/map.jsx
@@ -1,33 +1,21 @@
import { useEffect, useState } from "react";
import { MapContainer, TileLayer, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
+import { mapLocations, mapZooms, mapStyles } from "@/util/configurations";
-const DEFAULT_ZOOM = 14;
-
-const mapStyles = {
- default: {
- url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
- attribution: '©
OpenStreetMap'
- },
- satellite: {
- url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
- attribution: 'Tiles © Esri'
- },
-}
-
-export function MapPan({center, zoom}) {
+export function MapPan({center, zoom, animate=false}) {
const map = useMap();
useEffect(() => {
- if (center, zoom) {
- map.flyTo(center, zoom, { animate: false });
+ if (center && zoom) {
+ map.flyTo(center, zoom, { animate: animate });
}
}, [center, zoom]);
return null;
}
-export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
+export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDragStart }) {
const map = useMap();
// Handle the mouse click left
@@ -93,6 +81,17 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
map.off('mousemove', onMouseMove);
}
}, [onMouseMove]);
+
+ // Handle the drag start
+ useEffect(() => {
+ if (!onDragStart) return;
+
+ map.on('dragstart', onDragStart);
+
+ return () => {
+ map.off('dragstart', onDragStart);
+ }
+ }, [onDragStart]);
// Prevent right click context menu
useEffect(() => {
@@ -105,9 +104,23 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove }) {
return null;
}
+function MapResizeWatcher() {
+ const map = useMap();
+
+ useEffect(() => {
+ const observer = new ResizeObserver(() => {
+ map.invalidateSize();
+ });
+ observer.observe(map.getContainer());
+
+ return () => observer.disconnect();
+ }, [map]);
+
+ return null;
+}
+
export function CustomMapContainer({mapStyle, children}) {
const [location, setLocation] = useState(null);
- const [loading, setLoading] = useState(true);
useEffect(() => {
if (!navigator.geolocation) {
@@ -118,7 +131,6 @@ export function CustomMapContainer({mapStyle, children}) {
navigator.geolocation.getCurrentPosition(
(pos) => {
setLocation([pos.coords.latitude, pos.coords.longitude]);
- setLoading(false);
},
(err) => console.log("Error :", err),
{
@@ -129,13 +141,11 @@ export function CustomMapContainer({mapStyle, children}) {
);
}, []);
- if (loading) {
- return
- }
-
return (
-
+
+
+
{children}
)
diff --git a/traque-front/context/adminContext.jsx b/traque-front/context/adminContext.jsx
index 8b4ad63..fd7650b 100644
--- a/traque-front/context/adminContext.jsx
+++ b/traque-front/context/adminContext.jsx
@@ -2,7 +2,7 @@
import { createContext, useContext, useMemo, useState } from "react";
import { useSocket } from "./socketContext";
import useSocketListener from "@/hook/useSocketListener";
-import { GameState } from "@/util/gameState";
+import { GameState } from "@/util/types";
const adminContext = createContext();
diff --git a/traque-front/util/configurations.js b/traque-front/util/configurations.js
new file mode 100644
index 0000000..3b45441
--- /dev/null
+++ b/traque-front/util/configurations.js
@@ -0,0 +1,35 @@
+import { ZoneTypes } from "./types";
+
+export const mapLocations = {
+ paris: [48.86, 2.33]
+}
+
+export const mapZooms = {
+ low: 4,
+ high: 15,
+}
+
+export const mapStyles = {
+ default: {
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ attribution: '© OpenStreetMap'
+ },
+ satellite: {
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
+ attribution: 'Tiles © Esri'
+ },
+}
+
+export const defaultZoneSettings = {
+ circle: {type: ZoneTypes.CIRCLE, min: null, max: null, reductionCount: 4, duration: 10},
+ polygon: {type: ZoneTypes.POLYGON, polygons: []}
+}
+
+export const teamStatus = {
+ playing: { label: "En jeu", color: "text-custom-green" },
+ captured: { label: "Capturée", color: "text-custom-red" },
+ outofzone: { label: "Hors zone", color: "text-custom-orange" },
+ ready: { label: "Placée", color: "text-custom-green" },
+ notready: { label: "Non placée", color: "text-custom-red" },
+ waiting: { label: "En attente", color: "text-custom-grey" },
+}
diff --git a/traque-front/util/functions.js b/traque-front/util/functions.js
new file mode 100644
index 0000000..dc3a9d3
--- /dev/null
+++ b/traque-front/util/functions.js
@@ -0,0 +1,15 @@
+import { GameState } from './types';
+import { teamStatus } from './configurations';
+
+export function getStatus(team, gamestate) {
+ switch (gamestate) {
+ case GameState.SETUP:
+ return teamStatus.waiting;
+ case GameState.PLACEMENT:
+ return team.ready ? teamStatus.ready : teamStatus.notready;
+ case GameState.PLAYING:
+ return team.captured ? teamStatus.captured : team.outofzone ? teamStatus.outofzone : teamStatus.playing;
+ case GameState.FINISHED:
+ return team.captured ? teamStatus.captured : teamStatus.playing;
+ }
+}
diff --git a/traque-front/util/gameState.js b/traque-front/util/types.js
similarity index 61%
rename from traque-front/util/gameState.js
rename to traque-front/util/types.js
index e5514f7..e15c0f8 100644
--- a/traque-front/util/gameState.js
+++ b/traque-front/util/types.js
@@ -3,4 +3,9 @@ export const GameState = {
PLACEMENT: "placement",
PLAYING: "playing",
FINISHED: "finished"
-}
\ No newline at end of file
+}
+
+export const ZoneTypes = {
+ CIRCLE: "circle",
+ POLYGON: "polygon"
+}