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
|
|
@ -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?',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue