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

@ -37,6 +37,13 @@ class ThreadPatch(BaseModel):
is_pinned: Optional[int] = None
is_locked: Optional[int] = None
class ThreadUpdate(BaseModel):
titel: Optional[str] = None
text: Optional[str] = None
class PostUpdate(BaseModel):
text: str
class LikeBody(BaseModel):
target_type: str # 'thread' | 'post'
target_id: int
@ -330,6 +337,38 @@ async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current
return pd
# ------------------------------------------------------------------
# PATCH /api/forum/threads/{id}/content — Besitzer: titel/text
# ------------------------------------------------------------------
@router.patch("/threads/{thread_id}/content")
async def update_thread_content(thread_id: int, data: ThreadUpdate, user=Depends(get_current_user)):
with db() as conn:
t = conn.execute("SELECT user_id FROM forum_threads WHERE id=?", (thread_id,)).fetchone()
if not t: raise HTTPException(404, "Thread nicht gefunden.")
if t["user_id"] != user["id"]: raise HTTPException(403, "Nicht dein Beitrag.")
updates, vals = [], []
if data.titel is not None and data.titel.strip():
updates.append("titel=?"); vals.append(data.titel.strip())
if data.text is not None:
updates.append("text=?"); vals.append(data.text.strip())
if not updates: return {"ok": True}
conn.execute(f"UPDATE forum_threads SET {','.join(updates)} WHERE id=?", (*vals, thread_id))
return {"ok": True}
# ------------------------------------------------------------------
# PATCH /api/forum/posts/{id} — Besitzer: text
# ------------------------------------------------------------------
@router.patch("/posts/{post_id}")
async def update_post(post_id: int, data: PostUpdate, user=Depends(get_current_user)):
with db() as conn:
p = conn.execute("SELECT user_id FROM forum_posts WHERE id=?", (post_id,)).fetchone()
if not p: raise HTTPException(404, "Beitrag nicht gefunden.")
if p["user_id"] != user["id"]: raise HTTPException(403, "Nicht dein Beitrag.")
conn.execute("UPDATE forum_posts SET text=? WHERE id=?", (data.text.strip(), post_id))
return {"ok": True}
# ------------------------------------------------------------------
# DELETE /api/forum/posts/{id}
# ------------------------------------------------------------------

View file

@ -235,6 +235,7 @@
justify-content: center;
padding: 0 3px;
border: 2px solid var(--c-surface);
pointer-events: none;
}
.nav-item-label {

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

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v237';
const CACHE_VERSION = 'by-v238';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten