banyaro/backend/static/js/pages/breeder.js
rene 459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
PHASE 1 — Sofort-Cleanup ohne Risiko:
- Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen:
  * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary
  * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3
  * flex-between, flex-1-min, mb-1/3, mt-1/3
  * icon-xs/sm/md/lg, label-block, caption
- index.html bindet utilities.css ein
- mb-3/mt-3 ergänzt (waren in design-system.css unvollständig)

PHASE 2 — .by-tab Modifier für Vereinheitlichung:
- .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.)
- .by-tabs.sticky (Desktop vertikale Tabs für Admin)
- .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll)
- .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border)

PHASE 3 — Inline-Style → Klassen-Migration (Python-Script):
- 948 Inline-Styles entfernt (5101 → 4153, -18%)
- 962 Migrationen über 47 Page-Dateien
- Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67),
  litters.js (62), settings.js (61), zuchthunde.js (51)
- Patterns: text-muted, text-secondary, text-danger, text-xs-muted,
  text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3,
  p-3/4, mb-2/3/4, hidden, w-full, flex-1, ...
- Bewahrt bestehende class-Attribute (mergt korrekt)

Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
2026-05-27 07:11:27 +02:00

412 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Öffentliches Züchter-Profil (Visitenkarte)
============================================================ */
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]));
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState, params = {}) {
_container = container;
_appState = appState;
const zwingername = params?.zwingername
|| decodeURIComponent((window.location.pathname.split('/breeder/')[1] || '').replace(/\/$/, ''));
if (!zwingername) {
container.innerHTML = '<div style="padding:var(--space-6)">Kein Zwingername angegeben.</div>';
return;
}
container.innerHTML = `
<div id="breeder-profile-body" style="padding-bottom:calc(var(--space-16) + 24px)">
${UI.skeleton(5)}
</div>`;
// FAB an document.body hängen damit position:fixed zuverlässig funktioniert
// und destroy() der einzige Lifecycle-Kontrollpunkt bleibt
const fab = document.createElement('button');
fab.id = 'breeder-back-fab';
fab.setAttribute('aria-label', 'Zurück zur Wurfbörse');
fab.style.cssText = 'position:fixed;bottom:calc(var(--safe-bottom,0px) + 20px);right:20px;' +
'width:54px;height:54px;border-radius:50%;background:var(--c-primary);' +
'border:none;color:#fff;cursor:pointer;z-index:200;' +
'display:flex;align-items:center;justify-content:center;' +
'box-shadow:0 4px 18px rgba(196,132,58,.45);transition:transform .12s,box-shadow .12s;' +
'-webkit-tap-highlight-color:transparent';
fab.innerHTML = '<svg style="width:22px;height:22px" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#arrow-left"></use></svg>';
fab.addEventListener('click', () => App.navigate('wurfboerse'));
document.body.appendChild(fab);
try {
const p = await API.breeder.profile(zwingername);
_render(p);
} catch (e) {
document.getElementById('breeder-profile-body').innerHTML =
`<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>`;
}
}
// ----------------------------------------------------------
// RENDER
// ----------------------------------------------------------
function _render(p) {
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 isLoggedIn = !!_appState?.user;
const isOwnProfile = _appState?.user?.id === p.zuechter_user_id;
body.innerHTML = `
<!-- ═══ 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 class="flex-1-min">
<p style="margin:0 0 var(--space-1);font-size:var(--text-xs);opacity:.7;text-transform:uppercase;letter-spacing:.1em">
${UI.icon('seal-check')} 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>
${p.logo_url
? `<img src="${_esc(p.logo_url)}" alt="Zwinger-Logo"
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)"
onerror="this.style.display='none'">`
: `<div style="background:rgba(255,255,255,.15);border-radius:50%;width:64px;height:64px;
display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg style="width:32px;height:32px" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#paw-print"></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>
<div style="max-width:640px;margin:0 auto;padding:var(--space-4)">
<!-- 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>` : ''}
<!-- 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>` : ''}
<!-- 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 class="flex-col-gap-3">
${p.wuerfe.map(w => _wurfCard(w)).join('')}
</div>
</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: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>` : ''}
${seit ? _dl('Züchter seit', seit) : ''}
</dl>
</div>
<!-- Fotos / Gallery -->
${p.fotos?.length ? `
<div class="mb-4">
<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('images')} Galerie
<span style="font-size:var(--text-xs);color:var(--c-text-muted);font-weight:400">${p.fotos.length} Fotos</span>
</h2>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--space-2)">
${p.fotos.map((ph, i) => `
<a href="${_esc(ph.url)}" target="_blank" rel="noopener noreferrer"
style="display:block;border-radius:var(--radius-md);overflow:hidden;
border:${ph.primary ? '2px solid var(--c-primary)' : '1px solid var(--c-border)'};
aspect-ratio:1;position:relative">
<img src="${_esc(ph.thumb)}" alt="${_esc(ph.caption)}"
loading="${i < 6 ? 'eager' : 'lazy'}"
style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'">
${ph.primary ? `<span style="position:absolute;top:4px;left:4px;background:var(--c-primary);
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;
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>` : ''}
</a>`).join('')}
</div>
</div>` : ''}
<div id="breeder-photos-section" class="hidden"></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'));
_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 class="text-xs-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 class="text-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 class="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?.length) return;
section.innerHTML = `
<div class="mb-4">
<h2 style="margin:0 0 var(--space-3);font-size:var(--text-base);font-weight:700">
${UI.icon('images')} Fotos
</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 (_) {}
}
async function _contactBreeder(userId) {
if (!_appState?.user) { App.navigate('settings'); return; }
try {
await API.chat.start(userId);
App.navigate('chat');
} 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(); }
return { init, refresh, onDogChange, destroy };
})();