Server heavy refactoring 3 (not functionnal)

This commit is contained in:
Sebastien Riviere
2026-03-04 18:36:06 +01:00
parent 8046feadb0
commit e1b6c0e0c5
50 changed files with 6576 additions and 687 deletions

View File

@@ -1,8 +1,7 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
.vscode
.vscode
.env

View File

@@ -1,18 +1,31 @@
# Use Node 22 alpine as parent image
FROM node:22-alpine AS base
# Étape commune
FROM node:22-slim AS base
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY package.json package-lock.json ./
RUN npm ci
RUN apk add --no-cache libc6-compat
COPY package.json package-lock.json* ./
RUN npm install
# Étape développement
FROM base AS dev
ENV NODE_ENV=development
COPY . .
ENV NODE_ENV development
ENV NEXT_TELEMETRY_DISABLED 1
EXPOSE 3000
CMD ["npm", "run", "dev"]
CMD ["npm", "run", "dev"]
# Étape builder
FROM base AS builder
ENV NODE_ENV=production
COPY . .
RUN npm run build
# Étape production
FROM node:22-slim AS prod
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
COPY --from=builder --chown=node:node /app/public ./public
USER node
EXPOSE 3000
CMD ["node", "server.js"]

View File

@@ -1,27 +0,0 @@
# Use Node 22 alpine as parent image
FROM node:22-alpine
# Change the working directory on the Docker image to /app
WORKDIR /app
# Installs glibc compatibility on Alpine to support native Node.js modules that require glibc
RUN apk add --no-cache libc6-compat
# Change specified variables
ENV NODE_ENV=development
ENV NEXT_TELEMETRY_DISABLED=1
# Copy package.json and package-lock.json to the /app directory
COPY package.json package-lock.json* ./
# Install dependencies
RUN npm install
# Copy the rest of project files into this image
COPY . .
# Expose the port
EXPOSE 3000
# Start the server in dev mode
CMD ["npm", "run", "dev"]

View File

