Scrollable team list
|
Before Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,2 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
||||||
<svg width="800px" height="800px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><title>ionicons-v5-e</title><path d="M112,112l20,320c.95,18.49,14.4,32,32,32H348c17.67,0,30.87-13.51,32-32l20-320" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="80" y1="112" x2="432" y2="112" style="stroke:#000000;stroke-linecap:round;stroke-miterlimit:10;stroke-width:32px"/><path d="M192,112V72h0a23.93,23.93,0,0,1,24-24h80a23.93,23.93,0,0,1,24,24h0v40" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="256" y1="176" x2="256" y2="400" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="184" y1="176" x2="192" y2="400" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/><line x1="328" y1="176" x2="320" y2="400" style="fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -37,13 +37,14 @@ export function playersBroadcast(event, data) {
|
|||||||
*/
|
*/
|
||||||
export function sendUpdatedTeamInformations(teamId) {
|
export function sendUpdatedTeamInformations(teamId) {
|
||||||
const team = game.getTeam(teamId);
|
const team = game.getTeam(teamId);
|
||||||
|
if (!team) return;
|
||||||
teamBroadcast(teamId, "update_team", {
|
teamBroadcast(teamId, "update_team", {
|
||||||
// Identification
|
// Identification
|
||||||
name: team.name,
|
name: team.name,
|
||||||
captureCode: team.captureCode,
|
captureCode: team.captureCode,
|
||||||
// Chasing
|
// Chasing
|
||||||
captured: team.captured,
|
captured: team.captured,
|
||||||
enemyName: game.getTeam(team.chasing).name,
|
enemyName: game.getTeam(team.chasing)? game.getTeam(team.chasing).name : null,
|
||||||
// Locations
|
// Locations
|
||||||
lastSentLocation: team.lastSentLocation,
|
lastSentLocation: team.lastSentLocation,
|
||||||
enemyLocation: team.enemyLocation,
|
enemyLocation: team.enemyLocation,
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export function MapButton({ icon, title }) {
|
export function MapButton({ icon, ...props }) {
|
||||||
return (
|
return (
|
||||||
<button title={title} className="w-16 h-16 bg-custom-light-blue rounded-full hover:bg-blue-500 transition flex items-center justify-center">
|
<button className="w-16 h-16 bg-custom-light-blue rounded-full hover:bg-blue-500 transition flex items-center justify-center" {...props}>
|
||||||
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ControlButton({ icon, title }) {
|
export function ControlButton({ icon, ...props }) {
|
||||||
return (
|
return (
|
||||||
<button title={title} className="w-[4.5rem] h-[4.5rem] bg-custom-light-blue rounded-lg hover:bg-blue-500 transition flex items-center justify-center">
|
<button className="w-[4.5rem] h-[4.5rem] bg-custom-light-blue rounded-lg hover:bg-blue-500 transition flex items-center justify-center" {...props}>
|
||||||
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
<img src={`/icons/${icon}.png`} className="w-10 h-10" />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ import useAdmin from "@/hook/useAdmin";
|
|||||||
import { GameState } from "@/util/gameState";
|
import { GameState } from "@/util/gameState";
|
||||||
|
|
||||||
const DEFAULT_ZOOM = 14;
|
const DEFAULT_ZOOM = 14;
|
||||||
|
|
||||||
const positionIcon = new L.Icon({
|
const positionIcon = new L.Icon({
|
||||||
iconUrl: '/icons/location.png',
|
iconUrl: '/icons/marker/blue.png',
|
||||||
iconSize: [30, 30],
|
iconSize: [30, 30],
|
||||||
iconAnchor: [15, 15],
|
iconAnchor: [15, 15],
|
||||||
popupAnchor: [0, -15],
|
popupAnchor: [0, -15],
|
||||||
shadowSize: [30, 30],
|
shadowSize: [30, 30],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function LiveMap() {
|
export default function LiveMap({mapStyle, showZones, showNames, showArrows}) {
|
||||||
const location = useLocation(Infinity);
|
const location = useLocation(Infinity);
|
||||||
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
const [timeLeftNextZone, setTimeLeftNextZone] = useState(null);
|
||||||
const { zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
const { zoneExtremities, teams, nextZoneDate, getTeam, gameState } = useAdmin();
|
||||||
@@ -53,20 +54,17 @@ export default function LiveMap() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full'>
|
<div className='h-full w-full flex flex-col'>
|
||||||
{gameState == GameState.PLAYING && <p>{`Next zone in : ${formatTime(timeLeftNextZone)}`}</p>}
|
{gameState == GameState.PLAYING && <p>{`Next zone in : ${formatTime(timeLeftNextZone)}`}</p>}
|
||||||
<MapContainer className='h-full w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
<MapContainer className='flex-1 w-full' center={location} zoom={DEFAULT_ZOOM} scrollWheelZoom={true}>
|
||||||
<TileLayer
|
<TileLayer url={mapStyle.url} attribution={mapStyle.attribution}/>
|
||||||
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} />
|
<MapPan center={location} zoom={DEFAULT_ZOOM} />
|
||||||
{gameState == GameState.PLAYING && zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
{showZones && gameState == GameState.PLAYING && zoneExtremities.begin && <Polygon positions={zoneExtremities.begin.points} pathOptions={{ color: 'red', fillColor: 'red', fillOpacity: '0.1', weight: 3 }} />}
|
||||||
{gameState == GameState.PLAYING && zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
|
{showZones && gameState == GameState.PLAYING && zoneExtremities.end && <Polygon positions={zoneExtremities.end.points} pathOptions={{ color: 'green', fillColor: 'green', fillOpacity: '0.1', weight: 3 }} />}
|
||||||
{teams.map((team) => team.currentLocation && !team.captured &&
|
{teams.map((team) => team.currentLocation && !team.captured &&
|
||||||
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
<Marker key={team.id} position={team.currentLocation} icon={positionIcon}>
|
||||||
<Tooltip permanent direction="top" offset={[0, -5]} className="custom-tooltip">{team.name}</Tooltip>
|
{showNames && <Tooltip permanent direction="top" offset={[0.5, -15]} className="custom-tooltip">{team.name}</Tooltip>}
|
||||||
<Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>
|
{showArrows && <Arrow pos1={team.currentLocation} pos2={getTeam(team.chasing).currentLocation}/>}
|
||||||
</Marker>
|
</Marker>
|
||||||
)}
|
)}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function DotLine({ label, value }) {
|
|||||||
function IconValue({ color, icon, value }) {
|
function IconValue({ color, icon, value }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<img src={`/icons/${color}${icon}.png`} className="w-6 h-6" />
|
<img src={`/icons/${icon}/${color}.png`} className="w-6 h-6" />
|
||||||
<p className={`text-custom-${color}`}>{value}</p>
|
<p className={`text-custom-${color}`}>{value}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -95,7 +95,7 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative p-3 w-3/12 h-full flex flex-col gap-3">
|
<div className="w-full h-full relative flex flex-col p-3 gap-3">
|
||||||
<button className="absolute left-2 -top-1 text-black text-5xl" onClick={onClose} title="Fermer">x</button>
|
<button className="absolute left-2 -top-1 text-black text-5xl" onClick={onClose} title="Fermer">x</button>
|
||||||
<p className={`text-2xl font-bold text-center ${getStatus(team, gameState).color} font-bold`}>{getStatus(team, gameState).label}</p>
|
<p className={`text-2xl font-bold text-center ${getStatus(team, gameState).color} font-bold`}>{getStatus(team, gameState).label}</p>
|
||||||
<p className="text-4xl font-bold text-center">{team.name ?? NO_VALUE}</p>
|
<p className="text-4xl font-bold text-center">{team.name ?? NO_VALUE}</p>
|
||||||
@@ -103,7 +103,7 @@ export default function TeamSidePanel({ selectedTeamId, onClose }) {
|
|||||||
<img src={imgSrc ? imgSrc : "/images/missing_image.jpg"} alt="Photo de l'équipe"/>
|
<img src={imgSrc ? imgSrc : "/images/missing_image.jpg"} alt="Photo de l'équipe"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between items-center">
|
<div className="flex flex-row justify-between items-center">
|
||||||
<IconValue color={team.sockets.length > 0 ? "green" : "red"} icon="dude" value={team.sockets.length ?? NO_VALUE} />
|
<IconValue color={team.sockets.length > 0 ? "green" : "red"} icon="user" value={team.sockets.length ?? NO_VALUE} />
|
||||||
<IconValue color={team.battery >= 20 ? "green" : "red"} icon="battery" value={(team.battery ?? NO_VALUE) + "%"} />
|
<IconValue color={team.battery >= 20 ? "green" : "red"} icon="battery" value={(team.battery ?? NO_VALUE) + "%"} />
|
||||||
<IconValue
|
<IconValue
|
||||||
color={team.lastCurrentLocationDate != null && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "green" : "red"}
|
color={team.lastCurrentLocationDate != null && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "green" : "red"}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { List } from '@/components/list';
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
import { GameState } from '@/util/gameState';
|
import { GameState } from '@/util/gameState';
|
||||||
|
|
||||||
@@ -23,17 +24,17 @@ function getStatus(team, gamestate) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TeamListItem({ itemSelected, team }) {
|
function TeamViewerItem({ team, itemSelected, onSelected }) {
|
||||||
const { gameState } = useAdmin();
|
const { gameState } = useAdmin();
|
||||||
const status = getStatus(team, gameState);
|
const status = getStatus(team, gameState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'w-full p-2 bg-white flex flex-row gap-3 justify-between cursor-pointer' + (itemSelected ? " outline outline-4 outline-black" : "")}>
|
<div className={'w-full flex flex-row gap-3 p-2 bg-white justify-between cursor-pointer ' + (itemSelected ? "outline outline-4 outline-black" : "")} onClick={() => onSelected(team.id)}>
|
||||||
<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={team.sockets.length > 0 ? "/icons/greendude.png" : "/icons/reddude.png"} className="w-4 h-4" />
|
<img src={`/icons/user/${team.sockets.length > 0 ? "green" : "red"}.png`} className="w-4 h-4" />
|
||||||
<img src={team.battery > 20 ? "/icons/greenbattery.png" : "/icons/redbattery.png"} className="w-4 h-4" />
|
<img src={`/icons/battery/${team.battery >= 20 ? "green" : "red"}.png`} className="w-4 h-4" />
|
||||||
<img src={team.lastCurrentLocationDate != null && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "/icons/greenlocation.png" : "/icons/redlocation.png"} className="w-4 h-4" />
|
<img src={`/icons/location/${team.lastCurrentLocationDate != null && (Date.now() - team.lastCurrentLocationDate <= 30000) ? "green" : "red"}.png`} className="w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<p className={`text-xl font-bold`}>{team.name}</p>
|
<p className={`text-xl font-bold`}>{team.name}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,16 +45,14 @@ function TeamListItem({ itemSelected, team }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamList({selectedTeamId, onSelected}) {
|
export default function TeamViewer({selectedTeamId, onSelected}) {
|
||||||
const { teams } = useAdmin();
|
const { teams } = useAdmin();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className='h-full gap-1 flex flex-col'>
|
<List array={teams}>
|
||||||
{teams.map((team) => (
|
{(team) => (
|
||||||
<li key={team.id} onClick={() => onSelected(team.id)}>
|
<TeamViewerItem team={team} itemSelected={selectedTeamId === team.id} onSelected={onSelected}/>
|
||||||
<TeamListItem itemSelected={selectedTeamId === team.id} team={team} />
|
)}
|
||||||
</li>
|
</List>
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ export default function AdminLayout({ children }) {
|
|||||||
return (
|
return (
|
||||||
<AdminConnexionProvider>
|
<AdminConnexionProvider>
|
||||||
<AdminProvider>
|
<AdminProvider>
|
||||||
<div className="h-full overflow-y-scroll">
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
|
||||||
</AdminProvider>
|
</AdminProvider>
|
||||||
</AdminConnexionProvider>
|
</AdminConnexionProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,16 +4,34 @@ import dynamic from "next/dynamic";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Section } from "@/components/section";
|
import { Section } from "@/components/section";
|
||||||
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
||||||
|
import useAdmin from "@/hook/useAdmin";
|
||||||
|
import { GameState } from "@/util/gameState";
|
||||||
import TeamSidePanel from "./components/teamSidePanel";
|
import TeamSidePanel from "./components/teamSidePanel";
|
||||||
import TeamList from './components/teamViewer';
|
import TeamViewer from './components/teamViewer';
|
||||||
import { MapButton, ControlButton } from './components/buttons';
|
import { MapButton, ControlButton } from './components/buttons';
|
||||||
|
|
||||||
// Imported at runtime and not at compile time
|
// Imported at runtime and not at compile time
|
||||||
const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
|
const LiveMap = dynamic(() => import('./components/liveMap'), { ssr: false });
|
||||||
|
|
||||||
|
const mapStyles = {
|
||||||
|
default: {
|
||||||
|
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
},
|
||||||
|
satellite: {
|
||||||
|
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
||||||
|
attribution: 'Tiles © Esri'
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const { useProtect } = useAdminConnexion();
|
const { useProtect } = useAdminConnexion();
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState(null);
|
const [selectedTeamId, setSelectedTeamId] = useState(null);
|
||||||
|
const { changeState } = useAdmin();
|
||||||
|
const [mapStyle, setMapStyle] = useState(mapStyles.default);
|
||||||
|
const [showZones, setShowZones] = useState(true);
|
||||||
|
const [showNames, setShowNames] = useState(true);
|
||||||
|
const [showArrows, setShowArrows] = useState(false);
|
||||||
|
|
||||||
useProtect();
|
useProtect();
|
||||||
|
|
||||||
@@ -25,6 +43,22 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchMapStyle() {
|
||||||
|
setMapStyle(mapStyle == mapStyles.default ? mapStyles.satellite : mapStyles.default);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchZones() {
|
||||||
|
setShowZones(!showZones);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchNames() {
|
||||||
|
setShowNames(!showNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchArrows() {
|
||||||
|
setShowArrows(!showArrows);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full bg-gray-200 p-3 flex flex-row content-start gap-3'>
|
<div className='h-full bg-gray-200 p-3 flex flex-row content-start gap-3'>
|
||||||
<div className="h-full w-2/6 flex flex-col gap-3">
|
<div className="h-full w-2/6 flex flex-col gap-3">
|
||||||
@@ -32,36 +66,38 @@ export default function AdminPage() {
|
|||||||
<img src="/icons/home.png" className="w-8 h-8" />
|
<img src="/icons/home.png" className="w-8 h-8" />
|
||||||
<h2 className="text-3xl font-bold">Page principale</h2>
|
<h2 className="text-3xl font-bold">Page principale</h2>
|
||||||
</div>
|
</div>
|
||||||
<Section title="Contrôle">
|
<Section title="Contrôle" innerClassName='flex flex-row justify-between'>
|
||||||
<div className='w-full h-full flex flex-row justify-between'>
|
|
||||||
<Link href="/admin/parameters">
|
<Link href="/admin/parameters">
|
||||||
<ControlButton icon="parameters" title="Accéder aux paramètres du jeu"/>
|
<ControlButton icon="parameters" title="Accéder aux paramètres du jeu"/>
|
||||||
</Link>
|
</Link>
|
||||||
<ControlButton icon="play" title="Reprendre la partie"/>
|
{false && <ControlButton icon="play" title="Reprendre la partie" onClick={() => {}} />}
|
||||||
<ControlButton icon="reset" title="Réinitialiser la partie"/>
|
<ControlButton icon="reset" title="Réinitialiser la partie" onClick={() => changeState(GameState.SETUP)} />
|
||||||
<ControlButton icon="placement" title="Commencer les placements"/>
|
<ControlButton icon="placement" title="Commencer les placements" onClick={() => changeState(GameState.PLACEMENT)} />
|
||||||
<ControlButton icon="begin" title="Lancer la traque"/>
|
<ControlButton icon="begin" title="Lancer la traque" onClick={() => changeState(GameState.PLAYING)} />
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
<Section className="h-full" title="Équipes">
|
<Section title="Équipes" outerClassName="flex-1 min-h-0">
|
||||||
<div className="w-full h-full bg-gray-300 p-1">
|
<TeamViewer selectedTeamId={selectedTeamId} onSelected={onSelected}/>
|
||||||
<TeamList selectedTeamId={selectedTeamId} onSelected={onSelected}/>
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
<div className='grow flex-1 flex flex-col bg-white p-3 gap-3 shadow-2xl'>
|
<div className='grow flex-1 flex flex-col bg-white p-3 gap-3 shadow-2xl'>
|
||||||
<div className="flex-1 flex flex-row gap-3">
|
<div className="flex-1 flex flex-row gap-3">
|
||||||
<LiveMap/>
|
<div className="flex-1 h-full">
|
||||||
|
<LiveMap mapStyle={mapStyle} showZones={showZones} showNames={showNames} showArrows={showArrows}/>
|
||||||
|
</div>
|
||||||
|
{selectedTeamId &&
|
||||||
|
<div className="w-3/12 h-full">
|
||||||
<TeamSidePanel selectedTeamId={selectedTeamId} onClose={() => setSelectedTeamId(null)}/>
|
<TeamSidePanel selectedTeamId={selectedTeamId} onClose={() => setSelectedTeamId(null)}/>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div className='w-full flex flex-row items-center justify-evenly py-2'>
|
<div className='w-full flex flex-row items-center justify-evenly py-2'>
|
||||||
<MapButton icon="mapstyle" title="Changer le style de la carte"/>
|
<MapButton icon="mapstyle" title="Changer le style de la carte" onClick={switchMapStyle}/>
|
||||||
<MapButton icon="zones" title="Afficher/masquer les zones"/>
|
<MapButton icon="zones" title="Afficher/masquer les zones" onClick={switchZones}/>
|
||||||
<MapButton icon="names" title="Afficher/masquer les noms des équipes"/>
|
<MapButton icon="names" title="Afficher/masquer les noms des équipes" onClick={switchNames}/>
|
||||||
<MapButton icon="arrows" title="Afficher/masquer les relations de traque"/>
|
<MapButton icon="arrows" title="Afficher/masquer les relations de traque" onClick={switchArrows}/>
|
||||||
<MapButton icon="incertitude" title="Afficher/masquer les incertitudes de position"/>
|
{false && <MapButton icon="incertitude" title="Afficher/masquer les incertitudes de position"/>}
|
||||||
<MapButton icon="path" title="Afficher/masquer la trace de l'équipe sélectionnée"/>
|
{false && <MapButton icon="path" title="Afficher/masquer la trace de l'équipe sélectionnée"/>}
|
||||||
<MapButton icon="informations" title="Afficher/masquer les évènements de l'équipe sélectionnée"/>
|
{false && <MapButton icon="informations" title="Afficher/masquer les évènements de l'équipe sélectionnée"/>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react'
|
|
||||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
||||||
|
import { List } from '@/components/list';
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
|
|
||||||
function reorder(list, startIndex, endIndex) {
|
function reorder(list, startIndex, endIndex) {
|
||||||
@@ -9,23 +9,23 @@ function reorder(list, startIndex, endIndex) {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
function TeamListItem({ team, index }) {
|
function TeamManagerItem({ team, index }) {
|
||||||
const { removeTeam } = useAdmin();
|
const { updateTeam, removeTeam } = useAdmin();
|
||||||
|
|
||||||
function handleRemove() {
|
function handleRemove() {
|
||||||
removeTeam(team.id);
|
removeTeam(team.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={team.id.toString()} index={index} onClick={() => onSelected(team.id)}>
|
<Draggable draggableId={team.id.toString()} index={index}>
|
||||||
{provided => (
|
{provided => (
|
||||||
<div className='w-full p-2 bg-white flex flex-row items-center text-xl gap-3 font-bold' {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
<div className='w-full p-2 bg-white flex flex-row items-center text-xl gap-3 font-bold' {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
|
||||||
<div className='flex-1 w-full h-full flex flex-row items-center justify-between'>
|
<div className='flex-1 w-full h-full flex flex-row items-center justify-between'>
|
||||||
<p>{team.name}</p>
|
<p>{team.name}</p>
|
||||||
<div className='flex flex-row items-center justify-between gap-3'>
|
<div className='flex flex-row items-center justify-between gap-3'>
|
||||||
<p>{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}</p>
|
<p>{String(team.id).padStart(6, '0').replace(/(\d{3})(\d{3})/, "$1 $2")}</p>
|
||||||
<img src="/icons/home.png" className="w-8 h-8" />
|
<img src={`/icons/heart/${team.captured ? "grey" : "pink"}.png`} className="w-8 h-8" onClick={() => updateTeam(team.id, { captured: !team.captured })} />
|
||||||
<img src="/icons/home.png" className="w-8 h-8" onClick={handleRemove} />
|
<img src="/icons/trash.png" className="w-8 h-8" onClick={handleRemove} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,17 +34,8 @@ function TeamListItem({ team, index }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamList() {
|
export default function TeamManager() {
|
||||||
const { teams, reorderTeams, addTeam } = useAdmin();
|
const { teams, reorderTeams } = useAdmin();
|
||||||
const [teamName, setTeamName] = useState('');
|
|
||||||
|
|
||||||
function handleSubmit(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
if (teamName !== "") {
|
|
||||||
addTeam(teamName);
|
|
||||||
setTeamName("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDragEnd(result) {
|
function onDragEnd(result) {
|
||||||
if (!result.destination) return;
|
if (!result.destination) return;
|
||||||
@@ -54,29 +45,19 @@ export default function TeamList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full h-full flex flex-col gap-3'>
|
|
||||||
<form className='w-full flex flex-row gap-3' onSubmit={handleSubmit}>
|
|
||||||
<div className='w-full'>
|
|
||||||
<input name="teamName" label='Team name' value={teamName} onChange={(e) => setTeamName(e.target.value)} type="text" className="w-full h-full p-4 ring-1 ring-inset ring-gray-300" />
|
|
||||||
</div>
|
|
||||||
<div className='w-1/5'>
|
|
||||||
<button type="submit" className="w-full h-full bg-custom-light-blue hover:bg-blue-500 transition text-3xl font-bold">+</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<DragDropContext onDragEnd={onDragEnd} >
|
<DragDropContext onDragEnd={onDragEnd} >
|
||||||
<Droppable droppableId='team-list'>
|
<Droppable droppableId='team-list'>
|
||||||
{provided => (
|
{provided => (
|
||||||
<ul className='w-full h-full gap-1 flex flex-col bg-gray-300 p-1' ref={provided.innerRef} {...provided.droppableProps}>
|
<div className='w-full h-full' ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
{teams.map((team, i) => (
|
<List array={teams}>
|
||||||
<li key={team.id}>
|
{(team, i) => (
|
||||||
<TeamListItem index={i} team={team} />
|
<TeamManagerItem index={i} team={team}/>
|
||||||
</li>
|
)}
|
||||||
))}
|
</List>
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</ul>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ import { Section } from "@/components/section";
|
|||||||
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
import { useAdminConnexion } from "@/context/adminConnexionContext";
|
||||||
import useAdmin from '@/hook/useAdmin';
|
import useAdmin from '@/hook/useAdmin';
|
||||||
import Messages from "./components/messages";
|
import Messages from "./components/messages";
|
||||||
import TeamList from './components/teamManager';
|
import TeamManager from './components/teamManager';
|
||||||
|
|
||||||
// Imported at runtime and not at compile time
|
// Imported at runtime and not at compile time
|
||||||
const ZoneSelector = dynamic(() => import('./components/polygonZoneMap'), { ssr: false });
|
const ZoneSelector = dynamic(() => import('./components/polygonZoneMap'), { ssr: false });
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const {penaltySettings, changePenaltySettings} = useAdmin();
|
const {penaltySettings, changePenaltySettings, addTeam} = useAdmin();
|
||||||
const { useProtect } = useAdminConnexion();
|
const { useProtect } = useAdminConnexion();
|
||||||
const [allowedTimeBetweenUpdates, setAllowedTimeBetweenUpdates] = useState("");
|
const [allowedTimeBetweenUpdates, setAllowedTimeBetweenUpdates] = useState("");
|
||||||
|
const [teamName, setTeamName] = useState('');
|
||||||
|
|
||||||
useProtect();
|
useProtect();
|
||||||
|
|
||||||
@@ -31,9 +32,17 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (teamName !== "") {
|
||||||
|
addTeam(teamName);
|
||||||
|
setTeamName("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full bg-gray-200 p-3 flex flex-row gap-3'>
|
<div className='h-full bg-gray-200 p-3 flex flex-row gap-3'>
|
||||||
<div className="h-full w-3/6 gap-3 flex flex-col">
|
<div className="h-full w-2/6 gap-3 flex flex-col">
|
||||||
<div className='w-full bg-custom-light-blue gap-5 p-5 flex flex-row shadow-2xl'>
|
<div className='w-full bg-custom-light-blue gap-5 p-5 flex flex-row shadow-2xl'>
|
||||||
<Link href="/admin">
|
<Link href="/admin">
|
||||||
<img src="/icons/backarrow.png" className="w-8 h-8" title="Main page" />
|
<img src="/icons/backarrow.png" className="w-8 h-8" title="Main page" />
|
||||||
@@ -41,19 +50,27 @@ export default function AdminPage() {
|
|||||||
<h2 className="text-3xl font-bold">Paramètres</h2>
|
<h2 className="text-3xl font-bold">Paramètres</h2>
|
||||||
</div>
|
</div>
|
||||||
<Messages/>
|
<Messages/>
|
||||||
<Section className="h-full" title="Équipe">
|
<Section title="Équipe" outerClassName="flex-1 min-h-0" innerClassName="flex flex-col items-center gap-3">
|
||||||
<div className="w-full h-full gap-3 flex flex-col items-center">
|
<form className='w-full flex flex-row gap-3' onSubmit={handleSubmit}>
|
||||||
<TeamList/>
|
<div className='w-full'>
|
||||||
|
<input name="teamName" label='Team name' value={teamName} onChange={(e) => setTeamName(e.target.value)} type="text" className="w-full h-full p-4 ring-1 ring-inset ring-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className='w-1/5'>
|
||||||
|
<button type="submit" className="w-full h-full bg-custom-light-blue hover:bg-blue-500 transition text-3xl font-bold">+</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="w-full flex-1 min-h-0 ">
|
||||||
|
<TeamManager/>
|
||||||
|
</div>
|
||||||
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
<div className="w-full flex flex-row gap-2 items-center justify-between">
|
||||||
<p>Interval between position updates</p>
|
<p>Interval between position updates</p>
|
||||||
<div className="w-16 h-10">
|
<div className="w-16 h-10">
|
||||||
<TextInput value={allowedTimeBetweenUpdates} onChange={(e) => setAllowedTimeBetweenUpdates(e.target.value)} onBlur={applySettings} />
|
<TextInput value={allowedTimeBetweenUpdates} onChange={(e) => setAllowedTimeBetweenUpdates(e.target.value)} onBlur={applySettings} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full w-full">
|
<div className="h-full flex-1">
|
||||||
<ZoneSelector />
|
<ZoneSelector />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
14
traque-front/components/list.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export function List({array, children}) {
|
||||||
|
// The elements of array have to be identified by a field id
|
||||||
|
return (
|
||||||
|
<div className='w-full h-full bg-gray-300 overflow-y-auto'>
|
||||||
|
<ul className="w-full p-1 divide-y-4 divide-gray-300">
|
||||||
|
{array.map((elem, i) => (
|
||||||
|
<li className="w-full" key={elem.id}>
|
||||||
|
{children(elem, i)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
export function Section({title, className, children}) {
|
export function Section({title, outerClassName, innerClassName, children}) {
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={outerClassName}>
|
||||||
<div className='w-full h-full flex flex-col shadow-2xl'>
|
<div className='w-full h-full flex flex-col shadow-2xl'>
|
||||||
<div className='w-full p-1 bg-custom-light-blue text-center'>
|
<div className='w-full p-1 bg-custom-light-blue text-center'>
|
||||||
<h2 className="text-l">{title}</h2>
|
<h2 className="text-l">{title}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full h-full p-3 bg-white'>
|
<div className='w-full flex-1 min-h-0 p-3 bg-white'>
|
||||||
|
<div className={`w-full h-full ${innerClassName}`}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
traque-front/public/icons/battery/black.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
traque-front/public/icons/battery/red.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 10 KiB |
BIN
traque-front/public/icons/heart/grey.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
traque-front/public/icons/heart/heart.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
traque-front/public/icons/heart/pink.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
traque-front/public/icons/location/black.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
traque-front/public/icons/location/red.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
traque-front/public/icons/marker/grey.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB |
BIN
traque-front/public/icons/trash.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
BIN
traque-front/public/images/logo_traque.png
Normal file
|
After Width: | Height: | Size: 118 KiB |