Compare commits

...

12 Commits

Author SHA1 Message Date
ISA
aeec307909 chore(git): normalize line endings via .gitattributes 2025-09-15 14:01:23 +02:00
ISA
369f29a769 feat(ui): add AreaDropdown and exclusive toggle with layers panel
New AreaDropdown component for quick station selection (filters by allowed systems, ESC to close)
MapComponent: toggle AreaDropdown via MapMarkerIcon; auto-hide MapLayersControlPanel when dropdown is open and vice versa
fix(alarms): hasActiveAlarm now checks Statis[].Alarm for both array and object shapes
fix(panel): Kabelstrecken now auto-enables TALAS (system-1) when turned on; keeps behavior to disable polylines when TALAS is unchecked; persists visibility to localStorage and emits visibilityChanged
Minor: imports, state wiring, and render guards updated
Affected files:

MapComponent.js
MapLayersControlPanel.js
AreaDropdown.js (new)
2025-09-15 13:53:16 +02:00
ISA
d166b2468d feat: AreaDropdown separate from MapLayerControlPanel 2025-09-15 13:38:19 +02:00
ISA
59c8680c23 feat: AlarmIcon nur bei GisStationsStatusDistrict Attribute Alarm :1 2025-09-15 13:03:54 +02:00
ISA
1a046f8212 style: Icon as components and littwin-blue 2025-09-15 11:52:20 +02:00
ISA
e35216daf5 chore: change icons order 2025-09-15 10:47:25 +02:00
ISA
91ad47166f del: BasMapPanel entfernt aus rechliche Gründe,
Kurzantwort: Für kommerzielle Nutzung sind OSM‑Community‑Tile‑Server nicht geeignet. Nutze einen bezahlten Anbieter (z. B. Thunderforest, Tracestrack) oder hoste selbst. Attribution ist immer Pflicht.

Links und Hinweise je Layer/Provider:

OpenStreetMap Standard (osm-standard)

Lizenz/Daten: ODbL, Attribution Pflicht
Tile-Server-Policy (keine Produktion/hohe Last): https://operations.osmfoundation.org/policies/tiles/
Urheberrecht/Attribution: https://www.openstreetmap.org/copyright
HOT Humanitarian (osm-humanitarian)

Community-Server (OSM France); keine Produktion/hohe Last
Info/Policy OSM France Tiles: https://tile.openstreetmap.fr/
HOT: https://www.hotosm.org/
CyclOSM (cyclosm)

Community-Server (OSM France); keine Produktion/hohe Last
Projektseite: https://www.cyclosm.org/
Hinweise/Policy (OSM France): https://tile.openstreetmap.fr/
Wiki: https://wiki.openstreetmap.org/wiki/CyclOSM
Carto Light (carto-light / Positron)

Keylos nutzbar mit Attribution; Fair‑Use, für hohe Last über CARTO‑Pläne
Basemaps: https://carto.com/basemaps/
Attribution: https://carto.com/attributions
Pricing (Plattform): https://carto.com/pricing/ (bei großem Volumen Sales kontaktieren)
Thunderforest (Cycle/Transport u. a.)

Kommerziell mit API‑Key; Pläne von Free bis Pro
Pricing: https://www.thunderforest.com/pricing/
Terms/Attribution: https://www.thunderforest.com/terms/
Tracestrack Topo

API‑Key erforderlich; kostenlose und bezahlte Pläne
Übersicht/Pricing: https://www.tracestrack.com/en/maps/
Nutzungsbedingungen: https://www.tracestrack.com/en/terms/
2025-09-15 10:36:03 +02:00
ISA
3a9b436352 feat: Icons 2025-09-12 15:59:44 +02:00
ISA
7b881e80c2 link Ebenen 2025-09-12 15:18:55 +02:00
ISA
cc19a0a466 feat: hamburger menu und info Icons 2025-09-12 14:55:19 +02:00
ISA
f200d0bb20 chore(husky): remove deprecated v9 bootstrap lines from pre-commit 2025-09-12 13:58:44 +02:00
ISA
75a0ab000f chore(Websocket): Websocket dump refresh 2025-09-12 13:58:13 +02:00
33 changed files with 974 additions and 93 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.355
NEXT_PUBLIC_APP_VERSION=1.1.369

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.355
NEXT_PUBLIC_APP_VERSION=1.1.369

32
.gitattributes vendored Normal file
View File

@@ -0,0 +1,32 @@
# Normalize line endings and mark binary files
# Default: let Git manage line endings
* text=auto
# Source files use LF in repo
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
*.json text eol=lf
*.css text eol=lf
*.scss text eol=lf
*.md text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.svg text eol=lf
*.sh text eol=lf
# Windows scripts keep CRLF
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf
# Images and binaries
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.ico binary
*.pdf binary

View File

@@ -1,6 +1,3 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo "🔄 Version wird automatisch erhöht (bumpVersion.js)..."
# Version automatisch erhöhen

