diff --git a/VERSION b/VERSION index 1ea87ef..4d64262 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1101 \ No newline at end of file +1102 \ No newline at end of file diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 4ec2cef..69113a5 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -235,6 +235,45 @@ color: var(--c-primary); } +/* ----- .by-tabs Modifier-Varianten ----------------------------- */ + +/* Grid-Layout (Admin/Health/Übungen — Desktop oft 2-3 Spalten) */ +.by-tabs.grid { + display: grid; + grid-template-columns: repeat(var(--tab-cols, 4), minmax(0, 1fr)); + overflow: visible; + gap: var(--space-2); +} + +/* Flex-Wrap (Zuchthunde — Buttons brechen um statt zu scrollen) */ +.by-tabs.wrap { + flex-wrap: wrap; + overflow-x: visible; +} + +/* Separated — eigener Hintergrund + Border (Sitting) */ +.by-tabs.separated { + padding: var(--space-3) var(--space-4) var(--space-2); + border-bottom: 1px solid var(--c-border); + background: var(--c-surface); +} + +/* Sticky (Admin Desktop vertikal) — nur ab 1024px */ +@media (min-width: 1024px) { + .by-tabs.sticky { + position: sticky; + top: var(--space-3); + flex-direction: column; + width: 190px; + gap: var(--space-1); + } + .by-tabs.sticky .by-tab { + justify-content: flex-start; + text-align: left; + padding: var(--space-2) var(--space-3); + } +} + /* ------------------------------------------------------------ 4. BY-SECTION-LABEL + BY-TOOLBAR — weitere gemeinsame Elemente ------------------------------------------------------------ */ diff --git a/backend/static/css/utilities.css b/backend/static/css/utilities.css new file mode 100644 index 0000000..f9f5ec2 --- /dev/null +++ b/backend/static/css/utilities.css @@ -0,0 +1,65 @@ +/* ============================================================ + BAN YARO — Utility-Klassen für häufige Inline-Patterns + Ergänzt design-system.css (Single-Property-Utilities sind dort) + ============================================================ */ + +/* ------------------------------------------------------------ + Text + Farb-Kombinationen (häufigste Inline-Patterns) + ------------------------------------------------------------ */ +.text-xs-muted { font-size: var(--text-xs); color: var(--c-text-muted); } +.text-xs-secondary { font-size: var(--text-xs); color: var(--c-text-secondary); } +.text-sm-muted { font-size: var(--text-sm); color: var(--c-text-muted); } +.text-sm-secondary { font-size: var(--text-sm); color: var(--c-text-secondary); } + +/* Caption = Mini-Label/Hinweis unter einem Wert */ +.caption { + font-size: var(--text-xs); + color: var(--c-text-secondary); + margin-top: 2px; +} + +/* ------------------------------------------------------------ + Flex-Layouts (kombiniert) + ------------------------------------------------------------ */ +.flex-gap-2 { display: flex; gap: var(--space-2); } +.flex-gap-3 { display: flex; gap: var(--space-3); } +.flex-col-gap-2 { display: flex; flex-direction: column; gap: var(--space-2); } +.flex-col-gap-3 { display: flex; flex-direction: column; gap: var(--space-3); } +.flex-col-gap-4 { display: flex; flex-direction: column; gap: var(--space-4); } + +.flex-center { display: flex; align-items: center; } +.flex-center-gap-1 { display: flex; align-items: center; gap: var(--space-1); } +.flex-center-gap-2 { display: flex; align-items: center; gap: var(--space-2); } +.flex-center-gap-3 { display: flex; align-items: center; gap: var(--space-3); } + +.flex-between { display: flex; align-items: center; justify-content: space-between; } +.flex-between-gap-2 { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); } + +/* min-width:0 + flex:1 — verhindert Overflow in Flex-Children */ +.flex-1-min { flex: 1; min-width: 0; } + +/* ------------------------------------------------------------ + Spacing-Lücken in design-system.css füllen + ------------------------------------------------------------ */ +.mb-1 { margin-bottom: var(--space-1); } +.mb-3 { margin-bottom: var(--space-3); } +.mt-1 { margin-top: var(--space-1); } +.mt-3 { margin-top: var(--space-3); } + +/* ------------------------------------------------------------ + Icon-Größen (statt width:NNpx;height:NNpx inline) + ------------------------------------------------------------ */ +.icon-xs { width: 12px; height: 12px; } +.icon-sm { width: 14px; height: 14px; } +.icon-md { width: 18px; height: 18px; } +.icon-lg { width: 22px; height: 22px; } + +/* ------------------------------------------------------------ + Form-Helper + ------------------------------------------------------------ */ +.label-block { + display: block; + font-size: var(--text-sm); + font-weight: 600; + margin-bottom: var(--space-1); +} diff --git a/backend/static/index.html b/backend/static/index.html index b5a585a..1800abd 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,12 +86,13 @@ Ban Yaro - + - - - + + + + @@ -615,11 +616,11 @@ - - - - - + + + + + @@ -629,7 +630,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 2840d12..aeb7b90 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1101'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1102'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 4a1a63a..13e56af 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -203,12 +203,12 @@ window.Page_admin = (() => { const managerRows = d.managers.map(m => ` ${_esc(m.name)} - ${m.published} - ${m.with_link} + ${m.published} + ${m.with_link} ${m.published > 0 ? ` (${Math.round(m.with_link/m.published*100)}%)` : ''} - ${m.scheduled} - ${m.ideas} + ${m.scheduled} + ${m.ideas} ${m.total} `).join(''); @@ -255,24 +255,24 @@ window.Page_admin = (() => { el.innerHTML = `
-
+
Veröffentlicht nach Plattform
- ${platBars || '
Noch keine Posts
'} + ${platBars || '
Noch keine Posts
'}
-
+
Posts pro Monat
- ${monthBars || '
Noch keine Posts
'} + ${monthBars || '
Noch keine Posts
'}
${d.managers.length ? ` -
+
Manager-Übersicht
@@ -280,11 +280,11 @@ window.Page_admin = (() => { - - - - - + + + + + ${managerRows}
ManagerVeröffentlichtMit LinkGeplantIdeenGesamtVeröffentlichtMit LinkGeplantIdeenGesamt
@@ -367,7 +367,7 @@ window.Page_admin = (() => { const x = n === 1 ? W/2 : padX + i * ((W - 2*padX) / (n - 1)); const date = pvData[i]?.x ? new Date(pvData[i].x).toLocaleDateString('de',{day:'2-digit',month:'2-digit'}) : ''; return `${date}`; + font-size="10" fill="currentColor" class="text-muted">${date}`; }).join(''); return ` { // Balken-Chart für Top-Pages / Referrers function _barChart(items, labelKey = 'x', valKey = 'y') { - if (!items?.length) return `

Keine Daten

`; + if (!items?.length) return `

Keine Daten

`; const maxV = Math.max(...items.map(p => p[valKey] ?? 0), 1); - return `
+ return `
${items.map(p => { const pct = ((p[valKey] ?? 0) / maxV * 100).toFixed(0); return `
@@ -423,7 +423,7 @@ window.Page_admin = (() => {
-
+
Verlauf — letzte 30 Tage
@@ -440,7 +440,7 @@ window.Page_admin = (() => {
-
+
Jahresübersicht — letzte 12 Monate
@@ -492,7 +492,7 @@ window.Page_admin = (() => { ${label} + font-size="9" fill="currentColor" class="text-muted">${label} `; }).join(''); @@ -503,12 +503,12 @@ window.Page_admin = (() => {
-
+
Top Seiten — 30 Tage
${_barChart(d.top_pages)}
-
+
Referrers — 30 Tage
${_barChart(d.referrers)} @@ -536,7 +536,7 @@ window.Page_admin = (() => { padding:var(--space-2) var(--space-3);background:var(--c-surface-2); border-radius:var(--radius-md)"> - KI-Modus: off + KI-Modus: off
`; } const dot = ki.local_reachable ? 'var(--c-success)' : 'var(--c-danger)'; @@ -548,7 +548,7 @@ window.Page_admin = (() => { ${label} - · + · ${_esc(model)} @@ -576,7 +576,7 @@ window.Page_admin = (() => { ${_statCard('squares-four', 'Gecachte Tiles', s.osm_tiles.toLocaleString('de'), 'var(--c-text-secondary)', 'system')}
-
+

@@ -601,7 +601,7 @@ window.Page_admin = (() => { ].map(([label, val, color]) => ` - ${label} + ${label} ${val ?? 0} `).join('')}

@@ -618,7 +618,7 @@ window.Page_admin = (() => { }).join(' '); const first = hist[0]?.date?.slice(5) || ''; const last = hist[hist.length-1]?.date?.slice(5) || ''; - return `
+ return `
@@ -642,10 +642,10 @@ window.Page_admin = (() => { ${(kiH.top_users).map(u => ` ${_esc(u.name)} - ${_esc(u.email.length > 22 ? u.email.split('@')[1] : u.email)} + ${_esc(u.email.length > 22 ? u.email.split('@')[1] : u.email)} ${u.cloud} ${u.total} - ${u.last_date?.slice(5) || '—'} + ${u.last_date?.slice(5) || '—'} `).join('')} @@ -664,25 +664,25 @@ window.Page_admin = (() => { const label = t => POI_LABELS[t] || t; const row = ([type, count]) => `
- ${label(type)} + ${label(type)} ${count.toLocaleString('de')}
`; const userByType = s.user_poi_by_type || {}; const userTotal = s.user_poi_total ?? 0; return ` -
+

OSM-Cache nach Typ — ${(s.osm_total || 0).toLocaleString('de')} gecacht

-
+
${Object.entries(s.osm_by_type).map(row).join('')}
-
+

Nutzer-POIs nach Typ — ${userTotal.toLocaleString('de')} gesamt

-
+
${Object.keys(userByType).length ? Object.entries(userByType).map(row).join('') : '
Noch keine Nutzer-POIs
'} @@ -691,7 +691,7 @@ window.Page_admin = (() => { })()} -
+

📱 Social Media Tracking

+
Kategorien
@@ -743,9 +743,9 @@ window.Page_admin = (() => {
` : ''}
-
+

-

${total} Nutzer gefunden
-
+
${users.map(u => `
@@ -841,14 +841,14 @@ window.Page_admin = (() => {
-
+
${_esc(u.name)} ${u.is_banned ? ` GESPERRT` : ''}
-
+
${_esc(u.email)} · ${_esc(u.rolle)} @@ -876,11 +876,11 @@ window.Page_admin = (() => {
${u.is_banned ? `` : `` } @@ -897,7 +897,7 @@ window.Page_admin = (() => { ` : ''} @@ -953,7 +953,7 @@ window.Page_admin = (() => {

Aktuelle Rolle: ${currentRolle}

-
+
${rollen.map(r => ` ` : ''}
@@ -1166,18 +1166,18 @@ window.Page_admin = (() => { return; } el.innerHTML = ` -
+
${threads.map(t => `
-
+
${t.is_deleted ? '' : ''}${_esc(t.titel)}${t.is_deleted ? '' : ''}
-
+
von ${_esc(t.autor_name)} · ${t.antworten} Antworten · ${t.is_pinned ? '📌 ' : ''}${t.is_locked ? '🔒 ' : ''}${t.is_deleted ? '🗑 gelöscht' : ''} @@ -1194,12 +1194,12 @@ window.Page_admin = (() => { ` : ` `} @@ -1311,11 +1311,11 @@ window.Page_admin = (() => { box.innerHTML = rows.reverse().map(r => { const color = COLORS[r.l] || '#6b7280'; return `
` + - `${r.t} ` + + `${r.t} ` + `${r.l} ` + - `${_esc(r.n)} ` + + `${_esc(r.n)} ` + `${_esc(r.m)}
`; - }).join('') || 'Keine Einträge'; + }).join('') || 'Keine Einträge'; }; el.querySelector('#adm-sys-refresh').addEventListener('click', () => { _loadSystemCards(el.querySelector('#adm-sys-cards')); @@ -1401,7 +1401,7 @@ window.Page_admin = (() => {
${['vollstaendigkeit','korrektheit','sprachqualitaet','konsistenz','gesamt'].map(k => - `
+ `
${avg[k]?.toFixed(1) ?? '–'}
${{vollstaendigkeit:'Vollst.',korrektheit:'Korrekt.',sprachqualitaet:'Sprache',konsistenz:'Konsistenz',gesamt:'Gesamt'}[k]}
` @@ -1603,7 +1603,7 @@ window.Page_admin = (() => { function _historySection(label, items, renderItem) { const id = `hist-${label.replace(/\W/g,'').toLowerCase()}`; return ` -
+
Keine ausstehenden Einreichungen.

`; } else { - html += `
+ html += `
@@ -1712,7 +1712,7 @@ window.Page_admin = (() => { `).join('')}
RasseName / Zwingername OrtVDHAlterWebsite${z.website ? `Link` : '—'} - +
`; @@ -1736,19 +1736,19 @@ window.Page_admin = (() => { } else { html += `
${fotosPending.map(f => ` -
+
${_esc(f.rasse_name)}
von ${_esc(f.user_name)}
-
${_ageLabel(f.created_at)}
+
${_ageLabel(f.created_at)}
${f.aktuell_foto ? `Aktuell
↑ aktuelles Foto
` : ''} -
- - +
+ +
`).join('')}
`; @@ -1778,7 +1778,7 @@ window.Page_admin = (() => { ${reportsPending.map(r => `
-
+
${_esc(r.target_type)} #${r.target_id} · Gemeldet von ${_esc(r.melder_name || '?')} ${_ageLabel(r.created_at)} @@ -1815,7 +1815,7 @@ window.Page_admin = (() => { `; if (!poiPending.length) { - html += `

Keine ausstehenden POI-Korrekturen.

`; + html += `

Keine ausstehenden POI-Korrekturen.

`; } else { html += `
@@ -1832,16 +1832,16 @@ window.Page_admin = (() => { ${poiPending.map((e, i) => ` - + - - + + @@ -1853,7 +1853,7 @@ window.Page_admin = (() => { // POI-History if (poiDone.length) html += _historySection('POI-Korrekturen', poiDone, e => `${_esc(e.poi_name||`OSM #${e.osm_id}`)} · - ${_esc(e.field)}: + ${_esc(e.field)}: ${_esc(e.old_value||'—')} → ${_esc(e.new_value||'—')} · ${e.status==='approved' ? `${UI.icon('check-circle')} freigegeben` : `${UI.icon('x-circle')} abgelehnt`} @@ -1945,7 +1945,7 @@ window.Page_admin = (() => {
Lade…
-
Lade…
+
Lade…
`; el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => { _loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')); @@ -1968,7 +1968,7 @@ window.Page_admin = (() => { } if (!antraege.length) { - el.innerHTML = `
+ el.innerHTML = `
Offene Anträge
${UI.icon('check-circle')} Keine offenen Anträge @@ -1983,11 +1983,11 @@ window.Page_admin = (() => {
${antraege.map(a => ` -
+
-
+
${_esc(a.name)} @@ -2004,7 +2004,7 @@ window.Page_admin = (() => { ${UI.icon('certificate')} VDH: ${a.vdh_mitglied ? 'ja' : 'nein'} - ${a.created_at ? `${UI.icon('clock')} ${new Date(a.created_at).toLocaleDateString('de-DE')}` : ''} + ${a.created_at ? `${UI.icon('clock')} ${new Date(a.created_at).toLocaleDateString('de-DE')}` : ''}
${a.beschreibung ? `
+ class="text-danger"> ${UI.icon('x')} Ablehnen
@@ -2051,7 +2051,7 @@ window.Page_admin = (() => { UI.modal.open({ title: `${UI.icon('file-text')} Hochgeladene Dokumente`, body: docs.length - ? `
+ ? `` - : `

Keine Dokumente hochgeladen.

`, + : `

Keine Dokumente hochgeladen.

`, }); }); }); @@ -2090,7 +2090,7 @@ window.Page_admin = (() => { UI.modal.open({ title: `${UI.icon('x-circle')} Antrag ablehnen: ${name}`, body: ` -
+

Bitte gib einen Ablehnungsgrund an. Dieser wird dem Antragsteller mitgeteilt.

@@ -2156,14 +2156,14 @@ window.Page_admin = (() => {
@@ -2245,14 +2245,14 @@ window.Page_admin = (() => {
${_esc(j.id)}
- @@ -2307,23 +2307,23 @@ window.Page_admin = (() => { -
+

Neuen Partner-Code erstellen

- -
+ +
- +
- +
- +
@@ -2341,11 +2341,11 @@ window.Page_admin = (() => {
-
+

Aktive Codes

${codes.length === 0 - ? `

Noch keine Partner-Codes angelegt.

` + ? `

Noch keine Partner-Codes angelegt.

` : `
${_esc(e.poi_name || `OSM #${e.osm_id}`)}${_esc(e.field)}${_esc(e.field)} ${_esc(e.old_value || '—')}${_esc(e.new_value || '—')}${_esc(e.einreicher_name || '?')}${_esc(e.new_value || '—')}${_esc(e.einreicher_name || '?')} ${_ageLabel(e.created_at)} -
${_esc(b.name)}
-
${_esc(b.email)}
+
${_esc(b.email)}
${_esc(b.zwingername || '—')} ${_esc(b.rasse_text || '—')} ${_esc(b.stadt || '—')} ${b.wuerfe_count || 0} Würfe
- ${b.hunde_count || 0} Hunde + ${b.hunde_count || 0} Hunde
${tierBadge(b.subscription_tier)} @@ -2172,7 +2172,7 @@ window.Page_admin = (() => { - ${j.next_run_time ? _formatDateTime(j.next_run_time) : ''} + ${j.next_run_time ? _formatDateTime(j.next_run_time) : ''} ${_esc(j.trigger)} +
@@ -2384,14 +2384,14 @@ window.Page_admin = (() => { -
+

Nutzer-Status manuell vergeben

- +
- + -
+
@@ -2631,8 +2631,8 @@ window.Page_admin = (() => { - - + + @@ -2728,15 +2728,15 @@ window.Page_admin = (() => { UI.modal.open({ title: isNew ? 'Neue Vorlage' : 'Vorlage bearbeiten', body: ` - -
+ +
- +
- +
- +
- +
@@ -2931,7 +2931,7 @@ window.Page_admin = (() => { list.innerHTML = rows.map(r => `
-
+
${_esc(r.name)} ${r.username ? `(@${_esc(r.username)})` : ''}
@@ -2977,7 +2977,7 @@ window.Page_admin = (() => { ? app.docs.map(d => ` 📎 ${_esc(d.filename)}`).join('') - : 'Keine Anhänge'; + : 'Keine Anhänge'; UI.modal.open({ title: `Bewerbung — ${_esc(app.name)}`, @@ -3030,7 +3030,7 @@ window.Page_admin = (() => { }; el.innerHTML = ` -
+

Hilfe / FAQ

@@ -3075,7 +3075,7 @@ window.Page_admin = (() => { font-size:var(--text-sm);box-sizing:border-box; resize:vertical;font-family:inherit">
-
+
@@ -3209,11 +3209,11 @@ window.Page_admin = (() => { style="width:70px;padding:var(--space-2) var(--space-3); 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)"> -
+
+ class="text-xs">Abbrechen + class="text-xs">Speichern
@@ -3366,7 +3366,7 @@ window.Page_admin = (() => { 'koerperpflege': 'Körperpflege', 'hundesport': 'Hundesport', 'welpe-basics': 'Welpe Basics', }; - let html = `
+ let html = `

Trainingsübungen bearbeiten

`; @@ -3574,7 +3574,7 @@ window.Page_admin = (() => { const _pendingCard = r => `
-
+
${_esc(r.name)}
${_esc(r.email)}
@@ -3582,7 +3582,7 @@ window.Page_admin = (() => { ${r.discount_pct > 0 ? ` ${r.discount_pct}% Rabatt` : ''} - ${r.created_at?.slice(0,10) || ''} + ${r.created_at?.slice(0,10) || ''}
${r.discount_reason === 'founder' ? `
Gründer — kostenfrei
` : ''} ${r.discount_reason === 'referred_by_founder' ? `
Von Gründer eingeladen
` : ''} @@ -3628,15 +3628,15 @@ window.Page_admin = (() => { const _doneRow = r => `
+ ${_esc(r.email)}`; el.innerHTML = ` -
-
+
+
Offene Anfragen (${pending.length})
${pending.length @@ -3768,7 +3768,7 @@ window.Page_admin = (() => { async function _load() { el.innerHTML = `
-
+
@@ -3847,7 +3847,7 @@ window.Page_admin = (() => { } if (inv.status === 'sent') { actions.push(``); } @@ -3856,7 +3856,7 @@ window.Page_admin = (() => { ${UI.icon('check-circle')} Bezahlt `); actions.push(``); } @@ -3873,7 +3873,7 @@ window.Page_admin = (() => {
- + @@ -3990,7 +3990,7 @@ window.Page_admin = (() => { UI.modal.open({ title: `${UI.icon('receipt')} ${isEdit ? (isLocked ? 'Rechnung ansehen' : 'Rechnung bearbeiten') : 'Neue Rechnung erstellen'}`, body: ` - + ${lockedBanner} @@ -4003,24 +4003,24 @@ window.Page_admin = (() => { ` : ''} -
+
- +
- +
-
- + `).join(''); @@ -4479,7 +4479,7 @@ window.Page_admin = (() => { -
+
Monatliche Übersicht
@@ -4488,8 +4488,8 @@ window.Page_admin = (() => {
- - + + @@ -4500,17 +4500,17 @@ window.Page_admin = (() => { -
+
${UI.icon('file-csv')} Quartalsbericht herunterladen
- +
- +
- + `; }).join(''); resultEl.innerHTML = ` @@ -4608,14 +4608,14 @@ window.Page_admin = (() => {
${accountBadge(l.from_account)}${_esc(l.recipient)}${accountBadge(l.from_account)}${_esc(l.recipient)} ${_esc(l.subject)} ${_esc(l.sent_by_name || '')} ${(l.sent_at||'').slice(0,16).replace('T',' ')}
${_esc(r.name)}
- ${_esc(r.email)}
${tierBadge(r.tier)} ✓ ${r.fulfilled_at?.slice(0,10) || ''}
${_esc(inv.recipient_name)}
-
${_esc(inv.recipient_email || '')}
+
${_esc(inv.recipient_email || '')}
${_fmtEur(inv.amount_gross)} @@ -3881,7 +3881,7 @@ window.Page_admin = (() => { ? `
erhalten: ${_fmtEur(inv.paid_amount)} ${inv.paid_amount < inv.amount_gross - ? `-${_fmtEur(inv.amount_gross - inv.paid_amount)}` + ? `-${_fmtEur(inv.amount_gross - inv.paid_amount)}` : ''}
` : ''} @@ -3904,7 +3904,7 @@ window.Page_admin = (() => {
Nummer EmpfängerBetragBetrag Status Erstellt
${_esc(m.month)}${m.count}${m.count} ${_fmtEur(m.revenue)}
MonatRechnungenUmsatzRechnungenUmsatz
${_esc(inv.invoice_number)} ${_esc(inv.recipient_name)} ${_fmtE(effectiveAmt)}${amtNote} ${sL[inv.status]||inv.status}${_fmtD(inv.created_at)}${_fmtD(inv.created_at)}
- + ${rows2} - diff --git a/backend/static/js/pages/adoption.js b/backend/static/js/pages/adoption.js index b20682e..4dfad9b 100644 --- a/backend/static/js/pages/adoption.js +++ b/backend/static/js/pages/adoption.js @@ -270,7 +270,7 @@ window.Page_adoption = (() => { content.innerHTML = `
🐾
-

Finde Hunde in deiner Nähe

+

Finde Hunde in deiner Nähe

Erlaube den Zugriff auf deinen Standort oder gib eine PLZ ein, um Tierheim-Hunde in deiner Umgebung zu finden. @@ -339,7 +339,7 @@ window.Page_adoption = (() => {

+ class="btn btn-secondary text-sm"> ${UI.icon('arrow-square-out')} Tierheimhelden.de — alle Hunde
@@ -434,7 +434,7 @@ window.Page_adoption = (() => {

${shelters.length} Tierheim${shelters.length !== 1 ? 'e' : ''} im Umkreis von ${_radius} km

-
+
${shelters.map(s => _shelterRow(s)).join('')}
@@ -444,12 +444,12 @@ window.Page_adoption = (() => { @@ -473,12 +473,12 @@ window.Page_adoption = (() => { font-size:1.2rem"> 🏠
-
+
${_esc(s.name)}
-
+
${_esc(s.plz)} ${_esc(s.stadt)}
@@ -520,7 +520,7 @@ window.Page_adoption = (() => { content.innerHTML = `
🐾
-

Noch keine Hunde zur Weitervermittlung

+

Noch keine Hunde zur Weitervermittlung

Hier können Halter Hunde privat zur Weitervermittlung anbieten — zum Beispiel bei Umzug, Krankheit oder Allergie. @@ -530,7 +530,7 @@ window.Page_adoption = (() => { ${UI.icon('plus')} Hund zur Vermittlung anbieten ` : ` -

+

Bitte anmelden, um ein Inserat zu erstellen.

`} @@ -556,8 +556,8 @@ window.Page_adoption = (() => { ${isLoggedIn && _myListings && _myListings.length ? `
-

Meine Inserate

-
+

Meine Inserate

+
${_myListings.map(l => _myListingRow(l)).join('')}
@@ -714,12 +714,12 @@ window.Page_adoption = (() => {
-
+
${_esc(l.name)}
-
+
${l.interesse_count || 0} Interessent${(l.interesse_count || 0) !== 1 ? 'en' : ''}
@@ -764,7 +764,7 @@ window.Page_adoption = (() => { // Interesse bekunden — Modal mit optionaler Nachricht const body = ` - +

Du kannst optional eine Nachricht an den Anbieter schicken.

@@ -816,9 +816,9 @@ window.Page_adoption = (() => { } const body = ` - +
- +
@@ -857,7 +857,7 @@ window.Page_adoption = (() => {
- +
Mindestens 80 Zeichen
@@ -876,7 +876,7 @@ window.Page_adoption = (() => { const footer = `
- diff --git a/backend/static/js/pages/breeder-editor.js b/backend/static/js/pages/breeder-editor.js new file mode 100644 index 0000000..e8ccf0b --- /dev/null +++ b/backend/static/js/pages/breeder-editor.js @@ -0,0 +1,322 @@ +/* ============================================================ + BAN YARO — Züchter-Profil-Editor + Selbstverwaltung des öffentlichen Züchter-Profils. + ============================================================ */ + +window.Page_breeder_editor = (() => { + + let _container = null; + let _data = null; // { profile, litters, storage_mb, storage_limit_mb } + + async function init(container) { + _container = container; + _container.innerHTML = `
${UI.skeleton(5)}
`; + await _load(); + } + + function refresh() { _load(); } + function onDogChange() {} + + async function _load() { + try { + _data = await API.get('/breeder/my-editor'); + _render(); + } catch (e) { + _container.innerHTML = `
${e.message}
`; + } + } + + function _render() { + const { profile: p, litters, storage_mb, storage_limit_mb } = _data; + _container.innerHTML = ` +
+
+

Mein Züchter-Profil

+

+ Gestalte deine öffentliche Profilseite — Fotos, Videos und Infos zu deinen Würfen. +

+
+ + +
+
Logo / Titelbild
+
+
+ ${p.logo_url + ? `` + : ``} +
+
+ +
+ Quadratisch · max. 5 MB · HEIC wird unterstützt +
+
+
+
+ + +
+
Profil-Texte
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
+ Profil-Fotos & Videos +
+
+
+ JPG, PNG, HEIC, MP4, MOV · max. 200 MB pro Datei +
+ ${_storageBar(storage_mb, storage_limit_mb)} +
+ ${_renderPhotoGrid(p.photos || [])} +
+ +
+ + + ${litters.length ? ` +
+
+ Aktuelle Würfe — Fotos & Videos +
+
+ ${litters.map(l => _renderLitterCard(l)).join('')} +
+
` : ''} + +
+ `; + _bindEvents(); + } + + function _renderPhotoGrid(photos) { + return photos.map((ph, i) => { + const isVid = ph.media_type === 'video' || (ph.url || '').endsWith('.mp4'); + return ` +
+ ${isVid + ? ` +
▶ Video
` + : ``} + ${ph.is_primary ? `
LOGO
` : ''} + + ${!ph.is_primary ? `` : ''} +
`; + }).join(''); + } + + function _renderLitterCard(l) { + const label = l.geburtsdatum + ? `Wurf vom ${new Date(l.geburtsdatum).toLocaleDateString('de-DE')}` + : `Wurf #${l.id}`; + const info = [ + l.welpen_gesamt ? `${l.welpen_gesamt} Welpen` : null, + `${l.foto_count} Medien`, + ].filter(Boolean).join(' · '); + return ` +
+
+
+
${_esc(label)}
+
${info}
+
+ +
+
`; + } + + function _storageBar(usedMb, limitMb) { + const pct = Math.min(100, Math.round((usedMb / limitMb) * 100)); + const color = pct > 85 ? '#dc2626' : pct > 60 ? '#f59e0b' : '#22c55e'; + return ` +
+
+
+
+ ${usedMb.toFixed(1)} / ${limitMb} MB +
`; + } + + function _bindEvents() { + const el = _container; + + // Logo hochladen + el.querySelector('#be-logo-input')?.addEventListener('change', async e => { + const file = e.target.files[0]; + if (!file) return; + const fd = new FormData(); + fd.append('file', file); + fd.append('entity_type', 'breeder'); + fd.append('entity_id', String(_data.profile.id)); + fd.append('is_primary', '1'); + fd.append('visibility', 'public'); + try { + await API.breederPhotos.upload(fd); + UI.toast.success('Logo gespeichert.'); + await _load(); + } catch (err) { UI.toast.error(err.message); } + }); + + // Profil-Texte speichern + el.querySelector('#be-text-form')?.addEventListener('submit', async e => { + e.preventDefault(); + const btn = e.target.querySelector('[type="submit"]'); + const fd = UI.formData(e.target); + await UI.asyncButton(btn, async () => { + await API.put('/breeder/profile', fd); + _data.profile = { ..._data.profile, ...fd }; + UI.toast.success('Profil gespeichert.'); + }); + }); + + // Profil-Foto/-Video hochladen + el.querySelector('#be-profile-photo-input')?.addEventListener('change', async e => { + const file = e.target.files[0]; + if (!file) return; + const isVideo = file.type.startsWith('video/'); + if (isVideo) UI.toast.info('Video wird komprimiert – das kann 1–2 Minuten dauern …', 120_000); + const fd = new FormData(); + fd.append('file', file); + fd.append('entity_type', 'breeder'); + fd.append('entity_id', String(_data.profile.id)); + fd.append('visibility', 'public'); + try { + await API.breederPhotos.upload(fd); + UI.toast.success(isVideo ? 'Video hinzugefügt.' : 'Foto hinzugefügt.'); + await _load(); + } catch (err) { UI.toast.error(err.message); } + }); + + // Foto löschen + el.querySelectorAll('.be-photo-del').forEach(btn => { + btn.addEventListener('click', async () => { + if (!confirm('Löschen?')) return; + try { + await API.breederPhotos.remove(parseInt(btn.dataset.id)); + await _load(); + } catch (err) { UI.toast.error(err.message); } + }); + }); + + // Als Logo setzen + el.querySelectorAll('.be-photo-primary').forEach(btn => { + btn.addEventListener('click', async () => { + try { + await API.patch(`/breeder/photos/${btn.dataset.id}/primary`, {}); + await _load(); + } catch (err) { UI.toast.error(err.message); } + }); + }); + + // Wurf-Upload + el.querySelectorAll('.be-litter-input').forEach(input => { + input.addEventListener('change', async e => { + const file = e.target.files[0]; + if (!file) return; + const isVideo = file.type.startsWith('video/'); + const litterId = input.dataset.litterId; + const label = input.dataset.label; + if (isVideo) UI.toast.info('Video wird komprimiert – das kann 1–2 Minuten dauern …', 120_000); + const fd = new FormData(); + fd.append('file', file); + fd.append('entity_type', 'litter'); + fd.append('entity_id', litterId); + fd.append('visibility', 'public'); + try { + await API.breederPhotos.upload(fd); + UI.toast.success(`${isVideo ? 'Video' : 'Foto'} zu „${label}" hinzugefügt.`); + // Foto-Count aktualisieren + const litter = _data.litters.find(l => String(l.id) === String(litterId)); + if (litter) litter.foto_count++; + _render(); + } catch (err) { UI.toast.error(err.message); } + }); + }); + } + + function _esc(s) { + return String(s ?? '').replace(/[&<>"']/g, c => + ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + } + + return { init, refresh, onDogChange }; + +})(); diff --git a/backend/static/js/pages/breeder.js b/backend/static/js/pages/breeder.js index 58edb3d..2770ce8 100644 --- a/backend/static/js/pages/breeder.js +++ b/backend/static/js/pages/breeder.js @@ -75,7 +75,7 @@ window.Page_breeder = (() => { padding:var(--space-6) var(--space-4) var(--space-8);color:white;position:relative">
-
+

${UI.icon('seal-check')} Verifizierter Züchter

@@ -157,7 +157,7 @@ window.Page_breeder = (() => { display:flex;align-items:center;gap:var(--space-2)"> ${UI.icon('baby')} Aktuelle Würfe -
+
${p.wuerfe.map(w => _wurfCard(w)).join('')}
` : ''} @@ -201,7 +201,7 @@ window.Page_breeder = (() => { ${p.fotos?.length ? ` -
+

${UI.icon('images')} Galerie @@ -226,7 +226,7 @@ window.Page_breeder = (() => {

` : ''} - +
`; @@ -262,7 +262,7 @@ window.Page_breeder = (() => { ).join(''); const genBadge = h.gentests_total > 0 - ? ` + ? ` ${h.gentests_clear}/${h.gentests_total} Gentests frei ` : ''; @@ -271,7 +271,7 @@ window.Page_breeder = (() => {
- ${gIcon} + ${gIcon} ${_esc(h.name)} ${h.rufname ? `"${_esc(h.rufname)}"` : ''} ${alter !== null ? `${alter} J.` : ''} @@ -345,7 +345,7 @@ window.Page_breeder = (() => { ${stats.map(r => `
${_esc(r.ergebnis || '—')} - ${r.cnt}× + ${r.cnt}×
`).join('')} @@ -377,7 +377,7 @@ window.Page_breeder = (() => { const photos = await API.breederPhotos.list('breeder', breederId); if (!photos?.length) return; section.innerHTML = ` -
+

${UI.icon('images')} Fotos

diff --git a/backend/static/js/pages/chat.js b/backend/static/js/pages/chat.js index f5d7b63..8a29659 100644 --- a/backend/static/js/pages/chat.js +++ b/backend/static/js/pages/chat.js @@ -178,7 +178,7 @@ window.Page_chat = (() => { `}
?
- +
@@ -188,7 +188,7 @@ window.Page_chat = (() => {
-
- +
@@ -295,7 +295,7 @@ window.Page_diary = (() => { `; card.innerHTML = `
🐾
-
+
@@ -963,7 +963,7 @@ window.Page_diary = (() => { // Hunde-Chips (bei mehreren Hunden) const dogsHtml = dogIds.length > 1 - ? `
+ ? `
${dogIds.map(did => { const dog = _appState.dogs.find(d => d.id === did); return dog ? `
@@ -1279,7 +1279,7 @@ window.Page_diary = (() => { value="${entry?.datum || today}" required>
- +
@@ -1293,10 +1293,10 @@ window.Page_diary = (() => {
- + - +
- +
@@ -1318,7 +1318,7 @@ window.Page_diary = (() => {
-
+
@@ -1341,7 +1341,7 @@ window.Page_diary = (() => { ${dogPickerHtml}
+ ${entry?.is_milestone ? 'checked' : ''} class="hidden"> -
+
${isEdit ? `` : ''}
@@ -1843,32 +1843,32 @@ window.Page_diary = (() => { ${UI.escape(_appState.activeDog?.name || 'deinem Hund')}.

-
+
-
+
@@ -1917,7 +1917,7 @@ window.Page_diary = (() => { : await API.importData.csv(dogId, file); const errHtml = res.errors?.length - ? `
${res.errors.length} Fehler anzeigen + ? `
${res.errors.length} Fehler anzeigen
${UI.escape(res.errors.join('\n'))}
` : ''; @@ -1925,7 +1925,7 @@ window.Page_diary = (() => {
${res.imported} Einträge importiert - ${res.skipped ? ` · ${res.skipped} übersprungen` : ''} + ${res.skipped ? ` · ${res.skipped} übersprungen` : ''} ${errHtml}
`; resultEl.style.display = 'block'; diff --git a/backend/static/js/pages/dog-profile.js b/backend/static/js/pages/dog-profile.js index 2679ed3..e38c765 100644 --- a/backend/static/js/pages/dog-profile.js +++ b/backend/static/js/pages/dog-profile.js @@ -101,22 +101,22 @@ window.Page_dog_profile = (() => { : `

`} -
+
${geburtstag ? ` -
+
Geburtstag
${geburtstag}
-
+
${_calcAlter(dog.geburtstag)}
` : ''} ${dog.geschlecht ? ` -
+
${dog.geschlecht === 'm' ? '' : ''} Geschlecht
${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'} @@ -130,19 +130,19 @@ window.Page_dog_profile = (() => {
` : ''} ${dog.widerrist_cm ? ` -
+
Widerrist
${dog.widerrist_cm} cm
` : ''} -
+
Transponder
${dog.chip_nr ? `
${_esc(dog.chip_nr)}
` - : `
nicht eingetragen + : `
nicht eingetragen
` @@ -230,12 +230,12 @@ window.Page_dog_profile = (() => {
Sitter-Zugang
-
+
Gib einem Freund temporären Schreibzugang für diesen Hund. Deine bestehenden Daten und Medien bleiben unsichtbar und privat — der Sitter kann nur neue Einträge anlegen.
-
Lade…
+
Lade…
` : ''} `; @@ -340,7 +340,7 @@ window.Page_dog_profile = (() => { }; const sitztBlock = sitzt.length ? ` -
+
Sitzt
@@ -360,7 +360,7 @@ window.Page_dog_profile = (() => {
` : ''; el.innerHTML = ` -
+
+
🛁 @@ -457,7 +457,7 @@ window.Page_dog_profile = (() => { const katTipps = data.tipps.filter(t=>t.kategorie===kat); const katBadge = kat === 'Fell' ? pflegeArtBadge : ''; return ` -
+
${kat_icons[kat]||_ph('paw-print')} ${_esc(kat)}${katBadge}
@@ -528,7 +528,7 @@ window.Page_dog_profile = (() => {
${_esc(s.sitter_name)} - · bis ${_esc(s.valid_until)} + · bis ${_esc(s.valid_until)}
@@ -621,7 +621,7 @@ window.Page_dog_profile = (() => {
`, footer: `
- +
`, }); @@ -666,20 +666,20 @@ window.Page_dog_profile = (() => {
+ class="w-full">
` : ''}
`; const footer = `
- ${hasPhoto ? `` : ''} -
+ ${hasPhoto ? `` : ''} +
${hasPhoto ? `` : ''}
@@ -860,7 +860,7 @@ window.Page_dog_profile = (() => {
-
+
${ownerName ? `
Besitzer
${_esc(ownerName)}
` : ''}
banyaro.app
@@ -878,7 +878,7 @@ window.Page_dog_profile = (() => { UI.modal.open({ title: 'Visitenkarte', body: ` -
${cardHtml}
+
${cardHtml}

QR-Code auf NFC-Tag oder Anhänger kleben — jeder kann das Profil von ${_esc(dog.name)} sofort öffnen.

@@ -952,7 +952,7 @@ window.Page_dog_profile = (() => {
+ class="text-xs"> @@ -961,7 +961,7 @@ window.Page_dog_profile = (() => { Dieser Link kann einmalig angenommen werden.

-
`, +
`, footer: ` `, @@ -1010,7 +1010,7 @@ window.Page_dog_profile = (() => {
${s.shared_with_name ? `${_esc(s.shared_with_name)} · ${s.role}` - : `Ausstehend · ${s.role}`} + : `Ausstehend · ${s.role}`}
+
`, @@ -1073,8 +1073,8 @@ window.Page_dog_profile = (() => { body: _formHTML(dog, true), footer: `
- -
+ +
-
+
{
-
+
{
-
+
${tests.map(t => ` - + `).join('')} @@ -572,8 +572,8 @@ window.Page_laeufi = (() => { UI.modal.open({ title: 'Progesterontest eintragen', body: ` - -
+ +
@@ -586,7 +586,7 @@ window.Page_laeufi = (() => {
-
+
diff --git a/backend/static/js/pages/litters.js b/backend/static/js/pages/litters.js index f6f1883..fd56fcf 100644 --- a/backend/static/js/pages/litters.js +++ b/backend/static/js/pages/litters.js @@ -118,7 +118,7 @@ window.Page_litters = (() => { padding:var(--space-3) var(--space-4); display:flex;align-items:center;gap:var(--space-3)"> ${logoHtml} -
+

${_esc(zwinger)}

@@ -126,7 +126,7 @@ window.Page_litters = (() => { - Privater Bereich · Nur du siehst das + Privater Bereich · Nur du siehst das
`; @@ -232,7 +232,7 @@ window.Page_litters = (() => { el.innerHTML = `
-

Keine Würfe für diesen Filter.

+

Keine Würfe für diesen Filter.

`; return; } @@ -248,8 +248,8 @@ window.Page_litters = (() => { el.innerHTML = `
${UI.icon('dog')}
-

Noch keine Würfe angelegt.

-
`; @@ -325,10 +325,10 @@ window.Page_litters = (() => { const label = l.geburt_datum ? `Geburt ${_fmtDate(l.geburt_datum)}` : `Erwartet ${_fmtDate(l.erwartetes_datum)}`; let countdownHtml = ''; if (days !== null && !l.geburt_datum) { - const c = days < 0 ? `überfällig` - : days === 0 ? `heute!` + const c = days < 0 ? `überfällig` + : days === 0 ? `heute!` : days <= 7 ? `${days}d` - : `${days}d`; + : `${days}d`; countdownHtml = ` · ${c}`; } datumChip = `${UI.icon('calendar-dots')} ${label}${countdownHtml}`; @@ -359,7 +359,7 @@ window.Page_litters = (() => { ${l.wurf_name ? `${_esc(l.wurf_name)}` : ''}
` : ''}
- ${elternLabel} + ${elternLabel} ${_statusBadge(l.status)} ${sichtbarChip}
@@ -390,7 +390,7 @@ window.Page_litters = (() => { ${UI.icon('pencil-simple')}
@@ -401,10 +401,10 @@ window.Page_litters = (() => { @@ -412,10 +412,10 @@ window.Page_litters = (() => { @@ -461,7 +461,7 @@ window.Page_litters = (() => { function _renderPuppies(container, litterId, puppies) { if (!puppies.length) { - container.innerHTML = `

Noch keine Welpen eingetragen.

`; + container.innerHTML = `

Noch keine Welpen eingetragen.

`; return; } @@ -469,10 +469,10 @@ window.Page_litters = (() => {
${_genderIcon(p.geschlecht)} - ${p.name ? _esc(p.name) : 'Unbenannt'} + ${p.name ? _esc(p.name) : 'Unbenannt'} ${p.farbe ? `${_esc(p.farbe)}` : ''} ${_puppyStatusBadge(p.status)} - +
- +
`).join('')}
`; @@ -820,12 +820,12 @@ window.Page_litters = (() => { UI.modal.open({ title: isEdit ? 'Interessent bearbeiten' : 'Interessent eintragen', body: ` - +
-
+
@@ -835,7 +835,7 @@ window.Page_litters = (() => {
-
+
-
+
+ @@ -953,7 +953,7 @@ window.Page_litters = (() => {
-
+
${buildSelect('vater_name', 'vater_id', maennlich, v.vater_id, v.vater_name, 'Aus Zuchtkartei')} @@ -979,7 +979,7 @@ window.Page_litters = (() => {
-
+
{
- +
- +
@@ -1028,7 +1028,7 @@ window.Page_litters = (() => {
- +
@@ -1134,9 +1134,9 @@ window.Page_litters = (() => { const body = ` -
+
- +
@@ -1165,7 +1165,7 @@ window.Page_litters = (() => {
-
+
{
- +
@@ -1249,22 +1249,22 @@ window.Page_litters = (() => { const body = `
- +
- +
- +
- +
@@ -1317,11 +1317,11 @@ window.Page_litters = (() => { const visOrder = ['public', 'inquiry', 'private']; const body = ` -
-

Lädt…

+
+

Lädt…


- + @@ -1348,7 +1348,7 @@ window.Page_litters = (() => { try { const photos = await API.breederPhotos.list(entityType, entityId); if (!photos.length) { - el.innerHTML = `

Noch keine Fotos vorhanden.

`; + el.innerHTML = `

Noch keine Fotos vorhanden.

`; return; } el.innerHTML = ` @@ -1464,13 +1464,13 @@ window.Page_litters = (() => { const issueHTML = (welfare.issues || []).map(i => `
${UI.icon('warning')} - ${_esc(i.text)} + ${_esc(i.text)}
`).join(''); const okHTML = (welfare.ok_points || []).map(p => `
${UI.icon('check')} - ${_esc(p)} + ${_esc(p)}
`).join(''); const isProblematic = welfare.level === 'warning' || welfare.level === 'critical'; @@ -1500,7 +1500,7 @@ window.Page_litters = (() => { Trotzdem fortfahren
` : ` - `, }); @@ -1540,7 +1540,7 @@ window.Page_litters = (() => { } catch (err) { UI.modal.open({ title: `${UI.icon('sparkle')} KI-Wurfankündigung`, - body: `

${_esc(err.message || 'Fehler beim Generieren.')}

`, + body: `

${_esc(err.message || 'Fehler beim Generieren.')}

`, footer: ``, }); return; diff --git a/backend/static/js/pages/lost.js b/backend/static/js/pages/lost.js index db23517..ad6de69 100644 --- a/backend/static/js/pages/lost.js +++ b/backend/static/js/pages/lost.js @@ -413,7 +413,7 @@ window.Page_lost = (() => { border-radius:var(--radius-md);flex-shrink:0; display:flex;align-items:center;justify-content:center; font-size:2rem">🐕
`} -
+
@@ -436,7 +436,7 @@ window.Page_lost = (() => { color:var(--c-text)"> ${_escape(r.beschreibung.slice(0, 120))}${r.beschreibung.length > 120 ? '…' : ''}

-
+
Gemeldet ${_fmtDate(r.created_at)} ${r.melder_name ? '· ' + _escape(r.melder_name.split(' ')[0]) : ''}
@@ -450,7 +450,7 @@ window.Page_lost = (() => { 🗑 Verwerfen
` - : (_appState.user ? `
+ : (_appState.user ? `
+ data-id="${f.id}" class="flex-1"> Freigeben + data-id="${f.id}" class="text-danger"> Ablehnen
`).join('')} @@ -287,7 +287,7 @@ window.Page_moderation = (() => { el.innerHTML = `
${total} Nutzer gefunden
-
+
${visible.map(u => { const isAdminUser = u.rolle === 'admin' || u.is_admin; const canAction = isAdmin && !isAdminUser; @@ -301,7 +301,7 @@ window.Page_moderation = (() => { font-weight:var(--weight-bold);color:var(--c-text-secondary)"> ${_esc(u.name[0].toUpperCase())}
-
+
${_esc(u.name)} @@ -309,7 +309,7 @@ window.Page_moderation = (() => { border-radius:3px;background:var(--c-danger); color:#fff;margin-left:4px">GESPERRT` : ''}
-
+
${_esc(u.email)} · + title="Sperre aufheben" class="text-success"> ${UI.icon('lock-open')} ` : ``) : '' @@ -400,12 +400,12 @@ window.Page_moderation = (() => { return; } el.innerHTML = ` -
+
${reports.map(r => `
-
+
${_esc(r.target_type)} #${r.target_id} · @@ -476,13 +476,13 @@ window.Page_moderation = (() => { const STATUS_COLOR = { pending: 'var(--c-warning)', approved: 'var(--c-success,#22c55e)', rejected: 'var(--c-danger)' }; el.innerHTML = ` -
+
${edits.map(e => ` -
+
${_esc(e.poi_name)}
-
+
OSM-ID: ${_esc(e.osm_id)} · Feld: ${_esc(e.field)} · von ${_esc(e.einreicher_name)} · ${new Date(e.created_at).toLocaleDateString('de-DE')}
@@ -494,7 +494,7 @@ window.Page_moderation = (() => {
Aktuell
-
${_esc(e.old_value) || 'leer'}
+
${_esc(e.old_value) || 'leer'}
Vorschlag
diff --git a/backend/static/js/pages/movies.js b/backend/static/js/pages/movies.js index a36cf73..b57c0b6 100644 --- a/backend/static/js/pages/movies.js +++ b/backend/static/js/pages/movies.js @@ -96,7 +96,7 @@ window.Page_movies = (() => {
-
+
@@ -201,8 +201,8 @@ window.Page_movies = (() => { const stars = _starsHtml(film.bewertung_avg, film.id, film.user_rating, false); const _ico = name => ``; const typLabel = film.typ === 'serie' ? `${_ico('list')} Serie` : film.typ === 'doku' ? `${_ico('camera')} Doku` : ''; - const imdb = film.imdb_rating ? `IMDb ${film.imdb_rating}` : ''; - const streaming = film.streaming ? `${_esc(film.streaming)}` : ''; + const imdb = film.imdb_rating ? `IMDb ${film.imdb_rating}` : ''; + const streaming = film.streaming ? `${_esc(film.streaming)}` : ''; return `
@@ -210,7 +210,7 @@ window.Page_movies = (() => {
${_esc(film.titel)} (${film.jahr})
- ${_esc(film.genre)}${typLabel ? `${typLabel}` : ''} + ${_esc(film.genre)}${typLabel ? `${typLabel}` : ''}
${_esc(film.hund_rasse)}
${tag} @@ -240,7 +240,7 @@ window.Page_movies = (() => {
${bannerText}

${_esc(film.beschreibung)}

-
+
Community-Bewertung:
diff --git a/backend/static/js/pages/notes.js b/backend/static/js/pages/notes.js index d78797d..10bdfa3 100644 --- a/backend/static/js/pages/notes.js +++ b/backend/static/js/pages/notes.js @@ -499,7 +499,7 @@ window.Page_notes = (() => {

Neue Notiz

-
+
${ERSTELL_RUBRIKEN.map(r => ` @@ -514,7 +514,7 @@ window.Page_notes = (() => {
-
+
-
- - +
+ +
`; }; @@ -627,7 +627,7 @@ window.Page_notes = (() => {
-
+
${[1,2,3,4,5].map(n => ` -
@@ -222,7 +222,7 @@ window.Page_onboarding = (() => { Foto auswählen + accept="image/*" class="hidden">
@@ -234,13 +234,13 @@ window.Page_onboarding = (() => {
-
@@ -255,7 +255,7 @@ window.Page_onboarding = (() => { function _step3() { const dogName = _appState.activeDog?.name; return ` -
+
@@ -294,13 +294,13 @@ window.Page_onboarding = (() => {

-
- ${dogName ? ` - diff --git a/backend/static/js/pages/partner-profil.js b/backend/static/js/pages/partner-profil.js new file mode 100644 index 0000000..687b859 --- /dev/null +++ b/backend/static/js/pages/partner-profil.js @@ -0,0 +1,278 @@ +/* ============================================================ + BAN YARO — Partner-Profil-Editor + Nur für User mit is_partner=1. + ============================================================ */ + +window.Page_partner_profil = (() => { + + let _container = null; + let _profile = null; + + async function init(container, appState) { + _container = container; + _render(); + await _load(); + } + + function refresh() { _load(); } + function onDogChange() {} + + function _render() { + _container.innerHTML = ` +
+
+

+ Mein Partner-Profil +

+

+ Richte deine öffentliche Präsenz auf der Partner-Seite ein. + Nach dem Absenden prüfen wir dein Profil und schalten es frei. +

+
+
+
Lade…
+
+
+ `; + } + + async function _load() { + const el = _container.querySelector('#pp-content'); + try { + const d = await API.get('/partner/my-profile'); + _profile = d.profile || {}; + _profile._storage_mb = d.storage_mb || 0; + _profile._storage_limit_mb = d.storage_limit_mb || 200; + el.innerHTML = _renderEditor(); + _bindEvents(el); + } catch (e) { + el.innerHTML = `

${e.message}

`; + } + } + + function _statusBadge() { + if (!_profile?.submitted_at && !_profile?.approved) return ''; + const a = _profile.approved; + if (a === 1) return `✓ Freigegeben`; + if (a === -1) return `✗ Abgelehnt`; + if (_profile.submitted_at) return `⏳ In Prüfung`; + return `Entwurf`; + } + + function _renderEditor() { + const p = _profile || {}; + const photos = p.photos || []; + return ` + +
+ Status: + ${_statusBadge() || 'Noch kein Profil angelegt'} +
+ + +
+
Logo
+
+
+ ${p.logo_url + ? `` + : ``} +
+
+ +
+ PNG, JPG oder WebP · max. 5 MB · wird quadratisch zugeschnitten +
+
+
+
+ + +
+
Texte
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
+
+ Fotos & Videos (max. 6) +
+
+
+ JPG, PNG, HEIC, MP4, MOV · max. 200 MB pro Datei +
+
+ ${_storageBar(p._storage_mb || 0, p._storage_limit_mb || 200)} +
+
+ ${photos.map((url, i) => { + const isVid = url.endsWith('.mp4') || url.endsWith('.webm'); + return ` +
+ ${isVid + ? ` +
▶ Video
` + : ``} + +
`; + }).join('')} + ${photos.length < 6 ? ` + ` : ''} +
+
+ + +
+ +
+ `; + } + + function _bindEvents(el) { + // Logo hochladen + el.querySelector('#pp-logo-input')?.addEventListener('change', async e => { + const file = e.target.files[0]; + if (!file) return; + const fd = new FormData(); + fd.append('file', file); + try { + const r = await API.upload('/partner/my-profile/logo', fd); + el.querySelector('#pp-logo-preview').innerHTML = + ``; + _profile = { ..._profile, logo_url: r.logo_url }; + UI.toast.success('Logo gespeichert.'); + } catch (err) { UI.toast.error(err.message); } + }); + + // Texte speichern + el.querySelector('#pp-text-form')?.addEventListener('submit', async e => { + e.preventDefault(); + const btn = e.target.querySelector('[type="submit"]'); + const fd = UI.formData(e.target); + await UI.asyncButton(btn, async () => { + await API.put('/partner/my-profile', fd); + _profile = { ..._profile, ...fd }; + UI.toast.success('Gespeichert.'); + }); + }); + + // Foto/Video hochladen + el.querySelector('#pp-photo-input')?.addEventListener('change', async e => { + const file = e.target.files[0]; + if (!file) return; + const isVideo = file.type.startsWith('video/'); + const fd = new FormData(); + fd.append('file', file); + if (isVideo) UI.toast.info('Video wird hochgeladen und komprimiert – das kann 1–2 Minuten dauern …', 120_000); + try { + const r = await API.upload('/partner/my-profile/photos', fd); + _profile = { ..._profile, photos: r.photos }; + await _load(); + UI.toast.success(isVideo ? 'Video hinzugefügt.' : 'Foto hinzugefügt.'); + } catch (err) { UI.toast.error(err.message); } + }); + + // Foto löschen + el.querySelectorAll('.pp-photo-del').forEach(btn => { + btn.addEventListener('click', async () => { + const idx = parseInt(btn.dataset.idx); + try { + const r = await API.post(`/partner/my-profile/photos/${idx}/delete`, {}); + _profile = { ..._profile, photos: r.photos }; + await _load(); + } catch (err) { UI.toast.error(err.message); } + }); + }); + + // Einreichen + el.querySelector('#pp-submit-btn')?.addEventListener('click', async () => { + const btn = el.querySelector('#pp-submit-btn'); + await UI.asyncButton(btn, async () => { + await API.post('/partner/my-profile/submit', {}); + UI.toast.success('Eingereicht! Wir prüfen dein Profil und schalten es bald frei.'); + await _load(); + }); + }); + } + + function _storageBar(usedMb, limitMb) { + const pct = Math.min(100, Math.round((usedMb / limitMb) * 100)); + const color = pct > 85 ? '#dc2626' : pct > 60 ? '#f59e0b' : '#22c55e'; + return ` +
+
+
+
+ + ${usedMb.toFixed(1)} / ${limitMb} MB + +
`; + } + + function _esc(s) { + return String(s || '').replace(/[&<>"']/g, c => + ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + } + + return { init, refresh, onDogChange }; + +})(); diff --git a/backend/static/js/pages/partner.js b/backend/static/js/pages/partner.js new file mode 100644 index 0000000..884d4b2 --- /dev/null +++ b/backend/static/js/pages/partner.js @@ -0,0 +1,153 @@ +/* ============================================================ + BAN YARO — Partner-Seite + Showcase der offiziellen Ban Yaro Partner. + ============================================================ */ + +window.Page_partner = (() => { + + let _container = null; + + async function init(container) { + _container = container; + _render(); + _load(); + } + + function refresh() { _load(); } + function onDogChange() {} + + function _render() { + _container.innerHTML = ` +
+
+
🤝
+

+ Unsere Partner +

+

+ Diese Menschen glauben an Ban Yaro — und helfen uns, die Community zu wachsen. + Über ihre persönlichen Einladungscodes können sie neue Gründer vermitteln. +

+
+
+
Lade…
+
+
+ `; + } + + async function _load() { + const el = _container.querySelector('#partner-content'); + try { + const d = await API.get('/partners/public'); + if (!d?.partners) throw new Error('Keine Daten.'); + el.innerHTML = _renderPartners(d.partners); + } catch (e) { + el.innerHTML = `

${e.message || 'Fehler beim Laden.'}

`; + } + } + + function _renderPartners(partners) { + if (!partners.length) { + return ` +
+

+ Noch keine Partner — das könnte schon bald du sein. +

+
+ ${_cta()} + `; + } + + const COLORS = [ + 'linear-gradient(135deg,#7c3aed,#a855f7)', + 'linear-gradient(135deg,#2563eb,#3b82f6)', + 'linear-gradient(135deg,#059669,#10b981)', + 'linear-gradient(135deg,#d97706,#f59e0b)', + 'linear-gradient(135deg,#db2777,#ec4899)', + ]; + + return ` +
+ ${partners.map((p, i) => { + const initial = (p.name || '?')[0].toUpperCase(); + const grad = COLORS[i % COLORS.length]; + return ` +
+
+
+ ${p.logo_url + ? `` + : p.avatar_url + ? `` + : `
+ ${initial} +
` + } +
+
${_esc(p.display_name || p.name)}
+ ${p.tagline ? `
${_esc(p.tagline)}
` : ''} +
+ ${p.website ? ` + 🌐 ${_esc(p.website.replace(/^https?:\/\//, ''))}` : ''} + ${p.instagram ? `📸 ${_esc(p.instagram)}` : ''} +
+
+
+ ${p.pp_bio || p.bio ? `

+ ${_esc(p.pp_bio || p.bio)} +

` : ''} + ${p.photos?.length ? ` +
+ ${p.photos.slice(0,3).map(url => { + const isVid = url.endsWith('.mp4') || url.endsWith('.webm'); + return isVid + ? `` + : ``; + }).join('')} +
` : ''} +
+ `; + }).join('')} +
+ ${_cta()} + `; + } + + function _cta() { + return ` +
+
+ Du möchtest Partner werden? +
+

+ Schreib uns — wir richten deinen persönlichen Einladungscode ein. +

+ + 📧 partner@banyaro.app + +
+ `; + } + + function _esc(s) { + return String(s || '').replace(/[&<>"']/g, c => + ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + } + + return { init, refresh, onDogChange }; + +})(); diff --git a/backend/static/js/pages/personality.js b/backend/static/js/pages/personality.js index e1f159b..6a3bbbc 100644 --- a/backend/static/js/pages/personality.js +++ b/backend/static/js/pages/personality.js @@ -237,7 +237,7 @@ window.Page_personality = (() => {
- + Frage ${_current + 1} von ${FRAGEN.length} ${pct}% @@ -344,7 +344,7 @@ window.Page_personality = (() => { return `
${tp.emoji} -
+
@@ -414,7 +414,7 @@ window.Page_personality = (() => {
Dein Profil
-
${scoreBars}
+
${scoreBars}
diff --git a/backend/static/js/pages/places.js b/backend/static/js/pages/places.js index f8b662b..767f708 100644 --- a/backend/static/js/pages/places.js +++ b/backend/static/js/pages/places.js @@ -281,8 +281,8 @@ window.Page_places = (() => {
${place.adresse ? `

${UI.icon('map-pin')} ${UI.escape(place.adresse)}

` : ''} - ${place.telefon ? `

${UI.icon('phone')} ${UI.escape(place.telefon)}

` : ''} - ${place.website ? `

${UI.icon('arrow-square-out')} ${UI.escape(place.website)}

` : ''} + ${place.telefon ? `

${UI.icon('phone')} ${UI.escape(place.telefon)}

` : ''} + ${place.website ? `

${UI.icon('arrow-square-out')} ${UI.escape(place.website)}

` : ''} ${flags.length ? `
${flags.map(f => `${f}`).join('')}
` : ''}

@@ -291,7 +291,7 @@ window.Page_places = (() => { `; const footer = isOwn ? ` - + ` : ` @@ -348,24 +348,24 @@ window.Page_places = (() => {

- +
- +
- +
-
+
@@ -463,7 +463,7 @@ window.Page_zucht_profil = (() => { ${t.verliehen_am ? `${UI.icon('calendar-dots')} ${_fmtDate(t.verliehen_am)}` : ''} ${t.ort ? ` ·  ${UI.icon('map-pin')} ${_esc(t.ort)}` : ''} ${t.richter ? ` ·  ${UI.icon('user')} ${_esc(t.richter)}` : ''} - ${t.ausstellung ? `
${UI.icon('ticket')} ${_esc(t.ausstellung)}` : ''} + ${t.ausstellung ? `
${UI.icon('ticket')} ${_esc(t.ausstellung)}` : ''} `).join(''); diff --git a/backend/static/js/pages/zuchthunde.js b/backend/static/js/pages/zuchthunde.js index 9798b02..cb7bbb8 100644 --- a/backend/static/js/pages/zuchthunde.js +++ b/backend/static/js/pages/zuchthunde.js @@ -120,7 +120,7 @@ window.Page_zuchthunde = (() => { padding:var(--space-3) var(--space-4); display:flex;align-items:center;gap:var(--space-3)"> ${logoHtml} -
+

${_esc(zwinger)}

@@ -128,7 +128,7 @@ window.Page_zuchthunde = (() => { - Privater Bereich · Nur du siehst das + Privater Bereich · Nur du siehst das
`; @@ -232,8 +232,8 @@ window.Page_zuchthunde = (() => { : `
${UI.icon('dog')}
-

Noch keine Hunde angelegt.

-
`; @@ -299,7 +299,7 @@ window.Page_zuchthunde = (() => { // Hund-Card HTML // ---------------------------------------------------------- function _hundCardHTML(h) { - const nameLabel = h.name ? _esc(h.name) : 'Unbenannt'; + const nameLabel = h.name ? _esc(h.name) : 'Unbenannt'; const rufname = h.rufname ? ` (${_esc(h.rufname)})` : ''; const geburtstag = h.geburtsdatum ? _fmtDate(h.geburtsdatum) : null; @@ -314,7 +314,7 @@ window.Page_zuchthunde = (() => { return `
-
+
${_genderIcon(h.geschlecht)} ${nameLabel}${_esc(rufname)} @@ -326,7 +326,7 @@ window.Page_zuchthunde = (() => { ${h.chip_nr ? `${UI.icon('barcode')} ${_esc(h.chip_nr)}  ` : ''} ${h.zuchtbuchnummer ? `${UI.icon('book-open')} ${_esc(h.zuchtbuchnummer)}  ` : ''}
- ${eltern ? `
${eltern}
` : ''} + ${eltern ? `
${eltern}
` : ''}
@@ -364,7 +364,7 @@ window.Page_zuchthunde = (() => {
- +
`; } @@ -432,9 +432,9 @@ window.Page_zuchthunde = (() => { ${t.labor ? `${_esc(t.labor)}` : ''}
+ class="text-danger">${UI.icon('trash')} `).join('') - : `

Noch keine Gesundheitstests eingetragen.

`; + : `

Noch keine Gesundheitstests eingetragen.

`; wrap.innerHTML = `
@@ -495,9 +495,9 @@ window.Page_zuchthunde = (() => { ${t.labor ? `${_esc(t.labor)}` : ''}
+ class="text-danger">${UI.icon('trash')} `).join('') - : `

Noch keine Gentests eingetragen.

`; + : `

Noch keine Gentests eingetragen.

`; wrap.innerHTML = `
@@ -560,9 +560,9 @@ window.Page_zuchthunde = (() => { ${t.formwert ? `${_esc(t.formwert)}` : ''}
+ class="text-danger">${UI.icon('trash')} `).join('') - : `

Noch keine Titel eingetragen.

`; + : `

Noch keine Titel eingetragen.

`; wrap.innerHTML = `
@@ -626,9 +626,9 @@ window.Page_zuchthunde = (() => { const body = `
-
+
- +
@@ -639,7 +639,7 @@ window.Page_zuchthunde = (() => {
-
+
{
-
+
{ value="${_esc(v.zuchtbuchnummer || '')}" placeholder="z. B. SZ 123456">
-
+
@@ -698,7 +698,7 @@ window.Page_zuchthunde = (() => {
-
+
{
- +
@@ -807,9 +807,9 @@ window.Page_zuchthunde = (() => { const body = ` -
+
- +
- +
- + @@ -836,7 +836,7 @@ window.Page_zuchthunde = (() => {
-
+
@@ -847,7 +847,7 @@ window.Page_zuchthunde = (() => {
-
+
@@ -931,9 +931,9 @@ window.Page_zuchthunde = (() => { const body = ` -
+
- +
- + @@ -1020,9 +1020,9 @@ window.Page_zuchthunde = (() => { const body = ` -
+
- +
- +
-
+
@@ -1056,7 +1056,7 @@ window.Page_zuchthunde = (() => {
-
+
@@ -1208,7 +1208,7 @@ window.Page_zuchthunde = (() => { const genStr = genInfo.length ? ` (${genInfo.join(' / ')})` : ''; return `
  • ${_esc(v.name || '—')}${genStr}
  • `; }).join('') - : `
  • Keine gemeinsamen Vorfahren gefunden.
  • `; + : `
  • Keine gemeinsamen Vorfahren gefunden.
  • `; const welfare = result.welfare; let welfareHTML = ''; @@ -1220,13 +1220,13 @@ window.Page_zuchthunde = (() => { const wIssueHTML = (welfare.issues || []).map(i => `
    ${UI.icon('warning')} - ${_esc(i.text)} + ${_esc(i.text)}
    `).join(''); const wOkHTML = (welfare.ok_points || []).map(p => `
    ${UI.icon('check')} - ${_esc(p)} + ${_esc(p)}
    `).join(''); welfareHTML = ` @@ -1278,7 +1278,7 @@ window.Page_zuchthunde = (() => { const footer = `
    ${kiPaarungBtn} -
    +
    @@ -1314,7 +1314,7 @@ window.Page_zuchthunde = (() => { } catch (err) { UI.modal.open({ title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`, - body: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, + body: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, footer: ``, }); return; @@ -1414,7 +1414,7 @@ window.Page_zuchthunde = (() => { } catch (err) { UI.modal.open({ title: `${UI.icon('chart-bar')} KI-Jahresbericht`, - body: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, + body: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, footer: ``, }); return; @@ -1484,7 +1484,7 @@ window.Page_zuchthunde = (() => {
    Jahresbericht ${b.jahr}
    -
    +
    ${new Date(b.created_at).toLocaleDateString('de', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}
    @@ -1532,7 +1532,7 @@ window.Page_zuchthunde = (() => { } catch (err) { UI.modal.open({ title: `${UI.icon('sparkle')} KI-Paarungsanalyse`, - body: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, + body: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, footer: ``, }); return; @@ -1719,11 +1719,11 @@ window.Page_zuchthunde = (() => {

    Diese Fotos erscheinen im öffentlichen Züchterprofil. Das primäre Foto wird als Logo im Hero angezeigt.

    -
    -

    Lädt…

    +
    +

    Lädt…


    - + @@ -1739,7 +1739,7 @@ window.Page_zuchthunde = (() => { try { const photos = await API.breederPhotos.list('breeder', breederId); if (!photos.length) { - el.innerHTML = `

    Noch keine Fotos — lade das erste hoch.

    `; + el.innerHTML = `

    Noch keine Fotos — lade das erste hoch.

    `; return; } el.innerHTML = ` @@ -1805,7 +1805,7 @@ window.Page_zuchthunde = (() => { }); } catch (err) { const el = document.getElementById(galleryId); - if (el) el.innerHTML = `

    ${_esc(err.message || 'Fehler')}

    `; + if (el) el.innerHTML = `

    ${_esc(err.message || 'Fehler')}

    `; } } diff --git a/backend/static/landing.html b/backend/static/landing.html index 7bf238e..8a56643 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index 46becbb..cd866e4 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1101'; +const VER = '1102'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
    NummerEmpfängerBetragStatusBetragStatus Erstellt
    Gesamt ${_fmtE(data.total_gross)} + Netto: ${_fmtE(data.total_net)} · MwSt: ${_fmtE(data.total_tax)}
    ${_fmtDate(t.datum)}${_fmtDate(t.datum)} ${t.wert != null ? `${t.wert} ${UI.escape(t.einheit)}` : '—'} ${t.wert != null ? `${_progEinschaetzung(t.wert, t.einheit)}` : ''} @@ -543,7 +543,7 @@ window.Page_laeufi = (() => { ${t.labor ? UI.escape(t.labor) : '—'} + class="text-danger">${UI.icon('trash')}
    ${_esc(t.test_typ || 'Sonstiges')} - ${t.test_name ? `
    ${_esc(t.test_name)}` : ''} + ${t.test_name ? `
    ${_esc(t.test_name)}` : ''}
    ${_healthBadge(t.test_typ || '', t.ergebnis)} ${t.untersuch_am ? _fmtDate(t.untersuch_am) : '—'}