Feature/Fix: Routen-Navi, Badge-Klick-Fix, Forum-Edit, Chat-Unread-Refresh

- Routen: Navi-Button öffnet Apple Maps/Google Maps mit Start/Ziel-GPS
- Routen: Teilen-Button nutzt navigator.share() mit Fallback auf Clipboard
- Routen: Icon 'share' → 'arrow-square-out' (war nicht im Sprite)
- Nav-Badge: pointer-events:none → Badge blockiert keine Klicks mehr
- visibilitychange: Badges + Chat-Liste sofort refresh bei App-Rückkehr
- Forum: eigene Threads und Antworten bearbeiten (PATCH /threads/content, PATCH /posts)
- SW by-v238, APP_VER 215
This commit is contained in:
rene 2026-04-19 10:34:01 +02:00
parent 289158b2cd
commit 7a25ccae90
8 changed files with 169 additions and 11 deletions

View file

@ -320,6 +320,8 @@ const API = (() => {
patchThread(id, data) { return patch(`/forum/threads/${id}`, data); },
addPost(threadId, data){ return post(`/forum/threads/${threadId}/posts`, data); },
deletePost(id) { return del(`/forum/posts/${id}`); },
updateThread(id, data) { return patch(`/forum/threads/${id}/content`, data); },
updatePost(id, data) { return patch(`/forum/posts/${id}`, data); },
uploadThreadFoto(id, file) {
const fd = new FormData(); fd.append('file', file);
return upload(`/forum/threads/${id}/fotos`, fd);

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '214'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '215'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {
@ -427,6 +427,15 @@ const App = (() => {
_updateNotifBadge();
_updateChatBadge();
setInterval(() => { _updateNotifBadge(); _updateChatBadge(); }, 30_000);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
_updateNotifBadge();
_updateChatBadge();
if (state.page === 'chat') {
pages['chat']?.module?.refresh?.();
}
}
});
const pendingInvite = sessionStorage.getItem('pending_invite');
if (pendingInvite) {

View file

@ -446,9 +446,15 @@ window.Page_chat = (() => {
});
}
async function refresh() {
await _loadList();
await _updateChatBadge();
}
// ----------------------------------------------------------
return {
init,
refresh,
_showList,
_openThread,
_send,

View file

@ -387,6 +387,7 @@ window.Page_forum = (() => {
const footer = _appState.user ? `
${(isOwn || isMod) ? `<button type="button" class="btn btn-ghost btn-sm" id="ft-delete-thread" style="color:var(--c-danger)">${UI.icon('trash')} Löschen</button>` : ''}
${isOwn ? `<button type="button" class="btn btn-ghost btn-sm" id="ft-edit-thread">${UI.icon('pencil-simple')} Bearbeiten</button>` : ''}
<button type="button" class="btn btn-secondary" id="ft-close">Schließen</button>
${(!thread.is_locked && _appState.user) ? `<button type="button" class="btn btn-primary" id="ft-reply">Antworten</button>` : ''}
` : `<button type="button" class="btn btn-primary" id="ft-close">Schließen</button>`;
@ -435,6 +436,11 @@ window.Page_forum = (() => {
} catch (err) { UI.toast.error(err.message); }
});
// Edit thread (owner)
document.getElementById('ft-edit-thread')?.addEventListener('click', () => {
_showEditThreadModal(thread);
});
// Moderator: pin/lock/delete
document.querySelector('.forum-mod-pin')?.addEventListener('click', async () => {
try {
@ -564,7 +570,8 @@ window.Page_forum = (() => {
${UI.icon('heart')} <span class="forum-post-like-count">${p.likes || 0}</span>
</button>
${(!isOwn && uid) ? `<button class="forum-report-btn forum-post-report" data-post-id="${p.id}">${UI.icon('flag')}</button>` : ''}
${canDelete ? `<button class="btn btn-ghost btn-sm forum-post-delete" data-post-id="${p.id}" style="color:var(--c-danger);margin-left:auto">${UI.icon('trash')}</button>` : ''}
${isOwn ? `<button class="btn btn-ghost btn-sm forum-post-edit" data-post-id="${p.id}" data-text="${_esc(p.text || '')}" style="margin-left:auto">${UI.icon('pencil-simple')}</button>` : ''}
${canDelete ? `<button class="btn btn-ghost btn-sm forum-post-delete" data-post-id="${p.id}" style="color:var(--c-danger)${isOwn ? '' : ';margin-left:auto'}">${UI.icon('trash')}</button>` : ''}
</div>
</div>`;
}
@ -596,6 +603,16 @@ window.Page_forum = (() => {
});
});
// Edit (owner)
container.querySelectorAll('.forum-post-edit:not([data-bound])').forEach(btn => {
btn.dataset.bound = '1';
btn.addEventListener('click', () => {
const postId = parseInt(btn.dataset.postId);
const currentText = btn.dataset.text || '';
_showEditPostModal(postId, currentText, container, threadId, uid, isMod);
});
});
// Delete
container.querySelectorAll('.forum-post-delete:not([data-bound])').forEach(btn => {
btn.dataset.bound = '1';
@ -995,6 +1012,78 @@ window.Page_forum = (() => {
});
}
// ----------------------------------------------------------
// Post bearbeiten (Besitzer)
// ----------------------------------------------------------
function _showEditPostModal(postId, currentText, container, threadId, uid, isMod) {
const id = 'forum-edit-post-form';
UI.modal.open({
title: 'Antwort bearbeiten',
body: `<form id="${id}">
<div class="form-group">
<textarea class="form-control" name="text" rows="5" required>${_esc(currentText)}</textarea>
</div>
</form>`,
footer: `
<button class="btn btn-ghost flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button type="submit" form="${id}" class="btn btn-primary flex-1">${UI.icon('floppy-disk')} Speichern</button>`,
});
document.getElementById(id)?.addEventListener('submit', async e => {
e.preventDefault();
const text = new FormData(e.target).get('text')?.trim();
if (!text) return;
const btn = document.querySelector(`[form="${id}"][type="submit"]`);
await UI.asyncButton(btn, async () => {
await API.forum.updatePost(postId, { text });
UI.modal.close();
// Post-Text im DOM aktualisieren
if (container) {
const postEl = container.querySelector(`[data-post-id="${postId}"]`);
const textEl = postEl?.querySelector('.forum-post-text');
if (textEl) textEl.textContent = text;
// data-text auf Edit-Button aktualisieren
const editBtn = postEl?.querySelector('.forum-post-edit');
if (editBtn) editBtn.dataset.text = text;
}
UI.toast.success('Antwort aktualisiert.');
});
});
}
// ----------------------------------------------------------
// Thread bearbeiten (Besitzer)
// ----------------------------------------------------------
function _showEditThreadModal(thread) {
const id = 'forum-edit-thread-form';
UI.modal.open({
title: 'Beitrag bearbeiten',
body: `<form id="${id}">
<div class="form-group">
<label class="form-label">Titel</label>
<input class="form-control" name="titel" value="${_esc(thread.titel || '')}" required>
</div>
<div class="form-group">
<label class="form-label">Text</label>
<textarea class="form-control" name="text" rows="6">${_esc(thread.text || '')}</textarea>
</div>
</form>`,
footer: `
<button class="btn btn-ghost flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button type="submit" form="${id}" class="btn btn-primary flex-1">${UI.icon('floppy-disk')} Speichern</button>`,
});
document.getElementById(id)?.addEventListener('submit', async e => {
e.preventDefault();
const fd = new FormData(e.target);
const btn = document.querySelector(`[form="${id}"][type="submit"]`);
await UI.asyncButton(btn, async () => {
await API.forum.updateThread(thread.id, { titel: fd.get('titel'), text: fd.get('text') });
UI.modal.close();
_loadThreads(true);
UI.toast.success('Beitrag aktualisiert.');
});
});
}
function openNew() {
if (!_appState?.user) { UI.toast.info('Bitte erst anmelden.'); return; }
_showCreateForm();

View file

@ -742,7 +742,8 @@ window.Page_routes = (() => {
<div style="display:flex;gap:var(--space-2);width:100%;flex-wrap:wrap">
<div style="display:flex;gap:var(--space-2);flex:1">
<button type="button" class="btn btn-secondary btn-sm" id="rd-gpx" title="GPX herunterladen">${UI.icon('download-simple')} GPX</button>
<button type="button" class="btn btn-secondary btn-sm" id="rd-share" title="Route teilen">${UI.icon('share')}</button>
<button type="button" class="btn btn-secondary btn-sm" id="rd-share" title="Route teilen">${UI.icon('arrow-square-out')} Teilen</button>
<button type="button" class="btn btn-secondary btn-sm" id="rd-navi" title="Mit Navi öffnen">${UI.icon('map-pin')} Navi</button>
<button type="button" class="btn btn-secondary btn-sm" id="rd-send-friend" title="An Freund senden">${UI.icon('chat-circle-dots')}</button>
${isOwn ? `
<button type="button" class="btn btn-secondary btn-sm" id="rd-vis">
@ -768,19 +769,30 @@ window.Page_routes = (() => {
document.getElementById('rd-gpx')?.addEventListener('click', () => _downloadGpxDirect(route));
// Teilen-Button
document.getElementById('rd-share')?.addEventListener('click', () => {
document.getElementById('rd-share')?.addEventListener('click', async () => {
const shareUrl = location.origin + '/#routes?id=' + route.id;
const text = `${route.name}${(route.distanz_km||0).toFixed(1)} km`;
if (navigator.share) {
navigator.share({ title: route.name, url: shareUrl }).catch(() => {});
navigator.share({ title: route.name, text, url: shareUrl }).catch(() => {});
} else {
navigator.clipboard.writeText(shareUrl).then(() => {
UI.toast.success('Link kopiert!');
}).catch(() => {
UI.toast.error('Link konnte nicht kopiert werden.');
});
await navigator.clipboard.writeText(shareUrl);
UI.toast.info('Link kopiert!');
}
});
// Navi-Button
document.getElementById('rd-navi')?.addEventListener('click', () => {
const track = route.gps_track || [];
if (track.length < 2) { UI.toast.warning('Keine GPS-Daten vorhanden.'); return; }
const start = track[0];
const end = track[track.length - 1];
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
const url = isIOS
? `maps://maps.apple.com/?saddr=${start.lat},${start.lon}&daddr=${end.lat},${end.lon}&dirflg=w`
: `https://www.google.com/maps/dir/?api=1&origin=${start.lat},${start.lon}&destination=${end.lat},${end.lon}&travelmode=walking`;
window.open(url, '_blank');
});
// An Freund senden
document.getElementById('rd-send-friend')?.addEventListener('click', () => _openSendToFriendModal(route));