Feature: Züchter-Profil Komplett-Redesign — Hero, Hunde+Tests, Würfe, Gesundheitsstatistik (SW by-v900)

This commit is contained in:
rene 2026-05-13 18:47:49 +02:00
parent d5a3a1bb05
commit b17706e7ba
6 changed files with 362 additions and 143 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '899'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '900'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen

View file

@ -1,6 +1,5 @@
/* ============================================================
BAN YARO Öffentliches Züchter-Profil
Seiten-Modul: Zeigt das verifizierte Profil eines Züchters.
BAN YARO Öffentliches Züchter-Profil (Visitenkarte)
============================================================ */
window.Page_breeder = (() => {
@ -8,7 +7,8 @@ window.Page_breeder = (() => {
let _container = null;
let _appState = null;
const _esc = s => UI.esc ? UI.esc(s) : String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const _esc = s => UI.esc ? UI.esc(s) : String(s ?? '').replace(/[&<>"']/g,
c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// ----------------------------------------------------------
// INIT
@ -17,7 +17,6 @@ window.Page_breeder = (() => {
_container = container;
_appState = appState;
// Zwingername aus params oder URL-Pfad (/breeder/vom-sonnenfeld)
const zwingername = params?.zwingername
|| decodeURIComponent((window.location.pathname.split('/breeder/')[1] || '').replace(/\/$/, ''));
@ -27,8 +26,8 @@ window.Page_breeder = (() => {
}
container.innerHTML = `
<div id="breeder-profile-body" style="padding:var(--space-4) var(--space-4) calc(var(--space-16) + 24px)">
${UI.skeleton(3)}
<div id="breeder-profile-body" style="padding-bottom:calc(var(--space-16) + 24px)">
${UI.skeleton(5)}
</div>
<button id="breeder-back-fab" aria-label="Zurück zur Wurfbörse"
style="position:fixed;bottom:calc(var(--safe-bottom,0px) + 20px);right:20px;
@ -51,7 +50,9 @@ window.Page_breeder = (() => {
_render(p);
} catch (e) {
document.getElementById('breeder-profile-body').innerHTML =
`<p style="padding:var(--space-6);color:var(--c-text-secondary)">${_esc(e.message || 'Züchter nicht gefunden.')}</p>`;
`<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.')}
</div>`;
}
}
@ -59,176 +60,319 @@ window.Page_breeder = (() => {
// RENDER
// ----------------------------------------------------------
function _render(p) {
const verifiedDate = p.verified_at
? new Date(p.verified_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
const body = document.getElementById('breeder-profile-body') || _container;
const seit = p.verified_at
? new Date(p.verified_at).toLocaleDateString('de-DE', { month: 'long', year: 'numeric' })
: null;
const websiteHtml = p.website
? `<a href="${_esc(p.website)}" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);text-decoration:none;word-break:break-all">
${UI.icon('arrow-square-out')} ${_esc(p.website)}
</a>`
: '';
const isLoggedIn = !!_appState?.user;
const isOwnProfile = _appState?.user?.id === p.zuechter_user_id;
const beschreibungHtml = p.beschreibung
? `<div class="card" style="margin-bottom:var(--space-3)">
<p style="margin:0;white-space:pre-line;color:var(--c-text-secondary)">${_esc(p.beschreibung)}</p>
</div>`
: '';
const body = document.getElementById('breeder-profile-body') || _container;
body.innerHTML = `
<div style="padding:var(--space-4) 0">
<!-- Header-Card -->
<div class="card" style="margin-bottom:var(--space-3)">
<!-- HERO -->
<div style="background:linear-gradient(135deg,var(--c-primary-dark,#a86e2e),var(--c-primary,#C4843A));
padding:var(--space-6) var(--space-4) var(--space-8);color:white;position:relative">
<div style="max-width:640px;margin:0 auto">
<div style="display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap">
<div style="flex:1;min-width:0">
<h2 style="margin:0 0 var(--space-1);font-size:var(--text-xl);word-break:break-word">
${UI.icon('certificate')} ${_esc(p.zwingername)}
</h2>
<span class="badge badge-primary" style="background:var(--c-success,#22C55E);color:#fff;font-size:var(--text-xs)">
${UI.icon('seal-check')} Verifizierter Züchter
</span>
<p style="margin:0 0 var(--space-1);font-size:var(--text-xs);opacity:.7;text-transform:uppercase;letter-spacing:.1em">
Verifizierter Züchter
</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">
${_esc(p.zwingername)}
</h1>
<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.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>` : ''}
${seit ? `<span style="opacity:.7;font-size:var(--text-xs)">Züchter seit ${_esc(seit)}</span>` : ''}
</div>
</div>
<div style="background:rgba(255,255,255,.15);border-radius:50%;width:52px;height:52px;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg style="width:28px;height:28px" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#seal-check"></use></svg>
</div>
</div>
${!isOwnProfile ? `
<div style="margin-top:var(--space-5);display:flex;gap:var(--space-3);flex-wrap:wrap">
${isLoggedIn
? `<button class="breeder-chat-btn"
style="background:white;color:var(--c-primary-dark,#a86e2e);border:none;
border-radius:999px;padding:var(--space-2) var(--space-5);
font-weight:700;cursor:pointer;display:flex;align-items:center;gap:6px">
${UI.icon('chat-circle-dots')} Nachricht senden
</button>`
: `<button class="breeder-login-btn"
style="background:white;color:var(--c-primary-dark,#a86e2e);border:none;
border-radius:999px;padding:var(--space-2) var(--space-5);
font-weight:700;cursor:pointer">
Anmelden um zu schreiben
</button>`
}
${p.website ? `<a href="${_esc(p.website)}" target="_blank" rel="noopener noreferrer"
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);
font-weight:600;font-size:var(--text-sm);text-decoration:none;
display:flex;align-items:center;gap:6px">
${UI.icon('arrow-square-out')} Website
</a>` : ''}
</div>` : ''}
</div>
</div>
<!-- Details-Card -->
<div class="card" style="margin-bottom:var(--space-3)">
<dl style="margin:0;display:flex;flex-direction:column;gap:var(--space-3)">
<div style="max-width:640px;margin:0 auto;padding:var(--space-4)">
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Rasse</dt>
<dd style="margin:0;font-weight:600">${_esc(p.rasse_text || '')}</dd>
</div>
<!-- Beschreibung -->
${p.beschreibung ? `
<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)">
<p style="margin:0;line-height:1.7;color:var(--c-text-secondary);white-space:pre-line">${_esc(p.beschreibung)}</p>
</div>` : ''}
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Verein</dt>
<dd style="margin:0">${_esc(p.verein || '')}</dd>
</div>
<!-- Zuchthunde -->
${p.hunde?.length ? `
<div style="margin-bottom:var(--space-5)">
<h2 style="margin:0 0 var(--space-3);font-size:var(--text-base);font-weight:700;
display:flex;align-items:center;gap:var(--space-2)">
${UI.icon('dog')} Unsere Zuchthunde
<span style="font-size:var(--text-xs);color:var(--c-text-muted);font-weight:400">${p.hunde.length} Hunde</span>
</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-3)">
${p.hunde.map(h => _hundCard(h)).join('')}
</div>
</div>` : ''}
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">VDH-Mitglied</dt>
<dd style="margin:0">
${p.vdh_mitglied
? `<span class="badge badge-primary">${UI.icon('check')} Ja</span>`
: `<span style="color:var(--c-text-secondary)">Nein</span>`}
</dd>
</div>
<!-- Aktuelle Würfe -->
${p.wuerfe?.length ? `
<div style="margin-bottom:var(--space-5)">
<h2 style="margin:0 0 var(--space-3);font-size:var(--text-base);font-weight:700;
display:flex;align-items:center;gap:var(--space-2)">
${UI.icon('baby')} Aktuelle Würfe
</h2>
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
${p.wuerfe.map(w => _wurfCard(w)).join('')}
</div>
</div>` : ''}
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Stadt</dt>
<dd style="margin:0">${_esc(p.stadt || '')}</dd>
</div>
<!-- Gesundheits-Transparenz -->
${(p.hd_stats?.length || p.ed_stats?.length) ? `
<div style="margin-bottom:var(--space-5)">
<h2 style="margin:0 0 var(--space-3);font-size:var(--text-base);font-weight:700;
display:flex;align-items:center;gap:var(--space-2)">
${UI.icon('heartbeat')} Gesundheits-Transparenz
</h2>
<div style="background:var(--c-bg-secondary);border:1px solid var(--c-border);
border-radius:var(--radius-lg);padding:var(--space-4)">
${_statsSection('HD-Ergebnisse', p.hd_stats)}
${p.hd_stats?.length && p.ed_stats?.length ? '<hr style="border:none;border-top:1px solid var(--c-border);margin:var(--space-3) 0">' : ''}
${_statsSection('ED-Ergebnisse', p.ed_stats)}
</div>
</div>` : ''}
<!-- Kontakt/Details -->
<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)">
<h2 style="margin:0 0 var(--space-3);font-size:var(--text-base);font-weight:700">
${UI.icon('info')} Über den Züchter
</h2>
<dl style="margin:0;display:flex;flex-direction:column;gap:var(--space-2)">
${_dl('Züchter', p.zuechter_name)}
${_dl('Rasse(n)', p.rasse_text)}
${_dl('Verein', p.verein)}
${_dl('VDH-Mitglied', p.vdh_mitglied ? '✓ Ja' : 'Nein')}
${_dl('Stadt', p.stadt)}
${p.website ? `
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Website</dt>
<dd style="margin:0">${websiteHtml}</dd>
<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"
style="color:var(--c-primary);word-break:break-all">${_esc(p.website)}</a></dd>
</div>` : ''}
${verifiedDate ? `
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Verifiziert</dt>
<dd style="margin:0;color:var(--c-text-secondary);font-size:var(--text-sm)">${verifiedDate}</dd>
</div>` : ''}
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Züchter</dt>
<dd style="margin:0">${_esc(p.zuechter_name || '')}</dd>
</div>
${seit ? _dl('Züchter seit', seit) : ''}
</dl>
</div>
<!-- Beschreibung -->
${beschreibungHtml}
<!-- Fotos (werden asynchron nachgeladen) -->
<!-- Fotos -->
<div id="breeder-photos-section"></div>
<!-- Kontakt-Button -->
${(() => {
if (!p.zuechter_user_id) return '';
const isLoggedIn = !!_appState?.user;
const isOwnProfile = _appState?.user?.id === p.zuechter_user_id;
if (isOwnProfile) return '';
if (isLoggedIn) {
return `<button class="btn btn-primary breeder-chat-btn" style="width:100%">
${UI.icon('chat-circle')} Nachricht senden
</button>`;
}
return `<button class="btn btn-primary breeder-login-btn" style="width:100%">
${UI.icon('sign-in')} Anmelden um zu schreiben
</button>`;
})()}
</div>`;
</div>
`;
// Events
body.querySelector('.breeder-chat-btn')?.addEventListener('click', () => _contactBreeder(p.zuechter_user_id));
body.querySelector('.breeder-login-btn')?.addEventListener('click', () => App.navigate('settings'));
_container.querySelector('.breeder-chat-btn')?.addEventListener('click', () => {
_contactBreeder(p.zuechter_user_id);
});
_container.querySelector('.breeder-login-btn')?.addEventListener('click', () => {
App.navigate('settings');
});
// Öffentliche Fotos nachladen
_loadBreederPhotos(p.id);
}
// ----------------------------------------------------------
// Hund-Karte
// ----------------------------------------------------------
function _hundCard(h) {
const alter = h.geburtsdatum
? Math.floor((Date.now() - new Date(h.geburtsdatum)) / 31557600000)
: null;
const gIcon = h.geschlecht === 'maennlich' ? UI.icon('gender-male') : UI.icon('gender-female');
const hdTest = h.health_tests?.find(t => t.test_typ === 'HD');
const edTest = h.health_tests?.find(t => t.test_typ === 'ED');
const augeTest = h.health_tests?.find(t => t.test_typ === 'augen');
const testPills = [
hdTest ? `<span style="${_testPillStyle(hdTest.ergebnis,'HD')}">HD ${_esc(hdTest.ergebnis)}</span>` : '',
edTest ? `<span style="${_testPillStyle(edTest.ergebnis,'ED')}">ED ${_esc(edTest.ergebnis)}</span>` : '',
augeTest ? `<span style="${_testPillStyle('clear','augen')}">Augen ✓</span>` : '',
].filter(Boolean).join('');
const titlePills = (h.titel || []).map(t =>
`<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>`
).join('');
const genBadge = h.gentests_total > 0
? `<span style="font-size:var(--text-xs);color:var(--c-text-muted)">
${h.gentests_clear}/${h.gentests_total} Gentests frei
</span>`
: '';
return `
<div style="background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius-lg);
padding:var(--space-3);display:flex;flex-direction:column;gap:var(--space-2)">
<div style="display:flex;align-items:center;gap:var(--space-2)">
<span style="color:var(--c-primary)">${gIcon}</span>
<span style="font-weight:700;font-size:var(--text-sm)">${_esc(h.name)}</span>
${h.rufname ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">"${_esc(h.rufname)}"</span>` : ''}
${alter !== null ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs);margin-left:auto">${alter} J.</span>` : ''}
</div>
${h.farbe ? `<p style="margin:0;font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(h.farbe)}</p>` : ''}
${testPills ? `<div style="display:flex;flex-wrap:wrap;gap:4px">${testPills}</div>` : ''}
${titlePills ? `<div style="display:flex;flex-wrap:wrap;gap:4px">${titlePills}</div>` : ''}
${genBadge}
</div>`;
}
function _testPillStyle(ergebnis, typ) {
const e = (ergebnis || '').toUpperCase();
let bg = '#6b72801a', color = '#6b7280', border = '#6b728040';
if (typ === 'HD') {
if (['A','A1','A2'].includes(e)) { bg='#16a34a1a';color='#16a34a';border='#16a34a40'; }
else if (e === 'B' || e === 'B1' || e === 'B2') { bg='#86efac1a';color='#15803d';border='#86efac40'; }
else if (e === 'C') { bg='#eab3081a';color='#a16207';border='#eab30840'; }
else if (e === 'D' || e === 'E') { bg='#ef44441a';color='#dc2626';border='#ef444440'; }
} else if (typ === 'ED') {
if (e === '0' || e === 'ED 0') { bg='#16a34a1a';color='#16a34a';border='#16a34a40'; }
else if (e === '1') { bg='#eab3081a';color='#a16207';border='#eab30840'; }
else if (e === '2' || e === '3') { bg='#ef44441a';color='#dc2626';border='#ef444440'; }
} else if (typ === 'augen' || ergebnis === 'clear') {
bg='#16a34a1a';color='#16a34a';border='#16a34a40';
}
return `background:${bg};color:${color};border:1px solid ${border};border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600`;
}
// ----------------------------------------------------------
// Wurf-Karte
// ----------------------------------------------------------
const _STATUS_LABEL = { geplant: 'Geplant', geboren: 'Geboren', verfuegbar: 'Verfügbar', abgeschlossen: 'Abgeschlossen' };
const _STATUS_COLOR = { geplant: '#6b7280', geboren: '#3b82f6', verfuegbar: '#16a34a', abgeschlossen: '#9ca3af' };
function _wurfCard(w) {
const eltern = [w.vater_name, w.mutter_name].filter(Boolean).join(' × ') || '—';
const datum = w.geburt_datum
? `Geburt: ${_fmtDate(w.geburt_datum)}`
: w.erwartetes_datum ? `Erwartet: ${_fmtDate(w.erwartetes_datum)}` : '';
const sc = _STATUS_COLOR[w.status] || '#6b7280';
const sl = _STATUS_LABEL[w.status] || w.status;
return `
<div style="background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius-lg);
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)">
<span style="font-weight:700;font-size:var(--text-sm)">${_esc(eltern)}</span>
<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>
</div>
<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>` : ''}
${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>` : ''}
</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>` : ''}
</div>`;
}
// ----------------------------------------------------------
// Statistik-Sektion
// ----------------------------------------------------------
function _statsSection(label, stats) {
if (!stats?.length) return '';
const total = stats.reduce((s, r) => s + r.cnt, 0);
return `
<div>
<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>
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2)">
${stats.map(r => `
<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="color:var(--c-text-muted)">${r.cnt}×</span>
<span style="background:var(--c-border);border-radius:999px;height:6px;
width:${Math.round(r.cnt/total*80)+16}px;display:inline-block"></span>
</div>`).join('')}
</div>
</div>`;
}
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
function _dl(label, value) {
if (!value) return '';
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>
<dd style="margin:0;font-size:var(--text-sm)">${_esc(String(value))}</dd>
</div>`;
}
function _fmtDate(iso) {
if (!iso) return '—';
const [y,m,d] = iso.slice(0,10).split('-');
return `${d}.${m}.${y}`;
}
async function _loadBreederPhotos(breederId) {
const section = document.getElementById('breeder-photos-section');
if (!section) return;
try {
const photos = await API.breederPhotos.list('breeder', breederId);
if (!photos || !photos.length) return;
if (!photos?.length) return;
section.innerHTML = `
<div class="card" style="margin-bottom:var(--space-3)">
<h3 style="margin:0 0 var(--space-3);font-size:var(--text-md);font-weight:var(--weight-semibold)">
<div style="margin-bottom:var(--space-4)">
<h2 style="margin:0 0 var(--space-3);font-size:var(--text-base);font-weight:700">
${UI.icon('images')} Fotos
</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:var(--space-2)">
${photos.map(ph => {
const thumb = ph.thumbnail_url || ph.url || '';
return `
<a href="${_esc(ph.url || '')}" target="_blank" rel="noopener noreferrer"
style="display:block;border-radius:var(--radius-md);overflow:hidden;
border:1px solid var(--c-border);aspect-ratio:1">
<img src="${_esc(thumb)}"
alt="${_esc(ph.caption || '')}"
loading="lazy"
style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'">
</a>`;
}).join('')}
</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:var(--space-2)">
${photos.map(ph => `
<a href="${_esc(ph.url||'')}" target="_blank" rel="noopener noreferrer"
style="display:block;border-radius:var(--radius-md);overflow:hidden;
border:1px solid var(--c-border);aspect-ratio:1">
<img src="${_esc(ph.thumbnail_url||ph.url||'')}" alt="${_esc(ph.caption||'')}"
loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'">
</a>`).join('')}
</div>
</div>`;
} catch (_) {
// Fotos sind nicht kritisch — bei Fehler still ignorieren
}
} catch (_) {}
}
async function _contactBreeder(breederId) {
if (!_appState?.user) {
App.navigate('settings');
return;
}
async function _contactBreeder(userId) {
if (!_appState?.user) { App.navigate('settings'); return; }
try {
await API.chat.start(breederId);
await API.chat.start(userId);
App.navigate('chat');
} catch (e) {
UI.toast.error(e.message || 'Chat konnte nicht geöffnet werden.');
}
} catch (e) { UI.toast.error(e.message || 'Chat konnte nicht geöffnet werden.'); }
}
function refresh() {}
function onDogChange() {}
function destroy() {
document.getElementById('breeder-back-fab')?.remove();
}
function destroy() { document.getElementById('breeder-back-fab')?.remove(); }
return { init, refresh, onDogChange, destroy };