Routen-Navigation:
- POI-Marker: farbige Kreise mit Phosphor-Icons (wie Hauptkarte)
- Screensaver: Navi-Pfeil dreht sich via DeviceOrientationEvent (iOS+Android)
- Pfeil-Dämpfung: EMA α=0.12 mit Wrap-Around
- GPS-Distanz-Bug: Fortschritt nur wenn <500m zur Route
- fitBounds: User-Position nur wenn <20km von Route
- Screensaver: "zur Route" vs "verbleibend" kontextabhängig
- Richtungspfeile entlang Route (blau, max 7 Stück)
- Umkehren ins Route-Detail verschoben, Detail-Map rebuildet sich
- rk-header z-index:10 (Leaflet-Tiles liefen drüber)
- 2-Sek. Screensaver-Entsperrung
km-Tracking:
- route_walks Tabelle
- POST /api/routes/{id}/walked (≥50%)
- total_km = erstellte Routes + gelaufene route_walks
- Toast bei neuem Badge
Übungsfortschritt:
- exercise_progress + training_plan_progress Tabellen
- GET/POST /api/training/progress, /plan-progress, /suggestions
- uebungen.js: API-first + localStorage-Fallback + Auto-Migration
- Empfehlungs-Banner (regelbasiert)
- Toast bei "sitzt"
969 lines
44 KiB
JavaScript
969 lines
44 KiB
JavaScript
/* ============================================================
|
||
BAN YARO — Übungsbibliothek
|
||
Seiten-Modul: Grundkommandos, Tricks, Problemverhalten, Grundlagen.
|
||
============================================================ */
|
||
|
||
window.Page_uebungen = (() => {
|
||
|
||
let _container = null;
|
||
let _appState = null;
|
||
let _activeTab = 'grundkommandos';
|
||
|
||
// ----------------------------------------------------------
|
||
// DATEN
|
||
// ----------------------------------------------------------
|
||
|
||
const TABS = [
|
||
{ id: 'grundkommandos', label: 'Grundkommandos' },
|
||
{ id: 'tricks', label: 'Tricks & Beschäftigung' },
|
||
{ id: 'problemverhalten', label: 'Problemverhalten' },
|
||
{ id: 'grundlagen', label: 'Trainingsgrundlagen' },
|
||
{ id: 'ki-trainer', label: 'KI-Trainer' },
|
||
];
|
||
|
||
// ----------------------------------------------------------
|
||
// ÜBUNGS-STATUS
|
||
// ----------------------------------------------------------
|
||
const STATUS = [
|
||
{ id: null, icon: 'flag', color: 'var(--c-border)', label: 'Noch nicht geübt' },
|
||
{ id: 'noch-nicht', icon: 'x', color: 'var(--c-danger)', label: 'Klappt noch nicht' },
|
||
{ id: 'manchmal', icon: 'fire', color: '#f59e0b', label: 'Manchmal klappt es' },
|
||
{ id: 'meistens', icon: 'star', color: '#eab308', label: 'Meistens klappt es' },
|
||
{ id: 'sitzt', icon: 'trophy', color: 'var(--c-primary)', label: 'Sitzt!' },
|
||
];
|
||
|
||
function _statusKey(tab, name) {
|
||
return `ub_status_${tab}_${name.replace(/\s+/g, '_')}`;
|
||
}
|
||
|
||
// In-memory cache (loaded from API on init)
|
||
let _progressCache = {}; // key → statusId
|
||
|
||
function _progressKey(tab, name) {
|
||
return `${tab}_${name.replace(/[\s/]+/g, '_')}`;
|
||
}
|
||
|
||
function _getStatus(tab, name) {
|
||
const k = _progressKey(tab, name);
|
||
// Fallback to localStorage while API loads
|
||
return _progressCache[k] !== undefined
|
||
? _progressCache[k]
|
||
: localStorage.getItem(_statusKey(tab, name)) || null;
|
||
}
|
||
|
||
function _setStatus(tab, name, statusId) {
|
||
const k = _progressKey(tab, name);
|
||
_progressCache[k] = statusId;
|
||
localStorage.setItem(_statusKey(tab, name), statusId || ''); // keep localStorage in sync
|
||
API.training.setProgress(k, statusId).catch(() => {});
|
||
}
|
||
|
||
function _nextStatus(currentId) {
|
||
const idx = STATUS.findIndex(s => s.id === currentId);
|
||
const next = (idx + 1) % STATUS.length;
|
||
return STATUS[next].id;
|
||
}
|
||
|
||
function _statusMeta(statusId) {
|
||
return STATUS.find(s => s.id === statusId) || STATUS[0];
|
||
}
|
||
|
||
const DIFF_META = {
|
||
'Anfänger': { label: 'Anfänger', color: 'var(--c-success)' },
|
||
'Fortgeschrittener Anfänger': { label: 'Fortgeschr. Anfänger', color: '#eab308' },
|
||
'Mittel': { label: 'Mittel', color: 'var(--c-primary)' },
|
||
'Anfänger bis Fortgeschrittener': { label: 'Anfänger–Fortgeschr.', color: '#eab308' },
|
||
};
|
||
|
||
const GRUNDKOMMANDOS = [
|
||
{
|
||
name: 'Sitz',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 8 Wochen',
|
||
dauer: '3–5 Minuten',
|
||
material: 'Leckerlis, ruhige Umgebung',
|
||
beschreibung: 'Der Hund setzt sich auf ein Signal hin. Das ist meist das erste Kommando und bildet die Basis für viele weitere Übungen.',
|
||
schritte: [
|
||
'Halte ein Leckerli knapp vor die Nase deines Hundes.',
|
||
'Führe das Leckerli langsam nach oben und leicht nach hinten über seinen Kopf.',
|
||
'Der Hund folgt mit der Nase — sein Hinterteil senkt sich automatisch.',
|
||
'Sobald er sitzt: sofort Markerwort ("Ja!" oder Klicker) + Leckerli.',
|
||
'Wiederhole 5–10x, bevor du das Wort "Sitz" hinzufügst.',
|
||
'Ab Wiederholung 10: Sage "Sitz" kurz bevor du die Handbewegung machst.',
|
||
],
|
||
fehler: [
|
||
'Leckerli zu hoch halten → Hund springt hoch statt zu sitzen',
|
||
'Kommando zu früh einführen → Hund lernt das Wort bevor er die Bewegung kennt',
|
||
'Zu lange Einheiten → Hund wird unkonzentriert',
|
||
],
|
||
steigerung: 'Sitz mit Ablenkung → Sitz aus Bewegung → Sitz auf Distanz',
|
||
},
|
||
{
|
||
name: 'Platz',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 10 Wochen',
|
||
dauer: '3–5 Minuten',
|
||
material: 'Leckerlis, ruhige Umgebung',
|
||
beschreibung: 'Der Hund legt sich auf ein Signal hin. Wichtig für Ruhephasen, Wartesituationen und als Basis für "Bleib".',
|
||
schritte: [
|
||
'Beginne mit dem Hund im Sitz.',
|
||
'Halte ein Leckerli vor seine Nase und führe es langsam senkrecht nach unten zwischen seine Vorderpfoten.',
|
||
'Der Hund folgt mit der Nase und legt sich ab.',
|
||
'Sobald Ellenbogen und Hinterteil den Boden berühren: Markerwort + Leckerli.',
|
||
'Klappt es nicht: Leckerli unter ein angewinkeltes Knie halten — der Hund kriecht darunter durch und legt sich dabei ab.',
|
||
'Wort "Platz" erst nach 10–15 erfolgreichen Wiederholungen einführen.',
|
||
],
|
||
fehler: [
|
||
'Hund steht auf statt sich hinzulegen → Leckerli-Führung zu weit weg',
|
||
'Hund liegt nur kurz → zu früh belohnen, Dauer schrittweise aufbauen',
|
||
],
|
||
steigerung: 'Platz mit Dauer → Platz mit Distanz → Platz bei Ablenkung',
|
||
},
|
||
{
|
||
name: 'Bleib',
|
||
schwierigkeit: 'Anfänger bis Fortgeschrittener',
|
||
alter: 'Ab 12 Wochen',
|
||
dauer: '5 Minuten',
|
||
material: 'Leckerlis, Geduld',
|
||
beschreibung: 'Der Hund hält eine Position (Sitz oder Platz) bis er freigegeben wird. Drei Dimensionen: Dauer, Distanz, Ablenkung — immer nur eine auf einmal steigern.',
|
||
schritte: [
|
||
'Hund ins Sitz oder Platz bringen.',
|
||
'Einen Moment warten (2 Sekunden) → Markerwort + Leckerli.',
|
||
'Freigabewort einführen: "Okay" oder "Frei" — danach darf der Hund aufstehen.',
|
||
'Dauer schrittweise erhöhen: 2 → 5 → 10 → 30 Sekunden.',
|
||
'Erst wenn Dauer stabil ist: einen kleinen Schritt zurücktreten.',
|
||
'Zurückkehren zum Hund, belohnen — nicht den Hund zu dir kommen lassen.',
|
||
],
|
||
fehler: [
|
||
'Zu schnell Distanz aufbauen → Hund bricht ab',
|
||
'Hund wird zur Person gelobt statt an Ort → Hund kommt herangelaufen',
|
||
'Freigabewort vergessen → Hund weiß nicht wann er aufstehen darf',
|
||
],
|
||
steigerung: 'Bleib 1 Min → Bleib mit Sichtkontaktverlust → Bleib bei Ablenkung (Ball rollen, andere Person)',
|
||
},
|
||
{
|
||
name: 'Hier / Komm',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 8 Wochen',
|
||
dauer: '5 Minuten',
|
||
material: 'Leckerlis oder Spielzeug, Schleppleine empfohlen',
|
||
beschreibung: 'Der Hund kommt zuverlässig zurück wenn er gerufen wird. Eines der wichtigsten Kommandos — im Zweifel lebensrettend.',
|
||
schritte: [
|
||
'Beginne in der Wohnung auf kurze Distanz (2–3 Meter).',
|
||
'Knie dich hin, öffne die Arme, freudige Stimme: "Hier!" oder "Komm!"',
|
||
'Sobald der Hund ankommt: große Freude, Leckerli, Streicheln.',
|
||
'Niemals rufen und dann etwas Unangenehmes tun (Bad, Krallen schneiden).',
|
||
'Im Garten: Schleppleine verwenden — Hund kann nicht wegbleiben, Erfolg ist garantiert.',
|
||
'Ruf nur einmal — wer mehrfach ruft trainiert den Hund aufs Ignorieren.',
|
||
],
|
||
fehler: [
|
||
'Hund rufen wenn er sicher nicht kommt → schlechte Gewohnheit',
|
||
'Hund bestrafen nach dem Kommen → nächstes Mal kommt er nicht',
|
||
'Monotone Stimme → Hund motiviert sich nicht',
|
||
],
|
||
steigerung: 'Kurze Distanz innen → Garten mit Schleppleine → Park mit wenig Ablenkung → Freilauf',
|
||
},
|
||
{
|
||
name: 'Fuß',
|
||
schwierigkeit: 'Fortgeschrittener Anfänger',
|
||
alter: 'Ab 12 Wochen',
|
||
dauer: '5–10 Minuten',
|
||
material: 'Leckerlis, Leine',
|
||
beschreibung: 'Der Hund läuft ruhig an der Leine neben dir, ohne zu ziehen. Die Leine hängt locker durch.',
|
||
schritte: [
|
||
'Hund an deine linke Seite stellen (klassisch), Leckerli in der linken Hand.',
|
||
'Einen Schritt vorwärts gehen, Leckerli auf Höhe deiner linken Hüfte halten.',
|
||
'Hund folgt dem Leckerli → Markerwort + belohnen.',
|
||
'Schrittweise mehr Schritte, immer wieder belohnen wenn Leine locker ist.',
|
||
'Zieht der Hund: stehen bleiben oder Richtung wechseln — nie mitziehen lassen.',
|
||
'Wort "Fuß" einführen sobald der Hund die Position versteht.',
|
||
],
|
||
fehler: [
|
||
'Leine straff halten → Hund lernt Zug als Normalzustand',
|
||
'Zu selten belohnen am Anfang → Hund verliert Interesse an der Position',
|
||
'Zu lange Einheiten → Überforderung',
|
||
],
|
||
steigerung: 'Fuß in der Wohnung → ruhige Straße → belebte Umgebung → ohne Leckerli in der Hand',
|
||
},
|
||
{
|
||
name: 'Aus / Lass es',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 10 Wochen',
|
||
dauer: '3–5 Minuten',
|
||
material: 'Leckerlis, Spielzeug oder Gegenstand',
|
||
beschreibung: 'Der Hund lässt einen Gegenstand auf Kommando los. Wichtig für Sicherheit (Gefährliches fallen lassen) und Spielsituationen.',
|
||
schritte: [
|
||
'Gib dem Hund ein Spielzeug oder lass ihn etwas halten.',
|
||
'Halte ein Leckerli vor seine Nase — er lässt den Gegenstand fallen.',
|
||
'Sofort Markerwort + Leckerli geben.',
|
||
'Gegenstand kurz aufheben, dann wieder zurückgeben → Hund lernt: Loslassen lohnt sich.',
|
||
'Wort "Aus" kurz vor dem Leckerli-Zeigen einführen.',
|
||
],
|
||
fehler: [
|
||
'Gegenstand wegziehen → Hund lernt festhalten',
|
||
'Immer wegnehmen nach "Aus" → Hund gibt nicht mehr freiwillig her',
|
||
],
|
||
steigerung: 'Spielzeug → Leckerli auf dem Boden → Gegenstand unterwegs → hochwertige Beute',
|
||
},
|
||
{
|
||
name: 'Warte',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 10 Wochen',
|
||
dauer: '3–5 Minuten',
|
||
material: 'Leckerlis, Türschwelle oder Futterschüssel',
|
||
beschreibung: 'Der Hund wartet kurz in einer Situation bis er freigegeben wird — z.B. vor der Tür, vor dem Futter, beim Aussteigen aus dem Auto.',
|
||
schritte: [
|
||
'Stelle die Futterschüssel auf den Boden — Hund stürmt drauf zu.',
|
||
'Schüssel mit der Hand abdecken oder hochhalten.',
|
||
'Sobald der Hund zurückweicht oder sitzt: Schüssel freigeben + "Okay".',
|
||
'Alternativ: an der Türschwelle — Tür öffnen, Hund wartet bis "Okay".',
|
||
'Wort "Warte" einführen sobald das Verhalten klar ist.',
|
||
],
|
||
fehler: [
|
||
'Hund wird nie freigegeben → verliert Vertrauen ins System',
|
||
'Zu lange warten lassen am Anfang → Frustration',
|
||
],
|
||
steigerung: null,
|
||
},
|
||
];
|
||
|
||
const TRICKS = [
|
||
{
|
||
name: 'Pfote / Schütteln',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 12 Wochen',
|
||
dauer: '3 Minuten',
|
||
material: 'Leckerlis',
|
||
beschreibung: null,
|
||
schritte: [
|
||
'Hund ins Sitz bringen.',
|
||
'Leckerli in der Faust verstecken, Faust auf Kniehöhe halten.',
|
||
'Hund schnuppert, kratzt irgendwann mit der Pfote an der Faust.',
|
||
'Sofort öffnen: Markerwort + Leckerli.',
|
||
'Wort "Pfote" einführen sobald die Bewegung zuverlässig kommt.',
|
||
'Auf flache offene Hand umstellen → Hund legt Pfote rein.',
|
||
],
|
||
fehler: [],
|
||
steigerung: null,
|
||
},
|
||
{
|
||
name: 'Dreh / Runde',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 12 Wochen',
|
||
dauer: '3–5 Minuten',
|
||
material: 'Leckerlis',
|
||
beschreibung: null,
|
||
schritte: [
|
||
'Leckerli vor die Nase des Hundes halten.',
|
||
'Langsam einen Kreis in der Luft führen — der Hund folgt mit der Nase.',
|
||
'Volle Drehung → Markerwort + Leckerli.',
|
||
'Wort "Dreh" (links) und "Runde" (rechts) einführen.',
|
||
'Handbewegung schrittweise kleiner machen.',
|
||
],
|
||
fehler: [],
|
||
steigerung: null,
|
||
},
|
||
{
|
||
name: 'Platz auf Decke / Matte',
|
||
schwierigkeit: 'Fortgeschrittener Anfänger',
|
||
alter: 'Ab 4 Monate',
|
||
dauer: '5–10 Minuten',
|
||
material: 'Leckerlis, Decke oder Matte',
|
||
beschreibung: 'Der Hund geht selbstständig auf seine Decke und legt sich. Ideal für Besuche, Restaurant, ruhige Phasen.',
|
||
schritte: [
|
||
'Decke auf den Boden legen. Hund beschnuppert sie → Leckerli auf die Decke werfen.',
|
||
'Jedes Mal wenn der Hund die Decke betritt: Markerwort + Leckerli.',
|
||
'Warten bis der Hund sich spontan auf die Decke legt → große Belohnung.',
|
||
'"Platz" zeigen sobald er auf der Decke steht.',
|
||
'Decke schrittweise weiter wegstellen.',
|
||
'Wort "Decke" oder "Platz geh" einführen.',
|
||
],
|
||
fehler: [],
|
||
steigerung: null,
|
||
},
|
||
{
|
||
name: 'Suchspiel / Nasenarbeit',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 8 Wochen',
|
||
dauer: '5–10 Minuten',
|
||
material: 'Leckerlis, optional Döschen',
|
||
beschreibung: 'Nasenarbeit ist mentale Auslastung — 10 Minuten Suchen ermüdet mehr als 1 Stunde Spaziergang.',
|
||
schritte: [
|
||
'Leckerli vor den Augen des Hundes unter einem Becher verstecken.',
|
||
'Hund darf suchen → findet er es: Markerwort + Leckerli.',
|
||
'Steigerung: mehrere Becher, Hund muss den richtigen finden.',
|
||
'Später: Leckerlis im Gras verstecken → "Such!"',
|
||
'Noch später: Spielzeug oder Schlüssel suchen lassen.',
|
||
],
|
||
fehler: [],
|
||
steigerung: null,
|
||
},
|
||
];
|
||
|
||
const PROBLEMVERHALTEN = [
|
||
{
|
||
name: 'Nicht springen / Begrüßung',
|
||
schwierigkeit: 'Anfänger',
|
||
alter: 'Ab 8 Wochen',
|
||
dauer: 'Bei jeder Begrüßung',
|
||
material: 'Konsequenz aller Haushaltsmitglieder',
|
||
beschreibung: 'Der Hund begrüßt Menschen mit allen vier Pfoten auf dem Boden.',
|
||
schritte: [
|
||
'Springt der Hund hoch: keine Reaktion (kein "Nein", kein Wegdrücken, kein Augenkontakt).',
|
||
'Sobald alle vier Pfoten unten sind: sofort Markerwort + Leckerli + Aufmerksamkeit.',
|
||
'Konsequenz ist entscheidend — alle im Haushalt müssen gleich reagieren.',
|
||
'Alternative: Hund ins Sitz schicken bei Begrüßung → dann begrüßen.',
|
||
],
|
||
fehler: [],
|
||
steigerung: null,
|
||
},
|
||
{
|
||
name: 'Alleine bleiben',
|
||
schwierigkeit: 'Mittel',
|
||
alter: 'Ab 10 Wochen',
|
||
dauer: 'Mehrmals täglich kurze Einheiten',
|
||
material: 'Geduld, Zeit, Kong oder Kauartikel',
|
||
beschreibung: 'Der Hund bleibt ruhig wenn er allein ist — ohne Stress, Bellen oder Zerstören.',
|
||
schritte: [
|
||
'Hund beschäftigen (Kong mit Futter gefüllt, Kauartikel).',
|
||
'Zimmer verlassen für 10 Sekunden → zurückkommen, ruhig begrüßen.',
|
||
'Zeit schrittweise erhöhen: 30 Sek → 2 Min → 5 Min → 15 Min.',
|
||
'Nie dramatisch verabschieden oder begrüßen — Kommen und Gehen normalisieren.',
|
||
'Hund nicht bestrafen wenn er etwas angestellt hat — er versteht den Zusammenhang nicht mehr.',
|
||
],
|
||
fehler: [],
|
||
steigerung: null,
|
||
hinweis: 'Welpen sollten nie länger als 1–2 Stunden allein gelassen werden. Erwachsene Hunde maximal 4–6 Stunden.',
|
||
},
|
||
{
|
||
name: 'Leinenführigkeit — Nicht ziehen',
|
||
schwierigkeit: 'Mittel',
|
||
alter: 'Ab 12 Wochen',
|
||
dauer: 'Jeder Spaziergang',
|
||
material: 'Leine, Leckerlis, Geduld',
|
||
beschreibung: null,
|
||
schritte: [
|
||
'Beginne den Spaziergang ruhig — aufgeregte Starts fördern das Ziehen.',
|
||
'Zieht der Hund: stehen bleiben. Warten bis Leine locker ist.',
|
||
'Oder: Richtung wechseln sobald die Leine straff wird.',
|
||
'Locker Leine = Bewegung vorwärts + gelegentlich Leckerli.',
|
||
'Kein Ruck an der Leine — Hund lernt dadurch nicht.',
|
||
],
|
||
fehler: [],
|
||
steigerung: null,
|
||
hinweis: 'Hilfsmittel bei hartnäckigem Ziehen: Brustgeschirr mit vorderer Befestigung (kein Würge- oder Stachelband).',
|
||
},
|
||
];
|
||
|
||
// ----------------------------------------------------------
|
||
// INIT
|
||
// ----------------------------------------------------------
|
||
async function init(container, appState) {
|
||
_container = container;
|
||
_appState = appState;
|
||
_render();
|
||
|
||
// Progress vom Server laden
|
||
API.training.getProgress().then(rows => {
|
||
rows.forEach(r => { _progressCache[r.exercise_id] = r.status; });
|
||
// localStorage-Daten migrieren falls noch nicht im Backend
|
||
Object.keys(localStorage).filter(k => k.startsWith('ub_status_')).forEach(lsKey => {
|
||
const parts = lsKey.replace('ub_status_', '').split('_');
|
||
const tab = parts[0];
|
||
const name = parts.slice(1).join('_');
|
||
const apiKey = `${tab}_${name}`;
|
||
if (_progressCache[apiKey] === undefined) {
|
||
const val = localStorage.getItem(lsKey);
|
||
if (val) {
|
||
_progressCache[apiKey] = val;
|
||
API.training.setProgress(apiKey, val).catch(() => {});
|
||
}
|
||
}
|
||
});
|
||
_renderContent(); // Re-render with loaded progress
|
||
}).catch(() => {});
|
||
|
||
// Empfehlungen laden
|
||
API.training.getSuggestions().then(suggestions => {
|
||
if (suggestions.length) _showSuggestions(suggestions);
|
||
}).catch(() => {});
|
||
}
|
||
|
||
function refresh() {}
|
||
function onDogChange() {}
|
||
|
||
// ----------------------------------------------------------
|
||
// HAUPT-RENDER
|
||
// ----------------------------------------------------------
|
||
function _render() {
|
||
_container.innerHTML = `
|
||
<div id="ueb-wrap">
|
||
${_renderTabs()}
|
||
<div id="ueb-suggestions" style="padding:0 var(--space-4);display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-2)"></div>
|
||
<div id="ueb-content"></div>
|
||
</div>
|
||
`;
|
||
_bindTabs();
|
||
_renderContent();
|
||
}
|
||
|
||
function _renderTabs() {
|
||
return `
|
||
<div class="by-tabs" style="padding:var(--space-4) var(--space-4) 0" id="ueb-tabs">
|
||
${TABS.map(t => `
|
||
<button class="by-tab${t.id === _activeTab ? ' active' : ''}"
|
||
data-tab="${t.id}">
|
||
${_esc(t.label)}
|
||
</button>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function _showSuggestions(suggestions) {
|
||
const el = _container.querySelector('#ueb-suggestions');
|
||
if (!el || !suggestions.length) return;
|
||
|
||
const COLORS = {
|
||
help: { bg: '#fef2f2', border: '#fca5a5', text: '#dc2626' },
|
||
boost: { bg: '#fff7ed', border: '#fdba74', text: '#ea580c' },
|
||
next: { bg: '#f0fdf4', border: '#86efac', text: '#16a34a' },
|
||
start: { bg: 'var(--c-primary-subtle)', border: 'var(--c-primary-light)', text: 'var(--c-primary)' },
|
||
};
|
||
|
||
el.innerHTML = suggestions.map(s => {
|
||
const c = COLORS[s.type] || COLORS.start;
|
||
return `
|
||
<div style="background:${c.bg};border:1px solid ${c.border};border-radius:var(--radius-md);
|
||
padding:var(--space-3) var(--space-4);display:flex;gap:var(--space-3);align-items:flex-start;cursor:pointer"
|
||
data-action-tab="${_esc(s.action_tab || '')}"
|
||
data-action-name="${_esc(s.action_name || '')}"
|
||
class="ueb-suggestion-card">
|
||
<svg class="ph-icon" style="width:20px;height:20px;flex-shrink:0;color:${c.text};margin-top:2px" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${_esc(s.icon)}"></use>
|
||
</svg>
|
||
<div style="min-width:0">
|
||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:${c.text};margin-bottom:2px">
|
||
${_esc(s.title)}
|
||
</div>
|
||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
|
||
${_esc(s.text)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
el.querySelectorAll('.ueb-suggestion-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
const tab = card.dataset.actionTab;
|
||
if (tab && tab !== _activeTab) {
|
||
_activeTab = tab;
|
||
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(b =>
|
||
b.classList.toggle('active', b.dataset.tab === tab)
|
||
);
|
||
_renderContent();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function _bindTabs() {
|
||
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
_activeTab = btn.dataset.tab;
|
||
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(b =>
|
||
b.classList.toggle('active', b.dataset.tab === _activeTab)
|
||
);
|
||
_renderContent();
|
||
});
|
||
});
|
||
}
|
||
|
||
function _renderContent() {
|
||
const el = _container.querySelector('#ueb-content');
|
||
if (!el) return;
|
||
switch (_activeTab) {
|
||
case 'grundkommandos': el.innerHTML = _renderUebungsList(GRUNDKOMMANDOS); break;
|
||
case 'tricks': el.innerHTML = _renderUebungsList(TRICKS); break;
|
||
case 'problemverhalten': el.innerHTML = _renderUebungsList(PROBLEMVERHALTEN); break;
|
||
case 'grundlagen': el.innerHTML = _renderGrundlagen(); break;
|
||
case 'ki-trainer': el.innerHTML = _renderKiTrainer(); break;
|
||
}
|
||
_bindAccordions();
|
||
_bindStatusButtons();
|
||
if (_activeTab === 'ki-trainer') _bindKiTrainer();
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// ÜBUNGS-CARDS
|
||
// ----------------------------------------------------------
|
||
function _renderUebungsList(list) {
|
||
return `
|
||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
|
||
${list.map((u, i) => _renderCard(u, i)).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function _renderCard(u, i) {
|
||
const diff = DIFF_META[u.schwierigkeit] || { label: u.schwierigkeit, color: 'var(--c-text-secondary)' };
|
||
const uid = `ueb-acc-${_activeTab}-${i}`;
|
||
const currentId = _getStatus(_activeTab, u.name);
|
||
const sm = _statusMeta(currentId);
|
||
|
||
const hasBody = u.schritte.length > 0 || u.fehler.length > 0 || u.steigerung;
|
||
|
||
return `
|
||
<div class="card" style="padding:0;overflow:hidden">
|
||
<!-- Header -->
|
||
<div style="padding:var(--space-4) var(--space-4) var(--space-3)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-2)">
|
||
<span style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||
${_esc(u.name)}
|
||
</span>
|
||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||
<!-- Status-Button -->
|
||
<button class="ueb-status-btn"
|
||
data-tab="${_esc(_activeTab)}"
|
||
data-name="${_esc(u.name)}"
|
||
title="${_esc(sm.label)}"
|
||
style="background:none;border:none;cursor:pointer;padding:2px;
|
||
display:flex;align-items:center;gap:4px;
|
||
font-size:var(--text-xs);color:${sm.color};
|
||
border-radius:var(--radius-sm)">
|
||
<svg class="ph-icon" style="width:18px;height:18px;flex-shrink:0" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${sm.icon}"></use>
|
||
</svg>
|
||
${currentId ? `<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">${_esc(sm.label)}</span>` : ''}
|
||
</button>
|
||
<!-- Schwierigkeits-Badge -->
|
||
<span style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
padding:2px var(--space-2);border-radius:var(--radius-sm);
|
||
background:${diff.color}22;color:${diff.color}">
|
||
${_esc(diff.label)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<!-- Meta -->
|
||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:${u.beschreibung ? 'var(--space-3)' : '0'}">
|
||
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#clock"></use></svg>
|
||
${_esc(u.dauer)}
|
||
</span>
|
||
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>
|
||
${_esc(u.alter)}
|
||
</span>
|
||
<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
<svg class="ph-icon" style="width:12px;height:12px" aria-hidden="true"><use href="/icons/phosphor.svg#package"></use></svg>
|
||
${_esc(u.material)}
|
||
</span>
|
||
</div>
|
||
${u.beschreibung ? `
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5;margin:0">
|
||
${_esc(u.beschreibung)}
|
||
</p>
|
||
` : ''}
|
||
${u.hinweis ? `
|
||
<div style="margin-top:var(--space-2);padding:var(--space-2) var(--space-3);
|
||
background:var(--c-warning-bg,#fef3c7);border-radius:var(--radius-sm);
|
||
font-size:var(--text-xs);color:var(--c-text);line-height:1.4">
|
||
<svg class="ph-icon" style="width:12px;height:12px;color:#d97706" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||
${_esc(u.hinweis)}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
${hasBody ? `
|
||
<!-- Akkordeon Toggle -->
|
||
<button class="ueb-acc-btn"
|
||
data-acc="${uid}"
|
||
style="width:100%;display:flex;align-items:center;justify-content:space-between;
|
||
padding:var(--space-2) var(--space-4);
|
||
background:var(--c-surface-2);border:none;border-top:1px solid var(--c-border);
|
||
cursor:pointer;font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||
<span>Anleitung anzeigen</span>
|
||
<svg class="ph-icon ueb-chevron" data-acc="${uid}" aria-hidden="true" style="width:16px;height:16px;transition:transform 0.2s">
|
||
<use href="/icons/phosphor.svg#caret-down"></use>
|
||
</svg>
|
||
</button>
|
||
<div id="${uid}" hidden style="padding:var(--space-4);border-top:1px solid var(--c-border)">
|
||
${u.schritte.length ? `
|
||
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
|
||
Schritt für Schritt
|
||
</p>
|
||
<ol style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
|
||
${u.schritte.map(s => `
|
||
<li style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${_esc(s)}</li>
|
||
`).join('')}
|
||
</ol>
|
||
` : ''}
|
||
${u.fehler.length ? `
|
||
<p style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);margin-bottom:var(--space-2);text-transform:uppercase;letter-spacing:0.05em">
|
||
<svg class="ph-icon" style="width:12px;height:12px;color:#f59e0b" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>
|
||
Häufige Fehler
|
||
</p>
|
||
<ul style="margin:0 0 var(--space-4);padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
|
||
${u.fehler.map(f => `
|
||
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${_esc(f)}</li>
|
||
`).join('')}
|
||
</ul>
|
||
` : ''}
|
||
${u.steigerung ? `
|
||
<div style="padding:var(--space-3);background:var(--c-surface-2);border-radius:var(--radius-md);
|
||
font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
|
||
<svg class="ph-icon" style="width:14px;height:14px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#arrow-right"></use>
|
||
</svg>
|
||
<strong>Steigerung:</strong> ${_esc(u.steigerung)}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function _bindStatusButtons() {
|
||
_container.querySelectorAll('.ueb-status-btn').forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const tab = btn.dataset.tab;
|
||
const name = btn.dataset.name;
|
||
const cur = _getStatus(tab, name);
|
||
const next = _nextStatus(cur);
|
||
_setStatus(tab, name, next);
|
||
if (next === 'sitzt') UI.toast.success(`🏆 „${name}" sitzt! Gut gemacht!`);
|
||
|
||
// Update button in place (no full re-render)
|
||
const sm = _statusMeta(next);
|
||
btn.title = sm.label;
|
||
btn.style.color = sm.color;
|
||
btn.innerHTML = `
|
||
<svg class="ph-icon" style="width:18px;height:18px;flex-shrink:0" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${sm.icon}"></use>
|
||
</svg>
|
||
${next ? `<span style="font-size:10px;font-weight:var(--weight-semibold);white-space:nowrap">${_esc(sm.label)}</span>` : ''}
|
||
`;
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// KI-TRAINER
|
||
// ----------------------------------------------------------
|
||
function _renderKiTrainer() {
|
||
return `
|
||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
|
||
|
||
<!-- Intro -->
|
||
<div class="card" style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light)">
|
||
<div style="display:flex;gap:var(--space-3);align-items:flex-start">
|
||
<svg class="ph-icon" style="width:24px;height:24px;flex-shrink:0;color:var(--c-primary);margin-top:2px" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#robot"></use>
|
||
</svg>
|
||
<div>
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);margin:0 0 var(--space-1)">
|
||
KI-Hundetrainer
|
||
</h3>
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0;line-height:1.5">
|
||
Beschreibe ein konkretes Problem oder Verhalten deines Hundes —
|
||
du bekommst individuelle Trainingstipps.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Eingabe -->
|
||
<div class="card" style="padding:var(--space-4)">
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">
|
||
Rasse & Alter (optional)
|
||
</label>
|
||
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4)">
|
||
<input id="ki-rasse" type="text" placeholder="z.B. Labrador"
|
||
style="flex:1;padding:var(--space-2) var(--space-3);border:1.5px solid var(--c-border);
|
||
border-radius:var(--radius-md);font-size:var(--text-sm);
|
||
background:var(--c-surface);color:var(--c-text);font-family:inherit">
|
||
<input id="ki-alter" type="text" placeholder="z.B. 2 Jahre"
|
||
style="flex:1;padding:var(--space-2) var(--space-3);border:1.5px solid var(--c-border);
|
||
border-radius:var(--radius-md);font-size:var(--text-sm);
|
||
background:var(--c-surface);color:var(--c-text);font-family:inherit">
|
||
</div>
|
||
|
||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin-bottom:var(--space-2)">
|
||
Problem beschreiben *
|
||
</label>
|
||
<textarea id="ki-problem" rows="4"
|
||
placeholder="z.B. Mein Hund bellt bei jedem Klingeln an der Tür und lässt sich kaum beruhigen. Er springt Besucher an und ist sehr aufgedreht..."
|
||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||
font-size:var(--text-sm);font-family:inherit;resize:vertical;
|
||
background:var(--c-surface);color:var(--c-text);line-height:1.5;
|
||
min-height:100px"></textarea>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:var(--space-3)">
|
||
<span id="ki-char-count" style="font-size:var(--text-xs);color:var(--c-text-muted)">0 / 1000</span>
|
||
<button id="ki-submit" class="btn btn-primary">
|
||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
|
||
Tipps holen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Antwort -->
|
||
<div id="ki-result" hidden></div>
|
||
|
||
<!-- Hinweis -->
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin:0">
|
||
KI-Tipps ersetzen keinen professionellen Hundetrainer. Bei Aggression oder starker Angst
|
||
wende dich an einen zertifizierten Trainer vor Ort.
|
||
</p>
|
||
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function _bindKiTrainer() {
|
||
const textarea = _container.querySelector('#ki-problem');
|
||
const charCount = _container.querySelector('#ki-char-count');
|
||
const submitBtn = _container.querySelector('#ki-submit');
|
||
const result = _container.querySelector('#ki-result');
|
||
if (!textarea || !submitBtn) return;
|
||
|
||
textarea.addEventListener('input', () => {
|
||
charCount.textContent = `${textarea.value.length} / 1000`;
|
||
});
|
||
|
||
submitBtn.addEventListener('click', async () => {
|
||
const problem = textarea.value.trim();
|
||
if (problem.length < 10) {
|
||
UI.toast('Bitte beschreibe das Problem etwas genauer.', 'warning');
|
||
return;
|
||
}
|
||
const rasse = _container.querySelector('#ki-rasse')?.value.trim() || null;
|
||
const alter = _container.querySelector('#ki-alter')?.value.trim() || null;
|
||
|
||
submitBtn.disabled = true;
|
||
submitBtn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg> Denke nach…`;
|
||
|
||
result.hidden = true;
|
||
result.innerHTML = '';
|
||
|
||
try {
|
||
const resp = await API.post('/ki/training', { problem, rasse, alter });
|
||
const text = resp.antwort || '';
|
||
|
||
// Render with simple markdown-like formatting (text already escaped by API)
|
||
const safeText = text
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
const html = safeText
|
||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||
.replace(/^(\d+)\. (.+)$/gm, '<li><strong>$1.</strong> $2</li>')
|
||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||
.replace(/\n\n/g, '</p><p>')
|
||
.replace(/\n/g, '<br>');
|
||
|
||
result.innerHTML = `
|
||
<div class="card" style="border-left:3px solid var(--c-primary)">
|
||
<div style="display:flex;gap:var(--space-2);align-items:center;margin-bottom:var(--space-3)">
|
||
<svg class="ph-icon" style="color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#robot"></use>
|
||
</svg>
|
||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||
Empfehlung des KI-Trainers
|
||
</span>
|
||
</div>
|
||
<div style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7">
|
||
<p>${html}</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
result.hidden = false;
|
||
} catch (err) {
|
||
UI.toast(err.message || 'KI momentan nicht verfügbar.', 'error');
|
||
} finally {
|
||
submitBtn.disabled = false;
|
||
submitBtn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg> Tipps holen`;
|
||
}
|
||
});
|
||
}
|
||
|
||
function _bindAccordions() {
|
||
_container.querySelectorAll('.ueb-acc-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = btn.dataset.acc;
|
||
const body = document.getElementById(id);
|
||
const chevron = _container.querySelector(`.ueb-chevron[data-acc="${id}"]`);
|
||
if (!body) return;
|
||
const isOpen = !body.hidden;
|
||
body.hidden = isOpen;
|
||
if (chevron) chevron.style.transform = isOpen ? '' : 'rotate(180deg)';
|
||
btn.querySelector('span').textContent = isOpen ? 'Anleitung anzeigen' : 'Anleitung ausblenden';
|
||
});
|
||
});
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// TRAININGSGRUNDLAGEN
|
||
// ----------------------------------------------------------
|
||
function _renderGrundlagen() {
|
||
return `
|
||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
|
||
|
||
<!-- Markerwort -->
|
||
<div class="card">
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin:0 0 var(--space-3)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#megaphone-simple"></use>
|
||
</svg>
|
||
Das Markerwort
|
||
</h3>
|
||
<p style="font-size:var(--text-sm);color:var(--c-text);line-height:1.6;margin:0 0 var(--space-3)">
|
||
Ein Markerwort (z.B. <strong>"Ja!"</strong> oder ein Klicker) signalisiert dem Hund den <strong>exakten Moment</strong>
|
||
des richtigen Verhaltens. Es überbrückt die Zeit bis das Leckerli in seinem Mund ist.
|
||
</p>
|
||
<ul style="margin:0;padding-left:var(--space-5);display:flex;flex-direction:column;gap:var(--space-2)">
|
||
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">
|
||
Einmalig einführen: Markerwort sagen → sofort Leckerli (10x wiederholen)
|
||
</li>
|
||
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">
|
||
Immer nur ein Markerwort verwenden
|
||
</li>
|
||
<li style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">
|
||
Das Leckerli kommt <strong>immer</strong> nach dem Markerwort — sonst verliert es seinen Wert
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<!-- Wann belohnen -->
|
||
<div class="card">
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin:0 0 var(--space-3)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#star"></use>
|
||
</svg>
|
||
Wann belohnen?
|
||
</h3>
|
||
<div style="overflow-x:auto">
|
||
<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">
|
||
<thead>
|
||
<tr style="background:var(--c-surface-2)">
|
||
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Phase</th>
|
||
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Belohnungshäufigkeit</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${[
|
||
['Neue Übung lernen', 'Jede korrekte Wiederholung'],
|
||
['Übung bekannt', 'Jede 2.–3. Wiederholung'],
|
||
['Übung gefestigt', 'Unregelmäßig (stärkt Motivation)'],
|
||
['Ablenkungstraining', 'Wieder häufiger (höhere Anforderung)'],
|
||
].map(([p, b], i) => `
|
||
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
|
||
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text);border-bottom:1px solid var(--c-border)">${_esc(p)}</td>
|
||
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${_esc(b)}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Leckerli-Hierarchie -->
|
||
<div class="card">
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin:0 0 var(--space-3)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#trophy"></use>
|
||
</svg>
|
||
Leckerli-Hierarchie
|
||
</h3>
|
||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5;margin:0 0 var(--space-3)">
|
||
Nicht alle Leckerlis sind gleich. Bei Ablenkung oder schwierigen Übungen brauchst du hochwertigere Belohnungen:
|
||
</p>
|
||
<div style="overflow-x:auto">
|
||
<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">
|
||
<thead>
|
||
<tr style="background:var(--c-surface-2)">
|
||
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Stufe</th>
|
||
<th style="padding:var(--space-2) var(--space-3);text-align:left;font-weight:var(--weight-semibold);
|
||
color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">Beispiele</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${[
|
||
['Niedrig', 'Trockenfutter, normale Hundekekse', '#22c55e'],
|
||
['Mittel', 'Käse, Wurst, Hühnchen (gekocht)', '#eab308'],
|
||
['Hoch', 'Leberwurst, Lachs, Pansen (für besondere Momente)', '#ef4444'],
|
||
].map(([s, b, c], i) => `
|
||
<tr style="${i % 2 === 1 ? 'background:var(--c-surface-2)' : ''}">
|
||
<td style="padding:var(--space-2) var(--space-3);border-bottom:1px solid var(--c-border)">
|
||
<span style="font-weight:var(--weight-semibold);color:${c}">${_esc(s)}</span>
|
||
</td>
|
||
<td style="padding:var(--space-2) var(--space-3);color:var(--c-text-secondary);border-bottom:1px solid var(--c-border)">${_esc(b)}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Trainingsregeln -->
|
||
<div class="card" style="margin-bottom:var(--space-4)">
|
||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||
color:var(--c-text);margin:0 0 var(--space-3)">
|
||
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#list-checks"></use>
|
||
</svg>
|
||
Trainingsregeln auf einen Blick
|
||
</h3>
|
||
<ul style="margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--space-2)">
|
||
${[
|
||
[true, 'Kurze Einheiten (5–10 Min), lieber mehrmals täglich'],
|
||
[true, 'Immer mit Erfolg beenden'],
|
||
[true, 'Ein Kommando — eine Bedeutung'],
|
||
[true, 'Konsequenz bei allen Haushaltsmitgliedern'],
|
||
[false, 'Nie bestrafen, schreien oder Zwang anwenden'],
|
||
[false, 'Kommando nicht wiederholen wenn der Hund nicht reagiert'],
|
||
[false, 'Nicht trainieren wenn Hund müde, krank oder aufgewühlt ist'],
|
||
].map(([ok, text]) => `
|
||
<li style="display:flex;gap:var(--space-2);align-items:flex-start">
|
||
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:2px;
|
||
color:${ok ? 'var(--c-success)' : 'var(--c-danger)'}" aria-hidden="true">
|
||
<use href="/icons/phosphor.svg#${ok ? 'check-circle' : 'x-circle'}"></use>
|
||
</svg>
|
||
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">${_esc(text)}</span>
|
||
</li>
|
||
`).join('')}
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// HELPER
|
||
// ----------------------------------------------------------
|
||
function _esc(str) {
|
||
if (!str) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// PUBLIC
|
||
// ----------------------------------------------------------
|
||
return { init, refresh, onDogChange };
|
||
|
||
})();
|