This commit is contained in:
Sebastien Riviere
2026-02-13 00:22:29 +01:00
parent eb72a1e5da
commit 5f16500634
11 changed files with 79 additions and 37 deletions

View File

@@ -11,9 +11,9 @@ Problèmes
[ ] Une équipe perdait sans arrêt la connection avec le serveur [ ] Une équipe perdait sans arrêt la connection avec le serveur
[ ] La position en arrière plan, téléphone éteint par exemple, n'avait pas l'air de fonctionner (une équipe n'avait pas de notif) [ ] La position en arrière plan, téléphone éteint par exemple, n'avait pas l'air de fonctionner (une équipe n'avait pas de notif)
[ ] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe. [ ] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe.
[ ] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus [x] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus
automatique. automatique.
[ ] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les [x] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les
voyants des équipes encore en jeu soit mis en avant. voyants des équipes encore en jeu soit mis en avant.
[x] La visibilité des éléments de la map en calque satellite est mauvaise. Il faut revoir les couleurs. [x] La visibilité des éléments de la map en calque satellite est mauvaise. Il faut revoir les couleurs.
[ ] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre. [ ] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre.

View File

@@ -16,9 +16,9 @@ qu'un seul téléphone connecté.
[ ] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe. [ ] La photo d'une des équipes ne parvenait pas jusqu'au serveur. Tout semblait normal pour l'équipe.
[ ] Il y a apparement eu un problème de synchronisation de l'affichage entre admin concernant les zones de départs. Il s'est résolu [ ] Il y a apparement eu un problème de synchronisation de l'affichage entre admin concernant les zones de départs. Il s'est résolu
de lui même. Peut être un problème passager de connection. de lui même. Peut être un problème passager de connection.
[ ] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus [x] Le focus sur une équipe dans la page principale des admins est à revoir. Par exemple, le zoom seul devrait désactiver le focus
automatique. automatique.
[ ] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les [x] Il est pas évident de voir qu'elles équipes sont encore en jeu sur la page principale admin. Notamment, on voudrait que les
voyants des équipes encore en jeu soit mis en avant. voyants des équipes encore en jeu soit mis en avant.
[x] La visibilité des éléments de la map en calque satellite est mauvaise. Il faut revoir les couleurs. [x] La visibilité des éléments de la map en calque satellite est mauvaise. Il faut revoir les couleurs.
[ ] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre. [ ] Les zones polygonales s'affichent mal sur la version prod de l'app mobile. Les anciennes n'ont pas l'air de disparaitre.

View File

