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 && } { zoneExtremities.end && }
); - case zoneTypes.polygon: + case ZoneTypes.POLYGON: 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 (

{title}

setValue(e.target.value)} name="team-id"/> {buttonText} - ) + ); } 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" +}