diff options
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/src/components/AddRobotForm.tsx | 121 | ||||
| -rw-r--r-- | frontend/src/components/RobotList.tsx | 84 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 234 | ||||
| -rw-r--r-- | frontend/src/components/SimulationActions.tsx | 103 | ||||
| -rw-r--r-- | frontend/src/pages/Dashboard.tsx | 74 |
5 files changed, 336 insertions, 280 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; + diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index b8dd2c4..088fee2 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -14,7 +14,6 @@ import type { Robot, RobotsResponse } from "../types/robot"; function Dashboard() { const [errorMessage, setErrorMessage] = useState<string>(""); const [isLoading, setIsLoading] = useState<boolean>(true); - const [isSimulationActive, setIsSimulationActive] = useState(false); const [robots, setRobots] = useState<Robot[]>([]); const navigate = useNavigate(); @@ -29,72 +28,6 @@ function Dashboard() { navigate("/login", { replace: true }); } - async function handleStartAllRobots() { - setIsSimulationActive(true); - - try { - const response = await fetch(`${API_URL}/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(`${API_URL}/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); - } - } - // Request robot data from backend on component mount useEffect(() => { // Additional safety check to protect this page from unauthorized access @@ -162,13 +95,10 @@ function Dashboard() { <Header user={user} logout={handleLogout} /> <CityMap robots={robots} /> <Sidebar - activeSimulation={isSimulationActive} errorMessage={errorMessage} - setErrorMessage={setErrorMessage} - token={token} robots={robots} - onStartAllRobots={handleStartAllRobots} - onStopAllRobots={handleStopAllRobots} + token={token} + setErrorMessage={setErrorMessage} /> </div> ); |
