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

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. 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 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_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION; window.APP_VERSION = APP_VERSION;
@ -443,6 +443,20 @@ const App = (() => {
return; 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 // Header-User-Button → Settings
if (e.target.closest('#header-user-btn')) { if (e.target.closest('#header-user-btn')) {
navigate('settings'); navigate('settings');
@ -1164,7 +1178,7 @@ const App = (() => {
icon: UI.icon(icon), icon: UI.icon(icon),
title: 'Anmelden erforderlich', title: 'Anmelden erforderlich',
text, 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); background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-3);max-height:60vh;overflow-y:auto; padding:var(--space-3);max-height:60vh;overflow-y:auto;
color:var(--c-text)">${UI.escape(l.body || '(kein Text gespeichert)')}</pre>`, 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> </div>
</form>`, </form>`,
footer: ` 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>`, <button class="btn btn-primary" form="${id}" type="submit">Speichern</button>`,
}); });
@ -2993,7 +2993,7 @@ window.Page_admin = (() => {
</div> </div>
</div>`, </div>`,
footer: ` 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>`, <button class="btn btn-primary" id="adm-bew-save-note">Notiz speichern</button>`,
}); });
document.getElementById('adm-bew-save-note')?.addEventListener('click', async () => { 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"> <button type="submit" form="adp-interest-form" class="btn btn-primary flex-1" id="adp-interest-submit">
${UI.icon('heart')} Interesse bekunden ${UI.icon('heart')} Interesse bekunden
</button> </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> </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"> <button type="submit" form="adp-create-form" class="btn btn-primary w-full" id="adp-create-submit">
${UI.icon('plus')} Inserat erstellen ${UI.icon('plus')} Inserat erstellen
</button> </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> </div>
`; `;

View file

@ -18,6 +18,23 @@ window.Page_chat = (() => {
_container = container; _container = container;
_myId = appState?.user?.id || null; _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 // Heartbeat: alle 30s online-Status senden
API.chat.heartbeat().catch(() => {}); API.chat.heartbeat().catch(() => {});
_heartbeatTimer = setInterval(() => { _heartbeatTimer = setInterval(() => {
@ -132,7 +149,7 @@ window.Page_chat = (() => {
? `<span class="online-dot" title="Online"></span>` ? `<span class="online-dot" title="Online"></span>`
: ''; : '';
return ` 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 style="position:relative;flex-shrink:0">
<div class="chat-conv-avatar">${initials}</div> <div class="chat-conv-avatar">${initials}</div>
${onlineDot ? `<span class="online-dot chat-avatar-dot"></span>` : ''} ${onlineDot ? `<span class="online-dot chat-avatar-dot"></span>` : ''}
@ -166,14 +183,14 @@ window.Page_chat = (() => {
// Aktive Markierung in der Liste // Aktive Markierung in der Liste
document.querySelectorAll('.chat-conv-item').forEach(el => 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 = ` const threadHTML = `
<div class="chat-thread" id="chat-thread"> <div class="chat-thread" id="chat-thread">
<div class="chat-thread-header"> <div class="chat-thread-header">
${_isDesktop() ? '' : ` ${_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> <svg class="ph-icon"><use href="/icons/phosphor.svg#arrow-left"></use></svg>
</button>`} </button>`}
<div style="position:relative;flex-shrink:0"> <div style="position:relative;flex-shrink:0">
@ -188,14 +205,13 @@ window.Page_chat = (() => {
</div> </div>
</div> </div>
<div class="chat-input-bar"> <div class="chat-input-bar">
<input type="file" id="chat-photo-input" accept="image/*" class="hidden" <input type="file" id="chat-photo-input" accept="image/*" class="hidden">
onchange="Page_chat._onPhotoSelected(this)"> <button class="chat-photo-btn" data-chat-action="photo" title="Foto senden">
<button class="chat-photo-btn" onclick="document.getElementById('chat-photo-input').click()" title="Foto senden">
<svg class="ph-icon"><use href="/icons/phosphor.svg#camera"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#camera"></use></svg>
</button> </button>
<textarea id="chat-input" class="chat-input" rows="1" <textarea id="chat-input" class="chat-input" rows="1"
placeholder="Nachricht…" maxlength="2000"></textarea> 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> <svg class="ph-icon"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
</button> </button>
</div> </div>
@ -219,9 +235,11 @@ window.Page_chat = (() => {
input.addEventListener('keydown', e => { input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) { if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
Page_chat._send(); _send();
} }
}); });
document.getElementById('chat-photo-input')
?.addEventListener('change', e => _onPhotoSelected(e.target));
await _loadMessages(true); await _loadMessages(true);
await API.chat.markRead(_convId).catch(() => {}); await API.chat.markRead(_convId).catch(() => {});
@ -313,7 +331,7 @@ window.Page_chat = (() => {
const timeStr = _fmtTime(m.created_at); const timeStr = _fmtTime(m.created_at);
const deleteBtn = isMine && !m.is_deleted const deleteBtn = isMine && !m.is_deleted
? `<button class="btn btn-ghost" style="padding:2px;opacity:0.4;font-size:var(--text-xs)" ? `<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> <svg class="ph-icon" style="width:12px;height:12px"><use href="/icons/phosphor.svg#trash"></use></svg>
</button>` </button>`
: ''; : '';
@ -328,7 +346,7 @@ window.Page_chat = (() => {
// Medieninhalt // Medieninhalt
let bubbleContent = ''; let bubbleContent = '';
if (m.media_url) { 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) { if (m.text) {
bubbleContent += (m.media_url ? `<div style="margin-top:var(--space-1)">` : '') + 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> <span class="diary-detail-date-center">${datumLang}</span>
<div style="display:flex;align-items:center;gap:4px"> <div style="display:flex;align-items:center;gap:4px">
${!_appState?.activeDog?.is_guest ${!_appState?.activeDog?.is_guest
? `<button id="diary-dv-note" class="btn btn-ghost btn-xs" title="Notiz" ? `<button id="diary-dv-note" class="btn btn-ghost btn-xs" title="Notiz">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
</button> </button>
<button id="diary-dv-edit" class="diary-detail-edit"> <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>`, <div id="import-result" style="display:none;margin-top:var(--space-4)"></div>`,
footer: ` 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>`, <button class="btn btn-primary" id="import-start-btn">Importieren</button>`,
}); });

View file

@ -616,7 +616,7 @@ window.Page_dog_profile = (() => {
footer: ` footer: `
<div class="w3-btn-stack"> <div class="w3-btn-stack">
<button class="btn btn-primary" id="chip-edit-save-btn" class="w-full">Speichern</button> <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>`, </div>`,
}); });
document.getElementById('chip-edit-save-btn').addEventListener('click', async () => { 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>` : ''} ${hasPhoto ? `<button class="btn btn-primary" id="pe-save-btn" class="w-full">Speichern</button>` : ''}
<div class="flex-gap-2"> <div class="flex-gap-2">
${hasPhoto ? `<button class="btn btn-danger" id="pe-delete-btn">${UI.icon('trash')} Löschen</button>` : ''} ${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>
</div> </div>
`; `;
@ -957,7 +957,7 @@ window.Page_dog_profile = (() => {
</div> </div>
<div id="share-list-wrap" class="mt-4"></div>`, <div id="share-list-wrap" class="mt-4"></div>`,
footer: ` 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>`, <button class="btn btn-primary" id="share-create-btn">Link erstellen</button>`,
}); });
@ -1489,7 +1489,7 @@ window.Page_dog_profile = (() => {
</p> </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>` : ''} ${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>`, </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; return;
} }
@ -1608,7 +1608,7 @@ window.Page_dog_profile = (() => {
</div>`, </div>`,
footer: ` footer: `
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;justify-content:flex-end"> <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"> <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> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg>
Ausweis öffnen Ausweis öffnen
@ -1832,7 +1832,7 @@ window.Page_dog_profile = (() => {
</div>`, </div>`,
footer: ` footer: `
<div style="display:flex;gap:var(--space-2);justify-content:flex-end"> <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> <button class="btn btn-primary" id="pp-meta-save">Speichern</button>
</div>`, </div>`,
}); });
@ -1896,7 +1896,7 @@ window.Page_dog_profile = (() => {
</div>`, </div>`,
footer: ` footer: `
<div style="display:flex;gap:var(--space-2);justify-content:flex-end"> <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> <button class="btn btn-primary" id="pp-vacc-save">Speichern</button>
</div>`, </div>`,
}); });
@ -1960,7 +1960,7 @@ window.Page_dog_profile = (() => {
</div>`, </div>`,
footer: ` footer: `
<div style="display:flex;gap:var(--space-2);justify-content:flex-end"> <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> <button class="btn btn-primary" id="pp-med-save">Speichern</button>
</div>`, </div>`,
}); });
@ -2017,7 +2017,7 @@ window.Page_dog_profile = (() => {
UI.modal.open({ UI.modal.open({
title: 'Hundepass-Link teilen', title: 'Hundepass-Link teilen',
body: shareWrap.innerHTML, 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 () => { document.getElementById('pp-sharelink-copy')?.addEventListener('click', async () => {
await navigator.clipboard.writeText(url).catch(() => {}); await navigator.clipboard.writeText(url).catch(() => {});
@ -2209,7 +2209,7 @@ window.Page_dog_profile = (() => {
? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;' ? 'background:#7a4f1a;color:#f5e4c0;border-color:#7a4f1a;'
: 'background:#f5f0e8;color:#444;border-color:#e0d4b8;'; : 'background:#f5f0e8;color:#444;border-color:#e0d4b8;';
const label = y === 'alle' ? 'Alle' : y; 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; border:1px solid;border-radius:8px;padding:8px 16px;
font-size:0.9rem;cursor:pointer;font-family:inherit; font-size:0.9rem;cursor:pointer;font-family:inherit;
${active} ${active}
@ -2239,14 +2239,14 @@ window.Page_dog_profile = (() => {
<div style="margin-bottom:20px;display:flex;flex-direction:column;gap:10px"> <div style="margin-bottom:20px;display:flex;flex-direction:column;gap:10px">
<label style="display:flex;align-items:center;gap:12px;cursor:pointer"> <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; width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
${togStyle(nurFotos)} ${togStyle(nurFotos)}
">${nurFotos ? '✓' : ''}</button> ">${nurFotos ? '✓' : ''}</button>
<span style="font-size:0.95rem">Nur Einträge mit Fotos</span> <span style="font-size:0.95rem">Nur Einträge mit Fotos</span>
</label> </label>
<label style="display:flex;align-items:center;gap:12px;cursor:pointer"> <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; width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;
${togStyle(nurMeilensteine)} ${togStyle(nurMeilensteine)}
">${nurMeilensteine ? '✓' : ''}</button> ">${nurMeilensteine ? '✓' : ''}</button>
@ -2255,11 +2255,11 @@ window.Page_dog_profile = (() => {
</div> </div>
<div style="display:flex;gap:10px"> <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; flex:1;background:#7a4f1a;color:#f5e4c0;border:none;border-radius:10px;
padding:14px;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit; padding:14px;font-size:1rem;font-weight:700;cursor:pointer;font-family:inherit;
">📖 Buch öffnen</button> ">📖 Buch öffnen</button>
<button onclick="window._buchClose()" style=" <button data-buch-action="close" style="
background:#f0f0f0;color:#555;border:none;border-radius:10px; background:#f0f0f0;color:#555;border:none;border-radius:10px;
padding:14px 18px;font-size:1rem;cursor:pointer;font-family:inherit; padding:14px 18px;font-size:1rem;cursor:pointer;font-family:inherit;
"></button> "></button>
@ -2268,18 +2268,14 @@ window.Page_dog_profile = (() => {
`; `;
}; };
window._buchSetJahr = (y) => { selectedJahr = y; renderModal(); }; const setJahr = (y) => { selectedJahr = y; renderModal(); };
window._buchToggleFotos = () => { nurFotos = !nurFotos; renderModal(); }; const toggleFotos = () => { nurFotos = !nurFotos; renderModal(); };
window._buchToggleMeilensteine = () => { nurMeilensteine = !nurMeilensteine; renderModal(); }; const toggleMeilen = () => { nurMeilensteine = !nurMeilensteine; renderModal(); };
window._buchClose = () => { const closeModal = () => {
modalEl.remove(); modalEl.remove();
delete window._buchSetJahr; document.removeEventListener('keydown', onKey);
delete window._buchToggleFotos;
delete window._buchToggleMeilensteine;
delete window._buchOpen;
delete window._buchClose;
}; };
window._buchOpen = () => { const openBuch = () => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (selectedJahr !== 'alle') params.set('jahr', selectedJahr); if (selectedJahr !== 'alle') params.set('jahr', selectedJahr);
if (nurFotos) params.set('nur_fotos', 'true'); if (nurFotos) params.set('nur_fotos', 'true');
@ -2290,10 +2286,24 @@ window.Page_dog_profile = (() => {
renderModal(); renderModal();
document.body.appendChild(modalEl); 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 => { const onKey = e => {
if (e.key === 'Escape') { window._buchClose(); document.removeEventListener('keydown', onKey); } if (e.key === 'Escape') { closeModal(); }
}; };
document.addEventListener('keydown', onKey); document.addEventListener('keydown', onKey);
} }
@ -2310,7 +2320,7 @@ window.Page_dog_profile = (() => {
<use href="/icons/phosphor.svg#spinner-gap"></use> <use href="/icons/phosphor.svg#spinner-gap"></use>
</svg> </svg>
</div>`, </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', 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>', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bowl-food"></use></svg>',
title: 'Noch kein Hund angelegt', title: 'Noch kein Hund angelegt',
text: 'Erstelle zuerst ein Hundeprofil.', 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; return;
} }
@ -728,7 +728,7 @@ window.Page_ernaehrung = (() => {
</form> </form>
`; `;
const footer = ` 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> <button class="btn btn-primary" id="vert-futter-save-btn" form="${id}">Speichern</button>
`; `;
UI.modal.open({ title: 'Futter erfassen', body, footer }); UI.modal.open({ title: 'Futter erfassen', body, footer });
@ -840,7 +840,7 @@ window.Page_ernaehrung = (() => {
</form> </form>
`; `;
const footer = ` 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> <button class="btn btn-primary" id="vert-reaktion-save-btn" form="${id}">Speichern</button>
`; `;
UI.modal.open({ title: 'Reaktion erfassen', body, footer }); UI.modal.open({ title: 'Reaktion erfassen', body, footer });

View file

@ -221,17 +221,17 @@ window.Page_events = (() => {
</div> </div>
${ev.rsvp_count ? `<span class="event-attendees" data-ev-attendees="${ev.id}">${_icon('users')} ${ev.rsvp_count} nehmen teil</span>` : ''} ${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"> ${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 ${_icon('arrow-square-out')} Details
</a> </a>
</div>` : ''} </div>` : ''}
</div> </div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:var(--space-1)"> <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}" ${_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-label="${UI.escape(ev.titel + ' ' + ev.datum)}"
data-ev-note-ort="${UI.escape(ev.ort_name || '')}" 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>` : ''} ${_icon('note-pencil')}</button>` : ''}
</div> </div>
</div> </div>
@ -276,7 +276,7 @@ window.Page_events = (() => {
<span style="color:var(--c-text-muted);font-size:12px">${datum}</span><br> <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.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>` : ''} ${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> style="font-size:12px;color:var(--c-primary,#2563eb)">Details</a>
</div> </div>
`; `;
@ -512,7 +512,7 @@ window.Page_events = (() => {
</button> </button>
<div class="flex-gap-2"> <div class="flex-gap-2">
${isEdit ? `<button type="button" class="btn btn-danger" id="ev-form-delete">Löschen</button>` : ''} ${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>
</div> </div>
`; `;
@ -570,6 +570,14 @@ window.Page_events = (() => {
// Click-Handler // Click-Handler
// ---------------------------------------------------------- // ----------------------------------------------------------
function _onClick(e) { 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 // Quelle-Filter
const sourceBtn = e.target.closest('[data-ev-quelle]'); const sourceBtn = e.target.closest('[data-ev-quelle]');
if (sourceBtn) { if (sourceBtn) {

View file

@ -485,7 +485,7 @@ window.Page_expenses = (() => {
</form>`; </form>`;
const footer = ` 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>`; <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 }); 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"> style="color:var(--c-danger);margin-right:auto">
${UI.icon('trash')} ${UI.icon('trash')}
</button> </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="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> <button type="submit" form="${formId}" class="btn btn-primary flex-1">Speichern</button>
`; `;

View file

@ -295,7 +295,7 @@ function _fmtDate(iso) {
</div>`; </div>`;
UI.modal.open({ title: '🏆 Hund des Monats', body, 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 => { document.getElementById('hdm-login-link')?.addEventListener('click', e => {
e.preventDefault(); UI.modal.close(); App.navigate('settings'); e.preventDefault(); UI.modal.close(); App.navigate('settings');
@ -1022,7 +1022,7 @@ function _fmtDate(iso) {
</p> </p>
</div>`, </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> </div>
</form>`, </form>`,
footer: ` 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>`, <button type="submit" form="${id}" class="btn btn-primary flex-1">${UI.icon('floppy-disk')} Speichern</button>`,
}); });
document.getElementById(id)?.addEventListener('submit', async e => { document.getElementById(id)?.addEventListener('submit', async e => {
@ -1423,7 +1423,7 @@ function _fmtDate(iso) {
</div> </div>
</form>`, </form>`,
footer: ` 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>`, <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'), icon: UI.icon('users'),
title: 'Anmelden erforderlich', title: 'Anmelden erforderlich',
text: 'Melde dich an, um Freunde zu finden und Anfragen zu verwalten.', 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; return;
} }
@ -524,8 +524,7 @@ window.Page_friends = (() => {
<button class="btn btn-ghost btn-sm fr-note-btn" <button class="btn btn-ghost btn-sm fr-note-btn"
data-fr-note-id="${f.friend_id}" data-fr-note-id="${f.friend_id}"
data-fr-note-name="${UI.escape(f.friend_name)}" data-fr-note-name="${UI.escape(f.friend_name)}"
title="Notiz" title="Notiz">
onclick="event.stopPropagation()">
<svg class="ph-icon"><use href="/icons/phosphor.svg#note-pencil"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#note-pencil"></use></svg>
</button> </button>
<button class="btn btn-ghost btn-sm" <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>', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#syringe"></use></svg>',
title: 'Noch kein Hund angelegt', title: 'Noch kein Hund angelegt',
text: 'Erstelle zuerst ein Hundeprofil.', 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; return;
} }
@ -403,8 +403,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''} ${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" <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-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}" data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div> </div>
</div> </div>
`; `;
@ -511,8 +510,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''} ${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" <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-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}" data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div> </div>
</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>` : ''} ${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" <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-action="open-note" data-entry-id="${e.id}"
data-label="Gewicht ${UI.escape(e.datum)}" 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>
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div> </div>
</div> </div>
`).join(''); `).join('');
@ -801,8 +798,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''} ${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" <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-action="open-note" data-entry-id="${e.id}"
data-label="Läufigkeit ${UI.escape(e.datum)}" 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>
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
@ -839,8 +835,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''} ${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" <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-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}" data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div> </div>
</div> </div>
`).join('')} `).join('')}
@ -880,8 +875,7 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''} ${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" <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-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}" data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
</div> </div>
</div> </div>
`).join(''); `).join('');
@ -923,19 +917,16 @@ window.Page_health = (() => {
${e.notiz ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>` : ''} ${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" <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-action="open-note" data-entry-id="${e.id}"
data-label="${UI.escape(e.bezeichnung)}" data-label="${UI.escape(e.bezeichnung)}"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
onclick="event.stopPropagation()"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</button>
${count ${count
? `<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);align-items:center;flex-wrap:wrap"> ? `<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' ${mediaList.slice(0, 3).map(m => m.media_type === 'pdf'
? `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener" ? `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm" style="display:inline-flex" class="btn btn-secondary btn-sm" style="display:inline-flex">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF
</a>` </a>`
: `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener" : `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm" style="display:inline-flex" class="btn btn-secondary btn-sm" style="display:inline-flex">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg> Bild <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#image"></use></svg> Bild
</a>` </a>`
).join('')} ).join('')}
@ -1773,28 +1764,24 @@ window.Page_health = (() => {
${ratingHtml} ${ratingHtml}
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap"> <div style="display:flex;gap:var(--space-2);margin-top:var(--space-2);flex-wrap:wrap">
${p.telefon ? ` ${p.telefon ? `
<a href="tel:${UI.escape(p.telefon)}" class="btn btn-secondary btn-sm" <a href="tel:${UI.escape(p.telefon)}" class="btn btn-secondary btn-sm">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> Anrufen <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> Anrufen
</a>` : ''} </a>` : ''}
${p.notfall_telefon ? ` ${p.notfall_telefon ? `
<a href="tel:${UI.escape(p.notfall_telefon)}" class="btn btn-danger btn-sm" <a href="tel:${UI.escape(p.notfall_telefon)}" class="btn btn-danger btn-sm">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall
</a>` : ''} </a>` : ''}
<button class="btn btn-sm btn-secondary" <button class="btn btn-sm btn-secondary"
data-action="bewerten" data-praxis-id="${p.id}" data-action="bewerten" data-praxis-id="${p.id}"
title="Bewertung abgeben" title="Bewertung abgeben"
style="flex-shrink:0" style="flex-shrink:0">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg>
Bewerten Bewerten
</button> </button>
<button class="btn btn-sm ${isFav ? 'btn-primary' : 'btn-secondary'}" <button class="btn btn-sm ${isFav ? 'btn-primary' : 'btn-secondary'}"
data-action="toggle-fav" data-praxis-id="${p.id}" data-action="toggle-fav" data-praxis-id="${p.id}"
title="${isFav ? 'Favorit entfernen' : 'Als mein Tierarzt merken'}" title="${isFav ? 'Favorit entfernen' : 'Als mein Tierarzt merken'}"
style="flex-shrink:0" style="flex-shrink:0">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"> <svg class="ph-icon" aria-hidden="true">
<use href="/icons/phosphor.svg#${isFav ? 'heart-fill' : 'heart'}"></use> <use href="/icons/phosphor.svg#${isFav ? 'heart-fill' : 'heart'}"></use>
</svg> </svg>
@ -1803,8 +1790,7 @@ window.Page_health = (() => {
<button class="btn btn-sm btn-secondary" <button class="btn btn-sm btn-secondary"
data-action="edit-praxis" data-praxis-id="${p.id}" data-action="edit-praxis" data-praxis-id="${p.id}"
title="Praxis bearbeiten" title="Praxis bearbeiten"
style="flex-shrink:0" style="flex-shrink:0">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pencil-simple"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#pencil-simple"></use></svg>
</button> </button>
</div> </div>
@ -1881,7 +1867,7 @@ window.Page_health = (() => {
<use href="/icons/phosphor.svg#spinner-gap"></use> <use href="/icons/phosphor.svg#spinner-gap"></use>
</svg> </svg>
</div>`, </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"> <button class="btn btn-primary" id="detail-bewerten-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg>
Jetzt bewerten Jetzt bewerten
@ -1998,7 +1984,7 @@ window.Page_health = (() => {
title: `${UI.escape(praxis.name)} bewerten`, title: `${UI.escape(praxis.name)} bewerten`,
body, body,
footer: ` 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"> <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> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#star"></use></svg>
${existing ? 'Bewertung aktualisieren' : 'Bewertung abgeben'} ${existing ? 'Bewertung aktualisieren' : 'Bewertung abgeben'}
@ -2373,7 +2359,7 @@ window.Page_health = (() => {
value="${UI.escape(currentNr)}" placeholder="z.B. 276009200123456" maxlength="20"> value="${UI.escape(currentNr)}" placeholder="z.B. 276009200123456" maxlength="20">
</div>`, </div>`,
footer: ` 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>`, <button class="btn btn-primary" id="transponder-save-btn">Speichern</button>`,
}); });
document.getElementById('transponder-save-btn').addEventListener('click', async () => { document.getElementById('transponder-save-btn').addEventListener('click', async () => {
@ -2441,11 +2427,11 @@ window.Page_health = (() => {
const b = berichte[idx]; const b = berichte[idx];
const nav = berichte.length > 1 ? ` const nav = berichte.length > 1 ? `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px"> <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; 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> 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> <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; 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> font-size:var(--text-sm);${idx <= 0 ? 'opacity:.3;pointer-events:none' : ''}">Neuer </button>
</div>` : ''; </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="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>`, <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(); showBericht();
}); });
} catch (_) { } catch (_) {
@ -2620,15 +2609,13 @@ window.Page_health = (() => {
${adresse ? `<div class="list-item-meta-row">${UI.escape(adresse)}</div>` : ''} ${adresse ? `<div class="list-item-meta-row">${UI.escape(adresse)}</div>` : ''}
${vet.telefon ? ` ${vet.telefon ? `
<div class="mt-2"> <div class="mt-2">
<a href="tel:${UI.escape(vet.telefon)}" class="btn btn-secondary btn-sm" <a href="tel:${UI.escape(vet.telefon)}" class="btn btn-secondary btn-sm">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> ${UI.escape(vet.telefon)} <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> ${UI.escape(vet.telefon)}
</a> </a>
</div>` : ''} </div>` : ''}
${vet.notfall_telefon ? ` ${vet.notfall_telefon ? `
<div style="margin-top:var(--space-1)"> <div style="margin-top:var(--space-1)">
<a href="tel:${UI.escape(vet.notfall_telefon)}" class="btn btn-danger btn-sm" <a href="tel:${UI.escape(vet.notfall_telefon)}" class="btn btn-danger btn-sm">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall: ${UI.escape(vet.notfall_telefon)} <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall: ${UI.escape(vet.notfall_telefon)}
</a> </a>
</div>` : ''} </div>` : ''}
@ -2718,14 +2705,13 @@ window.Page_health = (() => {
${doc.beschreibung ? `<div class="list-item-text">${UI.escape(doc.beschreibung)}</div>` : ''} ${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"> <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" <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 ${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#image"></use></svg> Bild öffnen'
: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF öffnen'} : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg> PDF öffnen'}
</a> </a>
<button class="btn btn-ghost btn-xs text-danger" <button class="btn btn-ghost btn-xs text-danger"
data-action="delete-hdoc" data-doc-id="${doc.id}" data-action="delete-hdoc" data-doc-id="${doc.id}">
onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>
</button> </button>
</div> </div>
@ -3007,7 +2993,7 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) {
Bei ernsthaften oder sich verschlechternden Symptomen sofort zum Tierarzt. Bei ernsthaften oder sich verschlechternden Symptomen sofort zum Tierarzt.
</div>`, </div>`,
footer: ` 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>`, <button class="btn btn-primary" id="ki-tierarzt-submit-btn">Frage stellen</button>`,
}); });
@ -3233,7 +3219,7 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) {
</div> </div>
</form>`; </form>`;
const footer = ` 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>`; <button class="btn btn-primary" id="ins-save-btn" form="${id}">Speichern</button>`;
UI.modal.open({ title: existing ? 'Versicherung bearbeiten' : 'Versicherung eintragen', body, footer }); UI.modal.open({ title: existing ? 'Versicherung bearbeiten' : 'Versicherung eintragen', body, footer });
setTimeout(() => { setTimeout(() => {
@ -3385,7 +3371,7 @@ function _showPoiKorrekturModal(osmId, poiName, currentOh) {
</div> </div>
</form>`; </form>`;
const footer = ` 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>`; <button class="btn btn-primary" id="beh-save-btn" form="${id}">Speichern</button>`;
UI.modal.open({ title: 'Verhalten erfassen', body, footer }); UI.modal.open({ title: 'Verhalten erfassen', body, footer });
setTimeout(() => { setTimeout(() => {

View file

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

View file

@ -867,7 +867,7 @@ window.Page_litters = (() => {
</div> </div>
</form>`, </form>`,
footer: ` 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>`, <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> <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" <button class="btn btn-ghost btn-xs lost-discard-btn"
data-pending-id="${r.id}" data-pending-id="${r.id}"
onclick="event.stopPropagation()"
style="color:var(--c-danger,#dc2626)"> style="color:var(--c-danger,#dc2626)">
🗑 Verwerfen 🗑 Verwerfen
</button> </button>
@ -419,7 +418,7 @@ window.Page_lost = (() => {
<button class="btn btn-ghost btn-xs lost-note-btn" <button class="btn btn-ghost btn-xs lost-note-btn"
data-lost-note-id="${r.id}" data-lost-note-id="${r.id}"
data-lost-note-name="${UI.escape(r.name)}" 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 <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz
</button> </button>
</div>` : '')} </div>` : '')}

View file

@ -392,7 +392,7 @@ window.Page_personality = (() => {
<div style="display:flex;gap:8px;flex-wrap:wrap"> <div style="display:flex;gap:8px;flex-wrap:wrap">
${typ.aktivitaeten.map(a => ` ${typ.aktivitaeten.map(a => `
<button class="btn btn-secondary" style="font-size:var(--text-xs);padding:6px 14px;border-radius:999px" <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> </div>
</div> </div>

View file

@ -333,7 +333,7 @@ function _fmtDate(iso) {
icon: UI.icon('paw-print'), icon: UI.icon('paw-print'),
title: 'Noch kein Hund', title: 'Noch kein Hund',
text: 'Lege zuerst einen Hund in deinem Profil an.', 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; return;
} }

View file

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

View file

@ -1946,7 +1946,7 @@ window.Page_routes = (() => {
<textarea id="rk-nav-fb-text" class="form-control" rows="4" <textarea id="rk-nav-fb-text" class="form-control" rows="4"
placeholder="z.B. Der Weg nach links ist gesperrt…" maxlength="500"></textarea>`; placeholder="z.B. Der Weg nach links ist gesperrt…" maxlength="500"></textarea>`;
const footer = ` 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>`; <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 }); UI.modal.open({ title: `${UI.icon('chat-circle-dots')} Feedback senden`, body, footer });
document.getElementById('rk-nav-fb-send')?.addEventListener('click', async () => { 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> target="_blank" style="flex-shrink:0;margin-left:8px;color:var(--c-primary);font-size:12px">Navi</a>
</div>`).join('')} </div>`).join('')}
</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 ? ` const photoGallery = photos.length ? `
<div class="rk-photo-gallery"> <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"> ${isOwn ? `<label class="rk-photo-add" title="Foto hinzufügen">
<span>+</span> <span>+</span>
<input type="file" id="rk-photo-input" accept="image/*" multiple class="hidden"> <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}"> style="margin-top:2px;flex-shrink:0;accent-color:${color}">
<span> <span>
Ich habe die <span style="color:var(--c-primary);cursor:pointer" 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> </span>
</label> </label>
</div> </div>
@ -2396,7 +2396,7 @@ window.Page_settings = (() => {
</div> </div>
</form>`, </form>`,
footer: ` 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>`, <button class="btn btn-primary" form="${id}" type="submit">Link senden</button>`,
}); });
document.getElementById(id)?.addEventListener('submit', async e => { 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" ${_state.user ? `<button class="btn-icon sit-note-btn"
data-sit-note-id="${s.id}" data-sit-note-id="${s.id}"
data-sit-note-label="${UI.escHtml(s.sitter_name + ' ' + (s.datum || ''))}" data-sit-note-label="${UI.escHtml(s.sitter_name + ' ' + (s.datum || ''))}"
title="Notiz" style="color:var(--c-text-muted);margin-top:var(--space-1)" title="Notiz" style="color:var(--c-text-muted);margin-top:var(--space-1)">
onclick="event.stopPropagation()">
${UI.icon('note-pencil')}</button>` : ''} ${UI.icon('note-pencil')}</button>` : ''}
</div> </div>
</div> </div>
@ -324,7 +323,7 @@ window.Page_sitting = (() => {
</form> </form>
`; `;
const footer = ` 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> <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 }); 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"> <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`} ${s ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Profil erstellen`}
</button> </button>
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button> <button class="btn btn-secondary" data-modal-close>Abbrechen</button>
</div> </div>
`; `;
UI.modal.open({ title: s ? 'Sitter-Profil bearbeiten' : 'Sitter-Profil erstellen', body, footer }); 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-id="${w.id}"
data-wk-note-label="${UI.escape(w.titel + ' ' + w.datum)}" data-wk-note-label="${UI.escape(w.titel + ' ' + w.datum)}"
data-wk-note-ort="${UI.escape(w.ort_name || '')}" data-wk-note-ort="${UI.escape(w.ort_name || '')}"
title="Notiz" style="color:var(--c-text-muted);font-size:var(--text-xs)" title="Notiz" style="color:var(--c-text-muted);font-size:var(--text-xs)">
onclick="event.stopPropagation()">
${UI.icon('note-pencil')}</button>` : ''} ${UI.icon('note-pencil')}</button>` : ''}
</div> </div>
</div>`; </div>`;
@ -554,7 +553,7 @@ window.Page_walks = (() => {
: (walk.photos || []).map(p => ` : (walk.photos || []).map(p => `
<div style="position:relative;aspect-ratio:1"> <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" <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 ? ` ${p.user_id === _appState.user?.id || isOwn ? `
<button type="button" class="wd-photo-del" data-photo-id="${p.id}" <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> 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.style.cssText = 'position:relative;aspect-ratio:1';
div.innerHTML = ` div.innerHTML = `
<img src="${UI.escape(photo.url)}" style="width:100%;height:100%;object-fit:cover;border-radius:var(--radius-sm);cursor:pointer" <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}" <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>`; 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); grid.appendChild(div);
@ -1108,7 +1107,7 @@ window.Page_walks = (() => {
<div class="challenge-sub-card"> <div class="challenge-sub-card">
<img src="${UI.escape(s.foto_url)}" alt="Challenge-Foto" loading="lazy" <img src="${UI.escape(s.foto_url)}" alt="Challenge-Foto" loading="lazy"
onerror="this.src='/icons/icon-192.png'" 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-info">
<div class="challenge-sub-user">${UI.icon('user')} ${UI.escape(s.user_name || 'Anonym')} <div class="challenge-sub-user">${UI.icon('user')} ${UI.escape(s.user_name || 'Anonym')}
${s.dog_name ? ` · 🐕 ${UI.escape(s.dog_name)}` : ''}</div> ${s.dog_name ? ` · 🐕 ${UI.escape(s.dog_name)}` : ''}</div>
@ -1169,7 +1168,7 @@ window.Page_walks = (() => {
</form> </form>
`, `,
footer: ` 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> <button class="btn btn-primary" id="challenge-submit-ok">Einreichen</button>
`, `,
}); });
@ -1366,7 +1365,7 @@ window.Page_walks = (() => {
</form> </form>
`, `,
footer: ` 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> <button class="btn btn-primary" id="gz-save-btn">Speichern</button>
`, `,
}); });

View file

@ -30,7 +30,7 @@ window.Page_widget = (() => {
icon: UI.icon('dog'), icon: UI.icon('dog'),
title: 'Kein Hund angelegt', title: 'Kein Hund angelegt',
text: 'Erstelle zuerst ein Hundeprofil.', 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; return;
} }

View file

@ -80,6 +80,19 @@ window.Page_wiki = (() => {
async function init(container, appState) { async function init(container, appState) {
_container = container; _container = container;
_appState = appState; _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(); await _render();
} }
@ -180,11 +193,11 @@ window.Page_wiki = (() => {
</div> </div>
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)"> <div style="display:flex;gap:var(--space-2);margin-top:var(--space-3)">
<button class="btn btn-primary btn-sm flex-1" <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 ${UI.icon('check')} Freischalten
</button> </button>
<button class="btn btn-ghost btn-sm flex-1" <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 ${UI.icon('x')} Ablehnen
</button> </button>
</div> </div>
@ -1400,7 +1413,7 @@ window.Page_wiki = (() => {
</p> </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>` : ''} ${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>`, </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; 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="text-align:center;padding:var(--space-10) var(--space-4)">
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('warning')}</div> <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> <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>`; </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 style="font-size:4rem;margin-bottom:12px">🐾</div>
<div class="world-info-title">Dein Hund wartet</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> <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>`; </div>`;
el.querySelector('#world-login-btn')?.addEventListener('click', () => navigateTo('settings'));
return; 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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark"> <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> <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="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"> <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> <section>
<div class="section-label">Über Ban Yaro — Kurztext für Redaktionen</div> <div class="section-label">Über Ban Yaro — Kurztext für Redaktionen</div>
<div class="boilerplate" id="boilerplate-text"> <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> <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> </div>
</section> </section>
@ -386,16 +386,7 @@
</div> </div>
<script> <script src="/js/presse.js"></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>
</body> </body>
</html> </html>

View file

@ -4,7 +4,7 @@
============================================================ */ ============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab // ← 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_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten 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">VDH-kompatibel</span>
<span class="badge">Kein App Store nötig</span> <span class="badge">Kein App Store nötig</span>
</div> </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> <a href="#funktionen" class="cta-btn-secondary">Alle Features ansehen ↓</a>
</div> </div>
</header> </header>
@ -217,8 +217,8 @@
<a href="#vergleich">Vergleich</a> <a href="#vergleich">Vergleich</a>
<a href="#vorteil">Alleinstellung</a> <a href="#vorteil">Alleinstellung</a>
<a href="#start">Loslegen</a> <a href="#start">Loslegen</a>
<a href="/" onclick="sessionStorage.setItem('by_stay_in_app','1')">Zur App</a> <a href="/" data-stay-in-app>Zur App</a>
<a href="/#register?rolle=breeder" class="nav-cta" onclick="sessionStorage.setItem('by_stay_in_app','1')">Registrieren</a> <a href="/#register?rolle=breeder" class="nav-cta" data-stay-in-app>Registrieren</a>
</div> </div>
</nav> </nav>
@ -580,7 +580,7 @@
<p style="font-size:.9rem;opacity:.7;margin-bottom:1.75rem"> <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 39 €/Jahr für die ersten 20 Gründer-Züchter · danach 49 €/Jahr · kein App Store · keine Kreditkarte zum Start
</p> </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> <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> </div>
</section> </section>
@ -599,5 +599,6 @@
</div> </div>
</footer> </footer>
<script src="/js/zuechter.js"></script>
</body> </body>
</html> </html>