View File

@@ -0,0 +1,18 @@
const AlarmIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="red"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 6.9L3.87 4.78l1.41-1.41L7.4 5.5zM13 1v3h-2V1zm7.13 3.78L18 6.9l-1.4-1.4l2.12-2.13zM4.5 10.5v2h-3v-2zm15 0h3v2h-3zM6 20h12a2 2 0 0 1 2 2H4a2 2 0 0 1 2-2m6-15a6 6 0 0 1 6 6v8H6v-8a6 6 0 0 1 6-6"
/>
</svg>
);
export default AlarmIcon;

View File

@@ -0,0 +1,18 @@
const EditIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"
/>
</svg>
);
export default EditIcon;

View File

@@ -0,0 +1,18 @@
const EditOffIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3"
/>
</svg>
);
export default EditOffIcon;

View File

@@ -0,0 +1,18 @@
const ExpandIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
/>
</svg>
);
export default ExpandIcon;

View File

@@ -0,0 +1,18 @@
const InfoIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
);
export default InfoIcon;

View File

@@ -0,0 +1,19 @@
const MapMarkerIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="rgb(0, 174, 239)"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z"
/>
</svg>
);
export default MapMarkerIcon;

View File

@@ -0,0 +1,18 @@
const MenuIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
export default MenuIcon;

View File

@@ -0,0 +1,18 @@
const SearchIcon = ({ className = "h-8 w-8" }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="rgb(0, 174, 239)"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
/>
</svg>
);
export default SearchIcon;

View File

