summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorArne Rief <riearn@proton.me>2025-12-22 21:20:39 +0100
committerArne Rief <riearn@proton.me>2025-12-22 21:20:39 +0100
commite836e7dd4ed5e9fa60e949d159100040b22a8f48 (patch)
treea11954c06e55e8ef53fcb634fa5954dfcb42ffc3 /frontend/src/components
parentd1b64ddd78d8b8dc3eca76038a75071ab2a575d9 (diff)
Movement simulator for all and single robot, project v1 ready
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/AddRobotForm.tsx11
-rw-r--r--frontend/src/components/CityMap.tsx6
-rw-r--r--frontend/src/components/RobotList.tsx98
-rw-r--r--frontend/src/components/Sidebar.tsx26
-rw-r--r--frontend/src/components/SimulationActions.tsx35
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>