Feature: Sprint31 — 9 Features merged (Streak, Ausgaben, KI-Tierarzt, Rückrufe, Adoption, Vet+Befunde, Hundepass, Playdate, Rassenerkennung)
- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
This commit is contained in:
parent
031c6028ac
commit
742ad189e8
26 changed files with 5734 additions and 27 deletions
483
backend/static/js/pages/adoption.js
Normal file
483
backend/static/js/pages/adoption.js
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
/* ============================================================
|
||||
BAN YARO — Adoption (Tierheim-Hunde in der Nähe)
|
||||
Seiten-Modul: Hunde aus deutschen Tierheimen finden.
|
||||
============================================================ */
|
||||
|
||||
window.Page_adoption = (() => {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// MODUL-STATE
|
||||
// ----------------------------------------------------------
|
||||
let _container = null;
|
||||
let _appState = null;
|
||||
let _lat = null;
|
||||
let _lon = null;
|
||||
let _radius = 50;
|
||||
let _rasseFilter = '';
|
||||
let _activeTab = 'hunde';
|
||||
let _data = null; // { animals, shelters, has_petfinder }
|
||||
let _loading = false;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// INIT
|
||||
// ----------------------------------------------------------
|
||||
async function init(container, appState) {
|
||||
_container = container;
|
||||
_appState = appState;
|
||||
_render();
|
||||
// Standort automatisch versuchen
|
||||
_tryAutoLocate();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// REFRESH
|
||||
// ----------------------------------------------------------
|
||||
async function refresh() {
|
||||
if (_lat && _lon) {
|
||||
await _loadData();
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// RENDER — Grundstruktur
|
||||
// ----------------------------------------------------------
|
||||
function _render() {
|
||||
_container.innerHTML = `
|
||||
<!-- Filter-Leiste -->
|
||||
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3);align-items:center">
|
||||
<select id="adp-radius" class="form-control" style="width:auto;min-width:110px">
|
||||
<option value="10">10 km</option>
|
||||
<option value="25">25 km</option>
|
||||
<option value="50" selected>50 km</option>
|
||||
<option value="100">100 km</option>
|
||||
</select>
|
||||
<input id="adp-rasse" class="form-control" type="text"
|
||||
placeholder="Rasse filtern…"
|
||||
style="flex:1;min-width:120px;max-width:220px"
|
||||
value="${_esc(_rasseFilter)}">
|
||||
<button class="btn btn-secondary" id="adp-btn-locate"
|
||||
style="white-space:nowrap">
|
||||
${UI.icon('map-pin')} Mein Standort
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- PLZ-Fallback (anfangs versteckt) -->
|
||||
<div id="adp-plz-row" style="display:none;margin-bottom:var(--space-3)">
|
||||
<div style="display:flex;gap:var(--space-2);align-items:center">
|
||||
<input id="adp-plz" class="form-control" type="text"
|
||||
inputmode="numeric" maxlength="5"
|
||||
placeholder="PLZ eingeben (z.B. 80331)"
|
||||
style="max-width:180px">
|
||||
<button class="btn btn-primary" id="adp-btn-geocode">Suchen</button>
|
||||
</div>
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:var(--space-1)">
|
||||
Kein Standort verfügbar — PLZ als Ausgangspunkt eingeben.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div style="display:flex;gap:var(--space-1);margin-bottom:var(--space-4);
|
||||
border-bottom:1px solid var(--c-border)">
|
||||
<button class="adp-tab adp-tab--active" data-tab="hunde"
|
||||
style="padding:var(--space-2) var(--space-3);background:none;border:none;
|
||||
cursor:pointer;font-weight:600;color:var(--c-primary);
|
||||
border-bottom:2px solid var(--c-primary);font-size:var(--text-sm)">
|
||||
${UI.icon('paw-print')} Hunde
|
||||
</button>
|
||||
<button class="adp-tab" data-tab="tierheime"
|
||||
style="padding:var(--space-2) var(--space-3);background:none;border:none;
|
||||
cursor:pointer;color:var(--c-text-secondary);
|
||||
border-bottom:2px solid transparent;font-size:var(--text-sm)">
|
||||
${UI.icon('house-line')} Tierheime
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Inhalt -->
|
||||
<div id="adp-content">
|
||||
${UI.skeleton(4)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Events
|
||||
_container.querySelector('#adp-radius')
|
||||
?.addEventListener('change', e => {
|
||||
_radius = parseInt(e.target.value);
|
||||
if (_lat && _lon) _loadData();
|
||||
});
|
||||
|
||||
_container.querySelector('#adp-rasse')
|
||||
?.addEventListener('input', e => {
|
||||
_rasseFilter = e.target.value.trim().toLowerCase();
|
||||
_renderContent();
|
||||
});
|
||||
|
||||
_container.querySelector('#adp-btn-locate')
|
||||
?.addEventListener('click', _locateUser);
|
||||
|
||||
_container.querySelector('#adp-btn-geocode')
|
||||
?.addEventListener('click', _geocodePLZ);
|
||||
|
||||
_container.querySelector('#adp-plz')
|
||||
?.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') _geocodePLZ();
|
||||
});
|
||||
|
||||
_container.querySelectorAll('.adp-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
_activeTab = btn.dataset.tab;
|
||||
_container.querySelectorAll('.adp-tab').forEach(b => {
|
||||
const isActive = b.dataset.tab === _activeTab;
|
||||
b.style.color = isActive ? 'var(--c-primary)' : 'var(--c-text-secondary)';
|
||||
b.style.fontWeight = isActive ? '600' : 'normal';
|
||||
b.style.borderBottom = isActive ? '2px solid var(--c-primary)' : '2px solid transparent';
|
||||
});
|
||||
_renderContent();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STANDORT AUTOMATISCH ERMITTELN
|
||||
// ----------------------------------------------------------
|
||||
async function _tryAutoLocate() {
|
||||
try {
|
||||
const pos = await API.getLocation({ timeout: 6000, maximumAge: 300_000 });
|
||||
_lat = pos.lat;
|
||||
_lon = pos.lon;
|
||||
await _loadData();
|
||||
} catch {
|
||||
// Standort nicht verfügbar → PLZ-Eingabe zeigen
|
||||
document.getElementById('adp-plz-row')?.style.setProperty('display', 'flex', 'important');
|
||||
document.getElementById('adp-plz-row').style.display = 'flex';
|
||||
_showNoLocation();
|
||||
}
|
||||
}
|
||||
|
||||
async function _locateUser() {
|
||||
const btn = _container.querySelector('#adp-btn-locate');
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const pos = await API.getLocation({ timeout: 10000 });
|
||||
_lat = pos.lat;
|
||||
_lon = pos.lon;
|
||||
document.getElementById('adp-plz-row').style.display = 'none';
|
||||
await _loadData();
|
||||
} catch {
|
||||
UI.toast.error('Standort konnte nicht ermittelt werden. Bitte PLZ eingeben.');
|
||||
document.getElementById('adp-plz-row').style.display = 'flex';
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function _geocodePLZ() {
|
||||
const plz = (_container.querySelector('#adp-plz')?.value || '').trim();
|
||||
if (!plz) return;
|
||||
const btn = _container.querySelector('#adp-btn-geocode');
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const geo = await API.get(`/adoption/geocode?plz=${encodeURIComponent(plz)}`);
|
||||
if (geo.lat && geo.lon) {
|
||||
_lat = geo.lat;
|
||||
_lon = geo.lon;
|
||||
await _loadData();
|
||||
} else {
|
||||
UI.toast.error(`PLZ "${plz}" nicht gefunden.`);
|
||||
}
|
||||
} catch {
|
||||
UI.toast.error('Geocoding fehlgeschlagen. Bitte erneut versuchen.');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// DATEN LADEN
|
||||
// ----------------------------------------------------------
|
||||
async function _loadData() {
|
||||
if (_loading || !_lat || !_lon) return;
|
||||
_loading = true;
|
||||
const content = _container.querySelector('#adp-content');
|
||||
if (content) content.innerHTML = UI.skeleton(4);
|
||||
try {
|
||||
_data = await API.get(`/adoption/nearby?lat=${_lat}&lon=${_lon}&radius=${_radius}`);
|
||||
_renderContent();
|
||||
} catch {
|
||||
if (content) content.innerHTML = UI.emptyState({
|
||||
icon: 'warning',
|
||||
title: 'Daten konnten nicht geladen werden',
|
||||
text: 'Bitte versuche es erneut.',
|
||||
});
|
||||
} finally {
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// INHALT RENDERN (je nach Tab)
|
||||
// ----------------------------------------------------------
|
||||
function _renderContent() {
|
||||
const content = _container.querySelector('#adp-content');
|
||||
if (!content) return;
|
||||
if (!_data) { _showNoLocation(); return; }
|
||||
|
||||
if (_activeTab === 'hunde') _renderHunde(content);
|
||||
else _renderTierheime(content);
|
||||
}
|
||||
|
||||
function _showNoLocation() {
|
||||
const content = _container.querySelector('#adp-content');
|
||||
if (!content) return;
|
||||
content.innerHTML = `
|
||||
<div style="text-align:center;padding:var(--space-8) var(--space-4)">
|
||||
<div style="font-size:2.5rem;margin-bottom:var(--space-3)">🐾</div>
|
||||
<h3 style="margin-bottom:var(--space-2)">Finde Hunde in deiner Nähe</h3>
|
||||
<p style="color:var(--c-text-secondary);margin-bottom:var(--space-5);max-width:320px;margin-inline:auto">
|
||||
Erlaube den Zugriff auf deinen Standort oder gib eine PLZ ein, um Tierheim-Hunde
|
||||
in deiner Umgebung zu finden.
|
||||
</p>
|
||||
<a href="https://www.tierheimhelden.de/hunde/liste"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary">
|
||||
${UI.icon('arrow-square-out')} Alle Hunde auf Tierheimhelden.de
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB: HUNDE
|
||||
// ------------------------------------------------------------------
|
||||
function _renderHunde(content) {
|
||||
let animals = (_data?.animals || []);
|
||||
|
||||
// Rasse-Filter
|
||||
if (_rasseFilter) {
|
||||
animals = animals.filter(a =>
|
||||
(a.rasse || '').toLowerCase().includes(_rasseFilter) ||
|
||||
(a.name || '').toLowerCase().includes(_rasseFilter)
|
||||
);
|
||||
}
|
||||
|
||||
const hasPetFinder = _data?.has_petfinder;
|
||||
const infoText = hasPetFinder
|
||||
? `${animals.length} Hunde im Umkreis von ${_radius} km (via PetFinder)`
|
||||
: '';
|
||||
|
||||
if (!animals.length) {
|
||||
content.innerHTML = `
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||||
${_rasseFilter ? `Keine Hunde gefunden für "<strong>${_esc(_rasseFilter)}</strong>"` : `Keine Hunde im Umkreis von ${_radius} km gefunden.`}
|
||||
</p>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3);max-width:380px">
|
||||
<a href="https://www.tierheimhelden.de/hunde/liste"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary">
|
||||
${UI.icon('arrow-square-out')} Hunde auf Tierheimhelden.de suchen
|
||||
</a>
|
||||
<a href="https://www.tierschutz.com/tierheimsuche/"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-secondary">
|
||||
${UI.icon('magnifying-glass')} Tierheimsuche auf tierschutz.com
|
||||
</a>
|
||||
</div>
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:var(--space-4)">
|
||||
Tipp: Schau auch im Tab „Tierheime" nach lokalen Tierheimen direkt.
|
||||
</p>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
content.innerHTML = `
|
||||
${infoText ? `<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">${infoText}</p>` : ''}
|
||||
<div class="adp-grid"
|
||||
style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:var(--space-3)">
|
||||
${animals.map(a => _animalCard(a)).join('')}
|
||||
</div>
|
||||
<div style="margin-top:var(--space-5);padding-top:var(--space-4);border-top:1px solid var(--c-border)">
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)">
|
||||
Mehr Hunde finden:
|
||||
</p>
|
||||
<a href="https://www.tierheimhelden.de/hunde/liste"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-secondary" style="font-size:var(--text-sm)">
|
||||
${UI.icon('arrow-square-out')} Tierheimhelden.de — alle Hunde
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Klick-Events
|
||||
content.querySelectorAll('[data-adp-url]').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
window.open(card.dataset.adpUrl, '_blank', 'noopener,noreferrer');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _animalCard(a) {
|
||||
const foto = a.foto_url
|
||||
? `<img src="${_esc(a.foto_url)}" alt="${_esc(a.name)}"
|
||||
style="width:100%;height:100%;object-fit:cover"
|
||||
onerror="this.parentElement.innerHTML='<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2rem">🐶</div>'">`
|
||||
: '<div style="display:flex;align-items:center;justify-content:center;height:100%;font-size:2.5rem">🐶</div>';
|
||||
|
||||
const distTxt = a.distanz_km != null ? `${a.distanz_km} km` : '';
|
||||
const alterTxt = a.alter_jahre != null ? `${_formatAlter(a.alter_jahre)}` : '';
|
||||
const rasseTxt = a.rasse || '';
|
||||
const tierheim = a.tierheim || '';
|
||||
|
||||
return `
|
||||
<div data-adp-url="${_esc(a.adoptions_url)}"
|
||||
style="border-radius:var(--radius-md);overflow:hidden;
|
||||
background:var(--c-surface-2);cursor:pointer;
|
||||
box-shadow:0 1px 4px rgba(0,0,0,0.08);
|
||||
transition:transform .15s,box-shadow .15s"
|
||||
onmouseenter="this.style.transform='translateY(-2px)';this.style.boxShadow='0 4px 12px rgba(0,0,0,0.12)'"
|
||||
onmouseleave="this.style.transform='';this.style.boxShadow='0 1px 4px rgba(0,0,0,0.08)'">
|
||||
<div style="height:120px;overflow:hidden;background:var(--c-surface-3)">
|
||||
${foto}
|
||||
</div>
|
||||
<div style="padding:var(--space-2) var(--space-2) var(--space-3)">
|
||||
<div style="font-weight:600;font-size:var(--text-sm);
|
||||
margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
${_esc(a.name)}
|
||||
</div>
|
||||
${rasseTxt ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
${_esc(rasseTxt)}
|
||||
</div>` : ''}
|
||||
<div style="display:flex;gap:var(--space-1);flex-wrap:wrap;margin-top:var(--space-1)">
|
||||
${alterTxt ? `<span style="font-size:10px;background:var(--c-surface-3);
|
||||
border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)">
|
||||
${_esc(alterTxt)}
|
||||
</span>` : ''}
|
||||
${a.geschlecht ? `<span style="font-size:10px;background:var(--c-surface-3);
|
||||
border-radius:999px;padding:1px 6px;color:var(--c-text-secondary)">
|
||||
${a.geschlecht === 'männlich' ? '♂' : '♀'}
|
||||
</span>` : ''}
|
||||
${distTxt ? `<span style="font-size:10px;background:var(--c-primary-light,#ede9fe);
|
||||
border-radius:999px;padding:1px 6px;color:var(--c-primary)">
|
||||
${_esc(distTxt)}
|
||||
</span>` : ''}
|
||||
</div>
|
||||
${tierheim ? `<div style="font-size:10px;color:var(--c-text-muted);margin-top:var(--space-1);
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${_esc(tierheim)}">
|
||||
${UI.icon('house-line')} ${_esc(tierheim)}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB: TIERHEIME
|
||||
// ------------------------------------------------------------------
|
||||
function _renderTierheime(content) {
|
||||
const shelters = _data?.shelters || [];
|
||||
|
||||
if (!shelters.length) {
|
||||
content.innerHTML = `
|
||||
<div style="text-align:center;padding:var(--space-6) var(--space-4)">
|
||||
<p style="color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||||
Keine Tierheime im Umkreis von ${_radius} km gefunden.
|
||||
</p>
|
||||
<a href="https://www.tierheimhelden.de"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-primary">
|
||||
${UI.icon('arrow-square-out')} Tierheimhelden.de
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
content.innerHTML = `
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">
|
||||
${shelters.length} Tierheim${shelters.length !== 1 ? 'e' : ''} im Umkreis von ${_radius} km
|
||||
</p>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||
${shelters.map(s => _shelterRow(s)).join('')}
|
||||
</div>
|
||||
<div style="margin-top:var(--space-5);padding-top:var(--space-4);border-top:1px solid var(--c-border)">
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-2)">
|
||||
Noch mehr Tierheime:
|
||||
</p>
|
||||
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||||
<a href="https://www.tierheimhelden.de"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-secondary btn-sm" style="font-size:var(--text-sm)">
|
||||
${UI.icon('arrow-square-out')} Tierheimhelden.de
|
||||
</a>
|
||||
<a href="https://www.tierschutz.com/tierheimsuche/"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="btn btn-secondary btn-sm" style="font-size:var(--text-sm)">
|
||||
${UI.icon('magnifying-glass')} tierschutz.com
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _shelterRow(s) {
|
||||
return `
|
||||
<a href="${_esc(s.url)}" target="_blank" rel="noopener noreferrer"
|
||||
style="display:flex;align-items:center;gap:var(--space-3);
|
||||
padding:var(--space-3);border-radius:var(--radius-md);
|
||||
background:var(--c-surface-2);text-decoration:none;color:inherit;
|
||||
border:1px solid var(--c-border);
|
||||
transition:background .15s"
|
||||
onmouseenter="this.style.background='var(--c-surface-3)'"
|
||||
onmouseleave="this.style.background='var(--c-surface-2)'">
|
||||
<div style="width:40px;height:40px;border-radius:50%;
|
||||
background:var(--c-primary-light,#ede9fe);flex-shrink:0;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:1.2rem">
|
||||
🏠
|
||||
</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:var(--text-sm);
|
||||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||
${_esc(s.name)}
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
${_esc(s.plz)} ${_esc(s.stadt)}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex-shrink:0">
|
||||
<span style="font-size:var(--text-xs);font-weight:600;
|
||||
color:var(--c-primary);background:var(--c-primary-light,#ede9fe);
|
||||
border-radius:999px;padding:2px 8px">
|
||||
${s.distanz_km} km
|
||||
</span>
|
||||
<span style="font-size:10px;color:var(--c-text-muted)">Hunde ansehen ${UI.icon('arrow-right')}</span>
|
||||
</div>
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// HILFSFUNKTIONEN
|
||||
// ----------------------------------------------------------
|
||||
function _formatAlter(jahre) {
|
||||
if (jahre < 0.5) return 'Welpe';
|
||||
if (jahre < 1) return 'Jungtier';
|
||||
if (jahre < 2) return `${Math.round(jahre)} Jahr`;
|
||||
if (jahre < 10) return `${Math.round(jahre)} Jahre`;
|
||||
return 'Senior';
|
||||
}
|
||||
|
||||
function _esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC API
|
||||
// ----------------------------------------------------------
|
||||
return { init, refresh };
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue