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/.gitignore | 17 +---- 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 ++++++---- frontend/src/pages/Dashboard.tsx | 13 +++- frontend/src/styles/index.css | 16 ++--- frontend/src/types/robot.ts | 23 +++++-- 9 files changed, 182 insertions(+), 63 deletions(-) (limited to 'frontend') diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..0aa16e3 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,24 +1,9 @@ -# Logs logs *.log npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* node_modules dist dist-ssr +.env *.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? 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) { diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 088fee2..b1b38e4 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -14,6 +14,8 @@ import type { Robot, RobotsResponse } from "../types/robot"; function Dashboard() { const [errorMessage, setErrorMessage] = useState(""); const [isLoading, setIsLoading] = useState(true); + const [isSimulationActive, setIsSimulationActive] = + useState(false); const [robots, setRobots] = useState([]); const navigate = useNavigate(); @@ -25,6 +27,7 @@ function Dashboard() { async function handleLogout() { localStorage.removeItem("token-robot-tracker"); localStorage.removeItem("user"); + // setIsSimulationActive(false); navigate("/login", { replace: true }); } @@ -57,7 +60,9 @@ function Dashboard() { } const data: RobotsResponse = await response.json(); - setRobots(data.data); + + setRobots(data.robots); + setIsSimulationActive(data.simulationRunning); } catch (error) { console.error("Failed to load the robots:", error); @@ -66,7 +71,6 @@ function Dashboard() { } else { setErrorMessage("An unexpected error occurred."); } - } finally { setIsLoading(false); } @@ -78,7 +82,7 @@ function Dashboard() { const socket = io(API_URL); // Listen for real-time robot updates - socket.on("robots_update", (updatedRobots) => { + socket.on("robots_update", ({ updatedRobots }) => { setRobots(updatedRobots); }); @@ -95,10 +99,13 @@ function Dashboard() {
    ); diff --git a/frontend/src/styles/index.css b/frontend/src/styles/index.css index dcc2590..bd4eacd 100644 --- a/frontend/src/styles/index.css +++ b/frontend/src/styles/index.css @@ -20,25 +20,17 @@ --text-small: 0.9rem; box-sizing: border-box; + + background-color: #fff; + color: #213547; + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - - @media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #fff; - } - } } body { diff --git a/frontend/src/types/robot.ts b/frontend/src/types/robot.ts index 965afea..d9b2e7d 100644 --- a/frontend/src/types/robot.ts +++ b/frontend/src/types/robot.ts @@ -1,14 +1,16 @@ export type RobotPosition = { - lat: number; - lon: number; + lat: string; + lon: string; }; +export type RobotStatus = "idle" | "moving"; + export type Robot = { id: number; name: string; - status: "idle" | "moving"; - lat: number; - lon: number; + status: RobotStatus; + lat: string; + lon: string; robot_positions: RobotPosition[]; created_at: string; updated_at: string; @@ -16,7 +18,8 @@ export type Robot = { export type RobotsResponse = { source: "cache" | "database"; - data: Robot[]; + robots: Robot[]; + simulationRunning: boolean; }; export type CreateRobotResponse = { @@ -24,3 +27,11 @@ export type CreateRobotResponse = { robot: Robot; }; +export type RobotsUpdateBroadcast = { + updatedRobots: Robot[]; +}; + +export type SimulationResponse = { + message: string; + status?: RobotStatus; +}; -- cgit v1.2.3