@@ -29,7 +29,7 @@
"android" "android"
], ],
"android": { "android": {
"package": "com.anonymous.traqueapp", "package": "net.rezel.traque",
"permissions": [ "permissions": [
"ACCESS_FINE_LOCATION", "ACCESS_FINE_LOCATION",
"ACCESS_COARSE_LOCATION", "ACCESS_COARSE_LOCATION",
@@ -48,7 +48,7 @@
} }
}, },
"ios": { "ios": {
"bundleIdentifier": "com.anonymous.traqueapp", "bundleIdentifier": "net.rezel.traque",
"infoPlist": { "infoPlist": {
"UIBackgroundModes": [ "UIBackgroundModes": [
"location" "location"

View File

@@ -131,7 +131,7 @@ export default function Display() {
if (!Number.isInteger(time)) return "Inconnue"; if (!Number.isInteger(time)) return "Inconnue";
if (time < 0) time = 0; if (time < 0) time = 0;
const hours = Math.floor(time / 3600); const hours = Math.floor(time / 3600);
const minutes = Math.floor(time / 60); const minutes = Math.floor(time / 60 % 60);
const seconds = Math.floor(time % 60); const seconds = Math.floor(time % 60);
return String(hours).padStart(2,"0") + ":" + String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0"); return String(hours).padStart(2,"0") + ":" + String(minutes).padStart(2,"0") + ":" + String(seconds).padStart(2,"0");
} }

View File

@@ -1,9 +1,8 @@
import { createContext, useContext, useMemo } from "react"; import { createContext, useContext, useMemo } from "react";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import { URLS } from "../util/urls"
const SOCKET_URL = `wss://${URLS.HOST}/player`; const SOCKET_URL = `ws://172.16.1.180/player`;
const SERVER_URL = `https://${URLS.HOST}/back`; const SERVER_URL = `http://172.16.1.180/back`;
export const teamSocket = io(SOCKET_URL, { export const teamSocket = io(SOCKET_URL, {
path: "/back/socket.io", path: "/back/socket.io",

View File

@@ -1,3 +0,0 @@
export const URLS = {
HOST: 'traque.rezel.net'
}

View File

@@ -10,7 +10,6 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null); const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
const [isFullScreen, setIsFullScreen] = useState(false); const [isFullScreen, setIsFullScreen] = useState(false);
useEffect(() => { useEffect(() => {
if (nextZoneDate) { if (nextZoneDate) {
const updateTime = () => { const updateTime = () => {
@@ -38,13 +37,13 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF
switch (zoneType) { switch (zoneType) {
case ZoneTypes.CIRCLE: case ZoneTypes.CIRCLE:
return (<> return (<>
<CircleZone circle={zoneExtremities.begin} color="red" /> <CircleZone circle={zoneExtremities.begin} color={mapStyle.currentZoneColor} />
<CircleZone circle={zoneExtremities.end} color="green" /> <CircleZone circle={zoneExtremities.end} color={mapStyle.nextZoneColor} />
</>); </>);
case ZoneTypes.POLYGON: case ZoneTypes.POLYGON:
return (<> return (<>
<PolygonZone polygon={zoneExtremities.begin?.polygon} color="red" /> <PolygonZone polygon={zoneExtremities.begin?.polygon} color={mapStyle.currentZoneColor} />
<PolygonZone polygon={zoneExtremities.end?.polygon} color="green" /> <PolygonZone polygon={zoneExtremities.end?.polygon} color={mapStyle.nextZoneColor} />
</>); </>);
default: default:
return null; return null;
@@ -55,14 +54,14 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF
<div className={`${isFullScreen ? "fixed inset-0 z-[9999]" : "relative h-full w-full"}`}> <div className={`${isFullScreen ? "fixed inset-0 z-[9999]" : "relative h-full w-full"}`}>
<CustomMapContainer mapStyle={mapStyle}> <CustomMapContainer mapStyle={mapStyle}>
{isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />} {isFocusing && <MapPan center={getTeam(selectedTeamId)?.currentLocation} zoom={mapZooms.high} animate />}
<MapEventListener onDragStart={() => setIsFocusing(false)}/> <MapEventListener onDragStart={() => setIsFocusing(false)} onWheel={() => setIsFocusing(false)} />
<Zones/> <Zones/>
{teams.map((team) => team && <Fragment key={team.id}> {teams.map((team) => team && <Fragment key={team.id}>
<CircleZone circle={team.startingArea} color="blue" display={gameState == GameState.PLACEMENT && showZones}> <CircleZone circle={team.startingArea} color={mapStyle.placementZoneColor} display={gameState == GameState.PLACEMENT && showZones}>
<Tag text={team.name} display={showNames} /> <Tag text={team.name} display={showNames} />
</CircleZone> </CircleZone>
<Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing)?.currentLocation} display={showArrows}/> <Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing)?.currentLocation} color={mapStyle.arrowColor} display={showArrows}/>
<Position position={team.currentLocation} color={"blue"} onClick={() => onSelected(team.id)} display={!team.captured}> <Position position={team.currentLocation} color={mapStyle.playerColor} onClick={() => onSelected(team.id)} display={!team.captured}>
<Tag text={team.name} display={showNames} /> <Tag text={team.name} display={showNames} />
</Position> </Position>
</Fragment>)} </Fragment>)}

View File

@@ -1,6 +1,7 @@
import { List } from '@/components/list'; import { List } from '@/components/list';
import useAdmin from '@/hook/useAdmin'; import useAdmin from '@/hook/useAdmin';
import { getStatus } from '@/util/functions'; import { getStatus } from '@/util/functions';
import { useMemo } from 'react';
function TeamViewerItem({ team }) { function TeamViewerItem({ team }) {
const { gameState } = useAdmin(); const { gameState } = useAdmin();
@@ -8,7 +9,7 @@ function TeamViewerItem({ team }) {
const NO_VALUE = "XX"; const NO_VALUE = "XX";
return ( return (
<div className={'w-full flex flex-row gap-3 p-2 bg-white justify-between'}> <div className={`w-full flex flex-row gap-3 p-2 ${team.captured ? 'bg-gray-200' : 'bg-white'} justify-between`}>
<div className='flex flex-row items-center gap-3'> <div className='flex flex-row items-center gap-3'>
<div className='flex flex-row gap-1'> <div className='flex flex-row gap-1'>
<img src={`/icons/user/${team.sockets.length > 0 ? "green" : "red"}.png`} className="w-4 h-4" /> <img src={`/icons/user/${team.sockets.length > 0 ? "green" : "red"}.png`} className="w-4 h-4" />
@@ -27,8 +28,16 @@ function TeamViewerItem({ team }) {
export default function TeamViewer({selectedTeamId, onSelected}) { export default function TeamViewer({selectedTeamId, onSelected}) {
const { teams } = useAdmin(); const { teams } = useAdmin();
// Uncaptured teams first
const sortedTeams = useMemo(() => {
return [...teams].sort((a,b) => {
if (a.captured === b.captured) return 0;
return a.captured ? 1 : -1;
});
}, [teams]);
return ( return (
<List array={teams} selectedId={selectedTeamId} onSelected={onSelected} > <List array={sortedTeams} selectedId={selectedTeamId} onSelected={onSelected} >
{(team) => ( {(team) => (
<TeamViewerItem team={team}/> <TeamViewerItem team={team}/>
)} )}

View File

@@ -3,13 +3,18 @@ import { Marker, Tooltip, CircleMarker, Circle, Polygon, useMap } from "react-le
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import 'leaflet-polylinedecorator'; import 'leaflet-polylinedecorator';
export function Node({position, nodeSize = 5, color = 'black', display = true}) { export function Node({position, color = 'black', display = true}) {
const nodeSize = 5;
const fillOpacity = 1;
return ( return (
display && position && <CircleMarker center={position} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: 1 }} /> display && position && <CircleMarker center={position} radius={nodeSize} pathOptions={{ color: color, fillColor: color, fillOpacity: fillOpacity }} />
); );
} }
export function Label({position, label, color, size = 24, display = true}) { export function Label({position, label = "", color = "black", display = true}) {
const size = 24;
const labelIcon = L.divIcon({ const labelIcon = L.divIcon({
html: `<div style=" html: `<div style="
display: flex; display: flex;
@@ -18,7 +23,7 @@ export function Label({position, label, color, size = 24, display = true}) {
color: ${color}; color: ${color};
font-weight: bold; font-weight: bold;
font-size: ${size}px; font-size: ${size}px;
">${label || ""}</div>`, ">${label}</div>`,
className: 'custom-label-icon', className: 'custom-label-icon',
iconSize: [size, size], iconSize: [size, size],
iconAnchor: [size / 2, size / 2] iconAnchor: [size / 2, size / 2]
@@ -29,13 +34,19 @@ export function Label({position, label, color, size = 24, display = true}) {
); );
} }
export function Tag({text, display = true}) { export function Tag({text = "", display = true}) {
const offset = [0.5, -15];
const opacity = 1;
return ( return (
display && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{text || ""}</Tooltip> display && <Tooltip permanent direction="top" offset={offset} opacity={opacity} className="custom-tooltip">{text}</Tooltip>
); );
} }
export function CircleZone({circle, color, opacity = '0.1', border = 3, display = true, children}) { export function CircleZone({circle, color = "black", display = true, children}) {
const opacity = '0.1';
const border = 3;
return ( return (
display && circle && display && circle &&
<Circle center={circle.center} radius={circle.radius} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }}> <Circle center={circle.center} radius={circle.radius} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }}>
@@ -44,7 +55,10 @@ export function CircleZone({circle, color, opacity = '0.1', border = 3, display
); );
} }
export function PolygonZone({polygon, color, opacity = '0.1', border = 3, display = true, children}) { export function PolygonZone({polygon, color = "black", display = true, children}) {
const opacity = '0.1';
const border = 3;
return ( return (
display && polygon && polygon.length >= 3 && display && polygon && polygon.length >= 3 &&
<Polygon positions={polygon} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }}> <Polygon positions={polygon} pathOptions={{ color: color, fillColor: color, fillOpacity: opacity, weight: border }}>
@@ -53,7 +67,7 @@ export function PolygonZone({polygon, color, opacity = '0.1', border = 3, displa
); );
} }
export function Position({position, color, onClick, display = true, children}) { export function Position({position, color = "blue", onClick = () => {}, display = true, children}) {
const positionIcon = new L.Icon({ const positionIcon = new L.Icon({
iconUrl: `/icons/marker/${color}.png`, iconUrl: `/icons/marker/${color}.png`,
@@ -71,7 +85,10 @@ export function Position({position, color, onClick, display = true, children}) {
); );
} }
export function Arrow({ pos1, pos2, color = 'black', weight = 5, arrowSize = 20, insetPixels = 25, display = true }) { export function Arrow({ pos1, pos2, color = 'black', display = true }) {
const weight = 5;
const arrowSize = 20;
const insetPixels = 25;
const map = useMap(); const map = useMap();
const [insetPositions, setInsetPositions] = useState(null); const [insetPositions, setInsetPositions] = useState(null);

View File

@@ -15,7 +15,7 @@ export function MapPan({center, zoom, animate=false}) {
return null; return null;
} }
export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDragStart }) { export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDragStart, onWheel }) {
const map = useMap(); const map = useMap();
// TODO use useMapEvents instead of this + detect when zoom // TODO use useMapEvents instead of this + detect when zoom
@@ -94,6 +94,17 @@ export function MapEventListener({ onLeftClick, onRightClick, onMouseMove, onDra
} }
}, [onDragStart]); }, [onDragStart]);
useEffect(() => {
if (!onWheel) return;
const container = map.getContainer();
container.addEventListener('wheel', onWheel);
return () => {
container.removeEventListener('wheel', onWheel);
}
}, [onWheel]);
// Prevent right click context menu // Prevent right click context menu
useEffect(() => { useEffect(() => {
const container = map.getContainer(); const container = map.getContainer();

View File

@@ -12,11 +12,21 @@ export const mapZooms = {
export const mapStyles = { export const mapStyles = {
default: { default: {
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
arrowColor: "black",
currentZoneColor: "red",
nextZoneColor: "green",
placementZoneColor: "blue",
playerColor: "blue"
}, },
satellite: { satellite: {
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
attribution: 'Tiles &copy; Esri' attribution: 'Tiles &copy; Esri',
arrowColor: "white",
currentZoneColor: "red",
nextZoneColor: "#0F0",
placementZoneColor: "#0FF",
playerColor: "blue"
}, },
} }