Fix: alle funktionalen Inline-Event-Handler → addEventListener/Delegation (von CSP-Härtung 65cfa25 app-weit blockiert)

Chat (senden/öffnen/löschen/Foto), Tagebuch-Buch, KI-Berichte, Wiki-Moderation,
Events-Detail, Walks-Lightbox, Routen-Foto, Navigations-CTAs (data-page),
Presse-Copy + Züchter-Landing (externes JS). 35x UI.modal.close → data-modal-close,
28x totes event.stopPropagation entfernt. Verbleibend: kosmetische onerror/Hover. SW v1164
This commit is contained in:
rene 2026-06-04 13:59:27 +02:00
parent 152fde716c
commit 2ddd8ac350
34 changed files with 228 additions and 173 deletions

View file

@ -1 +1 @@
1163
1164

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1163"></script>
<script src="/js/boot-early.js?v=1164"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1163">
<link rel="stylesheet" href="/css/layout.css?v=1163">
<link rel="stylesheet" href="/css/components.css?v=1163">
<link rel="stylesheet" href="/css/utilities.css?v=1163">
<link rel="stylesheet" href="/css/lists.css?v=1163">
<link rel="stylesheet" href="/css/design-system.css?v=1164">
<link rel="stylesheet" href="/css/layout.css?v=1164">
<link rel="stylesheet" href="/css/components.css?v=1164">
<link rel="stylesheet" href="/css/utilities.css?v=1164">
<link rel="stylesheet" href="/css/lists.css?v=1164">
</head>
<body>
@ -617,11 +617,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1163"></script>
<script src="/js/ui.js?v=1163"></script>
<script src="/js/app.js?v=1163"></script>
<script src="/js/worlds.js?v=1163"></script>
<script src="/js/offline-indicator.js?v=1163"></script>
<script src="/js/api.js?v=1164"></script>
<script src="/js/ui.js?v=1164"></script>
<script src="/js/app.js?v=1164"></script>
<script src="/js/worlds.js?v=1164"></script>
<script src="/js/offline-indicator.js?v=1164"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -631,7 +631,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1163"></script>
<script src="/js/boot.js?v=1164"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1163'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1164'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;
@ -443,6 +443,20 @@ const App = (() => {
return;
}
// Foto-Lightbox (Inline-onclick ist CSP-blockiert)
const lb = e.target.closest('[data-lightbox-url]');
if (lb) {
window.UI?.lightbox?.show?.([{ url: lb.dataset.lightboxUrl }], 0);
return;
}
// Externer Link in neuem Tab
const ol = e.target.closest('[data-open-url]');
if (ol) {
window.open(ol.dataset.openUrl, '_blank');
return;
}
// Header-User-Button → Settings
if (e.target.closest('#header-user-btn')) {
navigate('settings');
@ -1164,7 +1178,7 @@ const App = (() => {
icon: UI.icon(icon),
title: 'Anmelden erforderlich',
text,
action: `<button class="btn btn-primary" onclick="App.navigate('settings')">Anmelden</button>`,
action: `<button class="btn btn-primary" data-page="settings">Anmelden</button>`,
});
}

View file

@ -2661,7 +2661,7 @@ window.Page_admin = (() => {
background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-3);max-height:60vh;overflow-y:auto;
color:var(--c-text)">${UI.escape(l.body || '(kein Text gespeichert)')}</pre>`,
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
});
});
@ -2760,7 +2760,7 @@ window.Page_admin = (() => {
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" form="${id}" type="submit">Speichern</button>`,
});
@ -2993,7 +2993,7 @@ window.Page_admin = (() => {
</div>
</div>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
<button class="btn btn-secondary" data-modal-close>Schließen</button>
<button class="btn btn-primary" id="adm-bew-save-note">Notiz speichern</button>`,
});
document.getElementById('adm-bew-save-note')?.addEventListener('click', async () => {

View file

@ -780,7 +780,7 @@ window.Page_adoption = (() => {
<button type="submit" form="adp-interest-form" class="btn btn-primary flex-1" id="adp-interest-submit">
${UI.icon('heart')} Interesse bekunden
</button>
<button type="button" class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
</div>
`;
@ -879,7 +879,7 @@ window.Page_adoption = (() => {
<button type="submit" form="adp-create-form" class="btn btn-primary w-full" id="adp-create-submit">
${UI.icon('plus')} Inserat erstellen
</button>
<button type="button" class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
</div>
`;

View file

@ -18,6 +18,23 @@ window.Page_chat = (() => {
_container = container;
_myId = appState?.user?.id || null;
// Delegierter Click-Handler — Inline-onclick wird von der CSP blockiert.
if (!_container._chatClickBound) {
_container.addEventListener('click', e => {
const t = e.target.closest('[data-chat-action]');
if (!t) return;
switch (t.dataset.chatAction) {
case 'open': _openThread(parseInt(t.dataset.chatId, 10)); break;
case 'list': _showList(); break;
case 'photo': document.getElementById('chat-photo-input')?.click(); break;
case 'send': _send(); break;
case 'delete': _deleteMsg(parseInt(t.dataset.chatId, 10)); break;
case 'img': window.open(t.dataset.chatUrl, '_blank'); break;
}
});
_container._chatClickBound = true;
}
// Heartbeat: alle 30s online-Status senden
API.chat.heartbeat().catch(() => {});
_heartbeatTimer = setInterval(() => {
@ -132,7 +149,7 @@ window.Page_chat = (() => {
? `<span class="online-dot" title="Online"></span>`
: '';
return `
<div class="chat-conv-item" onclick="Page_chat._openThread(${c.id})">
<div class="chat-conv-item" data-chat-action="open" data-chat-id="${c.id}">
<div style="position:relative;flex-shrink:0">
<div class="chat-conv-avatar">${initials}</div>
${onlineDot ? `<span class="online-dot chat-avatar-dot"></span>` : ''}
@ -166,14 +183,14 @@ window.Page_chat = (() => {
// Aktive Markierung in der Liste
document.querySelectorAll('.chat-conv-item').forEach(el =>
el.classList.toggle('active', el.getAttribute('onclick')?.includes(String(convId)))
el.classList.toggle('active', el.dataset.chatId === String(convId))
);
const threadHTML = `
<div class="chat-thread" id="chat-thread">
<div class="chat-thread-header">
${_isDesktop() ? '' : `
<button class="btn btn-ghost btn-sm" onclick="Page_chat._showList()" style="padding:var(--space-1)">
<button class="btn btn-ghost btn-sm" data-chat-action="list" style="padding:var(--space-1)">
<svg class="ph-icon"><use href="/icons/phosphor.svg#arrow-left"></use></svg>
</button>`}
<div style="position:relative;flex-shrink:0">
@ -188,14 +205,13 @@ window.Page_chat = (() => {
</div>
</div>
<div class="chat-input-bar">
<input type="file" id="chat-photo-input" accept="image/*" class="hidden"
onchange="Page_chat._onPhotoSelected(this)">
<button class="chat-photo-btn" onclick="document.getElementById('chat-photo-input').click()" title="Foto senden">
<input type="file" id="chat-photo-input" accept="image/*" class="hidden">
<button class="chat-photo-btn" data-chat-action="photo" title="Foto senden">
<svg class="ph-icon"><use href="/icons/phosphor.svg#camera"></use></svg>
</button>
<textarea id="chat-input" class="chat-input" rows="1"
placeholder="Nachricht…" maxlength="2000"></textarea>
<button class="chat-send-btn" id="chat-send-btn" onclick="Page_chat._send()">
<button class="chat-send-btn" id="chat-send-btn" data-chat-action="send">
<svg class="ph-icon"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
</button>
</div>
@ -219,9 +235,11 @@ window.Page_chat = (() => {
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
Page_chat._send();
_send();
}
});
document.getElementById('chat-photo-input')
?.addEventListener('change', e => _onPhotoSelected(e.target));
await _loadMessages(true);
await API.chat.markRead(_convId).catch(() => {});
@ -313,7 +331,7 @@ window.Page_chat = (() => {
const timeStr = _fmtTime(m.created_at);
const deleteBtn = isMine && !m.is_deleted
? `<button class="btn btn-ghost" style="padding:2px;opacity:0.4;font-size:var(--text-xs)"
onclick="Page_chat._deleteMsg(${m.id})" title="Löschen">
data-chat-action="delete" data-chat-id="${m.id}" title="Löschen">
<svg class="ph-icon" style="width:12px;height:12px"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>`
: '';
@ -328,7 +346,7 @@ window.Page_chat = (() => {
// Medieninhalt
let bubbleContent = '';
if (m.media_url) {
bubbleContent += `<img src="${UI.escape(m.media_url)}" class="chat-bubble-img" alt="Foto" onclick="window.open('${UI.escape(m.media_url)}','_blank')">`;
bubbleContent += `<img src="${UI.escape(m.media_url)}" class="chat-bubble-img" alt="Foto" data-chat-action="img" data-chat-url="${UI.escape(m.media_url)}">`;
}
if (m.text) {
bubbleContent += (m.media_url ? `<div style="margin-top:var(--space-1)">` : '') +

View file

@ -1113,8 +1113,7 @@ window.Page_diary = (() => {
<span class="diary-detail-date-center">${datumLang}</span>
<div style="display:flex;align-items:center;gap:4px">
${!_appState?.activeDog?.is_guest
? `<button id="diary-dv-note" class="btn btn-ghost btn-xs" title="Notiz"
onclick="event.stopPropagation()">
? `<button id="diary-dv-note" class="btn btn-ghost btn-xs" title="Notiz">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
</button>
<button id="diary-dv-edit" class="diary-detail-edit">
@ -1722,7 +1721,7 @@ window.Page_diary = (() => {
<div id="import-result" style="display:none;margin-top:var(--space-4)"></div>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
<button class="btn btn-secondary" data-modal-close>Schließen</button>
<button class="btn btn-primary" id="import-start-btn">Importieren</button>`,
});

View file

@ -616,7 +616,7 @@ window.Page_dog_profile = (() => {
footer: `
<div class="w3-btn-stack">
<button class="btn btn-primary" id="chip-edit-save-btn" class="w-full">Speichern</button>
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
</div>`,
});
document.getElementById('chip-edit-save-btn').addEventListener('click', async () => {
@ -675,7 +675,7 @@ window.Page_dog_profile = (() => {
${hasPhoto ? `<button class="btn btn-primary" id="pe-save-btn" class="w-full">Speichern</button>` : ''}
<div class="flex-gap-2">
${hasPhoto ? `<button class="btn btn-danger" id="pe-delete-btn">${UI.icon('trash')} Löschen</button>` : ''}
<button class="btn btn-secondary flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary flex-1" data-modal-close>Abbrechen</button>
</div>
</div>
`;
@ -957,7 +957,7 @@ window.Page_dog_profile = (() => {
</div>
<div id="share-list-wrap" class="mt-4"></div>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
<button class="btn btn-secondary" data-modal-close>Schließen</button>
<button class="btn btn-primary" id="share-create-btn">Link erstellen</button>`,
});
@ -1489,7 +1489,7 @@ window.Page_dog_profile = (() => {
</p>
${data.hinweis ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-3)">${UI.escape(data.hinweis)}</p>` : ''}
</div>`,
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
@ -1608,7 +1608,7 @@ window.Page_dog_profile = (() => {
</div>`,
footer: `
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;justify-content:flex-end">
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
<button class="btn btn-secondary" data-modal-close>Schließen</button>
<a class="btn btn-secondary" href="/ausweis/${dog.id}" target="_blank" rel="noopener">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg>
Ausweis öffnen
@ -1832,7 +1832,7 @@ window.Page_dog_profile = (() => {
</div>`,
footer: `
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="pp-meta-save">Speichern</button>
</div>`,
});
@ -1896,7 +1896,7 @@ window.Page_dog_profile = (() => {
</div>`,
footer: `
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="pp-vacc-save">Speichern</button>
</div>`,
});
@ -1960,7 +1960,7 @@ window.Page_dog_profile = (() => {
</div>`,
footer: `
<div style="display:flex;gap:var(--space-2);justify-content:flex-end">
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="pp-med-save">Speichern</button>
</div>`,
});
@ -2017,7 +2017,7 @@ window.Page_dog_profile = (() => {
UI.modal.open({
title: 'Hundepass-Link teilen',
body: shareWrap.innerHTML,
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
document.getElementById('pp-sharelink-copy')?.addEventListener('click', async () => {
await navigator.clipboard.writeText(url).catch(() => {});
@ -2209,7 +2209,7 @@ window.Page_dog_profile = (() => {
? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;'
: 'background:#f5f0e8;color:#444;border-color:#e0d4b8;';
const label = y === 'alle' ? 'Alle' : y;
return `<button onclick="window._buchSetJahr('${y}')" style="
return `<button data-buch-action="year" data-buch-year="${y}" style="
border:1px solid;border-radius:8px;padding:8px 16px;
font-size:0.9rem;cursor:pointer;font-family:inherit;
${active}
@ -2239,14 +2239,14 @@ window.Page_dog_profile = (() => {
<div style="margin-bottom:20px;display:flex;flex-direction:column;gap:10px">
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
<button onclick="window._buchToggleFotos()" style="
<button data-buch-action="fotos" style="
width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
${togStyle(nurFotos)}
">${nurFotos ? '✓' : ''}</button>
<span style="font-size:0.95rem">Nur Einträge mit Fotos</span>
</label>
<label style="display:flex;align-items:center;gap:12px;cursor:pointer">
<button onclick="window._buchToggleMeilensteine()" style="
<button data-buch-action="meilen" style="
width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
${togStyle(nurMeilensteine)}
">${nurMeilensteine ? '✓' : ''}</button>
@ -2255,11 +2255,11 @@ window.Page_dog_profile = (() => {
</div>
<div style="display:flex;gap:10px">
<button onclick="window._buchOpen()" style="
<button data-buch-action="open" style="
flex:1;background:#7a4f1a;color:#f5e4c0;border:none;border-radius:10px;
padding:14px;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;
">📖 Buch öffnen</button>
<button onclick="window._buchClose()" style="
<button data-buch-action="close" style="
background:#f0f0f0;color:#555;border:none;border-radius:10px;
padding:14px 18px;font-size:1rem;cursor:pointer;font-family:inherit;
"></button>
@ -2268,18 +2268,14 @@ window.Page_dog_profile = (() => {
`;
};
window._buchSetJahr = (y) => { selectedJahr = y; renderModal(); };
window._buchToggleFotos = () => { nurFotos = !nurFotos; renderModal(); };
window._buchToggleMeilensteine = () => { nurMeilensteine = !nurMeilensteine; renderModal(); };
window._buchClose = () => {
const setJahr = (y) => { selectedJahr = y; renderModal(); };
const toggleFotos = () => { nurFotos = !nurFotos; renderModal(); };
const toggleMeilen = () => { nurMeilensteine = !nurMeilensteine; renderModal(); };
const closeModal = () => {
modalEl.remove();
delete window._buchSetJahr;
delete window._buchToggleFotos;
delete window._buchToggleMeilensteine;
delete window._buchOpen;
delete window._buchClose;
document.removeEventListener('keydown', onKey);
};
window._buchOpen = () => {
const openBuch = () => {
const params = new URLSearchParams();
if (selectedJahr !== 'alle') params.set('jahr', selectedJahr);
if (nurFotos) params.set('nur_fotos', 'true');
@ -2290,10 +2286,24 @@ window.Page_dog_profile = (() => {
renderModal();
document.body.appendChild(modalEl);
modalEl.addEventListener('click', e => { if (e.target === modalEl) window._buchClose(); });
// Delegierter Click-Handler (Inline-onclick wird von der CSP blockiert);
// überlebt das Re-Rendern via renderModal().
modalEl.addEventListener('click', e => {
if (e.target === modalEl) { closeModal(); return; }
const btn = e.target.closest('[data-buch-action]');
if (!btn) return;
switch (btn.dataset.buchAction) {
case 'year': setJahr(btn.dataset.buchYear); break;
case 'fotos': toggleFotos(); break;
case 'meilen': toggleMeilen(); break;
case 'open': openBuch(); break;
case 'close': closeModal(); break;
}
});
const onKey = e => {
if (e.key === 'Escape') { window._buchClose(); document.removeEventListener('keydown', onKey); }
if (e.key === 'Escape') { closeModal(); }
};
document.addEventListener('keydown', onKey);
}
@ -2310,7 +2320,7 @@ window.Page_dog_profile = (() => {
<use href="/icons/phosphor.svg#spinner-gap"></use>
</svg>
</div>`,
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
size: 'large',
});

View file

@ -53,7 +53,7 @@ window.Page_ernaehrung = (() => {
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bowl-food"></use></svg>',
title: 'Noch kein Hund angelegt',
text: 'Erstelle zuerst ein Hundeprofil.',
action: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Profil erstellen</button>`,
action: `<button class="btn btn-primary" data-page="dog-profile">Profil erstellen</button>`,
});
return;
}
@ -728,7 +728,7 @@ window.Page_ernaehrung = (() => {
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="vert-futter-save-btn" form="${id}">Speichern</button>
`;
UI.modal.open({ title: 'Futter erfassen', body, footer });
@ -840,7 +840,7 @@ window.Page_ernaehrung = (() => {
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="vert-reaktion-save-btn" form="${id}">Speichern</button>
`;
UI.modal.open({ title: 'Reaktion erfassen', body, footer });

View file

@ -221,17 +221,17 @@ window.Page_events = (() => {
</div>
${ev.rsvp_count ? `<span class="event-attendees" data-ev-attendees="${ev.id}">${_icon('users')} ${ev.rsvp_count} nehmen teil</span>` : ''}
${ev.link ? `<div class="events-card-actions">
<a class="btn btn-ghost btn-xs ev-ext-link" href="${UI.escape(ev.link)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">
<a class="btn btn-ghost btn-xs ev-ext-link" href="${UI.escape(ev.link)}" target="_blank" rel="noopener">
${_icon('arrow-square-out')} Details
</a>
</div>` : ''}
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-1)">
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten" onclick="event.stopPropagation()">${_icon('pencil-simple')}</button>` : ''}
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten">${_icon('pencil-simple')}</button>` : ''}
${_state.user ? `<button class="btn-icon ev-note-btn" data-ev-note-id="${ev.id}"
data-ev-note-label="${UI.escape(ev.titel + ' ' + ev.datum)}"
data-ev-note-ort="${UI.escape(ev.ort_name || '')}"
title="Notiz" class="text-muted" onclick="event.stopPropagation()">
title="Notiz" class="text-muted">
${_icon('note-pencil')}</button>` : ''}
</div>
</div>
@ -276,7 +276,7 @@ window.Page_events = (() => {
<span style="color:var(--c-text-muted);font-size:12px">${datum}</span><br>
${ev.ort_name ? `<span style="font-size:12px">📍 ${UI.escape(ev.ort_name)}</span><br>` : ''}
${ev.beschreibung ? `<span style="font-size:12px">${UI.escape(ev.beschreibung.slice(0, 80))}${ev.beschreibung.length > 80 ? '…' : ''}</span><br>` : ''}
<a href="#" onclick="event.preventDefault();Page_events._openDetail(${ev.id})"
<a href="#" data-ev-detail="${ev.id}"
style="font-size:12px;color:var(--c-primary,#2563eb)">Details</a>
</div>
`;
@ -512,7 +512,7 @@ window.Page_events = (() => {
</button>
<div class="flex-gap-2">
${isEdit ? `<button type="button" class="btn btn-danger" id="ev-form-delete">Löschen</button>` : ''}
<button class="btn btn-secondary flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary flex-1" data-modal-close>Abbrechen</button>
</div>
</div>
`;
@ -570,6 +570,14 @@ window.Page_events = (() => {
// Click-Handler
// ----------------------------------------------------------
function _onClick(e) {
// Detail-Link (Karten-Popup) — Inline-onclick ist CSP-blockiert
const detailLink = e.target.closest('[data-ev-detail]');
if (detailLink) {
e.preventDefault();
_showDetail(parseInt(detailLink.dataset.evDetail, 10));
return;
}
// Quelle-Filter
const sourceBtn = e.target.closest('[data-ev-quelle]');
if (sourceBtn) {

View file

@ -485,7 +485,7 @@ window.Page_expenses = (() => {
</form>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button type="button" class="btn btn-secondary flex-1" data-modal-close>Abbrechen</button>
<button type="submit" form="exp-recurring-form" class="btn btn-primary flex-1">Speichern</button>`;
UI.modal.open({ title: r ? 'Dauerauftrag bearbeiten' : 'Neuer Dauerauftrag', body, footer });
@ -755,10 +755,10 @@ window.Page_expenses = (() => {
style="color:var(--c-danger);margin-right:auto">
${UI.icon('trash')}
</button>
<button type="button" class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button type="submit" form="${formId}" class="btn btn-primary">Speichern</button>
` : `
<button type="button" class="btn btn-secondary flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button type="button" class="btn btn-secondary flex-1" data-modal-close>Abbrechen</button>
<button type="submit" form="${formId}" class="btn btn-primary flex-1">Speichern</button>
`;

View file

@ -295,7 +295,7 @@ function _fmtDate(iso) {
</div>`;
UI.modal.open({ title: '🏆 Hund des Monats', body,
footer: `<button class="btn btn-secondary flex-1" onclick="UI.modal.close()">Schließen</button>` });
footer: `<button class="btn btn-secondary flex-1" data-modal-close>Schließen</button>` });
document.getElementById('hdm-login-link')?.addEventListener('click', e => {
e.preventDefault(); UI.modal.close(); App.navigate('settings');
@ -1022,7 +1022,7 @@ function _fmtDate(iso) {
</p>
</div>`,
footer: `<button class="btn btn-primary flex-1" onclick="UI.modal.close()">Verstanden</button>`,
footer: `<button class="btn btn-primary flex-1" data-modal-close>Verstanden</button>`,
});
}
@ -1376,7 +1376,7 @@ function _fmtDate(iso) {
</div>
</form>`,
footer: `
<button class="btn btn-ghost flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-ghost flex-1" data-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 => {
@ -1423,7 +1423,7 @@ function _fmtDate(iso) {
</div>
</form>`,
footer: `
<button class="btn btn-ghost flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-ghost flex-1" data-modal-close>Abbrechen</button>
<button type="submit" form="${id}" class="btn btn-primary flex-1">${UI.icon('floppy-disk')} Speichern</button>`,
});

View file

@ -27,7 +27,7 @@ window.Page_friends = (() => {
icon: UI.icon('users'),
title: 'Anmelden erforderlich',
text: 'Melde dich an, um Freunde zu finden und Anfragen zu verwalten.',
action: `<button class="btn btn-primary" onclick="App.navigate('settings')">Anmelden</button>`,
action: `<button class="btn btn-primary" data-page="settings">Anmelden</button>`,
});
return;
}
@ -524,8 +524,7 @@ window.Page_friends = (() => {
<button class="btn btn-ghost btn-sm fr-note-btn"
data-fr-note-id="${f.friend_id}"
data-fr-note-name="${UI.escape(f.friend_name)}"
title="Notiz"
onclick="event.stopPropagation()">
title="Notiz">
<svg class="ph-icon"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
</button>
<button class="btn btn-ghost btn-sm"

View file

@ -83,7 +83,7 @@ window.Page_health = (() => {
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>',
title: 'Noch kein Hund angelegt',
text: 'Erstelle zuerst ein Hundeprofil.',
action: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Profil erstellen</button>`,
action: `<button class="btn btn-primary" data-page="dog-profile">Profil erstellen</button>`,
});
return;
}
@ -403,8 +403,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`;
@ -511,8 +510,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`;
@ -563,8 +561,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text" style="padding-top:var(--space-1)">${UI.escape(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-1);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="Gewicht ${UI.escape(e.datum)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
data-label="Gewicht ${UI.escape(e.datum)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`).join('');
@ -801,8 +798,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="Läufigkeit ${UI.escape(e.datum)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
data-label="Läufigkeit ${UI.escape(e.datum)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>`;
}).join('');
@ -839,8 +835,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`).join('')}
@ -880,8 +875,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div>
</div>
`).join('');
@ -923,19 +917,16 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-muted);padding:2px 6px"
data-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}"
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
${count
? `<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);align-items:center;flex-wrap:wrap">
${mediaList.slice(0, 3).map(m => m.media_type === 'pdf'
? `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm" style="display:inline-flex"
onclick="event.stopPropagation()">
class="btn btn-secondary btn-sm" style="display:inline-flex">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF
</a>`
: `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm" style="display:inline-flex"
onclick="event.stopPropagation()">
class="btn btn-secondary btn-sm" style="display:inline-flex">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg> Bild
</a>`
).join('')}
@ -1773,28 +1764,24 @@ window.Page_health = (() => {
${ratingHtml}
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap">
${p.telefon ? `
<a href="tel:${UI.escape(p.telefon)}" class="btn btn-secondary btn-sm"
onclick="event.stopPropagation()">
<a href="tel:${UI.escape(p.telefon)}" class="btn btn-secondary btn-sm">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> Anrufen
</a>` : ''}
${p.notfall_telefon ? `
<a href="tel:${UI.escape(p.notfall_telefon)}" class="btn btn-danger btn-sm"
onclick="event.stopPropagation()">
<a href="tel:${UI.escape(p.notfall_telefon)}" class="btn btn-danger btn-sm">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall
</a>` : ''}
<button class="btn btn-sm btn-secondary"
data-action="bewerten" data-praxis-id="${p.id}"
title="Bewertung abgeben"
style="flex-shrink:0"
onclick="event.stopPropagation()">
style="flex-shrink:0">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg>
Bewerten
</button>
<button class="btn btn-sm ${isFav ? 'btn-primary' : 'btn-secondary'}"
data-action="toggle-fav" data-praxis-id="${p.id}"
title="${isFav ? 'Favorit entfernen' : 'Als mein Tierarzt merken'}"
style="flex-shrink:0"
onclick="event.stopPropagation()">
style="flex-shrink:0">
<svg class="ph-icon" aria-hidden="true">
<use href="/icons/phosphor.svg#${isFav ? 'heart-fill' : 'heart'}"></use>
</svg>
@ -1803,8 +1790,7 @@ window.Page_health = (() => {
<button class="btn btn-sm btn-secondary"
data-action="edit-praxis" data-praxis-id="${p.id}"
title="Praxis bearbeiten"
style="flex-shrink:0"
onclick="event.stopPropagation()">
style="flex-shrink:0">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pencil-simple"></use></svg>
</button>
</div>
@ -1881,7 +1867,7 @@ window.Page_health = (() => {
<use href="/icons/phosphor.svg#spinner-gap"></use>
</svg>
</div>`,
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>
<button class="btn btn-primary" id="detail-bewerten-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg>
Jetzt bewerten
@ -1998,7 +1984,7 @@ window.Page_health = (() => {
title: `${UI.escape(praxis.name)} bewerten`,
body,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="bew-submit-btn" form="bew-form">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg>
${existing ? 'Bewertung aktualisieren' : 'Bewertung abgeben'}
@ -2373,7 +2359,7 @@ window.Page_health = (() => {
value="${UI.escape(currentNr)}" placeholder="z.B. 276009200123456" maxlength="20">
</div>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="transponder-save-btn">Speichern</button>`,
});
document.getElementById('transponder-save-btn').addEventListener('click', async () => {
@ -2441,11 +2427,11 @@ window.Page_health = (() => {
const b = berichte[idx];
const nav = berichte.length > 1 ? `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<button onclick="window._kiPrev()" style="padding:6px 16px;border-radius:999px;
<button data-ki-nav="prev" style="padding:6px 16px;border-radius:999px;
border:1.5px solid var(--c-border);background:var(--c-surface);cursor:pointer;
font-size:var(--text-sm);${idx >= berichte.length-1 ? 'opacity:.3;pointer-events:none' : ''}"> Älter</button>
<span class="text-xs-muted">${idx+1} / ${berichte.length}</span>
<button onclick="window._kiNext()" style="padding:6px 16px;border-radius:999px;
<button data-ki-nav="next" style="padding:6px 16px;border-radius:999px;
border:1.5px solid var(--c-border);background:var(--c-surface);cursor:pointer;
font-size:var(--text-sm);${idx <= 0 ? 'opacity:.3;pointer-events:none' : ''}">Neuer </button>
</div>` : '';
@ -2455,10 +2441,13 @@ window.Page_health = (() => {
<div style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin-bottom:8px">${fmtDate(b)}</div>
<div style="white-space:pre-wrap;line-height:1.7;font-size:var(--text-sm)">${UI.escape(b.bericht)}</div>`,
});
// Inline-onclick wird von der CSP blockiert → per addEventListener verdrahten.
document.querySelector('[data-ki-nav="prev"]')
?.addEventListener('click', () => { if (idx < berichte.length - 1) { idx++; showBericht(); } });
document.querySelector('[data-ki-nav="next"]')
?.addEventListener('click', () => { if (idx > 0) { idx--; showBericht(); } });
}
window._kiPrev = () => { if (idx < berichte.length - 1) { idx++; showBericht(); } };
window._kiNext = () => { if (idx > 0) { idx--; showBericht(); } };
showBericht();
});
} catch (_) {
@ -2620,15 +2609,13 @@ window.Page_health = (() => {
${adresse ? `<div class="list-item-meta-row">${UI.escape(adresse)}</div>` : ''}
${vet.telefon ? `
<div class="mt-2">
<a href="tel:${UI.escape(vet.telefon)}" class="btn btn-secondary btn-sm"
onclick="event.stopPropagation()">
<a href="tel:${UI.escape(vet.telefon)}" class="btn btn-secondary btn-sm">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> ${UI.escape(vet.telefon)}
</a>
</div>` : ''}
${vet.notfall_telefon ? `
<div style="margin-top:var(--space-1)">
<a href="tel:${UI.escape(vet.notfall_telefon)}" class="btn btn-danger btn-sm"
onclick="event.stopPropagation()">
<a href="tel:${UI.escape(vet.notfall_telefon)}" class="btn btn-danger btn-sm">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall: ${UI.escape(vet.notfall_telefon)}
</a>
</div>` : ''}
@ -2718,14 +2705,13 @@ window.Page_health = (() => {
${doc.beschreibung ? `<div class="list-item-text">${UI.escape(doc.beschreibung)}</div>` : ''}
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap">
<a href="${UI.escape(doc.file_path)}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm" onclick="event.stopPropagation()">
class="btn btn-secondary btn-sm">
${isImg
? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg> Bild öffnen'
: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF öffnen'}
</a>
<button class="btn btn-ghost btn-xs text-danger"
data-action="delete-hdoc" data-doc-id="${doc.id}"
onclick="event.stopPropagation()">
data-action="delete-hdoc" data-doc-id="${doc.id}">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>
</div>
@ -3007,7 +2993,7 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) {
Bei ernsthaften oder sich verschlechternden Symptomen sofort zum Tierarzt.
</div>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
<button class="btn btn-secondary" data-modal-close>Schließen</button>
<button class="btn btn-primary" id="ki-tierarzt-submit-btn">Frage stellen</button>`,
});
@ -3233,7 +3219,7 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) {
</div>
</form>`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="ins-save-btn" form="${id}">Speichern</button>`;
UI.modal.open({ title: existing ? 'Versicherung bearbeiten' : 'Versicherung eintragen', body, footer });
setTimeout(() => {
@ -3385,7 +3371,7 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) {
</div>
</form>`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="beh-save-btn" form="${id}">Speichern</button>`;
UI.modal.open({ title: 'Verhalten erfassen', body, footer });
setTimeout(() => {

View file

@ -394,7 +394,7 @@ window.Page_laeufi = (() => {
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" form="laeufi-form" type="submit">${isEdit ? 'Speichern' : 'Eintragen'}</button>`,
});
document.getElementById('laeufi-form').addEventListener('submit', async e => {
@ -472,7 +472,7 @@ window.Page_laeufi = (() => {
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" form="deck-form" type="submit">${isEdit ? 'Speichern' : 'Eintragen'}</button>`,
});
document.getElementById('deck-form').addEventListener('submit', async e => {
@ -505,7 +505,7 @@ window.Page_laeufi = (() => {
title: `Progesterontests — ${_fmtDate(laeufi.beginn)}`,
body: `<div id="prog-modal-content"><p class="text-muted">Lädt…</p></div>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
<button class="btn btn-secondary" data-modal-close>Schließen</button>
<button class="btn btn-primary" id="prog-add-btn">${UI.icon('plus')} Test eintragen</button>`,
});
await _loadProgContent(laeufi.id);
@ -602,7 +602,7 @@ window.Page_laeufi = (() => {
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" form="prog-form" type="submit">Eintragen</button>`,
});
document.getElementById('prog-form').addEventListener('submit', async e => {

View file

@ -867,7 +867,7 @@ window.Page_litters = (() => {
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" form="wl-form" type="submit">${isEdit ? 'Speichern' : 'Eintragen'}</button>`,
});

View file

@ -410,7 +410,6 @@ window.Page_lost = (() => {
<span style="font-size:10px;color:var(--c-warning,#d97706);font-weight:600"> Sync ausstehend</span>
<button class="btn btn-ghost btn-xs lost-discard-btn"
data-pending-id="${r.id}"
onclick="event.stopPropagation()"
style="color:var(--c-danger,#dc2626)">
🗑 Verwerfen
</button>
@ -419,7 +418,7 @@ window.Page_lost = (() => {
<button class="btn btn-ghost btn-xs lost-note-btn"
data-lost-note-id="${r.id}"
data-lost-note-name="${UI.escape(r.name)}"
title="Notiz" onclick="event.stopPropagation()">
title="Notiz">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz
</button>
</div>` : '')}

View file

@ -392,7 +392,7 @@ window.Page_personality = (() => {
<div style="display:flex;gap:8px;flex-wrap:wrap">
${typ.aktivitaeten.map(a => `
<button class="btn btn-secondary" style="font-size:var(--text-xs);padding:6px 14px;border-radius:999px"
onclick="App.navigate('${a.page}')">${a.label} </button>`).join('')}
data-page="${a.page}">${a.label} </button>`).join('')}
</div>
</div>
</div>

View file

@ -333,7 +333,7 @@ function _fmtDate(iso) {
icon: UI.icon('paw-print'),
title: 'Noch kein Hund',
text: 'Lege zuerst einen Hund in deinem Profil an.',
action: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Hund anlegen</button>`,
action: `<button class="btn btn-primary" data-page="dog-profile">Hund anlegen</button>`,
});
return;
}

View file

@ -302,7 +302,7 @@ window.Page_poison = (() => {
${_appState.user ? `<div style="margin-top:var(--space-2);text-align:right">
<button class="btn btn-ghost btn-xs poison-note-btn"
data-poison-note-id="${r.id}"
title="Notiz" onclick="event.stopPropagation()">
title="Notiz">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz
</button>
</div>` : ''}

View file

@ -1946,7 +1946,7 @@ window.Page_routes = (() => {
<textarea id="rk-nav-fb-text" class="form-control" rows="4"
placeholder="z.B. Der Weg nach links ist gesperrt…" maxlength="500"></textarea>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" onclick="UI.modal.close()">Abbrechen</button>
<button type="button" class="btn btn-secondary flex-1" data-modal-close>Abbrechen</button>
<button type="button" class="btn btn-primary flex-1" id="rk-nav-fb-send">${UI.icon('paper-plane-tilt')} Senden</button>`;
UI.modal.open({ title: `${UI.icon('chat-circle-dots')} Feedback senden`, body, footer });
document.getElementById('rk-nav-fb-send')?.addEventListener('click', async () => {
@ -1987,7 +1987,7 @@ window.Page_routes = (() => {
target="_blank" style="flex-shrink:0;margin-left:8px;color:var(--c-primary);font-size:12px">Navi</a>
</div>`).join('')}
</div>`).join('');
UI.modal.open({ title: `${UI.icon('map-pin')} POIs entlang der Route`, body, footer: `<button class="btn btn-secondary flex-1" onclick="UI.modal.close()">Schließen</button>` });
UI.modal.open({ title: `${UI.icon('map-pin')} POIs entlang der Route`, body, footer: `<button class="btn btn-secondary flex-1" data-modal-close>Schließen</button>` });
});
}
@ -2299,7 +2299,7 @@ window.Page_routes = (() => {
const photoGallery = photos.length ? `
<div class="rk-photo-gallery">
${photos.map(u => `<img src="${UI.escape(u)}" class="rk-photo-thumb" onclick="window.open('${UI.escape(u)}','_blank')">`).join('')}
${photos.map(u => `<img src="${UI.escape(u)}" class="rk-photo-thumb" data-open-url="${UI.escape(u)}">`).join('')}
${isOwn ? `<label class="rk-photo-add" title="Foto hinzufügen">
<span>+</span>
<input type="file" id="rk-photo-input" accept="image/*" multiple class="hidden">

View file

@ -321,7 +321,7 @@ window.Page_settings = (() => {
style="margin-top:2px;flex-shrink:0;accent-color:${color}">
<span>
Ich habe die <span style="color:var(--c-primary);cursor:pointer"
onclick="App.navigate('agb')">AGB</span> gelesen und stimme ihnen zu.
data-page="agb">AGB</span> gelesen und stimme ihnen zu.
</span>
</label>
</div>
@ -2396,7 +2396,7 @@ window.Page_settings = (() => {
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" form="${id}" type="submit">Link senden</button>`,
});
document.getElementById(id)?.addEventListener('submit', async e => {

View file

@ -139,8 +139,7 @@ window.Page_sitting = (() => {
${_state.user ? `<button class="btn-icon sit-note-btn"
data-sit-note-id="${s.id}"
data-sit-note-label="${UI.escHtml(s.sitter_name + ' ' + (s.datum || ''))}"
title="Notiz" style="color:var(--c-text-muted);margin-top:var(--space-1)"
onclick="event.stopPropagation()">
title="Notiz" style="color:var(--c-text-muted);margin-top:var(--space-1)">
${UI.icon('note-pencil')}</button>` : ''}
</div>
</div>
@ -324,7 +323,7 @@ window.Page_sitting = (() => {
</form>
`;
const footer = `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" type="submit" form="${id}" id="sit-anfrage-submit">${UI.icon('paper-plane-tilt')} Anfrage senden</button>
`;
UI.modal.open({ title: 'Anfrage senden', body, footer });
@ -410,7 +409,7 @@ window.Page_sitting = (() => {
<button class="btn btn-primary" type="submit" form="${id}" id="sit-profil-submit" class="w-full">
${s ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Profil erstellen`}
</button>
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
</div>
`;
UI.modal.open({ title: s ? 'Sitter-Profil bearbeiten' : 'Sitter-Profil erstellen', body, footer });

View file

@ -351,8 +351,7 @@ window.Page_walks = (() => {
data-wk-note-id="${w.id}"
data-wk-note-label="${UI.escape(w.titel + ' ' + w.datum)}"
data-wk-note-ort="${UI.escape(w.ort_name || '')}"
title="Notiz" style="color:var(--c-text-muted);font-size:var(--text-xs)"
onclick="event.stopPropagation()">
title="Notiz" style="color:var(--c-text-muted);font-size:var(--text-xs)">
${UI.icon('note-pencil')}</button>` : ''}
</div>
</div>`;
@ -554,7 +553,7 @@ window.Page_walks = (() => {
: (walk.photos || []).map(p => `
<div style="position:relative;aspect-ratio:1">
<img src="${UI.escape(p.url)}" style="width:100%;height:100%;object-fit:cover;border-radius:var(--radius-sm);cursor:pointer"
onclick="UI.lightbox?.show?.([{ url:'${UI.escape(p.url)}' }], 0)">
data-lightbox-url="${UI.escape(p.url)}">
${p.user_id === _appState.user?.id || isOwn ? `
<button type="button" class="wd-photo-del" data-photo-id="${p.id}"
style="position:absolute;top:3px;right:3px;background:rgba(0,0,0,.6);color:#fff;border:none;border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:11px;display:flex;align-items:center;justify-content:center;padding:0"></button>
@ -637,7 +636,7 @@ window.Page_walks = (() => {
div.style.cssText = 'position:relative;aspect-ratio:1';
div.innerHTML = `
<img src="${UI.escape(photo.url)}" style="width:100%;height:100%;object-fit:cover;border-radius:var(--radius-sm);cursor:pointer"
onclick="UI.lightbox?.show?.([{ url:'${UI.escape(photo.url)}' }], 0)">
data-lightbox-url="${UI.escape(photo.url)}">
<button type="button" class="wd-photo-del" data-photo-id="${photo.id}"
style="position:absolute;top:3px;right:3px;background:rgba(0,0,0,.6);color:#fff;border:none;border-radius:50%;width:20px;height:20px;cursor:pointer;font-size:11px;display:flex;align-items:center;justify-content:center;padding:0"></button>`;
grid.appendChild(div);
@ -1108,7 +1107,7 @@ window.Page_walks = (() => {
<div class="challenge-sub-card">
<img src="${UI.escape(s.foto_url)}" alt="Challenge-Foto" loading="lazy"
onerror="this.src='/icons/icon-192.png'"
onclick="UI.lightbox?.show?.([{ url:'${UI.escape(s.foto_url)}' }], 0)">
data-lightbox-url="${UI.escape(s.foto_url)}">
<div class="challenge-sub-info">
<div class="challenge-sub-user">${UI.icon('user')} ${UI.escape(s.user_name || 'Anonym')}
${s.dog_name ? ` · 🐕 ${UI.escape(s.dog_name)}` : ''}</div>
@ -1169,7 +1168,7 @@ window.Page_walks = (() => {
</form>
`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="challenge-submit-ok">Einreichen</button>
`,
});
@ -1366,7 +1365,7 @@ window.Page_walks = (() => {
</form>
`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-secondary" data-modal-close>Abbrechen</button>
<button class="btn btn-primary" id="gz-save-btn">Speichern</button>
`,
});

View file

@ -30,7 +30,7 @@ window.Page_widget = (() => {
icon: UI.icon('dog'),
title: 'Kein Hund angelegt',
text: 'Erstelle zuerst ein Hundeprofil.',
action: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Profil erstellen</button>`,
action: `<button class="btn btn-primary" data-page="dog-profile">Profil erstellen</button>`,
});
return;
}

View file

@ -80,6 +80,19 @@ window.Page_wiki = (() => {
async function init(container, appState) {
_container = container;
_appState = appState;
// Delegierter Click-Handler — Inline-onclick wird von der CSP blockiert.
if (!_container._wikiClickBound) {
_container.addEventListener('click', e => {
const btn = e.target.closest('[data-wiki-action]');
if (!btn) return;
const id = parseInt(btn.dataset.wikiId, 10);
if (btn.dataset.wikiAction === 'approve') _approveSubmission(id);
else if (btn.dataset.wikiAction === 'reject') _rejectSubmission(id);
});
_container._wikiClickBound = true;
}
await _render();
}
@ -180,11 +193,11 @@ window.Page_wiki = (() => {
</div>
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
<button class="btn btn-primary btn-sm flex-1"
onclick="Page_wiki._approveSubmission(${s.id})">
data-wiki-action="approve" data-wiki-id="${s.id}">
${UI.icon('check')} Freischalten
</button>
<button class="btn btn-ghost btn-sm flex-1"
onclick="Page_wiki._rejectSubmission(${s.id})">
data-wiki-action="reject" data-wiki-id="${s.id}">
${UI.icon('x')} Ablehnen
</button>
</div>
@ -1400,7 +1413,7 @@ window.Page_wiki = (() => {
</p>
${data.hinweis ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-3)">${UI.escape(data.hinweis)}</p>` : ''}
</div>`,
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}

View file

@ -118,8 +118,9 @@ window.Page_zucht_profil = (() => {
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('warning')}</div>
<p class="text-danger">${UI.escape(err.message || 'Fehler beim Laden.')}</p>
<button class="btn btn-secondary" onclick="history.back()">Zurück</button>
<button class="btn btn-secondary" id="zp-back-btn">Zurück</button>
</div>`;
_container.querySelector('#zp-back-btn')?.addEventListener('click', () => history.back());
}
}

View file

@ -0,0 +1,11 @@
// Presse-Seite — ausgelagert, da Inline-Script/onclick von der CSP blockiert wird.
document.addEventListener('DOMContentLoaded', () => {
const btn = document.querySelector('.copy-btn');
btn?.addEventListener('click', () => {
const text = document.getElementById('boilerplate-text').innerText.replace('Kopieren', '').trim();
navigator.clipboard.writeText(text).then(() => {
btn.textContent = 'Kopiert ✓';
setTimeout(() => btn.textContent = 'Kopieren', 2000);
});
});
});

View file

@ -1399,8 +1399,9 @@ window.Worlds = (() => {
<div style="font-size:4rem;margin-bottom:12px">🐾</div>
<div class="world-info-title">Dein Hund wartet</div>
<div class="world-info-sub" style="margin-bottom:20px">Melde dich an um loszulegen</div>
<button class="btn btn-primary" onclick="Worlds.navigateTo('settings')">Anmelden</button>
<button class="btn btn-primary" id="world-login-btn">Anmelden</button>
</div>`;
el.querySelector('#world-login-btn')?.addEventListener('click', () => navigateTo('settings'));
return;
}

View file

@ -0,0 +1,6 @@
// Züchter-Landingpage — ausgelagert, da Inline-onclick von der CSP blockiert wird.
// Verhindert Redirect-Loop beim Öffnen der App aus der Landingpage.
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-stay-in-app]').forEach(el =>
el.addEventListener('click', () => sessionStorage.setItem('by_stay_in_app', '1')));
});

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1163"></script>
<script src="/js/landing-init.js?v=1164"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -221,7 +221,7 @@
<section>
<div class="section-label">Über Ban Yaro — Kurztext für Redaktionen</div>
<div class="boilerplate" id="boilerplate-text">
<button class="copy-btn" onclick="copyBoilerplate()">Kopieren</button>
<button class="copy-btn">Kopieren</button>
<p>Ban Yaro ist eine kostenlose Hunde-App für den deutschsprachigen Raum. Die App läuft als Progressive Web App direkt im Smartphone-Browser — ohne Installation über den App Store. Funktionen: Hunde-Tagebuch mit Fotos und Wetter, digitale Gesundheitsakte, interaktive Karte mit Hundewiesen und Giftköder-Alarm, Community-Forum und Trainingspläne. Gegründet 2024 von René Degelmann, Ebersberg bei München. Erreichbar unter banyaro.app.</p>
</div>
</section>
@ -386,16 +386,7 @@
</div>
<script>
function copyBoilerplate() {
const text = document.getElementById('boilerplate-text').innerText.replace('Kopieren', '').trim();
navigator.clipboard.writeText(text).then(() => {
const btn = document.querySelector('.copy-btn');
btn.textContent = 'Kopiert ✓';
setTimeout(() => btn.textContent = 'Kopieren', 2000);
});
}
</script>
<script src="/js/presse.js"></script>
</body>
</html>

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1163';
const VER = '1164';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten

View file

@ -204,7 +204,7 @@
<span class="badge">VDH-kompatibel</span>
<span class="badge">Kein App Store nötig</span>
</div>
<a href="/#register?rolle=breeder" class="cta-btn" onclick="sessionStorage.setItem('by_stay_in_app','1')">Jetzt als Züchter registrieren</a>
<a href="/#register?rolle=breeder" class="cta-btn" data-stay-in-app>Jetzt als Züchter registrieren</a>
<a href="#funktionen" class="cta-btn-secondary">Alle Features ansehen ↓</a>
</div>
</header>
@ -217,8 +217,8 @@
<a href="#vergleich">Vergleich</a>
<a href="#vorteil">Alleinstellung</a>
<a href="#start">Loslegen</a>
<a href="/" onclick="sessionStorage.setItem('by_stay_in_app','1')">Zur App</a>
<a href="/#register?rolle=breeder" class="nav-cta" onclick="sessionStorage.setItem('by_stay_in_app','1')">Registrieren</a>
<a href="/" data-stay-in-app>Zur App</a>
<a href="/#register?rolle=breeder" class="nav-cta" data-stay-in-app>Registrieren</a>
</div>
</nav>
@ -580,7 +580,7 @@
<p style="font-size:.9rem;opacity:.7;margin-bottom:1.75rem">
39 €/Jahr für die ersten 20 Gründer-Züchter · danach 49 €/Jahr · kein App Store · keine Kreditkarte zum Start
</p>
<a href="/#register?rolle=breeder" class="cta-btn" onclick="sessionStorage.setItem('by_stay_in_app','1')">Als Züchter registrieren</a>
<a href="/#register?rolle=breeder" class="cta-btn" data-stay-in-app>Als Züchter registrieren</a>
<p style="margin-top:1.5rem;font-size:0.85rem;opacity:0.55">Fragen? <a href="mailto:hallo@banyaro.app" style="color:rgba(255,255,255,.7)">hallo@banyaro.app</a></p>
</div>
</section>
@ -599,5 +599,6 @@
</div>
</footer>
<script src="/js/zuechter.js"></script>
</body>
</html>