@@ -6,7 +6,15 @@ import "leaflet-contextmenu/dist/leaflet.contextmenu.css";
import "leaflet-contextmenu";
import "leaflet.smooth_marker_bouncing";
import "react-toastify/dist/ReactToastify.css";
import { InformationCircleIcon } from "@heroicons/react/20/solid";
import { Icon } from "@iconify/react";
import EditIcon from "@/components/icons/material-symbols/EditIcon";
import EditOffIcon from "@/components/icons/material-symbols/EditOffIcon";
import SearchIcon from "@/components/icons/material-symbols/SearchIcon";
import MenuIcon from "@/components/icons/material-symbols/MenuIcon";
import InfoIcon from "@/components/icons/material-symbols/InfoIcon";
import AlarmIcon from "@/components/icons/material-symbols/AlarmIcon";
import MapMarkerIcon from "@/components/icons/material-symbols/MapMarkerIcon";
import ExpandIcon from "@/components/icons/material-symbols/ExpandIcon";
import PoiUpdateModal from "@/components/pois/poiUpdateModal/PoiUpdateModal.js";
import { ToastContainer, toast } from "react-toastify";
import plusRoundIcon from "../icons/devices/overlapping/PlusRoundIcon.js";
@@ -26,6 +34,7 @@ import CoordinatePopup from "@/components/contextmenu/CoordinatePopup.js";
import MapLayersControlPanel from "@/components/uiWidgets/mapLayersControlPanel/MapLayersControlPanel.js";
import CoordinateInput from "@/components/uiWidgets/CoordinateInput.js";
import VersionInfoModal from "@/components/uiWidgets/VersionInfoModal.js";
import AreaDropdown from "@/components/uiWidgets/AreaDropdown";
//----------Daten aus API--------------------
import { fetchPoiDataService } from "@/services/database/pois/fetchPoiDataByIdService.js";
import AddPOIModal from "@/components/pois/AddPOIModal.js";
@@ -39,6 +48,8 @@ import { setSelectedPoi } from "@/redux/slices/database/pois/selectedPoiSlice.js
import { setDisabled } from "@/redux/slices/database/polylines/polylineEventsDisabledSlice.js";
import { setMapId, setUserId } from "@/redux/slices/urlParameterSlice";
import { selectMapLayersState, setLayerVisibility } from "@/redux/slices/mapLayersSlice";
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
import { incrementZoomTrigger } from "@/redux/slices/zoomTriggerSlice";
import { setCurrentPoi } from "@/redux/slices/database/pois/currentPoiSlice.js";
import { selectGisLines } from "@/redux/slices/database/polylines/gisLinesSlice";
import { selectGisLinesStatus } from "@/redux/slices/webservice/gisLinesStatusSlice";
@@ -129,6 +140,13 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
const { data: gisLinesStatusData, status: statusGisLinesStatus } = useSelector(
selectGisLinesStatusFromWebservice
);
// Alarm Status aus GisStationsStatusDistrict
const gisStationsStatusDistrict = useSelector(state => state.gisStationsStatusDistrict.data);
// Unterstützt sowohl Array-Shape (Statis[]) als auch Objekt mit Statis-Array
const hasActiveAlarm = Array.isArray(gisStationsStatusDistrict)
? gisStationsStatusDistrict.some(item => item?.Alarm === 1)
: gisStationsStatusDistrict?.Statis?.some(item => item?.Alarm === 1) || false;
const poiIconsData = useSelector(selectPoiIconsData);
const poiIconsStatus = useSelector(selectPoiIconsStatus);
const poiTypData = useSelector(selectPoiTypData);
@@ -147,10 +165,39 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
const [showVersionInfoModal, setShowVersionInfoModal] = useState(false);
const [poiTypMap, setPoiTypMap] = useState(new Map());
const [showPopup, setShowPopup] = useState(false);
const [showAreaDropdown, setShowAreaDropdown] = useState(false);
const poiLayerRef = useRef(null); // Referenz auf die Layer-Gruppe für Datenbank-Marker
const mapRef = useRef(null); // Referenz auf das DIV-Element der Karte
const [map, setMap] = useState(null); // Zustand der Karteninstanz
const [oms, setOms] = useState(null); // State für OMS-Instanz
// Sichtbarkeit der App-Info-Karte (unten links)
const [showAppInfoCard, setShowAppInfoCard] = useState(() => {
try {
const v = localStorage.getItem("showAppInfoCard");
return v === null ? true : v === "true";
} catch (_) {
return true;
}
});
// Sichtbarkeit des Layer-Kontrollpanels (oben rechts)
const [showLayersPanel, setShowLayersPanel] = useState(() => {
try {
const v = localStorage.getItem("showLayersPanel");
return v === null ? true : v === "true";
} catch (_) {
return true;
}
});
// Base-Map Panel wurde entfernt
// 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
@@ -188,6 +235,14 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
const [popupCoordinates, setPopupCoordinates] = useState(null);
const [popupVisible, setPopupVisible] = useState(false);
const [poiData, setPoiData] = useState([]);
// Edit mode state mirrors MapLayersControlPanel's behavior
const [editMode, setEditMode] = useState(() => {
try {
return localStorage.getItem("editMode") === "true";
} catch (_) {
return false;
}
});
const openVersionInfoModal = () => {
setShowVersionInfoModal(true);
@@ -209,6 +264,26 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
}
});
// Persistiere Sichtbarkeit der App-Info-Karte
useEffect(() => {
try {
localStorage.setItem("showAppInfoCard", String(showAppInfoCard));
} catch (_) {}
}, [showAppInfoCard]);
// Persistiere Sichtbarkeit des Layer-Panels
useEffect(() => {
try {
localStorage.setItem("showLayersPanel", String(showLayersPanel));
} catch (_) {}
}, [showLayersPanel]);
// Persist-Logik für Base-Map Panel entfernt
// Persistiere Sichtbarkeit der Koordinaten-Suche
useEffect(() => {
try {
localStorage.setItem("showCoordinateInput", String(showCoordinateInput));
} catch (_) {}
}, [showCoordinateInput]);
//--------------------------------------------
const handleCoordinatesSubmit = coords => {
@@ -981,6 +1056,29 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
}, [GisSystemStatic, mapLayersVisibility, dispatch]);
//---------------------------------------------
//--------------------------------------------
// Expand handler (same behavior as MapLayersControlPanel expand icon)
const handleExpandClick = () => {
dispatch(setSelectedArea("Station wählen"));
dispatch(incrementZoomTrigger());
};
// Toggle edit mode (same logic as EditModeToggle component)
const hasEditRight = Array.isArray(userRights)
? userRights.includes?.(56) || userRights.some?.(r => r?.IdRight === 56)
: false;
const toggleEditMode = () => {
if (!hasEditRight) return;
const next = !editMode;
setEditMode(next);
try {
localStorage.setItem("editMode", String(next));
} catch (_) {}
if (typeof window !== "undefined") {
window.location.reload();
}
};
//--------------------------------------------
return (
<>
@@ -1035,31 +1133,135 @@ const MapComponent = ({ locations, onLocationUpdate, lineCoordinates }) => {
)}
</div>
{GisStationsStaticDistrict && GisStationsStaticDistrict.Points?.length > 0 && (
<MapLayersControlPanel
className="z-50"
handlePolylineCheckboxChange={handlePolylineCheckboxChange}
/>
)}
{GisStationsStaticDistrict &&
GisStationsStaticDistrict.Points?.length > 0 &&
showLayersPanel &&
!showAreaDropdown && (
<MapLayersControlPanel
className="z-50"
handlePolylineCheckboxChange={handlePolylineCheckboxChange}
/>
)}
<CoordinateInput onCoordinatesSubmit={handleCoordinatesSubmit} />
{showCoordinateInput && <CoordinateInput onCoordinatesSubmit={handleCoordinatesSubmit} />}
<div id="map" ref={mapRef} className="z-0" style={{ height: "100vh", width: "100vw" }}></div>
{/* 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">
{/* Alarm-Icon - nur anzeigen wenn Alarm aktiv */}
{hasActiveAlarm && (
<button
onClick={() => {}}
aria-label="Alarm aktiv"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Alarm aktiv"
>
<AlarmIcon className="h-8 w-8 animate-pulse text-red-500" />
</button>
)}
{/* Marker-Icon (line-md) */}
<button
onClick={() =>
setShowAreaDropdown(v => {
const next = !v;
if (next) setShowLayersPanel(false); // Dropdown öffnen -> Panel schließen
return next;
})
}
aria-label="Marker"
className="rounded-full bg-white/90 hover:bg-white shadow p-1"
title="Marker"
>
<MapMarkerIcon className="h-8 w-8" />
</button>
{showAreaDropdown && <AreaDropdown onClose={() => setShowAreaDropdown(false)} />}
{/*Lupe: Koordinatensuche 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"
}
>
<SearchIcon className="h-8 w-8" />
</button>
<button
onClick={toggleEditMode}
aria-label={editMode ? "Bearbeitungsmodus deaktivieren" : "Bearbeitungsmodus aktivieren"}
className={`rounded-full shadow p-1 ${
hasEditRight
? "bg-white/90 hover:bg-white"
: "bg-white/60 cursor-not-allowed opacity-50"
}`}
title={
hasEditRight
? editMode
? "Bearbeitungsmodus deaktivieren"
: "Bearbeitungsmodus aktivieren"
: "Keine Bearbeitungsrechte"
}
disabled={!hasEditRight}
>
{editMode ? <EditOffIcon className="h-8 w-8" /> : <EditIcon className="h-8 w-8" />}
</button>
{/* Expand: Karte auf Standardansicht */}
<button
onClick={handleExpandClick}
aria-label="Karte auf Standardansicht"
className="rounded-full bg-white/90 hover:bg-white shadow p-1 "
title="Karte auf Standardansicht"
>
<ExpandIcon className="h-8 w-8" />
</button>
{/* Lupe: Koordinaten-Suche ein-/ausblenden */}
<button
onClick={() =>
setShowLayersPanel(v => {
const next = !v;
if (next) setShowAreaDropdown(false); // Panel öffnen -> Dropdown schließen
return next;
})
}
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"}
>
<MenuIcon className="h-8 w-8" />
</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"}
>
<InfoIcon
className="h-8 w-8 pr-1"
title={showAppInfoCard ? "Info ausblenden" : "Info einblenden"}
/>
</button>
</div>
{/* BaseMapPanel entfernt */}
<CoordinatePopup isOpen={isPopupOpen} coordinates={currentCoordinates} onClose={closePopup} />
<div className="absolute bottom-3 left-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
<div className="flex justify-between items-center">
<div>
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
<br />
<span className="text-black text-lg">Version {appVersion}</span>
</div>
<div>
<button onClick={openVersionInfoModal}>
<InformationCircleIcon className="text-blue-900 h-8 w-8 pr-1" title="Weitere Infos" />
</button>
{showAppInfoCard && (
<div className="absolute bottom-3 left-3 w-72 p-4 bg-white rounded-lg shadow-md z-50">
<div className="flex justify-between items-center">
<div>
<span className="text-black text-lg font-semibold"> TALAS.Map </span>
<br />
<span className="text-black text-lg">Version {appVersion}</span>
</div>
<div>
<button onClick={openVersionInfoModal}>
<InfoIcon className="h-8 w-8 pr-1" title="Weitere Infos" />
</button>
</div>
</div>
</div>
</div>
)}
<VersionInfoModal
showVersionInfoModal={showVersionInfoModal}
closeVersionInfoModal={closeVersionInfoModal}