@@ -2,18 +2,18 @@ import { Fragment, useEffect, useState } from "react";
import { Arrow, CircleZone, PolygonZone, Position, Tag } from "@/components/layer";
import { CustomMapContainer, MapEventListener, MapPan } from "@/components/map";
import useAdmin from "@/hook/useAdmin";
import { GameState, ZoneTypes } from "@/util/types";
import { GameState } from "@/util/types";
import { mapZooms } from "@/util/configurations";
export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsFocusing, mapStyle, showZones, showNames, showArrows }) {
const { zoneType, zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
const { zones, teams, getTeam, gameState } = useAdmin();
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
const [isFullScreen, setIsFullScreen] = useState(false);
useEffect(() => {
if (nextZoneDate) {
if (zones?.zoneTransitionDate) {
const updateTime = () => {
setTimeLeftNextZone(Math.max(0, Math.floor((nextZoneDate - Date.now()) / 1000)));
setTimeLeftNextZone(Math.max(0, Math.floor((zones.zoneTransitionDate - Date.now()) / 1000)));
};
updateTime();
@@ -21,7 +21,7 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF
return () => clearInterval(interval);
}
}, [nextZoneDate]);
}, [zones?.zoneTransitionDate]);
function formatTime(time) {
// time is in seconds
@@ -34,20 +34,10 @@ export default function LiveMap({ selectedTeamId, onSelected, isFocusing, setIsF
function Zones() {
if (!(showZones && gameState == GameState.PLAYING)) return null;
switch (zoneType) {
case ZoneTypes.CIRCLE:
return (<>
<CircleZone circle={zoneExtremities.begin} color={mapStyle.currentZoneColor} />
<CircleZone circle={zoneExtremities.end} color={mapStyle.nextZoneColor} />
</>);
case ZoneTypes.POLYGON:
return (<>
<PolygonZone polygon={zoneExtremities.begin?.polygon} color={mapStyle.currentZoneColor} />
<PolygonZone polygon={zoneExtremities.end?.polygon} color={mapStyle.nextZoneColor} />
</>);
default:
return null;
}
return (<>
<PolygonZone polygon={zones.currentZone} color={mapStyle.currentZoneColor} />
<PolygonZone polygon={zones.nextZone} color={mapStyle.nextZoneColor} />
</>);
}
return (

View File

@@ -29,7 +29,7 @@ function IconValue({ color, icon, value }) {
}
export default function TeamSidePanel({ selectedTeamId, onClose }) {
const { getTeam, startDate, gameState } = useAdmin();
const { getTeam, gameState } = useAdmin();
const [imgSrc, setImgSrc] = useState("");
const [_, setRefreshKey] = useState(0);
const team = getTeam(selectedTeamId);
@@ -106,11 +106,11 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
<DotLine label="Chassé par" value={getTeam(team.chased)?.name ?? NO_VALUE} />
</div>
}
{ (gameState == GameState.PLAYING || gameState == GameState.FINISHED) &&
{ (gameState == GameState.PLAYING || gameState == GameState.FINISHED) && false &&
<div>
<DotLine label="Distance" value={formatDistance(team.distance)} />
<DotLine label="Temps de survie" value={formatTime(startDate, team.finishDate || Date.now())} />
<DotLine label="Vitesse moyenne" value={formatSpeed(team.distance, startDate, team.finishDate || Date.now())} />
<DotLine label="Temps de survie" value={formatTime(0, team.finishDate || Date.now())} />
<DotLine label="Vitesse moyenne" value={formatSpeed(team.distance, 0, team.finishDate || Date.now())} />
<DotLine label="Captures" value={team.nCaptures ?? NO_VALUE} />
<DotLine label="Observations" value={team.nSentLocation ?? NO_VALUE} />
<DotLine label="Observé" value={team.nObserved ?? NO_VALUE} />

View File

@@ -46,9 +46,9 @@ function Drawings({ minZone, setMinZone, maxZone, setMaxZone, editMode }) {
}
export default function CircleZoneSelector({ display }) {
const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin();
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e}));
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
const {settings, updateSettings} = useAdmin();
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(settings.playingZones, (e) => updateSettings({playingZones: e}));
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(settings.outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
const [editMode, setEditMode] = useState(EditMode.MAX);
useEffect(() => {

View File

@@ -1,30 +1,8 @@
import { Section } from "@/components/section";
import useAdmin from "@/hook/useAdmin";
import useLocalVariable from "@/hook/useLocalVariable";
function MessageInput({title, ...props}) {
return (
<div className="w-full flex flex-row gap-3 items-center">
<p>{title}</p>
<input className="w-full p-1 rounded ring-1 ring-inset ring-gray-400 placeholder:text-gray-600" {...props} />
</div>
);
}
export default function Messages() {
const {messages, updateSettings} = useAdmin();
const [localGameSettings, setLocalGameSettings, applyLocalGameSettings] = useLocalVariable(messages, (e) => updateSettings({messages: e}));
function modifyLocalZoneSettings(key, value) {
setLocalGameSettings(prev => ({...prev, [key]: value}));
};
return (
<Section title="Messages" innerClassName="w-full h-full flex flex-col gap-3 items-center">
<MessageInput id="waiting" title="Attente  :" value={localGameSettings?.waiting ?? ""} onChange={(e) => modifyLocalZoneSettings("waiting", e.target.value)} onBlur={applyLocalGameSettings}/>
<MessageInput id="captured" title="Capture :" value={localGameSettings?.captured ?? ""} onChange={(e) => modifyLocalZoneSettings("captured", e.target.value)} onBlur={applyLocalGameSettings}/>
<MessageInput id="winner" title="Victoire :" value={localGameSettings?.winner ?? ""} onChange={(e) => modifyLocalZoneSettings("winner", e.target.value)} onBlur={applyLocalGameSettings}/>
<MessageInput id="loser" title="Défaite  :" value={localGameSettings?.loser ?? ""} onChange={(e) => modifyLocalZoneSettings("loser", e.target.value)} onBlur={applyLocalGameSettings}/>
</Section>
);
}

View File

@@ -19,19 +19,17 @@ function ZoneTypeButton({title, onClick, isSelected}) {
}
export default function PlayingZoneSelector({ display }) {
const { zoneType } = useAdmin();
const [localZoneType, setLocalZoneType] = useLocalVariable(zoneType, () => {});
return (
<div className={display ? 'w-full h-full gap-3 flex flex-col' : "hidden"}>
<div className="w-full flex flex-row gap-3 items-center">
<p className="text-l">Type de zone :</p>
<ZoneTypeButton title="Cercles" onClick={() => setLocalZoneType(ZoneTypes.CIRCLE)} isSelected={localZoneType == ZoneTypes.CIRCLE} />
<ZoneTypeButton title="Polygones" onClick={() => setLocalZoneType(ZoneTypes.POLYGON)} isSelected={localZoneType == ZoneTypes.POLYGON} />
<ZoneTypeButton title="Cercles" onClick={() => setLocalZoneType(ZoneTypes.CIRCLE)} isSelected={ZoneTypes.POLYGON == ZoneTypes.CIRCLE} />
<ZoneTypeButton title="Polygones" onClick={() => setLocalZoneType(ZoneTypes.POLYGON)} isSelected={ZoneTypes.POLYGON == ZoneTypes.POLYGON} />
</div>
<div className="w-full flex-1">
<CircleZoneSelector display={localZoneType == ZoneTypes.CIRCLE} />
<PolygonZoneSelector display={localZoneType == ZoneTypes.POLYGON} />
<CircleZoneSelector display={ZoneTypes.POLYGON == ZoneTypes.CIRCLE} />
<PolygonZoneSelector display={ZoneTypes.POLYGON == ZoneTypes.POLYGON} />
</div>
</div>
);

View File

@@ -54,9 +54,9 @@ function Drawings({ localZoneSettings, addZone, removeZone }) {
export default function PolygonZoneSelector({ display }) {
const defaultDuration = 10;
const {zoneSettings, outOfZoneDelay, updateSettings} = useAdmin();
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(zoneSettings, (e) => updateSettings({zone: e}));
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
const {settings, updateSettings} = useAdmin();
const [localZoneSettings, setLocalZoneSettings, applyLocalZoneSettings] = useLocalVariable(settings.zones, (e) => updateSettings({zone: e}));
const [localOutOfZoneDelay, setLocalOutOfZoneDelay, applyLocalOutOfZoneDelay] = useLocalVariable(settings.outOfZoneDelay, (e) => updateSettings({outOfZoneDelay: e}));
useEffect(() => {
if (!localZoneSettings || localZoneSettings.type != ZoneTypes.POLYGON) {

View File

@@ -21,9 +21,9 @@ function TeamManagerItem({ team }) {
}
export default function TeamManager() {
const { teams, addTeam, reorderTeams, sendPositionDelay, updateSettings } = useAdmin();
const { teams, addTeam, reorderTeams, settings, updateSettings } = useAdmin();
const [teamName, setTeamName] = useState('');
const [localSendPositionDelay, setLocalSendPositionDelay, applyLocalSendPositionDelay] = useLocalVariable(sendPositionDelay, (e) => updateSettings({sendPositionDelay: e}));
const [localSendPositionDelay, setLocalSendPositionDelay, applyLocalSendPositionDelay] = useLocalVariable(settings.scanDelay, (e) => updateSettings({scanDelay: e}));
function handleTeamSubmit(e) {
e.preventDefault();

View File

@@ -8,44 +8,21 @@ const adminContext = createContext();
export function AdminProvider({ children }) {
const { adminSocket } = useSocket();
// teams
const [teams, setTeams] = useState([]);
// game_state
const [gameState, setGameState] = useState(GameState.SETUP);
const [startDate, setStartDate] = useState(null);
// current_zone
const [zoneType, setZoneType] = useState(null);
const [zoneExtremities, setZoneExtremities] = useState(null);
const [nextZoneDate, setNextZoneDate] = useState(null);
// settings
const [messages, setMessages] = useState(null);
const [zoneSettings, setZoneSettings] = useState(null)
const [sendPositionDelay, setSendPositionDelay] = useState(null);
const [outOfZoneDelay, setOutOfZoneDelay] = useState(null);
useSocketListener(adminSocket, "teams", setTeams);
useSocketListener(adminSocket, "game_state", (data) => {
setGameState(data.state);
setStartDate(data.date);
});
useSocketListener(adminSocket, "current_zone", (data) => {
setZoneExtremities({begin: data.begin, end: data.end});
setNextZoneDate(data.endDate);
});
useSocketListener(adminSocket, "settings", (data) => {
setMessages(data.messages);
setZoneSettings(data.zone);
setZoneType(data.zone.type);
setSendPositionDelay(data.sendPositionDelay);
setOutOfZoneDelay(data.outOfZoneDelay);
const [teams, setTeams] = useState([]);
const [zones, setZones] = useState(null);
const [settings, setSettings] = useState(null);
useSocketListener(adminSocket, "update-full", ({ gameState, teams, zones, settings }) => {
setGameState(gameState);
setTeams(teams);
setZones(zones);
setSettings(settings);
});
const value = useMemo(() => (
{ zoneSettings, teams, gameState, zoneType, zoneExtremities, sendPositionDelay, outOfZoneDelay, messages, nextZoneDate, startDate }
), [zoneSettings, teams, gameState, zoneType, zoneExtremities, sendPositionDelay, outOfZoneDelay, messages, nextZoneDate, startDate]);
{ gameState, teams, zones, settings }
), [gameState, teams, zones, settings]);
return (
<adminContext.Provider value={value}>

View File

@@ -12,15 +12,15 @@ export default function useAdmin() {
}
function addTeam(teamName) {
adminSocket.emit("add_team", teamName);
adminSocket.emit("add-team", teamName);
}
function removeTeam(teamId) {
adminSocket.emit("remove_team", teamId);
adminSocket.emit("remove-team", teamId);
}
function reorderTeams(newOrder) {
adminSocket.emit("reorder_teams", newOrder);
adminSocket.emit("reorder-teams", newOrder);
}
function captureTeam(teamId) {
@@ -32,11 +32,11 @@ export default function useAdmin() {
}
function changeState(state) {
adminSocket.emit("change_state", state);
adminSocket.emit("state", state);
}
function updateSettings(settings) {
adminSocket.emit("update_settings", settings);
adminSocket.emit("settings", settings);
}
return { ...adminContext, getTeam, reorderTeams, addTeam, removeTeam, captureTeam, placementTeam, changeState, updateSettings };

View File

@@ -1,19 +1,18 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
output: 'standalone',
async redirects() {
return [
{
source: '/',
destination: '/admin',
permanent: false, // The browser will not save the redirect in its cache
},
]
},
async redirects() {
return [
{
source: '/',
destination: '/admin',
permanent: false,
},
]
},
};
export default nextConfig;

6351
server/traque-front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
"name": "traque-front",
"version": "0.1.0",
"private": true,
"author": "Quentin Roussel",
"scripts": {
"dev": "next dev",
"build": "next build",

View File

@@ -7,7 +7,7 @@ export const Colors = {
}
export const GameState = {
SETUP: "setup",
SETUP: "default",
PLACEMENT: "placement",
PLAYING: "playing",
FINISHED: "finished"