diff options
| author | Arne Rief <riearn@proton.me> | 2025-12-22 12:28:33 +0100 |
|---|---|---|
| committer | Arne Rief <riearn@proton.me> | 2025-12-22 12:29:13 +0100 |
| commit | 3818739c5901cc3f1d4596b24cfe1b827a2eca23 (patch) | |
| tree | 18e0c755386e6598f1cfe4193866b0b62a8f368d /frontend/src/components | |
| parent | 237f8ae6c29bbf485c312b2fed4d5ab4f99a4eff (diff) | |
FE Sidebar, create & move requests, BE create controller
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..2e66865 --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,246 @@ +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>; + +type Props = { + activeSimulation: boolean; + errorMessage: string; + setErrorMessage: Dispatch<SetStateAction<string>>; + token: string | null; + robots: Robot[]; + onStartAllRobots: () => Promise<void>; + onStopAllRobots: () => Promise<void>; +}; + +const API_URL = import.meta.env.VITE_API_URL; + +function Sidebar({ + activeSimulation, + errorMessage, + setErrorMessage, + token, + robots, + onStartAllRobots, + onStopAllRobots, +}: Props) { + const [expandedRobots, setExpandedRobots] = useState<ExpandedRobotsState>( + {} + ); + 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"> + <h2>Your Robots</h2> + <button + className="btn btn-add-robot" + onClick={handleAddClick} + disabled={isAddingRobot} + > + + Neu + </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> + )} + + {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> + + <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> + </div> + ); +} + +export default Sidebar; + |