View File

@@ -0,0 +1,79 @@
// /components/uiWidgets/AreaDropdown.js
import React, { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { setSelectedArea } from "@/redux/slices/selectedAreaSlice";
import { selectGisStationsStaticDistrict } from "@/redux/slices/webservice/gisStationsStaticDistrictSlice";
import { selectGisSystemStatic } from "@/redux/slices/webservice/gisSystemStaticSlice";
/**
* Kleines Dropdown zur Auswahl der Station (Area_Name),
* nutzt dieselbe Datenquelle wie das MapLayersControlPanel.
*/
const AreaDropdown = ({ onClose }) => {
const dispatch = useDispatch();
const GisStationsStaticDistrict = useSelector(selectGisStationsStaticDistrict) || {};
const GisSystemStatic = useSelector(selectGisSystemStatic) || [];
// Erlaubte Systeme: Allow === 1 und Map === 1
const allowedSystems = useMemo(() => {
return new Set(
(Array.isArray(GisSystemStatic) ? GisSystemStatic : [])
.filter(sys => sys.Allow === 1 && sys.Map === 1)
.map(sys => sys.IdSystem)
);
}, [GisSystemStatic]);
// Uniqe Areas basierend auf Allowed Systems
const areaOptions = useMemo(() => {
const points = GisStationsStaticDistrict?.Points || [];
const seen = new Set();
const filtered = points.filter(p => {
if (!p?.Area_Name) return false;
if (!allowedSystems.has(p.System)) return false;
if (seen.has(p.Area_Name)) return false;
seen.add(p.Area_Name);
return true;
});
return filtered.map(p => ({ label: p.Area_Name, value: p.IdLD }));
}, [GisStationsStaticDistrict, allowedSystems]);
const handleChange = e => {
const selectedIndex = e.target.options.selectedIndex;
const label = e.target.options[selectedIndex].text;
dispatch(setSelectedArea(label));
onClose?.();
};
// Schließe mit ESC
useEffect(() => {
const onKey = e => {
if (e.key === "Escape") onClose?.();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
return (
<div className="absolute top-16 right-3 z-[60]">
<div className="bg-white rounded-md shadow-lg p-3 border border-gray-200 min-w-[220px]">
<div className="text-sm font-semibold mb-2">Station wählen</div>
<select
onChange={handleChange}
className="border p-2 rounded w-full"
defaultValue="__default__"
>
<option value="__default__" disabled>
Bitte wählen
</option>
{areaOptions.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
</div>
);
};
export default AreaDropdown;

View File

@@ -4,7 +4,7 @@ import React, { useState } from "react";
const CoordinateInput = ({ onCoordinatesSubmit }) => {
const [coordinates, setCoordinates] = useState("");
const handleSubmit = (e) => {
const handleSubmit = e => {
e.preventDefault();
if (onCoordinatesSubmit) {
onCoordinatesSubmit(coordinates);
@@ -12,9 +12,21 @@ const CoordinateInput = ({ onCoordinatesSubmit }) => {
};
return (
<form onSubmit={handleSubmit} className="fixed top-5 left-5 z-50 bg-white shadow-lg rounded-lg p-4 w-72">
<input type="text" placeholder="Koordinaten eingeben (lat,lng)" value={coordinates} onChange={(e) => setCoordinates(e.target.value)} className="border p-2 rounded w-full mb-2" />
<button type="submit" className="bg-blue-500 text-white p-2 rounded w-full hover:bg-blue-600">
<form
onSubmit={handleSubmit}
className="fixed top-5 left-5 z-50 bg-white shadow-lg rounded-lg p-4 w-72"
>
<input
type="text"
placeholder="Koordinaten eingeben (lat,lng)"
value={coordinates}
onChange={e => setCoordinates(e.target.value)}
className="border p-2 rounded w-full mb-2"
/>
<button
type="submit"
className="bg-littwin-blue text-white p-2 rounded w-full hover:bg-blue-600"
>
Zu Marker zoomen
</button>
</form>

View File

@@ -31,7 +31,7 @@ const VersionInfoModal = ({ showVersionInfoModal, closeVersionInfoModal, APP_VER
</p>
<button
onClick={closeVersionInfoModal}
className="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700 mx-auto block"
className="mt-4 bg-littwin-blue text-white px-4 py-2 rounded mx-auto block"
>
Schließen
</button>

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>
);
}

View File

@@ -241,28 +241,55 @@ function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
}, [GisStationsStaticDistrict]);
//---------------------------
// Polyline (Kabelstrecken) abhängig von TALAS (system-1)
const onPolylineToggle = checked => {
if (editMode) return;
// Wenn Nutzer Kabelstrecken einschaltet, aber TALAS aktuell ausgeblendet ist,
// dann TALAS automatisch aktivieren (sofern erlaubt)
const talasKey = "system-1";
const talasVisible = !!mapLayersVisibility[talasKey];
if (checked && isTalasAllowed && !talasVisible) {
dispatch(setLayerVisibility({ layer: talasKey, visibility: true }));
// Persistiere Sichtbarkeit map/user-spezifisch
const mapId2 = localStorage.getItem("currentMapId");
const userId2 = localStorage.getItem("currentUserId");
const mapStorageKey =
mapId2 && userId2 ? `mapLayersVisibility_m${mapId2}_u${userId2}` : "mapLayersVisibility";
localStorage.setItem(
mapStorageKey,
JSON.stringify({ ...mapLayersVisibility, [talasKey]: true })
);
// Event feuern wie an anderer Stelle
setTimeout(() => {
const event = new Event("visibilityChanged");
window.dispatchEvent(event);
}, 0);
}
// Sichtbarkeit der Kabelstrecken setzen
handlePolylineCheckboxChange(checked);
};
//---------------------------
return (
<div
id="mainDataSheet"
className="absolute top-3 right-3 w-1/6 min-w-[300px] max-w-[400px] z-10 bg-white p-2 rounded-lg shadow-lg"
className="absolute top-16 right-3 w-1/6 min-w-[300px] max-w-[200px] z-10 bg-white p-2 rounded-lg shadow-lg"
>
<div className="flex flex-col gap-4 p-4">
<div className="flex items-center justify-between space-x-2">
<select
{/*
<select
onChange={handleAreaChange}
id="stationListing"
className="border-solid-1 p-2 rounded ml-1 font-semibold"
style={{ minWidth: "150px", maxWidth: "200px" }}
>
<option value="Station wählen">Station wählen</option>
{/*
...new Map(
(GisStationsStaticDistrict.Points || [])
.filter(p => !!p.Area_Name)
.map(p => [p.Area_Name, p])
).values(),
*/}
{[
...new Map(
(GisStationsStaticDistrict.Points || [])
@@ -275,6 +302,9 @@ function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
</option>
))}
</select>
*/}
{/*
<div className="flex items-center space-x-2">
<EditModeToggle />
<img
@@ -284,6 +314,7 @@ function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
onClick={handleIconClick}
/>
</div>
*/}
</div>
{/* Checkboxen mit Untermenüs */}
@@ -310,7 +341,7 @@ function MapLayersControlPanel({ handlePolylineCheckboxChange }) {
<input
type="checkbox"
checked={kabelstreckenVisible}
onChange={e => handlePolylineCheckboxChange(e.target.checked)}
onChange={e => onPolylineToggle(e.target.checked)}
id="polyline-checkbox"
disabled={!isTalasAllowed || editMode}
/>

View File

@@ -102,3 +102,4 @@ Station suchen
Hamburger-Menü
- [ ] TODO: Info Karte: VersionInfoModal.js Modal soll über einem Icon 'Info' oben rechts
eingeblendet und ausgeblendet um mehr von der Karte zu sehen Info
https://www.openstreetmap.org/#map=13/51.80097/9.33495&layers=P

View File

@@ -22,4 +22,73 @@ Verzeichnisstruktur funktioniert.
---
## OSMbasierte, „open“ Quellen
Diese sind datenrechtlich offen (ODbL bzw. Community-Lizenzen), aber das „kostenlos“ gilt nicht im
Sinne unbegrenzter TileNutzung. Die TileServer werden als CommunityRessource bereitgestellt
bitte Policies respektieren.
- osm-standard (OpenStreetMap)
- - Key: Nein
- - Nutzung: FairUse; für produktive/hohe Last eigenen TileServer/Provider verwenden.
- - Attribution: „© OpenStreetMap contributors“
- osm-humanitarian (HOT)
- - Key: Nein
- - Nutzung: FairUse; für größere Last die Betreiber kontaktieren bzw. andere Infrastruktur nutzen.
- - Attribution: „© OpenStreetMap contributors <br>
-
- Humanitarian OpenStreetMap Team“ cyclosm
- - Key: Nein
- - Nutzung: FairUse (bereitgestellt u. a. über OSM France). Für höhere Last
Unterstützung/Hostingoptionen prüfen.
- - Attribution: „CyclOSM“ + „OpenStreetMap contributors“
- PraxisTipps Kleine bis mittlere Nutzung: Die oben genannten „keyless“ Quellen sind oft
ausreichend, solange du Attribution setzt und Limits respektierst. Produktion/hohe Last: Nutze
einen Anbieter mit Vertrag/Key (z. B. Thunderforest, Tracestrack, MapTiler, Mapbox) oder hoste
Tiles selbst. Schlüssel im Client: Für Thunderforest/Tracestrack stehen die Keys im Frontend. Das
ist üblich, aber der Key ist sichtbar. Wenn du ihn verbergen willst, richte einen kleinen
TileProxy auf deinem Server ein, der den Key serverseitig anhängt und optional cached.
Attribution: Dein BaseMapPanel setzt bereits Attributionsstrings aus config.json. Achte darauf,
dass sie je Quelle korrekt sind.
---
Kurzantwort: Für kommerzielle Nutzung sind OSMCommunityTileServer nicht geeignet. Nutze einen
bezahlten Anbieter (z.B. Thunderforest, Tracestrack) oder hoste selbst. Attribution ist immer
Pflicht.
Links und Hinweise je Layer/Provider:
OpenStreetMap Standard (osm-standard)
Lizenz/Daten: ODbL, Attribution Pflicht Tile-Server-Policy (keine Produktion/hohe Last):
https://operations.osmfoundation.org/policies/tiles/ Urheberrecht/Attribution:
https://www.openstreetmap.org/copyright HOT Humanitarian (osm-humanitarian)
Community-Server (OSM France); keine Produktion/hohe Last Info/Policy OSM France Tiles:
https://tile.openstreetmap.fr/ HOT: https://www.hotosm.org/ CyclOSM (cyclosm)
Community-Server (OSM France); keine Produktion/hohe Last Projektseite: https://www.cyclosm.org/
Hinweise/Policy (OSM France): https://tile.openstreetmap.fr/ Wiki:
https://wiki.openstreetmap.org/wiki/CyclOSM Carto Light (carto-light / Positron)
Keylos nutzbar mit Attribution; FairUse, für hohe Last über CARTOPläne Basemaps:
https://carto.com/basemaps/ Attribution: https://carto.com/attributions Pricing (Plattform):
https://carto.com/pricing/ (bei großem Volumen Sales kontaktieren) Thunderforest (Cycle/Transport u.
a.)
Kommerziell mit APIKey; Pläne von Free bis Pro Pricing: https://www.thunderforest.com/pricing/
Terms/Attribution: https://www.thunderforest.com/terms/ Tracestrack Topo
APIKey erforderlich; kostenlose und bezahlte Pläne Übersicht/Pricing:
https://www.tracestrack.com/en/maps/ Nutzungsbedingungen: https://www.tracestrack.com/en/terms/
Empfehlung:
Produktion: Nimm Thunderforest oder Tracestrack (oder MapTiler: https://www.maptiler.com/pricing/)
oder hoste Tiles selbst. Attribution in der Karte anzeigen (OSM + jeweiliger Anbieter). Soll ich
diese Links samt klarer Hinweise kompakt in eure README.md einpflegen?
---
[Zurück zur Übersicht](../README.md)

26
package-lock.json generated
View File

@@ -1,16 +1,17 @@
{
"name": "nodemap",
"version": "1.1.355",
"version": "1.1.369",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "nodemap",
"version": "1.1.355",
"version": "1.1.369",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@heroicons/react": "^2.1.5",
"@iconify/react": "^6.0.1",
"@mui/icons-material": "^6.0.2",
"@reduxjs/toolkit": "^2.5.1",
"autoprefixer": "^10.4.19",
@@ -357,6 +358,27 @@
"react": ">= 16 || ^19.0.0-rc"
}
},
"node_modules/@iconify/react": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@iconify/react/-/react-6.0.1.tgz",
"integrity": "sha512-fCocnAfiGXjrA0u7KkS3W/OQHNp9LRFICudvOtxmS3Mf7U92aDhP50wyzRbobZli51zYt9ksZ9g0J7H586XvOQ==",
"license": "MIT",
"dependencies": {
"@iconify/types": "^2.0.0"
},
"funding": {
"url": "https://github.com/sponsors/cyberalien"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/@iconify/types": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
"license": "MIT"
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",

View File

@@ -1,10 +1,11 @@
{
"name": "nodemap",
"version": "1.1.355",
"version": "1.1.369",
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@heroicons/react": "^2.1.5",
"@iconify/react": "^6.0.1",
"@mui/icons-material": "^6.0.2",
"@reduxjs/toolkit": "^2.5.1",
"autoprefixer": "^10.4.19",

View File

@@ -17,6 +17,61 @@
"_comment": "OpenStreetMap Online-Kartenquelle über Server-Proxy (relativ)",
"minZoom": 0,
"maxZoom": 19
},
"osm-standard": {
"name": "Standard",
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "&copy; OpenStreetMap contributors",
"subdomains": "abc",
"minZoom": 0,
"maxZoom": 19
},
"osm-humanitarian": {
"name": "Humanitarian",
"url": "https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png",
"attribution": "&copy; OpenStreetMap contributors, Humanitarian OpenStreetMap Team",
"subdomains": "abc",
"minZoom": 0,
"maxZoom": 19
},
"cyclosm": {
"name": "CyclOSM",
"url": "https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png",
"attribution": "&copy; OpenStreetMap contributors, CyclOSM",
"subdomains": "abc",
"minZoom": 0,
"maxZoom": 20
},
"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
},
"thunderforest-cycle": {
"name": "Cycle Map (Thunderforest)",
"url": "https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=YOUR_THUNDERFOREST_API_KEY",
"attribution": "&copy; OpenStreetMap contributors, &copy; Thunderforest",
"subdomains": "abc",
"minZoom": 0,
"maxZoom": 22
},
"thunderforest-transport": {
"name": "Transport Map (Thunderforest)",
"url": "https://{s}.tile.thunderforest.com/transport/{z}/{x}/{y}.png?apikey=YOUR_THUNDERFOREST_API_KEY",
"attribution": "&copy; OpenStreetMap contributors, &copy; Thunderforest",
"subdomains": "abc",
"minZoom": 0,
"maxZoom": 22
},
"tracestrack-topo": {
"name": "Tracestrack Topo",
"url": "https://tile.tracestrack.com/topo__/{z}/{x}/{y}.webp?key=YOUR_TRACESTRACK_KEY",
"attribution": "&copy; OpenStreetMap contributors, &copy; Tracestrack",
"minZoom": 0,
"maxZoom": 19
}
},
"active": "osm",

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15" />
</svg>

Before

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728L5.636 5.636m12.728 12.728L18 21l-3-3m-12.728-.364A9 9 0 015.636 5.636m0 0L3 3l3 3m9.364 9.364L18 21M5.636 5.636L3 3" />
</svg>

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="rgb(0, 174, 239)" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25s-7.5-4.108-7.5-11.25a7.5 7.5 0 1115 0z" />
</svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="rgb(0, 174, 239)">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -1,6 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}", "./hooks/**/*.{js,ts,jsx,tsx}"],
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./hooks/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
zIndex: {
@@ -10,6 +14,9 @@ module.exports = {
90: "90",
100: "100",
},
colors: {
"littwin-blue": "#00aeef",
},
},
},
plugins: [],

View File

@@ -6,7 +6,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 01 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -15,7 +16,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 31 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -24,7 +26,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 17 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -33,7 +36,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 05 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -42,7 +46,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 20 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -51,7 +56,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 32 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -60,7 +66,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 01 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -69,7 +76,8 @@
"Co": "#FF00FF",
"Me": "Station offline",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -78,7 +86,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 01 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -87,7 +96,8 @@
"Co": "#FF00FF",
"Me": "Station offline",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50066,
@@ -96,7 +106,8 @@
"Co": "#FF00FF",
"Me": "CPL offline",
"Feld": 5,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -105,7 +116,8 @@
"Co": "#FF00FF",
"Me": "CPL offline",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -114,7 +126,8 @@
"Co": "#FF00FF",
"Me": "Wasserdruck aus",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -123,7 +136,8 @@
"Co": "#FF00FF",
"Me": "Ein",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50011,
@@ -132,7 +146,8 @@
"Co": "#FF00FF",
"Me": "Digitaleingang 1 ON",
"Feld": 16,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50000,
@@ -141,7 +156,8 @@
"Co": "#FF00FF",
"Me": "Ein",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -150,7 +166,8 @@
"Co": "#FF00FF",
"Me": "Station offline",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -159,7 +176,8 @@
"Co": "#FF00FF",
"Me": "Eingang DE 32 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -168,7 +186,8 @@
"Co": "#FFFF00",
"Me": "KÜG 07: Überspannung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -177,7 +196,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -186,7 +206,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -195,7 +216,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -204,7 +226,8 @@
"Co": "#FFFF00",
"Me": "Eingang DE 02 kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -213,7 +236,8 @@
"Co": "#FFFF00",
"Me": "KÜG 08: Überspannung gehend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -222,7 +246,8 @@
"Co": "#FF9900",
"Me": "Eingang DE 03 Test Karte kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -231,7 +256,8 @@
"Co": "#FF0000",
"Me": "KÜG 01: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -240,7 +266,8 @@
"Co": "#FF0000",
"Me": "KÜG 02: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50922,
@@ -249,7 +276,8 @@
"Co": "#FF0000",
"Me": "KÜG 03: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50000,
@@ -258,7 +286,8 @@
"Co": "#FF0000",
"Me": "über 8V kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -267,7 +296,8 @@
"Co": "#FF0000",
"Me": "KÜG 05: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -276,7 +306,8 @@
"Co": "#FF0000",
"Me": "KÜG 02: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50984,
@@ -285,7 +316,8 @@
"Co": "#FF0000",
"Me": "KÜG 06: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -294,7 +326,8 @@
"Co": "#FF0000",
"Me": "KÜG 01: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -303,7 +336,8 @@
"Co": "#FF0000",
"Me": "KÜG 06: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50977,
@@ -312,7 +346,8 @@
"Co": "#FF0000",
"Me": "KÜG 05: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50976,
@@ -321,7 +356,8 @@
"Co": "#FF0000",
"Me": "CPL offline",
"Feld": 3,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50976,
@@ -330,7 +366,8 @@
"Co": "#FF0000",
"Me": "KÜG 03: Isolationsminderung kommend",
"Feld": 3,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -339,7 +376,8 @@
"Co": "#FF0000",
"Me": "KÜG 04: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -348,7 +386,8 @@
"Co": "#FF0000",
"Me": "KÜG 02: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -357,7 +396,8 @@
"Co": "#FF0000",
"Me": "KÜG 01: Isolationsminderung kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50001,
@@ -366,7 +406,8 @@
"Co": "#FF0000",
"Me": "Sammelstörung kommend",
"Feld": 5,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -375,7 +416,8 @@
"Co": "#FF0000",
"Me": "KÜG 06: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50975,
@@ -384,7 +426,8 @@
"Co": "#FF0000",
"Me": "KÜG 05: Aderbruch kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50963,
@@ -393,7 +436,8 @@
"Co": "#FF0000",
"Me": "CPL offline",
"Feld": 3,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50063,
@@ -402,7 +446,8 @@
"Co": "#FF0000",
"Me": "Digitaleingang 1 EIN",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
},
{
"IdLD": 50000,
@@ -411,6 +456,7 @@
"Co": "#FF0000",
"Me": "über 10V kommend",
"Feld": 4,
"Icon": 0
"Icon": 0,
"Alarm": 0
}
]