Feature: User-Feedback, Regen-Uhrzeit im Wetter-Chip, Admin-Karten klickbar (SW by-v833)
- Feedback-Modal im Settings (Kategorie + Text → E-Mail an support@banyaro.app) - Wetter-Chip (Karte + Gassi-Score): zeigt nächste Regenstunde ab ≥20% Wahrscheinlichkeit - Gassi-Score-Chip: zweizeilige Wetter-Info, linksbündig, volle Chipbreite - Admin-Übersicht: Stat-Karten anklickbar → navigiert direkt zum jeweiligen Tab - ui.js: visualViewport-Listener hebt Modal über Tastatur (alle Modals) - api.js: Pydantic v2 Array-Detail korrekt als Fehlermeldung extrahiert - map.js: Wetter-Fallback über watchPosition wenn getCurrentPosition scheitert - Update-Loop-Fix: index.html ?v= synchron mit APP_VER halten (alle 4 Stellen)
This commit is contained in:
parent
d18c592ef0
commit
70af387147
12 changed files with 211 additions and 42 deletions
|
|
@ -58,7 +58,10 @@ const API = (() => {
|
|||
try { data = await response.json(); } catch { data = null; }
|
||||
|
||||
if (!response.ok) {
|
||||
const message = data?.detail || data?.message || `Fehler ${response.status}`;
|
||||
const _d = data?.detail;
|
||||
const message = (typeof _d === 'string' ? _d
|
||||
: Array.isArray(_d) ? (_d[0]?.msg || 'Ungültige Eingabe')
|
||||
: null) || data?.message || `Fehler ${response.status}`;
|
||||
const isSwOffline = response.status === 503 && message.startsWith('Offline');
|
||||
|
||||
// Retry: GET auf echte 5xx (nicht SW-generierte Offline-503)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '826'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '833'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt
|
||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||
|
|
|
|||
|
|
@ -534,22 +534,22 @@ window.Page_admin = (() => {
|
|||
};
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="adm-stats-grid">
|
||||
${_statCard('users', 'Nutzer gesamt', s.users_total, 'var(--c-primary)')}
|
||||
${_statCard('user-plus', 'Neu heute', s.users_today, 'var(--c-success)')}
|
||||
${_statCard('activity', 'Aktiv (7 Tage)', s.active_users_7d, 'var(--c-primary)')}
|
||||
${_statCard('paw-print', 'Hunde', s.dogs_total, 'var(--c-primary)')}
|
||||
${_statCard('chat-circle-dots','Threads', s.threads, 'var(--c-text-secondary)')}
|
||||
${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)')}
|
||||
${_statCard('camera', 'Fotos freizugeben', s.pending_fotos ?? 0, (s.pending_fotos ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
|
||||
${_statCard('skull', 'Gesperrte User', s.banned, s.banned > 0 ? '#f59e0b' : 'var(--c-text-muted)')}
|
||||
${_statCard('warning-octagon', 'Giftk. aktiv', s.poison_active, 'var(--c-danger)')}
|
||||
${_statCard('bell', 'Push-Abos', s.push_subscriptions, 'var(--c-text-secondary)')}
|
||||
<div class="adm-stats-grid" id="adm-overview-grid">
|
||||
${_statCard('users', 'Nutzer gesamt', s.users_total, 'var(--c-primary)', 'nutzer')}
|
||||
${_statCard('user-plus', 'Neu heute', s.users_today, 'var(--c-success)', 'nutzer')}
|
||||
${_statCard('activity', 'Aktiv (7 Tage)', s.active_users_7d, 'var(--c-primary)', 'nutzer')}
|
||||
${_statCard('paw-print', 'Hunde', s.dogs_total, 'var(--c-primary)', 'nutzer')}
|
||||
${_statCard('chat-circle-dots','Threads', s.threads, 'var(--c-text-secondary)','forum')}
|
||||
${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)', 'moderation')}
|
||||
${_statCard('camera', 'Fotos freizugeben', s.pending_fotos ?? 0, (s.pending_fotos ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'moderation')}
|
||||
${_statCard('skull', 'Gesperrte User', s.banned, s.banned > 0 ? '#f59e0b' : 'var(--c-text-muted)', 'nutzer')}
|
||||
${_statCard('warning-octagon', 'Giftk. aktiv', s.poison_active, 'var(--c-danger)', 'system')}
|
||||
${_statCard('bell', 'Push-Abos', s.push_subscriptions, 'var(--c-text-secondary)','system')}
|
||||
${_statCard('image', 'Media-Einträge', s.media_count, 'var(--c-text-secondary)')}
|
||||
${_statCard('map-pin', 'Routen', s.routes_total, 'var(--c-text-secondary)')}
|
||||
${_statCard('calendar', 'Events', s.events_total, 'var(--c-text-secondary)')}
|
||||
${_statCard('map-trifold', 'OSM-Marker', s.osm_total.toLocaleString('de'), 'var(--c-success)')}
|
||||
${_statCard('squares-four', 'Gecachte Tiles', s.osm_tiles.toLocaleString('de'), 'var(--c-text-secondary)')}
|
||||
${_statCard('map-trifold', 'OSM-Marker', s.osm_total.toLocaleString('de'), 'var(--c-success)', 'system')}
|
||||
${_statCard('squares-four', 'Gecachte Tiles', s.osm_tiles.toLocaleString('de'), 'var(--c-text-secondary)', 'system')}
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:var(--space-4)">
|
||||
|
|
@ -705,11 +705,19 @@ window.Page_admin = (() => {
|
|||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.querySelector('#adm-overview-grid')?.addEventListener('click', e => {
|
||||
const card = e.target.closest('[data-adm-tab]');
|
||||
if (!card) return;
|
||||
const tab = card.dataset.admTab;
|
||||
_container.querySelector(`#adm-tabs .by-tab[data-tab="${tab}"]`)?.click();
|
||||
});
|
||||
}
|
||||
|
||||
function _statCard(icon, label, value, color) {
|
||||
function _statCard(icon, label, value, color, tab = null) {
|
||||
const clickable = tab ? `data-adm-tab="${tab}" style="padding:var(--space-4);text-align:center;cursor:pointer"` : `style="padding:var(--space-4);text-align:center"`;
|
||||
return `
|
||||
<div class="card" style="padding:var(--space-4);text-align:center">
|
||||
<div class="card" ${clickable}>
|
||||
<svg class="ph-icon" style="width:24px;height:24px;color:${color};margin-bottom:var(--space-2)"
|
||||
aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ window.Page_map = (() => {
|
|||
let _map = null;
|
||||
let _leafletLoaded = false;
|
||||
let _userPos = null;
|
||||
let _weatherLoaded = false;
|
||||
let _placingMarker = false;
|
||||
let _tempMarker = null;
|
||||
|
||||
|
|
@ -147,6 +148,7 @@ window.Page_map = (() => {
|
|||
_userPos = pos;
|
||||
if (_frankfurtTimer) { clearTimeout(_frankfurtTimer); _frankfurtTimer = null; }
|
||||
_map?.flyTo([pos.lat, pos.lon], 14, { duration: 1.2 });
|
||||
_weatherLoaded = true;
|
||||
_loadWeather(pos.lat, pos.lon);
|
||||
}).catch(() => {
|
||||
const btn = document.getElementById('map-locate-btn');
|
||||
|
|
@ -373,6 +375,7 @@ window.Page_map = (() => {
|
|||
pos => {
|
||||
const { latitude: lat, longitude: lon, accuracy: acc } = pos.coords;
|
||||
_userPos = { lat, lon };
|
||||
if (!_weatherLoaded) { _weatherLoaded = true; _loadWeather(lat, lon); }
|
||||
if (_locationMarker) {
|
||||
_locationMarker.setLatLng([lat, lon]);
|
||||
_locationAccuracy?.setLatLng([lat, lon]).setRadius(acc);
|
||||
|
|
@ -1628,7 +1631,9 @@ window.Page_map = (() => {
|
|||
const w = await API.weather.get(lat, lon);
|
||||
const temp = w.temp_c != null ? `${Math.round(w.temp_c)}°` : '–';
|
||||
const icon = `<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px;vertical-align:-2px"><use href="/icons/phosphor.svg#${w.icon}"></use></svg>`;
|
||||
const regen = w.precip_prob != null ? ` · 💧 ${w.precip_prob}%` : '';
|
||||
const regen = w.precip_prob != null
|
||||
? (w.next_rain_time ? ` · 💧 ${w.precip_prob}% ab ${w.next_rain_time}` : ` · 💧 ${w.precip_prob}%`)
|
||||
: '';
|
||||
let zecken = '';
|
||||
if (w.zecken_warnung) {
|
||||
const col = w.zecken_warnung === 'hoch' ? '#991B1B' : '#92400E';
|
||||
|
|
|
|||
|
|
@ -269,6 +269,12 @@ window.Page_settings = (() => {
|
|||
<span>Hilfe & FAQ</span>
|
||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||||
</div>
|
||||
<div class="sidebar-item" id="settings-feedback-btn"
|
||||
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border);cursor:pointer">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#chat-dots"></use></svg>
|
||||
<span>Feedback geben</span>
|
||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||||
</div>
|
||||
${!_appState.user?.subscription_tier || _appState.user.subscription_tier === 'standard' || _appState.user.subscription_tier === 'standard_test' ? `
|
||||
<div style="margin:var(--space-3) 0;padding:var(--space-3) var(--space-4);
|
||||
background:rgba(196,132,58,0.1);border-radius:var(--radius-md);
|
||||
|
|
@ -790,6 +796,52 @@ window.Page_settings = (() => {
|
|||
App.navigate('hilfe');
|
||||
});
|
||||
|
||||
document.getElementById('settings-feedback-btn')?.addEventListener('click', () => {
|
||||
const sel = (id) => document.getElementById(id);
|
||||
const inputStyle = 'width:100%;padding:10px 12px;border:1.5px solid var(--c-border);border-radius:var(--radius-md);background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm);box-sizing:border-box';
|
||||
UI.modal.open({
|
||||
title: 'Feedback geben',
|
||||
body: `
|
||||
<form id="feedback-form" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Kategorie</label>
|
||||
<select id="feedback-kat" name="kategorie" style="${inputStyle}">
|
||||
<option value="idee">💡 Idee / Wunsch</option>
|
||||
<option value="bug">🐛 Bug / Fehler</option>
|
||||
<option value="lob">🎉 Lob</option>
|
||||
<option value="sonstiges">💬 Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Deine Nachricht</label>
|
||||
<textarea id="feedback-text" name="text" rows="5" maxlength="2000"
|
||||
placeholder="Was möchtest du uns mitteilen?"
|
||||
style="${inputStyle};resize:vertical"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
footer: `
|
||||
<div class="w3-btn-stack">
|
||||
<button type="submit" form="feedback-form" id="feedback-submit-btn" class="btn btn-primary" style="width:100%">Absenden</button>
|
||||
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
sel('feedback-form')?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const btn = sel('feedback-submit-btn');
|
||||
const kat = sel('feedback-kat')?.value;
|
||||
const text = sel('feedback-text')?.value?.trim();
|
||||
if (!text) { UI.toast.error('Bitte schreib etwas.'); return; }
|
||||
await UI.asyncButton(btn, async () => {
|
||||
await API.post('/feedback', { kategorie: kat, text });
|
||||
UI.modal.close?.();
|
||||
UI.toast.success('Vielen Dank für dein Feedback!');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('settings-logout-btn')?.addEventListener('click', async () => {
|
||||
const ok = await UI.modal.confirm({
|
||||
title : 'Abmelden?',
|
||||
|
|
|
|||
|
|
@ -79,15 +79,34 @@ const UI = (() => {
|
|||
|
||||
document.getElementById('modal-container').appendChild(overlay);
|
||||
document.documentElement.classList.add('modal-open');
|
||||
_current = { overlay, onClose };
|
||||
|
||||
// Tastatur auf Mobilgeräten: Modal nach oben schieben wenn Keyboard erscheint
|
||||
let _vvCleanup = null;
|
||||
const vv = window.visualViewport;
|
||||
if (vv) {
|
||||
const adjust = () => {
|
||||
const kb = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
||||
overlay.style.paddingBottom = (kb + 16) + 'px';
|
||||
};
|
||||
vv.addEventListener('resize', adjust);
|
||||
vv.addEventListener('scroll', adjust);
|
||||
_vvCleanup = () => {
|
||||
vv.removeEventListener('resize', adjust);
|
||||
vv.removeEventListener('scroll', adjust);
|
||||
overlay.style.paddingBottom = '';
|
||||
};
|
||||
}
|
||||
|
||||
_current = { overlay, onClose, _vvCleanup };
|
||||
|
||||
return overlay.querySelector('.modal');
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!_current) return;
|
||||
const { onClose } = _current;
|
||||
const { onClose, _vvCleanup } = _current;
|
||||
onClose?.();
|
||||
_vvCleanup?.();
|
||||
_current.overlay.remove();
|
||||
document.documentElement.classList.remove('modal-open');
|
||||
_current = null;
|
||||
|
|
|
|||
|
|
@ -1097,7 +1097,10 @@ window.Worlds = (() => {
|
|||
<span style="font-size:1.25rem;font-weight:800;color:${gassiColor};line-height:1">${gassiScore ?? '—'}</span>
|
||||
${gassiScore ? `<span style="font-size:var(--text-xs);color:rgba(255,255,255,0.4);font-weight:600">/10</span>` : ''}
|
||||
</div>
|
||||
${w ? `<span style="font-size:9px;color:rgba(255,255,255,0.75);font-weight:500;margin-top:1px;white-space:nowrap">${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen</span>` : ''}
|
||||
${w ? `<div style="font-size:9px;color:rgba(255,255,255,0.75);font-weight:500;margin-top:2px;line-height:1.5">
|
||||
<div>${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen</div>
|
||||
${w.next_rain_time ? `<div style="color:rgba(255,255,255,0.5)">ab ${w.next_rain_time} Uhr</div>` : ''}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue