diff options
| author | Arne Rief <riearn@proton.me> | 2025-12-22 21:20:39 +0100 |
|---|---|---|
| committer | Arne Rief <riearn@proton.me> | 2025-12-22 21:20:39 +0100 |
| commit | e836e7dd4ed5e9fa60e949d159100040b22a8f48 (patch) | |
| tree | a11954c06e55e8ef53fcb634fa5954dfcb42ffc3 /frontend/src/components | |
| parent | d1b64ddd78d8b8dc3eca76038a75071ab2a575d9 (diff) | |
Movement simulator for all and single robot, project v1 ready
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/AddRobotForm.tsx | 11 | ||||
| -rw-r--r-- | frontend/src/components/CityMap.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/components/RobotList.tsx | 98 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 26 | ||||
| -rw-r--r-- | frontend/src/components/SimulationActions.tsx | 35 |
5 files changed, 150 insertions, 26 deletions
diff --git a/frontend/src/components/AddRobotForm.tsx b/frontend/src/components/AddRobotForm.tsx index c178657..0d2a00f 100644 --- a/frontend/src/components/AddRobotForm.tsx +++ b/frontend/src/components/AddRobotForm.tsx @@ -5,22 +5,26 @@ import { type SetStateAction, } from "react"; import type { ErrorResponse } from "../types/error"; -import type { CreateRobotResponse } from "../types/robot"; +import type { CreateRobotResponse, Robot } from "../types/robot"; type Props = { apiUrl: string; errorMessage: string; + robots: Robot[]; token: string | null; setErrorMessage: Dispatch<SetStateAction<string>>; setIsAddingRobot: Dispatch<SetStateAction<boolean>>; + setRobots: Dispatch<SetStateAction<Robot[]>>; }; function AddRobotForm({ apiUrl, errorMessage, + robots, token, setErrorMessage, setIsAddingRobot, + setRobots, }: Props) { const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [newRobotName, setNewRobotName] = useState<string>(""); @@ -69,7 +73,10 @@ function AddRobotForm({ } const data: CreateRobotResponse = await response.json(); - console.log("Robot created successfully: ", data); + console.log(data.message, data.robot); + + const newRobotList = [...robots, data.robot]; + setRobots(newRobotList); // Reset form and close input field setNewRobotName(""); diff --git a/frontend/src/components/CityMap.tsx b/frontend/src/components/CityMap.tsx index b61e69a..795598e 100644 --- a/frontend/src/components/CityMap.tsx +++ b/frontend/src/components/CityMap.tsx @@ -65,9 +65,11 @@ function CityMap({ robots }: Props) { source.clear(); - robots.forEach((robot) => { + robots?.forEach((robot) => { const feature = new Feature({ - geometry: new Point(fromLonLat([robot?.lon, robot?.lat])), + geometry: new Point( + fromLonLat([parseFloat(robot?.lon), parseFloat(robot?.lat)]) + ), robotId: robot?.id, }); diff --git a/frontend/src/components/RobotList.tsx b/frontend/src/components/RobotList.tsx index d711372..21d8e63 100644 --- a/frontend/src/components/RobotList.tsx +++ b/frontend/src/components/RobotList.tsx @@ -1,11 +1,22 @@ -import { useState } from "react"; -import type { Robot } from "../types/robot"; +import { + useState, + type Dispatch, + type MouseEvent, + type SetStateAction, +} from "react"; +import type { ErrorResponse } from "../types/error"; +import type { Robot, SimulationResponse } from "../types/robot"; type ExpandedRobotsState = Record<number, boolean>; -type Props = { robots: Robot[]; }; +type Props = { + apiUrl: string; + robots: Robot[]; + token: string | null; + setErrorMessage: Dispatch<SetStateAction<string>>; +}; -function RobotList({ robots }: Props) { +function RobotList({ apiUrl, robots, token, setErrorMessage }: Props) { const [expandedRobots, setExpandedRobots] = useState<ExpandedRobotsState>( {} ); @@ -17,15 +28,89 @@ function RobotList({ robots }: Props) { })); } + // Move or stop individual robot + async function controlSingleRobot( + event: MouseEvent<HTMLButtonElement>, + robotId: number, + robotStatus: Robot["status"] + ) { + const isRobotMoving = robotStatus === "moving"; + const button = event.currentTarget; + button.disabled = true; // prevent spamming + + try { + // Make button clickable again after 1 second + setTimeout(() => { + button.disabled = false; + }, 1000); + + const response = await fetch( + `${apiUrl}/robots/${robotId}/${ + isRobotMoving ? "stop" : "move" + }`, + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + const errorData: ErrorResponse = await response.json(); + throw new Error( + errorData.message || + `Failed to ${ + isRobotMoving ? "stop" : "start" + } robot ID ${robotId}.` + ); + } + + const data: SimulationResponse = await response.json(); + console.log(data.message); + } catch (error) { + console.error( + `Error ${ + isRobotMoving ? "stopping" : "starting" + } robot ID ${robotId}: `, + error + ); + + if (error instanceof Error) { + setErrorMessage(error.message); + } else { + setErrorMessage("An unexpected error occurred."); + } + } + } + return ( <ul className="sidebar-robot-list"> - {robots.map((robot) => { + {robots?.map((robot) => { const isExpanded = expandedRobots[robot?.id]; return ( <li key={robot?.id}> <p className="robot-name">{robot?.name}</p> + {/* Move/stop individual robot */} + <button + className={`btn btn-single-robot btn-${ + robot?.status === "idle" ? "start" : "stop" + }`} + onClick={(event) => + controlSingleRobot( + event, + robot?.id, + robot?.status + ) + } + > + {robot?.status === "idle" ? "MOVE" : "STOP"} + </button> + + {/* Movement status */} <p> Status:{" "} <span className={`robot-status-${robot?.status}`}> @@ -33,12 +118,14 @@ function RobotList({ robots }: Props) { </span> </p> + {/* Current position */} <p className="robot-coordinates-label">Position:</p> <ul className="robot-coordinates"> <li>Lat: {robot?.lat}</li> <li>Lon: {robot?.lon}</li> </ul> + {/* Expand position log */} <button className="btn btn-robot-history-toggle" onClick={() => toggleRobotHistory(robot?.id)} @@ -52,6 +139,7 @@ function RobotList({ robots }: Props) { </span> </button> + {/* Position log/history */} <div className={`robot-history ${ isExpanded ? "expanded" : "" diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 8bd795b..3760d4e 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -8,13 +8,24 @@ import RobotList from "./RobotList"; import SimulationActions from "./SimulationActions"; type Props = { + activeSimulation: boolean; errorMessage: string; robots: Robot[]; token: string | null; + setActiveSimulation: Dispatch<SetStateAction<boolean>>; setErrorMessage: Dispatch<SetStateAction<string>>; + setRobots: Dispatch<SetStateAction<Robot[]>>; }; -function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) { +function Sidebar({ + activeSimulation, + errorMessage, + robots, + token, + setActiveSimulation, + setErrorMessage, + setRobots, +}: Props) { const [isAddingRobot, setIsAddingRobot] = useState(false); function handleAddClick() { @@ -35,13 +46,15 @@ function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) { </button> </div> - {isAddingRobot && ( + {isAddingRobot && ( <AddRobotForm apiUrl={API_URL} errorMessage={errorMessage} + robots={robots} token={token} setErrorMessage={setErrorMessage} setIsAddingRobot={setIsAddingRobot} + setRobots={setRobots} /> )} @@ -50,12 +63,19 @@ function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) { )} <SimulationActions + activeSimulation={activeSimulation} apiUrl={API_URL} token={token} + setActiveSimulation={setActiveSimulation} setErrorMessage={setErrorMessage} /> - <RobotList robots={robots} /> + <RobotList + apiUrl={API_URL} + robots={robots} + token={token} + setErrorMessage={setErrorMessage} + /> </div> ); } diff --git a/frontend/src/components/SimulationActions.tsx b/frontend/src/components/SimulationActions.tsx index c91b933..0bfca3f 100644 --- a/frontend/src/components/SimulationActions.tsx +++ b/frontend/src/components/SimulationActions.tsx @@ -1,19 +1,24 @@ -import { useState, type Dispatch, type SetStateAction } from "react"; +import { type Dispatch, type SetStateAction } from "react"; import type { ErrorResponse } from "../types/error"; +import type { SimulationResponse } from "../types/robot"; type Props = { + activeSimulation: boolean; apiUrl: string; token: string | null; + setActiveSimulation: Dispatch<SetStateAction<boolean>>; setErrorMessage: Dispatch<SetStateAction<string>>; }; -function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { - const [isSimulationActive, setIsSimulationActive] = - useState<boolean>(false); - - // TODO type responses +function SimulationActions({ + activeSimulation, + apiUrl, + token, + setActiveSimulation, + setErrorMessage, +}: Props) { async function handleStartAllRobots() { - setIsSimulationActive(true); + setActiveSimulation(true); try { const response = await fetch(`${apiUrl}/robots/move`, { @@ -31,7 +36,8 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { ); } - console.log("All robots set moving."); + const data: SimulationResponse = await response.json(); + console.log(data.message); } catch (error) { console.error("Error starting robots:", error); @@ -41,12 +47,12 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { setErrorMessage("An unexpected error occurred."); } - setIsSimulationActive(false); + setActiveSimulation(false); } } async function handleStopAllRobots() { - setIsSimulationActive(false); + setActiveSimulation(false); try { const response = await fetch(`${apiUrl}/robots/stop`, { @@ -64,7 +70,8 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { ); } - console.log("All robots set idle."); + const data: SimulationResponse = await response.json(); + console.log(data.message); } catch (error) { console.error("Error stopping robots:", error); @@ -74,7 +81,7 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { setErrorMessage("An unexpected error occurred."); } - setIsSimulationActive(true); + setActiveSimulation(true); } } @@ -83,7 +90,7 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { <button className="btn btn-start" onClick={handleStartAllRobots} - disabled={isSimulationActive} + disabled={activeSimulation} > Start simulation </button> @@ -91,7 +98,7 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { <button className="btn btn-stop" onClick={handleStopAllRobots} - disabled={!isSimulationActive} + disabled={!activeSimulation} > Stop simulation </button> |
