summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorArne Rief <riearn@proton.me>2025-12-22 12:56:20 +0100
committerArne Rief <riearn@proton.me>2025-12-22 13:02:01 +0100
commitd1b64ddd78d8b8dc3eca76038a75071ab2a575d9 (patch)
tree6213650195cdddb0ec4938380735ebd70847a758 /frontend/src/components
parent3818739c5901cc3f1d4596b24cfe1b827a2eca23 (diff)
Sidebar split into smaller components
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/AddRobotForm.tsx121
-rw-r--r--frontend/src/components/RobotList.tsx84
-rw-r--r--frontend/src/components/Sidebar.tsx234
-rw-r--r--frontend/src/components/SimulationActions.tsx103
4 files changed, 334 insertions, 208 deletions
diff --git a/frontend/src/components/AddRobotForm.tsx b/frontend/src/components/AddRobotForm.tsx
new file mode 100644
index 0000000..c178657
--- /dev/null
+++ b/frontend/src/components/AddRobotForm.tsx
@@ -0,0 +1,121 @@
+import {
+ useState,
+ type ChangeEvent,
+ type Dispatch,
+ type SetStateAction,
+} from "react";
+import type { ErrorResponse } from "../types/error";
+import type { CreateRobotResponse } from "../types/robot";
+
+type Props = {
+ apiUrl: string;
+ errorMessage: string;
+ token: string | null;
+ setErrorMessage: Dispatch<SetStateAction<string>>;
+ setIsAddingRobot: Dispatch<SetStateAction<boolean>>;
+};
+
+function AddRobotForm({
+ apiUrl,
+ errorMessage,
+ token,
+ setErrorMessage,
+ setIsAddingRobot,
+}: Props) {
+ const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+ const [newRobotName, setNewRobotName] = useState<string>("");
+
+ // Cancel robot creation
+ function handleCancel() {
+ setIsAddingRobot(false);
+ setNewRobotName("");
+ setErrorMessage("");
+ }
+
+ function handleInputChange(event: ChangeEvent<HTMLInputElement>) {
+ if (errorMessage) {
+ setErrorMessage("");
+ }
+
+ setNewRobotName(event.target.value);
+ }
+
+ // Create new robot
+ async function handleSubmit() {
+ if (!newRobotName.trim()) {
+ setErrorMessage("Please enter a name for the robot.");
+ return;
+ }
+
+ setIsSubmitting(true);
+ setErrorMessage("");
+
+ try {
+ const response = await fetch(`${apiUrl}/robots`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ name: newRobotName.trim() }),
+ });
+
+ if (!response.ok) {
+ const errorData: ErrorResponse = await response.json();
+ throw new Error(
+ errorData.message ||
+ `Error creating the new robot: ${response.status}`
+ );
+ }
+
+ const data: CreateRobotResponse = await response.json();
+ console.log("Robot created successfully: ", data);
+
+ // Reset form and close input field
+ setNewRobotName("");
+ setIsAddingRobot(false);
+ } catch (error) {
+ console.error("Error creating the new robot:", error);
+
+ if (error instanceof Error) {
+ setErrorMessage(error.message);
+ } else {
+ setErrorMessage("Error creating the new robot.");
+ }
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+ <div className="add-robot-form">
+ <input
+ type="text"
+ placeholder="Robot name..."
+ value={newRobotName}
+ onChange={handleInputChange}
+ disabled={isSubmitting}
+ autoFocus
+ />
+ <div className="add-robot-actions">
+ <button
+ className="btn btn-start"
+ onClick={handleSubmit}
+ disabled={isSubmitting}
+ >
+ {isSubmitting ? "Creating..." : "Create"}
+ </button>
+ <button
+ className="btn btn-stop"
+ onClick={handleCancel}
+ disabled={isSubmitting}
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ );
+}
+
+export default AddRobotForm;
+
diff --git a/frontend/src/components/RobotList.tsx b/frontend/src/components/RobotList.tsx
new file mode 100644
index 0000000..d711372
--- /dev/null
+++ b/frontend/src/components/RobotList.tsx
@@ -0,0 +1,84 @@
+import { useState } from "react";
+import type { Robot } from "../types/robot";
+
+type ExpandedRobotsState = Record<number, boolean>;
+
+type Props = { robots: Robot[]; };
+
+function RobotList({ robots }: Props) {
+ const [expandedRobots, setExpandedRobots] = useState<ExpandedRobotsState>(
+ {}
+ );
+
+ function toggleRobotHistory(robotId: number) {
+ setExpandedRobots((prev) => ({
+ ...prev,
+ [robotId]: !prev[robotId],
+ }));
+ }
+
+ return (
+ <ul className="sidebar-robot-list">
+ {robots.map((robot) => {
+ const isExpanded = expandedRobots[robot?.id];
+
+ return (
+ <li key={robot?.id}>
+ <p className="robot-name">{robot?.name}</p>
+
+ <p>
+ Status:{" "}
+ <span className={`robot-status-${robot?.status}`}>
+ {robot?.status}
+ </span>
+ </p>
+
+ <p className="robot-coordinates-label">Position:</p>
+ <ul className="robot-coordinates">
+ <li>Lat: {robot?.lat}</li>
+ <li>Lon: {robot?.lon}</li>
+ </ul>
+
+ <button
+ className="btn btn-robot-history-toggle"
+ onClick={() => toggleRobotHistory(robot?.id)}
+ aria-expanded={isExpanded}
+ >
+ Position history
+ <span
+ className={`arrow ${isExpanded ? "open" : ""}`}
+ >
+ ▾
+ </span>
+ </button>
+
+ <div
+ className={`robot-history ${
+ isExpanded ? "expanded" : ""
+ }`}
+ >
+ <ul>
+ {robot?.robot_positions?.length ? (
+ robot?.robot_positions?.map(
+ (pos, index) => (
+ <li key={index}>
+ {`Lat: ${pos?.lat}, Lon: ${pos?.lon}`}
+ </li>
+ )
+ )
+ ) : (
+ <li className="robot-history-empty">
+ No previous positions.
+ </li>
+ )}
+ </ul>
+ </div>
+ </li>
+ );
+ })}
+ </ul>
+ );
+}
+
+export default RobotList;
+
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 2e66865..8bd795b 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,116 +1,27 @@
-import {
- useState,
- type ChangeEvent,
- type Dispatch,
- type SetStateAction,
-} from "react";
-import "../styles/Button.css";
-import "../styles/Sidebar.css";
-import type { ErrorResponse } from "../types/error";
-import type { CreateRobotResponse, Robot } from "../types/robot";
-
-type ExpandedRobotsState = Record<number, boolean>;
+import { useState, type Dispatch, type SetStateAction } from "react";
+import API_URL from "../config";
+import "../styles/button.css";
+import "../styles/sidebar.css";
+import type { Robot } from "../types/robot";
+import AddRobotForm from "./AddRobotForm";
+import RobotList from "./RobotList";
+import SimulationActions from "./SimulationActions";
type Props = {
- activeSimulation: boolean;
errorMessage: string;
- setErrorMessage: Dispatch<SetStateAction<string>>;
- token: string | null;
robots: Robot[];
- onStartAllRobots: () => Promise<void>;
- onStopAllRobots: () => Promise<void>;
+ token: string | null;
+ setErrorMessage: Dispatch<SetStateAction<string>>;
};
-const API_URL = import.meta.env.VITE_API_URL;
-
-function Sidebar({
- activeSimulation,
- errorMessage,
- setErrorMessage,
- token,
- robots,
- onStartAllRobots,
- onStopAllRobots,
-}: Props) {
- const [expandedRobots, setExpandedRobots] = useState<ExpandedRobotsState>(
- {}
- );
+function Sidebar({ errorMessage, robots, token, setErrorMessage }: Props) {
const [isAddingRobot, setIsAddingRobot] = useState(false);
- const [newRobotName, setNewRobotName] = useState("");
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- function toggleRobotHistory(robotId: number) {
- setExpandedRobots((prev) => ({
- ...prev,
- [robotId]: !prev[robotId],
- }));
- }
function handleAddClick() {
setIsAddingRobot(true);
- setNewRobotName("");
setErrorMessage("");
}
- function handleCancel() {
- setIsAddingRobot(false);
- setNewRobotName("");
- setErrorMessage("");
- }
-
- async function handleSubmit() {
- if (!newRobotName.trim()) {
- setErrorMessage("Please enter a name for the robot.");
- return;
- }
-
- setIsSubmitting(true);
- setErrorMessage("");
-
- try {
- const response = await fetch(`${API_URL}/robots`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${token}`,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ name: newRobotName.trim() }),
- });
-
- if (!response.ok) {
- const errorData: ErrorResponse = await response.json();
- throw new Error(
- errorData.message ||
- `Error creating the new robot: ${response.status}`
- );
- }
-
- const data: CreateRobotResponse = await response.json();
- console.log("Robot created successfully: ", data);
-
- // Reset form and close input field
- setNewRobotName("");
- setIsAddingRobot(false);
- } catch (error) {
- console.error("Error creating the new robot:", error);
-
- if (error instanceof Error) {
- setErrorMessage(error.message);
- } else {
- setErrorMessage("Error creating the new robot.");
- }
- } finally {
- setIsSubmitting(false);
- }
- }
-
- function handleInputChange(event: ChangeEvent<HTMLInputElement>) {
- setNewRobotName(event.target.value);
- if (errorMessage) {
- setErrorMessage("");
- }
- }
-
return (
<div className="sidebar">
<div className="sidebar-robots-header">
@@ -120,124 +31,31 @@ function Sidebar({
onClick={handleAddClick}
disabled={isAddingRobot}
>
- + Neu
+ + Add
</button>
</div>
- {isAddingRobot && (
- <div className="add-robot-form">
- <input
- type="text"
- placeholder="Robot name..."
- value={newRobotName}
- onChange={handleInputChange}
- disabled={isSubmitting}
- autoFocus
- />
- <div className="add-robot-actions">
- <button
- className="btn btn-start"
- onClick={handleSubmit}
- disabled={isSubmitting}
- >
- {isSubmitting ? "Creating..." : "Create"}
- </button>
- <button
- className="btn btn-stop"
- onClick={handleCancel}
- disabled={isSubmitting}
- >
- Cancel
- </button>
- </div>
- </div>
+ {isAddingRobot && (
+ <AddRobotForm
+ apiUrl={API_URL}
+ errorMessage={errorMessage}
+ token={token}
+ setErrorMessage={setErrorMessage}
+ setIsAddingRobot={setIsAddingRobot}
+ />
)}
{errorMessage && (
<div className="error-message">{errorMessage}</div>
)}
- <div className="simulation-actions">
- <button
- className="btn btn-start"
- onClick={onStartAllRobots}
- disabled={activeSimulation}
- >
- Start simulation
- </button>
-
- <button
- className="btn btn-stop"
- onClick={onStopAllRobots}
- disabled={!activeSimulation}
- >
- Stop simulation
- </button>
- </div>
-
- <ul className="sidebar-robot-list">
- {robots.map((robot) => {
- const isExpanded = expandedRobots[robot.id];
-
- return (
- <li key={robot.id}>
- <p className="robot-name">{robot.name}</p>
-
- <p>
- Status:{" "}
- <span
- className={`robot-status-${robot.status}`}
- >
- {robot.status}
- </span>
- </p>
-
- <p className="robot-coordinates-label">Position:</p>
- <ul className="robot-coordinates">
- <li>Lat: {robot.lat}</li>
- <li>Lon: {robot.lon}</li>
- </ul>
-
- <button
- className="btn btn-robot-history-toggle"
- onClick={() => toggleRobotHistory(robot.id)}
- aria-expanded={isExpanded}
- >
- Position history
- <span
- className={`arrow ${
- isExpanded ? "open" : ""
- }`}
- >
- ▾
- </span>
- </button>
+ <SimulationActions
+ apiUrl={API_URL}
+ token={token}
+ setErrorMessage={setErrorMessage}
+ />
- <div
- className={`robot-history ${
- isExpanded ? "expanded" : ""
- }`}
- >
- <ul>
- {robot.robot_positions?.length ? (
- robot.robot_positions.map(
- (pos, index) => (
- <li key={index}>
- {`Lat: ${pos.lat}, Lon: ${pos.lon}`}
- </li>
- )
- )
- ) : (
- <li className="robot-history-empty">
- No previous positions.
- </li>
- )}
- </ul>
- </div>
- </li>
- );
- })}
- </ul>
+ <RobotList robots={robots} />
</div>
);
}
diff --git a/frontend/src/components/SimulationActions.tsx b/frontend/src/components/SimulationActions.tsx
new file mode 100644
index 0000000..c91b933
--- /dev/null
+++ b/frontend/src/components/SimulationActions.tsx
@@ -0,0 +1,103 @@
+import { useState, type Dispatch, type SetStateAction } from "react";
+import type { ErrorResponse } from "../types/error";
+
+type Props = {
+ apiUrl: string;
+ token: string | null;
+ setErrorMessage: Dispatch<SetStateAction<string>>;
+};
+
+function SimulationActions({ apiUrl, token, setErrorMessage }: Props) {
+ const [isSimulationActive, setIsSimulationActive] =
+ useState<boolean>(false);
+
+ // TODO type responses
+ async function handleStartAllRobots() {
+ setIsSimulationActive(true);
+
+ try {
+ const response = await fetch(`${apiUrl}/robots/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 set all robots moving."
+ );
+ }
+
+ console.log("All robots set moving.");
+ } catch (error) {
+ console.error("Error starting robots:", error);
+
+ if (error instanceof Error) {
+ setErrorMessage(error.message);
+ } else {
+ setErrorMessage("An unexpected error occurred.");
+ }
+
+ setIsSimulationActive(false);
+ }
+ }
+
+ async function handleStopAllRobots() {
+ setIsSimulationActive(false);
+
+ try {
+ const response = await fetch(`${apiUrl}/robots/stop`, {
+ 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 set all robots idle."
+ );
+ }
+
+ console.log("All robots set idle.");
+ } catch (error) {
+ console.error("Error stopping robots:", error);
+
+ if (error instanceof Error) {
+ setErrorMessage(error.message);
+ } else {
+ setErrorMessage("An unexpected error occurred.");
+ }
+
+ setIsSimulationActive(true);
+ }
+ }
+
+ return (
+ <div className="simulation-actions">
+ <button
+ className="btn btn-start"
+ onClick={handleStartAllRobots}
+ disabled={isSimulationActive}
+ >
+ Start simulation
+ </button>
+
+ <button
+ className="btn btn-stop"
+ onClick={handleStopAllRobots}
+ disabled={!isSimulationActive}
+ >
+ Stop simulation
+ </button>
+ </div>
+ );
+}
+
+export default SimulationActions;
+