Refactor: 1167 _esc() → UI.escape() in 36 Dateien, SW by-v1113

Bündel 1 aus dem Duplikat-Audit: existierende zentrale Helper nutzen
statt lokale Duplikate.

Pure Migration ohne neuen Code:
- 1167 _esc()-Aufrufe in 36 Page-Modulen migriert auf UI.escape()
- 24 lokale _esc/_escape-Definitionen entfernt
- lost.js hatte _escape() (Variante) — 17 Aufrufe ebenfalls migriert
- jobs.js + breeder.js: tote Alias-Wrapper entfernt

UI.escape() existierte schon — wurde nur überall lokal nochmal
implementiert. Funktional identisch (gleiche 4-replace-chain für
& < > ").

Tests 19/19 grün. Frontend-LOC um ~120 Zeilen reduziert.

Hinweis: _emptyState (7 Stellen) und _icon (8 Stellen) wurden NICHT
migriert — sie haben abweichende Signaturen von UI.emptyState({...})
bzw. UI.icon(name). Eigener Sprint nötig.
This commit is contained in:
rene 2026-05-27 10:15:33 +02:00
parent e7939ce98e
commit c517c9281d
42 changed files with 1115 additions and 1341 deletions

View file

@ -1 +1 @@
1112 1113

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=1112"></script> <script src="/js/boot-early.js?v=1113"></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=1112"> <link rel="stylesheet" href="/css/design-system.css?v=1113">
<link rel="stylesheet" href="/css/layout.css?v=1112"> <link rel="stylesheet" href="/css/layout.css?v=1113">
<link rel="stylesheet" href="/css/components.css?v=1112"> <link rel="stylesheet" href="/css/components.css?v=1113">
<link rel="stylesheet" href="/css/utilities.css?v=1112"> <link rel="stylesheet" href="/css/utilities.css?v=1113">
<link rel="stylesheet" href="/css/lists.css?v=1112"> <link rel="stylesheet" href="/css/lists.css?v=1113">
</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=1112"></script> <script src="/js/api.js?v=1113"></script>
<script src="/js/ui.js?v=1112"></script> <script src="/js/ui.js?v=1113"></script>
<script src="/js/app.js?v=1112"></script> <script src="/js/app.js?v=1113"></script>
<script src="/js/worlds.js?v=1112"></script> <script src="/js/worlds.js?v=1113"></script>
<script src="/js/offline-indicator.js?v=1112"></script> <script src="/js/offline-indicator.js?v=1113"></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=1112"></script> <script src="/js/boot.js?v=1113"></script>
</body> </body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '1112'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '1113'; // ← 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;

View file

@ -202,7 +202,7 @@ window.Page_admin = (() => {
// Manager-Tabelle // Manager-Tabelle
const managerRows = d.managers.map(m => ` const managerRows = d.managers.map(m => `
<tr> <tr>
<td style="font-weight:600">${_esc(m.name)}</td> <td style="font-weight:600">${UI.escape(m.name)}</td>
<td class="text-right">${m.published}</td> <td class="text-right">${m.published}</td>
<td class="text-right">${m.with_link} <td class="text-right">${m.with_link}
${m.published > 0 ? `<span style="font-size:10px;color:var(--c-text-muted)"> ${m.published > 0 ? `<span style="font-size:10px;color:var(--c-text-muted)">
@ -241,13 +241,13 @@ window.Page_admin = (() => {
<tr> <tr>
<td style="color:var(--c-text-muted);white-space:nowrap">${_fmt(p.published_at)}</td> <td style="color:var(--c-text-muted);white-space:nowrap">${_fmt(p.published_at)}</td>
<td style="font-weight:500;max-width:200px;overflow:hidden;text-overflow:ellipsis; <td style="font-weight:500;max-width:200px;overflow:hidden;text-overflow:ellipsis;
white-space:nowrap">${_esc(p.topic)}</td> white-space:nowrap">${UI.escape(p.topic)}</td>
<td>${_PL[p.platform]||p.platform||''}</td> <td>${_PL[p.platform]||p.platform||''}</td>
<td style="font-size:10px;color:var(--c-text-muted)">${_esc(p.category||'')}</td> <td style="font-size:10px;color:var(--c-text-muted)">${UI.escape(p.category||'')}</td>
<td>${p.ai_score ? '⭐'.repeat(p.ai_score) : ''}</td> <td>${p.ai_score ? '⭐'.repeat(p.ai_score) : ''}</td>
<td style="font-weight:500">${_esc(p.manager||'')}</td> <td style="font-weight:500">${UI.escape(p.manager||'')}</td>
<td>${p.post_url <td>${p.post_url
? `<a href="${_esc(p.post_url)}" target="_blank" rel="noopener" ? `<a href="${UI.escape(p.post_url)}" target="_blank" rel="noopener"
style="font-size:11px;color:var(--c-primary)">🔗 Link</a>` style="font-size:11px;color:var(--c-primary)">🔗 Link</a>`
: `<span style="font-size:11px;color:var(--c-text-muted)"></span>`}</td> : `<span style="font-size:11px;color:var(--c-text-muted)"></span>`}</td>
</tr>`).join(''); </tr>`).join('');
@ -319,7 +319,7 @@ window.Page_admin = (() => {
try { d = await API.get('/admin/analytics'); } try { d = await API.get('/admin/analytics'); }
catch (err) { catch (err) {
el.innerHTML = `<div style="padding:var(--space-4);color:var(--c-danger);font-size:var(--text-sm)"> el.innerHTML = `<div style="padding:var(--space-4);color:var(--c-danger);font-size:var(--text-sm)">
${UI.icon('warning')} Fehler: ${_esc(err.message || String(err))}</div>`; ${UI.icon('warning')} Fehler: ${UI.escape(err.message || String(err))}</div>`;
return; return;
} }
@ -396,7 +396,7 @@ window.Page_admin = (() => {
const pct = ((p[valKey] ?? 0) / maxV * 100).toFixed(0); const pct = ((p[valKey] ?? 0) / maxV * 100).toFixed(0);
return `<div> return `<div>
<div style="display:flex;justify-content:space-between;font-size:var(--text-xs);margin-bottom:3px"> <div style="display:flex;justify-content:space-between;font-size:var(--text-xs);margin-bottom:3px">
<span style="color:var(--c-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:78%">${_esc(p[labelKey] || '—')}</span> <span style="color:var(--c-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:78%">${UI.escape(p[labelKey] || '—')}</span>
<span style="color:var(--c-text-secondary);flex-shrink:0;margin-left:var(--space-2)">${fmt(p[valKey] ?? 0)}</span> <span style="color:var(--c-text-secondary);flex-shrink:0;margin-left:var(--space-2)">${fmt(p[valKey] ?? 0)}</span>
</div> </div>
<div style="height:4px;border-radius:2px;background:var(--c-surface-2)"> <div style="height:4px;border-radius:2px;background:var(--c-surface-2)">
@ -549,7 +549,7 @@ window.Page_admin = (() => {
box-shadow:0 0 4px ${dot}"></span> box-shadow:0 0 4px ${dot}"></span>
<span style="font-size:var(--text-xs);color:var(--c-text-secondary);font-weight:600">${label}</span> <span style="font-size:var(--text-xs);color:var(--c-text-secondary);font-weight:600">${label}</span>
<span class="text-xs-muted">·</span> <span class="text-xs-muted">·</span>
<span style="font-size:var(--text-xs);color:var(--c-text-muted);font-family:monospace">${_esc(model)}</span> <span style="font-size:var(--text-xs);color:var(--c-text-muted);font-family:monospace">${UI.escape(model)}</span>
<span style="margin-left:auto;font-size:10px;padding:1px 6px;border-radius:10px; <span style="margin-left:auto;font-size:10px;padding:1px 6px;border-radius:10px;
background:var(--c-surface);color:var(--c-text-muted);border:1px solid var(--c-border)"> background:var(--c-surface);color:var(--c-text-muted);border:1px solid var(--c-border)">
Modus: ${ki.local_reachable ? 'local' : 'cloud'} Modus: ${ki.local_reachable ? 'local' : 'cloud'}
@ -641,8 +641,8 @@ window.Page_admin = (() => {
</tr></thead> </tr></thead>
<tbody> <tbody>
${(kiH.top_users).map(u => `<tr> ${(kiH.top_users).map(u => `<tr>
<td style="font-weight:600">${_esc(u.name)}</td> <td style="font-weight:600">${UI.escape(u.name)}</td>
<td class="text-muted">${_esc(u.email.length > 22 ? u.email.split('@')[1] : u.email)}</td> <td class="text-muted">${UI.escape(u.email.length > 22 ? u.email.split('@')[1] : u.email)}</td>
<td style="color:var(--c-primary);font-weight:600">${u.cloud}</td> <td style="color:var(--c-primary);font-weight:600">${u.cloud}</td>
<td>${u.total}</td> <td>${u.total}</td>
<td class="text-muted">${u.last_date?.slice(5) || '—'}</td> <td class="text-muted">${u.last_date?.slice(5) || '—'}</td>
@ -837,24 +837,24 @@ window.Page_admin = (() => {
background:var(--c-surface-2); background:var(--c-surface-2);
display:flex;align-items:center;justify-content:center; display:flex;align-items:center;justify-content:center;
font-weight:var(--weight-bold);color:var(--c-text-secondary)"> font-weight:var(--weight-bold);color:var(--c-text-secondary)">
${_esc(u.name[0].toUpperCase())} ${UI.escape(u.name[0].toUpperCase())}
</div> </div>
<!-- Info --> <!-- Info -->
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)"> <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)">
${_esc(u.name)} ${UI.escape(u.name)}
${u.is_banned ? `<span style="font-size:10px;padding:1px 5px;border-radius:3px; ${u.is_banned ? `<span style="font-size:10px;padding:1px 5px;border-radius:3px;
background:var(--c-danger);color:#fff;margin-left:4px"> background:var(--c-danger);color:#fff;margin-left:4px">
GESPERRT</span>` : ''} GESPERRT</span>` : ''}
</div> </div>
<div class="text-xs-muted"> <div class="text-xs-muted">
${_esc(u.email)} · ${UI.escape(u.email)} ·
<span style="color:${u.rolle === 'admin' ? 'var(--c-danger)' : u.rolle === 'moderator' ? '#f59e0b' : 'var(--c-text-muted)'}"> <span style="color:${u.rolle === 'admin' ? 'var(--c-danger)' : u.rolle === 'moderator' ? '#f59e0b' : 'var(--c-text-muted)'}">
${_esc(u.rolle)} ${UI.escape(u.rolle)}
</span> </span>
· <span style="color:${u.subscription_tier && u.subscription_tier !== 'standard' ? 'var(--c-primary)' : 'var(--c-text-muted)'}"> · <span style="color:${u.subscription_tier && u.subscription_tier !== 'standard' ? 'var(--c-primary)' : 'var(--c-text-muted)'}">
${_esc(u.subscription_tier || 'standard')} ${UI.escape(u.subscription_tier || 'standard')}
</span> </span>
· ${u.dog_count} Hund${u.dog_count !== 1 ? 'e' : ''} · ${u.dog_count} Hund${u.dog_count !== 1 ? 'e' : ''}
· ${u.thread_count} Threads · ${u.thread_count} Threads
@ -875,28 +875,28 @@ window.Page_admin = (() => {
<!-- Aktionen --> <!-- Aktionen -->
<div style="display:flex;gap:var(--space-1);flex-shrink:0"> <div style="display:flex;gap:var(--space-1);flex-shrink:0">
${u.is_banned ${u.is_banned
? `<button class="btn btn-sm btn-ghost adm-unban" data-uid="${u.id}" data-name="${_esc(u.name)}" ? `<button class="btn btn-sm btn-ghost adm-unban" data-uid="${u.id}" data-name="${UI.escape(u.name)}"
title="Sperre aufheben" class="text-success"> title="Sperre aufheben" class="text-success">
<svg class="ph-icon"><use href="/icons/phosphor.svg#lock-open"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#lock-open"></use></svg>
</button>` </button>`
: `<button class="btn btn-sm btn-ghost adm-ban" data-uid="${u.id}" data-name="${_esc(u.name)}" : `<button class="btn btn-sm btn-ghost adm-ban" data-uid="${u.id}" data-name="${UI.escape(u.name)}"
title="Sperren" class="text-danger"> title="Sperren" class="text-danger">
<svg class="ph-icon"><use href="/icons/phosphor.svg#lock"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#lock"></use></svg>
</button>` </button>`
} }
${isAdmin ? ` ${isAdmin ? `
<button class="btn btn-sm btn-ghost adm-rolle" data-uid="${u.id}" <button class="btn btn-sm btn-ghost adm-rolle" data-uid="${u.id}"
data-name="${_esc(u.name)}" data-rolle="${_esc(u.rolle)}" data-name="${UI.escape(u.name)}" data-rolle="${UI.escape(u.rolle)}"
title="Rolle ändern"> title="Rolle ändern">
<svg class="ph-icon"><use href="/icons/phosphor.svg#shield"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#shield"></use></svg>
</button> </button>
<button class="btn btn-sm btn-ghost adm-tier" data-uid="${u.id}" <button class="btn btn-sm btn-ghost adm-tier" data-uid="${u.id}"
data-name="${_esc(u.name)}" data-tier="${_esc(u.subscription_tier || 'standard')}" data-name="${UI.escape(u.name)}" data-tier="${UI.escape(u.subscription_tier || 'standard')}"
title="Abo-Stufe ändern"> title="Abo-Stufe ändern">
<svg class="ph-icon"><use href="/icons/phosphor.svg#star"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#star"></use></svg>
</button> </button>
<button class="btn btn-sm btn-ghost adm-delete" data-uid="${u.id}" <button class="btn btn-sm btn-ghost adm-delete" data-uid="${u.id}"
data-name="${_esc(u.name)}" title="Löschen" data-name="${UI.escape(u.name)}" title="Löschen"
class="text-danger"> class="text-danger">
<svg class="ph-icon"><use href="/icons/phosphor.svg#trash"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#trash"></use></svg>
</button> </button>
@ -1087,18 +1087,18 @@ window.Page_admin = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-1)"> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-1)">
${r.resolved ? '✓ Erledigt · ' : ''} ${r.resolved ? '✓ Erledigt · ' : ''}
${_esc(r.target_type)} #${r.target_id} · ${UI.escape(r.target_type)} #${r.target_id} ·
Gemeldet von <strong>${_esc(r.melder_name)}</strong> Gemeldet von <strong>${UI.escape(r.melder_name)}</strong>
</div> </div>
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-1)"> color:var(--c-text);margin-bottom:var(--space-1)">
Grund: ${_esc(r.grund)} Grund: ${UI.escape(r.grund)}
</div> </div>
${r.content_preview ? ` ${r.content_preview ? `
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);background:var(--c-surface-2); padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-sm)"> border-radius:var(--radius-sm)">
${_esc(r.content_preview)} ${UI.escape(r.content_preview)}
</div>` : ''} </div>` : ''}
</div> </div>
<div style="display:flex;flex-direction:column;gap:var(--space-2);flex-shrink:0"> <div style="display:flex;flex-direction:column;gap:var(--space-2);flex-shrink:0">
@ -1175,10 +1175,10 @@ window.Page_admin = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> color:var(--c-text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
${t.is_deleted ? '<s>' : ''}${_esc(t.titel)}${t.is_deleted ? '</s>' : ''} ${t.is_deleted ? '<s>' : ''}${UI.escape(t.titel)}${t.is_deleted ? '</s>' : ''}
</div> </div>
<div class="text-xs-muted"> <div class="text-xs-muted">
von ${_esc(t.autor_name)} · von ${UI.escape(t.autor_name)} ·
${t.antworten} Antworten · ${t.antworten} Antworten ·
${t.is_pinned ? '📌 ' : ''}${t.is_locked ? '🔒 ' : ''}${t.is_deleted ? '🗑 gelöscht' : ''} ${t.is_pinned ? '📌 ' : ''}${t.is_locked ? '🔒 ' : ''}${t.is_deleted ? '🗑 gelöscht' : ''}
</div> </div>
@ -1313,8 +1313,8 @@ window.Page_admin = (() => {
return `<div style="border-bottom:1px solid var(--c-border);padding:2px 0">` + return `<div style="border-bottom:1px solid var(--c-border);padding:2px 0">` +
`<span class="text-muted">${r.t}</span> ` + `<span class="text-muted">${r.t}</span> ` +
`<span style="color:${color};font-weight:600">${r.l}</span> ` + `<span style="color:${color};font-weight:600">${r.l}</span> ` +
`<span class="text-secondary">${_esc(r.n)}</span> ` + `<span class="text-secondary">${UI.escape(r.n)}</span> ` +
`<span>${_esc(r.m)}</span></div>`; `<span>${UI.escape(r.m)}</span></div>`;
}).join('') || '<span class="text-muted">Keine Einträge</span>'; }).join('') || '<span class="text-muted">Keine Einträge</span>';
}; };
el.querySelector('#adm-sys-refresh').addEventListener('click', () => { el.querySelector('#adm-sys-refresh').addEventListener('click', () => {
@ -1385,13 +1385,13 @@ window.Page_admin = (() => {
const scoreBar = v => `<span style="color:${scoreColor(v)};font-weight:600">${v.toFixed(1)}</span>`; const scoreBar = v => `<span style="color:${scoreColor(v)};font-weight:600">${v.toFixed(1)}</span>`;
const rows = d.results.filter(r => !r.error).map(r => const rows = d.results.filter(r => !r.error).map(r =>
`<tr> `<tr>
<td style="padding:2px 6px">${_esc(r.name)}</td> <td style="padding:2px 6px">${UI.escape(r.name)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.vollstaendigkeit)}</td> <td style="text-align:center;padding:2px 6px">${scoreBar(r.vollstaendigkeit)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.korrektheit)}</td> <td style="text-align:center;padding:2px 6px">${scoreBar(r.korrektheit)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.sprachqualitaet)}</td> <td style="text-align:center;padding:2px 6px">${scoreBar(r.sprachqualitaet)}</td>
<td style="text-align:center;padding:2px 6px">${scoreBar(r.konsistenz)}</td> <td style="text-align:center;padding:2px 6px">${scoreBar(r.konsistenz)}</td>
<td style="text-align:center;padding:2px 6px;font-weight:700">${scoreBar(r.gesamt)}</td> <td style="text-align:center;padding:2px 6px;font-weight:700">${scoreBar(r.gesamt)}</td>
<td style="padding:2px 6px;color:var(--c-text-muted);font-size:0.9em">${_esc(r.hinweis || '')}</td> <td style="padding:2px 6px;color:var(--c-text-muted);font-size:0.9em">${UI.escape(r.hinweis || '')}</td>
</tr>` </tr>`
).join(''); ).join('');
box.style.display = 'block'; box.style.display = 'block';
@ -1459,9 +1459,9 @@ window.Page_admin = (() => {
</div> </div>
</div> </div>
<div style="margin-top:var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:4px"> <div style="margin-top:var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:4px">
<span>Python ${_esc(s.python_version)}</span> <span>Python ${UI.escape(s.python_version)}</span>
<span style="font-family:monospace;font-size:var(--text-xs);background:var(--c-surface-2);padding:2px 8px;border-radius:4px;color:var(--c-text-secondary)"> <span style="font-family:monospace;font-size:var(--text-xs);background:var(--c-surface-2);padding:2px 8px;border-radius:4px;color:var(--c-text-secondary)">
APP v${typeof APP_VER !== 'undefined' ? APP_VER : '—'} · ${_esc(s.sw_version || '?')} APP v${typeof APP_VER !== 'undefined' ? APP_VER : '—'} · ${UI.escape(s.sw_version || '?')}
</span> </span>
</div> </div>
</div> </div>
@ -1504,9 +1504,9 @@ window.Page_admin = (() => {
const userRows = topUsers.map(u => { const userRows = topUsers.map(u => {
const emailDisplay = (u.email || '').length > 20 const emailDisplay = (u.email || '').length > 20
? '@' + (u.email || '').split('@')[1] ? '@' + (u.email || '').split('@')[1]
: _esc(u.email || ''); : UI.escape(u.email || '');
return `<tr> return `<tr>
<td style="padding:5px 8px;font-weight:500">${_esc(u.name || '')}</td> <td style="padding:5px 8px;font-weight:500">${UI.escape(u.name || '')}</td>
<td style="padding:5px 8px;color:var(--c-text-muted);font-size:var(--text-xs)">${emailDisplay}</td> <td style="padding:5px 8px;color:var(--c-text-muted);font-size:var(--text-xs)">${emailDisplay}</td>
<td style="padding:5px 8px;text-align:right;font-weight:600">${u.total ?? 0}</td> <td style="padding:5px 8px;text-align:right;font-weight:600">${u.total ?? 0}</td>
<td style="padding:5px 8px;text-align:right;color:var(--c-text-muted);font-size:var(--text-xs)">${u.last_week || ''}</td> <td style="padding:5px 8px;text-align:right;color:var(--c-text-muted);font-size:var(--text-xs)">${u.last_week || ''}</td>
@ -1549,7 +1549,7 @@ window.Page_admin = (() => {
<div style="display:flex;justify-content:space-between;font-size:var(--text-xs); <div style="display:flex;justify-content:space-between;font-size:var(--text-xs);
color:var(--c-text-muted);margin-bottom:var(--space-4)"> color:var(--c-text-muted);margin-bottom:var(--space-4)">
<span>30 Tage</span> <span>30 Tage</span>
<span>${_esc(lastDate)}</span> <span>${UI.escape(lastDate)}</span>
</div> </div>
${topUsers.length ? ` ${topUsers.length ? `
@ -1704,12 +1704,12 @@ window.Page_admin = (() => {
</tr></thead><tbody> </tr></thead><tbody>
${zuchterPending.map((z, i) => ` ${zuchterPending.map((z, i) => `
<tr style="${i%2===1?'background:var(--c-surface-2)':''}"> <tr style="${i%2===1?'background:var(--c-surface-2)':''}">
<td class="adm-td" style="font-weight:var(--weight-semibold)">${_esc(z.rasse_slug)}</td> <td class="adm-td" style="font-weight:var(--weight-semibold)">${UI.escape(z.rasse_slug)}</td>
<td class="adm-td">${_esc(z.name)}${z.zwingername ? `<br><span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(z.zwingername)}</span>` : ''}</td> <td class="adm-td">${UI.escape(z.name)}${z.zwingername ? `<br><span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(z.zwingername)}</span>` : ''}</td>
<td class="adm-td">${_esc([z.plz, z.ort, z.bundesland].filter(Boolean).join(' '))}</td> <td class="adm-td">${UI.escape([z.plz, z.ort, z.bundesland].filter(Boolean).join(' '))}</td>
<td class="adm-td">${z.vdh_mitglied ? `<span style="color:var(--c-success);display:flex;align-items:center;gap:2px"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg> VDH</span>` : '—'}</td> <td class="adm-td">${z.vdh_mitglied ? `<span style="color:var(--c-success);display:flex;align-items:center;gap:2px"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg> VDH</span>` : '—'}</td>
<td class="adm-td">${_ageLabel(z.created_at)}</td> <td class="adm-td">${_ageLabel(z.created_at)}</td>
<td class="adm-td">${z.website ? `<a href="${_esc(z.website)}" target="_blank" style="color:var(--c-primary);font-size:var(--text-xs)">Link</a>` : '—'}</td> <td class="adm-td">${z.website ? `<a href="${UI.escape(z.website)}" target="_blank" style="color:var(--c-primary);font-size:var(--text-xs)">Link</a>` : '—'}</td>
<td class="adm-td" style="text-align:right;white-space:nowrap"> <td class="adm-td" style="text-align:right;white-space:nowrap">
<button class="btn btn-sm btn-primary adm-zuchter-approve" data-id="${z.id}" style="margin-right:4px"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg> Freigeben</button> <button class="btn btn-sm btn-primary adm-zuchter-approve" data-id="${z.id}" style="margin-right:4px"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#check"></use></svg> Freigeben</button>
<button class="btn btn-sm btn-ghost adm-zuchter-delete" data-id="${z.id}" class="text-danger"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg></button> <button class="btn btn-sm btn-ghost adm-zuchter-delete" data-id="${z.id}" class="text-danger"><svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#x"></use></svg></button>
@ -1719,8 +1719,8 @@ window.Page_admin = (() => {
} }
// Züchter-History // Züchter-History
if (zuchterDone.length) html += _historySection('Züchter-Einreichungen', zuchterDone, if (zuchterDone.length) html += _historySection('Züchter-Einreichungen', zuchterDone,
z => `<span style="font-weight:600">${_esc(z.name)}</span> · ${_esc(z.rasse_slug)} · z => `<span style="font-weight:600">${UI.escape(z.name)}</span> · ${UI.escape(z.rasse_slug)} ·
${UI.icon('check-circle')} ${_esc(z.verified_by_name||'?')} · ${(z.verified_at||'').slice(0,10)}`); ${UI.icon('check-circle')} ${UI.escape(z.verified_by_name||'?')} · ${(z.verified_at||'').slice(0,10)}`);
// --- Wiki-Foto-Einreichungen --- // --- Wiki-Foto-Einreichungen ---
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold); html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
@ -1737,12 +1737,12 @@ window.Page_admin = (() => {
html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--space-4);margin-bottom:var(--space-3)"> html += `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:var(--space-4);margin-bottom:var(--space-3)">
${fotosPending.map(f => ` ${fotosPending.map(f => `
<div class="card p-4"> <div class="card p-4">
<img src="${_esc(f.foto_url)}" alt="" <img src="${UI.escape(f.foto_url)}" alt=""
style="width:100%;height:140px;object-fit:cover;border-radius:var(--radius-md);margin-bottom:var(--space-3)"> style="width:100%;height:140px;object-fit:cover;border-radius:var(--radius-md);margin-bottom:var(--space-3)">
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">${_esc(f.rasse_name)}</div> <div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">${UI.escape(f.rasse_name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">von ${_esc(f.user_name)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">von ${UI.escape(f.user_name)}</div>
<div class="mb-3">${_ageLabel(f.created_at)}</div> <div class="mb-3">${_ageLabel(f.created_at)}</div>
${f.aktuell_foto ? `<img src="${_esc(f.aktuell_foto)}" alt="Aktuell" ${f.aktuell_foto ? `<img src="${UI.escape(f.aktuell_foto)}" alt="Aktuell"
style="width:100%;height:80px;object-fit:cover;border-radius:var(--radius-sm); style="width:100%;height:80px;object-fit:cover;border-radius:var(--radius-sm);
opacity:.5;margin-bottom:var(--space-2)"> opacity:.5;margin-bottom:var(--space-2)">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-3)"> aktuelles Foto</div>` : ''} <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-3)"> aktuelles Foto</div>` : ''}
@ -1756,10 +1756,10 @@ window.Page_admin = (() => {
// Fotos-History // Fotos-History
if (fotosDone.length) html += _historySection('Foto-Einreichungen', fotosDone, if (fotosDone.length) html += _historySection('Foto-Einreichungen', fotosDone,
f => `<img src="${_esc(f.foto_url)}" style="width:32px;height:32px;object-fit:cover;border-radius:4px;vertical-align:middle;margin-right:6px"> f => `<img src="${UI.escape(f.foto_url)}" style="width:32px;height:32px;object-fit:cover;border-radius:4px;vertical-align:middle;margin-right:6px">
<span style="font-weight:600">${_esc(f.rasse_name||'?')}</span> · von ${_esc(f.user_name||'?')} · <span style="font-weight:600">${UI.escape(f.rasse_name||'?')}</span> · von ${UI.escape(f.user_name||'?')} ·
${f.status==='approved' ? `${UI.icon('check-circle')} genehmigt` : `${UI.icon('x-circle')} abgelehnt`} ${f.status==='approved' ? `${UI.icon('check-circle')} genehmigt` : `${UI.icon('x-circle')} abgelehnt`}
${f.reviewed_by_name ? ` von ${_esc(f.reviewed_by_name)}` : ''} · ${(f.reviewed_at||'').slice(0,10)}`); ${f.reviewed_by_name ? ` von ${UI.escape(f.reviewed_by_name)}` : ''} · ${(f.reviewed_at||'').slice(0,10)}`);
// --- Forum-Meldungen --- // --- Forum-Meldungen ---
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold); html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
@ -1780,16 +1780,16 @@ window.Page_admin = (() => {
<div style="display:flex;align-items:flex-start;gap:var(--space-3)"> <div style="display:flex;align-items:flex-start;gap:var(--space-3)">
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-1);display:flex;align-items:center;flex-wrap:wrap;gap:4px"> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-1);display:flex;align-items:center;flex-wrap:wrap;gap:4px">
${_esc(r.target_type)} #${r.target_id} · Gemeldet von <strong>${_esc(r.melder_name || '?')}</strong> ${UI.escape(r.target_type)} #${r.target_id} · Gemeldet von <strong>${UI.escape(r.melder_name || '?')}</strong>
${_ageLabel(r.created_at)} ${_ageLabel(r.created_at)}
</div> </div>
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);margin-bottom:var(--space-1)"> <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);margin-bottom:var(--space-1)">
Grund: ${_esc(r.grund)} Grund: ${UI.escape(r.grund)}
</div> </div>
${r.content_preview ? ` ${r.content_preview ? `
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);background:var(--c-surface-2); padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-sm)">${_esc(r.content_preview)}</div>` : ''} border-radius:var(--radius-sm)">${UI.escape(r.content_preview)}</div>` : ''}
</div> </div>
<button class="btn btn-sm btn-primary adm-mod-resolve" data-rid="${r.id}" title="Als erledigt markieren"> <button class="btn btn-sm btn-primary adm-mod-resolve" data-rid="${r.id}" title="Als erledigt markieren">
${UI.icon('check')} ${UI.icon('check')}
@ -1801,8 +1801,8 @@ window.Page_admin = (() => {
// Meldungen-History // Meldungen-History
if (reportsDone.length) html += _historySection('Forum-Meldungen', reportsDone, if (reportsDone.length) html += _historySection('Forum-Meldungen', reportsDone,
r => `${_esc(r.target_type)} #${r.target_id} · ${_esc(r.grund)} · Gemeldet von ${_esc(r.melder_name||'?')} · r => `${UI.escape(r.target_type)} #${r.target_id} · ${UI.escape(r.grund)} · Gemeldet von ${UI.escape(r.melder_name||'?')} ·
${UI.icon('check-circle')} ${_esc(r.resolved_by_name||'?')} · ${(r.resolved_at||'').slice(0,10)}`); ${UI.icon('check-circle')} ${UI.escape(r.resolved_by_name||'?')} · ${(r.resolved_at||'').slice(0,10)}`);
// --- POI-Korrekturen --- // --- POI-Korrekturen ---
html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold); html += `<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
@ -1831,11 +1831,11 @@ window.Page_admin = (() => {
<tbody> <tbody>
${poiPending.map((e, i) => ` ${poiPending.map((e, i) => `
<tr style="${i%2===1?'background:var(--c-surface-2)':''}"> <tr style="${i%2===1?'background:var(--c-surface-2)':''}">
<td class="adm-td" style="font-weight:var(--weight-semibold)">${_esc(e.poi_name || `OSM #${e.osm_id}`)}</td> <td class="adm-td" style="font-weight:var(--weight-semibold)">${UI.escape(e.poi_name || `OSM #${e.osm_id}`)}</td>
<td class="adm-td"><code class="text-xs">${_esc(e.field)}</code></td> <td class="adm-td"><code class="text-xs">${UI.escape(e.field)}</code></td>
<td class="adm-td" style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(e.old_value || '—')}</td> <td class="adm-td" style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(e.old_value || '—')}</td>
<td class="adm-td text-xs">${_esc(e.new_value || '—')}</td> <td class="adm-td text-xs">${UI.escape(e.new_value || '—')}</td>
<td class="adm-td text-muted">${_esc(e.einreicher_name || '?')}</td> <td class="adm-td text-muted">${UI.escape(e.einreicher_name || '?')}</td>
<td class="adm-td">${_ageLabel(e.created_at)}</td> <td class="adm-td">${_ageLabel(e.created_at)}</td>
<td class="adm-td" style="text-align:right;white-space:nowrap"> <td class="adm-td" style="text-align:right;white-space:nowrap">
<button class="btn btn-sm btn-primary adm-poi-approve" data-id="${e.id}" style="margin-right:4px"> <button class="btn btn-sm btn-primary adm-poi-approve" data-id="${e.id}" style="margin-right:4px">
@ -1852,12 +1852,12 @@ window.Page_admin = (() => {
} }
// POI-History // POI-History
if (poiDone.length) html += _historySection('POI-Korrekturen', poiDone, if (poiDone.length) html += _historySection('POI-Korrekturen', poiDone,
e => `<span style="font-weight:600">${_esc(e.poi_name||`OSM #${e.osm_id}`)}</span> · e => `<span style="font-weight:600">${UI.escape(e.poi_name||`OSM #${e.osm_id}`)}</span> ·
<code class="text-xs">${_esc(e.field)}</code>: <code class="text-xs">${UI.escape(e.field)}</code>:
<span style="text-decoration:line-through;color:var(--c-text-muted)">${_esc(e.old_value||'—')}</span> <span style="text-decoration:line-through;color:var(--c-text-muted)">${UI.escape(e.old_value||'—')}</span>
${_esc(e.new_value||'—')} · ${UI.escape(e.new_value||'—')} ·
${e.status==='approved' ? `${UI.icon('check-circle')} freigegeben` : `${UI.icon('x-circle')} abgelehnt`} ${e.status==='approved' ? `${UI.icon('check-circle')} freigegeben` : `${UI.icon('x-circle')} abgelehnt`}
${e.mod_name ? ` von ${_esc(e.mod_name)}` : ''} · ${(e.resolved_at||'').slice(0,10)}`); ${e.mod_name ? ` von ${UI.escape(e.mod_name)}` : ''} · ${(e.resolved_at||'').slice(0,10)}`);
el.innerHTML = html; el.innerHTML = html;
@ -1990,17 +1990,17 @@ window.Page_admin = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm); <div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);
color:var(--c-text);margin-bottom:var(--space-1)"> color:var(--c-text);margin-bottom:var(--space-1)">
${_esc(a.name)} ${UI.escape(a.name)}
<span style="font-size:var(--text-xs);color:var(--c-text-muted);font-weight:400;margin-left:6px"> <span style="font-size:var(--text-xs);color:var(--c-text-muted);font-weight:400;margin-left:6px">
${_esc(a.email)} ${UI.escape(a.email)}
</span> </span>
</div> </div>
<div style="display:flex;flex-wrap:wrap;gap:var(--space-3);font-size:var(--text-xs); <div style="display:flex;flex-wrap:wrap;gap:var(--space-3);font-size:var(--text-xs);
color:var(--c-text-secondary);margin-bottom:var(--space-2)"> color:var(--c-text-secondary);margin-bottom:var(--space-2)">
<span>${UI.icon('paw-print')} ${_esc(a.rasse_text || '')}</span> <span>${UI.icon('paw-print')} ${UI.escape(a.rasse_text || '')}</span>
<span>${UI.icon('house-line')} ${_esc(a.zwingername || '')}</span> <span>${UI.icon('house-line')} ${UI.escape(a.zwingername || '')}</span>
<span>${UI.icon('users')} ${_esc(a.verein || '')}</span> <span>${UI.icon('users')} ${UI.escape(a.verein || '')}</span>
<span>${UI.icon('map-pin')} ${_esc(a.stadt || '')}</span> <span>${UI.icon('map-pin')} ${UI.escape(a.stadt || '')}</span>
<span style="color:${a.vdh_mitglied ? 'var(--c-success)' : 'var(--c-text-muted)'}"> <span style="color:${a.vdh_mitglied ? 'var(--c-success)' : 'var(--c-text-muted)'}">
${UI.icon('certificate')} VDH: ${a.vdh_mitglied ? 'ja' : 'nein'} ${UI.icon('certificate')} VDH: ${a.vdh_mitglied ? 'ja' : 'nein'}
</span> </span>
@ -2010,7 +2010,7 @@ window.Page_admin = (() => {
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);background:var(--c-surface-2); padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-sm);margin-top:var(--space-1)"> border-radius:var(--radius-sm);margin-top:var(--space-1)">
${_esc(a.beschreibung)} ${UI.escape(a.beschreibung)}
</div>` : ''} </div>` : ''}
</div> </div>
@ -2021,11 +2021,11 @@ window.Page_admin = (() => {
${UI.icon('file-text')} Dokumente ${UI.icon('file-text')} Dokumente
</button> </button>
<button class="btn btn-sm btn-primary adm-breeder-approve" <button class="btn btn-sm btn-primary adm-breeder-approve"
data-uid="${a.user_id || a.id}" data-name="${_esc(a.name)}"> data-uid="${a.user_id || a.id}" data-name="${UI.escape(a.name)}">
${UI.icon('check')} Freischalten ${UI.icon('check')} Freischalten
</button> </button>
<button class="btn btn-sm btn-ghost adm-breeder-reject" <button class="btn btn-sm btn-ghost adm-breeder-reject"
data-uid="${a.user_id || a.id}" data-name="${_esc(a.name)}" data-uid="${a.user_id || a.id}" data-name="${UI.escape(a.name)}"
class="text-danger"> class="text-danger">
${UI.icon('x')} Ablehnen ${UI.icon('x')} Ablehnen
</button> </button>
@ -2053,11 +2053,11 @@ window.Page_admin = (() => {
body: docs.length body: docs.length
? `<div class="flex-col-gap-3"> ? `<div class="flex-col-gap-3">
${docs.map(d => ` ${docs.map(d => `
<a href="${_esc(API.breeder.documentUrl(uid, d.id))}" <a href="${UI.escape(API.breeder.documentUrl(uid, d.id))}"
target="_blank" rel="noopener" target="_blank" rel="noopener"
class="btn btn-secondary" class="btn btn-secondary"
style="text-align:left;word-break:break-all"> style="text-align:left;word-break:break-all">
${UI.icon('file')} ${_esc(d.filename || d.name || 'Dokument ' + d.id)} ${UI.icon('file')} ${UI.escape(d.filename || d.name || 'Dokument ' + d.id)}
</a>`).join('')} </a>`).join('')}
</div>` </div>`
: `<p class="text-sm-muted">Keine Dokumente hochgeladen.</p>`, : `<p class="text-sm-muted">Keine Dokumente hochgeladen.</p>`,
@ -2155,12 +2155,12 @@ window.Page_admin = (() => {
const rows = breeders.map(b => ` const rows = breeders.map(b => `
<tr> <tr>
<td style="padding:var(--space-2) var(--space-3)"> <td style="padding:var(--space-2) var(--space-3)">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(b.name)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(b.name)}</div>
<div class="text-xs-muted">${_esc(b.email)}</div> <div class="text-xs-muted">${UI.escape(b.email)}</div>
</td> </td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${_esc(b.zwingername || '—')}</td> <td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${UI.escape(b.zwingername || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.rasse_text || '—')}</td> <td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.escape(b.rasse_text || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.stadt || '—')}</td> <td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.escape(b.stadt || '—')}</td>
<td style="padding:var(--space-2) var(--space-3);text-align:center;font-size:var(--text-xs)"> <td style="padding:var(--space-2) var(--space-3);text-align:center;font-size:var(--text-xs)">
${b.wuerfe_count || 0} Würfe<br> ${b.wuerfe_count || 0} Würfe<br>
<span class="text-muted">${b.hunde_count || 0} Hunde</span> <span class="text-muted">${b.hunde_count || 0} Hunde</span>
@ -2171,7 +2171,7 @@ window.Page_admin = (() => {
</td> </td>
<td style="padding:var(--space-2) var(--space-3)"> <td style="padding:var(--space-2) var(--space-3)">
<button class="btn btn-sm btn-ghost adm-breeder-tier-btn" <button class="btn btn-sm btn-ghost adm-breeder-tier-btn"
data-uid="${b.id}" data-name="${_esc(b.name)}" data-tier="${_esc(b.subscription_tier || 'standard')}" data-uid="${b.id}" data-name="${UI.escape(b.name)}" data-tier="${UI.escape(b.subscription_tier || 'standard')}"
class="text-xs"> class="text-xs">
Abo Abo
</button> </button>
@ -2241,17 +2241,17 @@ window.Page_admin = (() => {
${jobs.map((j, i) => ` ${jobs.map((j, i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}"> <tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td class="adm-td" style="font-weight:var(--weight-semibold);color:var(--c-text)"> <td class="adm-td" style="font-weight:var(--weight-semibold);color:var(--c-text)">
${_esc(j.name)} ${UI.escape(j.name)}
<div class="adm-job-id">${_esc(j.id)}</div> <div class="adm-job-id">${UI.escape(j.id)}</div>
</td> </td>
<td class="adm-td" style="color:var(--c-text-secondary);white-space:nowrap"> <td class="adm-td" style="color:var(--c-text-secondary);white-space:nowrap">
${j.next_run_time ? _formatDateTime(j.next_run_time) : '<span class="text-muted">—</span>'} ${j.next_run_time ? _formatDateTime(j.next_run_time) : '<span class="text-muted">—</span>'}
</td> </td>
<td class="adm-td adm-td-trigger"> <td class="adm-td adm-td-trigger">
${_esc(j.trigger)} ${UI.escape(j.trigger)}
</td> </td>
<td class="adm-td text-right"> <td class="adm-td text-right">
<button class="btn btn-sm btn-ghost adm-job-trigger adm-icon-btn" data-id="${_esc(j.id)}" data-name="${_esc(j.name)}" <button class="btn btn-sm btn-ghost adm-job-trigger adm-icon-btn" data-id="${UI.escape(j.id)}" data-name="${UI.escape(j.name)}"
title="Jetzt ausführen" class="text-primary"> title="Jetzt ausführen" class="text-primary">
${UI.icon('play')} ${UI.icon('play')}
</button> </button>
@ -2542,11 +2542,11 @@ window.Page_admin = (() => {
background:var(--c-bg-elevated);border-radius:var(--radius-md);border:1px solid var(--c-border)"> background:var(--c-bg-elevated);border-radius:var(--radius-md);border:1px solid var(--c-border)">
<div class="flex-1-min"> <div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2)"> <div style="display:flex;align-items:center;gap:var(--space-2)">
<span style="font-size:var(--text-sm);font-weight:600">${_esc(t.label)}</span> <span style="font-size:var(--text-sm);font-weight:600">${UI.escape(t.label)}</span>
${accountBadge(t.from_account)} ${accountBadge(t.from_account)}
</div> </div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> <div style="font-size:var(--text-xs);color:var(--c-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(t.subject)} ${UI.escape(t.subject)}
</div> </div>
</div> </div>
<div style="display:flex;gap:var(--space-2);flex-shrink:0"> <div style="display:flex;gap:var(--space-2);flex-shrink:0">
@ -2632,9 +2632,9 @@ window.Page_admin = (() => {
onmouseover="this.style.background='var(--c-surface-2)'" onmouseover="this.style.background='var(--c-surface-2)'"
onmouseout="this.style.background=''"> onmouseout="this.style.background=''">
<td class="p-2">${accountBadge(l.from_account)}</td> <td class="p-2">${accountBadge(l.from_account)}</td>
<td class="p-2">${_esc(l.recipient)}</td> <td class="p-2">${UI.escape(l.recipient)}</td>
<td style="padding:var(--space-2);color:var(--c-text-secondary)">${_esc(l.subject)}</td> <td style="padding:var(--space-2);color:var(--c-text-secondary)">${UI.escape(l.subject)}</td>
<td style="padding:var(--space-2);color:var(--c-text-muted)">${_esc(l.sent_by_name || '')}</td> <td style="padding:var(--space-2);color:var(--c-text-muted)">${UI.escape(l.sent_by_name || '')}</td>
<td style="padding:var(--space-2);color:var(--c-text-muted)">${(l.sent_at||'').slice(0,16).replace('T',' ')}</td> <td style="padding:var(--space-2);color:var(--c-text-muted)">${(l.sent_at||'').slice(0,16).replace('T',' ')}</td>
</tr>`).join('')} </tr>`).join('')}
</tbody> </tbody>
@ -2650,17 +2650,17 @@ window.Page_admin = (() => {
const l = log[Number(row.dataset.logIdx)]; const l = log[Number(row.dataset.logIdx)];
if (!l) return; if (!l) return;
UI.modal.open({ UI.modal.open({
title: _esc(l.subject), title: UI.escape(l.subject),
body: ` body: `
<div style="margin-bottom:var(--space-3);font-size:var(--text-sm);color:var(--c-text-muted)"> <div style="margin-bottom:var(--space-3);font-size:var(--text-sm);color:var(--c-text-muted)">
<strong>An:</strong> ${_esc(l.recipient)} &nbsp;·&nbsp; <strong>An:</strong> ${UI.escape(l.recipient)} &nbsp;·&nbsp;
<strong>Von:</strong> ${_esc(l.from_account)}@banyaro.app &nbsp;·&nbsp; <strong>Von:</strong> ${UI.escape(l.from_account)}@banyaro.app &nbsp;·&nbsp;
${(l.sent_at||'').slice(0,16).replace('T',' ')} ${(l.sent_at||'').slice(0,16).replace('T',' ')}
</div> </div>
<pre style="white-space:pre-wrap;font-family:inherit;font-size:var(--text-sm); <pre style="white-space:pre-wrap;font-family:inherit;font-size:var(--text-sm);
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)">${_esc(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" onclick="UI.modal.close()">Schließen</button>`,
}); });
}); });
@ -2733,7 +2733,7 @@ window.Page_admin = (() => {
<div> <div>
<label class="form-label text-xs">Name (intern)</label> <label class="form-label text-xs">Name (intern)</label>
<input class="form-control" id="${id}-key" type="text" placeholder="z.B. willkommen_neu" <input class="form-control" id="${id}-key" type="text" placeholder="z.B. willkommen_neu"
value="${_esc(tpl?.key || '')}" ${isNew ? '' : 'readonly'}> value="${UI.escape(tpl?.key || '')}" ${isNew ? '' : 'readonly'}>
</div> </div>
<div> <div>
<label class="form-label text-xs">Absender</label> <label class="form-label text-xs">Absender</label>
@ -2746,17 +2746,17 @@ window.Page_admin = (() => {
<div> <div>
<label class="form-label text-xs">Bezeichnung (sichtbar)</label> <label class="form-label text-xs">Bezeichnung (sichtbar)</label>
<input class="form-control" id="${id}-label" type="text" placeholder="z.B. Willkommensnachricht" <input class="form-control" id="${id}-label" type="text" placeholder="z.B. Willkommensnachricht"
value="${_esc(tpl?.label || '')}"> value="${UI.escape(tpl?.label || '')}">
</div> </div>
<div> <div>
<label class="form-label text-xs">Betreff</label> <label class="form-label text-xs">Betreff</label>
<input class="form-control" id="${id}-subject" type="text" <input class="form-control" id="${id}-subject" type="text"
value="${_esc(tpl?.subject || '')}"> value="${UI.escape(tpl?.subject || '')}">
</div> </div>
<div> <div>
<label class="form-label text-xs">Text</label> <label class="form-label text-xs">Text</label>
<textarea id="${id}-body" class="form-control" rows="12" <textarea id="${id}-body" class="form-control" rows="12"
style="font-family:monospace;font-size:var(--text-sm);resize:vertical">${_esc(tpl?.body || '')}</textarea> style="font-family:monospace;font-size:var(--text-sm);resize:vertical">${UI.escape(tpl?.body || '')}</textarea>
</div> </div>
</form>`, </form>`,
footer: ` footer: `
@ -2826,14 +2826,14 @@ window.Page_admin = (() => {
${_formatDateTime(r.created_at)} ${_formatDateTime(r.created_at)}
</td> </td>
<td class="adm-td" style="color:var(--c-text);white-space:nowrap"> <td class="adm-td" style="color:var(--c-text);white-space:nowrap">
${_esc(r.admin_name || '—')} ${UI.escape(r.admin_name || '—')}
</td> </td>
<td class="adm-td"> <td class="adm-td">
<span class="adm-badge-mono">${_esc(r.action)}</span> <span class="adm-badge-mono">${UI.escape(r.action)}</span>
${r.detail ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(r.detail)}</div>` : ''} ${r.detail ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(r.detail)}</div>` : ''}
</td> </td>
<td class="adm-td" style="color:var(--c-text-secondary);font-size:var(--text-xs);white-space:nowrap"> <td class="adm-td" style="color:var(--c-text-secondary);font-size:var(--text-xs);white-space:nowrap">
${_esc(r.target || '—')} ${UI.escape(r.target || '—')}
</td> </td>
</tr> </tr>
`).join('')} `).join('')}
@ -2889,11 +2889,6 @@ window.Page_admin = (() => {
`; `;
} }
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// BEWERBUNGEN — Social-Media-Job // BEWERBUNGEN — Social-Media-Job
// ------------------------------------------------------------------ // ------------------------------------------------------------------
@ -2932,19 +2927,19 @@ window.Page_admin = (() => {
<div class="card" style="margin-bottom:var(--space-3);padding:var(--space-4)" data-id="${r.id}"> <div class="card" style="margin-bottom:var(--space-3);padding:var(--space-4)" data-id="${r.id}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3)"> <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-3)">
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:700;font-size:var(--text-base)">${_esc(r.name)} <div style="font-weight:700;font-size:var(--text-base)">${UI.escape(r.name)}
${r.username ? `<span style="color:var(--c-text-muted);font-weight:400;font-size:var(--text-sm)">(@${_esc(r.username)})</span>` : ''} ${r.username ? `<span style="color:var(--c-text-muted);font-weight:400;font-size:var(--text-sm)">(@${UI.escape(r.username)})</span>` : ''}
</div> </div>
<div style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-top:2px"> <div style="color:var(--c-text-secondary);font-size:var(--text-sm);margin-top:2px">
${_esc(r.email)} · @${_esc(r.social_handle||'—')} ${UI.escape(r.email)} · @${UI.escape(r.social_handle||'—')}
${r.dog_name ? ` · 🐕 ${_esc(r.dog_name)} (${_esc(r.dog_rasse||'')})` : ''} ${r.dog_name ? ` · 🐕 ${UI.escape(r.dog_name)} (${UI.escape(r.dog_rasse||'')})` : ''}
</div> </div>
<div style="color:var(--c-text-muted);font-size:var(--text-xs);margin-top:2px"> <div style="color:var(--c-text-muted);font-size:var(--text-xs);margin-top:2px">
${r.created_at?.slice(0,16).replace('T',' ')} · ${r.doc_count} Anhang/Anhänge ${r.created_at?.slice(0,16).replace('T',' ')} · ${r.doc_count} Anhang/Anhänge
</div> </div>
<div style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary); <div style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary);
background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3)"> background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3)">
${_esc((r.motivation||'').slice(0,200))}${(r.motivation||'').length>200?'…':''} ${UI.escape((r.motivation||'').slice(0,200))}${(r.motivation||'').length>200?'…':''}
</div> </div>
</div> </div>
<div style="display:flex;flex-direction:column;gap:var(--space-2);min-width:120px"> <div style="display:flex;flex-direction:column;gap:var(--space-2);min-width:120px">
@ -2976,25 +2971,25 @@ window.Page_admin = (() => {
const docsHtml = app.docs?.length const docsHtml = app.docs?.length
? app.docs.map(d => `<a href="/api/jobs/admin/applications/${id}/docs/${d.id}" ? app.docs.map(d => `<a href="/api/jobs/admin/applications/${id}/docs/${d.id}"
target="_blank" style="display:block;color:var(--c-primary);font-size:var(--text-sm);margin:4px 0"> target="_blank" style="display:block;color:var(--c-primary);font-size:var(--text-sm);margin:4px 0">
📎 ${_esc(d.filename)}</a>`).join('') 📎 ${UI.escape(d.filename)}</a>`).join('')
: '<span class="text-sm-muted">Keine Anhänge</span>'; : '<span class="text-sm-muted">Keine Anhänge</span>';
UI.modal.open({ UI.modal.open({
title: `Bewerbung — ${_esc(app.name)}`, title: `Bewerbung — ${UI.escape(app.name)}`,
body: ` body: `
<div style="display:grid;gap:var(--space-3)"> <div style="display:grid;gap:var(--space-3)">
<div><b>E-Mail:</b> ${_esc(app.email)}</div> <div><b>E-Mail:</b> ${UI.escape(app.email)}</div>
<div><b>Social:</b> @${_esc(app.social_handle||'')}</div> <div><b>Social:</b> @${UI.escape(app.social_handle||'')}</div>
${app.dog_name ? `<div><b>Hund:</b> ${_esc(app.dog_name)} (${_esc(app.dog_rasse||'')})</div>` : ''} ${app.dog_name ? `<div><b>Hund:</b> ${UI.escape(app.dog_name)} (${UI.escape(app.dog_rasse||'')})</div>` : ''}
<div><b>Motivation:</b><br> <div><b>Motivation:</b><br>
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3); <div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3);
margin-top:var(--space-1);font-size:var(--text-sm);white-space:pre-wrap">${_esc(app.motivation)}</div> margin-top:var(--space-1);font-size:var(--text-sm);white-space:pre-wrap">${UI.escape(app.motivation)}</div>
</div> </div>
<div><b>Anhänge:</b><br>${docsHtml}</div> <div><b>Anhänge:</b><br>${docsHtml}</div>
<div> <div>
<b>Admin-Notiz:</b> <b>Admin-Notiz:</b>
<textarea id="adm-bew-note" class="form-control" rows="2" style="margin-top:var(--space-1)" <textarea id="adm-bew-note" class="form-control" rows="2" style="margin-top:var(--space-1)"
placeholder="Interne Notiz / Nachricht an Bewerber">${_esc(app.admin_note||'')}</textarea> placeholder="Interne Notiz / Nachricht an Bewerber">${UI.escape(app.admin_note||'')}</textarea>
</div> </div>
</div>`, </div>`,
footer: ` footer: `
@ -3052,7 +3047,7 @@ window.Page_admin = (() => {
border:1.5px solid var(--c-border);border-radius:var(--radius-md); border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)"> background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
${Object.entries(KAT_LABEL).map(([k,v]) => ${Object.entries(KAT_LABEL).map(([k,v]) =>
`<option value="${k}">${_esc(v)}</option>` `<option value="${k}">${UI.escape(v)}</option>`
).join('')} ).join('')}
</select> </select>
</div> </div>
@ -3125,7 +3120,7 @@ window.Page_admin = (() => {
text-transform:uppercase;letter-spacing:0.05em; text-transform:uppercase;letter-spacing:0.05em;
padding:var(--space-2) 0;margin-bottom:var(--space-2); padding:var(--space-2) 0;margin-bottom:var(--space-2);
border-bottom:1px solid var(--c-border)"> border-bottom:1px solid var(--c-border)">
${_esc(label)} ${UI.escape(label)}
</div> </div>
`; `;
for (const a of items) { for (const a of items) {
@ -3138,7 +3133,7 @@ window.Page_admin = (() => {
padding:var(--space-3) var(--space-4)"> padding:var(--space-3) var(--space-4)">
<span style="flex:1;font-size:var(--text-sm);font-weight:500; <span style="flex:1;font-size:var(--text-sm);font-weight:500;
${a.aktiv ? '' : 'opacity:0.45;text-decoration:line-through'}"> ${a.aktiv ? '' : 'opacity:0.45;text-decoration:line-through'}">
${_esc(a.frage)} ${UI.escape(a.frage)}
</span> </span>
<span style="font-size:var(--text-xs);color:var(--c-text-muted); <span style="font-size:var(--text-xs);color:var(--c-text-muted);
white-space:nowrap"> white-space:nowrap">
@ -3159,7 +3154,7 @@ window.Page_admin = (() => {
<button class="btn btn-sm adm-hilfe-del-btn" <button class="btn btn-sm adm-hilfe-del-btn"
style="padding:2px 8px;font-size:var(--text-xs); style="padding:2px 8px;font-size:var(--text-xs);
background:var(--c-danger-bg,#fee2e2);color:var(--c-danger,#991b1b)" background:var(--c-danger-bg,#fee2e2);color:var(--c-danger,#991b1b)"
data-id="${a.id}" data-frage="${_esc(a.frage)}"> data-id="${a.id}" data-frage="${UI.escape(a.frage)}">
${UI.icon('trash')} ${UI.icon('trash')}
</button> </button>
</div> </div>
@ -3177,7 +3172,7 @@ window.Page_admin = (() => {
border:1.5px solid var(--c-border);border-radius:var(--radius-md); border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)"> background:var(--c-surface);color:var(--c-text);font-size:var(--text-sm)">
${Object.entries(KAT_LABEL).map(([k,v]) => ${Object.entries(KAT_LABEL).map(([k,v]) =>
`<option value="${k}" ${k === a.kategorie ? 'selected' : ''}>${_esc(v)}</option>` `<option value="${k}" ${k === a.kategorie ? 'selected' : ''}>${UI.escape(v)}</option>`
).join('')} ).join('')}
</select> </select>
</div> </div>
@ -3185,7 +3180,7 @@ window.Page_admin = (() => {
<label style="font-size:var(--text-xs);font-weight:600;display:block; <label style="font-size:var(--text-xs);font-weight:600;display:block;
margin-bottom:4px;color:var(--c-text-secondary)">Frage</label> margin-bottom:4px;color:var(--c-text-secondary)">Frage</label>
<input type="text" class="adm-hilfe-edit-frage" <input type="text" class="adm-hilfe-edit-frage"
value="${_esc(a.frage)}" value="${UI.escape(a.frage)}"
style="width:100%;padding:var(--space-2) var(--space-3); style="width:100%;padding:var(--space-2) var(--space-3);
border:1.5px solid var(--c-border);border-radius:var(--radius-md); border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text); background:var(--c-surface);color:var(--c-text);
@ -3199,7 +3194,7 @@ window.Page_admin = (() => {
border:1.5px solid var(--c-border);border-radius:var(--radius-md); border:1.5px solid var(--c-border);border-radius:var(--radius-md);
background:var(--c-surface);color:var(--c-text); background:var(--c-surface);color:var(--c-text);
font-size:var(--text-sm);box-sizing:border-box; font-size:var(--text-sm);box-sizing:border-box;
resize:vertical;font-family:inherit">${_esc(a.antwort)}</textarea> resize:vertical;font-family:inherit">${UI.escape(a.antwort)}</textarea>
</div> </div>
<div style="display:flex;align-items:center;gap:var(--space-3)"> <div style="display:flex;align-items:center;gap:var(--space-3)">
<label style="font-size:var(--text-xs);font-weight:600; <label style="font-size:var(--text-xs);font-weight:600;
@ -3494,8 +3489,8 @@ window.Page_admin = (() => {
const topRows = d.top_referrers.map((r, i) => ` const topRows = d.top_referrers.map((r, i) => `
<tr> <tr>
<td style="padding:8px 10px;color:var(--c-text-muted);font-weight:600">${i + 1}</td> <td style="padding:8px 10px;color:var(--c-text-muted);font-weight:600">${i + 1}</td>
<td style="padding:8px 10px;font-weight:600">${_esc(r.name)}</td> <td style="padding:8px 10px;font-weight:600">${UI.escape(r.name)}</td>
<td style="padding:8px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(r.email)}</td> <td style="padding:8px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${UI.escape(r.email)}</td>
<td style="padding:8px 10px;text-align:right"> <td style="padding:8px 10px;text-align:right">
<span style="font-size:var(--text-lg);font-weight:800;color:var(--c-primary)">${r.invited_count}</span> <span style="font-size:var(--text-lg);font-weight:800;color:var(--c-primary)">${r.invited_count}</span>
</td> </td>
@ -3503,8 +3498,8 @@ window.Page_admin = (() => {
const recentRows = d.recent_invites.slice(0, 50).map(r => ` const recentRows = d.recent_invites.slice(0, 50).map(r => `
<tr> <tr>
<td style="padding:6px 10px;font-weight:500">${_esc(r.name)}</td> <td style="padding:6px 10px;font-weight:500">${UI.escape(r.name)}</td>
<td style="padding:6px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(r.referrer_name)}</td> <td style="padding:6px 10px;color:var(--c-text-secondary);font-size:var(--text-xs)">${UI.escape(r.referrer_name)}</td>
<td style="padding:6px 10px;color:var(--c-text-muted);font-size:var(--text-xs)">${(r.created_at || '').slice(0, 10)}</td> <td style="padding:6px 10px;color:var(--c-text-muted);font-size:var(--text-xs)">${(r.created_at || '').slice(0, 10)}</td>
</tr>`).join(''); </tr>`).join('');
@ -3575,8 +3570,8 @@ window.Page_admin = (() => {
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)"> <div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap"> <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap">
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(r.name)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(r.name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">${_esc(r.email)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">${UI.escape(r.email)}</div>
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
${tierBadge(r.tier)} ${tierBadge(r.tier)}
${r.discount_pct > 0 ? `<span style="display:inline-block;padding:1px 8px;border-radius:999px; ${r.discount_pct > 0 ? `<span style="display:inline-block;padding:1px 8px;border-radius:999px;
@ -3590,7 +3585,7 @@ window.Page_admin = (() => {
${r.message ? `<div style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-secondary); ${r.message ? `<div style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md); padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
background:var(--c-surface-raised,rgba(0,0,0,.04))"> background:var(--c-surface-raised,rgba(0,0,0,.04))">
${_esc(r.message)} ${UI.escape(r.message)}
</div>` : ''} </div>` : ''}
</div> </div>
</div> </div>
@ -3598,15 +3593,15 @@ window.Page_admin = (() => {
${r.existing_invoice_id ? ` ${r.existing_invoice_id ? `
<button class="btn adm-invoice-edit-btn" <button class="btn adm-invoice-edit-btn"
data-invoice-id="${r.existing_invoice_id}" data-invoice-id="${r.existing_invoice_id}"
title="Rechnung ${_esc(r.existing_invoice_number)} (${_esc(r.existing_invoice_status)}) bearbeiten" title="Rechnung ${UI.escape(r.existing_invoice_number)} (${UI.escape(r.existing_invoice_status)}) bearbeiten"
style="background:#eab308;color:#1a1a1a;border:none; style="background:#eab308;color:#1a1a1a;border:none;
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md); padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
cursor:pointer;font-size:var(--text-sm);font-weight:600"> cursor:pointer;font-size:var(--text-sm);font-weight:600">
${UI.icon('receipt')} Rechnung bearbeiten ${UI.icon('receipt')} Rechnung bearbeiten
</button>` : ` </button>` : `
<button class="btn adm-invoice-btn" <button class="btn adm-invoice-btn"
data-name="${_esc(r.name)}" data-email="${_esc(r.email)}" data-name="${UI.escape(r.name)}" data-email="${UI.escape(r.email)}"
data-tier="${r.tier}" data-address="${_esc(r.billing_address || '')}" data-tier="${r.tier}" data-address="${UI.escape(r.billing_address || '')}"
data-discount="${r.discount_pct || 0}" data-discount="${r.discount_pct || 0}"
data-discount-reason="${r.discount_reason || ''}" data-discount-reason="${r.discount_reason || ''}"
data-referral-count="${r.referral_count || 0}" data-referral-count="${r.referral_count || 0}"
@ -3615,7 +3610,7 @@ window.Page_admin = (() => {
cursor:pointer;font-size:var(--text-sm);font-weight:600"> cursor:pointer;font-size:var(--text-sm);font-weight:600">
${UI.icon('receipt')} Rechnung erstellen ${UI.icon('receipt')} Rechnung erstellen
</button>`} </button>`}
<button class="btn adm-fulfill-btn" data-id="${r.id}" data-name="${_esc(r.name)}" data-tier="${r.tier}" <button class="btn adm-fulfill-btn" data-id="${r.id}" data-name="${UI.escape(r.name)}" data-tier="${r.tier}"
style="background:#16a34a;color:#fff;border:none; style="background:#16a34a;color:#fff;border:none;
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md); padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
cursor:pointer;font-size:var(--text-sm);font-weight:600"> cursor:pointer;font-size:var(--text-sm);font-weight:600">
@ -3627,8 +3622,8 @@ window.Page_admin = (() => {
// Erledigte als kompakte Tabellenzeilen // Erledigte als kompakte Tabellenzeilen
const _doneRow = r => ` const _doneRow = r => `
<tr> <tr>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${_esc(r.name)}<br> <td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${UI.escape(r.name)}<br>
<span class="text-xs-muted">${_esc(r.email)}</span></td> <span class="text-xs-muted">${UI.escape(r.email)}</span></td>
<td style="padding:var(--space-2) var(--space-3)">${tierBadge(r.tier)}</td> <td style="padding:var(--space-2) var(--space-3)">${tierBadge(r.tier)}</td>
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-success)"> <td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-success)">
${r.fulfilled_at?.slice(0,10) || ''}</td> ${r.fulfilled_at?.slice(0,10) || ''}</td>
@ -3841,12 +3836,12 @@ window.Page_admin = (() => {
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-edit" data-id="${inv.id}" title="Bearbeiten"> actions.push(`<button class="btn btn-sm btn-ghost adm-inv-edit" data-id="${inv.id}" title="Bearbeiten">
${UI.icon('pencil')} Bearbeiten ${UI.icon('pencil')} Bearbeiten
</button>`); </button>`);
actions.push(`<button class="btn btn-sm btn-primary adm-inv-send" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}" title="Senden"> actions.push(`<button class="btn btn-sm btn-primary adm-inv-send" data-id="${inv.id}" data-num="${UI.escape(inv.invoice_number)}" title="Senden">
${UI.icon('paper-plane-tilt')} Senden ${UI.icon('paper-plane-tilt')} Senden
</button>`); </button>`);
} }
if (inv.status === 'sent') { if (inv.status === 'sent') {
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-send" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}" title="Erneut senden" actions.push(`<button class="btn btn-sm btn-ghost adm-inv-send" data-id="${inv.id}" data-num="${UI.escape(inv.invoice_number)}" title="Erneut senden"
class="text-muted"> class="text-muted">
${UI.icon('paper-plane-tilt')} Erneut senden ${UI.icon('paper-plane-tilt')} Erneut senden
</button>`); </button>`);
@ -3855,7 +3850,7 @@ window.Page_admin = (() => {
actions.push(`<button class="btn btn-sm btn-secondary adm-inv-pay" data-id="${inv.id}" data-amount="${inv.amount_gross}" title="Als bezahlt markieren"> actions.push(`<button class="btn btn-sm btn-secondary adm-inv-pay" data-id="${inv.id}" data-amount="${inv.amount_gross}" title="Als bezahlt markieren">
${UI.icon('check-circle')} Bezahlt ${UI.icon('check-circle')} Bezahlt
</button>`); </button>`);
actions.push(`<button class="btn btn-sm btn-ghost adm-inv-cancel" data-id="${inv.id}" data-num="${_esc(inv.invoice_number)}" actions.push(`<button class="btn btn-sm btn-ghost adm-inv-cancel" data-id="${inv.id}" data-num="${UI.escape(inv.invoice_number)}"
class="text-danger" title="Stornieren"> class="text-danger" title="Stornieren">
${UI.icon('x-circle')} Storno ${UI.icon('x-circle')} Storno
</button>`); </button>`);
@ -3869,11 +3864,11 @@ window.Page_admin = (() => {
return ` return `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}"> <tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td class="adm-td" style="font-weight:600;font-family:monospace;font-size:var(--text-xs)"> <td class="adm-td" style="font-weight:600;font-family:monospace;font-size:var(--text-xs)">
${_esc(inv.invoice_number)} ${UI.escape(inv.invoice_number)}
</td> </td>
<td class="adm-td"> <td class="adm-td">
<div style="font-weight:500">${_esc(inv.recipient_name)}</div> <div style="font-weight:500">${UI.escape(inv.recipient_name)}</div>
<div class="text-xs-muted">${_esc(inv.recipient_email || '')}</div> <div class="text-xs-muted">${UI.escape(inv.recipient_email || '')}</div>
</td> </td>
<td class="adm-td" style="text-align:right;font-weight:700;white-space:nowrap"> <td class="adm-td" style="text-align:right;font-weight:700;white-space:nowrap">
${_fmtEur(inv.amount_gross)} ${_fmtEur(inv.amount_gross)}
@ -4007,12 +4002,12 @@ window.Page_admin = (() => {
<div> <div>
<label class="form-label text-xs">Empfänger Name *</label> <label class="form-label text-xs">Empfänger Name *</label>
<input class="form-control" name="recipient_name" type="text" required <input class="form-control" name="recipient_name" type="text" required
placeholder="Max Muster" value="${_esc(p.recipient_name || '')}"> placeholder="Max Muster" value="${UI.escape(p.recipient_name || '')}">
</div> </div>
<div> <div>
<label class="form-label text-xs">E-Mail</label> <label class="form-label text-xs">E-Mail</label>
<input class="form-control" name="recipient_email" type="email" <input class="form-control" name="recipient_email" type="email"
placeholder="max@example.com" value="${_esc(p.recipient_email || '')}"> placeholder="max@example.com" value="${UI.escape(p.recipient_email || '')}">
</div> </div>
</div> </div>
@ -4024,14 +4019,14 @@ window.Page_admin = (() => {
</label> </label>
<textarea class="form-control" name="recipient_address" rows="2" <textarea class="form-control" name="recipient_address" rows="2"
placeholder="Musterstr. 1&#10;12345 Berlin" placeholder="Musterstr. 1&#10;12345 Berlin"
style="resize:vertical;font-family:inherit">${_esc(p.recipient_address || '')}</textarea> style="resize:vertical;font-family:inherit">${UI.escape(p.recipient_address || '')}</textarea>
</div> </div>
<div> <div>
<label class="form-label text-xs">Leistungszeitraum <span class="text-muted">(optional)</span></label> <label class="form-label text-xs">Leistungszeitraum <span class="text-muted">(optional)</span></label>
<input class="form-control" name="service_period" type="text" <input class="form-control" name="service_period" type="text"
placeholder="z.B. 15.05.2026 oder einmalige Leistung" placeholder="z.B. 15.05.2026 oder einmalige Leistung"
value="${_esc(p.service_period || '')}"> value="${UI.escape(p.service_period || '')}">
</div> </div>
<!-- Positionen --> <!-- Positionen -->
@ -4066,7 +4061,7 @@ window.Page_admin = (() => {
<label class="form-label text-xs">Notizen <span class="text-muted">(optional)</span></label> <label class="form-label text-xs">Notizen <span class="text-muted">(optional)</span></label>
<textarea class="form-control" name="notes" rows="2" <textarea class="form-control" name="notes" rows="2"
style="resize:vertical;font-family:inherit" style="resize:vertical;font-family:inherit"
placeholder="Interne Notiz / Zahlungshinweis">${_esc(p.notes || (!isEdit && !p.recipient_name ? 'Zahlbar innerhalb von 14 Tagen ab Rechnungsdatum.' : ''))}</textarea> placeholder="Interne Notiz / Zahlungshinweis">${UI.escape(p.notes || (!isEdit && !p.recipient_name ? 'Zahlbar innerhalb von 14 Tagen ab Rechnungsdatum.' : ''))}</textarea>
</div> </div>
</form> </form>
@ -4120,7 +4115,7 @@ window.Page_admin = (() => {
itemEl.style.cssText = 'display:grid;grid-template-columns:1fr 60px 100px auto;gap:var(--space-2);align-items:center'; itemEl.style.cssText = 'display:grid;grid-template-columns:1fr 60px 100px auto;gap:var(--space-2);align-items:center';
itemEl.innerHTML = ` itemEl.innerHTML = `
<input class="form-control inv-item-desc" type="text" placeholder="Beschreibung *" <input class="form-control inv-item-desc" type="text" placeholder="Beschreibung *"
value="${_esc(desc)}" class="text-sm"> value="${UI.escape(desc)}" class="text-sm">
<input class="form-control inv-item-qty" type="number" min="1" value="${qty}" <input class="form-control inv-item-qty" type="number" min="1" value="${qty}"
style="font-size:var(--text-sm);text-align:right" title="Menge"> style="font-size:var(--text-sm);text-align:right" title="Menge">
<input class="form-control inv-item-price" type="number" min="0" step="0.01" value="${price.toFixed(2)}" <input class="form-control inv-item-price" type="number" min="0" step="0.01" value="${price.toFixed(2)}"
@ -4303,7 +4298,7 @@ window.Page_admin = (() => {
body: ` body: `
<form id="${id}" class="flex-col-gap-3"> <form id="${id}" class="flex-col-gap-3">
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0"> <p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0">
Rechnung <strong>${_esc(invoiceNum)}</strong> stornieren. Rechnung <strong>${UI.escape(invoiceNum)}</strong> stornieren.
</p> </p>
<div> <div>
<label class="form-label text-xs">Stornierungsgrund *</label> <label class="form-label text-xs">Stornierungsgrund *</label>
@ -4360,7 +4355,7 @@ window.Page_admin = (() => {
const itemsHtml = (inv.items || []).map(item => ` const itemsHtml = (inv.items || []).map(item => `
<tr> <tr>
<td style="padding:6px 8px">${_esc(item.description)}</td> <td style="padding:6px 8px">${UI.escape(item.description)}</td>
<td style="padding:6px 8px;text-align:right">${item.quantity}</td> <td style="padding:6px 8px;text-align:right">${item.quantity}</td>
<td style="padding:6px 8px;text-align:right">${_fmtEur(item.unit_price)}</td> <td style="padding:6px 8px;text-align:right">${_fmtEur(item.unit_price)}</td>
<td style="padding:6px 8px;text-align:right;font-weight:600">${_fmtEur(item.total)}</td> <td style="padding:6px 8px;text-align:right;font-weight:600">${_fmtEur(item.total)}</td>
@ -4368,15 +4363,15 @@ window.Page_admin = (() => {
`).join(''); `).join('');
UI.modal.open({ UI.modal.open({
title: `${UI.icon('receipt')} ${_esc(inv.invoice_number)}`, title: `${UI.icon('receipt')} ${UI.escape(inv.invoice_number)}`,
body: ` body: `
<div style="display:flex;flex-direction:column;gap:var(--space-3);font-size:var(--text-sm)"> <div style="display:flex;flex-direction:column;gap:var(--space-3);font-size:var(--text-sm)">
<div class="grid-2"> <div class="grid-2">
<div> <div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Empfänger</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Empfänger</div>
<div style="font-weight:600">${_esc(inv.recipient_name)}</div> <div style="font-weight:600">${UI.escape(inv.recipient_name)}</div>
${inv.recipient_email ? `<div style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(inv.recipient_email)}</div>` : ''} ${inv.recipient_email ? `<div style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(inv.recipient_email)}</div>` : ''}
${inv.recipient_address ? `<div style="color:var(--c-text-secondary);font-size:var(--text-xs);white-space:pre-line;margin-top:2px">${_esc(inv.recipient_address)}</div>` : ''} ${inv.recipient_address ? `<div style="color:var(--c-text-secondary);font-size:var(--text-xs);white-space:pre-line;margin-top:2px">${UI.escape(inv.recipient_address)}</div>` : ''}
</div> </div>
<div> <div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Status</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Status</div>
@ -4392,7 +4387,7 @@ window.Page_admin = (() => {
${inv.service_period ? ` ${inv.service_period ? `
<div> <div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Leistungszeitraum</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Leistungszeitraum</div>
<div>${_esc(inv.service_period)}</div> <div>${UI.escape(inv.service_period)}</div>
</div>` : ''} </div>` : ''}
<!-- Positionen --> <!-- Positionen -->
@ -4421,7 +4416,7 @@ window.Page_admin = (() => {
<div> <div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Notizen</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Notizen</div>
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3); <div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
font-size:var(--text-xs);white-space:pre-wrap">${_esc(inv.notes)}</div> font-size:var(--text-xs);white-space:pre-wrap">${UI.escape(inv.notes)}</div>
</div>` : ''} </div>` : ''}
</div> </div>
`, `,
@ -4451,7 +4446,7 @@ window.Page_admin = (() => {
const monthRows = (cf.monthly || []).map((m, i) => ` const monthRows = (cf.monthly || []).map((m, i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}"> <tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td class="adm-td">${_esc(m.month)}</td> <td class="adm-td">${UI.escape(m.month)}</td>
<td class="adm-td text-right">${m.count}</td> <td class="adm-td text-right">${m.count}</td>
<td class="adm-td" style="text-align:right;font-weight:600">${_fmtEur(m.revenue)}</td> <td class="adm-td" style="text-align:right;font-weight:600">${_fmtEur(m.revenue)}</td>
</tr>`).join(''); </tr>`).join('');
@ -4593,8 +4588,8 @@ window.Page_admin = (() => {
? ` <span class="text-xs-muted">(RG: ${_fmtE(inv.amount_gross)})</span>` : ''; ? ` <span class="text-xs-muted">(RG: ${_fmtE(inv.amount_gross)})</span>` : '';
return ` return `
<tr style="${i%2===1?'background:var(--c-surface-2)':''}"> <tr style="${i%2===1?'background:var(--c-surface-2)':''}">
<td class="adm-td" style="font-family:monospace;font-size:var(--text-xs);${isStorno?'color:var(--c-danger)':''}">${_esc(inv.invoice_number)}</td> <td class="adm-td" style="font-family:monospace;font-size:var(--text-xs);${isStorno?'color:var(--c-danger)':''}">${UI.escape(inv.invoice_number)}</td>
<td class="adm-td">${_esc(inv.recipient_name)}</td> <td class="adm-td">${UI.escape(inv.recipient_name)}</td>
<td class="adm-td" style="text-align:right;font-weight:600;${amtColor}">${_fmtE(effectiveAmt)}${amtNote}</td> <td class="adm-td" style="text-align:right;font-weight:600;${amtColor}">${_fmtE(effectiveAmt)}${amtNote}</td>
<td class="adm-td" style="${isStorno?'color:var(--c-danger)':''}">${sL[inv.status]||inv.status}</td> <td class="adm-td" style="${isStorno?'color:var(--c-danger)':''}">${sL[inv.status]||inv.status}</td>
<td class="adm-td text-xs-muted">${_fmtD(inv.created_at)}</td> <td class="adm-td text-xs-muted">${_fmtD(inv.created_at)}</td>
@ -4602,7 +4597,7 @@ window.Page_admin = (() => {
}).join(''); }).join('');
resultEl.innerHTML = ` resultEl.innerHTML = `
<div style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-secondary);margin-bottom:var(--space-2)"> <div style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-secondary);margin-bottom:var(--space-2)">
${_esc(data.period || `Q${q} ${year}`)} ${data.count} Buchung(en) · Summe: ${_fmtE(data.total_gross)} ${UI.escape(data.period || `Q${q} ${year}`)} ${data.count} Buchung(en) · Summe: ${_fmtE(data.total_gross)}
</div> </div>
<div class="adm-table-scroll"> <div class="adm-table-scroll">
<table class="adm-table"> <table class="adm-table">
@ -4622,7 +4617,7 @@ window.Page_admin = (() => {
</table> </table>
</div>`; </div>`;
} catch (e) { } catch (e) {
resultEl.innerHTML = `<div style="color:var(--c-danger);font-size:var(--text-xs)">Fehler: ${_esc(e.message)}</div>`; resultEl.innerHTML = `<div style="color:var(--c-danger);font-size:var(--text-xs)">Fehler: ${UI.escape(e.message)}</div>`;
} }
}); });
} }

View file

@ -56,7 +56,7 @@ window.Page_adoption = (() => {
<input id="adp-rasse" class="form-control" type="text" <input id="adp-rasse" class="form-control" type="text"
placeholder="Rasse filtern…" placeholder="Rasse filtern…"
style="flex:1;min-width:120px;max-width:220px" style="flex:1;min-width:120px;max-width:220px"
value="${_esc(_rasseFilter)}"> value="${UI.escape(_rasseFilter)}">
<button class="btn btn-secondary" id="adp-btn-locate" <button class="btn btn-secondary" id="adp-btn-locate"
style="white-space:nowrap"> style="white-space:nowrap">
${UI.icon('map-pin')} Mein Standort ${UI.icon('map-pin')} Mein Standort
@ -306,7 +306,7 @@ window.Page_adoption = (() => {
if (!animals.length) { if (!animals.length) {
content.innerHTML = ` content.innerHTML = `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)"> <p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
${_rasseFilter ? `Keine Hunde gefunden für "<strong>${_esc(_rasseFilter)}</strong>"` : `Keine Hunde im Umkreis von ${_radius} km gefunden.`} ${_rasseFilter ? `Keine Hunde gefunden für "<strong>${UI.escape(_rasseFilter)}</strong>"` : `Keine Hunde im Umkreis von ${_radius} km gefunden.`}
</p> </p>
<div style="display:flex;flex-direction:column;gap:var(--space-3);max-width:380px"> <div style="display:flex;flex-direction:column;gap:var(--space-3);max-width:380px">
<a href="https://www.tierheimhelden.de/hunde/liste" <a href="https://www.tierheimhelden.de/hunde/liste"
@ -355,7 +355,7 @@ window.Page_adoption = (() => {
function _animalCard(a) { function _animalCard(a) {
const foto = a.foto_url const foto = a.foto_url
? `<img src="${_esc(a.foto_url)}" alt="${_esc(a.name)}" ? `<img src="${UI.escape(a.foto_url)}" alt="${UI.escape(a.name)}"
style="width:100%;height:100%;object-fit:cover" style="width:100%;height:100%;object-fit:cover"
onerror="this.parentElement.innerHTML='<div style=&quot;display:flex;align-items:center;justify-content:center;height:100%;font-size:2rem&quot;>🐶</div>'">` onerror="this.parentElement.innerHTML='<div style=&quot;display:flex;align-items:center;justify-content:center;height:100%;font-size:2rem&quot;>🐶</div>'">`
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem">🐶</div>'; : '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem">🐶</div>';
@ -366,7 +366,7 @@ window.Page_adoption = (() => {
const tierheim = a.tierheim || ''; const tierheim = a.tierheim || '';
return ` return `
<div data-adp-url="${_esc(a.adoptions_url)}" <div data-adp-url="${UI.escape(a.adoptions_url)}"
style="border-radius:var(--radius-md);overflow:hidden; style="border-radius:var(--radius-md);overflow:hidden;
background:var(--c-surface-2);cursor:pointer; background:var(--c-surface-2);cursor:pointer;
box-shadow:0 1px 4px rgba(0,0,0,0.08); box-shadow:0 1px 4px rgba(0,0,0,0.08);
@ -379,16 +379,16 @@ window.Page_adoption = (() => {
<div style="padding:var(--space-2) var(--space-2) var(--space-3)"> <div style="padding:var(--space-2) var(--space-2) var(--space-3)">
<div style="font-weight:600;font-size:var(--text-sm); <div style="font-weight:600;font-size:var(--text-sm);
margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(a.name)} ${UI.escape(a.name)}
</div> </div>
${rasseTxt ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary); ${rasseTxt ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(rasseTxt)} ${UI.escape(rasseTxt)}
</div>` : ''} </div>` : ''}
<div style="display:flex;gap:var(--space-1);flex-wrap:wrap;margin-top:var(--space-1)"> <div style="display:flex;gap:var(--space-1);flex-wrap:wrap;margin-top:var(--space-1)">
${alterTxt ? `<span style="font-size:10px;background:var(--c-surface-3); ${alterTxt ? `<span style="font-size:10px;background:var(--c-surface-3);
border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)"> border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)">
${_esc(alterTxt)} ${UI.escape(alterTxt)}
</span>` : ''} </span>` : ''}
${a.geschlecht ? `<span style="font-size:10px;background:var(--c-surface-3); ${a.geschlecht ? `<span style="font-size:10px;background:var(--c-surface-3);
border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)"> border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)">
@ -396,12 +396,12 @@ window.Page_adoption = (() => {
</span>` : ''} </span>` : ''}
${distTxt ? `<span style="font-size:10px;background:var(--c-primary-light,#ede9fe); ${distTxt ? `<span style="font-size:10px;background:var(--c-primary-light,#ede9fe);
border-radius:999px;padding:1px 6px;color:var(--c-primary)"> border-radius:999px;padding:1px 6px;color:var(--c-primary)">
${_esc(distTxt)} ${UI.escape(distTxt)}
</span>` : ''} </span>` : ''}
</div> </div>
${tierheim ? `<div style="font-size:10px;color:var(--c-text-muted);margin-top:var(--space-1); ${tierheim ? `<div style="font-size:10px;color:var(--c-text-muted);margin-top:var(--space-1);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${_esc(tierheim)}"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${UI.escape(tierheim)}">
${UI.icon('house-line')} ${_esc(tierheim)} ${UI.icon('house-line')} ${UI.escape(tierheim)}
</div>` : ''} </div>` : ''}
</div> </div>
</div> </div>
@ -459,7 +459,7 @@ window.Page_adoption = (() => {
function _shelterRow(s) { function _shelterRow(s) {
return ` return `
<a href="${_esc(s.url)}" target="_blank" rel="noopener noreferrer" <a href="${UI.escape(s.url)}" target="_blank" rel="noopener noreferrer"
style="display:flex;align-items:center;gap:var(--space-3); style="display:flex;align-items:center;gap:var(--space-3);
padding:var(--space-3);border-radius:var(--radius-md); padding:var(--space-3);border-radius:var(--radius-md);
background:var(--c-surface-2);text-decoration:none;color:inherit; background:var(--c-surface-2);text-decoration:none;color:inherit;
@ -476,10 +476,10 @@ window.Page_adoption = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm); <div style="font-weight:600;font-size:var(--text-sm);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(s.name)} ${UI.escape(s.name)}
</div> </div>
<div class="text-xs-secondary"> <div class="text-xs-secondary">
${_esc(s.plz)} ${_esc(s.stadt)} ${UI.escape(s.plz)} ${UI.escape(s.stadt)}
</div> </div>
</div> </div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex-shrink:0"> <div style="display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex-shrink:0">
@ -610,7 +610,7 @@ window.Page_adoption = (() => {
function _communityCard(l) { function _communityCard(l) {
const foto = l.foto_url const foto = l.foto_url
? `<img src="${_esc(l.foto_url)}" alt="${_esc(l.name)}" ? `<img src="${UI.escape(l.foto_url)}" alt="${UI.escape(l.name)}"
style="width:100%;height:100%;object-fit:cover" style="width:100%;height:100%;object-fit:cover"
onerror="this.parentElement.innerHTML='<div style=&quot;display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem&quot;>🐾</div>'">` onerror="this.parentElement.innerHTML='<div style=&quot;display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem&quot;>🐾</div>'">`
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem">🐾</div>'; : '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem">🐾</div>';
@ -635,11 +635,11 @@ window.Page_adoption = (() => {
const interestBtn = l.user_interested const interestBtn = l.user_interested
? `<button class="btn btn-secondary btn-sm" style="width:100%;font-size:var(--text-xs)" ? `<button class="btn btn-secondary btn-sm" style="width:100%;font-size:var(--text-xs)"
data-adp-interest="${_esc(l.id)}" data-adp-interested="true"> data-adp-interest="${UI.escape(l.id)}" data-adp-interested="true">
Bereits gemeldet Bereits gemeldet
</button>` </button>`
: `<button class="btn btn-primary btn-sm" style="width:100%;font-size:var(--text-xs)" : `<button class="btn btn-primary btn-sm" style="width:100%;font-size:var(--text-xs)"
data-adp-interest="${_esc(l.id)}" data-adp-interested="false" data-adp-interest="${UI.escape(l.id)}" data-adp-interested="false"
${!isActive ? 'disabled' : ''}> ${!isActive ? 'disabled' : ''}>
Interesse bekunden Interesse bekunden
</button>`; </button>`;
@ -657,7 +657,7 @@ window.Page_adoption = (() => {
display:flex;align-items:center;justify-content:center"> display:flex;align-items:center;justify-content:center">
<span style="color:#fff;font-weight:700;font-size:var(--text-sm); <span style="color:#fff;font-weight:700;font-size:var(--text-sm);
background:rgba(0,0,0,0.6);padding:4px 12px;border-radius:999px"> background:rgba(0,0,0,0.6);padding:4px 12px;border-radius:999px">
${_esc(statusLabel)} ${UI.escape(statusLabel)}
</span> </span>
</div> </div>
` : ''} ` : ''}
@ -666,17 +666,17 @@ window.Page_adoption = (() => {
<div style="padding:var(--space-2) var(--space-2) var(--space-3);flex:1;display:flex;flex-direction:column;gap:var(--space-1)"> <div style="padding:var(--space-2) var(--space-2) var(--space-3);flex:1;display:flex;flex-direction:column;gap:var(--space-1)">
<div style="font-weight:600;font-size:var(--text-sm); <div style="font-weight:600;font-size:var(--text-sm);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(l.name)} ${UI.escape(l.name)}
</div> </div>
${l.rasse ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary); ${l.rasse ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(l.rasse)} ${UI.escape(l.rasse)}
</div>` : ''} </div>` : ''}
<!-- Badges --> <!-- Badges -->
<div style="display:flex;gap:4px;flex-wrap:wrap"> <div style="display:flex;gap:4px;flex-wrap:wrap">
${alterLabel ? `<span style="font-size:10px;background:var(--c-surface-3); ${alterLabel ? `<span style="font-size:10px;background:var(--c-surface-3);
border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)"> border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)">
${_esc(alterLabel)} ${UI.escape(alterLabel)}
</span>` : ''} </span>` : ''}
${genderIcon ? `<span style="font-size:10px;background:var(--c-surface-3); ${genderIcon ? `<span style="font-size:10px;background:var(--c-surface-3);
border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)"> border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)">
@ -684,14 +684,14 @@ window.Page_adoption = (() => {
</span>` : ''} </span>` : ''}
${distTxt ? `<span style="font-size:10px;background:var(--c-primary-light,#ede9fe); ${distTxt ? `<span style="font-size:10px;background:var(--c-primary-light,#ede9fe);
border-radius:999px;padding:1px 6px;color:var(--c-primary)"> border-radius:999px;padding:1px 6px;color:var(--c-primary)">
${_esc(distTxt)} ${UI.escape(distTxt)}
</span>` : ''} </span>` : ''}
</div> </div>
${ort ? `<div style="font-size:10px;color:var(--c-text-muted)">${_esc(ort)}</div>` : ''} ${ort ? `<div style="font-size:10px;color:var(--c-text-muted)">${UI.escape(ort)}</div>` : ''}
${l.beschreibung ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary); ${l.beschreibung ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
overflow:hidden;display:-webkit-box; overflow:hidden;display:-webkit-box;
-webkit-line-clamp:2;-webkit-box-orient:vertical"> -webkit-line-clamp:2;-webkit-box-orient:vertical">
${_esc(l.beschreibung)} ${UI.escape(l.beschreibung)}
</div>` : ''} </div>` : ''}
${l.interesse_count ? `<div style="font-size:10px;color:var(--c-text-muted)"> ${l.interesse_count ? `<div style="font-size:10px;color:var(--c-text-muted)">
${l.interesse_count} Interessent${l.interesse_count !== 1 ? 'en' : ''} ${l.interesse_count} Interessent${l.interesse_count !== 1 ? 'en' : ''}
@ -717,20 +717,20 @@ window.Page_adoption = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm); <div style="font-weight:600;font-size:var(--text-sm);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(l.name)} ${UI.escape(l.name)}
</div> </div>
<div class="text-xs-secondary"> <div class="text-xs-secondary">
${l.interesse_count || 0} Interessent${(l.interesse_count || 0) !== 1 ? 'en' : ''} ${l.interesse_count || 0} Interessent${(l.interesse_count || 0) !== 1 ? 'en' : ''}
</div> </div>
</div> </div>
<select class="form-control" style="width:auto;font-size:var(--text-xs)" <select class="form-control" style="width:auto;font-size:var(--text-xs)"
data-adp-status-change="${_esc(l.id)}"> data-adp-status-change="${UI.escape(l.id)}">
${statusOptions.map(o => ` ${statusOptions.map(o => `
<option value="${o.value}" ${l.status === o.value ? 'selected' : ''}>${o.label}</option> <option value="${o.value}" ${l.status === o.value ? 'selected' : ''}>${o.label}</option>
`).join('')} `).join('')}
</select> </select>
<button class="btn btn-danger btn-sm" style="font-size:var(--text-xs);white-space:nowrap" <button class="btn btn-danger btn-sm" style="font-size:var(--text-xs);white-space:nowrap"
data-adp-delete="${_esc(l.id)}"> data-adp-delete="${UI.escape(l.id)}">
${UI.icon('trash')} Löschen ${UI.icon('trash')} Löschen
</button> </button>
</div> </div>
@ -849,7 +849,7 @@ window.Page_adoption = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">PLZ</label> <label class="form-label">PLZ</label>
<input class="form-control" name="plz" inputmode="numeric" maxlength="5" <input class="form-control" name="plz" inputmode="numeric" maxlength="5"
placeholder="z.B. 80331" value="${_esc(_lat ? '' : '')}"> placeholder="z.B. 80331" value="${UI.escape(_lat ? '' : '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Ort</label> <label class="form-label">Ort</label>
@ -941,15 +941,6 @@ window.Page_adoption = (() => {
return 'Senior'; return 'Senior';
} }
function _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// PUBLIC API // PUBLIC API
// ---------------------------------------------------------- // ----------------------------------------------------------

View file

@ -46,7 +46,7 @@ window.Page_breeder_editor = (() => {
background:var(--c-surface-2);overflow:hidden;flex-shrink:0; background:var(--c-surface-2);overflow:hidden;flex-shrink:0;
display:flex;align-items:center;justify-content:center"> display:flex;align-items:center;justify-content:center">
${p.logo_url ${p.logo_url
? `<img src="${_esc(p.logo_url)}" style="width:100%;height:100%;object-fit:cover">` ? `<img src="${UI.escape(p.logo_url)}" style="width:100%;height:100%;object-fit:cover">`
: `<svg class="ph-icon" style="width:32px;height:32px;opacity:.3"><use href="/icons/phosphor.svg#image"></use></svg>`} : `<svg class="ph-icon" style="width:32px;height:32px;opacity:.3"><use href="/icons/phosphor.svg#image"></use></svg>`}
</div> </div>
<div> <div>
@ -70,45 +70,45 @@ window.Page_breeder_editor = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Zwingername *</label> <label class="form-label">Zwingername *</label>
<input class="form-control" name="zwingername" type="text" required <input class="form-control" name="zwingername" type="text" required
value="${_esc(p.zwingername || '')}"> value="${UI.escape(p.zwingername || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Rasse(n)</label> <label class="form-label">Rasse(n)</label>
<input class="form-control" name="rasse_text" type="text" <input class="form-control" name="rasse_text" type="text"
value="${_esc(p.rasse_text || '')}"> value="${UI.escape(p.rasse_text || '')}">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Slogan <span style="font-weight:400;color:var(--c-text-muted)">(max. 80 Zeichen)</span></label> <label class="form-label">Slogan <span style="font-weight:400;color:var(--c-text-muted)">(max. 80 Zeichen)</span></label>
<input class="form-control" name="tagline" type="text" maxlength="80" <input class="form-control" name="tagline" type="text" maxlength="80"
placeholder="z. B. Liebevolle Aufzucht seit 2010 · VDH-anerkannt" placeholder="z. B. Liebevolle Aufzucht seit 2010 · VDH-anerkannt"
value="${_esc(p.tagline || '')}"> value="${UI.escape(p.tagline || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Über uns / Zwingerbeschreibung</label> <label class="form-label">Über uns / Zwingerbeschreibung</label>
<textarea class="form-control" name="beschreibung" rows="4" maxlength="800" <textarea class="form-control" name="beschreibung" rows="4" maxlength="800"
placeholder="Wer seid ihr, was ist euch bei der Zucht wichtig?">${_esc(p.beschreibung || '')}</textarea> placeholder="Wer seid ihr, was ist euch bei der Zucht wichtig?">${UI.escape(p.beschreibung || '')}</textarea>
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">Stadt</label> <label class="form-label">Stadt</label>
<input class="form-control" name="stadt" type="text" value="${_esc(p.stadt || '')}"> <input class="form-control" name="stadt" type="text" value="${UI.escape(p.stadt || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Verein</label> <label class="form-label">Verein</label>
<input class="form-control" name="verein" type="text" value="${_esc(p.verein || '')}"> <input class="form-control" name="verein" type="text" value="${UI.escape(p.verein || '')}">
</div> </div>
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">Website</label> <label class="form-label">Website</label>
<input class="form-control" name="website" type="url" <input class="form-control" name="website" type="url"
placeholder="https://" value="${_esc(p.website || '')}"> placeholder="https://" value="${UI.escape(p.website || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Instagram</label> <label class="form-label">Instagram</label>
<input class="form-control" name="instagram" type="text" <input class="form-control" name="instagram" type="text"
placeholder="@zwingername" value="${_esc(p.instagram || '')}"> placeholder="@zwingername" value="${UI.escape(p.instagram || '')}">
</div> </div>
</div> </div>
<button type="submit" class="btn btn-secondary btn-sm" style="align-self:flex-start"> <button type="submit" class="btn btn-secondary btn-sm" style="align-self:flex-start">
@ -161,10 +161,10 @@ window.Page_breeder_editor = (() => {
return ` return `
<div style="position:relative;aspect-ratio:1;border-radius:var(--radius-md);overflow:hidden;background:var(--c-surface-2)"> <div style="position:relative;aspect-ratio:1;border-radius:var(--radius-md);overflow:hidden;background:var(--c-surface-2)">
${isVid ${isVid
? `<video src="${_esc(ph.url)}" style="width:100%;height:100%;object-fit:cover" muted playsinline loop ? `<video src="${UI.escape(ph.url)}" style="width:100%;height:100%;object-fit:cover" muted playsinline loop
onmouseenter="this.play()" onmouseleave="this.pause()"></video> onmouseenter="this.play()" onmouseleave="this.pause()"></video>
<div style="position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.55);border-radius:4px;padding:1px 5px;font-size:10px;color:#fff"> Video</div>` <div style="position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.55);border-radius:4px;padding:1px 5px;font-size:10px;color:#fff"> Video</div>`
: `<img src="${_esc(ph.thumbnail_url || ph.url)}" style="width:100%;height:100%;object-fit:cover">`} : `<img src="${UI.escape(ph.thumbnail_url || ph.url)}" style="width:100%;height:100%;object-fit:cover">`}
${ph.is_primary ? `<div style="position:absolute;top:4px;left:4px;background:rgba(196,132,58,.9);border-radius:3px;padding:1px 5px;font-size:9px;color:#fff;font-weight:700">LOGO</div>` : ''} ${ph.is_primary ? `<div style="position:absolute;top:4px;left:4px;background:rgba(196,132,58,.9);border-radius:3px;padding:1px 5px;font-size:9px;color:#fff;font-weight:700">LOGO</div>` : ''}
<button class="be-photo-del" data-id="${ph.id}" <button class="be-photo-del" data-id="${ph.id}"
style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,.6); style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,.6);
@ -190,14 +190,14 @@ window.Page_breeder_editor = (() => {
<div style="border:1px solid var(--c-border);border-radius:var(--radius-md);padding:var(--space-3)"> <div style="border:1px solid var(--c-border);border-radius:var(--radius-md);padding:var(--space-3)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-2)"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-2)">
<div> <div>
<div style="font-weight:700;font-size:var(--text-sm)">${_esc(label)}</div> <div style="font-weight:700;font-size:var(--text-sm)">${UI.escape(label)}</div>
<div class="text-xs-muted">${info}</div> <div class="text-xs-muted">${info}</div>
</div> </div>
<label class="btn btn-secondary btn-sm" style="cursor:pointer"> <label class="btn btn-secondary btn-sm" style="cursor:pointer">
<svg class="ph-icon" style="width:14px;height:14px"><use href="/icons/phosphor.svg#upload-simple"></use></svg> <svg class="ph-icon" style="width:14px;height:14px"><use href="/icons/phosphor.svg#upload-simple"></use></svg>
Upload Upload
<input type="file" class="be-litter-input" data-litter-id="${l.id}" <input type="file" class="be-litter-input" data-litter-id="${l.id}"
data-label="${_esc(label)}" accept="image/*,video/*" class="hidden"> data-label="${UI.escape(label)}" accept="image/*,video/*" class="hidden">
</label> </label>
</div> </div>
</div>`; </div>`;
@ -312,10 +312,6 @@ window.Page_breeder_editor = (() => {
}); });
} }
function _esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
return { init, refresh, onDogChange }; return { init, refresh, onDogChange };

View file

@ -7,8 +7,6 @@ window.Page_breeder = (() => {
let _container = null; let _container = null;
let _appState = null; let _appState = null;
const _esc = s => UI.esc ? UI.esc(s) : String(s ?? '').replace(/[&<>"']/g,
c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// ---------------------------------------------------------- // ----------------------------------------------------------
// INIT // INIT
@ -51,7 +49,7 @@ window.Page_breeder = (() => {
} catch (e) { } catch (e) {
document.getElementById('breeder-profile-body').innerHTML = document.getElementById('breeder-profile-body').innerHTML =
`<div style="padding:var(--space-8);text-align:center;color:var(--c-text-secondary)"> `<div style="padding:var(--space-8);text-align:center;color:var(--c-text-secondary)">
${UI.icon('magnifying-glass')} ${_esc(e.message || 'Züchter nicht gefunden.')} ${UI.icon('magnifying-glass')} ${UI.escape(e.message || 'Züchter nicht gefunden.')}
</div>`; </div>`;
} }
} }
@ -80,17 +78,17 @@ window.Page_breeder = (() => {
${UI.icon('seal-check')} Verifizierter Züchter ${UI.icon('seal-check')} Verifizierter Züchter
</p> </p>
<h1 style="margin:0 0 var(--space-2);font-size:clamp(1.3rem,4vw,1.9rem);font-weight:800;line-height:1.2;word-break:break-word"> <h1 style="margin:0 0 var(--space-2);font-size:clamp(1.3rem,4vw,1.9rem);font-weight:800;line-height:1.2;word-break:break-word">
${_esc(p.zwingername)} ${UI.escape(p.zwingername)}
</h1> </h1>
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center"> <div style="display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center">
${p.rasse_text ? `<span style="background:rgba(255,255,255,.2);border-radius:999px;padding:2px 10px;font-size:var(--text-xs);font-weight:600">${_esc(p.rasse_text)}</span>` : ''} ${p.rasse_text ? `<span style="background:rgba(255,255,255,.2);border-radius:999px;padding:2px 10px;font-size:var(--text-xs);font-weight:600">${UI.escape(p.rasse_text)}</span>` : ''}
${p.vdh_mitglied ? `<span style="background:rgba(255,255,255,.2);border-radius:999px;padding:2px 10px;font-size:var(--text-xs);font-weight:600">${UI.icon('certificate')} VDH</span>` : ''} ${p.vdh_mitglied ? `<span style="background:rgba(255,255,255,.2);border-radius:999px;padding:2px 10px;font-size:var(--text-xs);font-weight:600">${UI.icon('certificate')} VDH</span>` : ''}
${p.stadt ? `<span style="opacity:.8;font-size:var(--text-xs)">${UI.icon('map-pin')} ${_esc(p.stadt)}</span>` : ''} ${p.stadt ? `<span style="opacity:.8;font-size:var(--text-xs)">${UI.icon('map-pin')} ${UI.escape(p.stadt)}</span>` : ''}
${seit ? `<span style="opacity:.7;font-size:var(--text-xs)">Züchter seit ${_esc(seit)}</span>` : ''} ${seit ? `<span style="opacity:.7;font-size:var(--text-xs)">Züchter seit ${UI.escape(seit)}</span>` : ''}
</div> </div>
</div> </div>
${p.logo_url ${p.logo_url
? `<img src="${_esc(p.logo_url)}" alt="Zwinger-Logo" ? `<img src="${UI.escape(p.logo_url)}" alt="Zwinger-Logo"
style="width:72px;height:72px;border-radius:50%;object-fit:cover; style="width:72px;height:72px;border-radius:50%;object-fit:cover;
border:3px solid rgba(255,255,255,.5);flex-shrink:0;box-shadow:0 2px 12px rgba(0,0,0,.25)" border:3px solid rgba(255,255,255,.5);flex-shrink:0;box-shadow:0 2px 12px rgba(0,0,0,.25)"
onerror="this.style.display='none'">` onerror="this.style.display='none'">`
@ -117,7 +115,7 @@ window.Page_breeder = (() => {
Anmelden um zu schreiben Anmelden um zu schreiben
</button>` </button>`
} }
${p.website ? `<a href="${_esc(p.website)}" target="_blank" rel="noopener noreferrer" ${p.website ? `<a href="${UI.escape(p.website)}" target="_blank" rel="noopener noreferrer"
style="background:rgba(255,255,255,.2);color:white;border:1px solid rgba(255,255,255,.4); style="background:rgba(255,255,255,.2);color:white;border:1px solid rgba(255,255,255,.4);
border-radius:999px;padding:var(--space-2) var(--space-5); border-radius:999px;padding:var(--space-2) var(--space-5);
font-weight:600;font-size:var(--text-sm);text-decoration:none; font-weight:600;font-size:var(--text-sm);text-decoration:none;
@ -134,7 +132,7 @@ window.Page_breeder = (() => {
${p.beschreibung ? ` ${p.beschreibung ? `
<div style="background:var(--c-bg-secondary);border:1px solid var(--c-border);border-radius:var(--radius-lg); <div style="background:var(--c-bg-secondary);border:1px solid var(--c-border);border-radius:var(--radius-lg);
padding:var(--space-4);margin-bottom:var(--space-4)"> padding:var(--space-4);margin-bottom:var(--space-4)">
<p style="margin:0;line-height:1.7;color:var(--c-text-secondary);white-space:pre-line">${_esc(p.beschreibung)}</p> <p style="margin:0;line-height:1.7;color:var(--c-text-secondary);white-space:pre-line">${UI.escape(p.beschreibung)}</p>
</div>` : ''} </div>` : ''}
<!-- Zuchthunde --> <!-- Zuchthunde -->
@ -192,8 +190,8 @@ window.Page_breeder = (() => {
${p.website ? ` ${p.website ? `
<div style="display:flex;gap:var(--space-2);align-items:baseline"> <div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:110px;font-size:var(--text-sm);flex-shrink:0">Website</dt> <dt style="color:var(--c-text-secondary);min-width:110px;font-size:var(--text-sm);flex-shrink:0">Website</dt>
<dd style="margin:0"><a href="${_esc(p.website)}" target="_blank" rel="noopener noreferrer" <dd style="margin:0"><a href="${UI.escape(p.website)}" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);word-break:break-all">${_esc(p.website)}</a></dd> style="color:var(--c-primary);word-break:break-all">${UI.escape(p.website)}</a></dd>
</div>` : ''} </div>` : ''}
${seit ? _dl('Züchter seit', seit) : ''} ${seit ? _dl('Züchter seit', seit) : ''}
</dl> </dl>
@ -209,11 +207,11 @@ window.Page_breeder = (() => {
</h2> </h2>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-2)"> <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-2)">
${p.fotos.map((ph, i) => ` ${p.fotos.map((ph, i) => `
<a href="${_esc(ph.url)}" target="_blank" rel="noopener noreferrer" <a href="${UI.escape(ph.url)}" target="_blank" rel="noopener noreferrer"
style="display:block;border-radius:var(--radius-md);overflow:hidden; style="display:block;border-radius:var(--radius-md);overflow:hidden;
border:${ph.primary ? '2px solid var(--c-primary)' : '1px solid var(--c-border)'}; border:${ph.primary ? '2px solid var(--c-primary)' : '1px solid var(--c-border)'};
aspect-ratio:1;position:relative"> aspect-ratio:1;position:relative">
<img src="${_esc(ph.thumb)}" alt="${_esc(ph.caption)}" <img src="${UI.escape(ph.thumb)}" alt="${UI.escape(ph.caption)}"
loading="${i < 6 ? 'eager' : 'lazy'}" loading="${i < 6 ? 'eager' : 'lazy'}"
style="width:100%;height:100%;object-fit:cover;display:block" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'"> onerror="this.parentElement.style.display='none'">
@ -221,7 +219,7 @@ window.Page_breeder = (() => {
color:white;font-size:9px;font-weight:700;border-radius:999px;padding:1px 6px">Logo</span>` : ''} color:white;font-size:9px;font-weight:700;border-radius:999px;padding:1px 6px">Logo</span>` : ''}
${ph.caption ? `<div style="position:absolute;bottom:0;left:0;right:0; ${ph.caption ? `<div style="position:absolute;bottom:0;left:0;right:0;
background:linear-gradient(transparent,rgba(0,0,0,.6)); background:linear-gradient(transparent,rgba(0,0,0,.6));
color:white;font-size:10px;padding:12px 6px 4px;line-height:1.3">${_esc(ph.caption)}</div>` : ''} color:white;font-size:10px;padding:12px 6px 4px;line-height:1.3">${UI.escape(ph.caption)}</div>` : ''}
</a>`).join('')} </a>`).join('')}
</div> </div>
</div>` : ''} </div>` : ''}
@ -251,14 +249,14 @@ window.Page_breeder = (() => {
const augeTest = h.health_tests?.find(t => t.test_typ === 'augen'); const augeTest = h.health_tests?.find(t => t.test_typ === 'augen');
const testPills = [ const testPills = [
hdTest ? `<span style="${_testPillStyle(hdTest.ergebnis,'HD')}">HD ${_esc(hdTest.ergebnis)}</span>` : '', hdTest ? `<span style="${_testPillStyle(hdTest.ergebnis,'HD')}">HD ${UI.escape(hdTest.ergebnis)}</span>` : '',
edTest ? `<span style="${_testPillStyle(edTest.ergebnis,'ED')}">ED ${_esc(edTest.ergebnis)}</span>` : '', edTest ? `<span style="${_testPillStyle(edTest.ergebnis,'ED')}">ED ${UI.escape(edTest.ergebnis)}</span>` : '',
augeTest ? `<span style="${_testPillStyle('clear','augen')}">Augen ✓</span>` : '', augeTest ? `<span style="${_testPillStyle('clear','augen')}">Augen ✓</span>` : '',
].filter(Boolean).join(''); ].filter(Boolean).join('');
const titlePills = (h.titel || []).map(t => const titlePills = (h.titel || []).map(t =>
`<span style="background:var(--c-primary-light,#f5e6d3);color:var(--c-primary-dark,#a86e2e); `<span style="background:var(--c-primary-light,#f5e6d3);color:var(--c-primary-dark,#a86e2e);
border-radius:999px;padding:1px 8px;font-size:10px;font-weight:700">${_esc(t)}</span>` border-radius:999px;padding:1px 8px;font-size:10px;font-weight:700">${UI.escape(t)}</span>`
).join(''); ).join('');
const genBadge = h.gentests_total > 0 const genBadge = h.gentests_total > 0
@ -272,11 +270,11 @@ window.Page_breeder = (() => {
padding:var(--space-3);display:flex;flex-direction:column;gap:var(--space-2)"> padding:var(--space-3);display:flex;flex-direction:column;gap:var(--space-2)">
<div style="display:flex;align-items:center;gap:var(--space-2)"> <div style="display:flex;align-items:center;gap:var(--space-2)">
<span class="text-primary">${gIcon}</span> <span class="text-primary">${gIcon}</span>
<span style="font-weight:700;font-size:var(--text-sm)">${_esc(h.name)}</span> <span style="font-weight:700;font-size:var(--text-sm)">${UI.escape(h.name)}</span>
${h.rufname ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">"${_esc(h.rufname)}"</span>` : ''} ${h.rufname ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">"${UI.escape(h.rufname)}"</span>` : ''}
${alter !== null ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs);margin-left:auto">${alter} J.</span>` : ''} ${alter !== null ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs);margin-left:auto">${alter} J.</span>` : ''}
</div> </div>
${h.farbe ? `<p style="margin:0;font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(h.farbe)}</p>` : ''} ${h.farbe ? `<p style="margin:0;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.escape(h.farbe)}</p>` : ''}
${testPills ? `<div style="display:flex;flex-wrap:wrap;gap:4px">${testPills}</div>` : ''} ${testPills ? `<div style="display:flex;flex-wrap:wrap;gap:4px">${testPills}</div>` : ''}
${titlePills ? `<div style="display:flex;flex-wrap:wrap;gap:4px">${titlePills}</div>` : ''} ${titlePills ? `<div style="display:flex;flex-wrap:wrap;gap:4px">${titlePills}</div>` : ''}
${genBadge} ${genBadge}
@ -318,16 +316,16 @@ window.Page_breeder = (() => {
<div style="background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius-lg); <div style="background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius-lg);
padding:var(--space-3) var(--space-4)"> padding:var(--space-3) var(--space-4)">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)"> <div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)">
<span style="font-weight:700;font-size:var(--text-sm)">${_esc(eltern)}</span> <span style="font-weight:700;font-size:var(--text-sm)">${UI.escape(eltern)}</span>
<span style="background:${sc}1a;color:${sc};border:1px solid ${sc}40; <span style="background:${sc}1a;color:${sc};border:1px solid ${sc}40;
border-radius:999px;padding:1px 8px;font-size:var(--text-xs);font-weight:600">${sl}</span> border-radius:999px;padding:1px 8px;font-size:var(--text-xs);font-weight:600">${sl}</span>
</div> </div>
<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;font-size:var(--text-xs);color:var(--c-text-secondary)"> <div style="display:flex;gap:var(--space-4);flex-wrap:wrap;font-size:var(--text-xs);color:var(--c-text-secondary)">
${datum ? `<span>${UI.icon('calendar-dots')} ${_esc(datum)}</span>` : ''} ${datum ? `<span>${UI.icon('calendar-dots')} ${UI.escape(datum)}</span>` : ''}
${w.welpen_gesamt ? `<span>${UI.icon('dog')} ${w.welpen_verfuegbar ?? '?'}/${w.welpen_gesamt} verfügbar</span>` : ''} ${w.welpen_gesamt ? `<span>${UI.icon('dog')} ${w.welpen_verfuegbar ?? '?'}/${w.welpen_gesamt} verfügbar</span>` : ''}
${w.preis_spanne ? `<span>${UI.icon('currency-eur')} ${_esc(w.preis_spanne)}</span>` : ''} ${w.preis_spanne ? `<span>${UI.icon('currency-eur')} ${UI.escape(w.preis_spanne)}</span>` : ''}
</div> </div>
${w.beschreibung ? `<p style="margin:var(--space-2) 0 0;font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">${_esc(w.beschreibung)}</p>` : ''} ${w.beschreibung ? `<p style="margin:var(--space-2) 0 0;font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">${UI.escape(w.beschreibung)}</p>` : ''}
</div>`; </div>`;
} }
@ -340,11 +338,11 @@ window.Page_breeder = (() => {
return ` return `
<div> <div>
<p style="margin:0 0 var(--space-2);font-size:var(--text-xs);font-weight:700; <p style="margin:0 0 var(--space-2);font-size:var(--text-xs);font-weight:700;
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em">${_esc(label)}</p> color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.06em">${UI.escape(label)}</p>
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2)"> <div style="display:flex;flex-wrap:wrap;gap:var(--space-2)">
${stats.map(r => ` ${stats.map(r => `
<div style="display:flex;align-items:center;gap:6px;font-size:var(--text-sm)"> <div style="display:flex;align-items:center;gap:6px;font-size:var(--text-sm)">
<span style="font-weight:700">${_esc(r.ergebnis || '—')}</span> <span style="font-weight:700">${UI.escape(r.ergebnis || '—')}</span>
<span class="text-muted">${r.cnt}×</span> <span class="text-muted">${r.cnt}×</span>
<span style="background:var(--c-border);border-radius:999px;height:6px; <span style="background:var(--c-border);border-radius:999px;height:6px;
width:${Math.round(r.cnt/total*80)+16}px;display:inline-block"></span> width:${Math.round(r.cnt/total*80)+16}px;display:inline-block"></span>
@ -359,8 +357,8 @@ window.Page_breeder = (() => {
function _dl(label, value) { function _dl(label, value) {
if (!value) return ''; if (!value) return '';
return `<div style="display:flex;gap:var(--space-2);align-items:baseline"> return `<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:110px;font-size:var(--text-sm);flex-shrink:0">${_esc(label)}</dt> <dt style="color:var(--c-text-secondary);min-width:110px;font-size:var(--text-sm);flex-shrink:0">${UI.escape(label)}</dt>
<dd style="margin:0;font-size:var(--text-sm)">${_esc(String(value))}</dd> <dd style="margin:0;font-size:var(--text-sm)">${UI.escape(String(value))}</dd>
</div>`; </div>`;
} }
@ -383,10 +381,10 @@ window.Page_breeder = (() => {
</h2> </h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:var(--space-2)"> <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:var(--space-2)">
${photos.map(ph => ` ${photos.map(ph => `
<a href="${_esc(ph.url||'')}" target="_blank" rel="noopener noreferrer" <a href="${UI.escape(ph.url||'')}" target="_blank" rel="noopener noreferrer"
style="display:block;border-radius:var(--radius-md);overflow:hidden; style="display:block;border-radius:var(--radius-md);overflow:hidden;
border:1px solid var(--c-border);aspect-ratio:1"> border:1px solid var(--c-border);aspect-ratio:1">
<img src="${_esc(ph.thumbnail_url||ph.url||'')}" alt="${_esc(ph.caption||'')}" <img src="${UI.escape(ph.thumbnail_url||ph.url||'')}" alt="${UI.escape(ph.caption||'')}"
loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block" loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'"> onerror="this.parentElement.style.display='none'">
</a>`).join('')} </a>`).join('')}

View file

@ -122,7 +122,7 @@ window.Page_chat = (() => {
el.innerHTML = convs.map(c => { el.innerHTML = convs.map(c => {
const initials = (c.partner_name || '?')[0].toUpperCase(); const initials = (c.partner_name || '?')[0].toUpperCase();
const preview = c.last_text const preview = c.last_text
? _esc(c.last_text.substring(0, 60)) + (c.last_text.length > 60 ? '…' : '') ? UI.escape(c.last_text.substring(0, 60)) + (c.last_text.length > 60 ? '…' : '')
: '<em style="opacity:0.6">Noch keine Nachrichten</em>'; : '<em style="opacity:0.6">Noch keine Nachrichten</em>';
const timeStr = c.last_msg_at ? _fmtTime(c.last_msg_at) : ''; const timeStr = c.last_msg_at ? _fmtTime(c.last_msg_at) : '';
const badge = c.unread_count > 0 const badge = c.unread_count > 0
@ -138,7 +138,7 @@ window.Page_chat = (() => {
${onlineDot ? `<span class="online-dot chat-avatar-dot"></span>` : ''} ${onlineDot ? `<span class="online-dot chat-avatar-dot"></span>` : ''}
</div> </div>
<div class="chat-conv-info"> <div class="chat-conv-info">
<div class="chat-conv-name">${_esc(c.partner_name)}</div> <div class="chat-conv-name">${UI.escape(c.partner_name)}</div>
<div class="chat-conv-preview">${preview}</div> <div class="chat-conv-preview">${preview}</div>
</div> </div>
<div class="chat-conv-meta"> <div class="chat-conv-meta">
@ -332,10 +332,10 @@ window.Page_chat = (() => {
} }
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)">` : '') +
_esc(m.text) + UI.escape(m.text) +
(m.media_url ? `</div>` : ''); (m.media_url ? `</div>` : '');
} }
if (!bubbleContent) bubbleContent = _esc(m.text); if (!bubbleContent) bubbleContent = UI.escape(m.text);
html += ` html += `
<div class="chat-bubble-row ${rowClass}"> <div class="chat-bubble-row ${rowClass}">
@ -450,13 +450,6 @@ window.Page_chat = (() => {
return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' }); return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' });
} }
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')
.replace(/\n/g, '<br>');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// Neue Nachricht — Freundesliste als Picker // Neue Nachricht — Freundesliste als Picker
// ---------------------------------------------------------- // ----------------------------------------------------------

View file

@ -84,7 +84,7 @@ window.Page_dog_profile = (() => {
<div style="position:relative;display:inline-block;margin-bottom:var(--space-4);padding:4px"> <div style="position:relative;display:inline-block;margin-bottom:var(--space-4);padding:4px">
${dog.foto_url ${dog.foto_url
? `<div class="dp-avatar-ring"> ? `<div class="dp-avatar-ring">
<img src="${dog.foto_url}" alt="${_esc(dog.name)}" class="dp-avatar-img" <img src="${dog.foto_url}" alt="${UI.escape(dog.name)}" class="dp-avatar-img"
style="transform:scale(${dog.foto_zoom||1}) translate(${dog.foto_offset_x||0}%,${dog.foto_offset_y||0}%)"> style="transform:scale(${dog.foto_zoom||1}) translate(${dog.foto_offset_x||0}%,${dog.foto_offset_y||0}%)">
</div>` </div>`
: `<div class="dp-avatar-ring dp-avatar-empty">${UI.icon('dog')}</div>`} : `<div class="dp-avatar-ring dp-avatar-empty">${UI.icon('dog')}</div>`}
@ -95,9 +95,9 @@ window.Page_dog_profile = (() => {
<!-- Name + Rasse --> <!-- Name + Rasse -->
<h2 style="font-size:var(--text-2xl);font-weight:700; <h2 style="font-size:var(--text-2xl);font-weight:700;
color:var(--c-text);margin:0 0 var(--space-1)">${_esc(dog.name)}</h2> color:var(--c-text);margin:0 0 var(--space-1)">${UI.escape(dog.name)}</h2>
${dog.rasse ${dog.rasse
? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-2)">${_esc(dog.rasse)}</p>` ? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-2)">${UI.escape(dog.rasse)}</p>`
: `<p style="margin:0 0 var(--space-2)"></p>`} : `<p style="margin:0 0 var(--space-2)"></p>`}
<!-- Rassen-Community-Chip (wird async geladen) --> <!-- Rassen-Community-Chip (wird async geladen) -->
@ -141,7 +141,7 @@ window.Page_dog_profile = (() => {
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#wave-sine"></use></svg> Transponder <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#wave-sine"></use></svg> Transponder
</div> </div>
${dog.chip_nr ${dog.chip_nr
? `<div style="font-size:var(--text-xs);font-weight:500;word-break:break-all">${_esc(dog.chip_nr)}</div>` ? `<div style="font-size:var(--text-xs);font-weight:500;word-break:break-all">${UI.escape(dog.chip_nr)}</div>`
: `<div class="text-xs-muted">nicht eingetragen : `<div class="text-xs-muted">nicht eingetragen
<button class="btn btn-link btn-sm" id="dp-chip-edit-btn" <button class="btn btn-link btn-sm" id="dp-chip-edit-btn"
style="padding:0 0 0 var(--space-1);font-size:var(--text-xs)">Eintragen</button> style="padding:0 0 0 var(--space-1);font-size:var(--text-xs)">Eintragen</button>
@ -153,7 +153,7 @@ window.Page_dog_profile = (() => {
${dog.bio ? ` ${dog.bio ? `
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-5);text-align:left"> <div class="card" style="padding:var(--space-4);margin-bottom:var(--space-5);text-align:left">
<p style="margin:0;color:var(--c-text-secondary);font-style:italic;line-height:1.6"> <p style="margin:0;color:var(--c-text-secondary);font-style:italic;line-height:1.6">
"${_esc(dog.bio)}" "${UI.escape(dog.bio)}"
</p> </p>
</div> </div>
` : ''} ` : ''}
@ -335,7 +335,7 @@ window.Page_dog_profile = (() => {
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true"> <svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true">
<use href="/icons/phosphor.svg#${isGreen ? 'check' : 'fire'}"></use> <use href="/icons/phosphor.svg#${isGreen ? 'check' : 'fire'}"></use>
</svg> </svg>
${_esc(skill.exercise_name)} ${UI.escape(skill.exercise_name)}
</span>`; </span>`;
}; };
@ -413,7 +413,7 @@ window.Page_dog_profile = (() => {
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)"> <div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
<span style="font-size:1.1em">🛁</span> <span style="font-size:1.1em">🛁</span>
<span style="font-size:var(--text-sm);font-weight:600"> <span style="font-size:var(--text-sm);font-weight:600">
Pflegetipps${data.rasse_name ? ` für ${_esc(data.rasse_name)}` : ''} Pflegetipps${data.rasse_name ? ` für ${UI.escape(data.rasse_name)}` : ''}
</span> </span>
</div> </div>
@ -426,24 +426,24 @@ window.Page_dog_profile = (() => {
${t.saisonal_aktuell ? '🌸 Aktuell & Saisonal' : '💡 Tipp des Tages'} ${t.saisonal_aktuell ? '🌸 Aktuell & Saisonal' : '💡 Tipp des Tages'}
</div> </div>
<div style="font-weight:600;font-size:var(--text-sm);margin-bottom:4px"> <div style="font-weight:600;font-size:var(--text-sm);margin-bottom:4px">
${kat_icons[t.kategorie]||_ph('paw-print')} ${_esc(t.titel)} ${kat_icons[t.kategorie]||_ph('paw-print')} ${UI.escape(t.titel)}
</div> </div>
<div style="font-size:12px;color:var(--c-text-secondary);margin-bottom:8px; <div style="font-size:12px;color:var(--c-text-secondary);margin-bottom:8px;
line-height:1.5">${_esc(t.beschreibung||'')}</div> line-height:1.5">${UI.escape(t.beschreibung||'')}</div>
${t.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted)"> ${t.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted)">
🔄 ${_esc(t.haeufigkeit)}</div>` : ''} 🔄 ${UI.escape(t.haeufigkeit)}</div>` : ''}
${t.materialien ? `<div style="font-size:11px;color:var(--c-text-muted)"> ${t.materialien ? `<div style="font-size:11px;color:var(--c-text-muted)">
🛒 ${_esc(t.materialien)}</div>` : ''} 🛒 ${UI.escape(t.materialien)}</div>` : ''}
${t.schritte?.length ? ` ${t.schritte?.length ? `
<details style="margin-top:8px"> <details style="margin-top:8px">
<summary style="font-size:12px;cursor:pointer;color:var(--c-primary); <summary style="font-size:12px;cursor:pointer;color:var(--c-primary);
font-weight:600">Anleitung anzeigen</summary> font-weight:600">Anleitung anzeigen</summary>
<ol style="margin:8px 0 0 16px;padding:0;font-size:12px; <ol style="margin:8px 0 0 16px;padding:0;font-size:12px;
color:var(--c-text);line-height:1.6"> color:var(--c-text);line-height:1.6">
${t.schritte.map(s=>`<li style="margin-bottom:3px">${_esc(s)}</li>`).join('')} ${t.schritte.map(s=>`<li style="margin-bottom:3px">${UI.escape(s)}</li>`).join('')}
</ol> </ol>
${t.tipp ? `<div style="margin-top:8px;font-size:11px;color:#a78bfa; ${t.tipp ? `<div style="margin-top:8px;font-size:11px;color:#a78bfa;
font-style:italic">💜 ${_esc(t.tipp)}</div>` : ''} font-style:italic">💜 ${UI.escape(t.tipp)}</div>` : ''}
</details>` : ''} </details>` : ''}
</div>` : ''} </div>` : ''}
@ -460,26 +460,26 @@ window.Page_dog_profile = (() => {
<div class="mb-3"> <div class="mb-3">
<div style="font-size:11px;font-weight:700;color:var(--c-text-muted); <div style="font-size:11px;font-weight:700;color:var(--c-text-muted);
text-transform:uppercase;margin-bottom:8px;display:flex;align-items:center"> text-transform:uppercase;margin-bottom:8px;display:flex;align-items:center">
${kat_icons[kat]||_ph('paw-print')} ${_esc(kat)}${katBadge}</div> ${kat_icons[kat]||_ph('paw-print')} ${UI.escape(kat)}${katBadge}</div>
${katTipps.map(tip => ` ${katTipps.map(tip => `
<details style="background:var(--c-surface-2);border-radius:8px; <details style="background:var(--c-surface-2);border-radius:8px;
padding:10px;margin-bottom:6px"> padding:10px;margin-bottom:6px">
<summary style="font-size:var(--text-sm);font-weight:600;cursor:pointer; <summary style="font-size:var(--text-sm);font-weight:600;cursor:pointer;
list-style:none;display:flex;justify-content:space-between; list-style:none;display:flex;justify-content:space-between;
align-items:center"> align-items:center">
${_esc(tip.titel)} ${UI.escape(tip.titel)}
${tip.saisonal_aktuell ? '<span style="font-size:10px;color:#10b981">● Aktuell</span>' : ''} ${tip.saisonal_aktuell ? '<span style="font-size:10px;color:#10b981">● Aktuell</span>' : ''}
</summary> </summary>
<div style="margin-top:8px;font-size:12px;color:var(--c-text-secondary); <div style="margin-top:8px;font-size:12px;color:var(--c-text-secondary);
line-height:1.5">${_esc(tip.beschreibung||'')}</div> line-height:1.5">${UI.escape(tip.beschreibung||'')}</div>
${tip.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted); ${tip.haeufigkeit ? `<div style="font-size:11px;color:var(--c-text-muted);
margin-top:4px">🔄 ${_esc(tip.haeufigkeit)}</div>` : ''} margin-top:4px">🔄 ${UI.escape(tip.haeufigkeit)}</div>` : ''}
${tip.schritte?.length ? ` ${tip.schritte?.length ? `
<ol style="margin:8px 0 0 16px;padding:0;font-size:12px;line-height:1.6"> <ol style="margin:8px 0 0 16px;padding:0;font-size:12px;line-height:1.6">
${tip.schritte.map(s=>`<li style="margin-bottom:3px">${_esc(s)}</li>`).join('')} ${tip.schritte.map(s=>`<li style="margin-bottom:3px">${UI.escape(s)}</li>`).join('')}
</ol>` : ''} </ol>` : ''}
${tip.tipp ? `<div style="margin-top:6px;font-size:11px;color:#a78bfa; ${tip.tipp ? `<div style="margin-top:6px;font-size:11px;color:#a78bfa;
font-style:italic">💜 ${_esc(tip.tipp)}</div>` : ''} font-style:italic">💜 ${UI.escape(tip.tipp)}</div>` : ''}
</details>`).join('')} </details>`).join('')}
</div>`; </div>`;
}).join('')} }).join('')}
@ -499,12 +499,6 @@ window.Page_dog_profile = (() => {
}); });
} }
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// SITTER-ZUGANG // SITTER-ZUGANG
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -527,8 +521,8 @@ window.Page_dog_profile = (() => {
<div style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);background:var(--c-surface-2);border-radius:var(--radius-md);margin-bottom:var(--space-2)"> <div style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);background:var(--c-surface-2);border-radius:var(--radius-md);margin-bottom:var(--space-2)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg>
<div style="flex:1;font-size:var(--text-sm)"> <div style="flex:1;font-size:var(--text-sm)">
<strong>${_esc(s.sitter_name)}</strong> <strong>${UI.escape(s.sitter_name)}</strong>
<span class="text-muted"> · bis ${_esc(s.valid_until)}</span> <span class="text-muted"> · bis ${UI.escape(s.valid_until)}</span>
</div> </div>
<button class="btn btn-link btn-sm sa-revoke-btn" data-sub-id="${s.id}" <button class="btn btn-link btn-sm sa-revoke-btn" data-sub-id="${s.id}"
style="color:var(--c-danger);padding:0"> style="color:var(--c-danger);padding:0">
@ -538,7 +532,7 @@ window.Page_dog_profile = (() => {
} }
const friendOptions = friends.length const friendOptions = friends.length
? friends.map(f => `<option value="${f.friend_id}">${_esc(f.friend_name)}</option>`).join('') ? friends.map(f => `<option value="${f.friend_id}">${UI.escape(f.friend_name)}</option>`).join('')
: '<option value="" disabled>Keine Freunde vorhanden</option>'; : '<option value="" disabled>Keine Freunde vorhanden</option>';
const today = new Date().toISOString().slice(0, 10); const today = new Date().toISOString().slice(0, 10);
@ -617,7 +611,7 @@ window.Page_dog_profile = (() => {
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Chip-Nummer (15-stellig)</label> <label class="form-label">Chip-Nummer (15-stellig)</label>
<input id="chip-edit-input" class="form-control" type="text" <input id="chip-edit-input" class="form-control" type="text"
value="${_esc(dog.chip_nr || '')}" placeholder="z.B. 276009200123456" maxlength="20"> value="${UI.escape(dog.chip_nr || '')}" placeholder="z.B. 276009200123456" maxlength="20">
</div>`, </div>`,
footer: ` footer: `
<div class="w3-btn-stack"> <div class="w3-btn-stack">
@ -843,15 +837,15 @@ window.Page_dog_profile = (() => {
<!-- Header --> <!-- Header -->
<div style="display:flex;align-items:center;gap:12px;margin-bottom:18px"> <div style="display:flex;align-items:center;gap:12px;margin-bottom:18px">
${dog.foto_url ${dog.foto_url
? `<img src="${_esc(dog.foto_url)}" style="width:52px;height:52px;border-radius:50%;object-fit:cover; ? `<img src="${UI.escape(dog.foto_url)}" style="width:52px;height:52px;border-radius:50%;object-fit:cover;
border:2px solid rgba(196,132,58,0.6);flex-shrink:0">` border:2px solid rgba(196,132,58,0.6);flex-shrink:0">`
: `<div style="width:52px;height:52px;border-radius:50%;background:rgba(196,132,58,0.2); : `<div style="width:52px;height:52px;border-radius:50%;background:rgba(196,132,58,0.2);
display:flex;align-items:center;justify-content:center;font-size:1.6rem; display:flex;align-items:center;justify-content:center;font-size:1.6rem;
flex-shrink:0;border:2px solid rgba(196,132,58,0.4)">🐾</div>`} flex-shrink:0;border:2px solid rgba(196,132,58,0.4)">🐾</div>`}
<div> <div>
<div style="font-size:1.25rem;font-weight:800;color:#fff;line-height:1.2">${_esc(dog.name)}</div> <div style="font-size:1.25rem;font-weight:800;color:#fff;line-height:1.2">${UI.escape(dog.name)}</div>
${metaLine ? `<div style="font-size:0.8rem;color:rgba(255,255,255,0.6);margin-top:2px">${_esc(metaLine)}</div>` : ''} ${metaLine ? `<div style="font-size:0.8rem;color:rgba(255,255,255,0.6);margin-top:2px">${UI.escape(metaLine)}</div>` : ''}
${wohnort ? `<div style="font-size:0.75rem;color:rgba(196,132,58,0.9);margin-top:3px">📍 ${_esc(wohnort)}</div>` : ''} ${wohnort ? `<div style="font-size:0.75rem;color:rgba(196,132,58,0.9);margin-top:3px">📍 ${UI.escape(wohnort)}</div>` : ''}
</div> </div>
</div> </div>
@ -862,11 +856,11 @@ window.Page_dog_profile = (() => {
<div style="display:flex;align-items:flex-end;justify-content:space-between;gap:12px"> <div style="display:flex;align-items:flex-end;justify-content:space-between;gap:12px">
<div class="flex-1-min"> <div class="flex-1-min">
${ownerName ? `<div style="font-size:0.7rem;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Besitzer</div> ${ownerName ? `<div style="font-size:0.7rem;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px">Besitzer</div>
<div style="font-size:0.9rem;font-weight:600;color:rgba(255,255,255,0.85)">${_esc(ownerName)}</div>` : ''} <div style="font-size:0.9rem;font-weight:600;color:rgba(255,255,255,0.85)">${UI.escape(ownerName)}</div>` : ''}
<div style="font-size:0.65rem;color:rgba(255,255,255,0.35);margin-top:8px">banyaro.app</div> <div style="font-size:0.65rem;color:rgba(255,255,255,0.35);margin-top:8px">banyaro.app</div>
</div> </div>
<div style="flex-shrink:0;text-align:center"> <div style="flex-shrink:0;text-align:center">
<img id="dp-vcard-qr" src="${_esc(qrUrl)}" <img id="dp-vcard-qr" src="${UI.escape(qrUrl)}"
style="width:80px;height:80px;border-radius:10px;display:block" style="width:80px;height:80px;border-radius:10px;display:block"
alt="QR-Code"> alt="QR-Code">
<div style="font-size:0.6rem;color:rgba(255,255,255,0.35);margin-top:4px">Profil öffnen</div> <div style="font-size:0.6rem;color:rgba(255,255,255,0.35);margin-top:4px">Profil öffnen</div>
@ -880,7 +874,7 @@ window.Page_dog_profile = (() => {
body: ` body: `
<div class="mb-4">${cardHtml}</div> <div class="mb-4">${cardHtml}</div>
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);text-align:center;margin-bottom:0"> <p style="font-size:var(--text-xs);color:var(--c-text-secondary);text-align:center;margin-bottom:0">
QR-Code auf NFC-Tag oder Anhänger kleben jeder kann das Profil von ${_esc(dog.name)} sofort öffnen. QR-Code auf NFC-Tag oder Anhänger kleben jeder kann das Profil von ${UI.escape(dog.name)} sofort öffnen.
</p> </p>
`, `,
footer: ` footer: `
@ -935,7 +929,7 @@ window.Page_dog_profile = (() => {
async function _showShareModal(dog) { async function _showShareModal(dog) {
UI.modal.open({ UI.modal.open({
title: `${_esc(dog.name)} teilen`, title: `${UI.escape(dog.name)} teilen`,
body: ` body: `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)"> <p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
Erstelle einen Einladungslink, den du per WhatsApp, Signal oder E-Mail teilen kannst. Erstelle einen Einladungslink, den du per WhatsApp, Signal oder E-Mail teilen kannst.
@ -1009,7 +1003,7 @@ window.Page_dog_profile = (() => {
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg>
<div style="flex:1;font-size:var(--text-sm)"> <div style="flex:1;font-size:var(--text-sm)">
${s.shared_with_name ${s.shared_with_name
? `<strong>${_esc(s.shared_with_name)}</strong> · ${s.role}` ? `<strong>${UI.escape(s.shared_with_name)}</strong> · ${s.role}`
: `<em class="text-muted">Ausstehend</em> · ${s.role}`} : `<em class="text-muted">Ausstehend</em> · ${s.role}`}
</div> </div>
<button class="btn btn-link btn-sm share-revoke-btn" data-share-id="${s.id}" <button class="btn btn-link btn-sm share-revoke-btn" data-share-id="${s.id}"
@ -1101,7 +1095,7 @@ window.Page_dog_profile = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Name *</label> <label class="form-label">Name *</label>
<input class="form-control" type="text" name="name" <input class="form-control" type="text" name="name"
value="${_esc(dog?.name || '')}" value="${UI.escape(dog?.name || '')}"
placeholder="z. B. Ban Yaro" required> placeholder="z. B. Ban Yaro" required>
</div> </div>
@ -1113,7 +1107,7 @@ window.Page_dog_profile = (() => {
</label> </label>
<input class="form-control" type="text" name="rasse" <input class="form-control" type="text" name="rasse"
id="dp-rasse-input" id="dp-rasse-input"
value="${_esc(dog?.rasse || '')}" value="${UI.escape(dog?.rasse || '')}"
list="dp-rasse-list" list="dp-rasse-list"
autocomplete="off" autocomplete="off"
placeholder="z. B. Mischling, Golden Retriever…"> placeholder="z. B. Mischling, Golden Retriever…">
@ -1167,7 +1161,7 @@ window.Page_dog_profile = (() => {
${UI.help('Die 15-stellige Chip-Nummer findest du im Heimtierausweis oder beim Tierarzt.')} ${UI.help('Die 15-stellige Chip-Nummer findest du im Heimtierausweis oder beim Tierarzt.')}
</label> </label>
<input class="form-control" type="text" name="chip_nr" <input class="form-control" type="text" name="chip_nr"
value="${_esc(dog?.chip_nr || '')}" placeholder="15-stellig"> value="${UI.escape(dog?.chip_nr || '')}" placeholder="15-stellig">
</div> </div>
<div></div> <div></div>
</div> </div>
@ -1195,7 +1189,7 @@ window.Page_dog_profile = (() => {
<span class="text-secondary">(optional)</span> <span class="text-secondary">(optional)</span>
</label> </label>
<textarea class="form-control" name="bio" rows="2" <textarea class="form-control" name="bio" rows="2"
placeholder="Kurze Beschreibung…">${_esc(dog?.bio || '')}</textarea> placeholder="Kurze Beschreibung…">${UI.escape(dog?.bio || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -1477,7 +1471,7 @@ window.Page_dog_profile = (() => {
Auf diesem Foto konnte kein Hund erkannt werden.<br> Auf diesem Foto konnte kein Hund erkannt werden.<br>
Bitte lade ein deutlicheres Foto hoch. Bitte lade ein deutlicheres Foto hoch.
</p> </p>
${data.hinweis ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-3)">${_esc(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" onclick="UI.modal.close()">Schließen</button>`,
}); });
@ -1490,24 +1484,24 @@ window.Page_dog_profile = (() => {
return ` return `
<div class="rasse-result-card${isTop ? ' rasse-result-card--top' : ''}"> <div class="rasse-result-card${isTop ? ' rasse-result-card--top' : ''}">
<div style="display:flex;align-items:center;justify-content:space-between"> <div style="display:flex;align-items:center;justify-content:space-between">
<div class="rasse-result-name">${isTop ? '🐕 ' : ''}${_esc(r.name)}</div> <div class="rasse-result-name">${isTop ? '🐕 ' : ''}${UI.escape(r.name)}</div>
<span class="rasse-result-pct${isTop ? '' : ' rasse-result-pct--dim'}">${r.sicherheit}%</span> <span class="rasse-result-pct${isTop ? '' : ' rasse-result-pct--dim'}">${r.sicherheit}%</span>
</div> </div>
<div class="rasse-result-bar-wrap"> <div class="rasse-result-bar-wrap">
<div class="rasse-result-bar${isTop ? '' : ' rasse-result-bar--dim'}" <div class="rasse-result-bar${isTop ? '' : ' rasse-result-bar--dim'}"
style="width:${r.sicherheit}%"></div> style="width:${r.sicherheit}%"></div>
</div> </div>
${r.beschreibung ? `<div class="rasse-result-desc">${_esc(r.beschreibung)}</div>` : ''} ${r.beschreibung ? `<div class="rasse-result-desc">${UI.escape(r.beschreibung)}</div>` : ''}
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);flex-wrap:wrap"> <div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);flex-wrap:wrap">
${isTop ? `<button class="btn btn-primary btn-sm" data-action="uebernehmen" ${isTop ? `<button class="btn btn-primary btn-sm" data-action="uebernehmen"
data-rasse="${_esc(r.name)}" class="flex-1"> data-rasse="${UI.escape(r.name)}" class="flex-1">
Rasse übernehmen Rasse übernehmen
</button>` : `<button class="btn btn-secondary btn-sm" data-action="uebernehmen" </button>` : `<button class="btn btn-secondary btn-sm" data-action="uebernehmen"
data-rasse="${_esc(r.name)}" class="flex-1"> data-rasse="${UI.escape(r.name)}" class="flex-1">
Diese wählen Diese wählen
</button>`} </button>`}
${r.wiki_slug ? `<button class="btn btn-ghost btn-sm" data-action="wiki" ${r.wiki_slug ? `<button class="btn btn-ghost btn-sm" data-action="wiki"
data-slug="${_esc(r.wiki_slug)}"> data-slug="${UI.escape(r.wiki_slug)}">
Im Wiki Im Wiki
</button>` : ''} </button>` : ''}
</div> </div>
@ -1521,7 +1515,7 @@ window.Page_dog_profile = (() => {
<div style="padding-bottom:var(--space-2)"> <div style="padding-bottom:var(--space-2)">
${data.hinweis ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md); ${data.hinweis ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-3);margin-bottom:var(--space-3);font-size:var(--text-sm); padding:var(--space-3);margin-bottom:var(--space-3);font-size:var(--text-sm);
color:var(--c-text-secondary)"> ${_esc(data.hinweis)}</div>` : ''} color:var(--c-text-secondary)"> ${UI.escape(data.hinweis)}</div>` : ''}
${cardsHtml} ${cardsHtml}
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2); <p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2);
text-align:center"> text-align:center">
@ -1582,18 +1576,13 @@ window.Page_dog_profile = (() => {
: `${j} Jahr${j !== 1 ? 'e' : ''} alt`; : `${j} Jahr${j !== 1 ? 'e' : ''} alt`;
} }
function _esc(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// HUNDEPASS // HUNDEPASS
// ---------------------------------------------------------- // ----------------------------------------------------------
async function _showPassportModal(dog) { async function _showPassportModal(dog) {
UI.modal.open({ UI.modal.open({
title: `Hundepass — ${_esc(dog.name)}`, title: `Hundepass — ${UI.escape(dog.name)}`,
body: `<div id="pp-body" style="min-height:200px"> body: `<div id="pp-body" style="min-height:200px">
<div style="text-align:center;padding:var(--space-6)"> <div style="text-align:center;padding:var(--space-6)">
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true"> <svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
@ -1636,7 +1625,7 @@ window.Page_dog_profile = (() => {
try { try {
data = await API.get(`/passport/${dog.id}`); data = await API.get(`/passport/${dog.id}`);
} catch (e) { } catch (e) {
wrap.innerHTML = `<p class="text-danger">Fehler beim Laden: ${_esc(e.message)}</p>`; wrap.innerHTML = `<p class="text-danger">Fehler beim Laden: ${UI.escape(e.message)}</p>`;
return; return;
} }
@ -1670,13 +1659,13 @@ window.Page_dog_profile = (() => {
<div> <div>
<div class="text-xs-secondary">Blutgruppe</div> <div class="text-xs-secondary">Blutgruppe</div>
<div id="pp-meta-blutgruppe" style="font-size:var(--text-sm);font-weight:500"> <div id="pp-meta-blutgruppe" style="font-size:var(--text-sm);font-weight:500">
${_esc(meta.blutgruppe) || '<span class="text-muted">nicht eingetragen</span>'} ${UI.escape(meta.blutgruppe) || '<span class="text-muted">nicht eingetragen</span>'}
</div> </div>
</div> </div>
<div> <div>
<div class="text-xs-secondary">Allergien</div> <div class="text-xs-secondary">Allergien</div>
<div id="pp-meta-allergien" class="text-sm"> <div id="pp-meta-allergien" class="text-sm">
${_esc(meta.allergien) || '<span class="text-muted">keine</span>'} ${UI.escape(meta.allergien) || '<span class="text-muted">keine</span>'}
</div> </div>
</div> </div>
</div> </div>
@ -1684,7 +1673,7 @@ window.Page_dog_profile = (() => {
<div class="mt-3"> <div class="mt-3">
<div class="text-xs-secondary">Besonderheiten</div> <div class="text-xs-secondary">Besonderheiten</div>
<div id="pp-meta-besonderheiten" class="text-sm"> <div id="pp-meta-besonderheiten" class="text-sm">
${_esc(meta.besonderheiten)} ${UI.escape(meta.besonderheiten)}
</div> </div>
</div>` : ''} </div>` : ''}
</div> </div>
@ -1709,12 +1698,12 @@ window.Page_dog_profile = (() => {
<div class="pp-vacc-row" data-id="${v.id}" <div class="pp-vacc-row" data-id="${v.id}"
class="pp-data-row"> class="pp-data-row">
<div class="flex-1"> <div class="flex-1">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(v.krankheit)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(v.krankheit)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
Gegeben: ${_fmt(v.datum)} Gegeben: ${_fmt(v.datum)}
${v.naechste ? ` · Nächste: ${_fmt(v.naechste)}` : ''} ${v.naechste ? ` · Nächste: ${_fmt(v.naechste)}` : ''}
${v.tierarzt ? ` · ${_esc(v.tierarzt)}` : ''} ${v.tierarzt ? ` · ${UI.escape(v.tierarzt)}` : ''}
${v.charge_nr ? ` · Charge: ${_esc(v.charge_nr)}` : ''} ${v.charge_nr ? ` · Charge: ${UI.escape(v.charge_nr)}` : ''}
</div> </div>
</div> </div>
<button class="btn btn-link btn-sm pp-vacc-del" data-id="${v.id}" <button class="btn btn-link btn-sm pp-vacc-del" data-id="${v.id}"
@ -1746,12 +1735,12 @@ window.Page_dog_profile = (() => {
<div class="pp-med-row" data-id="${m.id}" <div class="pp-med-row" data-id="${m.id}"
class="pp-data-row"> class="pp-data-row">
<div class="flex-1"> <div class="flex-1">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(m.name)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(m.name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
${m.dosierung ? `${_esc(m.dosierung)} · ` : ''} ${m.dosierung ? `${UI.escape(m.dosierung)} · ` : ''}
${m.von ? `Von ${_fmt(m.von)}` : ''} ${m.von ? `Von ${_fmt(m.von)}` : ''}
${m.bis ? ` bis ${_fmt(m.bis)}` : m.von ? ' · dauerhaft' : ''} ${m.bis ? ` bis ${_fmt(m.bis)}` : m.von ? ' · dauerhaft' : ''}
${m.notiz ? ` · ${_esc(m.notiz)}` : ''} ${m.notiz ? ` · ${UI.escape(m.notiz)}` : ''}
</div> </div>
</div> </div>
<button class="btn btn-link btn-sm pp-med-del" data-id="${m.id}" <button class="btn btn-link btn-sm pp-med-del" data-id="${m.id}"
@ -1813,17 +1802,17 @@ window.Page_dog_profile = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Blutgruppe</label> <label class="form-label">Blutgruppe</label>
<input id="pp-meta-bg" class="form-control" type="text" <input id="pp-meta-bg" class="form-control" type="text"
value="${_esc(current.blutgruppe || '')}" placeholder="z. B. DEA 1.1 positiv"> value="${UI.escape(current.blutgruppe || '')}" placeholder="z. B. DEA 1.1 positiv">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Allergien</label> <label class="form-label">Allergien</label>
<textarea id="pp-meta-al" class="form-control" rows="2" <textarea id="pp-meta-al" class="form-control" rows="2"
placeholder="z. B. Hühnchen, Flohspeichel">${_esc(current.allergien || '')}</textarea> placeholder="z. B. Hühnchen, Flohspeichel">${UI.escape(current.allergien || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Besonderheiten</label> <label class="form-label">Besonderheiten</label>
<textarea id="pp-meta-be" class="form-control" rows="2" <textarea id="pp-meta-be" class="form-control" rows="2"
placeholder="z. B. Herzprobleme, Angstpatient">${_esc(current.besonderheiten || '')}</textarea> placeholder="z. B. Herzprobleme, Angstpatient">${UI.escape(current.besonderheiten || '')}</textarea>
</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">
@ -2001,7 +1990,7 @@ window.Page_dog_profile = (() => {
</p> </p>
<div style="display:flex;gap:var(--space-2);align-items:center"> <div style="display:flex;gap:var(--space-2);align-items:center">
<input id="pp-sharelink-input" class="form-control" type="text" readonly <input id="pp-sharelink-input" class="form-control" type="text" readonly
value="${_esc(url)}" class="text-xs"> value="${UI.escape(url)}" class="text-xs">
<button class="btn btn-secondary btn-sm" id="pp-sharelink-copy" style="flex-shrink:0"> <button class="btn btn-secondary btn-sm" id="pp-sharelink-copy" style="flex-shrink:0">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg>
</button> </button>
@ -2037,7 +2026,7 @@ window.Page_dog_profile = (() => {
return; return;
} }
const name = _esc(data.dog_name); const name = UI.escape(data.dog_name);
const km = data.gesamt_km || 0; const km = data.gesamt_km || 0;
const konfetti = km > 100; const konfetti = km > 100;
@ -2079,8 +2068,8 @@ window.Page_dog_profile = (() => {
<div style="font-size:1rem;color:#d0c8b8;font-weight:600">Tagebucheinträge</div> <div style="font-size:1rem;color:#d0c8b8;font-weight:600">Tagebucheinträge</div>
${data.fotos_gesamt > 0 ? `<div style="font-size:1.1rem;color:#a0c890;font-weight:700;margin-top:4px">📷 ${data.fotos_gesamt} Fotos</div>` : ''} ${data.fotos_gesamt > 0 ? `<div style="font-size:1.1rem;color:#a0c890;font-weight:700;margin-top:4px">📷 ${data.fotos_gesamt} Fotos</div>` : ''}
${data.gassi_tage > 0 ? `<div style="font-size:0.9rem;color:#888;margin-top:4px">🐾 ${data.gassi_tage} aktive Tage</div>` : ''} ${data.gassi_tage > 0 ? `<div style="font-size:0.9rem;color:#888;margin-top:4px">🐾 ${data.gassi_tage} aktive Tage</div>` : ''}
${data.lieblings_monat ? `<div style="font-size:0.85rem;color:#b89a6a;margin-top:4px">Meiste Einträge: ${_esc(data.lieblings_monat)}</div>` : ''} ${data.lieblings_monat ? `<div style="font-size:0.85rem;color:#b89a6a;margin-top:4px">Meiste Einträge: ${UI.escape(data.lieblings_monat)}</div>` : ''}
${aktivitaet ? `<div style="font-size:0.85rem;color:#888">Lieblingsaktivität: ${_esc(aktivitaet)}</div>` : ''} ${aktivitaet ? `<div style="font-size:0.85rem;color:#888">Lieblingsaktivität: ${UI.escape(aktivitaet)}</div>` : ''}
`), `),
_card(` _card(`
<div style="font-size:2rem">🌡</div> <div style="font-size:2rem">🌡</div>
@ -2299,7 +2288,7 @@ window.Page_dog_profile = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
async function _showTimelineModal(dog) { async function _showTimelineModal(dog) {
UI.modal.open({ UI.modal.open({
title: `Lebens-Timeline — ${_esc(dog.name)}`, title: `Lebens-Timeline — ${UI.escape(dog.name)}`,
body: `<div id="dp-timeline-body" style="min-height:200px;text-align:center;padding:var(--space-6)"> body: `<div id="dp-timeline-body" style="min-height:200px;text-align:center;padding:var(--space-6)">
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true"> <svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#spinner-gap"></use> <use href="/icons/phosphor.svg#spinner-gap"></use>
@ -2314,7 +2303,7 @@ window.Page_dog_profile = (() => {
data = await API.get(`/dogs/${dog.id}/timeline`); data = await API.get(`/dogs/${dog.id}/timeline`);
} catch (e) { } catch (e) {
const b = document.getElementById('dp-timeline-body'); const b = document.getElementById('dp-timeline-body');
if (b) b.innerHTML = `<p class="text-danger">Fehler: ${_esc(e.message)}</p>`; if (b) b.innerHTML = `<p class="text-danger">Fehler: ${UI.escape(e.message)}</p>`;
return; return;
} }
@ -2351,14 +2340,14 @@ window.Page_dog_profile = (() => {
for (const ev of events) { for (const ev of events) {
const year = ev.datum ? ev.datum.substring(0, 4) : null; const year = ev.datum ? ev.datum.substring(0, 4) : null;
if (year && year !== lastYear) { if (year && year !== lastYear) {
html += `<div class="tl-year">${_esc(year)}</div>`; html += `<div class="tl-year">${UI.escape(year)}</div>`;
lastYear = year; lastYear = year;
} }
const kat = _KAT[ev.kategorie] || _KAT.tagebuch; const kat = _KAT[ev.kategorie] || _KAT.tagebuch;
const big = ev.is_milestone; const big = ev.is_milestone;
let label = _esc(ev.titel); let label = UI.escape(ev.titel);
if (ev.is_first && ev.kategorie === 'tagebuch') label = `🎉 Erster Tagebucheintrag — ${label}`; if (ev.is_first && ev.kategorie === 'tagebuch') label = `🎉 Erster Tagebucheintrag — ${label}`;
if (ev.is_first && ev.kategorie === 'route') label = `🎉 Erste Route — ${label}`; if (ev.is_first && ev.kategorie === 'route') label = `🎉 Erste Route — ${label}`;
if (ev.is_first && ev.kategorie === 'training') label = `🎉 Erstes Training — ${label}`; if (ev.is_first && ev.kategorie === 'training') label = `🎉 Erstes Training — ${label}`;
@ -2376,13 +2365,13 @@ window.Page_dog_profile = (() => {
box-shadow:${big ? `0 0 0 4px ${kat.color}22` : 'none'}"></div> box-shadow:${big ? `0 0 0 4px ${kat.color}22` : 'none'}"></div>
<div class="tl-card"> <div class="tl-card">
${big && ev.foto_url ? ` ${big && ev.foto_url ? `
<div class="tl-foto" style="background-image:url(${_esc(ev.foto_url)})"></div>` : ''} <div class="tl-foto" style="background-image:url(${UI.escape(ev.foto_url)})"></div>` : ''}
<div class="tl-meta"> <div class="tl-meta">
<span class="tl-badge" style="background:${kat.color}22;color:${kat.color}"> <span class="tl-badge" style="background:${kat.color}22;color:${kat.color}">
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true"> <svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true">
<use href="/icons/phosphor.svg#${kat.icon}"></use> <use href="/icons/phosphor.svg#${kat.icon}"></use>
</svg> </svg>
${_esc(kat.label)} ${UI.escape(kat.label)}
</span> </span>
<span class="tl-date">${_fmtDate(ev.datum)}</span> <span class="tl-date">${_fmtDate(ev.datum)}</span>
</div> </div>
@ -2451,8 +2440,8 @@ window.Page_dog_profile = (() => {
if (!data || data.count === 0) return; if (!data || data.count === 0) return;
const hauptRasse = data.rassen[0]?.rasse || ''; const hauptRasse = data.rassen[0]?.rasse || '';
const label = data.count === 1 const label = data.count === 1
? `1 anderer ${_esc(hauptRasse)}-Halter in der App` ? `1 anderer ${UI.escape(hauptRasse)}-Halter in der App`
: `${data.count} andere ${_esc(hauptRasse)}-Halter in der App`; : `${data.count} andere ${UI.escape(hauptRasse)}-Halter in der App`;
el.innerHTML = ` el.innerHTML = `
<button class="breed-community-chip" id="dp-breed-chip-btn"> <button class="breed-community-chip" id="dp-breed-chip-btn">

View file

@ -21,14 +21,6 @@ window.Page_ernaehrung = (() => {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Escape helper // Escape helper
// ------------------------------------------------------------------ // ------------------------------------------------------------------
function _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// LIFECYCLE // LIFECYCLE
@ -156,12 +148,12 @@ window.Page_ernaehrung = (() => {
<div class="ern-field"> <div class="ern-field">
<label> Gewicht (kg)</label> <label> Gewicht (kg)</label>
<input id="ern-gewicht" type="number" step="0.1" min="0.5" max="100" <input id="ern-gewicht" type="number" step="0.1" min="0.5" max="100"
value="${_esc(gewichtDefault)}" placeholder="15"> value="${UI.escape(gewichtDefault)}" placeholder="15">
</div> </div>
<div class="ern-field"> <div class="ern-field">
<label>🎂 Alter (Jahre)</label> <label>🎂 Alter (Jahre)</label>
<input id="ern-alter" type="number" step="0.5" min="0" max="25" <input id="ern-alter" type="number" step="0.5" min="0" max="25"
value="${_esc(alterDefault)}" placeholder="3"> value="${UI.escape(alterDefault)}" placeholder="3">
</div> </div>
</div> </div>
@ -209,7 +201,7 @@ window.Page_ernaehrung = (() => {
<div class="by-form-group" style="margin:0"> <div class="by-form-group" style="margin:0">
<label class="by-label">Marke / Produkt</label> <label class="by-label">Marke / Produkt</label>
<input id="ern-prof-marke" type="text" class="by-input" <input id="ern-prof-marke" type="text" class="by-input"
value="${_esc(_profil.marke)}" placeholder="z. B. Royal Canin"> value="${UI.escape(_profil.marke)}" placeholder="z. B. Royal Canin">
</div> </div>
<div class="by-form-group" style="margin:0"> <div class="by-form-group" style="margin:0">
<label class="by-label">Portionen pro Tag</label> <label class="by-label">Portionen pro Tag</label>
@ -219,7 +211,7 @@ window.Page_ernaehrung = (() => {
<div class="by-form-group" style="margin:0"> <div class="by-form-group" style="margin:0">
<label class="by-label">Notizen</label> <label class="by-label">Notizen</label>
<textarea id="ern-prof-notizen" class="by-input" rows="2" <textarea id="ern-prof-notizen" class="by-input" rows="2"
placeholder="Besonderheiten, Allergien...">${_esc(_profil.notizen)}</textarea> placeholder="Besonderheiten, Allergien...">${UI.escape(_profil.notizen)}</textarea>
</div> </div>
<button class="btn btn-secondary" id="ern-prof-save-btn"> <button class="btn btn-secondary" id="ern-prof-save-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg>
@ -482,8 +474,8 @@ window.Page_ernaehrung = (() => {
<div style="display:flex;align-items:center;gap:var(--space-2)"> <div style="display:flex;align-items:center;gap:var(--space-2)">
<span style="font-size:1.4rem">${item.emoji}</span> <span style="font-size:1.4rem">${item.emoji}</span>
<div> <div>
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text)">${_esc(item.name)}</div> <div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text)">${UI.escape(item.name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-danger)">${_esc(item.grund)}</div> <div style="font-size:var(--text-xs);color:var(--c-danger)">${UI.escape(item.grund)}</div>
</div> </div>
</div> </div>
</div> </div>
@ -511,7 +503,7 @@ window.Page_ernaehrung = (() => {
border:1px solid var(--c-border)"> border:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>
Der KI-Futterberater beantwortet Ernährungsfragen für Der KI-Futterberater beantwortet Ernährungsfragen für
<strong>${_esc(dog?.name || 'deinen Hund')}</strong>. <strong>${UI.escape(dog?.name || 'deinen Hund')}</strong>.
Bei Gesundheitsfragen immer den Tierarzt zurate ziehen. Bei Gesundheitsfragen immer den Tierarzt zurate ziehen.
</div> </div>
@ -524,8 +516,8 @@ window.Page_ernaehrung = (() => {
'Welche Leckerlis sind gesund?', 'Welche Leckerlis sind gesund?',
].map(q => ` ].map(q => `
<button class="btn btn-sm btn-secondary ern-ki-vorschlag" <button class="btn btn-sm btn-secondary ern-ki-vorschlag"
data-q="${_esc(q)}" data-q="${UI.escape(q)}"
class="text-xs">${_esc(q)}</button> class="text-xs">${UI.escape(q)}</button>
`).join('')} `).join('')}
</div> </div>
@ -577,7 +569,7 @@ window.Page_ernaehrung = (() => {
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-2)"> <div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-2)">
<div style="background:var(--c-primary);color:#fff;border-radius:var(--radius-md); <div style="background:var(--c-primary);color:#fff;border-radius:var(--radius-md);
padding:var(--space-2) var(--space-3);max-width:80%;font-size:var(--text-sm)"> padding:var(--space-2) var(--space-3);max-width:80%;font-size:var(--text-sm)">
${_esc(frage)} ${UI.escape(frage)}
</div> </div>
</div> </div>
`); `);
@ -617,7 +609,7 @@ window.Page_ernaehrung = (() => {
} }
} }
const antwortHtml = _esc(antwort) const antwortHtml = UI.escape(antwort)
.replace(/\n\n/g, '</p><p style="margin:var(--space-1) 0">') .replace(/\n\n/g, '</p><p style="margin:var(--space-1) 0">')
.replace(/\n/g, '<br>'); .replace(/\n/g, '<br>');
@ -746,7 +738,7 @@ window.Page_ernaehrung = (() => {
const dl = document.getElementById('vert-futter-datalist'); const dl = document.getElementById('vert-futter-datalist');
if (!dl) return; if (!dl) return;
const names = [...new Set((list || []).map(e => e.futter_name))]; const names = [...new Set((list || []).map(e => e.futter_name))];
dl.innerHTML = names.map(n => `<option value="${_esc(n)}">`).join(''); dl.innerHTML = names.map(n => `<option value="${UI.escape(n)}">`).join('');
}).catch(() => {}); }).catch(() => {});
setTimeout(() => { setTimeout(() => {
@ -950,7 +942,7 @@ window.Page_ernaehrung = (() => {
<svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-warning,#f59e0b);margin-top:1px"> <svg class="ph-icon" aria-hidden="true" style="flex-shrink:0;color:var(--c-warning,#f59e0b);margin-top:1px">
<use href="/icons/phosphor.svg#warning-circle"></use> <use href="/icons/phosphor.svg#warning-circle"></use>
</svg> </svg>
<span>${_esc(data.hinweis)}</span> <span>${UI.escape(data.hinweis)}</span>
</div> </div>
` : ''; ` : '';
@ -978,7 +970,7 @@ window.Page_ernaehrung = (() => {
return `<span style="font-size:10px;font-weight:600;padding:2px 6px; return `<span style="font-size:10px;font-weight:600;padding:2px 6px;
border-radius:999px;border:1px solid ${chipColor}; border-radius:999px;border:1px solid ${chipColor};
color:${chipColor};white-space:nowrap"> color:${chipColor};white-space:nowrap">
${_esc(KAT_LABELS[kat] || kat)} ×${cnt} ${UI.escape(KAT_LABELS[kat] || kat)} ×${cnt}
</span>`; </span>`;
}).join(''); }).join('');
return ` return `
@ -988,17 +980,17 @@ window.Page_ernaehrung = (() => {
<div style="min-width:0;flex:1"> <div style="min-width:0;flex:1">
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text); <div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(f.name)} ${UI.escape(f.name)}
</div> </div>
<div class="text-xs-muted"> <div class="text-xs-muted">
${_esc(TYP_LABELS[f.typ] || f.typ)} &middot; ${f.mahlzeiten} Mahlzeit${f.mahlzeiten !== 1 ? 'en' : ''} ${UI.escape(TYP_LABELS[f.typ] || f.typ)} &middot; ${f.mahlzeiten} Mahlzeit${f.mahlzeiten !== 1 ? 'en' : ''}
${f.status !== 'neu' ? `&middot; <span style="color:var(--c-success,#22c55e)">+${f.positiv}</span> / <span style="color:var(--c-danger,#ef4444)">-${f.negativ}</span>` : ''} ${f.status !== 'neu' ? `&middot; <span style="color:var(--c-success,#22c55e)">+${f.positiv}</span> / <span style="color:var(--c-danger,#ef4444)">-${f.negativ}</span>` : ''}
</div> </div>
${katChips ? `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">${katChips}</div>` : ''} ${katChips ? `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">${katChips}</div>` : ''}
</div> </div>
<span style="flex-shrink:0;font-size:var(--text-xs);font-weight:700; <span style="flex-shrink:0;font-size:var(--text-xs);font-weight:700;
color:${cfg.color};white-space:nowrap"> color:${cfg.color};white-space:nowrap">
${_esc(cfg.label)} ${UI.escape(cfg.label)}
</span> </span>
</div> </div>
`; `;
@ -1085,9 +1077,9 @@ window.Page_ernaehrung = (() => {
<use href="/icons/phosphor.svg#bowl-food"></use> <use href="/icons/phosphor.svg#bowl-food"></use>
</svg> </svg>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(item.futter_name)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(item.futter_name)}</div>
<div class="text-xs-muted"> <div class="text-xs-muted">
${_esc(item.datum)} ${_esc(item.uhrzeit)} ${UI.escape(item.datum)} ${UI.escape(item.uhrzeit)}
${item.menge_g ? ` &middot; ${item.menge_g} g` : ''} ${item.menge_g ? ` &middot; ${item.menge_g} g` : ''}
</div> </div>
</div> </div>
@ -1112,11 +1104,11 @@ window.Page_ernaehrung = (() => {
</svg> </svg>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm);color:${col}"> <div style="font-weight:600;font-size:var(--text-sm);color:${col}">
${_esc(REAK_LABELS[item.reaktion_typ] || item.reaktion_typ)} ${UI.escape(REAK_LABELS[item.reaktion_typ] || item.reaktion_typ)}
<span style="font-weight:400;color:var(--c-text-muted)">(${item.intensitaet}/5)</span> <span style="font-weight:400;color:var(--c-text-muted)">(${item.intensitaet}/5)</span>
</div> </div>
<div class="text-xs-muted"> <div class="text-xs-muted">
${_esc(item.datum)} ${_esc(item.uhrzeit)} ${UI.escape(item.datum)} ${UI.escape(item.uhrzeit)}
</div> </div>
</div> </div>
<button class="btn-icon vert-del-reaktion" data-id="${item.id}" <button class="btn-icon vert-del-reaktion" data-id="${item.id}"

View file

@ -486,7 +486,7 @@ window.Page_erste_hilfe = (() => {
display:flex;align-items:center;justify-content:space-between;flex-shrink:0"> display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
<div> <div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div> <div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_esc(parentLabel)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
</div> </div>
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button> <button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
</div> </div>

View file

@ -71,7 +71,7 @@ window.Page_expenses = (() => {
if (dogs.length < 2) return ''; if (dogs.length < 2) return '';
const pills = [{ id: null, name: 'Alle' }, ...dogs].map(d => ` const pills = [{ id: null, name: 'Alle' }, ...dogs].map(d => `
<button class="exp-dog-pill${_selectedDogId === d.id ? ' active' : ''}" data-dog="${d.id ?? ''}"> <button class="exp-dog-pill${_selectedDogId === d.id ? ' active' : ''}" data-dog="${d.id ?? ''}">
${d.id ? UI.icon('paw-print') : ''} ${_esc(d.name)} ${d.id ? UI.icon('paw-print') : ''} ${UI.escape(d.name)}
</button>`).join(''); </button>`).join('');
return `<div class="exp-dog-selector" id="exp-dog-selector">${pills}</div>`; return `<div class="exp-dog-selector" id="exp-dog-selector">${pills}</div>`;
} }
@ -283,10 +283,10 @@ window.Page_expenses = (() => {
const datum = new Date(e.datum + 'T00:00:00') const datum = new Date(e.datum + 'T00:00:00')
.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }); .toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
const dogBadge = e.dog_name const dogBadge = e.dog_name
? `<span>${UI.icon('paw-print')} ${_esc(e.dog_name)}</span>` ? `<span>${UI.icon('paw-print')} ${UI.escape(e.dog_name)}</span>`
: ''; : '';
const notiz = e.notiz const notiz = e.notiz
? `<div class="list-item-text">${_esc(e.notiz)}</div>` ? `<div class="list-item-text">${UI.escape(e.notiz)}</div>`
: ''; : '';
return ` return `
<div class="list-item-card list-item-card--clickable exp-entry" data-id="${e.id}"> <div class="list-item-card list-item-card--clickable exp-entry" data-id="${e.id}">
@ -376,11 +376,11 @@ window.Page_expenses = (() => {
<div class="list-item-meta-badge" style="--meta-color:${k.color}">${UI.icon(k.icon)}</div> <div class="list-item-meta-badge" style="--meta-color:${k.color}">${UI.icon(k.icon)}</div>
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-title">${k.label}</div> <div class="list-item-title">${k.label}</div>
${r.notiz ? `<div class="list-item-text">${_esc(r.notiz)}</div>` : ''} ${r.notiz ? `<div class="list-item-text">${UI.escape(r.notiz)}</div>` : ''}
<div class="list-item-meta-row"> <div class="list-item-meta-row">
<span>${HAEUFIGKEIT_LABEL[r.haeufigkeit] || r.haeufigkeit}</span> <span>${HAEUFIGKEIT_LABEL[r.haeufigkeit] || r.haeufigkeit}</span>
· <span>${UI.icon('calendar')} ${naechste}</span> · <span>${UI.icon('calendar')} ${naechste}</span>
${r.dog_name ? `· <span>${UI.icon('paw-print')} ${_esc(r.dog_name)}</span>` : ''} ${r.dog_name ? `· <span>${UI.icon('paw-print')} ${UI.escape(r.dog_name)}</span>` : ''}
${!r.aktiv ? '· <span>Pausiert</span>' : ''} ${!r.aktiv ? '· <span>Pausiert</span>' : ''}
</div> </div>
</div> </div>
@ -444,7 +444,7 @@ window.Page_expenses = (() => {
].map(k => `<option value="${k.id}" ${r?.kategorie === k.id ? 'selected' : ''}>${k.label}</option>`).join(''); ].map(k => `<option value="${k.id}" ${r?.kategorie === k.id ? 'selected' : ''}>${k.label}</option>`).join('');
const dogOptions = (_appState.dogs || []).map(d => const dogOptions = (_appState.dogs || []).map(d =>
`<option value="${d.id}" ${r?.dog_id === d.id ? 'selected' : ''}>${_esc(d.name)}</option>` `<option value="${d.id}" ${r?.dog_id === d.id ? 'selected' : ''}>${UI.escape(d.name)}</option>`
).join(''); ).join('');
const body = ` const body = `
@ -480,7 +480,7 @@ window.Page_expenses = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Bezeichnung <span class="text-muted">(optional)</span></label> <label class="form-label">Bezeichnung <span class="text-muted">(optional)</span></label>
<input class="form-control" type="text" name="notiz" <input class="form-control" type="text" name="notiz"
value="${_esc(r?.notiz || '')}" placeholder="z.B. Haftpflicht Allianz"> value="${UI.escape(r?.notiz || '')}" placeholder="z.B. Haftpflicht Allianz">
</div> </div>
</form>`; </form>`;
@ -683,7 +683,7 @@ window.Page_expenses = (() => {
const defaultDogId = entry?.dog_id ?? _selectedDogId; const defaultDogId = entry?.dog_id ?? _selectedDogId;
const dogOptions = (_appState.dogs || []).map(d => const dogOptions = (_appState.dogs || []).map(d =>
`<option value="${d.id}"${defaultDogId === d.id ? ' selected' : ''}>${_esc(d.name)}</option>` `<option value="${d.id}"${defaultDogId === d.id ? ' selected' : ''}>${UI.escape(d.name)}</option>`
).join(''); ).join('');
// Kategorie-Kacheln statt Dropdown // Kategorie-Kacheln statt Dropdown
@ -725,7 +725,7 @@ window.Page_expenses = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Notiz <span class="form-label-hint">(optional)</span></label> <label class="form-label">Notiz <span class="form-label-hint">(optional)</span></label>
<input type="text" name="notiz" class="form-control" <input type="text" name="notiz" class="form-control"
value="${_esc(entry?.notiz || '')}" value="${UI.escape(entry?.notiz || '')}"
placeholder="z.B. Hundesteuer 2026, Allianz Haftpflicht …"> placeholder="z.B. Hundesteuer 2026, Allianz Haftpflicht …">
</div> </div>
@ -852,14 +852,5 @@ window.Page_expenses = (() => {
return Math.round(val) + ' €'; return Math.round(val) + ' €';
} }
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
return { init, refresh }; return { init, refresh };
})(); })();

View file

@ -39,11 +39,6 @@ window.Page_forum = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// Helpers // Helpers
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function _fmtDate(iso) { function _fmtDate(iso) {
if (!iso) return '—'; if (!iso) return '—';
const d = new Date(iso); const d = new Date(iso);
@ -108,7 +103,7 @@ window.Page_forum = (() => {
<div class="forum-category-tabs by-tabs" id="forum-tabs"> <div class="forum-category-tabs by-tabs" id="forum-tabs">
${KATEGORIEN.map(k => ` ${KATEGORIEN.map(k => `
<button class="by-tab ${k.key === _aktivKat ? 'active' : ''}" <button class="by-tab ${k.key === _aktivKat ? 'active' : ''}"
data-kat="${k.key}"><span class="by-tab-text">${_esc(k.label)}</span></button> data-kat="${k.key}"><span class="by-tab-text">${UI.escape(k.label)}</span></button>
`).join('')} `).join('')}
<button class="by-tab ${_activeSection === 'map' ? 'active' : ''}" <button class="by-tab ${_activeSection === 'map' ? 'active' : ''}"
data-section="map"><span class="by-tab-text">${UI.icon('users')} Mitgliederkarte</span></button> data-section="map"><span class="by-tab-text">${UI.icon('users')} Mitgliederkarte</span></button>
@ -217,7 +212,7 @@ window.Page_forum = (() => {
.format(new Date(+year, +month - 1, 1)); .format(new Date(+year, +month - 1, 1));
const top = data.top?.[0]; const top = data.top?.[0];
const winnerLine = top const winnerLine = top
? `🥇 ${_esc(top.name)}${top.rasse ? ` · ${_esc(top.rasse)}` : ''}` ? `🥇 ${UI.escape(top.name)}${top.rasse ? ` · ${UI.escape(top.rasse)}` : ''}`
: 'Noch keine Stimmen'; : 'Noch keine Stimmen';
const metaLine = top const metaLine = top
? `${top.stimmen} Stimme${top.stimmen !== 1 ? 'n' : ''}` ? `${top.stimmen} Stimme${top.stimmen !== 1 ? 'n' : ''}`
@ -227,7 +222,7 @@ window.Page_forum = (() => {
<div class="forum-hdm-tile" id="forum-hdm-tile"> <div class="forum-hdm-tile" id="forum-hdm-tile">
<div class="forum-hdm-tile-trophy">🏆</div> <div class="forum-hdm-tile-trophy">🏆</div>
<div class="forum-hdm-tile-body"> <div class="forum-hdm-tile-body">
<div class="forum-hdm-tile-title">Hund des Monats · ${_esc(monthName)}</div> <div class="forum-hdm-tile-title">Hund des Monats · ${UI.escape(monthName)}</div>
<div class="forum-hdm-tile-winner">${winnerLine}</div> <div class="forum-hdm-tile-winner">${winnerLine}</div>
<div class="forum-hdm-tile-meta">${metaLine}</div> <div class="forum-hdm-tile-meta">${metaLine}</div>
</div> </div>
@ -251,16 +246,16 @@ window.Page_forum = (() => {
? data.top.slice(0, 5).map((dog, i) => { ? data.top.slice(0, 5).map((dog, i) => {
const medal = ['🥇','🥈','🥉','4⃣','5⃣'][i]; const medal = ['🥇','🥈','🥉','4⃣','5⃣'][i];
const av = dog.foto_url const av = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}" class="hdm-top-av-img">` ? `<img src="${UI.escape(dog.foto_url)}" alt="${UI.escape(dog.name)}" class="hdm-top-av-img">`
: `<span class="hdm-top-av-placeholder">${_esc(dog.name.charAt(0).toUpperCase())}</span>`; : `<span class="hdm-top-av-placeholder">${UI.escape(dog.name.charAt(0).toUpperCase())}</span>`;
const vorname = dog.besitzer_name ? _esc(dog.besitzer_name.split(' ')[0]) : ''; const vorname = dog.besitzer_name ? UI.escape(dog.besitzer_name.split(' ')[0]) : '';
return ` return `
<div class="hdm-top-entry"> <div class="hdm-top-entry">
<span class="hdm-top-medal">${medal}</span> <span class="hdm-top-medal">${medal}</span>
<div class="hdm-top-av">${av}</div> <div class="hdm-top-av">${av}</div>
<div class="hdm-top-info"> <div class="hdm-top-info">
<div class="hdm-top-name">${_esc(dog.name)}</div> <div class="hdm-top-name">${UI.escape(dog.name)}</div>
${dog.rasse ? `<div class="hdm-top-rasse">${_esc(dog.rasse)}</div>` : ''} ${dog.rasse ? `<div class="hdm-top-rasse">${UI.escape(dog.rasse)}</div>` : ''}
${vorname ? `<div class="hdm-top-besitzer">von ${vorname}</div>` : ''} ${vorname ? `<div class="hdm-top-besitzer">von ${vorname}</div>` : ''}
</div> </div>
<div class="hdm-top-stimmen">${dog.stimmen} ${UI.icon('star')}</div> <div class="hdm-top-stimmen">${dog.stimmen} ${UI.icon('star')}</div>
@ -291,7 +286,7 @@ window.Page_forum = (() => {
<div class="hdm-header"> <div class="hdm-header">
<div class="hdm-trophy">🏆</div> <div class="hdm-trophy">🏆</div>
<h2 class="hdm-title">Hund des Monats</h2> <h2 class="hdm-title">Hund des Monats</h2>
<div class="hdm-monat">${_esc(monthName)}</div> <div class="hdm-monat">${UI.escape(monthName)}</div>
</div> </div>
${voteHint} ${voteHint}
<div class="hdm-section"> <div class="hdm-section">
@ -320,14 +315,14 @@ window.Page_forum = (() => {
grid.innerHTML = list.map(dog => { grid.innerHTML = list.map(dog => {
const isVoted = data.user_vote === dog.id; const isVoted = data.user_vote === dog.id;
const av = dog.foto_url const av = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}" class="hdm-vote-av-img">` ? `<img src="${UI.escape(dog.foto_url)}" alt="${UI.escape(dog.name)}" class="hdm-vote-av-img">`
: `<span class="hdm-vote-av-placeholder">${_esc(dog.name.charAt(0).toUpperCase())}</span>`; : `<span class="hdm-vote-av-placeholder">${UI.escape(dog.name.charAt(0).toUpperCase())}</span>`;
const vorname = dog.besitzer_name ? _esc(dog.besitzer_name.split(' ')[0]) : ''; const vorname = dog.besitzer_name ? UI.escape(dog.besitzer_name.split(' ')[0]) : '';
return ` return `
<div class="hdm-vote-card${isVoted ? ' hdm-vote-card--voted' : ''}"> <div class="hdm-vote-card${isVoted ? ' hdm-vote-card--voted' : ''}">
<div class="hdm-vote-av">${av}</div> <div class="hdm-vote-av">${av}</div>
<div class="hdm-vote-name">${_esc(dog.name)}</div> <div class="hdm-vote-name">${UI.escape(dog.name)}</div>
${dog.rasse ? `<div class="hdm-vote-rasse">${_esc(dog.rasse)}</div>` : ''} ${dog.rasse ? `<div class="hdm-vote-rasse">${UI.escape(dog.rasse)}</div>` : ''}
${vorname ? `<div class="hdm-vote-besitzer text-xs-muted">von ${vorname}</div>` : ''} ${vorname ? `<div class="hdm-vote-besitzer text-xs-muted">von ${vorname}</div>` : ''}
${dog.stimmen > 0 ? `<div class="text-xs-muted">${dog.stimmen} ${UI.icon('star')}</div>` : ''} ${dog.stimmen > 0 ? `<div class="text-xs-muted">${dog.stimmen} ${UI.icon('star')}</div>` : ''}
<button class="btn btn-sm ${isVoted ? 'btn-primary' : 'btn-secondary'} hdm-vote-btn" <button class="btn btn-sm ${isVoted ? 'btn-primary' : 'btn-secondary'} hdm-vote-btn"
@ -443,31 +438,31 @@ window.Page_forum = (() => {
function _threadCardHTML(t) { function _threadCardHTML(t) {
const preview = t.text_preview const preview = t.text_preview
? _esc(t.text_preview.slice(0, 120)) + (t.text_preview.length >= 120 ? '…' : '') ? UI.escape(t.text_preview.slice(0, 120)) + (t.text_preview.length >= 120 ? '…' : '')
: ''; : '';
const pinBadge = t.is_pinned ? `<span class="forum-pin-badge" title="Angepinnt">${UI.icon('push-pin')}</span>` : ''; const pinBadge = t.is_pinned ? `<span class="forum-pin-badge" title="Angepinnt">${UI.icon('push-pin')}</span>` : '';
const lockBadge = t.is_locked ? `<span class="forum-lock-badge" title="Gesperrt">${UI.icon('lock')}</span>` : ''; const lockBadge = t.is_locked ? `<span class="forum-lock-badge" title="Gesperrt">${UI.icon('lock')}</span>` : '';
const fotoHtml = t.foto_preview const fotoHtml = t.foto_preview
? /\.(mp4|mov|webm|m4v|avi)$/i.test(t.foto_preview) ? /\.(mp4|mov|webm|m4v|avi)$/i.test(t.foto_preview)
? `<div class="forum-card-thumb forum-card-thumb--video" style="display:flex;align-items:center;justify-content:center;background:var(--c-surface-2)">${UI.icon('video-camera')}</div>` ? `<div class="forum-card-thumb forum-card-thumb--video" style="display:flex;align-items:center;justify-content:center;background:var(--c-surface-2)">${UI.icon('video-camera')}</div>`
: `<img class="forum-card-thumb" src="${_esc(t.foto_preview_url || t.foto_preview)}" : `<img class="forum-card-thumb" src="${UI.escape(t.foto_preview_url || t.foto_preview)}"
${(t.foto_preview_url && t.foto_preview) ? `srcset="${_esc(t.foto_preview_url)} 800w" sizes="120px"` : ''} ${(t.foto_preview_url && t.foto_preview) ? `srcset="${UI.escape(t.foto_preview_url)} 800w" sizes="120px"` : ''}
alt="" loading="lazy" alt="" loading="lazy"
onerror="this.src='${_esc(t.foto_preview)}'">` onerror="this.src='${UI.escape(t.foto_preview)}'">`
: ''; : '';
return ` return `
<div class="forum-thread-card" data-id="${t.id}"> <div class="forum-thread-card" data-id="${t.id}">
<div class="forum-card-top"> <div class="forum-card-top">
<span class="forum-category-badge forum-category-badge--${_esc(t.kategorie)}">${_esc(t.kategorie)}</span> <span class="forum-category-badge forum-category-badge--${UI.escape(t.kategorie)}">${UI.escape(t.kategorie)}</span>
${pinBadge}${lockBadge} ${pinBadge}${lockBadge}
</div> </div>
<div class="forum-card-content"> <div class="forum-card-content">
<div class="forum-card-main"> <div class="forum-card-main">
<div class="forum-card-title">${_esc(t.titel)}</div> <div class="forum-card-title">${UI.escape(t.titel)}</div>
${preview ? `<div class="forum-card-preview">${preview}</div>` : ''} ${preview ? `<div class="forum-card-preview">${preview}</div>` : ''}
<div class="forum-card-meta"> <div class="forum-card-meta">
<span>${UI.icon('user')} ${_esc(t.autor_name || 'Unbekannt')}</span> <span>${UI.icon('user')} ${UI.escape(t.autor_name || 'Unbekannt')}</span>
<span>${UI.icon('calendar-dots')} ${_fmtDate(t.created_at)}</span> <span>${UI.icon('calendar-dots')} ${_fmtDate(t.created_at)}</span>
<span>${UI.icon('chat-circle-dots')} ${t.antworten || 0}</span> <span>${UI.icon('chat-circle-dots')} ${t.antworten || 0}</span>
<span class="${t.user_liked ? 'forum-liked' : ''}">${UI.icon('heart')} ${t.likes || 0}</span> <span class="${t.user_liked ? 'forum-liked' : ''}">${UI.icon('heart')} ${t.likes || 0}</span>
@ -493,7 +488,7 @@ window.Page_forum = (() => {
document.getElementById('forum-main').innerHTML = ` document.getElementById('forum-main').innerHTML = `
<div style="text-align:center;padding:var(--space-8)"> <div style="text-align:center;padding:var(--space-8)">
<div style="font-size:2rem;margin-bottom:var(--space-2)">${UI.icon('magnifying-glass')}</div> <div style="font-size:2rem;margin-bottom:var(--space-2)">${UI.icon('magnifying-glass')}</div>
<p class="text-secondary">Keine Ergebnisse für ${_esc(q)}"</p> <p class="text-secondary">Keine Ergebnisse für ${UI.escape(q)}"</p>
</div>`; </div>`;
return; return;
} }
@ -538,14 +533,14 @@ window.Page_forum = (() => {
const _forumMediaHtml = (u) => { const _forumMediaHtml = (u) => {
if (u.endsWith('.pdf')) if (u.endsWith('.pdf'))
return `<a href="${_esc(u)}" target="_blank" rel="noopener" class="forum-pdf-card"> return `<a href="${UI.escape(u)}" target="_blank" rel="noopener" class="forum-pdf-card">
${UI.icon('file-text')} <span>${_esc(u.split('/').pop())}</span></a>`; ${UI.icon('file-text')} <span>${UI.escape(u.split('/').pop())}</span></a>`;
if (/\.(mp4|mov|webm|m4v|avi)$/i.test(u)) { if (/\.(mp4|mov|webm|m4v|avi)$/i.test(u)) {
const poster = u.replace(/\.[^.]+$/, '_thumb.jpg'); const poster = u.replace(/\.[^.]+$/, '_thumb.jpg');
return `<video src="${_esc(u)}" poster="${_esc(poster)}" controls playsinline return `<video src="${UI.escape(u)}" poster="${UI.escape(poster)}" controls playsinline
style="max-width:100%;max-height:320px;border-radius:var(--radius-md);display:block"></video>`; style="max-width:100%;max-height:320px;border-radius:var(--radius-md);display:block"></video>`;
} }
return `<img src="${_esc(u)}" class="forum-foto-img" data-src="${_esc(u)}" alt="" loading="lazy">`; return `<img src="${UI.escape(u)}" class="forum-foto-img" data-src="${UI.escape(u)}" alt="" loading="lazy">`;
}; };
const fotoGallery = (thread.foto_urls?.length) const fotoGallery = (thread.foto_urls?.length)
? `<div class="forum-foto-grid">${thread.foto_urls.map(_forumMediaHtml).join('')}</div>` ? `<div class="forum-foto-grid">${thread.foto_urls.map(_forumMediaHtml).join('')}</div>`
@ -578,20 +573,20 @@ window.Page_forum = (() => {
<div class="forum-thread-detail"> <div class="forum-thread-detail">
${modToolbar} ${modToolbar}
<div class="forum-thread-header-row"> <div class="forum-thread-header-row">
<span class="forum-category-badge forum-category-badge--${_esc(thread.kategorie)}">${_esc(thread.kategorie)}</span> <span class="forum-category-badge forum-category-badge--${UI.escape(thread.kategorie)}">${UI.escape(thread.kategorie)}</span>
<span style="color:var(--c-text-muted);font-size:0.8rem">${_fmtDate(thread.created_at)}</span> <span style="color:var(--c-text-muted);font-size:0.8rem">${_fmtDate(thread.created_at)}</span>
${thread.is_pinned ? `<span>${UI.icon('push-pin')}</span>` : ''} ${thread.is_pinned ? `<span>${UI.icon('push-pin')}</span>` : ''}
${thread.is_locked ? `<span>${UI.icon('lock')}</span>` : ''} ${thread.is_locked ? `<span>${UI.icon('lock')}</span>` : ''}
</div> </div>
<div class="forum-thread-body"> <div class="forum-thread-body">
<p style="white-space:pre-wrap;word-break:break-word">${_esc(thread.text)}</p> <p style="white-space:pre-wrap;word-break:break-word">${UI.escape(thread.text)}</p>
${fotoGallery} ${fotoGallery}
</div> </div>
<div class="forum-thread-author-row"> <div class="forum-thread-author-row">
<div class="forum-avatar">${_esc(_initial(thread.autor_name))}</div> <div class="forum-avatar">${UI.escape(_initial(thread.autor_name))}</div>
<span style="font-size:0.85rem;color:var(--c-text-secondary)">${_esc(thread.autor_name || 'Unbekannt')}</span> <span style="font-size:0.85rem;color:var(--c-text-secondary)">${UI.escape(thread.autor_name || 'Unbekannt')}</span>
${thread.autor_founder_number ? `<span style="font-size:10px;font-weight:700;background:#7c3aed;color:#fff;padding:1px 5px;border-radius:4px">Gründer #${thread.autor_founder_number}</span>` : ''} ${thread.autor_founder_number ? `<span style="font-size:10px;font-weight:700;background:#7c3aed;color:#fff;padding:1px 5px;border-radius:4px">Gründer #${thread.autor_founder_number}</span>` : ''}
<div style="margin-left:auto;display:flex;gap:var(--space-2);align-items:center"> <div style="margin-left:auto;display:flex;gap:var(--space-2);align-items:center">
<button class="${likeClass}" id="thread-like-btn" data-count="${thread.likes || 0}"> <button class="${likeClass}" id="thread-like-btn" data-count="${thread.likes || 0}">
@ -623,7 +618,7 @@ window.Page_forum = (() => {
</div> </div>
` : `<button type="button" class="btn btn-primary w-full" id="ft-close">Schließen</button>`; ` : `<button type="button" class="btn btn-primary w-full" id="ft-close">Schließen</button>`;
UI.modal.open({ title: `${UI.icon('chat-circle-dots')} ${_esc(thread.titel)}`, body, footer }); UI.modal.open({ title: `${UI.icon('chat-circle-dots')} ${UI.escape(thread.titel)}`, body, footer });
// Close // Close
document.getElementById('ft-close')?.addEventListener('click', UI.modal.close); document.getElementById('ft-close')?.addEventListener('click', UI.modal.close);
@ -778,7 +773,7 @@ window.Page_forum = (() => {
const isOwn = uid && uid === p.user_id; const isOwn = uid && uid === p.user_id;
const fotoHtml = (p.foto_urls?.length) const fotoHtml = (p.foto_urls?.length)
? `<div class="forum-foto-grid">${p.foto_urls.map(u => ? `<div class="forum-foto-grid">${p.foto_urls.map(u =>
`<img src="${_esc(u)}" class="forum-foto-img" data-src="${_esc(u)}" alt="" loading="lazy">` `<img src="${UI.escape(u)}" class="forum-foto-img" data-src="${UI.escape(u)}" alt="" loading="lazy">`
).join('')}</div>` ).join('')}</div>`
: ''; : '';
@ -788,13 +783,13 @@ window.Page_forum = (() => {
return ` return `
<div class="forum-post" data-post-id="${p.id}" data-user-id="${p.user_id || ''}"> <div class="forum-post" data-post-id="${p.id}" data-user-id="${p.user_id || ''}">
<div class="forum-post-header"> <div class="forum-post-header">
<div class="forum-avatar forum-avatar--sm">${_esc(_initial(p.autor_name))}</div> <div class="forum-avatar forum-avatar--sm">${UI.escape(_initial(p.autor_name))}</div>
<span class="forum-post-author">${_esc(p.autor_name || 'Unbekannt')}</span> <span class="forum-post-author">${UI.escape(p.autor_name || 'Unbekannt')}</span>
${p.autor_founder_number ? `<span style="font-size:10px;font-weight:700;background:#7c3aed;color:#fff;padding:1px 5px;border-radius:4px">Gründer #${p.autor_founder_number}</span>` : ''} ${p.autor_founder_number ? `<span style="font-size:10px;font-weight:700;background:#7c3aed;color:#fff;padding:1px 5px;border-radius:4px">Gründer #${p.autor_founder_number}</span>` : ''}
<span class="forum-post-date">${_fmtDate(p.created_at)}</span> <span class="forum-post-date">${_fmtDate(p.created_at)}</span>
</div> </div>
<div class="forum-post-body"> <div class="forum-post-body">
<div class="forum-post-text">${_esc(p.text)}</div> <div class="forum-post-text">${UI.escape(p.text)}</div>
${fotoHtml} ${fotoHtml}
</div> </div>
<div class="forum-post-actions"> <div class="forum-post-actions">
@ -803,7 +798,7 @@ window.Page_forum = (() => {
</button> </button>
${(!isOwn && uid) ? `<button class="forum-icon-btn forum-post-report" data-post-id="${p.id}" title="Melden">${UI.icon('flag')}</button>` : ''} ${(!isOwn && uid) ? `<button class="forum-icon-btn forum-post-report" data-post-id="${p.id}" title="Melden">${UI.icon('flag')}</button>` : ''}
<div style="margin-left:auto;display:flex;gap:4px"> <div style="margin-left:auto;display:flex;gap:4px">
${isOwn ? `<button class="forum-icon-btn forum-post-edit" data-post-id="${p.id}" data-text="${_esc(p.text || '')}" title="Bearbeiten">${UI.icon('pencil-simple')}</button>` : ''} ${isOwn ? `<button class="forum-icon-btn forum-post-edit" data-post-id="${p.id}" data-text="${UI.escape(p.text || '')}" title="Bearbeiten">${UI.icon('pencil-simple')}</button>` : ''}
${canDelete ? `<button class="forum-icon-btn forum-icon-btn--danger forum-post-delete" data-post-id="${p.id}" title="Löschen">${UI.icon('trash')}</button>` : ''} ${canDelete ? `<button class="forum-icon-btn forum-icon-btn--danger forum-post-delete" data-post-id="${p.id}" title="Löschen">${UI.icon('trash')}</button>` : ''}
</div> </div>
</div> </div>
@ -991,7 +986,7 @@ window.Page_forum = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
function _showCreateForm() { function _showCreateForm() {
const katOptions = KATEGORIEN.filter(k => k.key !== 'alle').map(k => const katOptions = KATEGORIEN.filter(k => k.key !== 'alle').map(k =>
`<option value="${k.key}" ${k.key === _aktivKat ? 'selected' : ''}>${_esc(k.label)}</option>` `<option value="${k.key}" ${k.key === _aktivKat ? 'selected' : ''}>${UI.escape(k.label)}</option>`
).join(''); ).join('');
const body = ` const body = `
@ -1241,12 +1236,12 @@ window.Page_forum = (() => {
background:var(--c-primary);color:#fff;font-size:13px;font-weight:700; background:var(--c-primary);color:#fff;font-size:13px;font-weight:700;
display:flex;align-items:center;justify-content:center; display:flex;align-items:center;justify-content:center;
box-shadow:0 2px 5px rgba(0,0,0,0.35); box-shadow:0 2px 5px rgba(0,0,0,0.35);
border:2px solid rgba(255,255,255,0.8)">${_esc((m.vorname||'?')[0].toUpperCase())}</div>`, border:2px solid rgba(255,255,255,0.8)">${UI.escape((m.vorname||'?')[0].toUpperCase())}</div>`,
iconSize: [32, 32], iconAnchor: [16, 16], iconSize: [32, 32], iconAnchor: [16, 16],
}); });
_clusterGroup.addLayer( _clusterGroup.addLayer(
L.marker([m.lat, m.lon], { icon }) L.marker([m.lat, m.lon], { icon })
.bindPopup(`<strong>${_esc(m.vorname || '?')}</strong>`) .bindPopup(`<strong>${UI.escape(m.vorname || '?')}</strong>`)
); );
}); });
_map.addLayer(_clusterGroup); _map.addLayer(_clusterGroup);
@ -1296,11 +1291,11 @@ window.Page_forum = (() => {
${reports.map(r => ` ${reports.map(r => `
<div class="forum-mod-report-item" data-id="${r.id}"> <div class="forum-mod-report-item" data-id="${r.id}">
<div class="text-sm"> <div class="text-sm">
<strong>${_esc(r.target_type)} #${r.target_id}</strong> <strong>${UI.escape(r.target_type)} #${r.target_id}</strong>
${_esc(r.grund)} ${UI.escape(r.grund)}
</div> </div>
<div class="text-xs-muted"> <div class="text-xs-muted">
von ${_esc(r.melder_name || '?')} · ${_fmtDate(r.created_at)} von ${UI.escape(r.melder_name || '?')} · ${_fmtDate(r.created_at)}
</div> </div>
<button class="btn btn-sm btn-secondary forum-resolve-btn" data-id="${r.id}" class="mt-2"> <button class="btn btn-sm btn-secondary forum-resolve-btn" data-id="${r.id}" class="mt-2">
${UI.icon('check')} Erledigt ${UI.icon('check')} Erledigt
@ -1334,7 +1329,7 @@ window.Page_forum = (() => {
title: 'Antwort bearbeiten', title: 'Antwort bearbeiten',
body: `<form id="${id}"> body: `<form id="${id}">
<div class="form-group"> <div class="form-group">
<textarea class="form-control" name="text" rows="5" required>${_esc(currentText)}</textarea> <textarea class="form-control" name="text" rows="5" required>${UI.escape(currentText)}</textarea>
</div> </div>
</form>`, </form>`,
footer: ` footer: `
@ -1373,11 +1368,11 @@ window.Page_forum = (() => {
body: `<form id="${id}"> body: `<form id="${id}">
<div class="form-group"> <div class="form-group">
<label class="form-label">Titel</label> <label class="form-label">Titel</label>
<input class="form-control" name="titel" value="${_esc(thread.titel || '')}" required> <input class="form-control" name="titel" value="${UI.escape(thread.titel || '')}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Text</label> <label class="form-label">Text</label>
<textarea class="form-control" name="text" rows="5">${_esc(thread.text || '')}</textarea> <textarea class="form-control" name="text" rows="5">${UI.escape(thread.text || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Standort <span class="text-secondary">(optional)</span></label> <label class="form-label">Standort <span class="text-secondary">(optional)</span></label>

View file

@ -61,7 +61,7 @@ window.Page_friends = (() => {
background:var(--c-surface-2);border-radius:var(--radius-md); background:var(--c-surface-2);border-radius:var(--radius-md);
font-size:var(--text-xs);color:var(--c-text-secondary); font-size:var(--text-xs);color:var(--c-text-secondary);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
banyaro.app/#friends?suche=${_esc(encodeURIComponent(myName))} banyaro.app/#friends?suche=${UI.escape(encodeURIComponent(myName))}
</div> </div>
<button class="btn btn-ghost btn-sm" id="fr-copy-btn" title="Link kopieren"> <button class="btn btn-ghost btn-sm" id="fr-copy-btn" title="Link kopieren">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#link"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#link"></use></svg>
@ -82,7 +82,7 @@ window.Page_friends = (() => {
</svg> </svg>
<input id="fr-search" type="search" autocomplete="off" <input id="fr-search" type="search" autocomplete="off"
placeholder="Namen eines Hundebesitzers suchen…" placeholder="Namen eines Hundebesitzers suchen…"
value="${_esc(prefill || '')}" value="${UI.escape(prefill || '')}"
style="width:100%;box-sizing:border-box; style="width:100%;box-sizing:border-box;
padding:var(--space-3) var(--space-3) var(--space-3) 2.5rem; padding:var(--space-3) var(--space-3) var(--space-3) 2.5rem;
border:1.5px solid var(--c-border);border-radius:var(--radius-lg); border:1.5px solid var(--c-border);border-radius:var(--radius-lg);
@ -278,19 +278,19 @@ window.Page_friends = (() => {
const text = item.text || ''; const text = item.text || '';
const page = _ACTIVITY_PAGE[item.type] || ''; const page = _ACTIVITY_PAGE[item.type] || '';
const dogLabel = item.dog_name const dogLabel = item.dog_name
? `<span class="fr-activity-dog">${_esc(item.dog_name)}</span>` ? `<span class="fr-activity-dog">${UI.escape(item.dog_name)}</span>`
: ''; : '';
const avatar = item.dog_foto const avatar = item.dog_foto
? `<img src="${_esc(item.dog_foto)}" alt="${_esc(item.dog_name || '')}" ? `<img src="${UI.escape(item.dog_foto)}" alt="${UI.escape(item.dog_name || '')}"
loading="lazy" decoding="async" onerror="this.style.display='none'" loading="lazy" decoding="async" onerror="this.style.display='none'"
class="fr-activity-avatar">` class="fr-activity-avatar">`
: item.avatar_url : item.avatar_url
? `<img src="${_esc(item.avatar_url)}" alt="${_esc(item.user_name)}" ? `<img src="${UI.escape(item.avatar_url)}" alt="${UI.escape(item.user_name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'" loading="lazy" decoding="async" onerror="this.style.display='none'"
class="fr-activity-avatar">` class="fr-activity-avatar">`
: `<div class="fr-activity-avatar fr-activity-avatar--initial"> : `<div class="fr-activity-avatar fr-activity-avatar--initial">
${_esc((item.user_name || '?')[0].toUpperCase())} ${UI.escape((item.user_name || '?')[0].toUpperCase())}
</div>`; </div>`;
const tag = page ? `button type="button"` : `div`; const tag = page ? `button type="button"` : `div`;
@ -303,17 +303,17 @@ window.Page_friends = (() => {
${avatar} ${avatar}
<div class="fr-activity-icon-badge"> <div class="fr-activity-icon-badge">
<svg class="ph-icon" style="width:10px;height:10px" aria-hidden="true"> <svg class="ph-icon" style="width:10px;height:10px" aria-hidden="true">
<use href="/icons/phosphor.svg#${_esc(item.icon)}"></use> <use href="/icons/phosphor.svg#${UI.escape(item.icon)}"></use>
</svg> </svg>
</div> </div>
</div> </div>
<div class="fr-activity-body"> <div class="fr-activity-body">
<div class="fr-activity-meta"> <div class="fr-activity-meta">
<span class="fr-activity-user">${_esc(item.user_name)}</span> <span class="fr-activity-user">${UI.escape(item.user_name)}</span>
${dogLabel} ${dogLabel}
</div> </div>
${text ? `<div class="fr-activity-text">${_esc(text)}</div>` : ''} ${text ? `<div class="fr-activity-text">${UI.escape(text)}</div>` : ''}
<div class="fr-activity-time">${_esc(ago)}</div> <div class="fr-activity-time">${UI.escape(ago)}</div>
</div> </div>
</${page ? 'button' : 'div'}> </${page ? 'button' : 'div'}>
`; `;
@ -353,7 +353,7 @@ window.Page_friends = (() => {
<div style="flex:1;min-width:120px"> <div style="flex:1;min-width:120px">
<div style="font-weight:var(--weight-semibold);color:var(--c-text); <div style="font-weight:var(--weight-semibold);color:var(--c-text);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
${_esc(r.requester_name)} ${UI.escape(r.requester_name)}
</div> </div>
${_dogPills(r.dogs, 2)} ${_dogPills(r.dogs, 2)}
</div> </div>
@ -392,11 +392,11 @@ window.Page_friends = (() => {
display:flex;align-items:center;justify-content:center; display:flex;align-items:center;justify-content:center;
font-weight:var(--weight-bold);color:var(--c-text-secondary); font-weight:var(--weight-bold);color:var(--c-text-secondary);
font-size:var(--text-sm);flex-shrink:0"> font-size:var(--text-sm);flex-shrink:0">
${_esc((r.addressee_name || '?')[0].toUpperCase())} ${UI.escape((r.addressee_name || '?')[0].toUpperCase())}
</div> </div>
<div class="flex-1"> <div class="flex-1">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)">${_esc(r.addressee_name)}</div> color:var(--c-text)">${UI.escape(r.addressee_name)}</div>
<div class="text-xs-muted">Anfrage ausstehend</div> <div class="text-xs-muted">Anfrage ausstehend</div>
</div> </div>
<button class="btn btn-ghost btn-sm" <button class="btn btn-ghost btn-sm"
@ -473,9 +473,9 @@ window.Page_friends = (() => {
<div class="card fr-card" style="padding:var(--space-4);margin-bottom:var(--space-3);cursor:pointer; <div class="card fr-card" style="padding:var(--space-4);margin-bottom:var(--space-3);cursor:pointer;
transition:box-shadow 0.15s" transition:box-shadow 0.15s"
data-friend-id="${f.friend_id}" data-friend-id="${f.friend_id}"
data-friend-name="${_esc(f.friend_name)}" data-friend-name="${UI.escape(f.friend_name)}"
data-dogs="${_esc(JSON.stringify(dogs))}" data-dogs="${UI.escape(JSON.stringify(dogs))}"
data-profile="${_esc(JSON.stringify(profile))}"> data-profile="${UI.escape(JSON.stringify(profile))}">
<div style="display:flex;align-items:center;gap:var(--space-3)"> <div style="display:flex;align-items:center;gap:var(--space-3)">
<!-- Avatar (User-Avatar, erstes Hunde-Foto oder Initiale) --> <!-- Avatar (User-Avatar, erstes Hunde-Foto oder Initiale) -->
@ -486,7 +486,7 @@ window.Page_friends = (() => {
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:2px; <div style="display:flex;align-items:center;flex-wrap:wrap;gap:2px;
margin-bottom:var(--space-1)"> margin-bottom:var(--space-1)">
<span style="font-weight:var(--weight-semibold);color:var(--c-text)"> <span style="font-weight:var(--weight-semibold);color:var(--c-text)">
${_esc(f.friend_name)} ${UI.escape(f.friend_name)}
</span> </span>
${_erfahrungSpan(f.erfahrung)} ${_erfahrungSpan(f.erfahrung)}
</div> </div>
@ -506,7 +506,7 @@ window.Page_friends = (() => {
<div style="display:flex;gap:var(--space-1);flex-shrink:0"> <div style="display:flex;gap:var(--space-1);flex-shrink:0">
<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="${_esc(f.friend_name)}" data-fr-note-name="${UI.escape(f.friend_name)}"
title="Notiz" title="Notiz"
onclick="event.stopPropagation()"> 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>
@ -539,13 +539,13 @@ window.Page_friends = (() => {
padding-top:var(--space-3);border-top:1px solid var(--c-border)"> padding-top:var(--space-3);border-top:1px solid var(--c-border)">
${withPhotos.slice(0, 4).map(d => ` ${withPhotos.slice(0, 4).map(d => `
<div class="text-center"> <div class="text-center">
<img src="${_esc(d.foto_url)}" alt="${_esc(d.name)}" <img src="${UI.escape(d.foto_url)}" alt="${UI.escape(d.name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'" loading="lazy" decoding="async" onerror="this.style.display='none'"
style="width:44px;height:44px;border-radius:50%;object-fit:cover; style="width:44px;height:44px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-surface)"> border:2px solid var(--c-surface)">
<div style="font-size:10px;color:var(--c-text-muted);margin-top:2px; <div style="font-size:10px;color:var(--c-text-muted);margin-top:2px;
max-width:44px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> max-width:44px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
${_esc(d.name)} ${UI.escape(d.name)}
</div> </div>
</div> </div>
`).join('')} `).join('')}
@ -563,7 +563,7 @@ window.Page_friends = (() => {
${dogs.map(d => ` ${dogs.map(d => `
<div class="text-center"> <div class="text-center">
${d.foto_url ${d.foto_url
? `<img src="${_esc(d.foto_url)}" alt="${_esc(d.name)}" ? `<img src="${UI.escape(d.foto_url)}" alt="${UI.escape(d.name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'" loading="lazy" decoding="async" onerror="this.style.display='none'"
style="width:72px;height:72px;border-radius:50%;object-fit:cover; style="width:72px;height:72px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-primary);margin-bottom:var(--space-2)">` border:2px solid var(--c-primary);margin-bottom:var(--space-2)">`
@ -573,9 +573,9 @@ window.Page_friends = (() => {
font-size:1.75rem;margin:0 auto var(--space-2)">🐕</div>` font-size:1.75rem;margin:0 auto var(--space-2)">🐕</div>`
} }
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)">${_esc(d.name)}</div> color:var(--c-text)">${UI.escape(d.name)}</div>
${d.rasse ${d.rasse
? `<div class="text-xs-secondary">${_esc(d.rasse)}</div>` ? `<div class="text-xs-secondary">${UI.escape(d.rasse)}</div>`
: ''} : ''}
</div> </div>
`).join('')} `).join('')}
@ -589,7 +589,7 @@ window.Page_friends = (() => {
if (profile.wohnort) { if (profile.wohnort) {
parts.push(`<div style="display:flex;align-items:center;gap:var(--space-2); parts.push(`<div style="display:flex;align-items:center;gap:var(--space-2);
font-size:var(--text-sm);color:var(--c-text-secondary)"> font-size:var(--text-sm);color:var(--c-text-secondary)">
📍 ${_esc(profile.wohnort)} 📍 ${UI.escape(profile.wohnort)}
</div>`); </div>`);
} }
if (profile.erfahrung && _erfahrungBadge[profile.erfahrung]) { if (profile.erfahrung && _erfahrungBadge[profile.erfahrung]) {
@ -600,13 +600,13 @@ window.Page_friends = (() => {
if (profile.bio && profile.profil_sichtbarkeit !== 'private') { if (profile.bio && profile.profil_sichtbarkeit !== 'private') {
parts.push(`<div style="font-size:var(--text-sm);color:var(--c-text); parts.push(`<div style="font-size:var(--text-sm);color:var(--c-text);
line-height:1.5;padding-top:var(--space-2)"> line-height:1.5;padding-top:var(--space-2)">
${_esc(profile.bio)} ${UI.escape(profile.bio)}
</div>`); </div>`);
} }
if (profile.social_link) { if (profile.social_link) {
parts.push(`<div style="font-size:var(--text-xs);word-break:break-all"> parts.push(`<div style="font-size:var(--text-xs);word-break:break-all">
<a href="${_esc(profile.social_link)}" target="_blank" rel="noopener noreferrer" <a href="${UI.escape(profile.social_link)}" target="_blank" rel="noopener noreferrer"
class="text-primary">${_esc(profile.social_link)}</a> class="text-primary">${UI.escape(profile.social_link)}</a>
</div>`); </div>`);
} }
if (!parts.length) return ''; if (!parts.length) return '';
@ -627,7 +627,7 @@ window.Page_friends = (() => {
</div>` : ''; </div>` : '';
UI.modal.open({ UI.modal.open({
title: _esc(friendName), title: UI.escape(friendName),
body: ` body: `
<div> <div>
${badgesHTML} ${badgesHTML}
@ -687,7 +687,7 @@ window.Page_friends = (() => {
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px; <div style="display:flex;align-items:center;flex-wrap:wrap;gap:4px;
margin-bottom:2px"> margin-bottom:2px">
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)">${_esc(u.name)}</span> color:var(--c-text)">${UI.escape(u.name)}</span>
${u.is_founder ? `<span style="font-size:10px;font-weight:700;background:#7c3aed;color:#fff;padding:1px 5px;border-radius:4px">${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'}</span>` : ''} ${u.is_founder ? `<span style="font-size:10px;font-weight:700;background:#7c3aed;color:#fff;padding:1px 5px;border-radius:4px">${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'}</span>` : ''}
${u.is_partner ? `<span style="font-size:10px;font-weight:700;background:#0ea5e9;color:#fff;padding:1px 5px;border-radius:4px">Partner</span>` : ''} ${u.is_partner ? `<span style="font-size:10px;font-weight:700;background:#0ea5e9;color:#fff;padding:1px 5px;border-radius:4px">Partner</span>` : ''}
${_erfahrungSpan(u.erfahrung)} ${_erfahrungSpan(u.erfahrung)}
@ -697,12 +697,12 @@ window.Page_friends = (() => {
${u.dogs?.length ${u.dogs?.length
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary); ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-top:2px"> margin-top:2px">
${u.dogs.map(d => _esc(d.name) + (d.rasse ? ` · ${_esc(d.rasse)}` : '')).join(' &nbsp;|&nbsp; ')} ${u.dogs.map(d => UI.escape(d.name) + (d.rasse ? ` · ${UI.escape(d.rasse)}` : '')).join(' &nbsp;|&nbsp; ')}
</div>` </div>`
: ''} : ''}
</div> </div>
<button class="btn btn-primary btn-sm fr-add-btn" title="Anfrage senden" <button class="btn btn-primary btn-sm fr-add-btn" title="Anfrage senden"
data-user-id="${u.id}" data-user-name="${_esc(u.name)}"> data-user-id="${u.id}" data-user-name="${UI.escape(u.name)}">
<svg class="ph-icon"><use href="/icons/phosphor.svg#user-plus"></use></svg> <svg class="ph-icon"><use href="/icons/phosphor.svg#user-plus"></use></svg>
</button> </button>
</div> </div>
@ -786,13 +786,13 @@ window.Page_friends = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
function _userAvatar(name, firstDog, avatarUrl) { function _userAvatar(name, firstDog, avatarUrl) {
if (avatarUrl) { if (avatarUrl) {
return `<img src="${_esc(avatarUrl)}" alt="${_esc(name)}" return `<img src="${UI.escape(avatarUrl)}" alt="${UI.escape(name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'" loading="lazy" decoding="async" onerror="this.style.display='none'"
style="width:44px;height:44px;border-radius:50%;object-fit:cover; style="width:44px;height:44px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-primary);flex-shrink:0">`; border:2px solid var(--c-primary);flex-shrink:0">`;
} }
if (firstDog?.foto_url) { if (firstDog?.foto_url) {
return `<img src="${_esc(firstDog.foto_url)}" alt="${_esc(firstDog.name)}" return `<img src="${UI.escape(firstDog.foto_url)}" alt="${UI.escape(firstDog.name)}"
loading="lazy" decoding="async" onerror="this.style.display='none'" loading="lazy" decoding="async" onerror="this.style.display='none'"
style="width:44px;height:44px;border-radius:50%;object-fit:cover; style="width:44px;height:44px;border-radius:50%;object-fit:cover;
border:2px solid var(--c-primary);flex-shrink:0">`; border:2px solid var(--c-primary);flex-shrink:0">`;
@ -803,7 +803,7 @@ window.Page_friends = (() => {
border:2px solid var(--c-primary); border:2px solid var(--c-primary);
display:flex;align-items:center;justify-content:center; display:flex;align-items:center;justify-content:center;
font-weight:var(--weight-bold);color:var(--c-primary)"> font-weight:var(--weight-bold);color:var(--c-primary)">
${_esc((name || '?')[0].toUpperCase())} ${UI.escape((name || '?')[0].toUpperCase())}
</div>`; </div>`;
} }
@ -823,7 +823,7 @@ window.Page_friends = (() => {
function _wohnortLine(wohnort) { function _wohnortLine(wohnort) {
if (!wohnort) return ''; if (!wohnort) return '';
return `<span class="text-xs-muted">📍 ${_esc(wohnort)}</span>`; return `<span class="text-xs-muted">📍 ${UI.escape(wohnort)}</span>`;
} }
function _bioLine(bio, sichtbarkeit) { function _bioLine(bio, sichtbarkeit) {
@ -832,7 +832,7 @@ window.Page_friends = (() => {
return `<div style="font-size:var(--text-xs);color:var(--c-text-secondary); return `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-top:var(--space-1);line-height:1.4; margin-top:var(--space-1);line-height:1.4;
overflow:hidden;display:-webkit-box;-webkit-line-clamp:2; overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;
-webkit-box-orient:vertical">${_esc(text)}</div>`; -webkit-box-orient:vertical">${UI.escape(text)}</div>`;
} }
function _dogPills(dogs, max) { function _dogPills(dogs, max) {
@ -844,7 +844,7 @@ window.Page_friends = (() => {
${visible.map(d => ` ${visible.map(d => `
<span style="font-size:10px;padding:1px 6px;border-radius:var(--radius-full); <span style="font-size:10px;padding:1px 6px;border-radius:var(--radius-full);
background:var(--c-surface-2);color:var(--c-text-secondary)"> background:var(--c-surface-2);color:var(--c-text-secondary)">
🐕 ${_esc(d.name)}${d.rasse ? ` · ${_esc(d.rasse)}` : ''} 🐕 ${UI.escape(d.name)}${d.rasse ? ` · ${UI.escape(d.rasse)}` : ''}
</span> </span>
`).join('')} `).join('')}
${rest > 0 ? `<span style="font-size:10px;color:var(--c-text-muted)">+${rest}</span>` : ''} ${rest > 0 ? `<span style="font-size:10px;color:var(--c-text-muted)">+${rest}</span>` : ''}
@ -852,11 +852,6 @@ window.Page_friends = (() => {
`; `;
} }
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function _emptyState(icon, title, text, cta = '') { function _emptyState(icon, title, text, cta = '') {
return `<div class="empty-state"> return `<div class="empty-state">
<svg class="ph-icon empty-state-icon" aria-hidden="true"> <svg class="ph-icon empty-state-icon" aria-hidden="true">
@ -886,7 +881,7 @@ window.Page_friends = (() => {
display:flex;align-items:center;justify-content:space-between;flex-shrink:0"> display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
<div> <div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div> <div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_esc(parentLabel)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
</div> </div>
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button> <button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
</div> </div>

View file

@ -92,7 +92,7 @@ window.Page_gruender = (() => {
background:${i === 0 ? 'linear-gradient(135deg,#fef9c3,#fef3c7)' : 'var(--c-surface-2)'}"> background:${i === 0 ? 'linear-gradient(135deg,#fef9c3,#fef3c7)' : 'var(--c-surface-2)'}">
<div style="font-size:22px;min-width:32px;text-align:center">${medal}</div> <div style="font-size:22px;min-width:32px;text-align:center">${medal}</div>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:700;font-size:var(--text-sm)">${_esc(p.label)}</div> <div style="font-weight:700;font-size:var(--text-sm)">${UI.escape(p.label)}</div>
<div style="background:var(--c-surface-3,rgba(0,0,0,.08));border-radius:var(--radius-full); <div style="background:var(--c-surface-3,rgba(0,0,0,.08));border-radius:var(--radius-full);
height:6px;margin-top:var(--space-1);overflow:hidden"> height:6px;margin-top:var(--space-1);overflow:hidden">
<div style="background:#7c3aed;width:${barPct}%;height:100%; <div style="background:#7c3aed;width:${barPct}%;height:100%;
@ -120,7 +120,7 @@ window.Page_gruender = (() => {
background:var(--c-surface-2);display:flex;align-items:center;gap:var(--space-2)"> background:var(--c-surface-2);display:flex;align-items:center;gap:var(--space-2)">
<span style="font-size:var(--text-xs);font-weight:800;color:#7c3aed;min-width:28px">#${f.founder_number}</span> <span style="font-size:var(--text-xs);font-weight:800;color:#7c3aed;min-width:28px">#${f.founder_number}</span>
<span style="font-size:var(--text-sm);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> <span style="font-size:var(--text-sm);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
${_esc(f.name)} ${UI.escape(f.name)}
</span> </span>
</div> </div>
`).join('')} `).join('')}
@ -144,10 +144,6 @@ window.Page_gruender = (() => {
`; `;
} }
function _esc(s) {
return String(s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
return { init, refresh, onDogChange }; return { init, refresh, onDogChange };

View file

@ -92,7 +92,7 @@ window.Page_health = (() => {
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#wave-sine"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#wave-sine"></use></svg>
<span class="health-transponder-label">Transponder:</span> <span class="health-transponder-label">Transponder:</span>
<span class="health-transponder-nr" id="health-transponder-nr"> <span class="health-transponder-nr" id="health-transponder-nr">
${dog?.chip_nr ? `<strong>${_esc(dog.chip_nr)}</strong>` : '<em class="text-muted">nicht eingetragen</em>'} ${dog?.chip_nr ? `<strong>${UI.escape(dog.chip_nr)}</strong>` : '<em class="text-muted">nicht eingetragen</em>'}
</span> </span>
<button class="btn btn-link btn-sm health-transponder-edit" id="health-transponder-edit" <button class="btn btn-link btn-sm health-transponder-edit" id="health-transponder-edit"
style="padding:0;font-size:var(--text-xs);color:var(--c-text-muted)"> style="padding:0;font-size:var(--text-xs);color:var(--c-text-muted)">
@ -197,7 +197,7 @@ window.Page_health = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-sm);font-weight:var(--weight-medium); <div style="font-size:var(--text-sm);font-weight:var(--weight-medium);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(e.bezeichnung)} ${UI.escape(e.bezeichnung)}
</div> </div>
<div class="text-xs-muted"> <div class="text-xs-muted">
${ageLabel} · ${dateStr} ${ageLabel} · ${dateStr}
@ -379,19 +379,19 @@ window.Page_health = (() => {
<div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry"> <div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry">
<div class="health-card-ampel ampel-${ampel.color}" title="${ampel.label}"></div> <div class="health-card-ampel ampel-${ampel.color}" title="${ampel.label}"></div>
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-title">${_esc(e.bezeichnung)}</div> <div class="list-item-title">${UI.escape(e.bezeichnung)}</div>
<div class="list-item-meta-row"> <div class="list-item-meta-row">
${UI.time.format(e.datum + 'T00:00:00')} ${UI.time.format(e.datum + 'T00:00:00')}
${e.charge_nr ? ` · Ch.-Nr: ${_esc(e.charge_nr)}` : ''} ${e.charge_nr ? ` · Ch.-Nr: ${UI.escape(e.charge_nr)}` : ''}
</div> </div>
${vetName ? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-1)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(vetName)}</div>` : ''} ${vetName ? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-1)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${UI.escape(vetName)}</div>` : ''}
${e.naechstes ? `<div class="health-card-next ampel-text-${ampel.color}"> ${e.naechstes ? `<div class="health-card-next ampel-text-${ampel.color}">
Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon} Nächste Impfung: ${UI.time.format(e.naechstes + 'T00:00:00')} ${ampel.icon}
</div>` : ''} </div>` : ''}
${e.notiz ? `<div class="list-item-text">${_esc(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="${_esc(e.bezeichnung)}" 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> 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>
@ -426,7 +426,7 @@ window.Page_health = (() => {
return ` return `
<div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry"> <div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry">
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-title">${_esc(e.bezeichnung)}</div> <div class="list-item-title">${UI.escape(e.bezeichnung)}</div>
<div class="list-item-meta-row"> <div class="list-item-meta-row">
${UI.time.format(e.datum + 'T00:00:00')} ${UI.time.format(e.datum + 'T00:00:00')}
${e.kosten != null ? ` · ${Number(e.kosten).toFixed(2)}` : ''} ${e.kosten != null ? ` · ${Number(e.kosten).toFixed(2)}` : ''}
@ -434,13 +434,13 @@ window.Page_health = (() => {
${praxisName ? ` ${praxisName ? `
<div style="display:flex;align-items:center;gap:var(--space-1); <div style="display:flex;align-items:center;gap:var(--space-1);
margin-top:var(--space-1);font-size:var(--text-sm);color:var(--c-text-secondary)"> margin-top:var(--space-1);font-size:var(--text-sm);color:var(--c-text-secondary)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(praxisName)}${praxisOrt ? ` · ${_esc(praxisOrt)}` : ''} <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${UI.escape(praxisName)}${praxisOrt ? ` · ${UI.escape(praxisOrt)}` : ''}
</div>` : ''} </div>` : ''}
${e.diagnose ? `<div class="list-item-text"><b>Diagnose:</b> ${_esc(e.diagnose)}</div>` : ''} ${e.diagnose ? `<div class="list-item-text"><b>Diagnose:</b> ${UI.escape(e.diagnose)}</div>` : ''}
${e.notiz ? `<div class="list-item-text">${_esc(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="${_esc(e.bezeichnung)}" 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> 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>
@ -489,10 +489,10 @@ window.Page_health = (() => {
${e.wert} <span class="text-sm-secondary">${e.einheit || 'kg'}</span> ${e.wert} <span class="text-sm-secondary">${e.einheit || 'kg'}</span>
</span> </span>
</div> </div>
${e.notiz ? `<div class="list-item-text" style="padding-top:var(--space-1)">${_esc(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 ${_esc(e.datum)}" 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> 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>
@ -727,10 +727,10 @@ window.Page_health = (() => {
${e.wert ? `Dauer: ${e.wert} Tage` : 'Dauer nicht angegeben'} ${e.wert ? `Dauer: ${e.wert} Tage` : 'Dauer nicht angegeben'}
${interval ? ` · Abstand zur Vorherigen: ${interval} Tage` : ''} ${interval ? ` · Abstand zur Vorherigen: ${interval} Tage` : ''}
</div> </div>
${e.notiz ? `<div class="list-item-text">${_esc(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 ${_esc(e.datum)}" 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> 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>`;
@ -759,16 +759,16 @@ window.Page_health = (() => {
${items.map(e => ` ${items.map(e => `
<div class="list-item-card list-item-card--clickable health-card${e.aktiv ? '' : ' list-item-card--inactive health-card--inactive'}" data-id="${e.id}" data-action="open-entry"> <div class="list-item-card list-item-card--clickable health-card${e.aktiv ? '' : ' list-item-card--inactive health-card--inactive'}" data-id="${e.id}" data-action="open-entry">
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-title">${_esc(e.bezeichnung)}</div> <div class="list-item-title">${UI.escape(e.bezeichnung)}</div>
<div class="list-item-meta-row"> <div class="list-item-meta-row">
${e.dosierung ? _esc(e.dosierung) : ''} ${e.dosierung ? UI.escape(e.dosierung) : ''}
${e.haeufigkeit ? ` · ${_esc(e.haeufigkeit)}` : ''} ${e.haeufigkeit ? ` · ${UI.escape(e.haeufigkeit)}` : ''}
${e.bis_datum ? ` · bis ${UI.time.format(e.bis_datum + 'T00:00:00')}` : ''} ${e.bis_datum ? ` · bis ${UI.time.format(e.bis_datum + 'T00:00:00')}` : ''}
</div> </div>
${e.notiz ? `<div class="list-item-text">${_esc(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="${_esc(e.bezeichnung)}" 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> 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>
@ -799,17 +799,17 @@ window.Page_health = (() => {
<div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry"> <div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry">
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-title"> <div class="list-item-title">
${e.schweregrad ? SCHWEREGRAD[e.schweregrad] || '' : ''} ${_esc(e.bezeichnung)} ${e.schweregrad ? SCHWEREGRAD[e.schweregrad] || '' : ''} ${UI.escape(e.bezeichnung)}
</div> </div>
<div class="list-item-meta-row"> <div class="list-item-meta-row">
Erstmals: ${UI.time.format(e.datum + 'T00:00:00')} Erstmals: ${UI.time.format(e.datum + 'T00:00:00')}
${e.schweregrad ? ` · Schweregrad: ${_esc(e.schweregrad)}` : ''} ${e.schweregrad ? ` · Schweregrad: ${UI.escape(e.schweregrad)}` : ''}
</div> </div>
${e.reaktion ? `<div class="list-item-text"><b>Reaktion:</b> ${_esc(e.reaktion)}</div>` : ''} ${e.reaktion ? `<div class="list-item-text"><b>Reaktion:</b> ${UI.escape(e.reaktion)}</div>` : ''}
${e.notiz ? `<div class="list-item-text">${_esc(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="${_esc(e.bezeichnung)}" 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> 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>
@ -840,29 +840,29 @@ window.Page_health = (() => {
return ` return `
<div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry"> <div class="list-item-card list-item-card--clickable health-card" data-id="${e.id}" data-action="open-entry">
${firstImg ${firstImg
? `<img src="${_esc(firstImg.url)}" class="list-item-thumb health-doc-thumb" alt="Vorschau">` ? `<img src="${UI.escape(firstImg.url)}" class="list-item-thumb health-doc-thumb" alt="Vorschau">`
: `<div style="width:48px;height:48px;display:flex;align-items:center;justify-content:center; : `<div style="width:48px;height:48px;display:flex;align-items:center;justify-content:center;
font-size:2rem;flex-shrink:0"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg></div>`} font-size:2rem;flex-shrink:0"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg></div>`}
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-title">${_esc(e.bezeichnung)}</div> <div class="list-item-title">${UI.escape(e.bezeichnung)}</div>
<div class="list-item-meta-row"> <div class="list-item-meta-row">
${UI.time.format(e.datum + 'T00:00:00')} ${UI.time.format(e.datum + 'T00:00:00')}
${count > 1 ? ` · ${count} Dateien` : ''} ${count > 1 ? ` · ${count} Dateien` : ''}
</div> </div>
${e.notiz ? `<div class="list-item-text">${_esc(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="${_esc(e.bezeichnung)}" 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> 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="${_esc(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()"> 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="${_esc(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()"> 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
@ -992,12 +992,12 @@ window.Page_health = (() => {
const mediaHtml = mediaItems.length const mediaHtml = mediaItems.length
? `<div class="health-media-gallery mt-4"> ? `<div class="health-media-gallery mt-4">
${mediaItems.map(m => m.media_type === 'pdf' ${mediaItems.map(m => m.media_type === 'pdf'
? `<a href="${_esc(m.url)}" target="_blank" rel="noopener" ? `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener"
class="btn btn-secondary btn-sm health-media-gallery-pdf"> class="btn btn-secondary btn-sm health-media-gallery-pdf">
<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>`
: `<a href="${_esc(m.url)}" target="_blank" rel="noopener" class="health-media-gallery-img"> : `<a href="${UI.escape(m.url)}" target="_blank" rel="noopener" class="health-media-gallery-img">
<img src="${_esc(m.url)}" alt="Bild" loading="lazy"> <img src="${UI.escape(m.url)}" alt="Bild" loading="lazy">
</a>` </a>`
).join('')} ).join('')}
</div>` </div>`
@ -1015,7 +1015,7 @@ window.Page_health = (() => {
? `${tabInfo.icon} ${entry.wert} ${entry.einheit || 'kg'}` ? `${tabInfo.icon} ${entry.wert} ${entry.einheit || 'kg'}`
: entry.typ === 'laeufigkeit' : entry.typ === 'laeufigkeit'
? `${tabInfo.icon} Läufigkeit ${UI.time.format(entry.datum + 'T00:00:00')}` ? `${tabInfo.icon} Läufigkeit ${UI.time.format(entry.datum + 'T00:00:00')}`
: `${tabInfo.icon} ${_esc(entry.bezeichnung)}`; : `${tabInfo.icon} ${UI.escape(entry.bezeichnung)}`;
UI.modal.open({ title: modalTitle, body }); UI.modal.open({ title: modalTitle, body });
document.getElementById('health-detail-edit')?.addEventListener('click', () => { document.getElementById('health-detail-edit')?.addEventListener('click', () => {
@ -1041,23 +1041,23 @@ window.Page_health = (() => {
const praxis = _praxen.find(p => p.id === e.tierarzt_id); const praxis = _praxen.find(p => p.id === e.tierarzt_id);
if (praxis) { if (praxis) {
const adresse = [praxis.strasse, [praxis.plz, praxis.ort].filter(Boolean).join(' ')].filter(Boolean).join(', '); const adresse = [praxis.strasse, [praxis.plz, praxis.ort].filter(Boolean).join(' ')].filter(Boolean).join(', ');
const tel = praxis.telefon ? ` · <a href="tel:${_esc(praxis.telefon)}">${_esc(praxis.telefon)}</a>` : ''; const tel = praxis.telefon ? ` · <a href="tel:${UI.escape(praxis.telefon)}">${UI.escape(praxis.telefon)}</a>` : '';
const oh = praxis.opening_hours ? `<br><small class="text-secondary">🕐 ${_esc(_fmtOeffnungszeiten(praxis.opening_hours))}</small>` : ''; const oh = praxis.opening_hours ? `<br><small class="text-secondary">🕐 ${UI.escape(_fmtOeffnungszeiten(praxis.opening_hours))}</small>` : '';
rows.push(['Praxis', `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${_esc(praxis.name)}${adresse ? `<br><small class="text-secondary">${_esc(adresse)}${tel}</small>` : tel}${oh}`]); rows.push(['Praxis', `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ${UI.escape(praxis.name)}${adresse ? `<br><small class="text-secondary">${UI.escape(adresse)}${tel}</small>` : tel}${oh}`]);
} }
} else if (e.tierarzt_name) { } else if (e.tierarzt_name) {
rows.push(['Tierarzt', _esc(e.tierarzt_name)]); rows.push(['Tierarzt', UI.escape(e.tierarzt_name)]);
} }
if (e.charge_nr) rows.push(['Charge-Nr.', _esc(e.charge_nr)]); if (e.charge_nr) rows.push(['Charge-Nr.', UI.escape(e.charge_nr)]);
if (e.kosten != null) rows.push(['Kosten', `${Number(e.kosten).toFixed(2)}`]); if (e.kosten != null) rows.push(['Kosten', `${Number(e.kosten).toFixed(2)}`]);
if (e.diagnose) rows.push(['Diagnose', _esc(e.diagnose)]); if (e.diagnose) rows.push(['Diagnose', UI.escape(e.diagnose)]);
if (e.wert) rows.push(['Gewicht', `${e.wert} ${e.einheit || 'kg'}`]); if (e.wert) rows.push(['Gewicht', `${e.wert} ${e.einheit || 'kg'}`]);
if (e.dosierung) rows.push(['Dosierung', _esc(e.dosierung)]); if (e.dosierung) rows.push(['Dosierung', UI.escape(e.dosierung)]);
if (e.haeufigkeit) rows.push(['Häufigkeit', _esc(e.haeufigkeit)]); if (e.haeufigkeit) rows.push(['Häufigkeit', UI.escape(e.haeufigkeit)]);
if (e.bis_datum) rows.push(['Bis', UI.time.format(e.bis_datum + 'T00:00:00')]); if (e.bis_datum) rows.push(['Bis', UI.time.format(e.bis_datum + 'T00:00:00')]);
if (e.schweregrad) rows.push(['Schweregrad',_esc(e.schweregrad)]); if (e.schweregrad) rows.push(['Schweregrad',UI.escape(e.schweregrad)]);
if (e.reaktion) rows.push(['Reaktion', _esc(e.reaktion)]); if (e.reaktion) rows.push(['Reaktion', UI.escape(e.reaktion)]);
if (e.notiz) rows.push(['Notiz', _esc(e.notiz)]); if (e.notiz) rows.push(['Notiz', UI.escape(e.notiz)]);
return `<dl class="health-detail-dl">${ return `<dl class="health-detail-dl">${
rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('') rows.map(([k, v]) => `<dt>${k}</dt><dd>${v}</dd>`).join('')
@ -1077,7 +1077,7 @@ window.Page_health = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Bezeichnung *</label> <label class="form-label">Bezeichnung *</label>
<input class="form-control" type="text" name="bezeichnung" <input class="form-control" type="text" name="bezeichnung"
value="${_esc(entry?.bezeichnung || '')}" required value="${UI.escape(entry?.bezeichnung || '')}" required
placeholder="${_formPlaceholder(t)}"> placeholder="${_formPlaceholder(t)}">
</div>` : ''} </div>` : ''}
<div class="form-group"> <div class="form-group">
@ -1091,7 +1091,7 @@ window.Page_health = (() => {
const notizField = ` const notizField = `
<div class="form-group"> <div class="form-group">
<label class="form-label">Notiz</label> <label class="form-label">Notiz</label>
<textarea class="form-control" name="notiz" rows="3">${_esc(entry?.notiz || '')}</textarea> <textarea class="form-control" name="notiz" rows="3">${UI.escape(entry?.notiz || '')}</textarea>
</div> </div>
`; `;
@ -1110,7 +1110,7 @@ window.Page_health = (() => {
: ''; : '';
return `<div class="health-media-thumb" data-media-id="${m.id || ''}"> return `<div class="health-media-thumb" data-media-id="${m.id || ''}">
${isImg ${isImg
? `<img src="${_esc(m.url)}" alt="Vorschau">` ? `<img src="${UI.escape(m.url)}" alt="Vorschau">`
: `<div class="health-media-thumb-pdf"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg><span>PDF</span></div>`} : `<div class="health-media-thumb-pdf"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg><span>PDF</span></div>`}
${removeBtn} ${removeBtn}
</div>`; </div>`;
@ -1174,7 +1174,7 @@ window.Page_health = (() => {
const thumb = document.createElement('div'); const thumb = document.createElement('div');
thumb.className = 'health-media-thumb health-media-thumb--pending'; thumb.className = 'health-media-thumb health-media-thumb--pending';
if (isPdf) { if (isPdf) {
thumb.innerHTML = `<div class="health-media-thumb-pdf"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg><span>PDF</span></div><small>${_esc(f.name.slice(0, 18))}</small>`; thumb.innerHTML = `<div class="health-media-thumb-pdf"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#file-text"></use></svg><span>PDF</span></div><small>${UI.escape(f.name.slice(0, 18))}</small>`;
} else { } else {
const img = document.createElement('img'); const img = document.createElement('img');
img.src = URL.createObjectURL(f); img.src = URL.createObjectURL(f);
@ -1331,7 +1331,7 @@ window.Page_health = (() => {
<option value=""> optional </option> <option value=""> optional </option>
${aktivePraxen.map(p => ` ${aktivePraxen.map(p => `
<option value="${p.id}" ${entry?.tierarzt_id === p.id ? 'selected' : ''}> <option value="${p.id}" ${entry?.tierarzt_id === p.id ? 'selected' : ''}>
${_esc(p.name)}${p.ort ? ` · ${_esc(p.ort)}` : ''} ${UI.escape(p.name)}${p.ort ? ` · ${UI.escape(p.ort)}` : ''}
</option>`).join('')} </option>`).join('')}
</select> </select>
</div>`; </div>`;
@ -1350,7 +1350,7 @@ window.Page_health = (() => {
${_praxisSelectField(entry)} ${_praxisSelectField(entry)}
<div class="form-group"> <div class="form-group">
<label class="form-label">Chargen-Nr.</label> <label class="form-label">Chargen-Nr.</label>
<input class="form-control" type="text" name="charge_nr" value="${_esc(entry?.charge_nr || '')}"> <input class="form-control" type="text" name="charge_nr" value="${UI.escape(entry?.charge_nr || '')}">
</div> </div>
`; `;
case 'entwurmung': return ` case 'entwurmung': return `
@ -1373,7 +1373,7 @@ window.Page_health = (() => {
${aktivePraxen.map(p => ` ${aktivePraxen.map(p => `
<option value="${p.id}" <option value="${p.id}"
${entry?.tierarzt_id === p.id ? 'selected' : ''}> ${entry?.tierarzt_id === p.id ? 'selected' : ''}>
${_esc(p.name)}${p.ort ? ` · ${_esc(p.ort)}` : ''} ${UI.escape(p.name)}${p.ort ? ` · ${UI.escape(p.ort)}` : ''}
</option>`).join('')} </option>`).join('')}
</select> </select>
</div>` </div>`
@ -1386,13 +1386,13 @@ window.Page_health = (() => {
</div> </div>
<label class="form-label mt-2">Tierarzt / Praxis (Freitext)</label> <label class="form-label mt-2">Tierarzt / Praxis (Freitext)</label>
<input class="form-control" name="tierarzt_name" <input class="form-control" name="tierarzt_name"
value="${_esc(entry?.tierarzt_name || '')}" placeholder="Dr. Muster"> value="${UI.escape(entry?.tierarzt_name || '')}" placeholder="Dr. Muster">
</div>`; </div>`;
return ` return `
${praxisField} ${praxisField}
<div class="form-group"> <div class="form-group">
<label class="form-label">Diagnose</label> <label class="form-label">Diagnose</label>
<textarea class="form-control" name="diagnose" rows="2">${_esc(entry?.diagnose || '')}</textarea> <textarea class="form-control" name="diagnose" rows="2">${UI.escape(entry?.diagnose || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Kosten ()</label> <label class="form-label">Kosten ()</label>
@ -1416,12 +1416,12 @@ window.Page_health = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Dosierung</label> <label class="form-label">Dosierung</label>
<input class="form-control" type="text" name="dosierung" <input class="form-control" type="text" name="dosierung"
value="${_esc(entry?.dosierung || '')}" placeholder="z.B. 1 Tablette"> value="${UI.escape(entry?.dosierung || '')}" placeholder="z.B. 1 Tablette">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Häufigkeit</label> <label class="form-label">Häufigkeit</label>
<input class="form-control" type="text" name="haeufigkeit" <input class="form-control" type="text" name="haeufigkeit"
value="${_esc(entry?.haeufigkeit || '')}" placeholder="z.B. täglich, 2x wöchentlich"> value="${UI.escape(entry?.haeufigkeit || '')}" placeholder="z.B. täglich, 2x wöchentlich">
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
@ -1450,7 +1450,7 @@ window.Page_health = (() => {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Reaktion / Symptome</label> <label class="form-label">Reaktion / Symptome</label>
<textarea class="form-control" name="reaktion" rows="2">${_esc(entry?.reaktion || '')}</textarea> <textarea class="form-control" name="reaktion" rows="2">${UI.escape(entry?.reaktion || '')}</textarea>
</div> </div>
`; `;
case 'laeufigkeit': { case 'laeufigkeit': {
@ -1632,7 +1632,7 @@ window.Page_health = (() => {
<div style="font-size:1.5rem"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${p.ist_notfallpraxis ? 'warning' : 'first-aid'}"></use></svg></div> <div style="font-size:1.5rem"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${p.ist_notfallpraxis ? 'warning' : 'first-aid'}"></use></svg></div>
<div class="list-item-body"> <div class="list-item-body">
<div class="list-item-title"> <div class="list-item-title">
${_esc(p.name)} ${UI.escape(p.name)}
${!p.aktiv ? '<span style="font-size:var(--text-xs);color:var(--c-text-secondary);font-weight:400"> · Ehemalig</span>' : ''} ${!p.aktiv ? '<span style="font-size:var(--text-xs);color:var(--c-text-secondary);font-weight:400"> · Ehemalig</span>' : ''}
</div> </div>
${(p.strasse || p.plz || p.ort) ? ` ${(p.strasse || p.plz || p.ort) ? `
@ -1642,17 +1642,17 @@ window.Page_health = (() => {
${p.opening_hours ? ` ${p.opening_hours ? `
<div class="list-item-meta-row" style="margin-top:var(--space-1)"> <div class="list-item-meta-row" style="margin-top:var(--space-1)">
<svg class="ph-icon" aria-hidden="true" style="font-size:0.9em"><use href="/icons/phosphor.svg#clock"></use></svg> <svg class="ph-icon" aria-hidden="true" style="font-size:0.9em"><use href="/icons/phosphor.svg#clock"></use></svg>
${_esc(_fmtOeffnungszeiten(p.opening_hours))} ${UI.escape(_fmtOeffnungszeiten(p.opening_hours))}
</div>` : ''} </div>` : ''}
${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:${_esc(p.telefon)}" class="btn btn-secondary btn-sm" <a href="tel:${UI.escape(p.telefon)}" class="btn btn-secondary btn-sm"
onclick="event.stopPropagation()"> 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:${_esc(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()"> 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>` : ''}
@ -1749,7 +1749,7 @@ window.Page_health = (() => {
async function _showPraxisDetail(praxis) { async function _showPraxisDetail(praxis) {
// Erst mit Lade-Spinner öffnen, dann Daten laden // Erst mit Lade-Spinner öffnen, dann Daten laden
UI.modal.open({ UI.modal.open({
title: _esc(praxis.name), title: UI.escape(praxis.name),
body: `<div style="text-align:center;padding:var(--space-6)"> body: `<div style="text-align:center;padding:var(--space-6)">
<svg class="ph-icon spin" aria-hidden="true" style="font-size:2rem"> <svg class="ph-icon spin" aria-hidden="true" style="font-size:2rem">
<use href="/icons/phosphor.svg#spinner-gap"></use> <use href="/icons/phosphor.svg#spinner-gap"></use>
@ -1804,7 +1804,7 @@ window.Page_health = (() => {
${k.freundlichkeit ? `<span>Freundlichkeit: ${_renderStarsReadonly(k.freundlichkeit)}</span>` : ''} ${k.freundlichkeit ? `<span>Freundlichkeit: ${_renderStarsReadonly(k.freundlichkeit)}</span>` : ''}
${k.kompetenz ? `<span>Kompetenz: ${_renderStarsReadonly(k.kompetenz)}</span>` : ''} ${k.kompetenz ? `<span>Kompetenz: ${_renderStarsReadonly(k.kompetenz)}</span>` : ''}
</div>` : ''} </div>` : ''}
<p style="margin:0;font-size:var(--text-sm)">${_esc(k.text || '')}</p> <p style="margin:0;font-size:var(--text-sm)">${UI.escape(k.text || '')}</p>
</div>`).join('') </div>`).join('')
: `<p class="text-sm-muted">Noch keine Kommentare.</p>`; : `<p class="text-sm-muted">Noch keine Kommentare.</p>`;
@ -1863,13 +1863,13 @@ window.Page_health = (() => {
<div class="form-group mt-3"> <div class="form-group mt-3">
<label class="form-label">Kommentar <span style="font-weight:400;color:var(--c-text-muted)">(optional, anonym)</span></label> <label class="form-label">Kommentar <span style="font-weight:400;color:var(--c-text-muted)">(optional, anonym)</span></label>
<textarea class="form-control" name="text" maxlength="500" rows="3" <textarea class="form-control" name="text" maxlength="500" rows="3"
placeholder="Deine Erfahrungen mit dieser Praxis…">${_esc(cur.text || '')}</textarea> placeholder="Deine Erfahrungen mit dieser Praxis…">${UI.escape(cur.text || '')}</textarea>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:right">max. 500 Zeichen</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:right">max. 500 Zeichen</div>
</div> </div>
</form>`; </form>`;
UI.modal.open({ UI.modal.open({
title: `${_esc(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" onclick="UI.modal.close()">Abbrechen</button>
@ -1948,41 +1948,41 @@ window.Page_health = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Name der Praxis *</label> <label class="form-label">Name der Praxis *</label>
<input class="form-control" type="text" name="name" <input class="form-control" type="text" name="name"
value="${_esc(praxis?.name || '')}" placeholder="Dr. Muster Tierarztpraxis" required> value="${UI.escape(praxis?.name || '')}" placeholder="Dr. Muster Tierarztpraxis" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Straße &amp; Hausnummer</label> <label class="form-label">Straße &amp; Hausnummer</label>
<input class="form-control" type="text" name="strasse" <input class="form-control" type="text" name="strasse"
value="${_esc(praxis?.strasse || '')}" placeholder="Musterstraße 1"> value="${UI.escape(praxis?.strasse || '')}" placeholder="Musterstraße 1">
</div> </div>
<div style="display:grid;grid-template-columns:120px 1fr;gap:var(--space-3)"> <div style="display:grid;grid-template-columns:120px 1fr;gap:var(--space-3)">
<div class="form-group"> <div class="form-group">
<label class="form-label">PLZ</label> <label class="form-label">PLZ</label>
<input class="form-control" type="text" name="plz" inputmode="numeric" <input class="form-control" type="text" name="plz" inputmode="numeric"
value="${_esc(praxis?.plz || '')}" placeholder="12345"> value="${UI.escape(praxis?.plz || '')}" placeholder="12345">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Ort</label> <label class="form-label">Ort</label>
<input class="form-control" type="text" name="ort" <input class="form-control" type="text" name="ort"
value="${_esc(praxis?.ort || '')}" placeholder="Musterstadt"> value="${UI.escape(praxis?.ort || '')}" placeholder="Musterstadt">
</div> </div>
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">Telefon</label> <label class="form-label">Telefon</label>
<input class="form-control" type="tel" name="telefon" <input class="form-control" type="tel" name="telefon"
value="${_esc(praxis?.telefon || '')}" placeholder="089 123456"> value="${UI.escape(praxis?.telefon || '')}" placeholder="089 123456">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Notfall-Telefon</label> <label class="form-label">Notfall-Telefon</label>
<input class="form-control" type="tel" name="notfall_telefon" <input class="form-control" type="tel" name="notfall_telefon"
value="${_esc(praxis?.notfall_telefon || '')}" placeholder="089 999999"> value="${UI.escape(praxis?.notfall_telefon || '')}" placeholder="089 999999">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">E-Mail</label> <label class="form-label">E-Mail</label>
<input class="form-control" type="email" name="email" <input class="form-control" type="email" name="email"
value="${_esc(praxis?.email || '')}" placeholder="praxis@beispiel.de"> value="${UI.escape(praxis?.email || '')}" placeholder="praxis@beispiel.de">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label"> <label class="form-label">
@ -1994,14 +1994,14 @@ window.Page_health = (() => {
</label> </label>
<input class="form-control" type="text" name="opening_hours" <input class="form-control" type="text" name="opening_hours"
id="praxis-opening-hours" id="praxis-opening-hours"
value="${_esc(praxis?.opening_hours || '')}" value="${UI.escape(praxis?.opening_hours || '')}"
placeholder="z.B. MoFr 08:0018:00 · Sa 09:0013:00"> placeholder="z.B. MoFr 08:0018:00 · Sa 09:0013:00">
<div id="praxis-osm-results" style="display:none;margin-top:var(--space-2)"></div> <div id="praxis-osm-results" style="display:none;margin-top:var(--space-2)"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Notizen</label> <label class="form-label">Notizen</label>
<textarea class="form-control" name="notizen" rows="2" <textarea class="form-control" name="notizen" rows="2"
placeholder="Besonderheiten, interne Hinweise…">${_esc(praxis?.notizen || '')}</textarea> placeholder="Besonderheiten, interne Hinweise…">${UI.escape(praxis?.notizen || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer"> <label class="form-label" style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
@ -2043,20 +2043,20 @@ window.Page_health = (() => {
resultsEl.innerHTML = hits.map(h => ` resultsEl.innerHTML = hits.map(h => `
<div class="health-card mb-2"> <div class="health-card mb-2">
<div style="cursor:pointer;flex:1" <div style="cursor:pointer;flex:1"
data-osm-id="${_esc(h.osm_id)}" data-osm-id="${UI.escape(h.osm_id)}"
data-name="${_esc(h.name)}" data-name="${UI.escape(h.name)}"
data-oh="${_esc(h.opening_hours || '')}" data-oh="${UI.escape(h.opening_hours || '')}"
data-phone="${_esc(h.phone || '')}" data-phone="${UI.escape(h.phone || '')}"
data-action="pick-osm"> data-action="pick-osm">
<div style="font-weight:600">${_esc(h.name)}</div> <div style="font-weight:600">${UI.escape(h.name)}</div>
${h.opening_hours_fmt ? `<div class="text-sm-secondary">${_esc(h.opening_hours_fmt)}</div>` : '<div class="text-sm-muted">Öffnungszeiten unbekannt</div>'} ${h.opening_hours_fmt ? `<div class="text-sm-secondary">${UI.escape(h.opening_hours_fmt)}</div>` : '<div class="text-sm-muted">Öffnungszeiten unbekannt</div>'}
<div class="text-xs-secondary">${h.distanz_km} km entfernt</div> <div class="text-xs-secondary">${h.distanz_km} km entfernt</div>
</div> </div>
<button class="btn btn-secondary btn-sm" style="flex-shrink:0;align-self:flex-start" <button class="btn btn-secondary btn-sm" style="flex-shrink:0;align-self:flex-start"
data-action="korrigieren" data-action="korrigieren"
data-osm-id="${_esc(h.osm_id)}" data-osm-id="${UI.escape(h.osm_id)}"
data-poi-name="${_esc(h.name)}" data-poi-name="${UI.escape(h.name)}"
data-current-oh="${_esc(h.opening_hours || '')}"> data-current-oh="${UI.escape(h.opening_hours || '')}">
</button> </button>
</div> </div>
@ -2199,11 +2199,11 @@ window.Page_health = (() => {
tierarzt_sofort:{ badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Sofort zum Tierarzt!' }, tierarzt_sofort:{ badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Sofort zum Tierarzt!' },
notfall: { badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Notfall — sofort zum Tierarzt!' }, notfall: { badgeClass: 'badge-danger', icon: 'first-aid-kit', label: 'Notfall — sofort zum Tierarzt!' },
}; };
const d = DRINGLICHKEIT[result.dringlichkeit] || { badgeClass: 'badge-primary', icon: 'info', label: _esc(result.dringlichkeit) }; const d = DRINGLICHKEIT[result.dringlichkeit] || { badgeClass: 'badge-primary', icon: 'info', label: UI.escape(result.dringlichkeit) };
const hinweiseHtml = (result.hinweise || []).length const hinweiseHtml = (result.hinweise || []).length
? `<ul style="margin:var(--space-2) 0 0;padding-left:var(--space-5);font-size:var(--text-sm)"> ? `<ul style="margin:var(--space-2) 0 0;padding-left:var(--space-5);font-size:var(--text-sm)">
${result.hinweise.map(h => `<li style="margin-bottom:var(--space-1)">${_esc(h)}</li>`).join('')} ${result.hinweise.map(h => `<li style="margin-bottom:var(--space-1)">${UI.escape(h)}</li>`).join('')}
</ul>` </ul>`
: ''; : '';
@ -2211,7 +2211,7 @@ window.Page_health = (() => {
? `<div style="margin-top:var(--space-3);padding:var(--space-3); ? `<div style="margin-top:var(--space-3);padding:var(--space-3);
background:var(--c-surface-alt,var(--c-surface)); background:var(--c-surface-alt,var(--c-surface));
border-radius:var(--radius-md);font-size:var(--text-sm)"> border-radius:var(--radius-md);font-size:var(--text-sm)">
<strong>Zum Tierarzt wenn:</strong> ${_esc(result.zum_tierarzt_wenn)} <strong>Zum Tierarzt wenn:</strong> ${UI.escape(result.zum_tierarzt_wenn)}
</div>` </div>`
: ''; : '';
@ -2223,7 +2223,7 @@ window.Page_health = (() => {
</span> </span>
</div> </div>
${result.einschaetzung ${result.einschaetzung
? `<p style="font-size:var(--text-sm);line-height:1.6;margin:0">${_esc(result.einschaetzung)}</p>` ? `<p style="font-size:var(--text-sm);line-height:1.6;margin:0">${UI.escape(result.einschaetzung)}</p>`
: ''} : ''}
${hinweiseHtml} ${hinweiseHtml}
${zumTierarztHtml} ${zumTierarztHtml}
@ -2244,7 +2244,7 @@ window.Page_health = (() => {
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Chip-Nummer (15-stellig)</label> <label class="form-label">Chip-Nummer (15-stellig)</label>
<input id="transponder-input" class="form-control" type="text" <input id="transponder-input" class="form-control" type="text"
value="${_esc(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" onclick="UI.modal.close()">Abbrechen</button>
@ -2260,7 +2260,7 @@ window.Page_health = (() => {
UI.modal.close(); UI.modal.close();
const nrEl = _container.querySelector('#health-transponder-nr'); const nrEl = _container.querySelector('#health-transponder-nr');
if (nrEl) nrEl.innerHTML = nr if (nrEl) nrEl.innerHTML = nr
? `<strong>${_esc(nr)}</strong>` ? `<strong>${UI.escape(nr)}</strong>`
: '<em class="text-muted">nicht eingetragen</em>'; : '<em class="text-muted">nicht eingetragen</em>';
} catch (e) { } catch (e) {
UI.setLoading(btn, false); UI.setLoading(btn, false);
@ -2286,8 +2286,8 @@ window.Page_health = (() => {
? new Date(neuester.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) ? new Date(neuester.erstellt_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
: ''; : '';
const preview = neuester.bericht.length > 180 const preview = neuester.bericht.length > 180
? _esc(neuester.bericht.slice(0, 180)) + '&hellip;' ? UI.escape(neuester.bericht.slice(0, 180)) + '&hellip;'
: _esc(neuester.bericht); : UI.escape(neuester.bericht);
el.innerHTML = ` el.innerHTML = `
<div class="health-ki-bericht-banner" style=" <div class="health-ki-bericht-banner" style="
background:var(--c-surface-2,#f7f2eb); background:var(--c-surface-2,#f7f2eb);
@ -2327,7 +2327,7 @@ window.Page_health = (() => {
title: `${UI.icon('star')} KI-Gesundheitsberichte`, title: `${UI.icon('star')} KI-Gesundheitsberichte`,
body: `${nav} body: `${nav}
<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)">${_esc(b.bericht)}</div>`, <div style="white-space:pre-wrap;line-height:1.7;font-size:var(--text-sm)">${UI.escape(b.bericht)}</div>`,
}); });
} }
@ -2370,8 +2370,8 @@ window.Page_health = (() => {
return ` return `
<div class="health-card" style="flex-direction:row;align-items:center;gap:var(--space-3)"> <div class="health-card" style="flex-direction:row;align-items:center;gap:var(--space-3)">
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(v.bezeichnung)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(v.bezeichnung)}</div>
<div class="text-xs-secondary">${_esc(v.label)}${v.praxis_name ? ' · ' + _esc(v.praxis_name) : ''}</div> <div class="text-xs-secondary">${UI.escape(v.label)}${v.praxis_name ? ' · ' + UI.escape(v.praxis_name) : ''}</div>
${badge} ${badge}
</div> </div>
<div style="text-align:right;flex-shrink:0"> <div style="text-align:right;flex-shrink:0">
@ -2380,7 +2380,7 @@ window.Page_health = (() => {
<div class="text-xs-secondary">${v.uhrzeit_vorschlag} Uhr</div> <div class="text-xs-secondary">${v.uhrzeit_vorschlag} Uhr</div>
<button class="btn btn-primary btn-sm" style="margin-top:var(--space-1)" <button class="btn btn-primary btn-sm" style="margin-top:var(--space-1)"
data-action="termin-anlegen" data-action="termin-anlegen"
data-v='${_esc(JSON.stringify(v))}'> data-v='${UI.escape(JSON.stringify(v))}'>
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-plus"></use></svg> In Kalender <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-plus"></use></svg> In Kalender
</button> </button>
</div> </div>
@ -2417,21 +2417,21 @@ window.Page_health = (() => {
<form id="termin-form"> <form id="termin-form">
<div class="form-group"> <div class="form-group">
<label class="form-label">Bezeichnung</label> <label class="form-label">Bezeichnung</label>
<input class="form-control" type="text" name="titel" value="${_esc(titel)}" required> <input class="form-control" type="text" name="titel" value="${UI.escape(titel)}" required>
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">Datum</label> <label class="form-label">Datum</label>
<input class="form-control" type="date" name="datum" value="${_esc(v.datum_vorschlag)}" required> <input class="form-control" type="date" name="datum" value="${UI.escape(v.datum_vorschlag)}" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Uhrzeit</label> <label class="form-label">Uhrzeit</label>
<input class="form-control" type="time" name="uhrzeit" value="${_esc(v.uhrzeit_vorschlag)}"> <input class="form-control" type="time" name="uhrzeit" value="${UI.escape(v.uhrzeit_vorschlag)}">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Notiz</label> <label class="form-label">Notiz</label>
<input class="form-control" type="text" name="beschreibung" value="${_esc(beschreibung)}"> <input class="form-control" type="text" name="beschreibung" value="${UI.escape(beschreibung)}">
</div> </div>
</form> </form>
`, `,
@ -2490,20 +2490,20 @@ window.Page_health = (() => {
</div> </div>
<div class="list-item-body flex-1-min"> <div class="list-item-body flex-1-min">
${vet ? ` ${vet ? `
<div class="list-item-title">${_esc(vet.name)}</div> <div class="list-item-title">${UI.escape(vet.name)}</div>
${adresse ? `<div class="list-item-meta-row">${_esc(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:${_esc(vet.telefon)}" class="btn btn-secondary btn-sm" <a href="tel:${UI.escape(vet.telefon)}" class="btn btn-secondary btn-sm"
onclick="event.stopPropagation()"> onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#phone"></use></svg> ${_esc(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:${_esc(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()"> onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> Notfall: ${_esc(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>` : ''}
` : ` ` : `
@ -2581,17 +2581,17 @@ window.Page_health = (() => {
return ` return `
<div class="list-item-card health-card" style="align-items:flex-start"> <div class="list-item-card health-card" style="align-items:flex-start">
<div style="font-size:1.4rem;flex-shrink:0;color:var(--c-primary)"> <div style="font-size:1.4rem;flex-shrink:0;color:var(--c-primary)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${_esc(icon)}"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${UI.escape(icon)}"></use></svg>
</div> </div>
<div class="list-item-body flex-1-min"> <div class="list-item-body flex-1-min">
<div class="list-item-title">${_esc(doc.titel)}</div> <div class="list-item-title">${UI.escape(doc.titel)}</div>
<div class="list-item-meta-row"> <div class="list-item-meta-row">
${_esc(label)}${datum ? ' · ' + datum : ''} ${UI.escape(label)}${datum ? ' · ' + datum : ''}
${doc.vet_name ? ' · <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ' + _esc(doc.vet_name) : ''} ${doc.vet_name ? ' · <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> ' + UI.escape(doc.vet_name) : ''}
</div> </div>
${doc.beschreibung ? `<div class="list-item-text">${_esc(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="${_esc(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" onclick="event.stopPropagation()">
${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'
@ -2680,7 +2680,7 @@ window.Page_health = (() => {
<select class="form-control" name="vet_id"> <select class="form-control" name="vet_id">
<option value=""> optional </option> <option value=""> optional </option>
${aktivePraxen.map(p => ${aktivePraxen.map(p =>
`<option value="${p.id}">${_esc(p.name)}${p.ort ? ' · ' + _esc(p.ort) : ''}</option>` `<option value="${p.id}">${UI.escape(p.name)}${p.ort ? ' · ' + UI.escape(p.ort) : ''}</option>`
).join('')} ).join('')}
</select> </select>
</div>` : ''} </div>` : ''}
@ -2776,7 +2776,7 @@ window.Page_health = (() => {
else if (res.saved_count !== undefined) UI.toast.info(`${res.saved_count} Bericht(e) in DB`, { duration: 8000 }); else if (res.saved_count !== undefined) UI.toast.info(`${res.saved_count} Bericht(e) in DB`, { duration: 8000 });
UI.modal.open({ UI.modal.open({
title: `${UI.icon('star')} KI-Gesundheitsbericht`, title: `${UI.icon('star')} KI-Gesundheitsbericht`,
body: `<div style="white-space:pre-wrap;line-height:1.7;font-size:var(--text-sm)">${_esc(zusammenfassung)}</div>`, body: `<div style="white-space:pre-wrap;line-height:1.7;font-size:var(--text-sm)">${UI.escape(zusammenfassung)}</div>`,
}); });
// Berichte-Liste nach Generierung frisch laden (Cache-Buster) // Berichte-Liste nach Generierung frisch laden (Cache-Buster)
_loadKiBerichte(_appState.activeDog.id, true); _loadKiBerichte(_appState.activeDog.id, true);
@ -2796,32 +2796,25 @@ window.Page_health = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// HELPER // HELPER
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _showPoiKorrekturModal(osmId, poiName, currentOh) { function _showPoiKorrekturModal(osmId, poiName, currentOh) {
UI.modal.open({ UI.modal.open({
title: 'Öffnungszeiten korrigieren', title: 'Öffnungszeiten korrigieren',
body: ` body: `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)"> <p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">
Korrektur für <strong>${_esc(poiName)}</strong>.<br> Korrektur für <strong>${UI.escape(poiName)}</strong>.<br>
Dein Vorschlag wird von einem Moderator geprüft und dann für alle übernommen. Dein Vorschlag wird von einem Moderator geprüft und dann für alle übernommen.
</p> </p>
<form id="poi-korrektur-form"> <form id="poi-korrektur-form">
<div class="form-group"> <div class="form-group">
<label class="form-label">Aktuelle Angabe</label> <label class="form-label">Aktuelle Angabe</label>
<input class="form-control" type="text" value="${_esc(currentOh)}" disabled <input class="form-control" type="text" value="${UI.escape(currentOh)}" disabled
class="text-muted"> class="text-muted">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Korrekte Öffnungszeiten *</label> <label class="form-label">Korrekte Öffnungszeiten *</label>
<input class="form-control" type="text" name="new_value" required <input class="form-control" type="text" name="new_value" required
placeholder="z.B. MoFr 08:0018:00 · Sa 09:0013:00" placeholder="z.B. MoFr 08:0018:00 · Sa 09:0013:00"
value="${_esc(currentOh)}"> value="${UI.escape(currentOh)}">
</div> </div>
</form> </form>
`, `,
@ -2872,7 +2865,7 @@ window.Page_health = (() => {
display:flex;align-items:center;justify-content:space-between;flex-shrink:0"> display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
<div> <div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div> <div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_esc(parentLabel)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
</div> </div>
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button> <button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
</div> </div>
@ -2956,7 +2949,7 @@ window.Page_health = (() => {
</p> </p>
<div class="form-group"> <div class="form-group">
<textarea id="ki-tierarzt-symptom" class="form-control" rows="4" <textarea id="ki-tierarzt-symptom" class="form-control" rows="4"
placeholder="${_esc(placeholder)}"></textarea> placeholder="${UI.escape(placeholder)}"></textarea>
</div> </div>
<div id="ki-tierarzt-result" class="hidden"></div> <div id="ki-tierarzt-result" class="hidden"></div>
<div style="margin-top:var(--space-3);padding:var(--space-3); <div style="margin-top:var(--space-3);padding:var(--space-3);
@ -3018,7 +3011,7 @@ window.Page_health = (() => {
return; return;
} }
const antwortHtml = _esc(result.antwort) const antwortHtml = UI.escape(result.antwort)
.replace(/\n\n/g, '</p><p style="margin:var(--space-2) 0">') .replace(/\n\n/g, '</p><p style="margin:var(--space-2) 0">')
.replace(/\n/g, '<br>'); .replace(/\n/g, '<br>');
const restHtml = result.limit - result.anfragen_heute > 0 const restHtml = result.limit - result.anfragen_heute > 0
@ -3092,7 +3085,7 @@ window.Page_health = (() => {
<use href="/icons/phosphor.svg#bell-ringing"></use> <use href="/icons/phosphor.svg#bell-ringing"></use>
</svg> </svg>
<div class="flex-1-min"> <div class="flex-1-min">
<span style="font-weight:600;font-size:var(--text-sm);color:var(--c-text)">${_esc(r.bezeichnung)}</span> <span style="font-weight:600;font-size:var(--text-sm);color:var(--c-text)">${UI.escape(r.bezeichnung)}</span>
<span style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-left:var(--space-1)">${TYPE_LABEL[r.typ] || r.typ}</span> <span style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-left:var(--space-1)">${TYPE_LABEL[r.typ] || r.typ}</span>
</div> </div>
<span style="font-size:var(--text-xs);font-weight:600;color:${color};white-space:nowrap">${label}</span> <span style="font-size:var(--text-xs);font-weight:600;color:${color};white-space:nowrap">${label}</span>
@ -3122,8 +3115,8 @@ window.Page_health = (() => {
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)" data-ins-id="${p.id}"> <div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)" data-ins-id="${p.id}">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-2)"> <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-2)">
<div> <div>
<div style="font-weight:700;font-size:var(--text-base)">${_esc(p.anbieter)}</div> <div style="font-weight:700;font-size:var(--text-base)">${UI.escape(p.anbieter)}</div>
${p.police_nr ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">Police: ${_esc(p.police_nr)}</div>` : ''} ${p.police_nr ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">Police: ${UI.escape(p.police_nr)}</div>` : ''}
</div> </div>
<div style="display:flex;gap:var(--space-1)"> <div style="display:flex;gap:var(--space-1)">
<button class="btn btn-ghost btn-sm ins-edit-btn" data-id="${p.id}" style="padding:4px 8px"> <button class="btn btn-ghost btn-sm ins-edit-btn" data-id="${p.id}" style="padding:4px 8px">
@ -3137,8 +3130,8 @@ window.Page_health = (() => {
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);margin-top:var(--space-3);font-size:var(--text-sm)"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);margin-top:var(--space-3);font-size:var(--text-sm)">
<div><span class="text-secondary">Jahresbeitrag</span><br><strong>${_fmtEur(p.jahresbeitrag)}</strong></div> <div><span class="text-secondary">Jahresbeitrag</span><br><strong>${_fmtEur(p.jahresbeitrag)}</strong></div>
<div><span class="text-secondary">Läuft ab</span><br><strong>${_fmtDate(p.ablaufdatum)}</strong></div> <div><span class="text-secondary">Läuft ab</span><br><strong>${_fmtDate(p.ablaufdatum)}</strong></div>
${p.kontakt ? `<div style="grid-column:1/-1"><span class="text-secondary">Kontakt</span><br>${_esc(p.kontakt)}</div>` : ''} ${p.kontakt ? `<div style="grid-column:1/-1"><span class="text-secondary">Kontakt</span><br>${UI.escape(p.kontakt)}</div>` : ''}
${p.notizen ? `<div style="grid-column:1/-1"><span class="text-secondary">Notizen</span><br>${_esc(p.notizen)}</div>` : ''} ${p.notizen ? `<div style="grid-column:1/-1"><span class="text-secondary">Notizen</span><br>${UI.escape(p.notizen)}</div>` : ''}
</div> </div>
</div>`).join('') : ` </div>`).join('') : `
<div style="text-align:center;padding:var(--space-6);color:var(--c-text-muted)"> <div style="text-align:center;padding:var(--space-6);color:var(--c-text-muted)">
@ -3172,10 +3165,10 @@ window.Page_health = (() => {
const id = `ins-form-${Date.now()}`; const id = `ins-form-${Date.now()}`;
const body = `<form id="${id}"> const body = `<form id="${id}">
<div class="by-form-group"><label class="by-label">Anbieter *</label> <div class="by-form-group"><label class="by-label">Anbieter *</label>
<input type="text" name="anbieter" class="form-control by-input" value="${_esc(existing?.anbieter||'')}" required placeholder="z. B. HUK-Coburg"> <input type="text" name="anbieter" class="form-control by-input" value="${UI.escape(existing?.anbieter||'')}" required placeholder="z. B. HUK-Coburg">
</div> </div>
<div class="by-form-group"><label class="by-label">Police-Nr.</label> <div class="by-form-group"><label class="by-label">Police-Nr.</label>
<input type="text" name="police_nr" class="form-control by-input" value="${_esc(existing?.police_nr||'')}" placeholder="optional"> <input type="text" name="police_nr" class="form-control by-input" value="${UI.escape(existing?.police_nr||'')}" placeholder="optional">
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="by-form-group"><label class="by-label">Jahresbeitrag ()</label> <div class="by-form-group"><label class="by-label">Jahresbeitrag ()</label>
@ -3186,10 +3179,10 @@ window.Page_health = (() => {
</div> </div>
</div> </div>
<div class="by-form-group"><label class="by-label">Kontakt / Telefon</label> <div class="by-form-group"><label class="by-label">Kontakt / Telefon</label>
<input type="text" name="kontakt" class="form-control by-input" value="${_esc(existing?.kontakt||'')}" placeholder="optional"> <input type="text" name="kontakt" class="form-control by-input" value="${UI.escape(existing?.kontakt||'')}" placeholder="optional">
</div> </div>
<div class="by-form-group"><label class="by-label">Notizen</label> <div class="by-form-group"><label class="by-label">Notizen</label>
<textarea name="notizen" class="form-control by-input" rows="2">${_esc(existing?.notizen||'')}</textarea> <textarea name="notizen" class="form-control by-input" rows="2">${UI.escape(existing?.notizen||'')}</textarea>
</div> </div>
</form>`; </form>`;
const footer = ` const footer = `
@ -3271,12 +3264,12 @@ window.Page_health = (() => {
<div style="width:3px;border-radius:2px;background:${color};align-self:stretch;flex-shrink:0"></div> <div style="width:3px;border-radius:2px;background:${color};align-self:stretch;flex-shrink:0"></div>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
<span style="font-weight:700;font-size:var(--text-sm);color:${color}">${_esc(katLabel)}</span> <span style="font-weight:700;font-size:var(--text-sm);color:${color}">${UI.escape(katLabel)}</span>
${trigLabel ? `<span style="font-size:var(--text-xs);background:var(--c-surface-2);padding:1px 6px;border-radius:100px;color:var(--c-text-secondary)">${_esc(trigLabel)}</span>` : ''} ${trigLabel ? `<span style="font-size:var(--text-xs);background:var(--c-surface-2);padding:1px 6px;border-radius:100px;color:var(--c-text-secondary)">${UI.escape(trigLabel)}</span>` : ''}
<span style="font-size:var(--text-xs);color:var(--c-text-muted);margin-left:auto">${fmtDate(e.datum)}${e.uhrzeit ? ' ' + e.uhrzeit : ''}</span> <span style="font-size:var(--text-xs);color:var(--c-text-muted);margin-left:auto">${fmtDate(e.datum)}${e.uhrzeit ? ' ' + e.uhrzeit : ''}</span>
</div> </div>
<div style="display:flex;gap:3px;margin-top:4px">${dots}</div> <div style="display:flex;gap:3px;margin-top:4px">${dots}</div>
${e.notiz ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px">${_esc(e.notiz)}</div>` : ''} ${e.notiz ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px">${UI.escape(e.notiz)}</div>` : ''}
</div> </div>
<button class="btn btn-ghost btn-sm beh-del-btn" data-id="${e.id}" style="padding:4px 6px;color:var(--c-danger);flex-shrink:0"> <button class="btn btn-ghost btn-sm beh-del-btn" data-id="${e.id}" style="padding:4px 6px;color:var(--c-danger);flex-shrink:0">
<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>

View file

@ -103,7 +103,7 @@ window.Page_hilfe = (() => {
</p> </p>
<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0"> <p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0">
${_search ${_search
? `Zu "${_esc(_search)}" wurde nichts gefunden.` ? `Zu "${UI.escape(_search)}" wurde nichts gefunden.`
: 'Noch keine FAQ-Artikel vorhanden.'} : 'Noch keine FAQ-Artikel vorhanden.'}
</p> </p>
</div> </div>
@ -136,7 +136,7 @@ window.Page_hilfe = (() => {
color:var(--c-text-secondary);text-transform:uppercase; color:var(--c-text-secondary);text-transform:uppercase;
letter-spacing:0.08em;padding:var(--space-1) 0 var(--space-2); letter-spacing:0.08em;padding:var(--space-1) 0 var(--space-2);
margin-bottom:var(--space-1)"> margin-bottom:var(--space-1)">
${_esc(label)} ${UI.escape(label)}
</div> </div>
<div style="display:flex;flex-direction:column;gap:var(--space-1)"> <div style="display:flex;flex-direction:column;gap:var(--space-1)">
`; `;
@ -148,12 +148,12 @@ window.Page_hilfe = (() => {
// Highlight Suchtreffer in der Frage // Highlight Suchtreffer in der Frage
const frageHtml = _search const frageHtml = _search
? _highlight(a.frage, _search) ? _highlight(a.frage, _search)
: _esc(a.frage); : UI.escape(a.frage);
// Antwort: Zeilenumbrüche in <br> wandeln // Antwort: Zeilenumbrüche in <br> wandeln
const antwortHtml = _search const antwortHtml = _search
? _highlight(a.antwort, _search).replace(/\n/g, '<br>') ? _highlight(a.antwort, _search).replace(/\n/g, '<br>')
: _esc(a.antwort).replace(/\n/g, '<br>'); : UI.escape(a.antwort).replace(/\n/g, '<br>');
// Bei aktiver Suche: Antwort gleich aufgeklappt // Bei aktiver Suche: Antwort gleich aufgeklappt
const openByDefault = !!_search; const openByDefault = !!_search;
@ -222,20 +222,12 @@ window.Page_hilfe = (() => {
} }
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _highlight(text, term) { function _highlight(text, term) {
if (!term) return text; if (!term) return text;
const safe = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const safe = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`(${safe})`, 'gi'); const re = new RegExp(`(${safe})`, 'gi');
return _esc(text).replace(re, return UI.escape(text).replace(re,
'<mark style="background:var(--c-warning-bg,#fef3c7);color:inherit;border-radius:2px">$1</mark>' '<mark style="background:var(--c-warning-bg,#fef3c7);color:inherit;border-radius:2px">$1</mark>'
); );
} }

View file

@ -7,7 +7,6 @@ window.Page_jobs = (() => {
let _container = null; let _container = null;
let _appState = null; let _appState = null;
const _esc = s => UI.escape(s ?? '');
const _ph = (name, size = 22) => const _ph = (name, size = 22) =>
`<svg class="ph-icon" aria-hidden="true" style="width:${size}px;height:${size}px;flex-shrink:0;color:var(--c-primary)"><use href="/icons/phosphor.svg#${name}"></use></svg>`; `<svg class="ph-icon" aria-hidden="true" style="width:${size}px;height:${size}px;flex-shrink:0;color:var(--c-primary)"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
@ -130,7 +129,7 @@ window.Page_jobs = (() => {
</div> </div>
${app.admin_note ? `<div style="margin-top:var(--space-3);background:var(--c-surface-2); ${app.admin_note ? `<div style="margin-top:var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-md);padding:var(--space-3);font-size:var(--text-sm); border-radius:var(--radius-md);padding:var(--space-3);font-size:var(--text-sm);
color:var(--c-text-secondary);text-align:left">${_esc(app.admin_note)}</div>` : ''} color:var(--c-text-secondary);text-align:left">${UI.escape(app.admin_note)}</div>` : ''}
</div>`; </div>`;
} }
@ -147,13 +146,13 @@ window.Page_jobs = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Dein Name *</label> <label class="form-label">Dein Name *</label>
<input class="form-control" type="text" name="name" <input class="form-control" type="text" name="name"
value="${u ? _esc(u.name) : ''}" placeholder="Vorname oder Nickname" required> value="${u ? UI.escape(u.name) : ''}" placeholder="Vorname oder Nickname" required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">E-Mail *</label> <label class="form-label">E-Mail *</label>
<input class="form-control" type="email" name="email" <input class="form-control" type="email" name="email"
value="${u ? _esc(u.email || '') : ''}" placeholder="deine@email.de" required> value="${u ? UI.escape(u.email || '') : ''}" placeholder="deine@email.de" required>
</div> </div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-3)"> <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:var(--space-3)">

View file

@ -135,11 +135,11 @@ window.Page_knigge = (() => {
const cards = BEGEGNUNGEN.map((b, i) => ` const cards = BEGEGNUNGEN.map((b, i) => `
<div class="knigge-accordion" id="acc-${i}"> <div class="knigge-accordion" id="acc-${i}">
<button class="knigge-accordion-head" data-acc="${i}" aria-expanded="false"> <button class="knigge-accordion-head" data-acc="${i}" aria-expanded="false">
<span>${b.icon} <strong>${_esc(b.titel)}</strong></span> <span>${b.icon} <strong>${UI.escape(b.titel)}</strong></span>
<span class="knigge-accordion-arrow">${UI.icon('caret-down')}</span> <span class="knigge-accordion-arrow">${UI.icon('caret-down')}</span>
</button> </button>
<div class="knigge-accordion-body" id="acc-body-${i}" hidden> <div class="knigge-accordion-body" id="acc-body-${i}" hidden>
<p style="color:var(--c-text);line-height:1.6">${_esc(b.tipps)}</p> <p style="color:var(--c-text);line-height:1.6">${UI.escape(b.tipps)}</p>
</div> </div>
</div> </div>
`).join(''); `).join('');
@ -175,14 +175,14 @@ window.Page_knigge = (() => {
const cards = SZENARIEN.map(s => ` const cards = SZENARIEN.map(s => `
<div class="card mb-4" id="sz-${s.id}"> <div class="card mb-4" id="sz-${s.id}">
<p style="font-weight:var(--weight-semibold);margin:0;padding:var(--space-5) var(--space-5) var(--space-3);line-height:1.5"> <p style="font-weight:var(--weight-semibold);margin:0;padding:var(--space-5) var(--space-5) var(--space-3);line-height:1.5">
${_esc(s.frage)} ${UI.escape(s.frage)}
</p> </p>
<div class="knigge-vote-options" id="opts-${s.id}" style="padding:0 var(--space-5) var(--space-5)"> <div class="knigge-vote-options" id="opts-${s.id}" style="padding:0 var(--space-5) var(--space-5)">
${s.antworten.map(a => ` ${s.antworten.map(a => `
<button class="knigge-vote-btn btn btn-secondary" <button class="knigge-vote-btn btn btn-secondary"
data-sz="${s.id}" data-key="${a.key}" data-sz="${s.id}" data-key="${a.key}"
style="width:100%;margin-bottom:var(--space-2);justify-content:flex-start;text-align:left"> style="width:100%;margin-bottom:var(--space-2);justify-content:flex-start;text-align:left">
${_esc(a.text)} ${UI.escape(a.text)}
</button> </button>
`).join('')} `).join('')}
</div> </div>
@ -263,7 +263,7 @@ window.Page_knigge = (() => {
<div class="mb-3"> <div class="mb-3">
<div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:var(--text-sm)"> <div style="display:flex;justify-content:space-between;margin-bottom:4px;font-size:var(--text-sm)">
<span style="color:${isU ? 'var(--c-text)' : 'var(--c-text-secondary)'};font-weight:${isU ? 'var(--weight-semibold)' : 'normal'}"> <span style="color:${isU ? 'var(--c-text)' : 'var(--c-text-secondary)'};font-weight:${isU ? 'var(--weight-semibold)' : 'normal'}">
${isU ? UI.icon('arrow-right') + ' ' : ''}${_esc(a.text)}${isR ? ' ' + UI.icon('check') : ''} ${isU ? UI.icon('arrow-right') + ' ' : ''}${UI.escape(a.text)}${isR ? ' ' + UI.icon('check') : ''}
</span> </span>
<span class="text-secondary">${pct}% (${cnt})</span> <span class="text-secondary">${pct}% (${cnt})</span>
</div> </div>
@ -282,7 +282,7 @@ window.Page_knigge = (() => {
<div style="margin-bottom:var(--space-4);padding:0 var(--space-5)">${bars}</div> <div style="margin-bottom:var(--space-4);padding:0 var(--space-5)">${bars}</div>
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3) var(--space-5);font-size:var(--text-sm);line-height:1.5"> <div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3) var(--space-5);font-size:var(--text-sm);line-height:1.5">
${badge} ${badge}
<span class="text-secondary">${_esc(szenario.erklaerung)}</span> <span class="text-secondary">${UI.escape(szenario.erklaerung)}</span>
</div> </div>
`; `;
} }
@ -336,7 +336,7 @@ window.Page_knigge = (() => {
padding:var(--space-4);line-height:1.6;color:var(--c-text)"> padding:var(--space-4);line-height:1.6;color:var(--c-text)">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);margin-bottom:var(--space-2)">${UI.icon('robot')} KI-Rat</div> color:var(--c-text-secondary);margin-bottom:var(--space-2)">${UI.icon('robot')} KI-Rat</div>
${_esc(data.rat)} ${UI.escape(data.rat)}
</div> </div>
`; `;
result.style.display = 'block'; result.style.display = 'block';
@ -347,7 +347,7 @@ window.Page_knigge = (() => {
padding:var(--space-4);color:var(--c-text-secondary);font-size:var(--text-sm)"> padding:var(--space-4);color:var(--c-text-secondary);font-size:var(--text-sm)">
${is402 ${is402
? 'Für KI-Rat wird Ban Yaro Plus oder ein laufender KI-Server benötigt.' ? 'Für KI-Rat wird Ban Yaro Plus oder ein laufender KI-Server benötigt.'
: _esc(err.message || 'Fehler beim KI-Abruf.')} : UI.escape(err.message || 'Fehler beim KI-Abruf.')}
</div> </div>
`; `;
result.style.display = 'block'; result.style.display = 'block';
@ -400,15 +400,6 @@ window.Page_knigge = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// HELPER // HELPER
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// PUBLIC // PUBLIC
// ---------------------------------------------------------- // ----------------------------------------------------------

View file

@ -19,15 +19,11 @@ window.Page_litters = (() => {
return ` return `
<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(icon)}</div> <div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon(icon)}</div>
<h3 style="margin:0 0 var(--space-2)">${_esc(title)}</h3> <h3 style="margin:0 0 var(--space-2)">${UI.escape(title)}</h3>
<p style="color:var(--c-text-secondary);margin:0">${_esc(text)}</p> <p style="color:var(--c-text-secondary);margin:0">${UI.escape(text)}</p>
</div>`; </div>`;
} }
function _esc(s) {
return UI.escape ? UI.escape(s || '') : (s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function _statusBadge(status) { function _statusBadge(status) {
const map = { const map = {
@ -37,7 +33,7 @@ window.Page_litters = (() => {
abgeschlossen: { label: 'Abgeschlossen', cls: 'badge-muted' }, abgeschlossen: { label: 'Abgeschlossen', cls: 'badge-muted' },
}; };
const s = map[status] || { label: status, cls: 'badge-muted' }; const s = map[status] || { label: status, cls: 'badge-muted' };
return `<span class="badge ${s.cls}">${_esc(s.label)}</span>`; return `<span class="badge ${s.cls}">${UI.escape(s.label)}</span>`;
} }
function _fmtDate(iso) { function _fmtDate(iso) {
@ -59,7 +55,7 @@ window.Page_litters = (() => {
abgegeben: { label: 'Abgegeben', cls: 'badge-muted' }, abgegeben: { label: 'Abgegeben', cls: 'badge-muted' },
}; };
const s = map[status] || { label: status, cls: 'badge-muted' }; const s = map[status] || { label: status, cls: 'badge-muted' };
return `<span class="badge badge-sm ${s.cls}">${_esc(s.label)}</span>`; return `<span class="badge badge-sm ${s.cls}">${UI.escape(s.label)}</span>`;
} }
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -101,7 +97,7 @@ window.Page_litters = (() => {
const zwinger = _breederInfo?.zwingername || 'Mein Zwinger'; const zwinger = _breederInfo?.zwingername || 'Mein Zwinger';
const logoUrl = _breederInfo?.logo_url || null; const logoUrl = _breederInfo?.logo_url || null;
const logoHtml = logoUrl const logoHtml = logoUrl
? `<img src="${_esc(logoUrl)}" alt="Logo" ? `<img src="${UI.escape(logoUrl)}" alt="Logo"
style="width:48px;height:48px;border-radius:50%;object-fit:cover; style="width:48px;height:48px;border-radius:50%;object-fit:cover;
border:2px solid rgba(196,132,58,.5);flex-shrink:0" border:2px solid rgba(196,132,58,.5);flex-shrink:0"
onerror="this.style.display='none'">` onerror="this.style.display='none'">`
@ -121,7 +117,7 @@ window.Page_litters = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<h2 style="margin:0 0 2px;font-size:var(--text-lg);font-weight:700; <h2 style="margin:0 0 2px;font-size:var(--text-lg);font-weight:700;
color:var(--c-text);white-space:nowrap;overflow:hidden; color:var(--c-text);white-space:nowrap;overflow:hidden;
text-overflow:ellipsis;line-height:1.2">${_esc(zwinger)}</h2> text-overflow:ellipsis;line-height:1.2">${UI.escape(zwinger)}</h2>
<div style="display:flex;align-items:center;gap:var(--space-2)"> <div style="display:flex;align-items:center;gap:var(--space-2)">
<svg style="width:11px;height:11px;color:var(--c-primary);flex-shrink:0" viewBox="0 0 256 256"> <svg style="width:11px;height:11px;color:var(--c-primary);flex-shrink:0" viewBox="0 0 256 256">
<use href="/icons/phosphor.svg#lock-key"></use> <use href="/icons/phosphor.svg#lock-key"></use>
@ -315,7 +311,7 @@ window.Page_litters = (() => {
function _litterCardHTML(l) { function _litterCardHTML(l) {
const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?'; const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?';
const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?'; const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?';
const elternLabel = [l.vater_name, l.mutter_name].filter(Boolean).map(n => _esc(n)).join(' × ') || '—'; const elternLabel = [l.vater_name, l.mutter_name].filter(Boolean).map(n => UI.escape(n)).join(' × ') || '—';
// Datum + Countdown // Datum + Countdown
let datumChip = ''; let datumChip = '';
@ -341,7 +337,7 @@ window.Page_litters = (() => {
const welpenChip = `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar</span>`; const welpenChip = `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar</span>`;
const preisChip = l.preis_spanne const preisChip = l.preis_spanne
? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}</span>` ? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('currency-eur')} ${UI.escape(l.preis_spanne)}</span>`
: ''; : '';
return ` return `
@ -355,8 +351,8 @@ window.Page_litters = (() => {
<div style="min-width:0"> <div style="min-width:0">
${(l.wurf_rang || l.wurf_name) ? ` ${(l.wurf_rang || l.wurf_name) ? `
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)"> <div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)">
${l.wurf_rang ? `<span style="background:var(--c-primary);color:white;border-radius:999px;padding:1px 10px;font-size:var(--text-xs);font-weight:700">${_esc(l.wurf_rang)}-Wurf</span>` : ''} ${l.wurf_rang ? `<span style="background:var(--c-primary);color:white;border-radius:999px;padding:1px 10px;font-size:var(--text-xs);font-weight:700">${UI.escape(l.wurf_rang)}-Wurf</span>` : ''}
${l.wurf_name ? `<span style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${_esc(l.wurf_name)}</span>` : ''} ${l.wurf_name ? `<span style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${UI.escape(l.wurf_name)}</span>` : ''}
</div>` : ''} </div>` : ''}
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-2)"> <div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-2)">
<span class="text-sm-secondary">${elternLabel}</span> <span class="text-sm-secondary">${elternLabel}</span>
@ -395,7 +391,7 @@ window.Page_litters = (() => {
</button> </button>
</div> </div>
</div> </div>
${l.beschreibung ? `<p style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${_esc(l.beschreibung)}</p>` : ''} ${l.beschreibung ? `<p style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${UI.escape(l.beschreibung)}</p>` : ''}
</div> </div>
<!-- Welpen-Bereich --> <!-- Welpen-Bereich -->
@ -455,7 +451,7 @@ window.Page_litters = (() => {
const puppies = await API.litters.puppies(litterId); const puppies = await API.litters.puppies(litterId);
_renderPuppies(inner, litterId, puppies); _renderPuppies(inner, litterId, puppies);
} catch (err) { } catch (err) {
inner.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler beim Laden.')}</p>`; inner.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${UI.escape(err.message || 'Fehler beim Laden.')}</p>`;
} }
} }
@ -469,8 +465,8 @@ window.Page_litters = (() => {
<div class="litters-puppy-row" data-puppy-id="${p.id}"> <div class="litters-puppy-row" data-puppy-id="${p.id}">
<div class="litters-puppy-info"> <div class="litters-puppy-info">
${_genderIcon(p.geschlecht)} ${_genderIcon(p.geschlecht)}
<span class="litters-puppy-name">${p.name ? _esc(p.name) : '<em class="text-muted">Unbenannt</em>'}</span> <span class="litters-puppy-name">${p.name ? UI.escape(p.name) : '<em class="text-muted">Unbenannt</em>'}</span>
${p.farbe ? `<span style="color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(p.farbe)}</span>` : ''} ${p.farbe ? `<span style="color:var(--c-text-secondary);font-size:var(--text-xs)">${UI.escape(p.farbe)}</span>` : ''}
${_puppyStatusBadge(p.status)} ${_puppyStatusBadge(p.status)}
<span class="litters-puppy-last-weight" id="puppy-last-weight-${p.id}" class="text-xs-secondary"></span> <span class="litters-puppy-last-weight" id="puppy-last-weight-${p.id}" class="text-xs-secondary"></span>
</div> </div>
@ -565,7 +561,7 @@ window.Page_litters = (() => {
`; `;
UI.modal.open({ UI.modal.open({
title: `${UI.icon('scales')} Gewichtsverlauf — ${_esc(puppyLabel)}`, title: `${UI.icon('scales')} Gewichtsverlauf — ${UI.escape(puppyLabel)}`,
body, body,
footer, footer,
}); });
@ -695,7 +691,7 @@ window.Page_litters = (() => {
</tbody> </tbody>
</table>`; </table>`;
} catch (err) { } catch (err) {
el.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler beim Laden.')}</p>`; el.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${UI.escape(err.message || 'Fehler beim Laden.')}</p>`;
} }
} }
@ -737,7 +733,7 @@ window.Page_litters = (() => {
btn.innerHTML = `${UI.icon('list-bullets')} Warteliste${active ? ` <span style="background:var(--c-primary);color:white;border-radius:999px;padding:0 6px;font-size:10px;font-weight:700">${active}</span>` : ''}`; btn.innerHTML = `${UI.icon('list-bullets')} Warteliste${active ? ` <span style="background:var(--c-primary);color:white;border-radius:999px;padding:0 6px;font-size:10px;font-weight:700">${active}</span>` : ''}`;
} }
} catch (err) { } catch (err) {
inner.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler.')}</p>`; inner.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${UI.escape(err.message || 'Fehler.')}</p>`;
} }
} }
@ -776,18 +772,18 @@ window.Page_litters = (() => {
<div style="background:var(--c-primary);color:white;border-radius:50%;width:1.6rem;height:1.6rem;display:flex;align-items:center;justify-content:center;font-size:var(--text-xs);font-weight:700;flex-shrink:0;margin-top:2px">${i + 1}</div> <div style="background:var(--c-primary);color:white;border-radius:50%;width:1.6rem;height:1.6rem;display:flex;align-items:center;justify-content:center;font-size:var(--text-xs);font-weight:700;flex-shrink:0;margin-top:2px">${i + 1}</div>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)"> <div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)">
<span style="font-weight:600;font-size:var(--text-sm)">${_esc(e.name)}</span> <span style="font-weight:600;font-size:var(--text-sm)">${UI.escape(e.name)}</span>
${_wlStatusBadge(e.status)} ${_wlStatusBadge(e.status)}
${e.wunsch_geschlecht && e.wunsch_geschlecht !== 'egal' ? `<span class="text-xs-secondary">${e.wunsch_geschlecht === 'maennlich' ? '♂ Rüde' : '♀ Hündin'}</span>` : ''} ${e.wunsch_geschlecht && e.wunsch_geschlecht !== 'egal' ? `<span class="text-xs-secondary">${e.wunsch_geschlecht === 'maennlich' ? '♂ Rüde' : '♀ Hündin'}</span>` : ''}
${e.wunsch_farbe ? `<span class="text-xs-secondary">${_esc(e.wunsch_farbe)}</span>` : ''} ${e.wunsch_farbe ? `<span class="text-xs-secondary">${UI.escape(e.wunsch_farbe)}</span>` : ''}
</div> </div>
<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;font-size:var(--text-xs);color:var(--c-text-secondary)"> <div style="display:flex;gap:var(--space-4);flex-wrap:wrap;font-size:var(--text-xs);color:var(--c-text-secondary)">
${e.email ? `<span>${UI.icon('envelope')} ${_esc(e.email)}</span>` : ''} ${e.email ? `<span>${UI.icon('envelope')} ${UI.escape(e.email)}</span>` : ''}
${e.telefon ? `<span>${UI.icon('phone')} ${_esc(e.telefon)}</span>` : ''} ${e.telefon ? `<span>${UI.icon('phone')} ${UI.escape(e.telefon)}</span>` : ''}
<span>${UI.icon('calendar-dots')} ${e.created_at ? e.created_at.slice(0, 10) : '—'}</span> <span>${UI.icon('calendar-dots')} ${e.created_at ? e.created_at.slice(0, 10) : '—'}</span>
</div> </div>
${e.nachricht ? `<div style="margin-top:var(--space-1);font-size:var(--text-xs);color:var(--c-text-secondary);font-style:italic">"${_esc(e.nachricht)}"</div>` : ''} ${e.nachricht ? `<div style="margin-top:var(--space-1);font-size:var(--text-xs);color:var(--c-text-secondary);font-style:italic">"${UI.escape(e.nachricht)}"</div>` : ''}
${e.notiz ? `<div style="margin-top:var(--space-1);font-size:var(--text-xs);background:var(--c-warning-bg,#fffbeb);color:#92400e;border-radius:4px;padding:2px 6px">${UI.icon('note-pencil')} ${_esc(e.notiz)}</div>` : ''} ${e.notiz ? `<div style="margin-top:var(--space-1);font-size:var(--text-xs);background:var(--c-warning-bg,#fffbeb);color:#92400e;border-radius:4px;padding:2px 6px">${UI.icon('note-pencil')} ${UI.escape(e.notiz)}</div>` : ''}
</div> </div>
<div style="display:flex;gap:var(--space-1);flex-shrink:0"> <div style="display:flex;gap:var(--space-1);flex-shrink:0">
<button class="btn btn-ghost btn-xs wl-edit-btn" data-entry-id="${e.id}" title="Bearbeiten">${UI.icon('pencil-simple')}</button> <button class="btn btn-ghost btn-xs wl-edit-btn" data-entry-id="${e.id}" title="Bearbeiten">${UI.icon('pencil-simple')}</button>
@ -823,16 +819,16 @@ window.Page_litters = (() => {
<form id="wl-form" class="flex-col-gap-3"> <form id="wl-form" class="flex-col-gap-3">
<div class="form-group"> <div class="form-group">
<label class="form-label">Name *</label> <label class="form-label">Name *</label>
<input class="form-control" name="name" required value="${_esc(v.name || '')}"> <input class="form-control" name="name" required value="${UI.escape(v.name || '')}">
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
<label class="form-label">E-Mail</label> <label class="form-label">E-Mail</label>
<input class="form-control" type="email" name="email" value="${_esc(v.email || '')}"> <input class="form-control" type="email" name="email" value="${UI.escape(v.email || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Telefon</label> <label class="form-label">Telefon</label>
<input class="form-control" name="telefon" value="${_esc(v.telefon || '')}"> <input class="form-control" name="telefon" value="${UI.escape(v.telefon || '')}">
</div> </div>
</div> </div>
<div class="grid-2"> <div class="grid-2">
@ -846,12 +842,12 @@ window.Page_litters = (() => {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Wunsch Farbe</label> <label class="form-label">Wunsch Farbe</label>
<input class="form-control" name="wunsch_farbe" placeholder="z.B. schwarz-weiß" value="${_esc(v.wunsch_farbe || '')}"> <input class="form-control" name="wunsch_farbe" placeholder="z.B. schwarz-weiß" value="${UI.escape(v.wunsch_farbe || '')}">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Nachricht des Interessenten</label> <label class="form-label">Nachricht des Interessenten</label>
<textarea class="form-control" name="nachricht" rows="2" placeholder="Was hat der Interessent geschrieben?">${_esc(v.nachricht || '')}</textarea> <textarea class="form-control" name="nachricht" rows="2" placeholder="Was hat der Interessent geschrieben?">${UI.escape(v.nachricht || '')}</textarea>
</div> </div>
<div class="grid-2"> <div class="grid-2">
<div class="form-group"> <div class="form-group">
@ -867,7 +863,7 @@ window.Page_litters = (() => {
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Interne Notiz</label> <label class="form-label">Interne Notiz</label>
<input class="form-control" name="notiz" placeholder="Nur für dich sichtbar" value="${_esc(v.notiz || '')}"> <input class="form-control" name="notiz" placeholder="Nur für dich sichtbar" value="${UI.escape(v.notiz || '')}">
</div> </div>
</form>`, </form>`,
footer: ` footer: `
@ -919,7 +915,7 @@ window.Page_litters = (() => {
const buildSelect = (name, idName, list, currentId, currentName, placeholder) => { const buildSelect = (name, idName, list, currentId, currentName, placeholder) => {
const opts = list.map(h => { const opts = list.map(h => {
const label = h.name + (h.rufname ? ` (${h.rufname})` : '') + (h.zuchtbuchnummer ? ` · ${h.zuchtbuchnummer}` : ''); const label = h.name + (h.rufname ? ` (${h.rufname})` : '') + (h.zuchtbuchnummer ? ` · ${h.zuchtbuchnummer}` : '');
return `<option value="${h.id}" data-name="${_esc(h.name)}" ${currentId == h.id ? 'selected' : ''}>${_esc(label)}</option>`; return `<option value="${h.id}" data-name="${UI.escape(h.name)}" ${currentId == h.id ? 'selected' : ''}>${UI.escape(label)}</option>`;
}).join(''); }).join('');
return ` return `
<select class="form-control" name="${idName}" id="${idName}-sel" class="mb-2"> <select class="form-control" name="${idName}" id="${idName}-sel" class="mb-2">
@ -927,7 +923,7 @@ window.Page_litters = (() => {
${opts} ${opts}
</select> </select>
<input class="form-control" type="text" name="${name}" id="${name}-txt" <input class="form-control" type="text" name="${name}" id="${name}-txt"
value="${_esc(currentName || '')}" placeholder="oder Namen frei eingeben">`; value="${UI.escape(currentName || '')}" placeholder="oder Namen frei eingeben">`;
}; };
const rangOpts = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(l => const rangOpts = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(l =>
@ -949,7 +945,7 @@ window.Page_litters = (() => {
<label class="form-label">Wurf-Name <span style="font-weight:normal;color:var(--c-text-muted)">(optional)</span></label> <label class="form-label">Wurf-Name <span style="font-weight:normal;color:var(--c-text-muted)">(optional)</span></label>
<input class="form-control" type="text" name="wurf_name" <input class="form-control" type="text" name="wurf_name"
placeholder="z.B. Vatertags-Wurf, Frühlings-Wurf …" placeholder="z.B. Vatertags-Wurf, Frühlings-Wurf …"
value="${_esc(v.wurf_name || '')}"> value="${UI.escape(v.wurf_name || '')}">
</div> </div>
</div> </div>
@ -968,13 +964,13 @@ window.Page_litters = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Erwarteter Geburtstermin <span style="font-weight:normal;color:var(--c-text-muted)">(geplant)</span></label> <label class="form-label">Erwarteter Geburtstermin <span style="font-weight:normal;color:var(--c-text-muted)">(geplant)</span></label>
<input class="form-control" type="date" name="erwartetes_datum" <input class="form-control" type="date" name="erwartetes_datum"
value="${_esc(v.erwartetes_datum || '')}"> value="${UI.escape(v.erwartetes_datum || '')}">
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:4px 0 0">Für geplante Würfe / laufende Trächtigkeit</p> <p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:4px 0 0">Für geplante Würfe / laufende Trächtigkeit</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Geburtsdatum <span style="font-weight:normal;color:var(--c-text-muted)">(tatsächlich)</span></label> <label class="form-label">Geburtsdatum <span style="font-weight:normal;color:var(--c-text-muted)">(tatsächlich)</span></label>
<input class="form-control" type="date" name="geburt_datum" <input class="form-control" type="date" name="geburt_datum"
value="${_esc(v.geburt_datum || '')}"> value="${UI.escape(v.geburt_datum || '')}">
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:4px 0 0">Wenn die Welpen bereits geboren sind</p> <p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:4px 0 0">Wenn die Welpen bereits geboren sind</p>
</div> </div>
</div> </div>
@ -1005,19 +1001,19 @@ window.Page_litters = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Preisspanne</label> <label class="form-label">Preisspanne</label>
<input class="form-control" type="text" name="preis_spanne" <input class="form-control" type="text" name="preis_spanne"
value="${_esc(v.preis_spanne || '')}" placeholder="z. B. 1.500 2.000 €"> value="${UI.escape(v.preis_spanne || '')}" placeholder="z. B. 1.500 2.000 €">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Beschreibung <span class="text-secondary">(optional)</span></label> <label class="form-label">Beschreibung <span class="text-secondary">(optional)</span></label>
<textarea class="form-control" name="beschreibung" rows="3" <textarea class="form-control" name="beschreibung" rows="3"
placeholder="Elternlinie, Besonderheiten, Charakter…">${_esc(v.beschreibung || '')}</textarea> placeholder="Elternlinie, Besonderheiten, Charakter…">${UI.escape(v.beschreibung || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Gesundheitstests <span class="text-secondary">(optional)</span></label> <label class="form-label">Gesundheitstests <span class="text-secondary">(optional)</span></label>
<textarea class="form-control" name="gesundheitstests" rows="2" <textarea class="form-control" name="gesundheitstests" rows="2"
placeholder="HD, ED, Gentest, Augenkontrolle…">${_esc(v.gesundheitstests || '')}</textarea> placeholder="HD, ED, Gentest, Augenkontrolle…">${UI.escape(v.gesundheitstests || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -1030,7 +1026,7 @@ window.Page_litters = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Sichtbar bis <span class="text-secondary">(optional)</span></label> <label class="form-label">Sichtbar bis <span class="text-secondary">(optional)</span></label>
<input class="form-control" type="date" name="sichtbar_bis" <input class="form-control" type="date" name="sichtbar_bis"
value="${_esc(v.sichtbar_bis || '')}"> value="${UI.escape(v.sichtbar_bis || '')}">
</div> </div>
</form> </form>
@ -1138,7 +1134,7 @@ window.Page_litters = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Name <span class="text-secondary">(optional)</span></label> <label class="form-label">Name <span class="text-secondary">(optional)</span></label>
<input class="form-control" type="text" name="name" <input class="form-control" type="text" name="name"
value="${_esc(v.name || '')}" placeholder="z. B. Max"> value="${UI.escape(v.name || '')}" placeholder="z. B. Max">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Geschlecht</label> <label class="form-label">Geschlecht</label>
@ -1153,7 +1149,7 @@ window.Page_litters = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Farbe / Fellzeichnung</label> <label class="form-label">Farbe / Fellzeichnung</label>
<input class="form-control" type="text" name="farbe" <input class="form-control" type="text" name="farbe"
value="${_esc(v.farbe || '')}" placeholder="z. B. schwarz-braun"> value="${UI.escape(v.farbe || '')}" placeholder="z. B. schwarz-braun">
</div> </div>
<div class="form-group"> <div class="form-group">
@ -1169,7 +1165,7 @@ window.Page_litters = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Chip-Nr.</label> <label class="form-label">Chip-Nr.</label>
<input class="form-control" type="text" name="chip_nr" <input class="form-control" type="text" name="chip_nr"
value="${_esc(v.chip_nr || '')}" placeholder="15-stellig"> value="${UI.escape(v.chip_nr || '')}" placeholder="15-stellig">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Geburtsgewicht (g)</label> <label class="form-label">Geburtsgewicht (g)</label>
@ -1188,7 +1184,7 @@ window.Page_litters = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Notiz <span class="text-secondary">(intern)</span></label> <label class="form-label">Notiz <span class="text-secondary">(intern)</span></label>
<textarea class="form-control" name="notiz" rows="2" <textarea class="form-control" name="notiz" rows="2"
placeholder="Interne Notizen…">${_esc(v.notiz || '')}</textarea> placeholder="Interne Notizen…">${UI.escape(v.notiz || '')}</textarea>
</div> </div>
</form> </form>
@ -1279,7 +1275,7 @@ window.Page_litters = (() => {
`; `;
UI.modal.open({ UI.modal.open({
title: `${UI.icon('file-text')} Kaufvertrag — ${_esc(puppyLabel)}`, title: `${UI.icon('file-text')} Kaufvertrag — ${UI.escape(puppyLabel)}`,
body, body,
footer, footer,
}); });
@ -1336,7 +1332,7 @@ window.Page_litters = (() => {
`; `;
UI.modal.open({ UI.modal.open({
title: `${UI.icon('images')} Fotos — ${_esc(label)}`, title: `${UI.icon('images')} Fotos — ${UI.escape(label)}`,
body, body,
footer, footer,
}); });
@ -1358,21 +1354,21 @@ window.Page_litters = (() => {
const vis = visLabels[ph.visibility] || visLabels.private; const vis = visLabels[ph.visibility] || visLabels.private;
return ` return `
<div style="position:relative;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--c-border);aspect-ratio:1"> <div style="position:relative;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--c-border);aspect-ratio:1">
<a href="${_esc(ph.url || '')}" target="_blank" rel="noopener noreferrer"> <a href="${UI.escape(ph.url || '')}" target="_blank" rel="noopener noreferrer">
<img src="${_esc(thumb)}" alt="${_esc(ph.caption || '')}" <img src="${UI.escape(thumb)}" alt="${UI.escape(ph.caption || '')}"
loading="lazy" loading="lazy"
style="width:100%;height:100%;object-fit:cover;display:block" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.src='/static/img/placeholder.webp'"> onerror="this.src='/static/img/placeholder.webp'">
</a> </a>
<button class="photos-vis-btn" <button class="photos-vis-btn"
data-photo-id="${ph.id}" data-photo-id="${ph.id}"
data-vis="${_esc(ph.visibility)}" data-vis="${UI.escape(ph.visibility)}"
title="Sichtbarkeit ändern" title="Sichtbarkeit ändern"
style="position:absolute;bottom:0;left:0;right:0; style="position:absolute;bottom:0;left:0;right:0;
background:${vis.color};color:#fff; background:${vis.color};color:#fff;
border:none;cursor:pointer;font-size:10px;padding:2px 4px; border:none;cursor:pointer;font-size:10px;padding:2px 4px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(vis.text)} ${UI.escape(vis.text)}
</button> </button>
<button class="photos-del-btn" <button class="photos-del-btn"
data-photo-id="${ph.id}" data-photo-id="${ph.id}"
@ -1418,7 +1414,7 @@ window.Page_litters = (() => {
} catch (err) { } catch (err) {
const el = document.getElementById(galleryId); const el = document.getElementById(galleryId);
if (el) el.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler beim Laden.')}</p>`; if (el) el.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${UI.escape(err.message || 'Fehler beim Laden.')}</p>`;
} }
} }
@ -1464,13 +1460,13 @@ window.Page_litters = (() => {
const issueHTML = (welfare.issues || []).map(i => ` const issueHTML = (welfare.issues || []).map(i => `
<div style="display:flex;gap:8px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.06)"> <div style="display:flex;gap:8px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.06)">
<span style="color:${color};flex-shrink:0">${UI.icon('warning')}</span> <span style="color:${color};flex-shrink:0">${UI.icon('warning')}</span>
<span class="text-sm">${_esc(i.text)}</span> <span class="text-sm">${UI.escape(i.text)}</span>
</div>`).join(''); </div>`).join('');
const okHTML = (welfare.ok_points || []).map(p => ` const okHTML = (welfare.ok_points || []).map(p => `
<div style="display:flex;gap:8px;padding:4px 0"> <div style="display:flex;gap:8px;padding:4px 0">
<span style="color:#16a34a;flex-shrink:0">${UI.icon('check')}</span> <span style="color:#16a34a;flex-shrink:0">${UI.icon('check')}</span>
<span class="text-sm-secondary">${_esc(p)}</span> <span class="text-sm-secondary">${UI.escape(p)}</span>
</div>`).join(''); </div>`).join('');
const isProblematic = welfare.level === 'warning' || welfare.level === 'critical'; const isProblematic = welfare.level === 'warning' || welfare.level === 'critical';
@ -1540,7 +1536,7 @@ window.Page_litters = (() => {
} catch (err) { } catch (err) {
UI.modal.open({ UI.modal.open({
title: `${UI.icon('sparkle')} KI-Wurfankündigung`, title: `${UI.icon('sparkle')} KI-Wurfankündigung`,
body: `<p class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`, body: `<p class="text-danger">${UI.escape(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`, footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
}); });
return; return;
@ -1548,7 +1544,7 @@ window.Page_litters = (() => {
UI.modal.open({ UI.modal.open({
title: `${UI.icon('sparkle')} KI-Wurfankündigung`, title: `${UI.icon('sparkle')} KI-Wurfankündigung`,
body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`, body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${UI.escape(text)}</div>`,
footer: ` footer: `
<button class="btn btn-secondary flex-1" id="ki-announce-copy"> <button class="btn btn-secondary flex-1" id="ki-announce-copy">
${UI.icon('clipboard-text')} Kopieren ${UI.icon('clipboard-text')} Kopieren

View file

@ -286,8 +286,8 @@ window.Page_lost = (() => {
const marker = UI.map.svgMarker(r.lat, r.lon, html, { size: 34, anchorY: 17 }) const marker = UI.map.svgMarker(r.lat, r.lon, html, { size: 34, anchorY: 17 })
.addTo(_map) .addTo(_map)
.bindPopup(` .bindPopup(`
<b>🔍 ${_escape(r.name)}</b><br> <b>🔍 ${UI.escape(r.name)}</b><br>
${r.rasse ? _escape(r.rasse) + '<br>' : ''} ${r.rasse ? UI.escape(r.rasse) + '<br>' : ''}
${distStr ? `<small>📍 ${distStr} entfernt</small><br>` : ''} ${distStr ? `<small>📍 ${distStr} entfernt</small><br>` : ''}
${r._isPending ? '<small>⏳ Sync ausstehend</small><br>' : ''} ${r._isPending ? '<small>⏳ Sync ausstehend</small><br>' : ''}
<small>📅 ${_fmtDate(r.created_at)}</small> <small>📅 ${_fmtDate(r.created_at)}</small>
@ -382,10 +382,10 @@ window.Page_lost = (() => {
<div style="display:flex;align-items:center;gap:var(--space-2); <div style="display:flex;align-items:center;gap:var(--space-2);
margin-bottom:var(--space-1);flex-wrap:wrap"> margin-bottom:var(--space-1);flex-wrap:wrap">
<span style="font-weight:var(--weight-semibold);font-size:var(--text-base)"> <span style="font-weight:var(--weight-semibold);font-size:var(--text-base)">
${_escape(r.name)} ${UI.escape(r.name)}
</span> </span>
${r.rasse ${r.rasse
? `<span class="badge">${_escape(r.rasse)}</span>` ? `<span class="badge">${UI.escape(r.rasse)}</span>`
: ''} : ''}
${isOwn ${isOwn
? '<span class="badge badge-warning">Meine Meldung</span>' ? '<span class="badge badge-warning">Meine Meldung</span>'
@ -399,11 +399,11 @@ window.Page_lost = (() => {
</div> </div>
<p style="margin:0 0 var(--space-1);font-size:var(--text-sm); <p style="margin:0 0 var(--space-1);font-size:var(--text-sm);
color:var(--c-text)"> color:var(--c-text)">
${_escape(r.beschreibung.slice(0, 120))}${r.beschreibung.length > 120 ? '…' : ''} ${UI.escape(r.beschreibung.slice(0, 120))}${r.beschreibung.length > 120 ? '…' : ''}
</p> </p>
<div class="text-xs-secondary"> <div class="text-xs-secondary">
Gemeldet ${_fmtDate(r.created_at)} Gemeldet ${_fmtDate(r.created_at)}
${r.melder_name ? '· ' + _escape(r.melder_name.split(' ')[0]) : ''} ${r.melder_name ? '· ' + UI.escape(r.melder_name.split(' ')[0]) : ''}
</div> </div>
${r._isPending ${r._isPending
? `<div style="margin-top:var(--space-2);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap"> ? `<div style="margin-top:var(--space-2);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
@ -418,7 +418,7 @@ window.Page_lost = (() => {
: (_appState.user ? `<div class="mt-2"> : (_appState.user ? `<div class="mt-2">
<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="${_escape(r.name)}" data-lost-note-name="${UI.escape(r.name)}"
title="Notiz" onclick="event.stopPropagation()"> title="Notiz" onclick="event.stopPropagation()">
<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>
@ -447,19 +447,19 @@ window.Page_lost = (() => {
: ''} : ''}
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)"> <div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)">
<span class="badge badge-danger">🐕 ${_escape(r.name)}</span> <span class="badge badge-danger">🐕 ${UI.escape(r.name)}</span>
${r.rasse ? `<span class="badge">${_escape(r.rasse)}</span>` : ''} ${r.rasse ? `<span class="badge">${UI.escape(r.rasse)}</span>` : ''}
</div> </div>
<p style="white-space:pre-wrap;margin-bottom:var(--space-3)"> <p style="white-space:pre-wrap;margin-bottom:var(--space-3)">
${_escape(r.beschreibung)} ${UI.escape(r.beschreibung)}
</p> </p>
<div style="font-size:var(--text-sm);color:var(--c-text-secondary); <div style="font-size:var(--text-sm);color:var(--c-text-secondary);
margin-bottom:var(--space-4);line-height:1.8"> margin-bottom:var(--space-4);line-height:1.8">
<div>📍 ${r.lat.toFixed(5)}, ${r.lon.toFixed(5)}${distStr ? ' (' + distStr + ' entfernt)' : ''}</div> <div>📍 ${r.lat.toFixed(5)}, ${r.lon.toFixed(5)}${distStr ? ' (' + distStr + ' entfernt)' : ''}</div>
<div>📅 Gemeldet: ${_fmtDate(r.created_at)}</div> <div>📅 Gemeldet: ${_fmtDate(r.created_at)}</div>
${r.melder_name ? `<div>👤 Gemeldet von: ${_escape(r.melder_name.split(' ')[0])}</div>` : ''} ${r.melder_name ? `<div>👤 Gemeldet von: ${UI.escape(r.melder_name.split(' ')[0])}</div>` : ''}
</div> </div>
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap"> <div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
@ -473,7 +473,7 @@ window.Page_lost = (() => {
</div> </div>
`; `;
UI.modal.open({ title: `🔍 ${_escape(r.name)} wird vermisst`, body }); UI.modal.open({ title: `🔍 ${UI.escape(r.name)} wird vermisst`, body });
document.getElementById('detail-lost-map')?.addEventListener('click', () => { document.getElementById('detail-lost-map')?.addEventListener('click', () => {
UI.modal.close(); UI.modal.close();
@ -511,10 +511,10 @@ window.Page_lost = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
function _showFoundDialog(r) { function _showFoundDialog(r) {
UI.modal.open({ UI.modal.open({
title: `🎉 ${_escape(r.name)} gefunden?`, title: `🎉 ${UI.escape(r.name)} gefunden?`,
body: ` body: `
<p style="color:var(--c-text-secondary);margin-bottom:var(--space-4)"> <p style="color:var(--c-text-secondary);margin-bottom:var(--space-4)">
Wurde ${_escape(r.name)} wiedergefunden? Die Meldung wird als Wurde ${UI.escape(r.name)} wiedergefunden? Die Meldung wird als
abgeschlossen markiert und aus der Liste entfernt. abgeschlossen markiert und aus der Liste entfernt.
</p> </p>
`, `,
@ -555,7 +555,7 @@ window.Page_lost = (() => {
const dogs = _appState.dogs || []; const dogs = _appState.dogs || [];
const dogOpts = dogs.length > 0 const dogOpts = dogs.length > 0
? `<option value="">— kein registrierter Hund —</option>` + ? `<option value="">— kein registrierter Hund —</option>` +
dogs.map(d => `<option value="${d.id}">${_escape(d.name)}${d.rasse ? ' (' + _escape(d.rasse) + ')' : ''}</option>`).join('') dogs.map(d => `<option value="${d.id}">${UI.escape(d.name)}${d.rasse ? ' (' + UI.escape(d.rasse) + ')' : ''}</option>`).join('')
: ''; : '';
const body = ` const body = `
@ -790,16 +790,6 @@ window.Page_lost = (() => {
day: '2-digit', month: '2-digit', year: 'numeric' day: '2-digit', month: '2-digit', year: 'numeric'
}); });
} }
function _escape(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _emptyState(icon, title, text, cta = '') { function _emptyState(icon, title, text, cta = '') {
return `<div class="empty-state"> return `<div class="empty-state">
<svg class="ph-icon empty-state-icon" aria-hidden="true"> <svg class="ph-icon empty-state-icon" aria-hidden="true">
@ -829,7 +819,7 @@ window.Page_lost = (() => {
display:flex;align-items:center;justify-content:space-between;flex-shrink:0"> display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
<div> <div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div> <div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_escape(parentLabel)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
</div> </div>
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button> <button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
</div> </div>

View file

@ -1344,15 +1344,15 @@ window.Page_map = (() => {
}); });
const marker = L.marker([b.location_lat, b.location_lng], { icon, zIndexOffset: t.z ?? 0 }) const marker = L.marker([b.location_lat, b.location_lng], { icon, zIndexOffset: t.z ?? 0 })
.bindTooltip(_esc(b.zwingername), { direction: 'top', offset: [0, -16] }); .bindTooltip(UI.escape(b.zwingername), { direction: 'top', offset: [0, -16] });
marker.on('click', () => { marker.on('click', () => {
const rasseText = b.rasse_text ? `<div style="font-size:12px;color:#666;margin-bottom:4px">${_esc(b.rasse_text)}</div>` : ''; const rasseText = b.rasse_text ? `<div style="font-size:12px;color:#666;margin-bottom:4px">${UI.escape(b.rasse_text)}</div>` : '';
const stadtText = b.stadt ? `<div style="font-size:12px;color:#888;margin-bottom:8px">${_esc(b.stadt)}</div>` : ''; const stadtText = b.stadt ? `<div style="font-size:12px;color:#888;margin-bottom:8px">${UI.escape(b.stadt)}</div>` : '';
marker.bindPopup(` marker.bindPopup(`
<div style="min-width:170px;max-width:240px"> <div style="min-width:170px;max-width:240px">
<div style="font-weight:600;margin-bottom:6px">${t.icon} ${_esc(b.zwingername)}</div> <div style="font-weight:600;margin-bottom:6px">${t.icon} ${UI.escape(b.zwingername)}</div>
${rasseText}${stadtText} ${rasseText}${stadtText}
<button class="btn btn-primary btn-sm" id="breeder-profile-btn">Profil ansehen</button> <button class="btn btn-primary btn-sm" id="breeder-profile-btn">Profil ansehen</button>
</div> </div>

View file

@ -162,17 +162,17 @@ window.Page_moderation = (() => {
gap:var(--space-4)"> gap:var(--space-4)">
${fotos.map(f => ` ${fotos.map(f => `
<div class="card p-4" data-id="${f.id}"> <div class="card p-4" data-id="${f.id}">
<a href="#wiki?rasse=${_esc(f.rasse_slug)}" style="display:block;text-decoration:none"> <a href="#wiki?rasse=${UI.escape(f.rasse_slug)}" style="display:block;text-decoration:none">
<img src="${_esc(f.foto_url)}" alt="" <img src="${UI.escape(f.foto_url)}" alt=""
style="width:100%;height:140px;object-fit:cover; style="width:100%;height:140px;object-fit:cover;
border-radius:var(--radius-md);margin-bottom:var(--space-3)"> border-radius:var(--radius-md);margin-bottom:var(--space-3)">
</a> </a>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)"> <div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
${_esc(f.rasse_name || f.rasse_slug)} ${UI.escape(f.rasse_name || f.rasse_slug)}
</div> </div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted); <div style="font-size:var(--text-xs);color:var(--c-text-muted);
margin-bottom:var(--space-2)"> margin-bottom:var(--space-2)">
von ${_esc(f.user_name)} von ${UI.escape(f.user_name)}
</div> </div>
<div class="mb-3"> <div class="mb-3">
${f.rights_confirmed ${f.rights_confirmed
@ -183,7 +183,7 @@ window.Page_moderation = (() => {
</div> </div>
${f.aktuell_foto ? ` ${f.aktuell_foto ? `
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:4px">Aktuell:</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:4px">Aktuell:</div>
<img src="${_esc(f.aktuell_foto)}" alt="Aktuell" <img src="${UI.escape(f.aktuell_foto)}" alt="Aktuell"
style="width:100%;height:70px;object-fit:cover; style="width:100%;height:70px;object-fit:cover;
border-radius:var(--radius-sm);opacity:.5; border-radius:var(--radius-sm);opacity:.5;
margin-bottom:var(--space-3)"> margin-bottom:var(--space-3)">
@ -299,23 +299,23 @@ window.Page_moderation = (() => {
background:var(--c-surface-2); background:var(--c-surface-2);
display:flex;align-items:center;justify-content:center; display:flex;align-items:center;justify-content:center;
font-weight:var(--weight-bold);color:var(--c-text-secondary)"> font-weight:var(--weight-bold);color:var(--c-text-secondary)">
${_esc(u.name[0].toUpperCase())} ${UI.escape(u.name[0].toUpperCase())}
</div> </div>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)"> color:var(--c-text)">
${_esc(u.name)} ${UI.escape(u.name)}
${u.is_banned ? `<span style="font-size:10px;padding:1px 5px; ${u.is_banned ? `<span style="font-size:10px;padding:1px 5px;
border-radius:3px;background:var(--c-danger); border-radius:3px;background:var(--c-danger);
color:#fff;margin-left:4px">GESPERRT</span>` : ''} color:#fff;margin-left:4px">GESPERRT</span>` : ''}
</div> </div>
<div class="text-xs-muted"> <div class="text-xs-muted">
${_esc(u.email)} · ${UI.escape(u.email)} ·
<span style="color:${ <span style="color:${
u.rolle === 'admin' ? 'var(--c-danger)' u.rolle === 'admin' ? 'var(--c-danger)'
: u.rolle === 'moderator' ? '#f59e0b' : u.rolle === 'moderator' ? '#f59e0b'
: 'var(--c-text-muted)'}"> : 'var(--c-text-muted)'}">
${_esc(u.rolle)} ${UI.escape(u.rolle)}
</span> </span>
</div> </div>
</div> </div>
@ -323,12 +323,12 @@ window.Page_moderation = (() => {
${canAction ${canAction
? (u.is_banned ? (u.is_banned
? `<button class="btn btn-sm btn-ghost mod-unban" ? `<button class="btn btn-sm btn-ghost mod-unban"
data-uid="${u.id}" data-name="${_esc(u.name)}" data-uid="${u.id}" data-name="${UI.escape(u.name)}"
title="Sperre aufheben" class="text-success"> title="Sperre aufheben" class="text-success">
${UI.icon('lock-open')} ${UI.icon('lock-open')}
</button>` </button>`
: `<button class="btn btn-sm btn-ghost mod-ban" : `<button class="btn btn-sm btn-ghost mod-ban"
data-uid="${u.id}" data-name="${_esc(u.name)}" data-uid="${u.id}" data-name="${UI.escape(u.name)}"
title="Sperren" class="text-danger"> title="Sperren" class="text-danger">
${UI.icon('lock')} ${UI.icon('lock')}
</button>`) </button>`)
@ -408,19 +408,19 @@ window.Page_moderation = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-xs);color:var(--c-text-muted); <div style="font-size:var(--text-xs);color:var(--c-text-muted);
margin-bottom:var(--space-1)"> margin-bottom:var(--space-1)">
${_esc(r.target_type)} #${r.target_id} · ${UI.escape(r.target_type)} #${r.target_id} ·
Gemeldet von <strong>${_esc(r.melder_name)}</strong> Gemeldet von <strong>${UI.escape(r.melder_name)}</strong>
</div> </div>
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-1)"> color:var(--c-text);margin-bottom:var(--space-1)">
Grund: ${_esc(r.grund)} Grund: ${UI.escape(r.grund)}
</div> </div>
${r.content_preview ? ` ${r.content_preview ? `
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div style="font-size:var(--text-xs);color:var(--c-text-secondary);
padding:var(--space-2) var(--space-3); padding:var(--space-2) var(--space-3);
background:var(--c-surface-2); background:var(--c-surface-2);
border-radius:var(--radius-sm)"> border-radius:var(--radius-sm)">
${_esc(r.content_preview)} ${UI.escape(r.content_preview)}
</div>` : ''} </div>` : ''}
</div> </div>
<button class="btn btn-sm btn-primary mod-resolve-btn" <button class="btn btn-sm btn-primary mod-resolve-btn"
@ -481,9 +481,9 @@ window.Page_moderation = (() => {
<div class="card p-4" data-edit-id="${e.id}"> <div class="card p-4" data-edit-id="${e.id}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-2);flex-wrap:wrap"> <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-2);flex-wrap:wrap">
<div> <div>
<div style="font-weight:600">${_esc(e.poi_name)}</div> <div style="font-weight:600">${UI.escape(e.poi_name)}</div>
<div class="text-xs-muted"> <div class="text-xs-muted">
OSM-ID: ${_esc(e.osm_id)} · Feld: ${_esc(e.field)} · von ${_esc(e.einreicher_name)} OSM-ID: ${UI.escape(e.osm_id)} · Feld: ${UI.escape(e.field)} · von ${UI.escape(e.einreicher_name)}
· ${new Date(e.created_at).toLocaleDateString('de-DE')} · ${new Date(e.created_at).toLocaleDateString('de-DE')}
</div> </div>
</div> </div>
@ -494,11 +494,11 @@ window.Page_moderation = (() => {
<div style="margin-top:var(--space-3);display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)"> <div style="margin-top:var(--space-3);display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
<div style="background:var(--c-surface-2);border-radius:var(--radius-sm);padding:var(--space-2)"> <div style="background:var(--c-surface-2);border-radius:var(--radius-sm);padding:var(--space-2)">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Aktuell</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Aktuell</div>
<div class="text-sm">${_esc(e.old_value) || '<em class="text-muted">leer</em>'}</div> <div class="text-sm">${UI.escape(e.old_value) || '<em class="text-muted">leer</em>'}</div>
</div> </div>
<div style="background:var(--c-surface-2);border-radius:var(--radius-sm);padding:var(--space-2)"> <div style="background:var(--c-surface-2);border-radius:var(--radius-sm);padding:var(--space-2)">
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Vorschlag</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">Vorschlag</div>
<div style="font-size:var(--text-sm);font-weight:600">${_esc(e.new_value)}</div> <div style="font-size:var(--text-sm);font-weight:600">${UI.escape(e.new_value)}</div>
</div> </div>
</div> </div>
${e.status === 'pending' ? ` ${e.status === 'pending' ? `
@ -532,15 +532,6 @@ window.Page_moderation = (() => {
}); });
} }
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
return { init, refresh, onDogChange }; return { init, refresh, onDogChange };

View file

@ -88,7 +88,7 @@ window.Page_movies = (() => {
<div class="movies-search-row"> <div class="movies-search-row">
<svg class="ph-icon movies-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg> <svg class="ph-icon movies-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
<input type="search" id="movies-search" class="form-control movies-search-input" <input type="search" id="movies-search" class="form-control movies-search-input"
placeholder="Film, Serie oder Rasse suchen …" value="${_esc(_search)}" autocomplete="off"> placeholder="Film, Serie oder Rasse suchen …" value="${UI.escape(_search)}" autocomplete="off">
</div> </div>
<div class="movies-filter-row"> <div class="movies-filter-row">
<button class="movies-filter-btn${_filter === 'alle' ? ' movies-filter-btn--active' : ''}" data-filter="alle">Alle</button> <button class="movies-filter-btn${_filter === 'alle' ? ' movies-filter-btn--active' : ''}" data-filter="alle">Alle</button>
@ -202,17 +202,17 @@ window.Page_movies = (() => {
const _ico = name => `<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px;vertical-align:middle"><use href="/icons/phosphor.svg#${name}"></use></svg>`; const _ico = name => `<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px;vertical-align:middle"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
const typLabel = film.typ === 'serie' ? `${_ico('list')} Serie` : film.typ === 'doku' ? `${_ico('camera')} Doku` : ''; const typLabel = film.typ === 'serie' ? `${_ico('list')} Serie` : film.typ === 'doku' ? `${_ico('camera')} Doku` : '';
const imdb = film.imdb_rating ? `<span class="text-xs-muted">IMDb ${film.imdb_rating}</span>` : ''; const imdb = film.imdb_rating ? `<span class="text-xs-muted">IMDb ${film.imdb_rating}</span>` : '';
const streaming = film.streaming ? `<span class="text-xs-muted">${_esc(film.streaming)}</span>` : ''; const streaming = film.streaming ? `<span class="text-xs-muted">${UI.escape(film.streaming)}</span>` : '';
return ` return `
<div class="movie-card" data-film-id="${_esc(film.id)}"> <div class="movie-card" data-film-id="${UI.escape(film.id)}">
<div class="movie-card-emoji">${film.bild_emoji}</div> <div class="movie-card-emoji">${film.bild_emoji}</div>
<div class="movie-card-body"> <div class="movie-card-body">
<div class="movie-card-title">${_esc(film.titel)} <span class="movie-card-year">(${film.jahr})</span></div> <div class="movie-card-title">${UI.escape(film.titel)} <span class="movie-card-year">(${film.jahr})</span></div>
<div class="movie-card-genre" style="display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap"> <div class="movie-card-genre" style="display:flex;gap:var(--space-2);align-items:center;flex-wrap:wrap">
<span>${_esc(film.genre)}</span>${typLabel ? `<span class="text-xs-muted">${typLabel}</span>` : ''} <span>${UI.escape(film.genre)}</span>${typLabel ? `<span class="text-xs-muted">${typLabel}</span>` : ''}
</div> </div>
<div class="movie-card-rasse"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#paw-print"></use></svg> ${_esc(film.hund_rasse)}</div> <div class="movie-card-rasse"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#paw-print"></use></svg> ${UI.escape(film.hund_rasse)}</div>
${tag} ${tag}
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-1)">${imdb}${streaming}</div> <div style="display:flex;gap:var(--space-3);margin-top:var(--space-1)">${imdb}${streaming}</div>
<div class="movie-card-stars">${stars}</div> <div class="movie-card-stars">${stars}</div>
@ -234,17 +234,17 @@ window.Page_movies = (() => {
const body = ` const body = `
<div class="movie-modal-emoji">${film.bild_emoji}</div> <div class="movie-modal-emoji">${film.bild_emoji}</div>
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)"> <div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)">
<span class="badge badge-primary">${_esc(film.genre)}</span> <span class="badge badge-primary">${UI.escape(film.genre)}</span>
<span class="badge"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#paw-print"></use></svg> ${_esc(film.hund_rasse)}</span> <span class="badge"><svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#paw-print"></use></svg> ${UI.escape(film.hund_rasse)}</span>
<span class="badge">${film.jahr}</span> <span class="badge">${film.jahr}</span>
</div> </div>
<div class="${bannerClass}" style="margin-bottom:var(--space-4);font-size:var(--text-base)">${bannerText}</div> <div class="${bannerClass}" style="margin-bottom:var(--space-4);font-size:var(--text-base)">${bannerText}</div>
<p style="line-height:1.6;color:var(--c-text);margin-bottom:var(--space-5)">${_esc(film.beschreibung)}</p> <p style="line-height:1.6;color:var(--c-text);margin-bottom:var(--space-5)">${UI.escape(film.beschreibung)}</p>
<div class="mb-2"> <div class="mb-2">
<strong>Community-Bewertung:</strong> <strong>Community-Bewertung:</strong>
</div> </div>
<div id="modal-stars-${_esc(film.id)}">${stars}</div> <div id="modal-stars-${UI.escape(film.id)}">${stars}</div>
<div id="modal-avg-${_esc(film.id)}" style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-1)"> <div id="modal-avg-${UI.escape(film.id)}" style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-top:var(--space-1)">
Ø ${film.bewertung_avg} von ${film.bewertung_cnt || 0} Bewertungen Ø ${film.bewertung_avg} von ${film.bewertung_cnt || 0} Bewertungen
</div> </div>
${loginHint} ${loginHint}
@ -262,9 +262,9 @@ window.Page_movies = (() => {
const filled = Math.round(avg); const filled = Math.round(avg);
const stars = [1,2,3,4,5].map(i => { const stars = [1,2,3,4,5].map(i => {
const active = i <= (userRating || filled) ? ' movie-star--active' : ''; const active = i <= (userRating || filled) ? ' movie-star--active' : '';
return `<span class="movie-star${active}" data-film-id="${_esc(filmId)}" data-val="${i}"><svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px"><use href="/icons/phosphor.svg#star"></use></svg></span>`; return `<span class="movie-star${active}" data-film-id="${UI.escape(filmId)}" data-val="${i}"><svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px"><use href="/icons/phosphor.svg#star"></use></svg></span>`;
}).join(''); }).join('');
return `<div class="movie-star-rating" data-film-id="${_esc(filmId)}">${stars} <span class="movie-star-avg">${avg}</span></div>`; return `<div class="movie-star-rating" data-film-id="${UI.escape(filmId)}">${stars} <span class="movie-star-avg">${avg}</span></div>`;
} }
function _bindStarRatings(container) { function _bindStarRatings(container) {
@ -339,9 +339,9 @@ window.Page_movies = (() => {
<div class="movie-promi-card"> <div class="movie-promi-card">
<div class="movie-promi-emoji">${p.emoji}</div> <div class="movie-promi-emoji">${p.emoji}</div>
<div class="movie-promi-body"> <div class="movie-promi-body">
<div class="movie-promi-name">${_esc(p.name)}</div> <div class="movie-promi-name">${UI.escape(p.name)}</div>
<div class="movie-promi-rasse">${_esc(p.rasse)}</div> <div class="movie-promi-rasse">${UI.escape(p.rasse)}</div>
<div class="movie-promi-text">${_esc(p.bekannt_fuer)}</div> <div class="movie-promi-text">${UI.escape(p.bekannt_fuer)}</div>
</div> </div>
</div> </div>
`).join('')} `).join('')}
@ -370,13 +370,13 @@ window.Page_movies = (() => {
const voteCards = _appState.dogs.map(dog => { const voteCards = _appState.dogs.map(dog => {
const isVoted = data.user_vote === dog.id; const isVoted = data.user_vote === dog.id;
const av = dog.foto_url const av = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}" class="hdm-vote-av-img">` ? `<img src="${UI.escape(dog.foto_url)}" alt="${UI.escape(dog.name)}" class="hdm-vote-av-img">`
: `<span class="hdm-vote-av-placeholder">${_esc(dog.name.charAt(0).toUpperCase())}</span>`; : `<span class="hdm-vote-av-placeholder">${UI.escape(dog.name.charAt(0).toUpperCase())}</span>`;
return ` return `
<div class="hdm-vote-card${isVoted ? ' hdm-vote-card--voted' : ''}" data-dog-id="${dog.id}"> <div class="hdm-vote-card${isVoted ? ' hdm-vote-card--voted' : ''}" data-dog-id="${dog.id}">
<div class="hdm-vote-av">${av}</div> <div class="hdm-vote-av">${av}</div>
<div class="hdm-vote-name">${_esc(dog.name)}</div> <div class="hdm-vote-name">${UI.escape(dog.name)}</div>
${dog.rasse ? `<div class="hdm-vote-rasse">${_esc(dog.rasse)}</div>` : ''} ${dog.rasse ? `<div class="hdm-vote-rasse">${UI.escape(dog.rasse)}</div>` : ''}
<button class="btn${isVoted ? ' btn-primary' : ' btn-secondary'} hdm-vote-btn" data-dog-id="${dog.id}"> <button class="btn${isVoted ? ' btn-primary' : ' btn-secondary'} hdm-vote-btn" data-dog-id="${dog.id}">
${isVoted ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg> Gewählt' : 'Abstimmen'} ${isVoted ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg> Gewählt' : 'Abstimmen'}
</button> </button>
@ -405,16 +405,16 @@ window.Page_movies = (() => {
? data.top.slice(0, 5).map((dog, i) => { ? data.top.slice(0, 5).map((dog, i) => {
const medal = ['🥇','🥈','🥉','4⃣','5⃣'][i] || `${i+1}.`; const medal = ['🥇','🥈','🥉','4⃣','5⃣'][i] || `${i+1}.`;
const av = dog.foto_url const av = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" alt="${_esc(dog.name)}" class="hdm-top-av-img">` ? `<img src="${UI.escape(dog.foto_url)}" alt="${UI.escape(dog.name)}" class="hdm-top-av-img">`
: `<span class="hdm-top-av-placeholder">${_esc(dog.name.charAt(0).toUpperCase())}</span>`; : `<span class="hdm-top-av-placeholder">${UI.escape(dog.name.charAt(0).toUpperCase())}</span>`;
const vorname = dog.besitzer_name ? _esc(dog.besitzer_name.split(' ')[0]) : ''; const vorname = dog.besitzer_name ? UI.escape(dog.besitzer_name.split(' ')[0]) : '';
return ` return `
<div class="hdm-top-entry"> <div class="hdm-top-entry">
<span class="hdm-top-medal">${medal}</span> <span class="hdm-top-medal">${medal}</span>
<div class="hdm-top-av">${av}</div> <div class="hdm-top-av">${av}</div>
<div class="hdm-top-info"> <div class="hdm-top-info">
<div class="hdm-top-name">${_esc(dog.name)}</div> <div class="hdm-top-name">${UI.escape(dog.name)}</div>
${dog.rasse ? `<div class="hdm-top-rasse">${_esc(dog.rasse)}</div>` : ''} ${dog.rasse ? `<div class="hdm-top-rasse">${UI.escape(dog.rasse)}</div>` : ''}
${vorname ? `<div class="hdm-top-besitzer">von ${vorname}</div>` : ''} ${vorname ? `<div class="hdm-top-besitzer">von ${vorname}</div>` : ''}
</div> </div>
<div class="hdm-top-stimmen">${dog.stimmen} <svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#star"></use></svg></div> <div class="hdm-top-stimmen">${dog.stimmen} <svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#star"></use></svg></div>
@ -427,7 +427,7 @@ window.Page_movies = (() => {
<div class="hdm-header"> <div class="hdm-header">
<div class="hdm-trophy">🏆</div> <div class="hdm-trophy">🏆</div>
<h2 class="hdm-title">Hund des Monats</h2> <h2 class="hdm-title">Hund des Monats</h2>
<div class="hdm-monat">${_esc(monthName)}</div> <div class="hdm-monat">${UI.escape(monthName)}</div>
</div> </div>
${voteSection} ${voteSection}
@ -465,15 +465,6 @@ window.Page_movies = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// HELPER // HELPER
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(str) {
if (!str && str !== 0) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// PUBLIC // PUBLIC
// ---------------------------------------------------------- // ----------------------------------------------------------

View file

@ -47,14 +47,6 @@ window.Page_notes = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// Hilfsfunktionen // Hilfsfunktionen
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _formatTime(isoStr) { function _formatTime(isoStr) {
if (!isoStr) return ''; if (!isoStr) return '';
@ -125,7 +117,7 @@ window.Page_notes = (() => {
.filter(([, items]) => items.length > 0) .filter(([, items]) => items.length > 0)
.map(([label, items]) => ` .map(([label, items]) => `
<div class="notes-group"> <div class="notes-group">
<div class="list-group-header">${_esc(label)}</div> <div class="list-group-header">${UI.escape(label)}</div>
${items.map(_noteCard).join('')} ${items.map(_noteCard).join('')}
</div> </div>
`).join(''); `).join('');
@ -166,9 +158,9 @@ window.Page_notes = (() => {
<div class="notes-filter-chips"> <div class="notes-filter-chips">
${RUBRIKEN.map(r => ` ${RUBRIKEN.map(r => `
<button class="notes-chip ${_filterType === r.type ? 'notes-chip--active' : ''}" <button class="notes-chip ${_filterType === r.type ? 'notes-chip--active' : ''}"
data-type="${_esc(r.type)}" data-type="${UI.escape(r.type)}"
style="${_filterType === r.type ? `--chip-color:${r.color}` : ''}"> style="${_filterType === r.type ? `--chip-color:${r.color}` : ''}">
${_esc(r.label)} ${UI.escape(r.label)}
</button> </button>
`).join('')} `).join('')}
</div> </div>
@ -178,7 +170,7 @@ window.Page_notes = (() => {
<div class="notes-search-wrap"> <div class="notes-search-wrap">
<svg class="ph-icon notes-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg> <svg class="ph-icon notes-search-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
<input id="notes-search" type="search" class="notes-search-input" <input id="notes-search" type="search" class="notes-search-input"
placeholder="Suche…" value="${_esc(_searchQ)}"> placeholder="Suche…" value="${UI.escape(_searchQ)}">
</div> </div>
<div class="notes-sort-btns"> <div class="notes-sort-btns">
<button class="notes-sort-btn ${_sortMode === 'newest' ? 'notes-sort-btn--active' : ''}" <button class="notes-sort-btn ${_sortMode === 'newest' ? 'notes-sort-btn--active' : ''}"
@ -292,11 +284,11 @@ window.Page_notes = (() => {
<button class="notes-ki-btn" id="notes-ki-analyse-btn" ${_kiLoading ? 'disabled' : ''}> <button class="notes-ki-btn" id="notes-ki-analyse-btn" ${_kiLoading ? 'disabled' : ''}>
${_kiLoading ? '<svg class="ph-icon" aria-hidden="true" style="animation:spin 1s linear infinite"><use href="/icons/phosphor.svg#spinner-gap"></use></svg> Analysiere…' : 'Analysieren'} ${_kiLoading ? '<svg class="ph-icon" aria-hidden="true" style="animation:spin 1s linear infinite"><use href="/icons/phosphor.svg#spinner-gap"></use></svg> Analysiere…' : 'Analysieren'}
</button> </button>
${_kiError ? `<div class="notes-ki-error"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning-circle"></use></svg> ${_esc(_kiError)}</div>` : ''} ${_kiError ? `<div class="notes-ki-error"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning-circle"></use></svg> ${UI.escape(_kiError)}</div>` : ''}
${_kiSuggestions ? ` ${_kiSuggestions ? `
<div class="notes-ki-suggestions"> <div class="notes-ki-suggestions">
<ul> <ul>
${_kiSuggestions.map(s => `<li>${_esc(s)}</li>`).join('')} ${_kiSuggestions.map(s => `<li>${UI.escape(s)}</li>`).join('')}
</ul> </ul>
</div> </div>
` : ''} ` : ''}
@ -326,10 +318,10 @@ window.Page_notes = (() => {
<div class="notes-card-top"> <div class="notes-card-top">
<span class="list-item-chip" style="--chip-color:${rb.color}"> <span class="list-item-chip" style="--chip-color:${rb.color}">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${rb.icon}"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${rb.icon}"></use></svg>
${_esc(rb.label)} ${UI.escape(rb.label)}
</span> </span>
${note.parent_label ${note.parent_label
? `<span class="notes-parent-label" title="${_esc(note.parent_label)}">${_esc(note.parent_label)}</span>` ? `<span class="notes-parent-label" title="${UI.escape(note.parent_label)}">${UI.escape(note.parent_label)}</span>`
: '' : ''
} }
<div class="list-item-actions notes-card-actions"> <div class="list-item-actions notes-card-actions">
@ -343,20 +335,20 @@ window.Page_notes = (() => {
</div> </div>
<!-- Notiztext --> <!-- Notiztext -->
<p class="list-item-text notes-card-text">${_esc(_truncate(note.text))}</p> <p class="list-item-text notes-card-text">${UI.escape(_truncate(note.text))}</p>
<!-- Micro-Badges --> <!-- Micro-Badges -->
${microBadges.length ? ` ${microBadges.length ? `
<div class="list-item-micro-badges"> <div class="list-item-micro-badges">
${microBadges.map(b => `<span class="list-item-micro-badge">${_esc(b)}</span>`).join('')} ${microBadges.map(b => `<span class="list-item-micro-badge">${UI.escape(b)}</span>`).join('')}
</div> </div>
` : ''} ` : ''}
<!-- Meta: Zeit + Ort --> <!-- Meta: Zeit + Ort -->
<div class="list-item-meta-row"> <div class="list-item-meta-row">
<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#clock"></use></svg> <svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#clock"></use></svg>
${_esc(_formatTime(note.updated_at || note.created_at))} ${UI.escape(_formatTime(note.updated_at || note.created_at))}
${hasLocation ? `<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#map-pin"></use></svg> ${_esc(note.location_name)}` : ''} ${hasLocation ? `<svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#map-pin"></use></svg> ${UI.escape(note.location_name)}` : ''}
</div> </div>
</div> </div>
`; `;
@ -514,7 +506,7 @@ window.Page_notes = (() => {
border-radius:999px;border:1.5px solid ${_selType===r.type ? r.color : 'var(--c-border)'}; border-radius:999px;border:1.5px solid ${_selType===r.type ? r.color : 'var(--c-border)'};
background:${_selType===r.type ? r.color+'22' : 'var(--c-surface-2)'}; background:${_selType===r.type ? r.color+'22' : 'var(--c-surface-2)'};
color:${_selType===r.type ? r.color : 'var(--c-text-secondary)'};cursor:pointer"> color:${_selType===r.type ? r.color : 'var(--c-text-secondary)'};cursor:pointer">
${_esc(r.label)} ${UI.escape(r.label)}
</button>`).join('')} </button>`).join('')}
</div> </div>
</div> </div>
@ -607,7 +599,7 @@ window.Page_notes = (() => {
<span style="display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs); <span style="display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs);
font-weight:var(--weight-semibold);padding:2px var(--space-2);border-radius:999px; font-weight:var(--weight-semibold);padding:2px var(--space-2);border-radius:999px;
background:${rb.color}22;color:${rb.color}"> background:${rb.color}22;color:${rb.color}">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${rb.icon}"></use></svg> ${_esc(rb.label)} <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${rb.icon}"></use></svg> ${UI.escape(rb.label)}
</span> </span>
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);margin:0"> <h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);margin:0">
Notiz bearbeiten Notiz bearbeiten
@ -625,7 +617,7 @@ window.Page_notes = (() => {
border-radius:var(--radius-md);font-size:var(--text-sm); border-radius:var(--radius-md);font-size:var(--text-sm);
font-family:var(--font-sans);background:var(--c-surface); font-family:var(--font-sans);background:var(--c-surface);
color:var(--c-text);resize:vertical;outline:none;line-height:1.5; color:var(--c-text);resize:vertical;outline:none;line-height:1.5;
box-sizing:border-box">${_esc(note.text)}</textarea> box-sizing:border-box">${UI.escape(note.text)}</textarea>
</div> </div>
${note.parent_type === 'training_session' ? ` ${note.parent_type === 'training_session' ? `

View file

@ -276,7 +276,7 @@ window.Page_onboarding = (() => {
${dogName ? ` ${dogName ? `
<p style="font-size:var(--text-base);color:var(--c-text-secondary); <p style="font-size:var(--text-base);color:var(--c-text-secondary);
line-height:1.6;margin:0 0 var(--space-3)"> line-height:1.6;margin:0 0 var(--space-3)">
<strong>${_esc(dogName)}</strong> ist jetzt in Ban Yaro. <strong>${UI.escape(dogName)}</strong> ist jetzt in Ban Yaro.
Du kannst jetzt Einträge im Tagebuch anlegen, die Gesundheit pflegen Du kannst jetzt Einträge im Tagebuch anlegen, die Gesundheit pflegen
und viele weitere Funktionen nutzen. und viele weitere Funktionen nutzen.
</p> </p>
@ -416,7 +416,7 @@ window.Page_onboarding = (() => {
} }
App.renderDogSwitcher(); App.renderDogSwitcher();
UI.toast.success(`${_esc(dog.name)} wurde angelegt!`); UI.toast.success(`${UI.escape(dog.name)} wurde angelegt!`);
_step = 3; _step = 3;
_render(); _render();
@ -452,9 +452,6 @@ window.Page_onboarding = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// HELPER // HELPER
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(s) {
return UI.escape(s || '');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// PUBLIC // PUBLIC

View file

@ -78,7 +78,7 @@ window.Page_partner_profil = (() => {
background:var(--c-surface-2);display:flex;align-items:center;justify-content:center; background:var(--c-surface-2);display:flex;align-items:center;justify-content:center;
overflow:hidden;flex-shrink:0"> overflow:hidden;flex-shrink:0">
${p.logo_url ${p.logo_url
? `<img src="${_esc(p.logo_url)}" style="width:100%;height:100%;object-fit:contain">` ? `<img src="${UI.escape(p.logo_url)}" style="width:100%;height:100%;object-fit:contain">`
: `<svg class="ph-icon" style="width:32px;height:32px;opacity:.3"><use href="/icons/phosphor.svg#image"></use></svg>`} : `<svg class="ph-icon" style="width:32px;height:32px;opacity:.3"><use href="/icons/phosphor.svg#image"></use></svg>`}
</div> </div>
<div> <div>
@ -102,30 +102,30 @@ window.Page_partner_profil = (() => {
<label class="form-label">Anzeigename *</label> <label class="form-label">Anzeigename *</label>
<input class="form-control" name="display_name" type="text" maxlength="60" required <input class="form-control" name="display_name" type="text" maxlength="60" required
placeholder="z. B. Hundeblog Musterfrau" placeholder="z. B. Hundeblog Musterfrau"
value="${_esc(p.display_name || '')}"> value="${UI.escape(p.display_name || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Kurzslogan <span style="font-weight:400;color:var(--c-text-muted)">(max. 80 Zeichen)</span></label> <label class="form-label">Kurzslogan <span style="font-weight:400;color:var(--c-text-muted)">(max. 80 Zeichen)</span></label>
<input class="form-control" name="tagline" type="text" maxlength="80" <input class="form-control" name="tagline" type="text" maxlength="80"
placeholder="z. B. Hundetrainerin · 15.000 Follower auf Instagram" placeholder="z. B. Hundetrainerin · 15.000 Follower auf Instagram"
value="${_esc(p.tagline || '')}"> value="${UI.escape(p.tagline || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Über dich / euer Kanal</label> <label class="form-label">Über dich / euer Kanal</label>
<textarea class="form-control" name="bio" rows="4" maxlength="500" <textarea class="form-control" name="bio" rows="4" maxlength="500"
placeholder="Wer bist du, was machst du, was verbindet dich mit Hunden?">${_esc(p.bio || p.pp_bio || '')}</textarea> placeholder="Wer bist du, was machst du, was verbindet dich mit Hunden?">${UI.escape(p.bio || p.pp_bio || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Website</label> <label class="form-label">Website</label>
<input class="form-control" name="website" type="url" <input class="form-control" name="website" type="url"
placeholder="https://deine-seite.de" placeholder="https://deine-seite.de"
value="${_esc(p.website || '')}"> value="${UI.escape(p.website || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Instagram</label> <label class="form-label">Instagram</label>
<input class="form-control" name="instagram" type="text" <input class="form-control" name="instagram" type="text"
placeholder="@deinkanal" placeholder="@deinkanal"
value="${_esc(p.instagram || '')}"> value="${UI.escape(p.instagram || '')}">
</div> </div>
<button type="submit" class="btn btn-secondary btn-sm" style="align-self:flex-start"> <button type="submit" class="btn btn-secondary btn-sm" style="align-self:flex-start">
Texte speichern Texte speichern
@ -154,11 +154,11 @@ window.Page_partner_profil = (() => {
<div style="position:relative;aspect-ratio:1;border-radius:var(--radius-md);overflow:hidden; <div style="position:relative;aspect-ratio:1;border-radius:var(--radius-md);overflow:hidden;
background:var(--c-surface-2)"> background:var(--c-surface-2)">
${isVid ${isVid
? `<video src="${_esc(url)}" style="width:100%;height:100%;object-fit:cover" muted playsinline loop ? `<video src="${UI.escape(url)}" style="width:100%;height:100%;object-fit:cover" muted playsinline loop
onmouseenter="this.play()" onmouseleave="this.pause()"></video> onmouseenter="this.play()" onmouseleave="this.pause()"></video>
<div style="position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.55); <div style="position:absolute;bottom:4px;left:4px;background:rgba(0,0,0,.55);
border-radius:4px;padding:1px 5px;font-size:10px;color:#fff"> Video</div>` border-radius:4px;padding:1px 5px;font-size:10px;color:#fff"> Video</div>`
: `<img src="${_esc(url)}" style="width:100%;height:100%;object-fit:cover">`} : `<img src="${UI.escape(url)}" style="width:100%;height:100%;object-fit:cover">`}
<button class="pp-photo-del" data-idx="${i}" <button class="pp-photo-del" data-idx="${i}"
style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,.6); style="position:absolute;top:4px;right:4px;background:rgba(0,0,0,.6);
border:none;border-radius:50%;width:24px;height:24px;cursor:pointer; border:none;border-radius:50%;width:24px;height:24px;cursor:pointer;
@ -197,7 +197,7 @@ window.Page_partner_profil = (() => {
try { try {
const r = await API.upload('/partner/my-profile/logo', fd); const r = await API.upload('/partner/my-profile/logo', fd);
el.querySelector('#pp-logo-preview').innerHTML = el.querySelector('#pp-logo-preview').innerHTML =
`<img src="${_esc(r.logo_url)}" style="width:100%;height:100%;object-fit:contain">`; `<img src="${UI.escape(r.logo_url)}" style="width:100%;height:100%;object-fit:contain">`;
_profile = { ..._profile, logo_url: r.logo_url }; _profile = { ..._profile, logo_url: r.logo_url };
UI.toast.success('Logo gespeichert.'); UI.toast.success('Logo gespeichert.');
} catch (err) { UI.toast.error(err.message); } } catch (err) { UI.toast.error(err.message); }
@ -268,10 +268,6 @@ window.Page_partner_profil = (() => {
</div>`; </div>`;
} }
function _esc(s) {
return String(s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
return { init, refresh, onDogChange }; return { init, refresh, onDogChange };

View file

@ -77,10 +77,10 @@ window.Page_partner = (() => {
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:${grad}"></div> <div style="position:absolute;top:0;left:0;right:0;height:3px;background:${grad}"></div>
<div style="display:flex;align-items:center;gap:var(--space-3)"> <div style="display:flex;align-items:center;gap:var(--space-3)">
${p.logo_url ${p.logo_url
? `<img src="${_esc(p.logo_url)}" alt="" ? `<img src="${UI.escape(p.logo_url)}" alt=""
style="width:56px;height:56px;border-radius:var(--radius-md);object-fit:contain;flex-shrink:0;background:var(--c-surface-2);padding:4px">` style="width:56px;height:56px;border-radius:var(--radius-md);object-fit:contain;flex-shrink:0;background:var(--c-surface-2);padding:4px">`
: p.avatar_url : p.avatar_url
? `<img src="${_esc(p.avatar_url)}" alt="" ? `<img src="${UI.escape(p.avatar_url)}" alt=""
style="width:56px;height:56px;border-radius:50%;object-fit:cover;flex-shrink:0">` style="width:56px;height:56px;border-radius:50%;object-fit:cover;flex-shrink:0">`
: `<div style="width:56px;height:56px;border-radius:50%;flex-shrink:0; : `<div style="width:56px;height:56px;border-radius:50%;flex-shrink:0;
background:${grad};display:flex;align-items:center; background:${grad};display:flex;align-items:center;
@ -89,19 +89,19 @@ window.Page_partner = (() => {
</div>` </div>`
} }
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:700;font-size:var(--text-base)">${_esc(p.display_name || p.name)}</div> <div style="font-weight:700;font-size:var(--text-base)">${UI.escape(p.display_name || p.name)}</div>
${p.tagline ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:1px">${_esc(p.tagline)}</div>` : ''} ${p.tagline ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:1px">${UI.escape(p.tagline)}</div>` : ''}
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-1)"> <div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-1)">
${p.website ? `<a href="${_esc(p.website)}" target="_blank" rel="noopener" ${p.website ? `<a href="${UI.escape(p.website)}" target="_blank" rel="noopener"
style="font-size:var(--text-xs);color:var(--c-primary)"> style="font-size:var(--text-xs);color:var(--c-primary)">
🌐 ${_esc(p.website.replace(/^https?:\/\//, ''))}</a>` : ''} 🌐 ${UI.escape(p.website.replace(/^https?:\/\//, ''))}</a>` : ''}
${p.instagram ? `<span class="text-xs-muted">📸 ${_esc(p.instagram)}</span>` : ''} ${p.instagram ? `<span class="text-xs-muted">📸 ${UI.escape(p.instagram)}</span>` : ''}
</div> </div>
</div> </div>
</div> </div>
${p.pp_bio || p.bio ? `<p style="margin:var(--space-3) 0 0;font-size:var(--text-sm); ${p.pp_bio || p.bio ? `<p style="margin:var(--space-3) 0 0;font-size:var(--text-sm);
color:var(--c-text-secondary);line-height:1.5"> color:var(--c-text-secondary);line-height:1.5">
${_esc(p.pp_bio || p.bio)} ${UI.escape(p.pp_bio || p.bio)}
</p>` : ''} </p>` : ''}
${p.photos?.length ? ` ${p.photos?.length ? `
<div style="display:grid;grid-template-columns:repeat(${Math.min(p.photos.length,3)},1fr); <div style="display:grid;grid-template-columns:repeat(${Math.min(p.photos.length,3)},1fr);
@ -109,9 +109,9 @@ window.Page_partner = (() => {
${p.photos.slice(0,3).map(url => { ${p.photos.slice(0,3).map(url => {
const isVid = url.endsWith('.mp4') || url.endsWith('.webm'); const isVid = url.endsWith('.mp4') || url.endsWith('.webm');
return isVid return isVid
? `<video src="${_esc(url)}" style="width:100%;aspect-ratio:1;object-fit:cover" ? `<video src="${UI.escape(url)}" style="width:100%;aspect-ratio:1;object-fit:cover"
muted playsinline loop autoplay></video>` muted playsinline loop autoplay></video>`
: `<img src="${_esc(url)}" style="width:100%;aspect-ratio:1;object-fit:cover">`; : `<img src="${UI.escape(url)}" style="width:100%;aspect-ratio:1;object-fit:cover">`;
}).join('')} }).join('')}
</div>` : ''} </div>` : ''}
</div> </div>
@ -143,10 +143,6 @@ window.Page_partner = (() => {
`; `;
} }
function _esc(s) {
return String(s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
return { init, refresh, onDogChange }; return { init, refresh, onDogChange };

View file

@ -15,11 +15,6 @@ window.Page_playdate = (() => {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Helpers // Helpers
// ------------------------------------------------------------------ // ------------------------------------------------------------------
function _esc(s) {
return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function _fmtDate(iso) { function _fmtDate(iso) {
if (!iso) return ''; if (!iso) return '';
const d = new Date(iso.replace(' ', 'T')); const d = new Date(iso.replace(' ', 'T'));
@ -27,9 +22,9 @@ window.Page_playdate = (() => {
} }
function _dogAvatar(foto_url, name, size = 48) { function _dogAvatar(foto_url, name, size = 48) {
const initials = _esc((name || '?').charAt(0).toUpperCase()); const initials = UI.escape((name || '?').charAt(0).toUpperCase());
if (foto_url) { if (foto_url) {
return `<img src="${_esc(foto_url)}" alt="${initials}" return `<img src="${UI.escape(foto_url)}" alt="${initials}"
style="width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;display:block;" style="width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;display:block;"
onerror="this.outerHTML='<div style=\'width:${size}px;height:${size}px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;font-size:${Math.round(size*0.45)}px;font-weight:700;color:var(--c-primary);\'>${initials}</div>'">`; onerror="this.outerHTML='<div style=\'width:${size}px;height:${size}px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;font-size:${Math.round(size*0.45)}px;font-weight:700;color:var(--c-primary);\'>${initials}</div>'">`;
} }
@ -250,29 +245,29 @@ window.Page_playdate = (() => {
${_dogAvatar(d.foto_url, d.dog_name, 56)} ${_dogAvatar(d.foto_url, d.dog_name, 56)}
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base); <div style="font-weight:var(--weight-semibold);font-size:var(--text-base);
color:var(--c-text)">${_esc(d.dog_name)}</div> color:var(--c-text)">${UI.escape(d.dog_name)}</div>
${d.rasse ? `<div class="text-sm-secondary">${_esc(d.rasse)}</div>` : ''} ${d.rasse ? `<div class="text-sm-secondary">${UI.escape(d.rasse)}</div>` : ''}
${d.alter ? `<div class="text-xs-muted">${_esc(d.alter)}</div>` : ''} ${d.alter ? `<div class="text-xs-muted">${UI.escape(d.alter)}</div>` : ''}
</div> </div>
</div> </div>
<div style="display:flex;gap:var(--space-3);margin-bottom:var(--space-3);flex-wrap:wrap"> <div style="display:flex;gap:var(--space-3);margin-bottom:var(--space-3);flex-wrap:wrap">
<span style="display:flex;align-items:center;gap:4px;font-size:var(--text-xs);color:var(--c-text-secondary)"> <span style="display:flex;align-items:center;gap:4px;font-size:var(--text-xs);color:var(--c-text-secondary)">
${UI.icon('map-pin')} ${UI.icon('map-pin')}
${d.ort_name ? _esc(d.ort_name) + ' · ' : ''}${d.entfernung_km} km entfernt ${d.ort_name ? UI.escape(d.ort_name) + ' · ' : ''}${d.entfernung_km} km entfernt
</span> </span>
${d.geschlecht ? `<span class="text-xs-muted">${_esc(d.geschlecht)}</span>` : ''} ${d.geschlecht ? `<span class="text-xs-muted">${UI.escape(d.geschlecht)}</span>` : ''}
</div> </div>
${d.beschreibung ? ` ${d.beschreibung ? `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary); <p style="font-size:var(--text-sm);color:var(--c-text-secondary);
margin:0 0 var(--space-3);line-height:1.5"> margin:0 0 var(--space-3);line-height:1.5">
${_esc(d.beschreibung)} ${UI.escape(d.beschreibung)}
</p>` : ''} </p>` : ''}
<button class="btn btn-primary btn-sm playdate-anfrage-btn" <button class="btn btn-primary btn-sm playdate-anfrage-btn"
data-dog-id="${d.dog_id}" data-dog-id="${d.dog_id}"
data-dog-name="${_esc(d.dog_name)}"> data-dog-name="${UI.escape(d.dog_name)}">
${UI.icon('paw-print')} Spielkamerad anfragen ${UI.icon('paw-print')} Spielkamerad anfragen
</button> </button>
</div> </div>
@ -393,8 +388,8 @@ window.Page_playdate = (() => {
<div style="display:flex;gap:var(--space-3);align-items:center;margin-bottom:var(--space-3)"> <div style="display:flex;gap:var(--space-3);align-items:center;margin-bottom:var(--space-3)">
${_dogAvatar(dog.foto_url, dog.name, 44)} ${_dogAvatar(dog.foto_url, dog.name, 44)}
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:var(--weight-semibold);color:var(--c-text)">${_esc(dog.name)}</div> <div style="font-weight:var(--weight-semibold);color:var(--c-text)">${UI.escape(dog.name)}</div>
${dog.rasse ? `<div class="text-xs-secondary">${_esc(dog.rasse)}</div>` : ''} ${dog.rasse ? `<div class="text-xs-secondary">${UI.escape(dog.rasse)}</div>` : ''}
</div> </div>
<span style="font-size:var(--text-xs);font-weight:600; <span style="font-size:var(--text-xs);font-weight:600;
padding:2px 10px;border-radius:999px; padding:2px 10px;border-radius:999px;
@ -407,12 +402,12 @@ window.Page_playdate = (() => {
${isAktiv ? ` ${isAktiv ? `
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)"> <div style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">
${UI.icon('map-pin')} ${UI.icon('map-pin')}
${listing.ort_name ? _esc(listing.ort_name) + ' · ' : ''} ${listing.ort_name ? UI.escape(listing.ort_name) + ' · ' : ''}
Radius: ${listing.radius_km} km Radius: ${listing.radius_km} km
</div> </div>
${listing.beschreibung ? ` ${listing.beschreibung ? `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary); <p style="font-size:var(--text-sm);color:var(--c-text-secondary);
margin:0 0 var(--space-3);line-height:1.5">${_esc(listing.beschreibung)}</p>` : ''} margin:0 0 var(--space-3);line-height:1.5">${UI.escape(listing.beschreibung)}</p>` : ''}
` : ` ` : `
<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0 0 var(--space-3)"> <p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0 0 var(--space-3)">
Noch kein Inserat trage dich ein, damit andere dich finden können. Noch kein Inserat trage dich ein, damit andere dich finden können.
@ -445,7 +440,7 @@ window.Page_playdate = (() => {
<div class="flex-gap-2"> <div class="flex-gap-2">
<input type="text" id="listing-ort" class="form-control" <input type="text" id="listing-ort" class="form-control"
placeholder="z.B. München" placeholder="z.B. München"
value="${_esc(existing?.ort_name || '')}"> value="${UI.escape(existing?.ort_name || '')}">
<button type="button" class="btn btn-ghost btn-sm" id="listing-gps-btn" <button type="button" class="btn btn-ghost btn-sm" id="listing-gps-btn"
title="GPS-Standort ermitteln"> title="GPS-Standort ermitteln">
${UI.icon('crosshair')} ${UI.icon('crosshair')}
@ -472,7 +467,7 @@ window.Page_playdate = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Beschreibung (optional)</label> <label class="form-label">Beschreibung (optional)</label>
<textarea id="listing-beschreibung" class="form-control" rows="3" maxlength="400" <textarea id="listing-beschreibung" class="form-control" rows="3" maxlength="400"
placeholder="Erzähl etwas über deinen Hund und was ihr sucht…">${_esc(existing?.beschreibung || '')}</textarea> placeholder="Erzähl etwas über deinen Hund und was ihr sucht…">${UI.escape(existing?.beschreibung || '')}</textarea>
</div> </div>
</form> </form>
`, `,
@ -635,11 +630,11 @@ window.Page_playdate = (() => {
<div style="display:flex;gap:var(--space-3);align-items:flex-start;margin-bottom:var(--space-3)"> <div style="display:flex;gap:var(--space-3);align-items:flex-start;margin-bottom:var(--space-3)">
${_dogAvatar(r.from_dog_foto, r.from_dog_name, 44)} ${_dogAvatar(r.from_dog_foto, r.from_dog_name, 44)}
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:var(--weight-semibold);color:var(--c-text)">${_esc(r.from_dog_name)}</div> <div style="font-weight:var(--weight-semibold);color:var(--c-text)">${UI.escape(r.from_dog_name)}</div>
<div class="text-xs-secondary"> <div class="text-xs-secondary">
${r.from_dog_rasse ? _esc(r.from_dog_rasse) + ' · ' : ''} ${r.from_dog_rasse ? UI.escape(r.from_dog_rasse) + ' · ' : ''}
${r.alter ? _esc(r.alter) + ' · ' : ''} ${r.alter ? UI.escape(r.alter) + ' · ' : ''}
von ${_esc(r.from_user_name)} von ${UI.escape(r.from_user_name)}
</div> </div>
<div class="text-xs-muted">${_fmtDate(r.created_at)}</div> <div class="text-xs-muted">${_fmtDate(r.created_at)}</div>
</div> </div>
@ -651,7 +646,7 @@ window.Page_playdate = (() => {
background:var(--c-surface-2);border-radius:var(--radius-md); background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-2) var(--space-3);margin-bottom:var(--space-3); padding:var(--space-2) var(--space-3);margin-bottom:var(--space-3);
line-height:1.5"> line-height:1.5">
"${_esc(r.nachricht)}" "${UI.escape(r.nachricht)}"
</div>` : ''} </div>` : ''}
${isPending ? ` ${isPending ? `
@ -680,10 +675,10 @@ window.Page_playdate = (() => {
<div style="display:flex;gap:var(--space-3);align-items:flex-start;margin-bottom:var(--space-3)"> <div style="display:flex;gap:var(--space-3);align-items:flex-start;margin-bottom:var(--space-3)">
${_dogAvatar(r.to_dog_foto, r.to_dog_name, 44)} ${_dogAvatar(r.to_dog_foto, r.to_dog_name, 44)}
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:var(--weight-semibold);color:var(--c-text)">${_esc(r.to_dog_name)}</div> <div style="font-weight:var(--weight-semibold);color:var(--c-text)">${UI.escape(r.to_dog_name)}</div>
<div class="text-xs-secondary"> <div class="text-xs-secondary">
${r.to_dog_rasse ? _esc(r.to_dog_rasse) + ' · ' : ''} ${r.to_dog_rasse ? UI.escape(r.to_dog_rasse) + ' · ' : ''}
von ${_esc(r.to_user_name)} von ${UI.escape(r.to_user_name)}
</div> </div>
<div class="text-xs-muted">${_fmtDate(r.created_at)}</div> <div class="text-xs-muted">${_fmtDate(r.created_at)}</div>
</div> </div>
@ -692,7 +687,7 @@ window.Page_playdate = (() => {
${r.nachricht ? ` ${r.nachricht ? `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3)"> <p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3)">
"${_esc(r.nachricht)}" "${UI.escape(r.nachricht)}"
</p>` : ''} </p>` : ''}
${r.status === 'accepted' ? ` ${r.status === 'accepted' ? `

View file

@ -106,14 +106,6 @@ window.Page_reise = (() => {
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Helpers // Helpers
// ------------------------------------------------------------------ // ------------------------------------------------------------------
function _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _loadChecked() { function _loadChecked() {
try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); }
@ -212,16 +204,16 @@ window.Page_reise = (() => {
const done = !!checked[key]; const done = !!checked[key];
if (_editMode) { if (_editMode) {
return `<div class="reise-check-row" style="justify-content:space-between"> return `<div class="reise-check-row" style="justify-content:space-between">
<span style="flex:1;color:var(--c-text)">${_esc(item)}</span> <span style="flex:1;color:var(--c-text)">${UI.escape(item)}</span>
<button class="reise-del-btn" data-hide="${_esc(key)}" <button class="reise-del-btn" data-hide="${UI.escape(key)}"
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0"> style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg> <svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
</button> </button>
</div>`; </div>`;
} }
return `<label class="reise-check-row${done ? ' done' : ''}"> return `<label class="reise-check-row${done ? ' done' : ''}">
<input type="checkbox" class="reise-cb" data-key="${_esc(key)}" ${done ? 'checked' : ''}> <input type="checkbox" class="reise-cb" data-key="${UI.escape(key)}" ${done ? 'checked' : ''}>
<span>${_esc(item)}</span> <span>${UI.escape(item)}</span>
</label>`; </label>`;
}).join(''); }).join('');
@ -230,27 +222,27 @@ window.Page_reise = (() => {
const done = !!checked[key]; const done = !!checked[key];
if (_editMode) { if (_editMode) {
return `<div class="reise-check-row" style="justify-content:space-between"> return `<div class="reise-check-row" style="justify-content:space-between">
<span style="flex:1;color:var(--c-primary)">${_esc(item)}</span> <span style="flex:1;color:var(--c-primary)">${UI.escape(item)}</span>
<button class="reise-del-custom-btn" data-cat="${_esc(cat.key)}" data-idx="${i}" <button class="reise-del-custom-btn" data-cat="${UI.escape(cat.key)}" data-idx="${i}"
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0"> style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg> <svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
</button> </button>
</div>`; </div>`;
} }
return `<label class="reise-check-row${done ? ' done' : ''}"> return `<label class="reise-check-row${done ? ' done' : ''}">
<input type="checkbox" class="reise-cb" data-key="${_esc(key)}" ${done ? 'checked' : ''}> <input type="checkbox" class="reise-cb" data-key="${UI.escape(key)}" ${done ? 'checked' : ''}>
<span class="text-primary">${_esc(item)}</span> <span class="text-primary">${UI.escape(item)}</span>
</label>`; </label>`;
}).join(''); }).join('');
const addRow = _editMode ? ` const addRow = _editMode ? `
<div style="padding:var(--space-2) 0;border-top:1px dashed var(--c-border);margin-top:4px"> <div style="padding:var(--space-2) 0;border-top:1px dashed var(--c-border);margin-top:4px">
<div style="display:flex;gap:8px;align-items:center"> <div style="display:flex;gap:8px;align-items:center">
<input class="reise-add-input" data-cat="${_esc(cat.key)}" <input class="reise-add-input" data-cat="${UI.escape(cat.key)}"
style="flex:1;padding:8px 10px;border-radius:8px;border:1px solid var(--c-border); style="flex:1;padding:8px 10px;border-radius:8px;border:1px solid var(--c-border);
background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm)" background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm)"
placeholder="Eigenes Item hinzufügen…"> placeholder="Eigenes Item hinzufügen…">
<button class="reise-add-btn btn btn-primary" data-cat="${_esc(cat.key)}" <button class="reise-add-btn btn btn-primary" data-cat="${UI.escape(cat.key)}"
style="padding:8px 12px;flex-shrink:0;font-size:var(--text-sm)"> style="padding:8px 12px;flex-shrink:0;font-size:var(--text-sm)">
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#plus"></use></svg> <svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#plus"></use></svg>
</button> </button>
@ -261,9 +253,9 @@ window.Page_reise = (() => {
<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border); <div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border);
display:flex;align-items:center;gap:var(--space-2)"> display:flex;align-items:center;gap:var(--space-2)">
<svg class="ph-icon text-primary" aria-hidden="true"> <svg class="ph-icon text-primary" aria-hidden="true">
<use href="/icons/phosphor.svg#${_esc(cat.icon)}"></use> <use href="/icons/phosphor.svg#${UI.escape(cat.icon)}"></use>
</svg> </svg>
<span style="font-weight:var(--weight-semibold)">${_esc(cat.label)}</span> <span style="font-weight:var(--weight-semibold)">${UI.escape(cat.label)}</span>
</div> </div>
<div style="padding:var(--space-2) var(--space-4)"> <div style="padding:var(--space-2) var(--space-4)">
${stdRows}${customRows}${addRow} ${stdRows}${customRows}${addRow}
@ -389,10 +381,10 @@ window.Page_reise = (() => {
<span style="font-size:2rem;line-height:1">${l.flag}</span> <span style="font-size:2rem;line-height:1">${l.flag}</span>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:var(--weight-semibold);margin-bottom:var(--space-1)"> <div style="font-weight:var(--weight-semibold);margin-bottom:var(--space-1)">
${_esc(l.name)} ${UI.escape(l.name)}
</div> </div>
<div class="text-sm-secondary"> <div class="text-sm-secondary">
${_esc(l.regel)} ${UI.escape(l.regel)}
</div> </div>
</div> </div>
${l.warn ? `<svg class="ph-icon" style="color:var(--c-warning,#f59e0b);flex-shrink:0;width:20px;height:20px" aria-hidden="true"> ${l.warn ? `<svg class="ph-icon" style="color:var(--c-warning,#f59e0b);flex-shrink:0;width:20px;height:20px" aria-hidden="true">
@ -425,9 +417,9 @@ window.Page_reise = (() => {
<div style="display:flex;align-items:flex-start;gap:var(--space-3); <div style="display:flex;align-items:flex-start;gap:var(--space-3);
padding:var(--space-3) 0;border-bottom:1px solid var(--c-surface-2)"> padding:var(--space-3) 0;border-bottom:1px solid var(--c-surface-2)">
<svg class="ph-icon" style="color:var(--c-danger,#ef4444);flex-shrink:0;margin-top:1px" aria-hidden="true"> <svg class="ph-icon" style="color:var(--c-danger,#ef4444);flex-shrink:0;margin-top:1px" aria-hidden="true">
<use href="/icons/phosphor.svg#${_esc(m.icon)}"></use> <use href="/icons/phosphor.svg#${UI.escape(m.icon)}"></use>
</svg> </svg>
<span style="font-size:var(--text-sm);color:var(--c-text)">${_esc(m.text)}</span> <span style="font-size:var(--text-sm);color:var(--c-text)">${UI.escape(m.text)}</span>
</div>`).join(''); </div>`).join('');
el.innerHTML = ` el.innerHTML = `

View file

@ -509,9 +509,9 @@ window.Page_settings = (() => {
// Avatar: Bild oder Buchstabe // Avatar: Bild oder Buchstabe
const avatarInner = u.avatar_url const avatarInner = u.avatar_url
? `<img src="${_esc(u.avatar_url)}" alt="Avatar" ? `<img src="${UI.escape(u.avatar_url)}" alt="Avatar"
style="width:56px;height:56px;border-radius:50%;object-fit:cover;display:block">` style="width:56px;height:56px;border-radius:50%;object-fit:cover;display:block">`
: _esc(u.name.charAt(0).toUpperCase()); : UI.escape(u.name.charAt(0).toUpperCase());
// Mitglied seit // Mitglied seit
const memberSince = (() => { const memberSince = (() => {
@ -547,9 +547,9 @@ window.Page_settings = (() => {
<input type="file" id="settings-avatar-input" accept="image/*" <input type="file" id="settings-avatar-input" accept="image/*"
class="hidden"> class="hidden">
<div> <div>
<div style="font-weight:700;font-size:var(--text-lg)">${_esc(u.name)}</div> <div style="font-weight:700;font-size:var(--text-lg)">${UI.escape(u.name)}</div>
<div style="display:flex;align-items:center;gap:var(--space-2);color:var(--c-text-secondary);font-size:var(--text-sm)"> <div style="display:flex;align-items:center;gap:var(--space-2);color:var(--c-text-secondary);font-size:var(--text-sm)">
${_esc(u.email)} ${UI.escape(u.email)}
${u.email_verified ${u.email_verified
? `<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px;color:#22c55e" title="Bestätigt"><use href="/icons/phosphor.svg#check-circle"></use></svg>` ? `<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px;color:#22c55e" title="Bestätigt"><use href="/icons/phosphor.svg#check-circle"></use></svg>`
: `<span id="settings-verify-chip" : `<span id="settings-verify-chip"
@ -600,26 +600,26 @@ window.Page_settings = (() => {
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)"> <div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">
${memberSince ${memberSince
? `<div class="text-sm-secondary"> ? `<div class="text-sm-secondary">
Mitglied seit ${_esc(memberSince)} Mitglied seit ${UI.escape(memberSince)}
</div>` </div>`
: ''} : ''}
${u.bio ${u.bio
? `<div class="text-sm">${_esc(u.bio)}</div>` ? `<div class="text-sm">${UI.escape(u.bio)}</div>`
: ''} : ''}
${u.wohnort ${u.wohnort
? `<div class="text-sm-secondary"> ? `<div class="text-sm-secondary">
📍 ${_esc(u.wohnort)} 📍 ${UI.escape(u.wohnort)}
</div>` </div>`
: ''} : ''}
${u.erfahrung && erfahrungLabel[u.erfahrung] ${u.erfahrung && erfahrungLabel[u.erfahrung]
? `<div class="text-sm-secondary"> ? `<div class="text-sm-secondary">
${_esc(erfahrungLabel[u.erfahrung])} ${UI.escape(erfahrungLabel[u.erfahrung])}
</div>` </div>`
: ''} : ''}
${u.social_link ${u.social_link
? `<div class="text-sm"> ? `<div class="text-sm">
<a href="${_esc(u.social_link)}" target="_blank" rel="noopener" <a href="${UI.escape(u.social_link)}" target="_blank" rel="noopener"
class="text-primary">${_esc(u.social_link)}</a> class="text-primary">${UI.escape(u.social_link)}</a>
</div>` </div>`
: ''} : ''}
${!u.bio && !u.wohnort && !u.erfahrung && !u.social_link ${!u.bio && !u.wohnort && !u.erfahrung && !u.social_link
@ -877,17 +877,17 @@ window.Page_settings = (() => {
if (!el || !dogs.length) return; if (!el || !dogs.length) return;
el.innerHTML = dogs.map(d => { el.innerHTML = dogs.map(d => {
const av = d.foto_url const av = d.foto_url
? `<img src="${_esc(d.foto_url)}" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0">` ? `<img src="${UI.escape(d.foto_url)}" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0">`
: `<div style="width:36px;height:36px;border-radius:50%;background:var(--c-surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0"> : `<div style="width:36px;height:36px;border-radius:50%;background:var(--c-surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg class="ph-icon" style="width:18px;height:18px;color:var(--c-text-muted)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg> <svg class="ph-icon" style="width:18px;height:18px;color:var(--c-text-muted)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
</div>`; </div>`;
const jahr = d.verstorben_am ? d.verstorben_am.slice(0, 4) : ''; const jahr = d.verstorben_am ? d.verstorben_am.slice(0, 4) : '';
return ` return `
<div class="sidebar-item settings-erinnerung-btn" data-dog-id="${d.id}" data-dog-name="${_esc(d.name)}" <div class="sidebar-item settings-erinnerung-btn" data-dog-id="${d.id}" data-dog-name="${UI.escape(d.name)}"
style="padding:var(--space-3) var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border);cursor:pointer"> style="padding:var(--space-3) var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border);cursor:pointer">
${av} ${av}
<div style="display:flex;flex-direction:column;gap:1px;flex:1;min-width:0"> <div style="display:flex;flex-direction:column;gap:1px;flex:1;min-width:0">
<span style="font-weight:600;font-size:var(--text-sm)">${_esc(d.name)}</span> <span style="font-weight:600;font-size:var(--text-sm)">${UI.escape(d.name)}</span>
<span class="text-xs-muted"> <span class="text-xs-muted">
<svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg> <svg class="ph-icon" style="width:11px;height:11px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg>
Erinnerungen${jahr ? ' · ' + jahr : ''} Erinnerungen${jahr ? ' · ' + jahr : ''}
@ -1034,7 +1034,7 @@ window.Page_settings = (() => {
// Alle Stufen als kleine Punkte // Alle Stufen als kleine Punkte
const dots = (cat.alle_stufen || []).map(s => const dots = (cat.alle_stufen || []).map(s =>
`<div title="${_esc(s.name)}" style="width:8px;height:8px;border-radius:50%; `<div title="${UI.escape(s.name)}" style="width:8px;height:8px;border-radius:50%;
background:${s.earned ? s.color : 'var(--c-border)'}"></div>` background:${s.earned ? s.color : 'var(--c-border)'}"></div>`
).join(''); ).join('');
@ -1046,7 +1046,7 @@ window.Page_settings = (() => {
// Fortschrittsbalken // Fortschrittsbalken
const progressBar = nxt ? ` const progressBar = nxt ? `
<div style="font-size:10px;color:var(--c-text-muted);margin-top:4px"> <div style="font-size:10px;color:var(--c-text-muted);margin-top:4px">
${val}${cat.einheit} / ${nxt.schwelle}${cat.einheit} ${_esc(nxt.name)} ${val}${cat.einheit} / ${nxt.schwelle}${cat.einheit} ${UI.escape(nxt.name)}
</div> </div>
<div style="height:4px;background:var(--c-border);border-radius:2px;margin-top:4px;overflow:hidden"> <div style="height:4px;background:var(--c-border);border-radius:2px;margin-top:4px;overflow:hidden">
<div style="height:100%;width:${cat.progress}%;background:${nxt.color};border-radius:2px;transition:width .4s"></div> <div style="height:100%;width:${cat.progress}%;background:${nxt.color};border-radius:2px;transition:width .4s"></div>
@ -1061,9 +1061,9 @@ window.Page_settings = (() => {
${shieldSvg} ${shieldSvg}
<div class="flex-1-min"> <div class="flex-1-min">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:2px"> <div style="display:flex;align-items:center;gap:6px;margin-bottom:2px">
<span style="font-weight:700;font-size:var(--text-sm)">${_esc(cat.name)}</span> <span style="font-weight:700;font-size:var(--text-sm)">${UI.escape(cat.name)}</span>
${cur ? `<span style="font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px; ${cur ? `<span style="font-size:10px;font-weight:600;padding:1px 6px;border-radius:999px;
background:${cur.color};color:${cur.text}">${_esc(cur.name)}</span>` : ''} background:${cur.color};color:${cur.text}">${UI.escape(cur.name)}</span>` : ''}
</div> </div>
<div style="display:flex;gap:4px;margin-bottom:6px">${dots}</div> <div style="display:flex;gap:4px;margin-bottom:6px">${dots}</div>
${progressBar} ${progressBar}
@ -1131,7 +1131,7 @@ window.Page_settings = (() => {
['trainer', 'Trainer / Ausbilder'], ['trainer', 'Trainer / Ausbilder'],
['zuechter', 'Züchter'], ['zuechter', 'Züchter'],
].map(([val, label]) => ].map(([val, label]) =>
`<option value="${_esc(val)}" ${u.erfahrung === val ? 'selected' : ''}>${_esc(label)}</option>` `<option value="${UI.escape(val)}" ${u.erfahrung === val ? 'selected' : ''}>${UI.escape(label)}</option>`
).join(''); ).join('');
const sichtbarkeitOpts = [ const sichtbarkeitOpts = [
@ -1139,7 +1139,7 @@ window.Page_settings = (() => {
['friends', 'Nur Freunde'], ['friends', 'Nur Freunde'],
['private', 'Privat'], ['private', 'Privat'],
].map(([val, label]) => ].map(([val, label]) =>
`<option value="${_esc(val)}" ${(u.profil_sichtbarkeit || 'public') === val ? 'selected' : ''}>${_esc(label)}</option>` `<option value="${UI.escape(val)}" ${(u.profil_sichtbarkeit || 'public') === val ? 'selected' : ''}>${UI.escape(label)}</option>`
).join(''); ).join('');
UI.modal.open({ UI.modal.open({
@ -1150,20 +1150,20 @@ window.Page_settings = (() => {
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Echter Name (privat)</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Echter Name (privat)</label>
<input name="real_name" type="text" maxlength="80" <input name="real_name" type="text" maxlength="80"
placeholder="z. B. Maria Müller" placeholder="z. B. Maria Müller"
value="${_esc(u.real_name || '')}" value="${UI.escape(u.real_name || '')}"
style="${inputStyle}"> style="${inputStyle}">
</div> </div>
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Bio</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Bio</label>
<textarea name="bio" maxlength="300" rows="4" <textarea name="bio" maxlength="300" rows="4"
placeholder="Kurze Vorstellung (max. 300 Zeichen)" placeholder="Kurze Vorstellung (max. 300 Zeichen)"
style="${inputStyle};resize:vertical">${_esc(u.bio || '')}</textarea> style="${inputStyle};resize:vertical">${UI.escape(u.bio || '')}</textarea>
</div> </div>
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Wohnort</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Wohnort</label>
<input name="wohnort" type="text" maxlength="60" <input name="wohnort" type="text" maxlength="60"
placeholder="z.B. München" placeholder="z.B. München"
value="${_esc(u.wohnort || '')}" value="${UI.escape(u.wohnort || '')}"
style="${inputStyle}"> style="${inputStyle}">
</div> </div>
<div> <div>
@ -1174,7 +1174,7 @@ window.Page_settings = (() => {
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Social-Link</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Social-Link</label>
<input name="social_link" type="url" maxlength="120" <input name="social_link" type="url" maxlength="120"
placeholder="https://instagram.com/dein-hundeaccount" placeholder="https://instagram.com/dein-hundeaccount"
value="${_esc(u.social_link || '')}" value="${UI.escape(u.social_link || '')}"
style="${inputStyle}"> style="${inputStyle}">
</div> </div>
<div style="border-top:1px solid var(--c-border);padding-top:var(--space-3);margin-top:var(--space-1)"> <div style="border-top:1px solid var(--c-border);padding-top:var(--space-3);margin-top:var(--space-1)">
@ -1182,12 +1182,12 @@ window.Page_settings = (() => {
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">Wird auf Rechnungen gedruckt. Straße in Zeile 1, PLZ + Ort in Zeile 2.</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">Wird auf Rechnungen gedruckt. Straße in Zeile 1, PLZ + Ort in Zeile 2.</div>
<textarea name="billing_address" rows="2" maxlength="200" <textarea name="billing_address" rows="2" maxlength="200"
placeholder="Musterstraße 1&#10;12345 Berlin" placeholder="Musterstraße 1&#10;12345 Berlin"
style="${inputStyle};resize:vertical;font-family:inherit">${_esc(u.billing_address || '')}</textarea> style="${inputStyle};resize:vertical;font-family:inherit">${UI.escape(u.billing_address || '')}</textarea>
</div> </div>
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Dein Geburtstag <span style="font-weight:400;color:var(--c-text-secondary)">(optional)</span></label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Dein Geburtstag <span style="font-weight:400;color:var(--c-text-secondary)">(optional)</span></label>
<input name="geburtstag" type="text" maxlength="5" placeholder="TT.MM" <input name="geburtstag" type="text" maxlength="5" placeholder="TT.MM"
value="${_esc(u.geburtstag || '')}" value="${UI.escape(u.geburtstag || '')}"
pattern="\\d{2}\\.\\d{2}" pattern="\\d{2}\\.\\d{2}"
title="Format: TT.MM, z.B. 16.05" title="Format: TT.MM, z.B. 16.05"
style="${inputStyle}"> style="${inputStyle}">
@ -1525,8 +1525,8 @@ window.Page_settings = (() => {
return ` return `
<div style="display:flex;justify-content:space-between;align-items:center; <div style="display:flex;justify-content:space-between;align-items:center;
padding:var(--space-2) 0;font-size:var(--text-sm)"> padding:var(--space-2) 0;font-size:var(--text-sm)">
<span>${_esc(label)}</span> <span>${UI.escape(label)}</span>
<button class="by-toggle ki-toggle-btn" data-key="${_esc(key)}" <button class="by-toggle ki-toggle-btn" data-key="${UI.escape(key)}"
data-active="${active ? '1' : '0'}" data-active="${active ? '1' : '0'}"
style="position:relative;display:inline-block;width:44px;height:24px; style="position:relative;display:inline-block;width:44px;height:24px;
border:none;border-radius:12px;cursor:pointer;flex-shrink:0; border:none;border-radius:12px;cursor:pointer;flex-shrink:0;
@ -1572,8 +1572,8 @@ window.Page_settings = (() => {
</span>`; </span>`;
actionBlock = ` actionBlock = `
<div style="margin-top:var(--space-3);font-size:var(--text-sm);display:flex;flex-direction:column;gap:var(--space-1)"> <div style="margin-top:var(--space-3);font-size:var(--text-sm);display:flex;flex-direction:column;gap:var(--space-1)">
${profile?.zwingername ? `<div class="text-secondary">Zwinger: <strong>${_esc(profile.zwingername)}</strong></div>` : ''} ${profile?.zwingername ? `<div class="text-secondary">Zwinger: <strong>${UI.escape(profile.zwingername)}</strong></div>` : ''}
${profile?.rasse_text ? `<div class="text-secondary">Rasse: <strong>${_esc(profile.rasse_text)}</strong></div>` : ''} ${profile?.rasse_text ? `<div class="text-secondary">Rasse: <strong>${UI.escape(profile.rasse_text)}</strong></div>` : ''}
</div> </div>
${rolle === 'breeder' && profile ? ` ${rolle === 'breeder' && profile ? `
<button class="btn btn-secondary btn-sm" id="breeder-edit-profile-btn" class="mt-3"> <button class="btn btn-secondary btn-sm" id="breeder-edit-profile-btn" class="mt-3">
@ -1700,22 +1700,22 @@ window.Page_settings = (() => {
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Zwingername</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Zwingername</label>
<input name="zwingername" type="text" maxlength="100" style="${inputStyle}" <input name="zwingername" type="text" maxlength="100" style="${inputStyle}"
value="${_esc(profile?.zwingername || '')}"> value="${UI.escape(profile?.zwingername || '')}">
</div> </div>
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Rasse</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Rasse</label>
<input name="rasse_text" type="text" maxlength="100" style="${inputStyle}" <input name="rasse_text" type="text" maxlength="100" style="${inputStyle}"
value="${_esc(profile?.rasse_text || '')}"> value="${UI.escape(profile?.rasse_text || '')}">
</div> </div>
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Zuchtverein</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Zuchtverein</label>
<input name="verein" type="text" maxlength="100" style="${inputStyle}" <input name="verein" type="text" maxlength="100" style="${inputStyle}"
value="${_esc(profile?.verein || '')}"> value="${UI.escape(profile?.verein || '')}">
</div> </div>
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Stadt</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Stadt</label>
<input name="stadt" type="text" maxlength="80" style="${inputStyle}" <input name="stadt" type="text" maxlength="80" style="${inputStyle}"
value="${_esc(profile?.stadt || '')}"> value="${UI.escape(profile?.stadt || '')}">
</div> </div>
<div style="display:flex;align-items:center;gap:var(--space-3)"> <div style="display:flex;align-items:center;gap:var(--space-3)">
<input name="vdh_mitglied" type="checkbox" id="edit-breeder-vdh" <input name="vdh_mitglied" type="checkbox" id="edit-breeder-vdh"
@ -1726,12 +1726,12 @@ window.Page_settings = (() => {
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Website (optional)</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Website (optional)</label>
<input name="website" type="url" maxlength="200" style="${inputStyle}" <input name="website" type="url" maxlength="200" style="${inputStyle}"
value="${_esc(profile?.website || '')}" placeholder="https://mein-zwinger.de"> value="${UI.escape(profile?.website || '')}" placeholder="https://mein-zwinger.de">
</div> </div>
<div> <div>
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Beschreibung (optional)</label> <label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Beschreibung (optional)</label>
<textarea name="beschreibung" maxlength="500" rows="3" <textarea name="beschreibung" maxlength="500" rows="3"
style="${inputStyle};resize:vertical">${_esc(profile?.beschreibung || '')}</textarea> style="${inputStyle};resize:vertical">${UI.escape(profile?.beschreibung || '')}</textarea>
</div> </div>
</form>`, </form>`,
footer: ` footer: `
@ -1969,7 +1969,7 @@ window.Page_settings = (() => {
// GEDENKSEITE — für verstorbene Hunde // GEDENKSEITE — für verstorbene Hunde
// ---------------------------------------------------------- // ----------------------------------------------------------
async function _openGedenkseite(dogId, dogName) { async function _openGedenkseite(dogId, dogName) {
UI.modal.open({ title: `Erinnerungen an ${_esc(dogName)}`, body: ` UI.modal.open({ title: `Erinnerungen an ${UI.escape(dogName)}`, body: `
<div style="text-align:center;padding:var(--space-4)"> <div style="text-align:center;padding:var(--space-4)">
<svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary);animation:spin 1s linear infinite" aria-hidden="true"> <svg class="ph-icon" style="width:32px;height:32px;color:var(--c-primary);animation:spin 1s linear infinite" aria-hidden="true">
<use href="/icons/phosphor.svg#spinner"></use> <use href="/icons/phosphor.svg#spinner"></use>
@ -1982,12 +1982,12 @@ window.Page_settings = (() => {
const d = data; const d = data;
const av = d.dog.foto_url const av = d.dog.foto_url
? `<img src="${_esc(d.dog.foto_url)}" style="width:100px;height:100px;border-radius:50%;object-fit:cover;border:3px solid var(--c-primary)">` ? `<img src="${UI.escape(d.dog.foto_url)}" style="width:100px;height:100px;border-radius:50%;object-fit:cover;border:3px solid var(--c-primary)">`
: `<div style="width:100px;height:100px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;border:3px solid var(--c-primary)"><svg class="ph-icon" style="width:48px;height:48px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg></div>`; : `<div style="width:100px;height:100px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;border:3px solid var(--c-primary)"><svg class="ph-icon" style="width:48px;height:48px;color:var(--c-primary)" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg></div>`;
const photoGrid = d.photos?.length ? ` const photoGrid = d.photos?.length ? `
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin:var(--space-4) 0"> <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin:var(--space-4) 0">
${d.photos.map(url => `<img src="${_esc(url)}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px">`).join('')} ${d.photos.map(url => `<img src="${UI.escape(url)}" style="width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px">`).join('')}
</div>` : ''; </div>` : '';
const statsHtml = ` const statsHtml = `
@ -2012,11 +2012,11 @@ window.Page_settings = (() => {
: ''; : '';
UI.modal.open({ UI.modal.open({
title: `Erinnerungen an ${_esc(d.dog.name)}`, title: `Erinnerungen an ${UI.escape(d.dog.name)}`,
body: ` body: `
<div style="text-align:center;margin-bottom:var(--space-4)"> <div style="text-align:center;margin-bottom:var(--space-4)">
${av} ${av}
<div style="margin-top:var(--space-3);font-size:var(--text-lg);font-weight:700">${_esc(d.dog.name)}</div> <div style="margin-top:var(--space-3);font-size:var(--text-lg);font-weight:700">${UI.escape(d.dog.name)}</div>
${passedStr ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:4px"> ${passedStr ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:4px">
<svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg> <svg class="ph-icon" style="width:14px;height:14px" aria-hidden="true"><use href="/icons/phosphor.svg#heart-break"></use></svg>
${passedStr} ${passedStr}
@ -2033,7 +2033,7 @@ window.Page_settings = (() => {
${d.ki_abschied ? `<div style="font-style:italic;font-size:var(--text-sm);color:var(--c-text-secondary); ${d.ki_abschied ? `<div style="font-style:italic;font-size:var(--text-sm);color:var(--c-text-secondary);
line-height:1.7;padding:var(--space-3);background:var(--c-surface); line-height:1.7;padding:var(--space-3);background:var(--c-surface);
border-radius:var(--radius-md);border:1px solid var(--c-border)"> border-radius:var(--radius-md);border:1px solid var(--c-border)">
"${_esc(d.ki_abschied)}" "${UI.escape(d.ki_abschied)}"
</div>` : ''} </div>` : ''}
`, `,
}); });
@ -2591,11 +2591,6 @@ window.Page_settings = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// HELPER // HELPER
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// PUBLIC // PUBLIC

View file

@ -242,13 +242,13 @@ window.Page_social = (() => {
🌙 Noch nicht gezeigt: 🌙 Noch nicht gezeigt:
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px"> <div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
${_unusedBreeds.map(b => ${_unusedBreeds.map(b =>
`<button class="sm-breed-chip" data-id="${b.id}" data-name="${_esc(b.name)}" `<button class="sm-breed-chip" data-id="${b.id}" data-name="${UI.escape(b.name)}"
style="padding:5px 12px;border-radius:var(--radius-full); style="padding:5px 12px;border-radius:var(--radius-full);
border:1.5px solid var(--c-border); border:1.5px solid var(--c-border);
background:var(--c-surface-2);color:var(--c-text); background:var(--c-surface-2);color:var(--c-text);
font-size:12px;cursor:pointer;font-family:inherit; font-size:12px;cursor:pointer;font-family:inherit;
transition:all var(--transition-fast)"> transition:all var(--transition-fast)">
${_esc(b.name)}</button>`).join('')} ${UI.escape(b.name)}</button>`).join('')}
</div> </div>
</div>` : ''} </div>` : ''}
<input id="sm-breed-search" list="sm-breed-list" <input id="sm-breed-search" list="sm-breed-list"
@ -259,7 +259,7 @@ window.Page_social = (() => {
padding:9px 12px;font-size:var(--text-sm); padding:9px 12px;font-size:var(--text-sm);
font-family:inherit;box-sizing:border-box"> font-family:inherit;box-sizing:border-box">
<datalist id="sm-breed-list"> <datalist id="sm-breed-list">
${_breeds.map(b => `<option value="${_esc(b.name)}" data-id="${b.id}">`).join('')} ${_breeds.map(b => `<option value="${UI.escape(b.name)}" data-id="${b.id}">`).join('')}
</datalist> </datalist>
<input type="hidden" id="sm-breed-id"> <input type="hidden" id="sm-breed-id">
</div> </div>
@ -414,9 +414,9 @@ window.Page_social = (() => {
<span style="font-size:1.4em;flex-shrink:0;line-height:1.2">${idea.emoji||'💡'}</span> <span style="font-size:1.4em;flex-shrink:0;line-height:1.2">${idea.emoji||'💡'}</span>
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:600;font-size:var(--text-sm);margin-bottom:4px; <div style="font-weight:600;font-size:var(--text-sm);margin-bottom:4px;
line-height:1.3">${_esc(idea.thema)}</div> line-height:1.3">${UI.escape(idea.thema)}</div>
<div style="font-size:11px;color:var(--c-text-secondary);margin-bottom:6px; <div style="font-size:11px;color:var(--c-text-secondary);margin-bottom:6px;
line-height:1.4">🎓 ${_esc(idea.warum)}</div> line-height:1.4">🎓 ${UI.escape(idea.warum)}</div>
<div style="display:flex;gap:6px"> <div style="display:flex;gap:6px">
<span style="font-size:10px;background:var(--c-surface-2); <span style="font-size:10px;background:var(--c-surface-2);
padding:2px 6px;border-radius:4px">${_FL[idea.format]||idea.format}</span> padding:2px 6px;border-radius:4px">${_FL[idea.format]||idea.format}</span>
@ -427,16 +427,16 @@ window.Page_social = (() => {
<div style="display:flex;flex-direction:column;gap:5px;flex-shrink:0"> <div style="display:flex;flex-direction:column;gap:5px;flex-shrink:0">
<button class="btn btn-primary btn-sm sm-use" <button class="btn btn-primary btn-sm sm-use"
style="font-size:11px;padding:6px 10px;min-height:34px;white-space:nowrap" style="font-size:11px;padding:6px 10px;min-height:34px;white-space:nowrap"
data-thema="${_esc(idea.thema)}" data-thema="${UI.escape(idea.thema)}"
data-format="${idea.format||'post'}" data-format="${idea.format||'post'}"
data-platform="${idea.platform||'both'}"> data-platform="${idea.platform||'both'}">
Nutzen </button> Nutzen </button>
<button class="btn btn-secondary btn-sm sm-save-idea" <button class="btn btn-secondary btn-sm sm-save-idea"
style="font-size:10px;padding:4px 8px;min-height:26px;white-space:nowrap" style="font-size:10px;padding:4px 8px;min-height:26px;white-space:nowrap"
data-thema="${_esc(idea.thema)}" data-thema="${UI.escape(idea.thema)}"
data-format="${idea.format||'post'}" data-format="${idea.format||'post'}"
data-platform="${idea.platform||'both'}" data-platform="${idea.platform||'both'}"
data-category="${_esc(idea.category||'')}"> data-category="${UI.escape(idea.category||'')}">
<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#push-pin"></use></svg> Merken</button> <svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#push-pin"></use></svg> Merken</button>
</div> </div>
</div> </div>
@ -522,15 +522,15 @@ window.Page_social = (() => {
margin-bottom:6px;display:flex;align-items:center;gap:10px"> margin-bottom:6px;display:flex;align-items:center;gap:10px">
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-size:var(--text-sm);font-weight:600;color:var(--c-text)"> <div style="font-size:var(--text-sm);font-weight:600;color:var(--c-text)">
${_esc(e.name)}</div> ${UI.escape(e.name)}</div>
<div style="font-size:10px;color:var(--c-text-muted)"> <div style="font-size:10px;color:var(--c-text-muted)">
${_esc(e.schwierigkeit||'')} · ${_esc(e.alter_ab||'')} · ${_esc(e.dauer||'')}</div> ${UI.escape(e.schwierigkeit||'')} · ${UI.escape(e.alter_ab||'')} · ${UI.escape(e.dauer||'')}</div>
</div> </div>
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0"> <div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
${e.posts_count > 0 ? `<span style="font-size:10px;background:#10b981; ${e.posts_count > 0 ? `<span style="font-size:10px;background:#10b981;
color:#fff;padding:1px 6px;border-radius:4px"><svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#check"></use></svg> ${e.posts_count}x</span>` : ''} color:#fff;padding:1px 6px;border-radius:4px"><svg class="ph-icon" aria-hidden="true" style="width:12px;height:12px"><use href="/icons/phosphor.svg#check"></use></svg> ${e.posts_count}x</span>` : ''}
<button class="btn btn-sm btn-primary sm-ex-use" <button class="btn btn-sm btn-primary sm-ex-use"
data-id="${e.exercise_id}" data-name="${_esc(e.name)}" data-id="${e.exercise_id}" data-name="${UI.escape(e.name)}"
style="font-size:11px;padding:4px 10px;min-height:28px"> style="font-size:11px;padding:4px 10px;min-height:28px">
Nutzen</button> Nutzen</button>
</div> </div>
@ -580,9 +580,9 @@ window.Page_social = (() => {
<span style="font-size:2.2em;flex-shrink:0">🎾</span> <span style="font-size:2.2em;flex-shrink:0">🎾</span>
<div> <div>
<div style="font-size:11px;color:#4ade80;font-weight:600;margin-bottom:2px"> <div style="font-size:11px;color:#4ade80;font-weight:600;margin-bottom:2px">
Trainingstipp · ${_esc(data.exercise_kat||'')} · ${stilLabel}</div> Trainingstipp · ${UI.escape(data.exercise_kat||'')} · ${stilLabel}</div>
<div style="font-weight:700;font-size:var(--text-base);color:#15803d"> <div style="font-weight:700;font-size:var(--text-base);color:#15803d">
${_esc(data.exercise_name||'')}</div> ${UI.escape(data.exercise_name||'')}</div>
</div> </div>
</div> </div>
${_renderResult(data, null)}`; ${_renderResult(data, null)}`;
@ -592,7 +592,7 @@ window.Page_social = (() => {
} catch(e) { } catch(e) {
clearInterval(interval.bar); clearInterval(interval.msg); clearInterval(interval.bar); clearInterval(interval.msg);
res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)"> res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)">
😬 ${_esc(e.message||String(e))}</div>`; 😬 ${UI.escape(e.message||String(e))}</div>`;
} finally { } finally {
btn.disabled = false; btn.disabled = false;
} }
@ -620,10 +620,10 @@ window.Page_social = (() => {
<span style="font-size:2.2em;flex-shrink:0">🛁</span> <span style="font-size:2.2em;flex-shrink:0">🛁</span>
<div> <div>
<div style="font-size:11px;color:#c084fc;font-weight:600;margin-bottom:2px"> <div style="font-size:11px;color:#c084fc;font-weight:600;margin-bottom:2px">
Pflegetipp · ${_esc(data.pflege_kat||'')} Pflegetipp · ${UI.escape(data.pflege_kat||'')}
${data.rasse_name ? ` · speziell für ${_esc(data.rasse_name)}` : ''}</div> ${data.rasse_name ? ` · speziell für ${UI.escape(data.rasse_name)}` : ''}</div>
<div style="font-weight:700;font-size:var(--text-base);color:#7c3aed"> <div style="font-weight:700;font-size:var(--text-base);color:#7c3aed">
${_esc(data.pflege_titel||'')}</div> ${UI.escape(data.pflege_titel||'')}</div>
</div> </div>
</div> </div>
${_renderResult(data, null)}`; ${_renderResult(data, null)}`;
@ -633,7 +633,7 @@ window.Page_social = (() => {
} catch(e) { } catch(e) {
clearInterval(interval.bar); clearInterval(interval.msg); clearInterval(interval.bar); clearInterval(interval.msg);
res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)"> res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)">
😬 ${_esc(e.message||String(e))}</div>`; 😬 ${UI.escape(e.message||String(e))}</div>`;
} finally { btn.disabled = false; } } finally { btn.disabled = false; }
}); });
@ -663,7 +663,7 @@ window.Page_social = (() => {
<div style="font-size:11px;color:var(--c-primary);font-weight:600;margin-bottom:2px"> <div style="font-size:11px;color:var(--c-primary);font-weight:600;margin-bottom:2px">
Rasse des Tages</div> Rasse des Tages</div>
<div style="font-weight:700;font-size:var(--text-base);color:var(--c-primary-dark)"> <div style="font-weight:700;font-size:var(--text-base);color:var(--c-primary-dark)">
${_esc(data.topic?.replace('Rasse des Tages: ',''))}</div> ${UI.escape(data.topic?.replace('Rasse des Tages: ',''))}</div>
</div> </div>
</div> </div>
${_renderResult(data, mediaUrl)}`; ${_renderResult(data, mediaUrl)}`;
@ -675,7 +675,7 @@ window.Page_social = (() => {
} catch(e) { } catch(e) {
clearInterval(interval.bar); clearInterval(interval.msg); clearInterval(interval.bar); clearInterval(interval.msg);
res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)"> res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)">
😬 ${_esc(e.message||String(e))}</div>`; 😬 ${UI.escape(e.message||String(e))}</div>`;
} finally { } finally {
btn.disabled = false; btn.disabled = false;
} }
@ -719,7 +719,7 @@ window.Page_social = (() => {
clearInterval(interval); clearInterval(interval);
res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3); res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3);
border-radius:8px;background:var(--c-surface-2)"> border-radius:8px;background:var(--c-surface-2)">
😬 Ups: ${_esc(e.message||String(e))}</div>`; 😬 Ups: ${UI.escape(e.message||String(e))}</div>`;
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.innerHTML = '<svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px"><use href="/icons/phosphor.svg#sparkle"></use></svg> Los geht\'s!'; btn.innerHTML = '<svg class="ph-icon" aria-hidden="true" style="width:16px;height:16px"><use href="/icons/phosphor.svg#sparkle"></use></svg> Los geht\'s!';
@ -816,7 +816,7 @@ window.Page_social = (() => {
<div> <div>
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px; <div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px;
text-transform:uppercase;letter-spacing:.5px">Luna sagt:</div> text-transform:uppercase;letter-spacing:.5px">Luna sagt:</div>
<div style="font-size:var(--text-sm);line-height:1.6;color:var(--c-text)">${_esc(data.coaching)}</div> <div style="font-size:var(--text-sm);line-height:1.6;color:var(--c-text)">${UI.escape(data.coaching)}</div>
</div> </div>
</div> </div>
</div>` : ''} </div>` : ''}
@ -903,9 +903,9 @@ window.Page_social = (() => {
${data.hook ? `<div class="sm-label">🎣 Hook</div> ${data.hook ? `<div class="sm-label">🎣 Hook</div>
<div style="font-size:var(--text-sm);font-style:italic;margin-bottom:var(--space-3); <div style="font-size:var(--text-sm);font-style:italic;margin-bottom:var(--space-3);
line-height:1.6"> line-height:1.6">
"${_esc(data.hook)}"</div>` : ''} "${UI.escape(data.hook)}"</div>` : ''}
${data.cta ? `<div class="sm-label">📣 Call-to-Action</div> ${data.cta ? `<div class="sm-label">📣 Call-to-Action</div>
<div style="font-size:var(--text-sm);line-height:1.6">${_esc(data.cta)}</div>` : ''} <div style="font-size:var(--text-sm);line-height:1.6">${UI.escape(data.cta)}</div>` : ''}
</div>` : ''} </div>` : ''}
${_resultBlock('📸 Was du filmen/fotografieren solltest', data.visual_brief, false)} ${_resultBlock('📸 Was du filmen/fotografieren solltest', data.visual_brief, false)}
${data.script ? ` ${data.script ? `
@ -914,7 +914,7 @@ window.Page_social = (() => {
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)"> margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
<div class="sm-label">🎬 Video-Aufbau</div> <div class="sm-label">🎬 Video-Aufbau</div>
<div style="font-size:var(--text-sm);white-space:pre-wrap; <div style="font-size:var(--text-sm);white-space:pre-wrap;
line-height:1.7">${_esc(data.script)}</div> line-height:1.7">${UI.escape(data.script)}</div>
</div>` : ''} </div>` : ''}
${(data.image_prompt||data.canva_notes||unsplash) ? ` ${(data.image_prompt||data.canva_notes||unsplash) ? `
<div style="background:var(--c-surface);border:1px solid var(--c-border); <div style="background:var(--c-surface);border:1px solid var(--c-border);
@ -926,12 +926,12 @@ window.Page_social = (() => {
DALL-E / Midjourney:</div> DALL-E / Midjourney:</div>
<div style="font-size:11px;background:var(--c-surface-2);padding:10px; <div style="font-size:11px;background:var(--c-surface-2);padding:10px;
border-radius:var(--radius-md);font-family:monospace;word-break:break-word; border-radius:var(--radius-md);font-family:monospace;word-break:break-word;
margin-bottom:var(--space-3);line-height:1.5">${_esc(data.image_prompt)}</div> margin-bottom:var(--space-3);line-height:1.5">${UI.escape(data.image_prompt)}</div>
${_copyBtn(data.image_prompt)}` : ''} ${_copyBtn(data.image_prompt)}` : ''}
${data.canva_notes ? ` ${data.canva_notes ? `
<div style="font-size:11px;color:var(--c-text-muted);margin:var(--space-3) 0 6px">Canva:</div> <div style="font-size:11px;color:var(--c-text-muted);margin:var(--space-3) 0 6px">Canva:</div>
<div style="font-size:var(--text-sm);margin-bottom:var(--space-3); <div style="font-size:var(--text-sm);margin-bottom:var(--space-3);
line-height:1.6">${_esc(data.canva_notes)}</div>` : ''} line-height:1.6">${UI.escape(data.canva_notes)}</div>` : ''}
${unsplash ? `<a href="${unsplash}" target="_blank" rel="noopener" ${unsplash ? `<a href="${unsplash}" target="_blank" rel="noopener"
style="font-size:var(--text-sm);color:var(--c-primary);display:inline-block"> style="font-size:var(--text-sm);color:var(--c-primary);display:inline-block">
🔍 Kostenlose Fotos auf Unsplash </a>` : ''} 🔍 Kostenlose Fotos auf Unsplash </a>` : ''}
@ -945,14 +945,14 @@ window.Page_social = (() => {
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)"> margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
<div class="sm-label">${label}</div> <div class="sm-label">${label}</div>
<div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.7; <div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.7;
margin-bottom:${copyable?'var(--space-3)':'0'}">${_esc(text)}</div> margin-bottom:${copyable?'var(--space-3)':'0'}">${UI.escape(text)}</div>
${copyable ? _copyBtn(text) : ''} ${copyable ? _copyBtn(text) : ''}
</div>`; </div>`;
} }
function _copyBtn(text) { function _copyBtn(text) {
return `<button class="btn btn-sm btn-secondary sm-copy" return `<button class="btn btn-sm btn-secondary sm-copy"
data-copy="${_esc(text)}" data-copy="${UI.escape(text)}"
style="font-size:11px;padding:5px 14px;min-height:32px; style="font-size:11px;padding:5px 14px;min-height:32px;
border-radius:var(--radius-full)"> border-radius:var(--radius-full)">
📋 Kopieren</button>`; 📋 Kopieren</button>`;
@ -1012,7 +1012,7 @@ window.Page_social = (() => {
<div style="text-align:center;padding:8px;color:var(--c-success); <div style="text-align:center;padding:8px;color:var(--c-success);
font-weight:600;font-size:var(--text-sm)"> font-weight:600;font-size:var(--text-sm)">
🎉 Super! Post als veröffentlicht markiert. 🎉 Super! Post als veröffentlicht markiert.
${url ? `<br><a href="${_esc(url)}" target="_blank" rel="noopener" ${url ? `<br><a href="${UI.escape(url)}" target="_blank" rel="noopener"
style="font-size:11px;color:var(--c-primary)">Post ansehen </a>` : ''} style="font-size:11px;color:var(--c-primary)">Post ansehen </a>` : ''}
</div>`; </div>`;
// Stats aktualisieren // Stats aktualisieren
@ -1073,7 +1073,7 @@ window.Page_social = (() => {
<div style="font-size:11px;color:#000;line-height:1.4; <div style="font-size:11px;color:#000;line-height:1.4;
display:-webkit-box;-webkit-line-clamp:3; display:-webkit-box;-webkit-line-clamp:3;
-webkit-box-orient:vertical;overflow:hidden"> -webkit-box-orient:vertical;overflow:hidden">
${_esc((item.caption||'').substring(0,150))}${(item.caption||'').length>150?'…':''}</div> ${UI.escape((item.caption||'').substring(0,150))}${(item.caption||'').length>150?'…':''}</div>
${item.hashtags ? `<div style="font-size:10px;color:#00376b;margin-top:4px; ${item.hashtags ? `<div style="font-size:10px;color:#00376b;margin-top:4px;
word-break:break-word;display:-webkit-box;-webkit-line-clamp:2; word-break:break-word;display:-webkit-box;-webkit-line-clamp:2;
-webkit-box-orient:vertical;overflow:hidden"> -webkit-box-orient:vertical;overflow:hidden">
@ -1156,11 +1156,11 @@ window.Page_social = (() => {
${c.ai_score ? `<span style="font-size:10px">${'⭐'.repeat(c.ai_score)}</span>` : ''} ${c.ai_score ? `<span style="font-size:10px">${'⭐'.repeat(c.ai_score)}</span>` : ''}
</div> </div>
<div style="font-weight:600;font-size:var(--text-sm);margin-bottom:3px; <div style="font-weight:600;font-size:var(--text-sm);margin-bottom:3px;
line-height:1.3">${_esc(c.topic)}</div> line-height:1.3">${UI.escape(c.topic)}</div>
${c.hook ? `<div style="font-size:11px;color:var(--c-text-secondary); ${c.hook ? `<div style="font-size:11px;color:var(--c-text-secondary);
font-style:italic;margin-bottom:2px">🎣 ${_esc(c.hook)}</div>` : ''} font-style:italic;margin-bottom:2px">🎣 ${UI.escape(c.hook)}</div>` : ''}
${c.post_url ${c.post_url
? `<a href="${_esc(c.post_url)}" target="_blank" rel="noopener" ? `<a href="${UI.escape(c.post_url)}" target="_blank" rel="noopener"
style="font-size:10px;color:var(--c-primary);display:inline-flex; style="font-size:10px;color:var(--c-primary);display:inline-flex;
align-items:center;gap:3px;margin-top:2px"> align-items:center;gap:3px;margin-top:2px">
<svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#link-simple"></use></svg> Post ansehen</a>` <svg class="ph-icon" aria-hidden="true" style="width:14px;height:14px"><use href="/icons/phosphor.svg#link-simple"></use></svg> Post ansehen</a>`
@ -1195,10 +1195,10 @@ window.Page_social = (() => {
border-top:1px solid var(--c-border);padding-top:10px"> border-top:1px solid var(--c-border);padding-top:10px">
${c.coaching ? `<div style="background:var(--c-surface-2);border-radius:8px; ${c.coaching ? `<div style="background:var(--c-surface-2);border-radius:8px;
padding:10px;margin-bottom:10px;font-size:11px;line-height:1.5"> padding:10px;margin-bottom:10px;font-size:11px;line-height:1.5">
🌙 ${_esc(c.coaching)}</div>` : ''} 🌙 ${UI.escape(c.coaching)}</div>` : ''}
${c.caption ? `<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:2px">Caption:</div> ${c.caption ? `<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:2px">Caption:</div>
<div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.5; <div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.5;
margin-bottom:8px">${_esc(c.caption)}</div> margin-bottom:8px">${UI.escape(c.caption)}</div>
${_copyBtn(c.caption)}` : ''} ${_copyBtn(c.caption)}` : ''}
${c.hashtags ? `<div style="font-size:11px;color:var(--c-primary);margin-top:8px; ${c.hashtags ? `<div style="font-size:11px;color:var(--c-primary);margin-top:8px;
word-break:break-word"> word-break:break-word">
@ -1375,7 +1375,7 @@ window.Page_social = (() => {
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px; <div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px;
text-transform:uppercase;letter-spacing:.5px"> text-transform:uppercase;letter-spacing:.5px">
Lunas Feedback:</div> Lunas Feedback:</div>
<div style="font-size:var(--text-sm);line-height:1.6">${_esc(data.notes)}</div> <div style="font-size:var(--text-sm);line-height:1.6">${UI.escape(data.notes)}</div>
</div> </div>
</div> </div>
</div>` : ''} </div>` : ''}
@ -1384,7 +1384,7 @@ window.Page_social = (() => {
API.get('/social/stats').then(s => { _stats = s; }); API.get('/social/stats').then(s => { _stats = s; });
} catch(e) { } catch(e) {
clearInterval(interval); clearInterval(interval);
res.innerHTML = `<div class="text-danger">😬 Fehler: ${_esc(e.message||String(e))}</div>`; res.innerHTML = `<div class="text-danger">😬 Fehler: ${UI.escape(e.message||String(e))}</div>`;
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.innerHTML = '🔍 Luna, schau mal drüber!'; btn.innerHTML = '🔍 Luna, schau mal drüber!';
@ -1404,12 +1404,6 @@ window.Page_social = (() => {
return picked; return picked;
} }
function _esc(s) {
if (!s) return '';
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// CSS // CSS
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = ` style.textContent = `

View file

@ -26,15 +26,6 @@ window.Page_trainingsplaene = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// HELPER // HELPER
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function _icon(name) { function _icon(name) {
return `<svg class="ph-icon" aria-hidden="true" style="width:1em;height:1em;vertical-align:-0.15em"><use href="/icons/phosphor.svg#${name}"></use></svg>`; return `<svg class="ph-icon" aria-hidden="true" style="width:1em;height:1em;vertical-align:-0.15em"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
} }
@ -57,9 +48,9 @@ window.Page_trainingsplaene = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
function _renderTable(headers, rows) { function _renderTable(headers, rows) {
const ths = headers.map(h => `<th style="padding:6px 10px;background:var(--c-surface-2);font-weight:var(--weight-semibold);font-size:var(--text-sm);white-space:nowrap">${_esc(h)}</th>`).join(''); const ths = headers.map(h => `<th style="padding:6px 10px;background:var(--c-surface-2);font-weight:var(--weight-semibold);font-size:var(--text-sm);white-space:nowrap">${UI.escape(h)}</th>`).join('');
const trs = rows.map(row => { const trs = rows.map(row => {
const tds = row.map((cell, i) => `<td style="padding:6px 10px;font-size:var(--text-sm);color:var(--c-text);border-top:1px solid var(--c-border);${i === 0 ? 'white-space:nowrap;font-weight:var(--weight-semibold)' : ''}">${_esc(cell)}</td>`).join(''); const tds = row.map((cell, i) => `<td style="padding:6px 10px;font-size:var(--text-sm);color:var(--c-text);border-top:1px solid var(--c-border);${i === 0 ? 'white-space:nowrap;font-weight:var(--weight-semibold)' : ''}">${UI.escape(cell)}</td>`).join('');
return `<tr>${tds}</tr>`; return `<tr>${tds}</tr>`;
}).join(''); }).join('');
return ` return `
@ -80,15 +71,15 @@ window.Page_trainingsplaene = (() => {
if (checked) doneCount++; if (checked) doneCount++;
return ` return `
<label style="display:flex;align-items:flex-start;gap:var(--space-2);cursor:pointer;padding:var(--space-1) 0" class="tp-goal-label"> <label style="display:flex;align-items:flex-start;gap:var(--space-2);cursor:pointer;padding:var(--space-1) 0" class="tp-goal-label">
<input type="checkbox" data-lskey="${_esc(key)}" ${checked ? 'checked' : ''} <input type="checkbox" data-lskey="${UI.escape(key)}" ${checked ? 'checked' : ''}
style="margin-top:2px;flex-shrink:0;accent-color:var(--c-primary);width:16px;height:16px"> style="margin-top:2px;flex-shrink:0;accent-color:var(--c-primary);width:16px;height:16px">
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${_esc(goal)}</span> <span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${UI.escape(goal)}</span>
</label>`; </label>`;
}).join(''); }).join('');
const progress = total > 0 ? Math.round((doneCount / total) * 100) : 0; const progress = total > 0 ? Math.round((doneCount / total) * 100) : 0;
return ` return `
<div class="tp-goals" data-plan="${_esc(planId)}" data-total="${total}"> <div class="tp-goals" data-plan="${UI.escape(planId)}" data-total="${total}">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)">
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text-secondary)"> <span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text-secondary)">
${_icon('check-circle')} Lernziele ${_icon('check-circle')} Lernziele
@ -106,13 +97,13 @@ window.Page_trainingsplaene = (() => {
function _renderAccordionPhase(id, title, content) { function _renderAccordionPhase(id, title, content) {
return ` return `
<div class="tp-acc" id="tp-acc-${_esc(id)}"> <div class="tp-acc" id="tp-acc-${UI.escape(id)}">
<button class="tp-acc-head" data-acc="${_esc(id)}" <button class="tp-acc-head" data-acc="${UI.escape(id)}"
style="width:100%;display:flex;justify-content:space-between;align-items:center;padding:var(--space-3) var(--space-4);background:none;border:none;border-top:1px solid var(--c-border);cursor:pointer;text-align:left"> style="width:100%;display:flex;justify-content:space-between;align-items:center;padding:var(--space-3) var(--space-4);background:none;border:none;border-top:1px solid var(--c-border);cursor:pointer;text-align:left">
<span style="font-weight:var(--weight-semibold);color:var(--c-text)">${title}</span> <span style="font-weight:var(--weight-semibold);color:var(--c-text)">${title}</span>
<span class="tp-acc-arrow">${_icon('caret-down')}</span> <span class="tp-acc-arrow">${_icon('caret-down')}</span>
</button> </button>
<div class="tp-acc-body" id="tp-acc-body-${_esc(id)}" hidden <div class="tp-acc-body" id="tp-acc-body-${UI.escape(id)}" hidden
style="padding:var(--space-3) var(--space-4) var(--space-4)"> style="padding:var(--space-3) var(--space-4) var(--space-4)">
${content} ${content}
</div> </div>
@ -122,7 +113,7 @@ window.Page_trainingsplaene = (() => {
function _renderHintBox(text) { function _renderHintBox(text) {
return ` return `
<div style="background:var(--c-surface-2);border-left:3px solid var(--c-primary);border-radius:var(--radius-sm);padding:var(--space-3) var(--space-4);margin:var(--space-3) 0;font-size:var(--text-sm);color:var(--c-text);line-height:1.6"> <div style="background:var(--c-surface-2);border-left:3px solid var(--c-primary);border-radius:var(--radius-sm);padding:var(--space-3) var(--space-4);margin:var(--space-3) 0;font-size:var(--text-sm);color:var(--c-text);line-height:1.6">
${_icon('info')} ${_esc(text)} ${_icon('info')} ${UI.escape(text)}
</div>`; </div>`;
} }
@ -141,7 +132,7 @@ window.Page_trainingsplaene = (() => {
justify-content:center;gap:2px;white-space:normal;text-align:center;line-height:1.2"> justify-content:center;gap:2px;white-space:normal;text-align:center;line-height:1.2">
<svg class="ph-icon" aria-hidden="true" style="width:22px;height:22px"><use href="/icons/phosphor.svg#${p.icon}"></use></svg> <svg class="ph-icon" aria-hidden="true" style="width:22px;height:22px"><use href="/icons/phosphor.svg#${p.icon}"></use></svg>
<span style="font-size:var(--text-sm);font-weight:600">${p.label}</span> <span style="font-size:var(--text-sm);font-weight:600">${p.label}</span>
<span style="font-size:var(--text-xs);opacity:0.8">${_esc(p.sub)}</span> <span style="font-size:var(--text-xs);opacity:0.8">${UI.escape(p.sub)}</span>
</button>`).join(''); </button>`).join('');
return `<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4);flex-wrap:wrap">${btns}</div>`; return `<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4);flex-wrap:wrap">${btns}</div>`;
} }
@ -764,7 +755,7 @@ window.Page_trainingsplaene = (() => {
<div style="flex:1;min-width:220px"> <div style="flex:1;min-width:220px">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);margin-bottom:var(--space-2)"> color:var(--c-text);margin-bottom:var(--space-2)">
${_esc(MONTHS[month - 1])} ${year} ${UI.escape(MONTHS[month - 1])} ${year}
</div> </div>
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:3px"> <div style="display:grid;grid-template-columns:repeat(7,1fr);gap:3px">
${wdayHeader} ${wdayHeader}
@ -793,7 +784,7 @@ window.Page_trainingsplaene = (() => {
display:flex;align-items:center;justify-content:space-between;flex-shrink:0"> display:flex;align-items:center;justify-content:space-between;flex-shrink:0">
<div> <div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div> <div style="font-weight:var(--weight-semibold);font-size:var(--text-base)"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz</div>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${_esc(parentLabel)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:2px">${UI.escape(parentLabel)}</div>
</div> </div>
<button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button> <button id="by-note-close" style="background:none;border:none;cursor:pointer;font-size:1.4rem;color:var(--c-text-muted);padding:4px 8px" aria-label="Schließen">×</button>
</div> </div>

View file

@ -633,7 +633,7 @@ window.Page_uebungen = (() => {
border-radius:var(--radius-full,999px); border-radius:var(--radius-full,999px);
background:var(--c-primary-subtle);color:var(--c-primary); background:var(--c-primary-subtle);color:var(--c-primary);
font-size:var(--text-xs);font-weight:var(--weight-semibold)"> font-size:var(--text-xs);font-weight:var(--weight-semibold)">
${b.icon || '🏅'} ${_esc(b.name || b.badge_id)} ${b.icon || '🏅'} ${UI.escape(b.name || b.badge_id)}
</span> </span>
`).join(''); `).join('');
const more = rest > 0 const more = rest > 0
@ -715,8 +715,8 @@ window.Page_uebungen = (() => {
text-transform:uppercase;letter-spacing:.04em">${cfg.label}</span> text-transform:uppercase;letter-spacing:.04em">${cfg.label}</span>
</div> </div>
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);line-height:1.3">${_esc(r.exercise_name)}</div> color:var(--c-text);line-height:1.3">${UI.escape(r.exercise_name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.4">${_esc(r.reason)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.4">${UI.escape(r.reason)}</div>
<div style="display:flex;flex-direction:column;gap:var(--space-2);margin-top:auto;padding-top:var(--space-1)"> <div style="display:flex;flex-direction:column;gap:var(--space-2);margin-top:auto;padding-top:var(--space-1)">
<div> <div>
<span class="text-xs-secondary">${r.suggested_reps}× empfohlen</span> <span class="text-xs-secondary">${r.suggested_reps}× empfohlen</span>
@ -724,7 +724,7 @@ window.Page_uebungen = (() => {
${prognose} ${prognose}
</div> </div>
<button class="ueb-trainer-btn btn btn-primary" <button class="ueb-trainer-btn btn btn-primary"
data-tab="${_esc(r.tab)}" data-name="${_esc(r.exercise_name)}" data-reps="${r.suggested_reps}" data-tab="${UI.escape(r.tab)}" data-name="${UI.escape(r.exercise_name)}" data-reps="${r.suggested_reps}"
style="font-size:var(--text-xs);padding:4px 10px;width:100%"> style="font-size:var(--text-xs);padding:4px 10px;width:100%">
Üben Üben
</button> </button>
@ -817,25 +817,25 @@ window.Page_uebungen = (() => {
${ALL.map(g => ` ${ALL.map(g => `
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--c-text-secondary); <div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);color:var(--c-text-secondary);
text-transform:uppercase;letter-spacing:.05em;padding:var(--space-3) 0 var(--space-1)"> text-transform:uppercase;letter-spacing:.05em;padding:var(--space-3) 0 var(--space-1)">
${_esc(g.group)} ${UI.escape(g.group)}
</div> </div>
${g.items.map(u => { ${g.items.map(u => {
const key = _progressKey(g.tab, u.name); const key = _progressKey(g.tab, u.name);
const cur = _getStatus(g.tab, u.name); const cur = _getStatus(g.tab, u.name);
return ` return `
<div style="display:flex;align-items:center;gap:var(--space-2); <div style="display:flex;align-items:center;gap:var(--space-2);
padding:var(--space-2) 0;border-bottom:1px solid var(--c-border)" data-key="${_esc(key)}"> padding:var(--space-2) 0;border-bottom:1px solid var(--c-border)" data-key="${UI.escape(key)}">
<span style="flex:1;font-size:var(--text-sm);color:var(--c-text);min-width:0; <span style="flex:1;font-size:var(--text-sm);color:var(--c-text);min-width:0;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${_esc(u.name)}</span> overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(u.name)}</span>
<div style="display:flex;gap:3px;flex-shrink:0"> <div style="display:flex;gap:3px;flex-shrink:0">
${STATUS_QUICK.map(s => ` ${STATUS_QUICK.map(s => `
<button class="qs-btn" data-key="${_esc(key)}" data-val="${s.id === null ? '' : _esc(s.id)}" <button class="qs-btn" data-key="${UI.escape(key)}" data-val="${s.id === null ? '' : UI.escape(s.id)}"
style="font-size:9px;font-weight:700;padding:3px 6px;border-radius:999px;cursor:pointer; style="font-size:9px;font-weight:700;padding:3px 6px;border-radius:999px;cursor:pointer;
white-space:nowrap;transition:all .15s; white-space:nowrap;transition:all .15s;
background:${cur === s.id ? s.bg : 'var(--c-surface-2)'}; background:${cur === s.id ? s.bg : 'var(--c-surface-2)'};
color:${cur === s.id ? s.color : 'var(--c-text-secondary)'}; color:${cur === s.id ? s.color : 'var(--c-text-secondary)'};
border:1px solid ${cur === s.id ? s.color + '66' : 'var(--c-border)'}"> border:1px solid ${cur === s.id ? s.color + '66' : 'var(--c-border)'}">
${_esc(s.label)} ${UI.escape(s.label)}
</button>`).join('')} </button>`).join('')}
</div> </div>
</div>`; </div>`;
@ -899,7 +899,7 @@ window.Page_uebungen = (() => {
${TABS.map(t => ` ${TABS.map(t => `
<button class="by-tab${t.id === _activeTab ? ' active' : ''}" <button class="by-tab${t.id === _activeTab ? ' active' : ''}"
data-tab="${t.id}"> data-tab="${t.id}">
${_esc(t.label)} ${UI.escape(t.label)}
</button> </button>
`).join('')} `).join('')}
</div> </div>
@ -922,18 +922,18 @@ window.Page_uebungen = (() => {
return ` return `
<div style="background:${c.bg};border:1px solid ${c.border};border-radius:var(--radius-md); <div style="background:${c.bg};border:1px solid ${c.border};border-radius:var(--radius-md);
padding:var(--space-3) var(--space-4);display:flex;gap:var(--space-3);align-items:flex-start;cursor:pointer" padding:var(--space-3) var(--space-4);display:flex;gap:var(--space-3);align-items:flex-start;cursor:pointer"
data-action-tab="${_esc(s.action_tab || '')}" data-action-tab="${UI.escape(s.action_tab || '')}"
data-action-name="${_esc(s.action_name || '')}" data-action-name="${UI.escape(s.action_name || '')}"
class="ueb-suggestion-card"> class="ueb-suggestion-card">
<svg class="ph-icon" style="width:20px;height:20px;flex-shrink:0;color:${c.text};margin-top:2px" aria-hidden="true"> <svg class="ph-icon" style="width:20px;height:20px;flex-shrink:0;color:${c.text};margin-top:2px" aria-hidden="true">
<use href="/icons/phosphor.svg#${_esc(s.icon)}"></use> <use href="/icons/phosphor.svg#${UI.escape(s.icon)}"></use>
</svg> </svg>
<div style="min-width:0"> <div style="min-width:0">
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:${c.text};margin-bottom:2px"> <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:${c.text};margin-bottom:2px">
${_esc(s.title)} ${UI.escape(s.title)}
</div> </div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
${_esc(s.text)} ${UI.escape(s.text)}
</div> </div>
</div> </div>
</div> </div>
@ -1074,7 +1074,7 @@ window.Page_uebungen = (() => {
); );
if (!allExercises.length) { if (!allExercises.length) {
return `<div style="padding:var(--space-8);text-align:center;color:var(--c-text-muted)"> return `<div style="padding:var(--space-8);text-align:center;color:var(--c-text-muted)">
${UI.icon('magnifying-glass')} Keine Übungen gefunden für ${_esc(q)}" ${UI.icon('magnifying-glass')} Keine Übungen gefunden für ${UI.escape(q)}"
</div>`; </div>`;
} }
return `<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)"> return `<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
@ -1082,7 +1082,7 @@ window.Page_uebungen = (() => {
<div> <div>
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold); <div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.05em; color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.05em;
margin-bottom:var(--space-1);padding:0 var(--space-2)">${_esc(u.kategorie)}</div> margin-bottom:var(--space-1);padding:0 var(--space-2)">${UI.escape(u.kategorie)}</div>
${_renderCard(u, i)} ${_renderCard(u, i)}
</div>`).join('')} </div>`).join('')}
</div>`; </div>`;
@ -1114,25 +1114,25 @@ window.Page_uebungen = (() => {
const hasBody = (u.schritte?.length > 0) || (u.fehler?.length > 0) || !!u.steigerung || !!u.tipp; const hasBody = (u.schritte?.length > 0) || (u.fehler?.length > 0) || !!u.steigerung || !!u.tipp;
return ` return `
<div class="card" style="padding:0;overflow:hidden" data-exercise-name="${_esc(u.name)}" data-exercise-id="${_esc(_progressKey(_activeTab, u.name))}"> <div class="card" style="padding:0;overflow:hidden" data-exercise-name="${UI.escape(u.name)}" data-exercise-id="${UI.escape(_progressKey(_activeTab, u.name))}">
<!-- Header --> <!-- Header -->
<div style="padding:var(--space-4) var(--space-4) var(--space-3)"> <div style="padding:var(--space-4) var(--space-4) var(--space-3)">
<!-- Zeile 1: Name + Schwierigkeits-Badge --> <!-- Zeile 1: Name + Schwierigkeits-Badge -->
<div style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-2);margin-bottom:var(--space-2)"> <div style="display:flex;align-items:baseline;justify-content:space-between;gap:var(--space-2);margin-bottom:var(--space-2)">
<span style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);line-height:1.3"> <span style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);line-height:1.3">
${_esc(u.name)} ${UI.escape(u.name)}
</span> </span>
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);white-space:nowrap; <span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);white-space:nowrap;
padding:2px var(--space-2);border-radius:var(--radius-sm);flex-shrink:0; padding:2px var(--space-2);border-radius:var(--radius-sm);flex-shrink:0;
background:${diff.color}22;color:${diff.color}"> background:${diff.color}22;color:${diff.color}">
${_esc(diff.label)} ${UI.escape(diff.label)}
</span> </span>
</div> </div>
<!-- Zeile 2: Aktions-Buttons --> <!-- Zeile 2: Aktions-Buttons -->
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2);flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2);flex-wrap:wrap">
<button class="ueb-log-btn" <button class="ueb-log-btn"
data-tab="${_esc(_activeTab)}" data-tab="${UI.escape(_activeTab)}"
data-name="${_esc(u.name)}" data-name="${UI.escape(u.name)}"
title="Trainingseinheit loggen" title="Trainingseinheit loggen"
style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light); style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
color:var(--c-primary);cursor:pointer;padding:3px 9px; color:var(--c-primary);cursor:pointer;padding:3px 9px;
@ -1146,8 +1146,8 @@ window.Page_uebungen = (() => {
</button> </button>
${_sessionStatsChip(_activeTab, u.name)} ${_sessionStatsChip(_activeTab, u.name)}
<button class="ueb-notiz-btn" <button class="ueb-notiz-btn"
data-tab="${_esc(_activeTab)}" data-tab="${UI.escape(_activeTab)}"
data-name="${_esc(u.name)}" data-name="${UI.escape(u.name)}"
title="Notiz hinzufügen" title="Notiz hinzufügen"
style="background:none;border:1px solid var(--c-border);cursor:pointer; style="background:none;border:1px solid var(--c-border);cursor:pointer;
padding:3px 7px;border-radius:var(--radius-sm); padding:3px 7px;border-radius:var(--radius-sm);
@ -1159,9 +1159,9 @@ window.Page_uebungen = (() => {
Notiz Notiz
</button> </button>
<button class="ueb-status-btn" <button class="ueb-status-btn"
data-tab="${_esc(_activeTab)}" data-tab="${UI.escape(_activeTab)}"
data-name="${_esc(u.name)}" data-name="${UI.escape(u.name)}"
title="${_esc(sm.label)}" title="${UI.escape(sm.label)}"
style="background:none;border:none;cursor:pointer;padding:2px; style="background:none;border:none;cursor:pointer;padding:2px;
display:flex;align-items:center;gap:4px; display:flex;align-items:center;gap:4px;
font-size:var(--text-xs);color:${sm.color}; font-size:var(--text-xs);color:${sm.color};
@ -1170,7 +1170,7 @@ window.Page_uebungen = (() => {
<use href="/icons/phosphor.svg#${sm.icon}"></use> <use href="/icons/phosphor.svg#${sm.icon}"></use>
</svg> </svg>
<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap"> <span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">
${_esc(sm.label)} ${UI.escape(sm.label)}
</span> </span>
</button> </button>
</div> </div>
@ -1178,20 +1178,20 @@ window.Page_uebungen = (() => {
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:${u.beschreibung ? 'var(--space-3)' : '0'}"> <div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:${u.beschreibung ? 'var(--space-3)' : '0'}">
<span class="text-xs-secondary"> <span class="text-xs-secondary">
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#clock"></use></svg> <svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#clock"></use></svg>
${_esc(u.dauer)} ${UI.escape(u.dauer)}
</span> </span>
<span class="text-xs-secondary"> <span class="text-xs-secondary">
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg> <svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
${_esc(u.alter)} ${UI.escape(u.alter)}
</span> </span>
<span class="text-xs-secondary"> <span class="text-xs-secondary">
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#package"></use></svg> <svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#package"></use></svg>
${_esc(u.material)} ${UI.escape(u.material)}
</span> </span>
</div> </div>
${u.beschreibung ? ` ${u.beschreibung ? `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5;margin:0"> <p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5;margin:0">
${_esc(u.beschreibung)} ${UI.escape(u.beschreibung)}
</p> </p>
` : ''} ` : ''}
${u.hinweis ? ` ${u.hinweis ? `
@ -1200,7 +1200,7 @@ window.Page_uebungen = (() => {
font-size:var(--text-xs);color:var(--c-text);line-height:1.4; font-size:var(--text-xs);color:var(--c-text);line-height:1.4;
display:flex;align-items:flex-start;gap:var(--space-2)"> display:flex;align-items:flex-start;gap:var(--space-2)">
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg> <svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0;margin-top:1px;color:var(--c-warning)" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
<span>${_esc(u.hinweis)}</span> <span>${UI.escape(u.hinweis)}</span>
</div> </div>
` : ''} ` : ''}
</div> </div>
@ -1225,7 +1225,7 @@ window.Page_uebungen = (() => {
</p> </p>
<ol style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)"> <ol style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
${u.schritte.map(s => ` ${u.schritte.map(s => `
<li style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${_esc(s)}</li> <li style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${UI.escape(s)}</li>
`).join('')} `).join('')}
</ol> </ol>
` : ''} ` : ''}
@ -1237,7 +1237,7 @@ window.Page_uebungen = (() => {
</p> </p>
<ul style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)"> <ul style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
${u.fehler.map(f => ` ${u.fehler.map(f => `
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${_esc(f)}</li> <li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${UI.escape(f)}</li>
`).join('')} `).join('')}
</ul> </ul>
` : ''} ` : ''}
@ -1247,14 +1247,14 @@ window.Page_uebungen = (() => {
<svg class="ph-icon" style="width:14px;height:14px;color:var(--c-primary)" aria-hidden="true"> <svg class="ph-icon" style="width:14px;height:14px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#arrow-right"></use> <use href="/icons/phosphor.svg#arrow-right"></use>
</svg> </svg>
<strong>Steigerung:</strong> ${_esc(u.steigerung)} <strong>Steigerung:</strong> ${UI.escape(u.steigerung)}
</div> </div>
` : ''} ` : ''}
${u.tipp ? ` ${u.tipp ? `
<div style="margin-top:var(--space-3);padding:var(--space-2) var(--space-3); <div style="margin-top:var(--space-3);padding:var(--space-2) var(--space-3);
background:var(--c-primary-subtle);border-radius:var(--radius-sm); background:var(--c-primary-subtle);border-radius:var(--radius-sm);
font-size:var(--text-xs);color:var(--c-text-secondary)"> font-size:var(--text-xs);color:var(--c-text-secondary)">
💡 ${_esc(u.tipp)} 💡 ${UI.escape(u.tipp)}
</div>` : ''} </div>` : ''}
</div> </div>
` : ''} ` : ''}
@ -1281,7 +1281,7 @@ window.Page_uebungen = (() => {
<svg class="ph-icon" style="width:18px;height:18px;flex-shrink:0" aria-hidden="true"> <svg class="ph-icon" style="width:18px;height:18px;flex-shrink:0" aria-hidden="true">
<use href="/icons/phosphor.svg#${sm.icon}"></use> <use href="/icons/phosphor.svg#${sm.icon}"></use>
</svg> </svg>
${next ? `<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">${_esc(sm.label)}</span>` : ''} ${next ? `<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">${UI.escape(sm.label)}</span>` : ''}
`; `;
}); });
}); });
@ -1344,7 +1344,7 @@ window.Page_uebungen = (() => {
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text); <h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
margin:0 0 var(--space-4);text-align:center"> margin:0 0 var(--space-4);text-align:center">
Notiz: ${_esc(exerciseName)} Notiz: ${UI.escape(exerciseName)}
</h3> </h3>
<div style="display:flex;flex-direction:column;gap:var(--space-4)"> <div style="display:flex;flex-direction:column;gap:var(--space-4)">
@ -1359,7 +1359,7 @@ window.Page_uebungen = (() => {
border-radius:var(--radius-md);font-size:var(--text-sm); border-radius:var(--radius-md);font-size:var(--text-sm);
font-family:var(--font-sans);background:var(--c-surface); font-family:var(--font-sans);background:var(--c-surface);
color:var(--c-text);resize:vertical;outline:none; color:var(--c-text);resize:vertical;outline:none;
line-height:1.5">${_esc(noteText)}</textarea> line-height:1.5">${UI.escape(noteText)}</textarea>
</div> </div>
<!-- Erfolgsquote --> <!-- Erfolgsquote -->
@ -1569,7 +1569,7 @@ window.Page_uebungen = (() => {
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text); <h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
margin:0 0 var(--space-4);text-align:center"> margin:0 0 var(--space-4);text-align:center">
Einheit loggen: ${_esc(exerciseName)} Einheit loggen: ${UI.escape(exerciseName)}
</h3> </h3>
<form id="${formId}" style="display:flex;flex-direction:column;gap:var(--space-4)"> <form id="${formId}" style="display:flex;flex-direction:column;gap:var(--space-4)">
@ -1620,7 +1620,7 @@ window.Page_uebungen = (() => {
border-radius:var(--radius-md);border:1.5px solid var(--c-border); border-radius:var(--radius-md);border:1.5px solid var(--c-border);
background:var(--c-surface-2);cursor:pointer;transition:all 0.15s"> background:var(--c-surface-2);cursor:pointer;transition:all 0.15s">
${emoji} ${emoji}
<span style="font-size:9px;color:var(--c-text-secondary)">${_esc(val)}</span> <span style="font-size:9px;color:var(--c-text-secondary)">${UI.escape(val)}</span>
</button> </button>
`).join('')} `).join('')}
</div> </div>
@ -2103,7 +2103,7 @@ window.Page_uebungen = (() => {
: ''; : '';
const noteHtml = s.notiz const noteHtml = s.notiz
? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:3px; ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:3px;
line-height:1.4;font-style:italic">${_esc(s.notiz)}</div>` line-height:1.4;font-style:italic">${UI.escape(s.notiz)}</div>`
: ''; : '';
return ` return `
<div style="display:flex;align-items:flex-start;gap:var(--space-3); <div style="display:flex;align-items:flex-start;gap:var(--space-3);
@ -2113,7 +2113,7 @@ window.Page_uebungen = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap"> <div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text)">${_esc(s.exercise_name)}</span> color:var(--c-text)">${UI.escape(s.exercise_name)}</span>
${topBadge} ${topBadge}
</div> </div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:1px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:1px">
@ -2146,7 +2146,7 @@ window.Page_uebungen = (() => {
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold); <div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
color:var(--c-text-secondary);text-transform:uppercase; color:var(--c-text-secondary);text-transform:uppercase;
letter-spacing:.05em;padding:var(--space-2) 0 var(--space-1)"> letter-spacing:.05em;padding:var(--space-2) 0 var(--space-1)">
${_esc(label)} ${UI.escape(label)}
</div> </div>
<div style="display:flex;flex-direction:column;gap:var(--space-1)"> <div style="display:flex;flex-direction:column;gap:var(--space-1)">
${sessions.map(_sessionRow).join('')} ${sessions.map(_sessionRow).join('')}
@ -2216,7 +2216,7 @@ window.Page_uebungen = (() => {
<div style="display:flex;align-items:center;gap:var(--space-2); <div style="display:flex;align-items:center;gap:var(--space-2);
padding:4px var(--space-2);border-radius:var(--radius-sm); padding:4px var(--space-2);border-radius:var(--radius-sm);
font-size:var(--text-xs);color:var(--c-text-secondary)"> font-size:var(--text-xs);color:var(--c-text-secondary)">
<span style="flex-shrink:0;min-width:52px">${_esc(dateLabel)}</span> <span style="flex-shrink:0;min-width:52px">${UI.escape(dateLabel)}</span>
<span style="flex-shrink:0">${erfolg}</span> <span style="flex-shrink:0">${erfolg}</span>
<span style="flex-shrink:0">${s.erfolgsquote}%${top}</span> <span style="flex-shrink:0">${s.erfolgsquote}%${top}</span>
<span class="flex-1-min">${s.wiederholungen}× Wdh.${stimmung ? ' ' + stimmung : ''}</span> <span class="flex-1-min">${s.wiederholungen}× Wdh.${stimmung ? ' ' + stimmung : ''}</span>
@ -2244,12 +2244,12 @@ window.Page_uebungen = (() => {
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold); <div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
color:var(--c-text);line-height:1.3; color:var(--c-text);line-height:1.3;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
${_esc(ex.name)} ${UI.escape(ex.name)}
</div> </div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
${ex.sessions.length} Einheit${ex.sessions.length !== 1 ? 'en' : ''} ${ex.sessions.length} Einheit${ex.sessions.length !== 1 ? 'en' : ''}
${ex.topCount ? ' · ' + ex.topCount + '× TOP' : ''} ${ex.topCount ? ' · ' + ex.topCount + '× TOP' : ''}
· ${_esc(lastLabel)} · ${UI.escape(lastLabel)}
</div> </div>
</div> </div>
<!-- Chevron --> <!-- Chevron -->
@ -2350,8 +2350,8 @@ window.Page_uebungen = (() => {
['Ablenkungstraining', 'Wieder häufiger (höhere Anforderung)'], ['Ablenkungstraining', 'Wieder häufiger (höhere Anforderung)'],
].map(([p, b], i) => ` ].map(([p, b], i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}"> <tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text);border-bottom:1px solid var(--c-border)">${_esc(p)}</td> <td style="padding:var(--space-2) var(--space-3);color:var(--c-text);border-bottom:1px solid var(--c-border)">${UI.escape(p)}</td>
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${_esc(b)}</td> <td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${UI.escape(b)}</td>
</tr> </tr>
`).join('')} `).join('')}
</tbody> </tbody>
@ -2391,9 +2391,9 @@ window.Page_uebungen = (() => {
].map(([s, b, c], i) => ` ].map(([s, b, c], i) => `
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}"> <tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
<td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)"> <td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">
<span style="font-weight:var(--weight-semibold);color:${c}">${_esc(s)}</span> <span style="font-weight:var(--weight-semibold);color:${c}">${UI.escape(s)}</span>
</td> </td>
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${_esc(b)}</td> <td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${UI.escape(b)}</td>
</tr> </tr>
`).join('')} `).join('')}
</tbody> </tbody>
@ -2425,7 +2425,7 @@ window.Page_uebungen = (() => {
color:${ok ? 'var(--c-success)' : 'var(--c-danger)'}" aria-hidden="true"> color:${ok ? 'var(--c-success)' : 'var(--c-danger)'}" aria-hidden="true">
<use href="/icons/phosphor.svg#${ok ? 'check-circle' : 'x-circle'}"></use> <use href="/icons/phosphor.svg#${ok ? 'check-circle' : 'x-circle'}"></use>
</svg> </svg>
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${_esc(text)}</span> <span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${UI.escape(text)}</span>
</li> </li>
`).join('')} `).join('')}
</ul> </ul>
@ -2438,15 +2438,6 @@ window.Page_uebungen = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// HELPER // HELPER
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// PUBLIC // PUBLIC
// ---------------------------------------------------------- // ----------------------------------------------------------

View file

@ -368,7 +368,7 @@ window.Page_wetter = (() => {
box-shadow:${shadow};transform:${transform}; box-shadow:${shadow};transform:${transform};
transition:all .15s;user-select:none"> transition:all .15s;user-select:none">
<span style="font-size:var(--text-xs);font-weight:600; <span style="font-size:var(--text-xs);font-weight:600;
margin-bottom:var(--space-1)">${_esc(dayName)}</span> margin-bottom:var(--space-1)">${UI.escape(dayName)}</span>
<div style="margin-bottom:var(--space-1)">${_wmoIcon(d.weathercode, '1.5rem', active ? 'filter:brightness(0) invert(1)' : '')}</div> <div style="margin-bottom:var(--space-1)">${_wmoIcon(d.weathercode, '1.5rem', active ? 'filter:brightness(0) invert(1)' : '')}</div>
<span class="wttr-temp" <span class="wttr-temp"
style="font-size:var(--text-xs);color:${textSec};white-space:nowrap"> style="font-size:var(--text-xs);color:${textSec};white-space:nowrap">
@ -414,13 +414,13 @@ window.Page_wetter = (() => {
: 0; : 0;
} }
const locName = _data.location_name ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">${_esc(_data.location_name)}</div>` : ''; const locName = _data.location_name ? `<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:2px">${UI.escape(_data.location_name)}</div>` : '';
el.innerHTML = ` el.innerHTML = `
${locName} ${locName}
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-4)"> <div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-4)">
${_wmoIcon(d.weathercode, '3.5rem')} ${_wmoIcon(d.weathercode, '3.5rem')}
<div> <div>
<div style="font-weight:700;font-size:var(--text-lg)">${_esc(desc)}</div> <div style="font-weight:700;font-size:var(--text-lg)">${UI.escape(desc)}</div>
<div style="font-size:var(--text-2xl);font-weight:800;color:var(--c-primary);line-height:1.1"> <div style="font-size:var(--text-2xl);font-weight:800;color:var(--c-primary);line-height:1.1">
${Math.round(d.temp_max)}° ${Math.round(d.temp_max)}°
<span style="font-size:var(--text-base);font-weight:400;color:var(--c-text-secondary)"> <span style="font-size:var(--text-base);font-weight:400;color:var(--c-text-secondary)">
@ -445,10 +445,10 @@ window.Page_wetter = (() => {
margin-bottom:var(--space-1)"> margin-bottom:var(--space-1)">
<span style="display:flex;align-items:center;gap:4px"> <span style="display:flex;align-items:center;gap:4px">
<svg class="ph-icon" style="width:14px;height:14px;color:#F97316"><use href="/icons/phosphor.svg#sun-horizon"></use></svg> <svg class="ph-icon" style="width:14px;height:14px;color:#F97316"><use href="/icons/phosphor.svg#sun-horizon"></use></svg>
${_esc(sunriseStr)} ${UI.escape(sunriseStr)}
</span> </span>
<span style="display:flex;align-items:center;gap:4px"> <span style="display:flex;align-items:center;gap:4px">
${_esc(sunsetStr)} ${UI.escape(sunsetStr)}
<svg class="ph-icon" style="width:14px;height:14px;color:#7C3AED"><use href="/icons/phosphor.svg#moon-stars"></use></svg> <svg class="ph-icon" style="width:14px;height:14px;color:#7C3AED"><use href="/icons/phosphor.svg#moon-stars"></use></svg>
</span> </span>
</div> </div>
@ -469,9 +469,9 @@ window.Page_wetter = (() => {
</span> </span>
<div class="flex-1"> <div class="flex-1">
<div style="font-size:var(--text-sm);font-weight:600"> <div style="font-size:var(--text-sm);font-weight:600">
${_esc(compass)} · ${Math.round(d.wind_kmh ?? 0)} km/h ${UI.escape(compass)} · ${Math.round(d.wind_kmh ?? 0)} km/h
</div> </div>
<div class="text-xs-secondary">${_esc(bft)}</div> <div class="text-xs-secondary">${UI.escape(bft)}</div>
</div> </div>
${d.precip_sum != null ? ` ${d.precip_sum != null ? `
<div class="text-right"> <div class="text-right">
@ -488,7 +488,7 @@ window.Page_wetter = (() => {
font-size:var(--text-xs);margin-bottom:4px"> font-size:var(--text-xs);margin-bottom:4px">
<span class="text-secondary">UV-Index</span> <span class="text-secondary">UV-Index</span>
<span style="font-weight:600;color:${uvColor}"> <span style="font-weight:600;color:${uvColor}">
${d.uv_index ?? 0} ${_esc(uvLabel)} ${d.uv_index ?? 0} ${UI.escape(uvLabel)}
</span> </span>
</div> </div>
<div style="height:6px;border-radius:999px;background:var(--c-border);overflow:hidden"> <div style="height:6px;border-radius:999px;background:var(--c-border);overflow:hidden">
@ -596,7 +596,7 @@ window.Page_wetter = (() => {
Niederschlagswahrscheinlichkeit Niederschlagswahrscheinlichkeit
</span> </span>
<span style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-left:auto"> <span style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-left:auto">
${_selDay === 0 ? 'heute' : _esc(d.date ? new Date(d.date + 'T12:00').toLocaleDateString('de', {weekday:'short', day:'numeric', month:'short'}) : '')} ${_selDay === 0 ? 'heute' : UI.escape(d.date ? new Date(d.date + 'T12:00').toLocaleDateString('de', {weekday:'short', day:'numeric', month:'short'}) : '')}
</span> </span>
</div> </div>
<!-- Baseline --> <!-- Baseline -->
@ -648,10 +648,10 @@ window.Page_wetter = (() => {
margin-bottom:var(--space-4);text-align:center"> margin-bottom:var(--space-4);text-align:center">
<div style="font-size:2rem;line-height:1;margin-bottom:4px">${_wl.emoji}</div> <div style="font-size:2rem;line-height:1;margin-bottom:4px">${_wl.emoji}</div>
<div style="font-weight:800;font-size:var(--text-lg);color:${_wl.color};line-height:1.2"> <div style="font-weight:800;font-size:var(--text-lg);color:${_wl.color};line-height:1.2">
${_esc(_wl.label)} ${UI.escape(_wl.label)}
</div> </div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px">
${_esc(_wl.sub)} ${UI.escape(_wl.sub)}
</div> </div>
</div> </div>
<h3 style="font-size:var(--text-base);font-weight:700;margin-bottom:var(--space-4)"> <h3 style="font-size:var(--text-base);font-weight:700;margin-bottom:var(--space-4)">
@ -670,10 +670,10 @@ window.Page_wetter = (() => {
<svg class="ph-icon" style="width:1.3rem;height:1.3rem;flex-shrink:0;color:var(--c-primary)"><use href="/icons/phosphor.svg#paw-print"></use></svg> <svg class="ph-icon" style="width:1.3rem;height:1.3rem;flex-shrink:0;color:var(--c-primary)"><use href="/icons/phosphor.svg#paw-print"></use></svg>
<div class="flex-1"> <div class="flex-1">
<div style="font-weight:600;font-size:var(--text-sm);color:${aspColor}"> <div style="font-weight:600;font-size:var(--text-sm);color:${aspColor}">
Asphalt ~${Math.round(d.asphalt_temp)}°C ${_esc(aspText)} Asphalt ~${Math.round(d.asphalt_temp)}°C ${UI.escape(aspText)}
</div> </div>
${aspAdvice ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> ${aspAdvice ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
${_esc(aspAdvice)} ${UI.escape(aspAdvice)}
</div>` : ''} </div>` : ''}
</div> </div>
</div> </div>
@ -735,7 +735,7 @@ window.Page_wetter = (() => {
padding:3px 10px;background:${col}22; padding:3px 10px;background:${col}22;
border:1px solid ${col}55;color:${col};font-weight:600"> border:1px solid ${col}55;color:${col};font-weight:600">
<span style="width:6px;height:6px;border-radius:50%;background:${col};display:inline-block"></span> <span style="width:6px;height:6px;border-radius:50%;background:${col};display:inline-block"></span>
${_esc(name)}: ${_esc(lbl)} ${UI.escape(name)}: ${UI.escape(lbl)}
</span>`; </span>`;
}).join('')} }).join('')}
</div> </div>
@ -756,7 +756,7 @@ window.Page_wetter = (() => {
<div class="flex-1"> <div class="flex-1">
<span style="font-size:var(--text-sm);font-weight:600">Zecken-Risiko: </span> <span style="font-size:var(--text-sm);font-weight:600">Zecken-Risiko: </span>
<span style="font-size:var(--text-sm);color:${tickColor};font-weight:700"> <span style="font-size:var(--text-sm);color:${tickColor};font-weight:700">
${_esc(tickLabel)} ${UI.escape(tickLabel)}
</span> </span>
</div> </div>
</div> </div>
@ -786,7 +786,7 @@ window.Page_wetter = (() => {
<svg class="ph-icon" style="width:1.3rem;height:1.3rem;flex-shrink:0;color:${fellHint.color}"> <svg class="ph-icon" style="width:1.3rem;height:1.3rem;flex-shrink:0;color:${fellHint.color}">
<use href="/icons/phosphor.svg#${fellHint.icon}"></use> <use href="/icons/phosphor.svg#${fellHint.icon}"></use>
</svg> </svg>
<div class="text-sm">${_esc(fellHint.text)}</div> <div class="text-sm">${UI.escape(fellHint.text)}</div>
</div> </div>
`; `;
} }
@ -876,7 +876,7 @@ window.Page_wetter = (() => {
</span> </span>
<span class="text-xs-secondary">/&nbsp;10</span> <span class="text-xs-secondary">/&nbsp;10</span>
<span style="font-size:var(--text-xs);font-weight:600;color:${color}; <span style="font-size:var(--text-xs);font-weight:600;color:${color};
white-space:nowrap"> ${_esc(text)}</span> white-space:nowrap"> ${UI.escape(text)}</span>
</div> </div>
`; `;
} }
@ -912,7 +912,7 @@ window.Page_wetter = (() => {
padding:3px 10px;background:${s.color}22; padding:3px 10px;background:${s.color}22;
border:1px solid ${s.color}55;color:${s.color};font-weight:600"> border:1px solid ${s.color}55;color:${s.color};font-weight:600">
<svg class="ph-icon" style="width:12px;height:12px"><use href="/icons/phosphor.svg#nose"></use></svg> <svg class="ph-icon" style="width:12px;height:12px"><use href="/icons/phosphor.svg#nose"></use></svg>
Schnüffel: ${_esc(s.label)} Schnüffel: ${UI.escape(s.label)}
</span> </span>
`; `;
} }
@ -1072,15 +1072,6 @@ window.Page_wetter = (() => {
return ['hoch', '#F44336']; return ['hoch', '#F44336'];
} }
function _esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// MEINE WETTERREKORDE // MEINE WETTERREKORDE
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -1116,15 +1107,15 @@ window.Page_wetter = (() => {
display:flex;align-items:center;gap:3px;font-weight:700; display:flex;align-items:center;gap:3px;font-weight:700;
text-transform:uppercase;letter-spacing:.04em"> text-transform:uppercase;letter-spacing:.04em">
<span>${emoji}</span> <span>${emoji}</span>
<span>${_esc(title)}</span> <span>${UI.escape(title)}</span>
</div> </div>
<div style="font-size:var(--text-lg);font-weight:800;color:${color};line-height:1.1"> <div style="font-size:var(--text-lg);font-weight:800;color:${color};line-height:1.1">
${_esc(value)} ${UI.escape(value)}
</div> </div>
<div style="font-size:10px;color:var(--c-text-secondary); <div style="font-size:10px;color:var(--c-text-secondary);
overflow:hidden;display:-webkit-box; overflow:hidden;display:-webkit-box;
-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.3"> -webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.3">
${_esc(subtitle)} ${UI.escape(subtitle)}
</div> </div>
</div> </div>
`; `;

View file

@ -49,9 +49,9 @@ window.Page_widget = (() => {
const photoHtml = photo const photoHtml = photo
? `<div class="widget-photo-wrap"> ? `<div class="widget-photo-wrap">
<img src="${_esc(photo.media_url)}" alt="${_esc(photo.titel || '')}" class="widget-photo"> <img src="${UI.escape(photo.media_url)}" alt="${UI.escape(photo.titel || '')}" class="widget-photo">
<div class="widget-photo-caption"> <div class="widget-photo-caption">
${_esc(photo.titel || '')} ${UI.escape(photo.titel || '')}
<span class="widget-photo-date">${_fmtDate(photo.datum)}</span> <span class="widget-photo-date">${_fmtDate(photo.datum)}</span>
</div> </div>
</div>` </div>`
@ -64,7 +64,7 @@ window.Page_widget = (() => {
? `<div class="widget-reminder"> ? `<div class="widget-reminder">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-check"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-check"></use></svg>
<div> <div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">${_esc(rem.bezeichnung)}</div> <div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">${UI.escape(rem.bezeichnung)}</div>
<div class="text-xs-muted">${_fmtDate(rem.naechstes)}</div> <div class="text-xs-muted">${_fmtDate(rem.naechstes)}</div>
</div> </div>
</div>` </div>`
@ -79,7 +79,7 @@ window.Page_widget = (() => {
</div>`; </div>`;
const dogAvatar = dog.foto_url const dogAvatar = dog.foto_url
? `<img src="${_esc(dog.foto_url)}" class="widget-dog-av" alt="${_esc(dog.name)}">` ? `<img src="${UI.escape(dog.foto_url)}" class="widget-dog-av" alt="${UI.escape(dog.name)}">`
: `<div class="widget-dog-av widget-dog-av--placeholder">🐕</div>`; : `<div class="widget-dog-av widget-dog-av--placeholder">🐕</div>`;
_container.innerHTML = ` _container.innerHTML = `
@ -89,8 +89,8 @@ window.Page_widget = (() => {
<div class="widget-dog-row"> <div class="widget-dog-row">
${dogAvatar} ${dogAvatar}
<div> <div>
<div style="font-weight:var(--weight-bold);font-size:var(--text-lg)">${_esc(dog.name)}</div> <div style="font-weight:var(--weight-bold);font-size:var(--text-lg)">${UI.escape(dog.name)}</div>
${dog.rasse ? `<div class="text-sm-muted">${_esc(dog.rasse)}</div>` : ''} ${dog.rasse ? `<div class="text-sm-muted">${UI.escape(dog.rasse)}</div>` : ''}
</div> </div>
<button class="btn btn-secondary btn-sm" id="widget-refresh-btn" style="margin-left:auto" <button class="btn btn-secondary btn-sm" id="widget-refresh-btn" style="margin-left:auto"
title="Neues Zufallsbild"> title="Neues Zufallsbild">
@ -126,12 +126,6 @@ window.Page_widget = (() => {
_container.querySelector('#widget-refresh-btn')?.addEventListener('click', () => _render()); _container.querySelector('#widget-refresh-btn')?.addEventListener('click', () => _render());
} }
function _esc(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function _fmtDate(d) { function _fmtDate(d) {
if (!d) return ''; if (!d) return '';
try { try {

View file

@ -137,7 +137,7 @@ window.Page_wiki = (() => {
try { try {
subs = await _apiFetch('/api/wiki/foto-submissions'); subs = await _apiFetch('/api/wiki/foto-submissions');
} catch (e) { } catch (e) {
el.innerHTML = `<div class="empty-state"><p>${_esc(e.message)}</p></div>`; el.innerHTML = `<div class="empty-state"><p>${UI.escape(e.message)}</p></div>`;
return; return;
} }
@ -163,16 +163,16 @@ window.Page_wiki = (() => {
${subs.map(s => ` ${subs.map(s => `
<div class="card" style="margin-bottom:var(--space-3);padding:var(--space-3)" id="wiki-sub-${s.id}"> <div class="card" style="margin-bottom:var(--space-3);padding:var(--space-3)" id="wiki-sub-${s.id}">
<div style="display:flex;gap:var(--space-3);align-items:flex-start"> <div style="display:flex;gap:var(--space-3);align-items:flex-start">
<img src="${_esc(s.foto_url)}" alt="" <img src="${UI.escape(s.foto_url)}" alt=""
style="width:100px;height:80px;object-fit:cover;border-radius:var(--radius-md);flex-shrink:0;background:var(--c-surface-2)"> style="width:100px;height:80px;object-fit:cover;border-radius:var(--radius-md);flex-shrink:0;background:var(--c-surface-2)">
<div class="flex-1-min"> <div class="flex-1-min">
<div style="font-weight:var(--weight-semibold)">${_esc(s.rasse_name)}</div> <div style="font-weight:var(--weight-semibold)">${UI.escape(s.rasse_name)}</div>
<div class="text-xs-muted"> <div class="text-xs-muted">
von ${_esc(s.user_name)} · ${_formatDate(s.created_at)} von ${UI.escape(s.user_name)} · ${_formatDate(s.created_at)}
</div> </div>
${s.aktuell_foto ${s.aktuell_foto
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px"> ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:4px">
Aktuelles Foto: <img src="${_esc(s.aktuell_foto)}" style="height:20px;vertical-align:middle;border-radius:2px"> Aktuelles Foto: <img src="${UI.escape(s.aktuell_foto)}" style="height:20px;vertical-align:middle;border-radius:2px">
</div>` </div>`
: `<div style="font-size:var(--text-xs);color:var(--c-warning);margin-top:4px">Kein Foto vorhanden</div>` : `<div style="font-size:var(--text-xs);color:var(--c-warning);margin-top:4px">Kein Foto vorhanden</div>`
} }
@ -335,7 +335,7 @@ window.Page_wiki = (() => {
// Preserve current selection // Preserve current selection
const cur = _currentGruppe; const cur = _currentGruppe;
sel.innerHTML = `<option value="">Alle Gruppen</option>` + sel.innerHTML = `<option value="">Alle Gruppen</option>` +
_gruppen.map(g => `<option value="${_esc(g)}"${g === cur ? ' selected' : ''}>${_esc(g)}</option>`).join(''); _gruppen.map(g => `<option value="${UI.escape(g)}"${g === cur ? ' selected' : ''}>${UI.escape(g)}</option>`).join('');
} }
} }
@ -389,24 +389,24 @@ window.Page_wiki = (() => {
? fotoUrl.replace(/\.(jpe?g|png|gif|webp)$/i, '_preview.webp') ? fotoUrl.replace(/\.(jpe?g|png|gif|webp)$/i, '_preview.webp')
: fotoUrl; : fotoUrl;
const photoHtml = fotoUrl const photoHtml = fotoUrl
? `<img class="wiki-breed-photo" src="${_esc(srcUrl)}" loading="lazy" alt="${_esc(r.name)}" ? `<img class="wiki-breed-photo" src="${UI.escape(srcUrl)}" loading="lazy" alt="${UI.escape(r.name)}"
onerror="if(this.src.includes('_preview')){this.src='${_esc(fotoUrl)}'}else{this.style.display='none';this.nextElementSibling.style.display='flex'}">` onerror="if(this.src.includes('_preview')){this.src='${UI.escape(fotoUrl)}'}else{this.style.display='none';this.nextElementSibling.style.display='flex'}">`
: ''; : '';
const fallbackHtml = `<div class="wiki-breed-photo-fallback" style="${fotoUrl ? 'display:none' : ''}">${_DOG_SILHOUETTE}</div>`; const fallbackHtml = `<div class="wiki-breed-photo-fallback" style="${fotoUrl ? 'display:none' : ''}">${_DOG_SILHOUETTE}</div>`;
return ` return `
<div class="wiki-breed-card" data-slug="${_esc(r.slug)}"> <div class="wiki-breed-card" data-slug="${UI.escape(r.slug)}">
<div class="wiki-breed-photo-wrap"> <div class="wiki-breed-photo-wrap">
${photoHtml} ${photoHtml}
${fallbackHtml} ${fallbackHtml}
</div> </div>
<div class="wiki-breed-card-body"> <div class="wiki-breed-card-body">
<div class="wiki-breed-card-name">${_esc(r.name)}</div> <div class="wiki-breed-card-name">${UI.escape(r.name)}</div>
<div class="wiki-breed-card-gruppe">${_esc(r.gruppe || '—')}</div> <div class="wiki-breed-card-gruppe">${UI.escape(r.gruppe || '—')}</div>
<div class="wiki-breed-badges"> <div class="wiki-breed-badges">
<span class="wiki-badge-groesse wiki-badge-groesse--${_esc(r.groesse)}">${_groesseLabel(r.groesse)}</span> <span class="wiki-badge-groesse wiki-badge-groesse--${UI.escape(r.groesse)}">${_groesseLabel(r.groesse)}</span>
<span class="wiki-badge-aktivitaet wiki-badge-aktivitaet--${_esc(r.aktivitaet)}">${_aktivLabel(r.aktivitaet)}</span> <span class="wiki-badge-aktivitaet wiki-badge-aktivitaet--${UI.escape(r.aktivitaet)}">${_aktivLabel(r.aktivitaet)}</span>
<span class="wiki-badge-erfahrung wiki-badge-erfahrung--${_esc(r.erfahrung)}">${_erfahrungLabel(r.erfahrung)}</span> <span class="wiki-badge-erfahrung wiki-badge-erfahrung--${UI.escape(r.erfahrung)}">${_erfahrungLabel(r.erfahrung)}</span>
</div> </div>
</div> </div>
</div> </div>
@ -472,12 +472,12 @@ window.Page_wiki = (() => {
const rows = [ const rows = [
['Größe', _groesseLabel(rasse.groesse) || '&mdash;'], ['Größe', _groesseLabel(rasse.groesse) || '&mdash;'],
['Gewicht', gewicht], ['Gewicht', gewicht],
['Lebensdauer', _esc(rasse.lebensdauer) || '&mdash;'], ['Lebensdauer', UI.escape(rasse.lebensdauer) || '&mdash;'],
['Aktivität', _aktivLabel(rasse.aktivitaet) || '&mdash;'], ['Aktivität', _aktivLabel(rasse.aktivitaet) || '&mdash;'],
['Eignung', _erfahrungLabel(rasse.erfahrung) || '&mdash;'], ['Eignung', _erfahrungLabel(rasse.erfahrung) || '&mdash;'],
['Kinder', kinderLabel], ['Kinder', kinderLabel],
['Wohnung', wohnungLabel], ['Wohnung', wohnungLabel],
['FCI-Gruppe', _esc(rasse.gruppe) || '&mdash;'], ['FCI-Gruppe', UI.escape(rasse.gruppe) || '&mdash;'],
]; ];
return ` return `
@ -517,12 +517,12 @@ window.Page_wiki = (() => {
<div class="flex-gap-2"> <div class="flex-gap-2">
<button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-hat" <button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-hat"
style="flex:1;${hatStyle}" style="flex:1;${hatStyle}"
data-slug="${_esc(slug)}" data-typ="hat"> data-slug="${UI.escape(slug)}" data-typ="hat">
${isLoggedIn ? '' : '&#128274; '}Ich hab einen ${isLoggedIn ? '' : '&#128274; '}Ich hab einen
</button> </button>
<button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-will" <button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-will"
style="flex:1;${willStyle}" style="flex:1;${willStyle}"
data-slug="${_esc(slug)}" data-typ="will"> data-slug="${UI.escape(slug)}" data-typ="will">
${isLoggedIn ? '' : '&#128274; '}Ich will einen ${isLoggedIn ? '' : '&#128274; '}Ich will einen
</button> </button>
</div> </div>
@ -593,13 +593,13 @@ window.Page_wiki = (() => {
? `<p style="color:var(--c-text-secondary);font-size:var(--text-sm)">Noch keine Züchter eingetragen.</p>` ? `<p style="color:var(--c-text-secondary);font-size:var(--text-sm)">Noch keine Züchter eingetragen.</p>`
: zuchter.map(z => ` : zuchter.map(z => `
<div class="wiki-zuchter-card" style="padding:var(--space-3);border-radius:var(--radius-md);background:var(--c-surface-2);margin-bottom:var(--space-2)"> <div class="wiki-zuchter-card" style="padding:var(--space-3);border-radius:var(--radius-md);background:var(--c-surface-2);margin-bottom:var(--space-2)">
<div style="font-weight:var(--weight-semibold)">${_esc(z.name)} <div style="font-weight:var(--weight-semibold)">${UI.escape(z.name)}
${z.zwingername ? `<em style="font-weight:normal;color:var(--c-text-secondary)"> &bdquo;${_esc(z.zwingername)}&ldquo;</em>` : ''} ${z.zwingername ? `<em style="font-weight:normal;color:var(--c-text-secondary)"> &bdquo;${UI.escape(z.zwingername)}&ldquo;</em>` : ''}
${z.vdh_mitglied ? `<span class="badge badge-sm" style="margin-left:var(--space-1);background:var(--c-primary);color:#fff">VDH</span>` : ''} ${z.vdh_mitglied ? `<span class="badge badge-sm" style="margin-left:var(--space-1);background:var(--c-primary);color:#fff">VDH</span>` : ''}
</div> </div>
${(z.ort || z.bundesland) ? `<div class="text-sm-secondary">${[z.ort, z.bundesland].filter(Boolean).map(_esc).join(', ')}</div>` : ''} ${(z.ort || z.bundesland) ? `<div class="text-sm-secondary">${[z.ort, z.bundesland].filter(Boolean).map(_esc).join(', ')}</div>` : ''}
${z.beschreibung ? `<p style="font-size:var(--text-sm);margin-top:var(--space-1)">${_esc(z.beschreibung)}</p>` : ''} ${z.beschreibung ? `<p style="font-size:var(--text-sm);margin-top:var(--space-1)">${UI.escape(z.beschreibung)}</p>` : ''}
${z.website ? `<a href="${_esc(z.website)}" target="_blank" rel="noopener" style="font-size:var(--text-sm);color:var(--c-primary)">${_esc(z.website)}</a>` : ''} ${z.website ? `<a href="${UI.escape(z.website)}" target="_blank" rel="noopener" style="font-size:var(--text-sm);color:var(--c-primary)">${UI.escape(z.website)}</a>` : ''}
</div> </div>
`).join(''); `).join('');
@ -627,7 +627,7 @@ window.Page_wiki = (() => {
<label class="form-label">Bundesland</label> <label class="form-label">Bundesland</label>
<select class="form-control" name="bundesland"> <select class="form-control" name="bundesland">
<option value=""> bitte wählen </option> <option value=""> bitte wählen </option>
${DE_BUNDESLAENDER.map(bl => `<option value="${_esc(bl)}">${_esc(bl)}</option>`).join('')} ${DE_BUNDESLAENDER.map(bl => `<option value="${UI.escape(bl)}">${UI.escape(bl)}</option>`).join('')}
</select> </select>
</div> </div>
<div class="form-group" style="grid-column:1/-1"> <div class="form-group" style="grid-column:1/-1">
@ -731,7 +731,7 @@ window.Page_wiki = (() => {
// Temperament chips // Temperament chips
const chips = rasse.temperament const chips = rasse.temperament
? rasse.temperament.split(',').map(t => `<span class="wiki-trait-chip">${_esc(t.trim())}</span>`).join('') ? rasse.temperament.split(',').map(t => `<span class="wiki-trait-chip">${UI.escape(t.trim())}</span>`).join('')
: ''; : '';
const _dogSvgLg = _DOG_SILHOUETTE.replace('width="48" height="48"', 'width="56" height="56"'); const _dogSvgLg = _DOG_SILHOUETTE.replace('width="48" height="48"', 'width="56" height="56"');
@ -743,7 +743,7 @@ window.Page_wiki = (() => {
const photoHtml = allFotos.length const photoHtml = allFotos.length
? `<div class="wiki-gallery-wrap"> ? `<div class="wiki-gallery-wrap">
<img class="wiki-detail-photo wiki-gallery-main" id="wiki-main-photo" <img class="wiki-detail-photo wiki-gallery-main" id="wiki-main-photo"
src="${_esc(allFotos[0].foto_url)}" alt="${_esc(rasse.name)}" src="${UI.escape(allFotos[0].foto_url)}" alt="${UI.escape(rasse.name)}"
onerror="this.style.display='none';document.getElementById('wiki-photo-fallback').style.display='flex'"> onerror="this.style.display='none';document.getElementById('wiki-photo-fallback').style.display='flex'">
<div id="wiki-photo-fallback" class="wiki-detail-photo-placeholder hidden">${_dogSvgLg}<span>Kein Foto verfügbar</span></div> <div id="wiki-photo-fallback" class="wiki-detail-photo-placeholder hidden">${_dogSvgLg}<span>Kein Foto verfügbar</span></div>
${allFotos.length > 1 ? ` ${allFotos.length > 1 ? `
@ -751,10 +751,10 @@ window.Page_wiki = (() => {
${allFotos.map((f, i) => ` ${allFotos.map((f, i) => `
<button class="wiki-gallery-thumb${i === 0 ? ' active' : ''}" data-idx="${i}" <button class="wiki-gallery-thumb${i === 0 ? ' active' : ''}" data-idx="${i}"
aria-label="Foto ${i + 1}"> aria-label="Foto ${i + 1}">
<img src="${_esc(f.foto_url.startsWith('/media/') ? f.foto_url.replace(/\.(jpe?g|png|gif|webp)$/i,'_preview.webp') : f.foto_url)}" <img src="${UI.escape(f.foto_url.startsWith('/media/') ? f.foto_url.replace(/\.(jpe?g|png|gif|webp)$/i,'_preview.webp') : f.foto_url)}"
alt="" loading="lazy" alt="" loading="lazy"
onerror="if(this.src.includes('_preview')){this.src='${_esc(f.foto_url)}'}else{this.style.display='none'}"> onerror="if(this.src.includes('_preview')){this.src='${UI.escape(f.foto_url)}'}else{this.style.display='none'}">
${f.user_name ? `<span class="wiki-gallery-thumb-label">von ${_esc(f.user_name)}</span>` : ''} ${f.user_name ? `<span class="wiki-gallery-thumb-label">von ${UI.escape(f.user_name)}</span>` : ''}
</button>`).join('')} </button>`).join('')}
</div>` : ''} </div>` : ''}
<button class="wiki-gallery-expand" id="wiki-gallery-expand" aria-label="Vollbild"> <button class="wiki-gallery-expand" id="wiki-gallery-expand" aria-label="Vollbild">
@ -771,9 +771,9 @@ window.Page_wiki = (() => {
<div class="wiki-detail-hero" style="text-align:center;margin-bottom:var(--space-4)"> <div class="wiki-detail-hero" style="text-align:center;margin-bottom:var(--space-4)">
${photoHtml} ${photoHtml}
${userFotosHtml} ${userFotosHtml}
<h1 style="font-size:var(--text-xl);font-weight:var(--weight-bold);margin:var(--space-2) 0 var(--space-1)">${_esc(rasse.name)}</h1> <h1 style="font-size:var(--text-xl);font-weight:var(--weight-bold);margin:var(--space-2) 0 var(--space-1)">${UI.escape(rasse.name)}</h1>
${rasse.herkunft ? `<div class="text-sm-secondary">${UI.icon('map-pin')} ${_esc(rasse.herkunft)}</div>` : ''} ${rasse.herkunft ? `<div class="text-sm-secondary">${UI.icon('map-pin')} ${UI.escape(rasse.herkunft)}</div>` : ''}
${rasse.gruppe ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:2px">${_esc(rasse.gruppe)}</div>` : ''} ${rasse.gruppe ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:2px">${UI.escape(rasse.gruppe)}</div>` : ''}
</div> </div>
${/* 2. Charakter-Badges */ chips ? ` ${/* 2. Charakter-Badges */ chips ? `
@ -785,11 +785,11 @@ window.Page_wiki = (() => {
${/* 3. Beschreibung */ rasse.beschreibung ? ` ${/* 3. Beschreibung */ rasse.beschreibung ? `
<div class="wiki-detail-section"> <div class="wiki-detail-section">
<div class="wiki-detail-label">Beschreibung</div> <div class="wiki-detail-label">Beschreibung</div>
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.beschreibung)}</p> <p style="line-height:1.6;color:var(--c-text)">${UI.escape(rasse.beschreibung)}</p>
</div>` : (rasse.bred_for ? ` </div>` : (rasse.bred_for ? `
<div class="wiki-detail-section"> <div class="wiki-detail-section">
<div class="wiki-detail-label">Ursprüngliche Aufgabe</div> <div class="wiki-detail-label">Ursprüngliche Aufgabe</div>
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.bred_for)}</p> <p style="line-height:1.6;color:var(--c-text)">${UI.escape(rasse.bred_for)}</p>
</div>` : '')} </div>` : '')}
${/* 4. Steckbrief */ _renderSteckbriefGrid(rasse)} ${/* 4. Steckbrief */ _renderSteckbriefGrid(rasse)}
@ -797,7 +797,7 @@ window.Page_wiki = (() => {
${/* 5. Vorkommen */ rasse.vorkommen_de ? ` ${/* 5. Vorkommen */ rasse.vorkommen_de ? `
<div class="wiki-detail-section"> <div class="wiki-detail-section">
<div class="wiki-detail-label">Vorkommen in Deutschland</div> <div class="wiki-detail-label">Vorkommen in Deutschland</div>
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.vorkommen_de)}</p> <p style="line-height:1.6;color:var(--c-text)">${UI.escape(rasse.vorkommen_de)}</p>
</div>` : ''} </div>` : ''}
${/* 6. Interesse — wird async befüllt */ ` ${/* 6. Interesse — wird async befüllt */ `
@ -835,7 +835,7 @@ window.Page_wiki = (() => {
</div>` : ''}`} </div>` : ''}`}
`; `;
UI.modal.open({ title: _esc(rasse.name), body }); UI.modal.open({ title: UI.escape(rasse.name), body });
// Async: load stats + züchter in parallel // Async: load stats + züchter in parallel
Promise.all([_fetchStats(slug), _fetchZuchter(slug)]).then(([stats, zuchter]) => { Promise.all([_fetchStats(slug), _fetchZuchter(slug)]).then(([stats, zuchter]) => {
@ -936,7 +936,7 @@ window.Page_wiki = (() => {
</p> </p>
<form id="wiki-foto-form" autocomplete="off"> <form id="wiki-foto-form" autocomplete="off">
<div class="form-group"> <div class="form-group">
<label class="form-label">Foto von <strong>${_esc(rasseName)}</strong></label> <label class="form-label">Foto von <strong>${UI.escape(rasseName)}</strong></label>
<input class="form-control" type="file" id="wiki-foto-input" <input class="form-control" type="file" id="wiki-foto-input"
accept="image/jpeg,image/png,image/webp" required> accept="image/jpeg,image/png,image/webp" required>
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:4px"> <div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:4px">
@ -1030,14 +1030,14 @@ window.Page_wiki = (() => {
return berichte.map(b => ` return berichte.map(b => `
<div class="wiki-bericht-item" data-id="${b.id}"> <div class="wiki-bericht-item" data-id="${b.id}">
<div class="wiki-bericht-header"> <div class="wiki-bericht-header">
<span class="wiki-bericht-autor">${_esc(b.autor)}</span> <span class="wiki-bericht-autor">${UI.escape(b.autor)}</span>
<span class="wiki-bericht-date">${_formatDate(b.created_at)}</span> <span class="wiki-bericht-date">${_formatDate(b.created_at)}</span>
${_appState.user && _appState.user.name === b.autor ${_appState.user && _appState.user.name === b.autor
? `<button class="btn btn-danger btn-xs wiki-bericht-del" data-id="${b.id}" data-slug="${_esc(slug)}" style="margin-left:auto;padding:2px 8px;font-size:0.7rem">Löschen</button>` ? `<button class="btn btn-danger btn-xs wiki-bericht-del" data-id="${b.id}" data-slug="${UI.escape(slug)}" style="margin-left:auto;padding:2px 8px;font-size:0.7rem">Löschen</button>`
: ''} : ''}
</div> </div>
<div class="wiki-bericht-titel">${_esc(b.titel)}</div> <div class="wiki-bericht-titel">${UI.escape(b.titel)}</div>
<p class="wiki-bericht-text">${_esc(b.text)}</p> <p class="wiki-bericht-text">${UI.escape(b.text)}</p>
</div> </div>
`).join(''); `).join('');
} }
@ -1047,7 +1047,7 @@ window.Page_wiki = (() => {
<form id="wiki-bericht-form" autocomplete="off"> <form id="wiki-bericht-form" autocomplete="off">
<div class="form-group"> <div class="form-group">
<label class="form-label">Rasse</label> <label class="form-label">Rasse</label>
<input class="form-control" type="text" value="${_esc(rasseName)}" disabled> <input class="form-control" type="text" value="${UI.escape(rasseName)}" disabled>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Titel</label> <label class="form-label">Titel</label>
@ -1095,11 +1095,11 @@ window.Page_wiki = (() => {
<div class="wiki-section" data-idx="${i}"> <div class="wiki-section" data-idx="${i}">
<div class="wiki-section-header"> <div class="wiki-section-header">
<span class="wiki-section-icon">${UI.icon(s.icon)}</span> <span class="wiki-section-icon">${UI.icon(s.icon)}</span>
<span class="wiki-section-titel">${_esc(s.titel)}</span> <span class="wiki-section-titel">${UI.escape(s.titel)}</span>
<span class="wiki-section-arrow">${UI.icon('caret-down')}</span> <span class="wiki-section-arrow">${UI.icon('caret-down')}</span>
</div> </div>
<div class="wiki-section-body hidden"> <div class="wiki-section-body hidden">
<p style="white-space:pre-wrap;line-height:1.6;color:var(--c-text)">${_esc(s.text)}</p> <p style="white-space:pre-wrap;line-height:1.6;color:var(--c-text)">${UI.escape(s.text)}</p>
</div> </div>
</div> </div>
`).join(''); `).join('');
@ -1126,13 +1126,13 @@ window.Page_wiki = (() => {
<div class="wiki-section" data-idx="${i}"> <div class="wiki-section" data-idx="${i}">
<div class="wiki-section-header"> <div class="wiki-section-header">
<span class="wiki-section-icon">${UI.icon('map-pin')}</span> <span class="wiki-section-icon">${UI.icon('map-pin')}</span>
<span class="wiki-section-titel">${_esc(r.land)}</span> <span class="wiki-section-titel">${UI.escape(r.land)}</span>
<span class="wiki-section-arrow">${UI.icon('caret-down')}</span> <span class="wiki-section-arrow">${UI.icon('caret-down')}</span>
</div> </div>
<div class="wiki-section-body hidden"> <div class="wiki-section-body hidden">
<div class="wiki-recht-row"><span class="wiki-recht-label">Leinenpflicht</span><span>${_esc(r.leine)}</span></div> <div class="wiki-recht-row"><span class="wiki-recht-label">Leinenpflicht</span><span>${UI.escape(r.leine)}</span></div>
<div class="wiki-recht-row"><span class="wiki-recht-label">Rasseliste</span><span>${_esc(r.rasse)}</span></div> <div class="wiki-recht-row"><span class="wiki-recht-label">Rasseliste</span><span>${UI.escape(r.rasse)}</span></div>
<div class="wiki-recht-row"><span class="wiki-recht-label">Hundesteuer</span><span>${_esc(r.steuer)}</span></div> <div class="wiki-recht-row"><span class="wiki-recht-label">Hundesteuer</span><span>${UI.escape(r.steuer)}</span></div>
</div> </div>
</div> </div>
`).join(''); `).join('');
@ -1174,8 +1174,8 @@ window.Page_wiki = (() => {
const optionsHtml = frage.optionen.map(o => ` const optionsHtml = frage.optionen.map(o => `
<button class="wiki-quiz-option${_quizAnswers[frage.key] === o.val ? ' selected' : ''}" <button class="wiki-quiz-option${_quizAnswers[frage.key] === o.val ? ' selected' : ''}"
data-key="${_esc(frage.key)}" data-val="${_esc(o.val)}"> data-key="${UI.escape(frage.key)}" data-val="${UI.escape(o.val)}">
${_esc(o.label)} ${UI.escape(o.label)}
</button> </button>
`).join(''); `).join('');
@ -1185,7 +1185,7 @@ window.Page_wiki = (() => {
<div class="wiki-quiz-progress" style="width:${progress}%"></div> <div class="wiki-quiz-progress" style="width:${progress}%"></div>
</div> </div>
<p class="wiki-quiz-step-info">Frage ${_quizStep + 1} von ${QUIZ_FRAGEN.length}</p> <p class="wiki-quiz-step-info">Frage ${_quizStep + 1} von ${QUIZ_FRAGEN.length}</p>
<p class="wiki-quiz-frage">${_esc(frage.frage)}</p> <p class="wiki-quiz-frage">${UI.escape(frage.frage)}</p>
<div class="wiki-quiz-options">${optionsHtml}</div> <div class="wiki-quiz-options">${optionsHtml}</div>
<div class="wiki-quiz-nav"> <div class="wiki-quiz-nav">
${_quizStep > 0 ${_quizStep > 0
@ -1225,24 +1225,24 @@ window.Page_wiki = (() => {
const cardsHtml = data.results.map(r => { const cardsHtml = data.results.map(r => {
const photoHtml = r.foto_url const photoHtml = r.foto_url
? `<img class="wiki-quiz-result-photo" src="${_esc(r.foto_url)}" loading="lazy" alt="${_esc(r.name)}" onerror="this.style.display='none'">` ? `<img class="wiki-quiz-result-photo" src="${UI.escape(r.foto_url)}" loading="lazy" alt="${UI.escape(r.name)}" onerror="this.style.display='none'">`
: `<div class="wiki-quiz-result-photo-fallback">${UI.icon('dog')}</div>`; : `<div class="wiki-quiz-result-photo-fallback">${UI.icon('dog')}</div>`;
return ` return `
<div class="wiki-quiz-result-card"> <div class="wiki-quiz-result-card">
<div class="wiki-quiz-result-photo-wrap">${photoHtml}</div> <div class="wiki-quiz-result-photo-wrap">${photoHtml}</div>
<div class="wiki-quiz-result-card-body"> <div class="wiki-quiz-result-card-body">
<div class="wiki-quiz-result-name">${_esc(r.name)}</div> <div class="wiki-quiz-result-name">${UI.escape(r.name)}</div>
<div class="wiki-quiz-result-gruppe">${_esc(r.gruppe || '')}</div> <div class="wiki-quiz-result-gruppe">${UI.escape(r.gruppe || '')}</div>
<div class="wiki-breed-badges" style="margin:var(--space-2) 0"> <div class="wiki-breed-badges" style="margin:var(--space-2) 0">
<span class="wiki-badge-groesse wiki-badge-groesse--${_esc(r.groesse)}">${_groesseLabel(r.groesse)}</span> <span class="wiki-badge-groesse wiki-badge-groesse--${UI.escape(r.groesse)}">${_groesseLabel(r.groesse)}</span>
<span class="wiki-badge-aktivitaet wiki-badge-aktivitaet--${_esc(r.aktivitaet)}">${_aktivLabel(r.aktivitaet)}</span> <span class="wiki-badge-aktivitaet wiki-badge-aktivitaet--${UI.escape(r.aktivitaet)}">${_aktivLabel(r.aktivitaet)}</span>
</div> </div>
${r.temperament ? `<p class="wiki-quiz-result-char">${_esc(r.temperament.split(',').slice(0,4).join(', '))}</p>` : ''} ${r.temperament ? `<p class="wiki-quiz-result-char">${UI.escape(r.temperament.split(',').slice(0,4).join(', '))}</p>` : ''}
<div class="wiki-fit-row" style="font-size:var(--text-xs);margin-top:var(--space-1)"> <div class="wiki-fit-row" style="font-size:var(--text-xs);margin-top:var(--space-1)">
<span>${UI.icon('house-line')} ${r.wohnung_geeignet ? 'Wohnung' : 'Haus'}</span> <span>${UI.icon('house-line')} ${r.wohnung_geeignet ? 'Wohnung' : 'Haus'}</span>
<span>${UI.icon('users')} ${r.kinder_geeignet ? 'Kinderfreundlich' : 'Erfahrung nötig'}</span> <span>${UI.icon('users')} ${r.kinder_geeignet ? 'Kinderfreundlich' : 'Erfahrung nötig'}</span>
</div> </div>
<button class="btn btn-secondary btn-sm wiki-quiz-mehr" data-slug="${_esc(r.slug)}" class="mt-2">Mehr erfahren</button> <button class="btn btn-secondary btn-sm wiki-quiz-mehr" data-slug="${UI.escape(r.slug)}" class="mt-2">Mehr erfahren</button>
</div> </div>
</div> </div>
`; `;
@ -1334,13 +1334,6 @@ window.Page_wiki = (() => {
return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch { return iso; } } catch { return iso; }
} }
function _esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ---------------------------------------------------------- // ----------------------------------------------------------
// RASSEN-ERKENNUNG PER KI (Wiki-Tab) // RASSEN-ERKENNUNG PER KI (Wiki-Tab)
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -1405,7 +1398,7 @@ window.Page_wiki = (() => {
Auf diesem Foto konnte kein Hund erkannt werden.<br> Auf diesem Foto konnte kein Hund erkannt werden.<br>
Bitte lade ein deutlicheres Foto hoch. Bitte lade ein deutlicheres Foto hoch.
</p> </p>
${data.hinweis ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-3)">${_esc(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" onclick="UI.modal.close()">Schließen</button>`,
}); });
@ -1418,18 +1411,18 @@ window.Page_wiki = (() => {
return ` return `
<div class="rasse-result-card${isTop ? ' rasse-result-card--top' : ''}"> <div class="rasse-result-card${isTop ? ' rasse-result-card--top' : ''}">
<div style="display:flex;align-items:center;justify-content:space-between"> <div style="display:flex;align-items:center;justify-content:space-between">
<div class="rasse-result-name">${isTop ? '🐕 ' : ''}${_esc(r.name)}</div> <div class="rasse-result-name">${isTop ? '🐕 ' : ''}${UI.escape(r.name)}</div>
<span class="rasse-result-pct${isTop ? '' : ' rasse-result-pct--dim'}">${r.sicherheit}%</span> <span class="rasse-result-pct${isTop ? '' : ' rasse-result-pct--dim'}">${r.sicherheit}%</span>
</div> </div>
<div class="rasse-result-bar-wrap"> <div class="rasse-result-bar-wrap">
<div class="rasse-result-bar${isTop ? '' : ' rasse-result-bar--dim'}" <div class="rasse-result-bar${isTop ? '' : ' rasse-result-bar--dim'}"
style="width:${r.sicherheit}%"></div> style="width:${r.sicherheit}%"></div>
</div> </div>
${r.beschreibung ? `<div class="rasse-result-desc">${_esc(r.beschreibung)}</div>` : ''} ${r.beschreibung ? `<div class="rasse-result-desc">${UI.escape(r.beschreibung)}</div>` : ''}
${r.wiki_slug ? ` ${r.wiki_slug ? `
<div class="mt-3"> <div class="mt-3">
<button class="btn btn-${isTop ? 'primary' : 'secondary'} btn-sm w-full" <button class="btn btn-${isTop ? 'primary' : 'secondary'} btn-sm w-full"
data-action="wiki" data-slug="${_esc(r.wiki_slug)}"> data-action="wiki" data-slug="${UI.escape(r.wiki_slug)}">
Im Wiki nachschlagen Im Wiki nachschlagen
</button> </button>
</div>` : ''} </div>` : ''}
@ -1443,7 +1436,7 @@ window.Page_wiki = (() => {
<div style="padding-bottom:var(--space-2)"> <div style="padding-bottom:var(--space-2)">
${data.hinweis ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md); ${data.hinweis ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md);
padding:var(--space-3);margin-bottom:var(--space-3);font-size:var(--text-sm); padding:var(--space-3);margin-bottom:var(--space-3);font-size:var(--text-sm);
color:var(--c-text-secondary)"> ${_esc(data.hinweis)}</div>` : ''} color:var(--c-text-secondary)"> ${UI.escape(data.hinweis)}</div>` : ''}
${cardsHtml} ${cardsHtml}
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2); <p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2);
text-align:center"> text-align:center">

View file

@ -12,10 +12,6 @@ window.Page_wurfboerse = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// Hilfsfunktionen // Hilfsfunktionen
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(s) {
return UI.escape ? UI.escape(s || '') : (s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function _fmtDate(iso) { function _fmtDate(iso) {
if (!iso) return '—'; if (!iso) return '—';
@ -157,8 +153,8 @@ window.Page_wurfboerse = (() => {
el.innerHTML = ` el.innerHTML = `
<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('dog')}</div> <div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('dog')}</div>
<h3 style="margin:0 0 var(--space-2)">${_esc(title)}</h3> <h3 style="margin:0 0 var(--space-2)">${UI.escape(title)}</h3>
<p style="color:var(--c-text-secondary);margin:0">${_esc(text)}</p> <p style="color:var(--c-text-secondary);margin:0">${UI.escape(text)}</p>
</div>`; </div>`;
} }
@ -168,13 +164,13 @@ window.Page_wurfboerse = (() => {
function _cardHTML(b) { function _cardHTML(b) {
// Züchter-Kopfzeile // Züchter-Kopfzeile
const zuechterName = b.zuechter_name || b.zwingername || '—'; const zuechterName = b.zuechter_name || b.zwingername || '—';
const zwingername = b.zwingername ? ` (${_esc(b.zwingername)})` : ''; const zwingername = b.zwingername ? ` (${UI.escape(b.zwingername)})` : '';
const stadtLine = b.stadt ? ` · ${_esc(b.stadt)}` : ''; const stadtLine = b.stadt ? ` · ${UI.escape(b.stadt)}` : '';
// Elterntiere // Elterntiere
const elternParts = []; const elternParts = [];
if (b.vater_name) elternParts.push(_esc(b.vater_name)); if (b.vater_name) elternParts.push(UI.escape(b.vater_name));
if (b.mutter_name) elternParts.push(_esc(b.mutter_name)); if (b.mutter_name) elternParts.push(UI.escape(b.mutter_name));
const elternLine = elternParts.length === 2 const elternLine = elternParts.length === 2
? `<div class="wb-card-eltern">${UI.icon('gender-male')} ${elternParts[0]} × ${UI.icon('gender-female')} ${elternParts[1]}</div>` ? `<div class="wb-card-eltern">${UI.icon('gender-male')} ${elternParts[0]} × ${UI.icon('gender-female')} ${elternParts[1]}</div>`
: elternParts.length === 1 : elternParts.length === 1
@ -194,34 +190,34 @@ window.Page_wurfboerse = (() => {
if (b.welpen_gesamt != null || b.welpen_verfuegbar != null) { if (b.welpen_gesamt != null || b.welpen_verfuegbar != null) {
const gesamt = b.welpen_gesamt != null ? b.welpen_gesamt : '?'; const gesamt = b.welpen_gesamt != null ? b.welpen_gesamt : '?';
const verfuegb = b.welpen_verfuegbar != null ? b.welpen_verfuegbar : '?'; const verfuegb = b.welpen_verfuegbar != null ? b.welpen_verfuegbar : '?';
welpenLine = `<div class="wb-card-welpen">${UI.icon('paw-print')} Welpen verfügbar: ${_esc(String(verfuegb))} von ${_esc(String(gesamt))}</div>`; welpenLine = `<div class="wb-card-welpen">${UI.icon('paw-print')} Welpen verfügbar: ${UI.escape(String(verfuegb))} von ${UI.escape(String(gesamt))}</div>`;
} }
// Preis // Preis
const preisLine = b.preis_spanne const preisLine = b.preis_spanne
? `<div class="wb-card-preis">${UI.icon('currency-eur')} Preis: ${_esc(b.preis_spanne)} €</div>` ? `<div class="wb-card-preis">${UI.icon('currency-eur')} Preis: ${UI.escape(b.preis_spanne)} €</div>`
: ''; : '';
// Gesundheitstests // Gesundheitstests
const gesundheitLine = b.gesundheitstests const gesundheitLine = b.gesundheitstests
? `<div class="wb-card-gesundheit">${UI.icon('heart')} ${_esc(b.gesundheitstests)}</div>` ? `<div class="wb-card-gesundheit">${UI.icon('heart')} ${UI.escape(b.gesundheitstests)}</div>`
: ''; : '';
// Beschreibung (max. 150 Zeichen) // Beschreibung (max. 150 Zeichen)
const beschreibungLine = b.beschreibung const beschreibungLine = b.beschreibung
? `<div class="wb-card-beschreibung">${_esc(_truncate(b.beschreibung, 150))}</div>` ? `<div class="wb-card-beschreibung">${UI.escape(_truncate(b.beschreibung, 150))}</div>`
: ''; : '';
return ` return `
<div class="wb-card"> <div class="wb-card">
<div class="wb-card-header"> <div class="wb-card-header">
<div class="wb-card-zuechter"> <div class="wb-card-zuechter">
${_esc(zuechterName)}${zwingername}${stadtLine} ${UI.escape(zuechterName)}${zwingername}${stadtLine}
</div> </div>
${_statusBadge(b.status)} ${_statusBadge(b.status)}
</div> </div>
${b.rasse_text ? `<div class="wb-card-rasse">${UI.icon('dog')} ${_esc(b.rasse_text)}</div>` : ''} ${b.rasse_text ? `<div class="wb-card-rasse">${UI.icon('dog')} ${UI.escape(b.rasse_text)}</div>` : ''}
<div class="wb-card-details"> <div class="wb-card-details">
${elternLine} ${elternLine}
@ -235,7 +231,7 @@ window.Page_wurfboerse = (() => {
<div class="wb-card-footer"> <div class="wb-card-footer">
<button <button
class="btn btn-secondary btn-sm wb-profile-btn" class="btn btn-secondary btn-sm wb-profile-btn"
data-zwingername="${_esc(b.zwingername || '')}" data-zwingername="${UI.escape(b.zwingername || '')}"
> >
${UI.icon('user')} Profil ansehen ${UI.icon('user')} Profil ansehen
</button> </button>

View file

@ -15,15 +15,6 @@ window.Page_zucht_profil = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// Hilfsfunktionen // Hilfsfunktionen
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(s) {
if (!s) return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function _fmtDate(iso) { function _fmtDate(iso) {
if (!iso) return '—'; if (!iso) return '—';
@ -56,7 +47,7 @@ window.Page_zucht_profil = (() => {
if (el === 'affected') color = '#EF4444'; if (el === 'affected') color = '#EF4444';
} }
return `<span class="zp-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`; return `<span class="zp-badge" style="background:${color}">${UI.escape(ergebnis || '—')}</span>`;
} }
function _geneticBadge(ergebnis) { function _geneticBadge(ergebnis) {
@ -65,7 +56,7 @@ window.Page_zucht_profil = (() => {
if (e === 'clear') color = '#22C55E'; if (e === 'clear') color = '#22C55E';
if (e === 'carrier') color = '#F59E0B'; if (e === 'carrier') color = '#F59E0B';
if (e === 'affected') color = '#EF4444'; if (e === 'affected') color = '#EF4444';
return `<span class="zp-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`; return `<span class="zp-badge" style="background:${color}">${UI.escape(ergebnis || '—')}</span>`;
} }
function _titleTypBadge(typ) { function _titleTypBadge(typ) {
@ -78,7 +69,7 @@ window.Page_zucht_profil = (() => {
zucht: '#10B981', zucht: '#10B981',
}; };
const color = colors[t] || '#6B7280'; const color = colors[t] || '#6B7280';
return `<span class="zp-badge" style="background:${color}">${_esc(typ || '—')}</span>`; return `<span class="zp-badge" style="background:${color}">${UI.escape(typ || '—')}</span>`;
} }
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -126,7 +117,7 @@ window.Page_zucht_profil = (() => {
_container.innerHTML = ` _container.innerHTML = `
<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">${_esc(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" onclick="history.back()">Zurück</button>
</div>`; </div>`;
} }
@ -240,21 +231,21 @@ window.Page_zucht_profil = (() => {
h.geschlecht === 'weiblich' ? 'Hündin' : null; h.geschlecht === 'weiblich' ? 'Hündin' : null;
const metaItems = [ const metaItems = [
h.rasse ? `${UI.icon('paw-print')} ${_esc(h.rasse)}` : null, h.rasse ? `${UI.icon('paw-print')} ${UI.escape(h.rasse)}` : null,
geschlechtLabel ? `${gIcon} ${geschlechtLabel}` : null, geschlechtLabel ? `${gIcon} ${geschlechtLabel}` : null,
geburtsjahrLabel ? `${UI.icon('calendar-dots')} ${geburtsjahrLabel}` : null, geburtsjahrLabel ? `${UI.icon('calendar-dots')} ${geburtsjahrLabel}` : null,
].filter(Boolean); ].filter(Boolean);
const identItems = [ const identItems = [
h.chip_nr ? `${UI.icon('barcode')} Chip: ${_esc(h.chip_nr)}` : null, h.chip_nr ? `${UI.icon('barcode')} Chip: ${UI.escape(h.chip_nr)}` : null,
h.zuchtbuchnummer ? `${UI.icon('book-open')} ZB-Nr.: ${_esc(h.zuchtbuchnummer)}` : null, h.zuchtbuchnummer ? `${UI.icon('book-open')} ZB-Nr.: ${UI.escape(h.zuchtbuchnummer)}` : null,
h.taetowier_nr ? `${UI.icon('pencil-simple')} Tätowierung: ${_esc(h.taetowier_nr)}` : null, h.taetowier_nr ? `${UI.icon('pencil-simple')} Tätowierung: ${UI.escape(h.taetowier_nr)}` : null,
h.farbe ? `${UI.icon('palette')} ${_esc(h.farbe)}` : null, h.farbe ? `${UI.icon('palette')} ${UI.escape(h.farbe)}` : null,
].filter(Boolean); ].filter(Boolean);
const elternItems = [ const elternItems = [
h.vater_name ? `Vater: ${_esc(h.vater_name)}` : null, h.vater_name ? `Vater: ${UI.escape(h.vater_name)}` : null,
h.mutter_name ? `Mutter: ${_esc(h.mutter_name)}` : null, h.mutter_name ? `Mutter: ${UI.escape(h.mutter_name)}` : null,
].filter(Boolean); ].filter(Boolean);
return ` return `
@ -262,8 +253,8 @@ window.Page_zucht_profil = (() => {
<div class="zp-header-icon">${gIcon}</div> <div class="zp-header-icon">${gIcon}</div>
<div class="zp-header-body"> <div class="zp-header-body">
<h2 class="zp-header-name"> <h2 class="zp-header-name">
${_esc(h.name)} ${UI.escape(h.name)}
${h.rufname ? `<span class="zp-header-rufname">(${_esc(h.rufname)})</span>` : ''} ${h.rufname ? `<span class="zp-header-rufname">(${UI.escape(h.rufname)})</span>` : ''}
</h2> </h2>
${metaItems.length ? ` ${metaItems.length ? `
<div class="zp-header-meta"> <div class="zp-header-meta">
@ -277,7 +268,7 @@ window.Page_zucht_profil = (() => {
<div class="zp-header-meta text-xs-secondary"> <div class="zp-header-meta text-xs-secondary">
${elternItems.join(' &nbsp;·&nbsp; ')} ${elternItems.join(' &nbsp;·&nbsp; ')}
</div>` : ''} </div>` : ''}
${h.notiz ? `<div class="zp-header-notiz">${_esc(h.notiz)}</div>` : ''} ${h.notiz ? `<div class="zp-header-notiz">${UI.escape(h.notiz)}</div>` : ''}
</div> </div>
</div>`; </div>`;
} }
@ -350,18 +341,18 @@ window.Page_zucht_profil = (() => {
box-sizing:border-box;"> box-sizing:border-box;">
<div style="font-weight:600;font-size:var(--text-sm); <div style="font-weight:600;font-size:var(--text-sm);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"> white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
${gIcon} ${_esc(node.name)} ${gIcon} ${UI.escape(node.name)}
</div> </div>
${node.rufname ${node.rufname
? `<div style="font-size:var(--text-xs);opacity:.75;white-space:nowrap; ? `<div style="font-size:var(--text-xs);opacity:.75;white-space:nowrap;
overflow:hidden;text-overflow:ellipsis;">${_esc(node.rufname)}</div>` overflow:hidden;text-overflow:ellipsis;">${UI.escape(node.rufname)}</div>`
: ''} : ''}
${dob ${dob
? `<div style="font-size:var(--text-xs);opacity:.65;">${dob}</div>` ? `<div style="font-size:var(--text-xs);opacity:.65;">${dob}</div>`
: ''} : ''}
${node.zuchtbuchnummer ${node.zuchtbuchnummer
? `<div style="font-size:var(--text-xs);opacity:.55;white-space:nowrap; ? `<div style="font-size:var(--text-xs);opacity:.55;white-space:nowrap;
overflow:hidden;text-overflow:ellipsis;">${_esc(node.zuchtbuchnummer)}</div>` overflow:hidden;text-overflow:ellipsis;">${UI.escape(node.zuchtbuchnummer)}</div>`
: ''} : ''}
</div>`; </div>`;
} }
@ -377,12 +368,12 @@ window.Page_zucht_profil = (() => {
const rows = tests.map(t => ` const rows = tests.map(t => `
<tr> <tr>
<td class="zp-td"> <td class="zp-td">
<span style="font-weight:var(--weight-medium)">${_esc(t.test_typ || 'Sonstiges')}</span> <span style="font-weight:var(--weight-medium)">${UI.escape(t.test_typ || 'Sonstiges')}</span>
${t.test_name ? `<br><span class="text-xs-secondary">${_esc(t.test_name)}</span>` : ''} ${t.test_name ? `<br><span class="text-xs-secondary">${UI.escape(t.test_name)}</span>` : ''}
</td> </td>
<td class="zp-td">${_healthBadge(t.test_typ || '', t.ergebnis)}</td> <td class="zp-td">${_healthBadge(t.test_typ || '', t.ergebnis)}</td>
<td class="zp-td zp-td-muted">${t.untersuch_am ? _fmtDate(t.untersuch_am) : '—'}</td> <td class="zp-td zp-td-muted">${t.untersuch_am ? _fmtDate(t.untersuch_am) : '—'}</td>
<td class="zp-td zp-td-muted">${t.labor ? _esc(t.labor) : '—'}</td> <td class="zp-td zp-td-muted">${t.labor ? UI.escape(t.labor) : '—'}</td>
</tr>`).join(''); </tr>`).join('');
return ` return `
@ -412,11 +403,11 @@ window.Page_zucht_profil = (() => {
const rows = tests.map(t => ` const rows = tests.map(t => `
<tr> <tr>
<td class="zp-td"> <td class="zp-td">
<span style="font-weight:var(--weight-medium)">${_esc(t.marker_name || '—')}</span> <span style="font-weight:var(--weight-medium)">${UI.escape(t.marker_name || '—')}</span>
</td> </td>
<td class="zp-td">${_geneticBadge(t.ergebnis_klasse)}</td> <td class="zp-td">${_geneticBadge(t.ergebnis_klasse)}</td>
<td class="zp-td zp-td-muted">${t.getestet_am ? _fmtDate(t.getestet_am) : '—'}</td> <td class="zp-td zp-td-muted">${t.getestet_am ? _fmtDate(t.getestet_am) : '—'}</td>
<td class="zp-td zp-td-muted">${t.labor ? _esc(t.labor) : '—'}</td> <td class="zp-td zp-td-muted">${t.labor ? UI.escape(t.labor) : '—'}</td>
</tr>`).join(''); </tr>`).join('');
return ` return `
@ -455,15 +446,15 @@ window.Page_zucht_profil = (() => {
<div class="zp-title-badges"> <div class="zp-title-badges">
${_titleTypBadge(t.titel_typ)} ${_titleTypBadge(t.titel_typ)}
${t.formwert ${t.formwert
? `<span class="zp-badge" style="background:#3B82F6">${_esc(t.formwert)}</span>` ? `<span class="zp-badge" style="background:#3B82F6">${UI.escape(t.formwert)}</span>`
: ''} : ''}
</div> </div>
<div class="zp-title-name">${_esc(t.titel_name || '—')}</div> <div class="zp-title-name">${UI.escape(t.titel_name || '—')}</div>
<div class="zp-title-meta"> <div class="zp-title-meta">
${t.verliehen_am ? `${UI.icon('calendar-dots')} ${_fmtDate(t.verliehen_am)}` : ''} ${t.verliehen_am ? `${UI.icon('calendar-dots')} ${_fmtDate(t.verliehen_am)}` : ''}
${t.ort ? `&nbsp;·&nbsp; ${UI.icon('map-pin')} ${_esc(t.ort)}` : ''} ${t.ort ? `&nbsp;·&nbsp; ${UI.icon('map-pin')} ${UI.escape(t.ort)}` : ''}
${t.richter ? `&nbsp;·&nbsp; ${UI.icon('user')} ${_esc(t.richter)}` : ''} ${t.richter ? `&nbsp;·&nbsp; ${UI.icon('user')} ${UI.escape(t.richter)}` : ''}
${t.ausstellung ? `<br><span class="text-xs">${UI.icon('ticket')} ${_esc(t.ausstellung)}</span>` : ''} ${t.ausstellung ? `<br><span class="text-xs">${UI.icon('ticket')} ${UI.escape(t.ausstellung)}</span>` : ''}
</div> </div>
</div>`).join(''); </div>`).join('');

View file

@ -17,10 +17,6 @@ window.Page_zuchthunde = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// Hilfsfunktionen // Hilfsfunktionen
// ---------------------------------------------------------- // ----------------------------------------------------------
function _esc(s) {
return UI.escape ? UI.escape(s || '') : (s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function _fmtDate(iso) { function _fmtDate(iso) {
if (!iso) return '—'; if (!iso) return '—';
@ -54,7 +50,7 @@ window.Page_zuchthunde = (() => {
else if (e === '3' || e === 'ED 3') color = '#EF4444'; else if (e === '3' || e === 'ED 3') color = '#EF4444';
} }
return `<span class="zh-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`; return `<span class="zh-badge" style="background:${color}">${UI.escape(ergebnis || '—')}</span>`;
} }
function _geneticBadge(ergebnis) { function _geneticBadge(ergebnis) {
@ -63,7 +59,7 @@ window.Page_zuchthunde = (() => {
if (e === 'clear') color = '#22C55E'; if (e === 'clear') color = '#22C55E';
if (e === 'carrier') color = '#EAB308'; if (e === 'carrier') color = '#EAB308';
if (e === 'affected') color = '#EF4444'; if (e === 'affected') color = '#EF4444';
return `<span class="zh-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`; return `<span class="zh-badge" style="background:${color}">${UI.escape(ergebnis || '—')}</span>`;
} }
// ---------------------------------------------------------- // ----------------------------------------------------------
@ -103,7 +99,7 @@ window.Page_zuchthunde = (() => {
const zwinger = _breederInfo?.zwingername || 'Mein Zwinger'; const zwinger = _breederInfo?.zwingername || 'Mein Zwinger';
const logoUrl = _breederInfo?.logo_url || null; const logoUrl = _breederInfo?.logo_url || null;
const logoHtml = logoUrl const logoHtml = logoUrl
? `<img src="${_esc(logoUrl)}" alt="Logo" ? `<img src="${UI.escape(logoUrl)}" alt="Logo"
style="width:48px;height:48px;border-radius:50%;object-fit:cover; style="width:48px;height:48px;border-radius:50%;object-fit:cover;
border:2px solid rgba(196,132,58,.5);flex-shrink:0" border:2px solid rgba(196,132,58,.5);flex-shrink:0"
onerror="this.style.display='none'">` onerror="this.style.display='none'">`
@ -123,7 +119,7 @@ window.Page_zuchthunde = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<h2 style="margin:0 0 2px;font-size:var(--text-lg);font-weight:700; <h2 style="margin:0 0 2px;font-size:var(--text-lg);font-weight:700;
color:var(--c-text);white-space:nowrap;overflow:hidden; color:var(--c-text);white-space:nowrap;overflow:hidden;
text-overflow:ellipsis;line-height:1.2">${_esc(zwinger)}</h2> text-overflow:ellipsis;line-height:1.2">${UI.escape(zwinger)}</h2>
<div style="display:flex;align-items:center;gap:var(--space-2)"> <div style="display:flex;align-items:center;gap:var(--space-2)">
<svg style="width:11px;height:11px;color:var(--c-primary);flex-shrink:0" viewBox="0 0 256 256"> <svg style="width:11px;height:11px;color:var(--c-primary);flex-shrink:0" viewBox="0 0 256 256">
<use href="/icons/phosphor.svg#lock-key"></use> <use href="/icons/phosphor.svg#lock-key"></use>
@ -208,7 +204,7 @@ window.Page_zuchthunde = (() => {
const el = document.getElementById('zh-list'); const el = document.getElementById('zh-list');
if (el) el.innerHTML = ` if (el) el.innerHTML = `
<p style="color:var(--c-danger);text-align:center;padding:var(--space-8)"> <p style="color:var(--c-danger);text-align:center;padding:var(--space-8)">
${_esc(err.message || 'Fehler beim Laden.')} ${UI.escape(err.message || 'Fehler beim Laden.')}
</p>`; </p>`;
} }
} }
@ -228,7 +224,7 @@ window.Page_zuchthunde = (() => {
if (!filtered.length) { if (!filtered.length) {
el.innerHTML = _query el.innerHTML = _query
? `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">Keine Treffer für „${_esc(_query)}".</p>` ? `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">Keine Treffer für „${UI.escape(_query)}".</p>`
: ` : `
<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('dog')}</div> <div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('dog')}</div>
@ -299,12 +295,12 @@ window.Page_zuchthunde = (() => {
// Hund-Card HTML // Hund-Card HTML
// ---------------------------------------------------------- // ----------------------------------------------------------
function _hundCardHTML(h) { function _hundCardHTML(h) {
const nameLabel = h.name ? _esc(h.name) : '<em class="text-muted">Unbenannt</em>'; const nameLabel = h.name ? UI.escape(h.name) : '<em class="text-muted">Unbenannt</em>';
const rufname = h.rufname ? ` (${_esc(h.rufname)})` : ''; const rufname = h.rufname ? ` (${UI.escape(h.rufname)})` : '';
const geburtstag = h.geburtsdatum ? _fmtDate(h.geburtsdatum) : null; const geburtstag = h.geburtsdatum ? _fmtDate(h.geburtsdatum) : null;
const vaterLabel = h.vater_name ? `Vater: ${_esc(h.vater_name)}` : null; const vaterLabel = h.vater_name ? `Vater: ${UI.escape(h.vater_name)}` : null;
const mutterLabel = h.mutter_name ? `Mutter: ${_esc(h.mutter_name)}` : null; const mutterLabel = h.mutter_name ? `Mutter: ${UI.escape(h.mutter_name)}` : null;
const eltern = [vaterLabel, mutterLabel].filter(Boolean).join(' &nbsp;·&nbsp; '); const eltern = [vaterLabel, mutterLabel].filter(Boolean).join(' &nbsp;·&nbsp; ');
const pubLabel = h.is_public const pubLabel = h.is_public
@ -317,14 +313,14 @@ window.Page_zuchthunde = (() => {
<div class="flex-1-min"> <div class="flex-1-min">
<div class="zh-card-title"> <div class="zh-card-title">
${_genderIcon(h.geschlecht)} ${_genderIcon(h.geschlecht)}
${nameLabel}${_esc(rufname)} ${nameLabel}${UI.escape(rufname)}
${pubLabel} ${pubLabel}
</div> </div>
<div class="zh-card-meta"> <div class="zh-card-meta">
${h.rasse ? `${UI.icon('paw-print')} ${_esc(h.rasse)}&nbsp;&nbsp;` : ''} ${h.rasse ? `${UI.icon('paw-print')} ${UI.escape(h.rasse)}&nbsp;&nbsp;` : ''}
${geburtstag ? `${UI.icon('calendar-dots')} ${geburtstag}&nbsp;&nbsp;` : ''} ${geburtstag ? `${UI.icon('calendar-dots')} ${geburtstag}&nbsp;&nbsp;` : ''}
${h.chip_nr ? `${UI.icon('barcode')} ${_esc(h.chip_nr)}&nbsp;&nbsp;` : ''} ${h.chip_nr ? `${UI.icon('barcode')} ${UI.escape(h.chip_nr)}&nbsp;&nbsp;` : ''}
${h.zuchtbuchnummer ? `${UI.icon('book-open')} ${_esc(h.zuchtbuchnummer)}&nbsp;&nbsp;` : ''} ${h.zuchtbuchnummer ? `${UI.icon('book-open')} ${UI.escape(h.zuchtbuchnummer)}&nbsp;&nbsp;` : ''}
</div> </div>
${eltern ? `<div class="zh-card-meta text-xs-secondary">${eltern}</div>` : ''} ${eltern ? `<div class="zh-card-meta text-xs-secondary">${eltern}</div>` : ''}
</div> </div>
@ -416,7 +412,7 @@ window.Page_zuchthunde = (() => {
const tests = await API.zuchthunde.healthTests(hundId); const tests = await API.zuchthunde.healthTests(hundId);
_renderHealthSection(hundId, wrap, tests); _renderHealthSection(hundId, wrap, tests);
} catch (err) { } catch (err) {
wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${_esc(err.message || 'Fehler.')}</p>`; wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${UI.escape(err.message || 'Fehler.')}</p>`;
} }
} }
@ -425,11 +421,11 @@ window.Page_zuchthunde = (() => {
? tests.map(t => ` ? tests.map(t => `
<div class="zh-detail-row"> <div class="zh-detail-row">
<div class="zh-detail-info"> <div class="zh-detail-info">
<span class="zh-detail-label">${_esc(t.test_typ || 'Sonstiges')}</span> <span class="zh-detail-label">${UI.escape(t.test_typ || 'Sonstiges')}</span>
${t.test_name ? `<span style="color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(t.test_name)}</span>` : ''} ${t.test_name ? `<span style="color:var(--c-text-secondary);font-size:var(--text-xs)">${UI.escape(t.test_name)}</span>` : ''}
${_healthBadge(t.test_typ || '', t.ergebnis)} ${_healthBadge(t.test_typ || '', t.ergebnis)}
${t.untersuch_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.untersuch_am)}</span>` : ''} ${t.untersuch_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.untersuch_am)}</span>` : ''}
${t.labor ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.labor)}</span>` : ''} ${t.labor ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(t.labor)}</span>` : ''}
</div> </div>
<button class="btn btn-ghost btn-xs zh-health-del-btn" data-tid="${t.id}" title="Löschen" <button class="btn btn-ghost btn-xs zh-health-del-btn" data-tid="${t.id}" title="Löschen"
class="text-danger">${UI.icon('trash')}</button> class="text-danger">${UI.icon('trash')}</button>
@ -480,7 +476,7 @@ window.Page_zuchthunde = (() => {
const tests = await API.zuchthunde.geneticTests(hundId); const tests = await API.zuchthunde.geneticTests(hundId);
_renderGeneticSection(hundId, wrap, tests); _renderGeneticSection(hundId, wrap, tests);
} catch (err) { } catch (err) {
wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${_esc(err.message || 'Fehler.')}</p>`; wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${UI.escape(err.message || 'Fehler.')}</p>`;
} }
} }
@ -489,10 +485,10 @@ window.Page_zuchthunde = (() => {
? tests.map(t => ` ? tests.map(t => `
<div class="zh-detail-row"> <div class="zh-detail-row">
<div class="zh-detail-info"> <div class="zh-detail-info">
<span class="zh-detail-label">${_esc(t.marker_name || '—')}</span> <span class="zh-detail-label">${UI.escape(t.marker_name || '—')}</span>
${_geneticBadge(t.ergebnis_klasse)} ${_geneticBadge(t.ergebnis_klasse)}
${t.getestet_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.getestet_am)}</span>` : ''} ${t.getestet_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.getestet_am)}</span>` : ''}
${t.labor ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.labor)}</span>` : ''} ${t.labor ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(t.labor)}</span>` : ''}
</div> </div>
<button class="btn btn-ghost btn-xs zh-genetic-del-btn" data-tid="${t.id}" title="Löschen" <button class="btn btn-ghost btn-xs zh-genetic-del-btn" data-tid="${t.id}" title="Löschen"
class="text-danger">${UI.icon('trash')}</button> class="text-danger">${UI.icon('trash')}</button>
@ -543,7 +539,7 @@ window.Page_zuchthunde = (() => {
const titles = await API.zuchthunde.titles(hundId); const titles = await API.zuchthunde.titles(hundId);
_renderTitlesSection(hundId, wrap, titles); _renderTitlesSection(hundId, wrap, titles);
} catch (err) { } catch (err) {
wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${_esc(err.message || 'Fehler.')}</p>`; wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${UI.escape(err.message || 'Fehler.')}</p>`;
} }
} }
@ -552,12 +548,12 @@ window.Page_zuchthunde = (() => {
? titles.map(t => ` ? titles.map(t => `
<div class="zh-detail-row"> <div class="zh-detail-row">
<div class="zh-detail-info"> <div class="zh-detail-info">
<span class="zh-detail-label">${_esc(t.titel_name || '—')}</span> <span class="zh-detail-label">${UI.escape(t.titel_name || '—')}</span>
${t.titel_typ ? `<span class="zh-badge" style="background:#6B7280">${_esc(t.titel_typ)}</span>` : ''} ${t.titel_typ ? `<span class="zh-badge" style="background:#6B7280">${UI.escape(t.titel_typ)}</span>` : ''}
${t.verliehen_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.verliehen_am)}</span>` : ''} ${t.verliehen_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.verliehen_am)}</span>` : ''}
${t.ort ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.ort)}</span>` : ''} ${t.ort ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(t.ort)}</span>` : ''}
${t.richter ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.richter)}</span>` : ''} ${t.richter ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.escape(t.richter)}</span>` : ''}
${t.formwert ? `<span class="zh-badge" style="background:#3B82F6">${_esc(t.formwert)}</span>` : ''} ${t.formwert ? `<span class="zh-badge" style="background:#3B82F6">${UI.escape(t.formwert)}</span>` : ''}
</div> </div>
<button class="btn btn-ghost btn-xs zh-title-del-btn" data-tid="${t.id}" title="Löschen" <button class="btn btn-ghost btn-xs zh-title-del-btn" data-tid="${t.id}" title="Löschen"
class="text-danger">${UI.icon('trash')}</button> class="text-danger">${UI.icon('trash')}</button>
@ -614,13 +610,13 @@ window.Page_zuchthunde = (() => {
const vaterOptions = [ const vaterOptions = [
`<option value="">— kein Vater —</option>`, `<option value="">— kein Vater —</option>`,
...maennliche.map(h => ...maennliche.map(h =>
`<option value="${h.id}" ${v.vater_id === h.id ? 'selected' : ''}>${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`), `<option value="${h.id}" ${v.vater_id === h.id ? 'selected' : ''}>${UI.escape(h.name)}${h.rufname ? ` (${UI.escape(h.rufname)})` : ''}</option>`),
].join(''); ].join('');
const mutterOptions = [ const mutterOptions = [
`<option value="">— keine Mutter —</option>`, `<option value="">— keine Mutter —</option>`,
...weibliche.map(h => ...weibliche.map(h =>
`<option value="${h.id}" ${v.mutter_id === h.id ? 'selected' : ''}>${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`), `<option value="${h.id}" ${v.mutter_id === h.id ? 'selected' : ''}>${UI.escape(h.name)}${h.rufname ? ` (${UI.escape(h.rufname)})` : ''}</option>`),
].join(''); ].join('');
const body = ` const body = `
@ -630,12 +626,12 @@ window.Page_zuchthunde = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Vollständiger Name <span class="text-danger">*</span></label> <label class="form-label">Vollständiger Name <span class="text-danger">*</span></label>
<input class="form-control" type="text" name="name" required <input class="form-control" type="text" name="name" required
value="${_esc(v.name || '')}" placeholder="z. B. Banyaro's Black Diamond"> value="${UI.escape(v.name || '')}" placeholder="z. B. Banyaro's Black Diamond">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Rufname</label> <label class="form-label">Rufname</label>
<input class="form-control" type="text" name="rufname" <input class="form-control" type="text" name="rufname"
value="${_esc(v.rufname || '')}" placeholder="z. B. Diamond"> value="${UI.escape(v.rufname || '')}" placeholder="z. B. Diamond">
</div> </div>
</div> </div>
@ -651,7 +647,7 @@ window.Page_zuchthunde = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Geburtsdatum</label> <label class="form-label">Geburtsdatum</label>
<input class="form-control" type="date" name="geburtsdatum" <input class="form-control" type="date" name="geburtsdatum"
value="${_esc(v.geburtsdatum || '')}"> value="${UI.escape(v.geburtsdatum || '')}">
</div> </div>
</div> </div>
@ -659,12 +655,12 @@ window.Page_zuchthunde = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Sterbedatum</label> <label class="form-label">Sterbedatum</label>
<input class="form-control" type="date" name="sterbedatum" <input class="form-control" type="date" name="sterbedatum"
value="${_esc(v.sterbedatum || '')}"> value="${UI.escape(v.sterbedatum || '')}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Farbe / Fell</label> <label class="form-label">Farbe / Fell</label>
<input class="form-control" type="text" name="farbe" <input class="form-control" type="text" name="farbe"
value="${_esc(v.farbe || '')}" placeholder="z. B. schwarz-braun"> value="${UI.escape(v.farbe || '')}" placeholder="z. B. schwarz-braun">
</div> </div>
</div> </div>
@ -672,19 +668,19 @@ window.Page_zuchthunde = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Chip-Nr.</label> <label class="form-label">Chip-Nr.</label>
<input class="form-control" type="text" name="chip_nr" <input class="form-control" type="text" name="chip_nr"
value="${_esc(v.chip_nr || '')}" placeholder="15-stellig"> value="${UI.escape(v.chip_nr || '')}" placeholder="15-stellig">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Tätowiernummer</label> <label class="form-label">Tätowiernummer</label>
<input class="form-control" type="text" name="taetowier_nr" <input class="form-control" type="text" name="taetowier_nr"
value="${_esc(v.taetowier_nr || '')}"> value="${UI.escape(v.taetowier_nr || '')}">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Zuchtbuchnummer</label> <label class="form-label">Zuchtbuchnummer</label>
<input class="form-control" type="text" name="zuchtbuchnummer" <input class="form-control" type="text" name="zuchtbuchnummer"
value="${_esc(v.zuchtbuchnummer || '')}" placeholder="z. B. SZ 123456"> value="${UI.escape(v.zuchtbuchnummer || '')}" placeholder="z. B. SZ 123456">
</div> </div>
<div class="grid-2"> <div class="grid-2">
@ -702,19 +698,19 @@ window.Page_zuchthunde = (() => {
<div class="form-group"> <div class="form-group">
<label class="form-label">Züchter-Name</label> <label class="form-label">Züchter-Name</label>
<input class="form-control" type="text" name="zuechter_name" <input class="form-control" type="text" name="zuechter_name"
value="${_esc(v.zuechter_name || '')}" placeholder="Bei Fremdzüchter"> value="${UI.escape(v.zuechter_name || '')}" placeholder="Bei Fremdzüchter">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Eigentümer-Name</label> <label class="form-label">Eigentümer-Name</label>
<input class="form-control" type="text" name="eigentuemer_name" <input class="form-control" type="text" name="eigentuemer_name"
value="${_esc(v.eigentuemer_name || '')}"> value="${UI.escape(v.eigentuemer_name || '')}">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Notiz <span class="text-secondary">(intern)</span></label> <label class="form-label">Notiz <span class="text-secondary">(intern)</span></label>
<textarea class="form-control" name="notiz" rows="2" <textarea class="form-control" name="notiz" rows="2"
placeholder="Interne Anmerkungen…">${_esc(v.notiz || '')}</textarea> placeholder="Interne Anmerkungen…">${UI.escape(v.notiz || '')}</textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -1121,14 +1117,14 @@ window.Page_zuchthunde = (() => {
`<option value="">-- Aus eigenen Hunden --</option>`, `<option value="">-- Aus eigenen Hunden --</option>`,
..._hunde ..._hunde
.filter(h => h.geschlecht !== 'weiblich') .filter(h => h.geschlecht !== 'weiblich')
.map(h => `<option value="${h.id}">${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`), .map(h => `<option value="${h.id}">${UI.escape(h.name)}${h.rufname ? ` (${UI.escape(h.rufname)})` : ''}</option>`),
].join(''); ].join('');
const muetterOptions = [ const muetterOptions = [
`<option value="">-- Aus eigenen Hunden --</option>`, `<option value="">-- Aus eigenen Hunden --</option>`,
..._hunde ..._hunde
.filter(h => h.geschlecht !== 'maennlich') .filter(h => h.geschlecht !== 'maennlich')
.map(h => `<option value="${h.id}">${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`), .map(h => `<option value="${h.id}">${UI.escape(h.name)}${h.rufname ? ` (${UI.escape(h.rufname)})` : ''}</option>`),
].join(''); ].join('');
const body = ` const body = `
@ -1206,7 +1202,7 @@ window.Page_zuchthunde = (() => {
if (v.gen_vater != null) genInfo.push(`Gen. ${v.gen_vater} Vater`); if (v.gen_vater != null) genInfo.push(`Gen. ${v.gen_vater} Vater`);
if (v.gen_mutter != null) genInfo.push(`Gen. ${v.gen_mutter} Mutter`); if (v.gen_mutter != null) genInfo.push(`Gen. ${v.gen_mutter} Mutter`);
const genStr = genInfo.length ? ` <span style="color:var(--c-text-secondary);font-size:var(--text-xs)">(${genInfo.join(' / ')})</span>` : ''; const genStr = genInfo.length ? ` <span style="color:var(--c-text-secondary);font-size:var(--text-xs)">(${genInfo.join(' / ')})</span>` : '';
return `<li style="padding:var(--space-1) 0">${_esc(v.name || '—')}${genStr}</li>`; return `<li style="padding:var(--space-1) 0">${UI.escape(v.name || '—')}${genStr}</li>`;
}).join('') }).join('')
: `<li class="text-muted">Keine gemeinsamen Vorfahren gefunden.</li>`; : `<li class="text-muted">Keine gemeinsamen Vorfahren gefunden.</li>`;
@ -1220,13 +1216,13 @@ window.Page_zuchthunde = (() => {
const wIssueHTML = (welfare.issues || []).map(i => ` const wIssueHTML = (welfare.issues || []).map(i => `
<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid rgba(0,0,0,.06)"> <div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid rgba(0,0,0,.06)">
<span style="color:${wColor};flex-shrink:0">${UI.icon('warning')}</span> <span style="color:${wColor};flex-shrink:0">${UI.icon('warning')}</span>
<span class="text-sm">${_esc(i.text)}</span> <span class="text-sm">${UI.escape(i.text)}</span>
</div>`).join(''); </div>`).join('');
const wOkHTML = (welfare.ok_points || []).map(p => ` const wOkHTML = (welfare.ok_points || []).map(p => `
<div style="display:flex;gap:8px;padding:4px 0"> <div style="display:flex;gap:8px;padding:4px 0">
<span style="color:#16a34a;flex-shrink:0">${UI.icon('check')}</span> <span style="color:#16a34a;flex-shrink:0">${UI.icon('check')}</span>
<span class="text-sm-secondary">${_esc(p)}</span> <span class="text-sm-secondary">${UI.escape(p)}</span>
</div>`).join(''); </div>`).join('');
welfareHTML = ` welfareHTML = `
@ -1254,7 +1250,7 @@ window.Page_zuchthunde = (() => {
${ik.toFixed(2)} % ${ik.toFixed(2)} %
</div> </div>
<div style="font-size:var(--text-sm);color:${ampelColor};font-weight:var(--weight-semibold)"> <div style="font-size:var(--text-sm);color:${ampelColor};font-weight:var(--weight-semibold)">
${_esc(result.ik_rating || ampelLabel)} ${UI.escape(result.ik_rating || ampelLabel)}
</div> </div>
</div> </div>
</div> </div>
@ -1314,7 +1310,7 @@ window.Page_zuchthunde = (() => {
} catch (err) { } catch (err) {
UI.modal.open({ UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`, title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<p class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`, body: `<p class="text-danger">${UI.escape(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`, footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
}); });
return; return;
@ -1322,7 +1318,7 @@ window.Page_zuchthunde = (() => {
UI.modal.open({ UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`, title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`, body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${UI.escape(text)}</div>`,
footer: ` footer: `
<button class="btn btn-secondary flex-1" id="ki-desc-copy"> <button class="btn btn-secondary flex-1" id="ki-desc-copy">
${UI.icon('clipboard-text')} Kopieren ${UI.icon('clipboard-text')} Kopieren
@ -1414,7 +1410,7 @@ window.Page_zuchthunde = (() => {
} catch (err) { } catch (err) {
UI.modal.open({ UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht`, title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
body: `<p class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`, body: `<p class="text-danger">${UI.escape(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`, footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
}); });
return; return;
@ -1429,7 +1425,7 @@ window.Page_zuchthunde = (() => {
body: ` body: `
${savedId ? `<p style="font-size:var(--text-xs);color:var(--c-success);margin:0 0 var(--space-3);display:flex;align-items:center;gap:4px"> ${savedId ? `<p style="font-size:var(--text-xs);color:var(--c-success);margin:0 0 var(--space-3);display:flex;align-items:center;gap:4px">
${UI.icon('check-circle')} Automatisch gespeichert</p>` : ''} ${UI.icon('check-circle')} Automatisch gespeichert</p>` : ''}
<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`, <div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${UI.escape(text)}</div>`,
footer: ` footer: `
<button class="btn btn-ghost btn-sm" id="ki-bericht-copy"> <button class="btn btn-ghost btn-sm" id="ki-bericht-copy">
${UI.icon('clipboard-text')} Kopieren ${UI.icon('clipboard-text')} Kopieren
@ -1532,7 +1528,7 @@ window.Page_zuchthunde = (() => {
} catch (err) { } catch (err) {
UI.modal.open({ UI.modal.open({
title: `${UI.icon('sparkle')} KI-Paarungsanalyse`, title: `${UI.icon('sparkle')} KI-Paarungsanalyse`,
body: `<p class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`, body: `<p class="text-danger">${UI.escape(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`, footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
}); });
return; return;
@ -1561,9 +1557,9 @@ window.Page_zuchthunde = (() => {
<div style="padding:var(--space-3);border-radius:var(--radius-md); <div style="padding:var(--space-3);border-radius:var(--radius-md);
background:${empfehlungColor}18;border:1.5px solid ${empfehlungColor}40; background:${empfehlungColor}18;border:1.5px solid ${empfehlungColor}40;
font-weight:var(--weight-semibold);color:${empfehlungColor}"> font-weight:var(--weight-semibold);color:${empfehlungColor}">
${UI.icon('check-circle')} ${_esc(empfehlungLabel)} ${UI.icon('check-circle')} ${UI.escape(empfehlungLabel)}
</div>` : ''} </div>` : ''}
<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div> <div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${UI.escape(text)}</div>
</div>`, </div>`,
footer: ` footer: `
<button class="btn btn-secondary flex-1" id="ki-paarung-copy"> <button class="btn btn-secondary flex-1" id="ki-paarung-copy">
@ -1752,15 +1748,15 @@ window.Page_zuchthunde = (() => {
<div style="position:relative;border-radius:var(--radius-md);overflow:hidden; <div style="position:relative;border-radius:var(--radius-md);overflow:hidden;
border:${isPrimary ? '2px solid var(--c-primary)' : '1px solid var(--c-border)'};aspect-ratio:1" border:${isPrimary ? '2px solid var(--c-primary)' : '1px solid var(--c-border)'};aspect-ratio:1"
data-photo-id="${ph.id}"> data-photo-id="${ph.id}">
<a href="${_esc(ph.url || '')}" target="_blank" rel="noopener noreferrer"> <a href="${UI.escape(ph.url || '')}" target="_blank" rel="noopener noreferrer">
<img src="${_esc(thumb)}" alt="${_esc(ph.caption || '')}" <img src="${UI.escape(thumb)}" alt="${UI.escape(ph.caption || '')}"
loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block" loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.parentElement.style.opacity='.4'"> onerror="this.parentElement.parentElement.style.opacity='.4'">
</a> </a>
${isPrimary ? `<span style="position:absolute;top:3px;left:3px;background:var(--c-primary);color:white; ${isPrimary ? `<span style="position:absolute;top:3px;left:3px;background:var(--c-primary);color:white;
font-size:9px;font-weight:700;border-radius:999px;padding:1px 5px">Logo</span>` : ''} font-size:9px;font-weight:700;border-radius:999px;padding:1px 5px">Logo</span>` : ''}
<!-- Sichtbarkeit --> <!-- Sichtbarkeit -->
<button class="bp-vis-btn" data-photo-id="${ph.id}" data-vis="${_esc(ph.visibility)}" <button class="bp-vis-btn" data-photo-id="${ph.id}" data-vis="${UI.escape(ph.visibility)}"
style="position:absolute;bottom:0;left:0;right:0;background:${vis.color};color:#fff; style="position:absolute;bottom:0;left:0;right:0;background:${vis.color};color:#fff;
border:none;cursor:pointer;font-size:9px;padding:2px 4px;font-weight:700"> border:none;cursor:pointer;font-size:9px;padding:2px 4px;font-weight:700">
${vis.text} ${vis.text}
@ -1805,7 +1801,7 @@ window.Page_zuchthunde = (() => {
}); });
} catch (err) { } catch (err) {
const el = document.getElementById(galleryId); const el = document.getElementById(galleryId);
if (el) el.innerHTML = `<p class="text-danger">${_esc(err.message || 'Fehler')}</p>`; if (el) el.innerHTML = `<p class="text-danger">${UI.escape(err.message || 'Fehler')}</p>`;
} }
} }

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=1112"></script> <script src="/js/landing-init.js?v=1113"></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

@ -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 = '1112'; const VER = '1113';
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