From e836e7dd4ed5e9fa60e949d159100040b22a8f48 Mon Sep 17 00:00:00 2001 From: Arne Rief Date: Mon, 22 Dec 2025 21:20:39 +0100 Subject: Movement simulator for all and single robot, project v1 ready --- frontend/src/components/AddRobotForm.tsx | 11 ++- frontend/src/components/CityMap.tsx | 6 +- frontend/src/components/RobotList.tsx | 98 +++++++++++++++++++++++++-- frontend/src/components/Sidebar.tsx | 26 ++++++- frontend/src/components/SimulationActions.tsx | 35 ++++++---- 5 files changed, 150 insertions(+), 26 deletions(-) (limited to 'frontend/src/components') 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>; setIsAddingRobot: Dispatch>; + setRobots: Dispatch>; }; function AddRobotForm({ apiUrl, errorMessage, + robots, token, setErrorMessage, setIsAddingRobot, + setRobots, }: Props) { const [isSubmitting, setIsSubmitting] = useState(false); const [newRobotName, setNewRobotName] = useState(""); @@ -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; -type Props = { robots: Robot[]; }; +type Props = { + apiUrl: string; + robots: Robot[]; + token: string | null; + setErrorMessage: Dispatch>; +}; -function RobotList({ robots }: Props) { +function RobotList({ apiUrl, robots, token, setErrorMessage }: Props) { const [expandedRobots, setExpandedRobots] = useState( {} ); @@ -17,15 +28,89 @@ function RobotList({ robots }: Props) { })); } + // Move or stop individual robot + async function controlSingleRobot( + event: MouseEvent, + 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 (
    - {robots.map((robot) => { + {robots?.map((robot) => { const isExpanded = expandedRobots[robot?.id]; return (
  • {robot?.name}

    + {/* Move/stop individual robot */} + + + {/* Movement status */}

    Status:{" "} @@ -33,12 +118,14 @@ function RobotList({ robots }: Props) {

    + {/* Current position */}

    Position:

    • Lat: {robot?.lat}
    • Lon: {robot?.lon}
    + {/* Expand position log */} + {/* Position log/history */}
    >; setErrorMessage: Dispatch>; + setRobots: Dispatch>; }; -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) {
    - {isAddingRobot && ( + {isAddingRobot && ( )} @@ -50,12 +63,19 @@ function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) { )} - + ); } 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>; setErrorMessage: Dispatch>; }; -function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { - const [isSimulationActive, setIsSimulationActive] = - useState(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) { @@ -91,7 +98,7 @@ function SimulationActions({ apiUrl, token, setErrorMessage }: Props) { -- cgit v1.2.3