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:
parent
289158b2cd
commit
7a25ccae90
8 changed files with 169 additions and 11 deletions
|
|
@ -235,6 +235,7 @@
|
|||
justify-content: center;
|
||||
padding: 0 3px;
|
||||
border: 2px solid var(--c-surface);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-item-label {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -446,9 +446,15 @@ window.Page_chat = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await _loadList();
|
||||
await _updateChatBadge();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
return {
|
||||
init,
|
||||
refresh,
|
||||
_showList,
|
||||
_openThread,
|
||||
_send,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue