Scrollable team list

This commit is contained in:
Sebastien Riviere
2025-08-30 16:19:30 +02:00
parent cb534ed1aa
commit e5d90d824e
39 changed files with 163 additions and 119 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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

View File

@@ -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,

View File

@@ -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>
); );

View File

@@ -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='&copy; <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>

View File

@@ -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"}

View File

@@ -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>
); );
} }

View File

@@ -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>
); );

View File

@@ -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: '&copy; <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 &copy; 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> {false && <ControlButton icon="play" title="Reprendre la partie" onClick={() => {}} />}
<ControlButton icon="play" title="Reprendre la partie"/> <ControlButton icon="reset" title="Réinitialiser la partie" onClick={() => changeState(GameState.SETUP)} />
<ControlButton icon="reset" title="Réinitialiser la partie"/> <ControlButton icon="placement" title="Commencer les placements" onClick={() => changeState(GameState.PLACEMENT)} />
<ControlButton icon="placement" title="Commencer les placements"/> <ControlButton icon="begin" title="Lancer la traque" onClick={() => changeState(GameState.PLAYING)} />
<ControlButton icon="begin" title="Lancer la traque"/>
</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">
<TeamSidePanel selectedTeamId={selectedTeamId} onClose={() => setSelectedTeamId(null)}/> <LiveMap mapStyle={mapStyle} showZones={showZones} showNames={showNames} showArrows={showArrows}/>
</div>
{selectedTeamId &&
<div className="w-3/12 h-full">
<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>

View File

@@ -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'> <DragDropContext onDragEnd={onDragEnd} >
<form className='w-full flex flex-row gap-3' onSubmit={handleSubmit}> <Droppable droppableId='team-list'>
<div className='w-full'> {provided => (
<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 className='w-full h-full' ref={provided.innerRef} {...provided.droppableProps}>
</div> <List array={teams}>
<div className='w-1/5'> {(team, i) => (
<button type="submit" className="w-full h-full bg-custom-light-blue hover:bg-blue-500 transition text-3xl font-bold">+</button> <TeamManagerItem index={i} team={team}/>
</div> )}
</form> </List>
<DragDropContext onDragEnd={onDragEnd} > {provided.placeholder}
<Droppable droppableId='team-list'> </div>
{provided => ( )}
<ul className='w-full h-full gap-1 flex flex-col bg-gray-300 p-1' ref={provided.innerRef} {...provided.droppableProps}> </Droppable>
{teams.map((team, i) => ( </DragDropContext>
<li key={team.id}>
<TeamListItem index={i} team={team} />
</li>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</div>
); );
} }

View File

@@ -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'>
<div className="w-full flex flex-row gap-2 items-center justify-between"> <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" />
<p>Interval between position updates</p> </div>
<div className="w-16 h-10"> <div className='w-1/5'>
<TextInput value={allowedTimeBetweenUpdates} onChange={(e) => setAllowedTimeBetweenUpdates(e.target.value)} onBlur={applySettings} /> <button type="submit" className="w-full h-full bg-custom-light-blue hover:bg-blue-500 transition text-3xl font-bold">+</button>
</div> </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">
<p>Interval between position updates</p>
<div className="w-16 h-10">
<TextInput value={allowedTimeBetweenUpdates} onChange={(e) => setAllowedTimeBetweenUpdates(e.target.value)} onBlur={applySettings} />
</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>

View 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>
);
}

View File

@@ -1,12 +1,14 @@
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'>
{children} <div className={`w-full h-full ${innerClassName}`}>
{children}
</div>
</div> </div>
</div> </div>
</div> </div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB