Add roadmap and implement top UX improvements for score transparency and map selection
This commit is contained in:
parent
a118c3ca33
commit
c0ea660e48
6 changed files with 324 additions and 16 deletions
73
src/app.js
73
src/app.js
|
|
@ -24,6 +24,9 @@ const useLocationEl = document.getElementById("use-location");
|
|||
const scoreEl = document.getElementById("score");
|
||||
const ampelEl = document.getElementById("ampel");
|
||||
const reasonsEl = document.getElementById("reasons");
|
||||
const factorDetailsEl = document.getElementById("factor-details");
|
||||
const qualityBadgeEl = document.getElementById("quality-badge");
|
||||
const spotContextEl = document.getElementById("spot-context");
|
||||
const nightWindowEl = document.getElementById("night-window");
|
||||
const networkStatusEl = document.getElementById("network-status");
|
||||
const dataStatusEl = document.getElementById("data-status");
|
||||
|
|
@ -240,6 +243,7 @@ function initializeMap() {
|
|||
|
||||
function setCoordinates(lat, lon, options = {}) {
|
||||
const zoom = options.zoom || null;
|
||||
const skipMarkerUpdate = Boolean(options.skipMarkerUpdate);
|
||||
latEl.value = Number(lat).toFixed(6);
|
||||
lonEl.value = Number(lon).toFixed(6);
|
||||
|
||||
|
|
@ -248,16 +252,20 @@ function setCoordinates(lat, lon, options = {}) {
|
|||
}
|
||||
|
||||
const latLng = [Number(lat), Number(lon)];
|
||||
if (!mapMarker) {
|
||||
mapMarker = L.circleMarker(latLng, {
|
||||
radius: 8,
|
||||
color: "#006680",
|
||||
fillColor: "#1ca4c7",
|
||||
fillOpacity: 0.8,
|
||||
weight: 2,
|
||||
}).addTo(map);
|
||||
} else {
|
||||
mapMarker.setLatLng(latLng);
|
||||
if (!skipMarkerUpdate) {
|
||||
if (!mapMarker) {
|
||||
mapMarker = L.marker(latLng, {
|
||||
draggable: true,
|
||||
icon: L.divIcon({ className: "spot-pin", html: "<span></span>", iconSize: [16, 16], iconAnchor: [8, 8] }),
|
||||
}).addTo(map);
|
||||
mapMarker.on("dragend", () => {
|
||||
const pos = mapMarker.getLatLng();
|
||||
setCoordinates(pos.lat, pos.lng, { fromMap: true, zoom: map.getZoom(), skipMarkerUpdate: true });
|
||||
searchStatusEl.textContent = "Position per Pin verschoben.";
|
||||
});
|
||||
} else {
|
||||
mapMarker.setLatLng(latLng);
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isFinite(zoom)) {
|
||||
|
|
@ -684,6 +692,40 @@ function renderScore(data, fromCache, cacheTime = "") {
|
|||
reasonsEl.appendChild(li);
|
||||
});
|
||||
|
||||
const quality = (data.meta && data.meta.quality) || null;
|
||||
qualityBadgeEl.classList.remove("high", "medium", "low");
|
||||
if (quality && quality.level) {
|
||||
qualityBadgeEl.classList.add(quality.level);
|
||||
qualityBadgeEl.textContent = `${quality.label} (${quality.score})`;
|
||||
} else {
|
||||
qualityBadgeEl.textContent = "-";
|
||||
}
|
||||
|
||||
factorDetailsEl.innerHTML = "";
|
||||
const details = (data.explanation && data.explanation.factors) || data.factors || [];
|
||||
if (!details.length) {
|
||||
const empty = document.createElement("li");
|
||||
empty.textContent = "Keine Faktoren vorhanden.";
|
||||
factorDetailsEl.appendChild(empty);
|
||||
} else {
|
||||
details.slice(0, 8).forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${item.label} (${Number(item.points).toFixed(1)}) | ${item.source}`;
|
||||
factorDetailsEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
const spotCtx = (data.explanation && data.explanation.spot_context) || null;
|
||||
if (spotCtx) {
|
||||
spotContextEl.textContent =
|
||||
`Spot-Kontext: area ${spotCtx.area_type}, road ${spotCtx.road_type}, ` +
|
||||
`Polizei ${formatMeters(spotCtx.distance_police_m)}, ` +
|
||||
`Feuerwehr ${formatMeters(spotCtx.distance_fire_m)}, ` +
|
||||
`Krankenhaus ${formatMeters(spotCtx.distance_hospital_m)}`;
|
||||
} else {
|
||||
spotContextEl.textContent = "Spot-Kontext: -";
|
||||
}
|
||||
|
||||
const health = (data.meta && data.meta.health) || {};
|
||||
if (health.has_data) {
|
||||
const freshness = `freshest ${health.freshest_age_hours}h, stalest ${health.stalest_age_hours}h`;
|
||||
|
|
@ -706,6 +748,17 @@ function renderScore(data, fromCache, cacheTime = "") {
|
|||
}
|
||||
}
|
||||
|
||||
function formatMeters(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n)) {
|
||||
return "-";
|
||||
}
|
||||
if (n >= 1000) {
|
||||
return `${(n / 1000).toFixed(1)} km`;
|
||||
}
|
||||
return `${Math.round(n)} m`;
|
||||
}
|
||||
|
||||
function buildSignal(signalType) {
|
||||
if (!currentSpot || !currentSpot.spot_id) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -67,10 +67,19 @@
|
|||
<div class="score" id="score">--</div>
|
||||
<div class="ampel" id="ampel">-</div>
|
||||
</div>
|
||||
<div class="quality-wrap">
|
||||
<span class="quality-label">Datenqualitaet:</span>
|
||||
<span id="quality-badge" class="quality-badge">-</span>
|
||||
</div>
|
||||
<p id="night-window">Bezug: Heute Nacht 22:00-06:00</p>
|
||||
<ul id="reasons" class="reasons">
|
||||
<li>Noch keine Daten geladen.</li>
|
||||
</ul>
|
||||
<h3 class="subheading">Score-Faktoren</h3>
|
||||
<ul id="factor-details" class="factor-details">
|
||||
<li>Details werden nach der ersten Abfrage angezeigt.</li>
|
||||
</ul>
|
||||
<div id="spot-context" class="spot-context">Spot-Kontext: -</div>
|
||||
</div>
|
||||
|
||||
<h3>Community Signal</h3>
|
||||
|
|
|
|||
|
|
@ -197,6 +197,21 @@ select {
|
|||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.spot-pin {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.spot-pin span {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid #fff;
|
||||
background: #1ca4c7;
|
||||
box-shadow: 0 2px 8px rgba(6, 31, 43, 0.35);
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
|
|
@ -263,6 +278,68 @@ select {
|
|||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.quality-wrap {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quality-label {
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.quality-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 72px;
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
color: #fff;
|
||||
background: #5f7280;
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.quality-badge.high {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.quality-badge.medium {
|
||||
background: var(--yellow);
|
||||
}
|
||||
|
||||
.quality-badge.low {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.factor-details {
|
||||
margin: 6px 0 0;
|
||||
padding-left: 18px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.factor-details li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.spot-context {
|
||||
margin-top: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 0.84rem;
|
||||
border-top: 1px dashed var(--line);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.signal-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue