summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/CityMap.tsx98
-rw-r--r--frontend/src/components/Header.tsx31
2 files changed, 129 insertions, 0 deletions
diff --git a/frontend/src/components/CityMap.tsx b/frontend/src/components/CityMap.tsx
new file mode 100644
index 0000000..b61e69a
--- /dev/null
+++ b/frontend/src/components/CityMap.tsx
@@ -0,0 +1,98 @@
+import { Feature, Map, View } from "ol";
+import { defaults } from "ol/control";
+import { Point } from "ol/geom";
+import TileLayer from "ol/layer/Tile";
+import VectorLayer from "ol/layer/Vector";
+import { fromLonLat, transformExtent } from "ol/proj";
+import { OSM } from "ol/source";
+import VectorSource from "ol/source/Vector";
+import Fill from "ol/style/Fill";
+import Icon from "ol/style/Icon";
+import Stroke from "ol/style/Stroke";
+import Style from "ol/style/Style";
+import Text from "ol/style/Text";
+import { useEffect, useRef } from "react";
+import robotMarker from "../assets/robot-head.svg";
+import "../styles/dashboard.css";
+import type { Robot } from "../types/robot";
+
+type Props = {
+ robots: Robot[];
+};
+
+function CityMap({ robots }: Props) {
+ const mapRef = useRef<HTMLDivElement | null>(null);
+ const mapInstance = useRef<Map | null>(null);
+ const markerLayer = useRef<VectorLayer<VectorSource> | null>(null);
+
+ useEffect(() => {
+ if (mapInstance.current) return; // prevent re-init
+
+ markerLayer.current = new VectorLayer({
+ source: new VectorSource(),
+ });
+
+ const leipzigCenter = fromLonLat([12.375919, 51.340863]);
+ const leipzigArea = transformExtent(
+ // west, south, east, north; transform from WGS84 lat-long system to Web Mercator system
+ [12.22, 51.26, 12.54, 51.44],
+ "EPSG:4326",
+ "EPSG:3857"
+ );
+
+ mapInstance.current = new Map({
+ target: mapRef.current as HTMLDivElement,
+ controls: defaults({ attribution: false }),
+ layers: [
+ new TileLayer({
+ source: new OSM(),
+ }),
+ markerLayer.current,
+ ],
+ view: new View({
+ center: leipzigCenter,
+ zoom: 15,
+ extent: leipzigArea,
+ }),
+ });
+ }, []);
+
+ useEffect(() => {
+ if (!markerLayer.current) return;
+
+ const source = markerLayer.current.getSource();
+ if (!source) return;
+
+ source.clear();
+
+ robots.forEach((robot) => {
+ const feature = new Feature({
+ geometry: new Point(fromLonLat([robot?.lon, robot?.lat])),
+ robotId: robot?.id,
+ });
+
+ feature.setStyle(
+ new Style({
+ image: new Icon({
+ src: robotMarker,
+ scale: 1.2,
+ anchor: [0.5, 0.4], // anchor label bottom center of icon
+ }),
+ text: new Text({
+ text: robot?.name,
+ font: "bold 14px sans-serif",
+ fill: new Fill({ color: "#000" }),
+ stroke: new Stroke({ color: "#fff", width: 3 }), // outline for better visibility
+ offsetY: -25, // move label above the marker
+ }),
+ })
+ );
+
+ source.addFeature(feature);
+ });
+ }, [robots]);
+
+ return <div id="map" ref={mapRef}></div>;
+}
+
+export default CityMap;
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
new file mode 100644
index 0000000..b9a51b8
--- /dev/null
+++ b/frontend/src/components/Header.tsx
@@ -0,0 +1,31 @@
+import "../styles/button.css";
+import "../styles/header.css";
+import Logo from "./Logo";
+import userIcon from "../assets/user-icon.svg";
+import type { AuthorizedUser } from "../types/login";
+
+type Props = {
+ user: AuthorizedUser;
+ logout: () => Promise<void>;
+};
+
+function Header({ user, logout }: Props) {
+ return (
+ <header className="header">
+ <Logo />
+
+ <div className="header-user">
+ <span className="header-user-info">
+ <img src={userIcon} alt="User Icon" />
+ <p>{user?.email}</p>
+ </span>
+
+ <button className="btn btn-stop btn-logout" onClick={logout}>
+ Logout
+ </button>
+ </div>
+ </header>
+ );
+}
+
+export default Header;