feat: Icons

This commit is contained in:
ISA
2025-09-12 15:59:44 +02:00
parent 7b881e80c2
commit 3a9b436352
6 changed files with 273 additions and 35 deletions

View File

@@ -23,4 +23,4 @@ NEXT_PUBLIC_USE_MOCKS=true
# z.B. http://10.10.0.13/xyz/index.aspx -> basePath in config.json auf /xyz setzen
# basePath wird jetzt in public/config.json gepflegt
# App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.1.360
NEXT_PUBLIC_APP_VERSION=1.1.361

View File

@@ -24,4 +24,4 @@ NEXT_PUBLIC_USE_MOCKS=false
# basePath wird jetzt in public/config.json gepflegt
# App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.1.360
NEXT_PUBLIC_APP_VERSION=1.1.361

View File

@@ -24,6 +24,7 @@ import { useMapComponentState } from "@/components/hooks/useMapComponentState.js
import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js";
//----------Ui Widgets----------------
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
import BaseMapPanel from "@/components/uiWidgets/baseMapPanel/BaseMapPanel.js";
import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js";
import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js";
//----------Daten aus API--------------------
@@ -171,6 +172,24 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
return true;
}
});
// Sichtbarkeit des Base-Map Panels (oben rechts, unter Toolbar)
const [showBaseMapPanel, setShowBaseMapPanel] = useState(() => {
try {
const v = localStorage.getItem("showBaseMapPanel");
return v === null ? false : v === "true";
} catch (_) {
return false;
}
});
// Sichtbarkeit der Koordinaten-Suche (Lupe)
const [showCoordinateInput, setShowCoordinateInput] = useState(() => {
try {
const v = localStorage.getItem("showCoordinateInput");
return v === null ? false : v === "true";
} catch (_) {
return false;
}
});
// Flag, ob Nutzer die Polyline-Checkbox manuell betätigt hat
// Nutzer-Flag global auf window, damit auch Redux darauf zugreifen kann
@@ -249,6 +268,18 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
localStorage.setItem("showLayersPanel", String(showLayersPanel));
} catch (_) {}
}, [showLayersPanel]);
// Persistiere Sichtbarkeit des Base-Map Panels
useEffect(() => {
try {
localStorage.setItem("showBaseMapPanel", String(showBaseMapPanel));
} catch (_) {}
}, [showBaseMapPanel]);
// Persistiere Sichtbarkeit der Koordinaten-Suche
useEffect(() => {
try {
localStorage.setItem("showCoordinateInput", String(showCoordinateInput));
} catch (_) {}
}, [showCoordinateInput]);
//--------------------------------------------
@@ -1108,38 +1139,10 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
/>
)}
<CoordinateInput onCoordinatesSubmit={handleCoordinatesSubmit} />
{showCoordinateInput && <CoordinateInput onCoordinatesSubmit={handleCoordinatesSubmit} />}
<div id="map" ref={mapRef} className="z-0" style={{ height: "100vh", width: "100vw" }}></div>
{/* Top-right controls: info toggle + hamburger (layers) */}
{/* Top-right controls: layers, info, expand, edit, and base map stack */}
<div className="absolute top-3 right-3 z-50 pointer-events-auto flex items-center gap-2">
<button
onClick={() => setShowLayersPanel(v => !v)}
aria-label={showLayersPanel ? "Layer-Panel ausblenden" : "Layer-Panel einblenden"}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showLayersPanel ? "Layer-Panel ausblenden" : "Layer-Panel einblenden"}
>
<Icon icon="material-symbols:menu-rounded" className="h-8 w-8 text-blue-900" />
</button>
<button
onClick={() => setShowAppInfoCard(v => !v)}
aria-label={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
>
<Icon
icon="material-symbols:info-rounded"
className="text-blue-900 h-8 w-8 pr-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
/>
</button>
<button
onClick={handleExpandClick}
aria-label="Karte auf Standardansicht"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Karte auf Standardansicht"
>
<img src="/img/expand-icon.svg" alt="Expand" className="h-8 w-8" />
</button>
<button
onClick={toggleEditMode}
aria-label={editMode ? "Bearbeitungsmodus deaktivieren" : "Bearbeitungsmodus aktivieren"}
@@ -1162,7 +1165,79 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
className="h-8 w-8 text-blue-900"
/>
</button>
<button
onClick={handleExpandClick}
aria-label="Karte auf Standardansicht"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Karte auf Standardansicht"
>
<img src="/img/expand-icon.svg" alt="Expand" className="h-8 w-8" />
</button>
<button
onClick={() => setShowBaseMapPanel(v => !v)}
aria-label={
showBaseMapPanel ? "Kartenhintergrund ausblenden" : "Kartenhintergrund wählen"
}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showBaseMapPanel ? "Kartenhintergrund ausblenden" : "Kartenhintergrund wählen"}
>
<Icon icon="material-symbols:layers-rounded" className="h-8 w-8 text-blue-900" />
</button>
<button
onClick={() => setShowLayersPanel(v => !v)}
aria-label={showLayersPanel ? "Layer-Panel ausblenden" : "Layer-Panel einblenden"}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showLayersPanel ? "Layer-Panel ausblenden" : "Layer-Panel einblenden"}
>
<Icon icon="material-symbols:menu-rounded" className="h-8 w-8 text-blue-900" />
</button>
{/* Lupe: Koordinaten-Suche ein-/ausblenden */}
<button
onClick={() => setShowCoordinateInput(v => !v)}
aria-label={
showCoordinateInput ? "Koordinatensuche ausblenden" : "Koordinatensuche einblenden"
}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={
showCoordinateInput ? "Koordinatensuche ausblenden" : "Koordinatensuche einblenden"
}
>
<Icon icon="material-symbols:search-rounded" className="h-8 w-8 text-blue-900" />
</button>
{/* Marker-Icon (line-md) */}
<button
onClick={() => {}}
aria-label="Marker"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Marker"
>
<Icon icon="line-md:map-marker-filled" className="h-8 w-8 text-blue-900" />
</button>
{/* Alarm-Icon (mdi) */}
<button
onClick={() => {}}
aria-label="Alarm"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Alarm"
>
<Icon icon="mdi:alarm-light-outline" className="h-8 w-8 text-blue-900" />
</button>
<button
onClick={() => setShowAppInfoCard(v => !v)}
aria-label={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
>
<Icon
icon="material-symbols:info-rounded"
className="text-blue-900 h-8 w-8 pr-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
/>
</button>
</div>
{showBaseMapPanel && map && (
<BaseMapPanel map={map} onClose={() => setShowBaseMapPanel(false)} />
)}
<CoordinatePopup isOpen={isPopupOpen} coordinates={currentCoordinates} onClose={closePopup} />
{showAppInfoCard && (

View File

@@ -0,0 +1,163 @@
// components/uiWidgets/baseMapPanel/BaseMapPanel.js
import React, { useEffect, useMemo, useRef, useState } from "react";
import L from "leaflet";
import { Icon } from "@iconify/react";
// Minimal, safe defaults (no API key required). You can extend via config later.
const DEFAULT_BASE_LAYERS = [
{
id: "osm-standard",
name: "Standard",
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: "&copy; OpenStreetMap contributors",
minZoom: 0,
maxZoom: 19,
},
{
id: "osm-humanitarian",
name: "Humanitarian",
url: "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
attribution: "&copy; OpenStreetMap contributors, Humanitarian OpenStreetMap Team",
minZoom: 0,
maxZoom: 19,
},
{
id: "cyclosm",
name: "CyclOSM",
url: "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png",
attribution: "&copy; OpenStreetMap contributors, CyclOSM",
minZoom: 0,
maxZoom: 20,
},
{
id: "carto-light",
name: "Carto Light",
url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
attribution: "&copy; OpenStreetMap contributors, &copy; CARTO",
subdomains: "abcd",
minZoom: 0,
maxZoom: 20,
},
];
function getCurrentTileLayer(map) {
let found = null;
if (!map) return null;
map.eachLayer(layer => {
if (!found && layer instanceof L.TileLayer) {
found = layer;
}
});
return found;
}
export default function BaseMapPanel({ map, onSelect, onClose, initialId }) {
const [activeId, setActiveId] = useState(initialId || null);
const layerCacheRef = useRef({});
const bases = useMemo(() => {
try {
if (typeof window !== "undefined") {
const cfg = window.__leafletConfig;
if (cfg && cfg.tileSources) {
return Object.entries(cfg.tileSources).map(([key, ts]) => ({
id: key,
name: ts.name || key,
url: ts.url,
attribution: ts.attribution || "&copy; OpenStreetMap contributors",
minZoom: ts.minZoom ?? cfg.minZoom ?? 0,
maxZoom: ts.maxZoom ?? cfg.maxZoom ?? 19,
subdomains: ts.subdomains,
}));
}
}
} catch (_) {}
return DEFAULT_BASE_LAYERS;
}, []);
const applyBase = id => {
if (!map) return;
const base = bases.find(b => b.id === id) || bases[0];
if (!base) return;
// Remove current tile layer
const current = getCurrentTileLayer(map);
if (current) {
try {
map.removeLayer(current);
} catch (_) {}
}
// Get or create the new layer
let nextLayer = layerCacheRef.current[id];
if (!nextLayer) {
nextLayer = L.tileLayer(base.url, {
attribution: base.attribution,
subdomains: base.subdomains || "abc",
tileSize: 256,
minZoom: base.minZoom ?? 0,
maxZoom: base.maxZoom ?? 19,
noWrap: true,
// Ensure base tiles stay behind overlays
zIndex: 1,
});
layerCacheRef.current[id] = nextLayer;
}
nextLayer.addTo(map);
try {
if (typeof map.setMinZoom === "function") map.setMinZoom(base.minZoom ?? 0);
if (typeof map.setMaxZoom === "function") map.setMaxZoom(base.maxZoom ?? 19);
if (typeof window !== "undefined") {
window.__tileSourceMinZoom = base.minZoom ?? 0;
window.__tileSourceMaxZoom = base.maxZoom ?? 19;
}
} catch (_) {}
setActiveId(id);
try {
localStorage.setItem("baseMapId", id);
} catch (_) {}
onSelect && onSelect(id);
};
useEffect(() => {
const saved = (() => {
try {
return localStorage.getItem("baseMapId");
} catch (_) {
return null;
}
})();
const targetId = initialId || saved || bases[0]?.id;
if (targetId) {
applyBase(targetId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map]);
return (
<div className="absolute top-16 right-3 z-50 w-64 bg-white rounded-lg shadow-lg p-3">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold">Map Layers</h3>
<button onClick={onClose} aria-label="Schließen" title="Schließen">
<Icon icon="material-symbols:close-rounded" className="h-5 w-5 text-gray-700" />
</button>
</div>
<div className="flex flex-col gap-2">
{bases.map(b => (
<button
key={b.id}
onClick={() => applyBase(b.id)}
className={`text-left rounded-md border p-2 hover:bg-gray-50 ${
activeId === b.id ? "ring-2 ring-blue-500" : ""
}`}
title={b.name}
>
<div className="font-medium text-sm">{b.name}</div>
<div className="text-[10px] text-gray-500 truncate">{b.url}</div>
</button>
))}
</div>
</div>
);
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "nodemap",
"version": "1.1.360",
"version": "1.1.361",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nodemap",
"version": "1.1.360",
"version": "1.1.361",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "nodemap",
"version": "1.1.360",
"version": "1.1.361",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",