Session 2026-04-21: SEO, Wiki-Anreicherung, Training, Lober
SEO & Crawler:
- robots.txt, llms.txt, sitemap.xml (508 Seiten bei Google)
- SSR-Seiten: /info, /wiki/rassen, /wiki/rasse/{slug}, /knigge
- Open Graph, JSON-LD, Breadcrumbs in index.html
Navigation:
- Training unter "Mein Hund", Wissen collapsible
- Welcome-Seite und Landing-Page auf 5-Gruppen-Struktur
Wiki:
- KI-Anreicherung (Claude API): beschreibung, vorkommen_de, Steckbrief
- "So einen hab ich" / Züchter-Verzeichnis
- Scheduler: 50 Rassen beim Start, 20/Nacht
Training:
- Session-Logging (Erfolgsquote, Stimmung, Zufriedenheit)
- Virtueller KI-Trainer (6h-Cache)
- Trainingskalender (Habit-Tracker)
- Top-Training → automatischer Tagebucheintrag
- Gamification ohne Druck: Badges, Streak, Stats
Fortschritts-Lober:
- Jeden Montag 09:00: Claude schreibt Lob-Text pro Hund
- Push + Karte im Tagebuch
Monitoring:
- 4× täglich Status-Mail mit Scheduler-Status + Wiki-Fortschritt
This commit is contained in:
parent
65d1cf6c7f
commit
180de32e57
22 changed files with 4351 additions and 189 deletions
|
|
@ -4824,6 +4824,79 @@ html.modal-open {
|
|||
color: var(--c-text-secondary);
|
||||
}
|
||||
|
||||
/* Detail Hero (neues Layout) */
|
||||
.wiki-detail-hero-photo-wrap {
|
||||
width: 100%;
|
||||
max-height: 240px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: var(--space-3);
|
||||
background: var(--c-surface-2);
|
||||
}
|
||||
.wiki-detail-hero-photo-wrap .wiki-detail-photo {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
margin-bottom: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Steckbrief-Grid */
|
||||
.wiki-steckbrief-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1px;
|
||||
background: var(--c-border-light);
|
||||
border: 1px solid var(--c-border-light);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.wiki-steckbrief-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--c-surface);
|
||||
}
|
||||
.wiki-steckbrief-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--c-text-muted);
|
||||
font-weight: var(--weight-medium);
|
||||
}
|
||||
.wiki-steckbrief-value {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--c-text);
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
|
||||
/* Interesse-Section */
|
||||
.wiki-interesse-section {
|
||||
background: var(--c-surface-2);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3);
|
||||
}
|
||||
.wiki-interesse-btn {
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--c-surface);
|
||||
color: var(--c-text);
|
||||
font-size: var(--text-sm);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
.wiki-interesse-btn:hover {
|
||||
border-color: var(--c-primary);
|
||||
color: var(--c-primary);
|
||||
}
|
||||
|
||||
/* Züchter-Karten */
|
||||
.wiki-zuchter-card {
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.hdm-vote-rasse {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--c-text-secondary);
|
||||
|
|
|
|||
|
|
@ -393,6 +393,48 @@
|
|||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.sidebar-section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: var(--weight-semibold);
|
||||
color: var(--c-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: var(--space-3) var(--space-3) var(--space-1);
|
||||
margin-top: var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color var(--transition-fast);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.sidebar-section-toggle:hover { color: var(--c-text); }
|
||||
.sidebar-section-toggle .wissen-caret {
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sidebar-section-toggle[aria-expanded="true"] .wissen-caret {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.sidebar-section-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
transition: max-height 0.25s ease, opacity 0.2s ease;
|
||||
}
|
||||
.sidebar-section-body.open {
|
||||
max-height: 300px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,64 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#C4843A">
|
||||
<meta name="description" content="Ban Yaro — Die Hunde-Plattform. Alles rund um deinen Hund.">
|
||||
<meta name="description" content="Ban Yaro — Die kostenlose Hunde-App für Deutschland. Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community, Hundesitting. DSGVO-konform, ohne App Store.">
|
||||
<meta name="keywords" content="Hunde App, Hunde Tagebuch, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundesitting, Hunde Wiki, Hunderassen, PWA Hunde, DSGVO Hunde App">
|
||||
<link rel="canonical" href="https://banyaro.app/">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Ban Yaro — Die deutschsprachige Hunde-Plattform">
|
||||
<meta property="og:description" content="Alles rund um deinen Hund — Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community, Hundesitting. Kostenlos, DSGVO-konform, ohne App Store.">
|
||||
<meta property="og:url" content="https://banyaro.app/">
|
||||
<meta property="og:image" content="https://banyaro.app/icons/icon-512.png">
|
||||
<meta property="og:locale" content="de_DE">
|
||||
<meta property="og:site_name" content="Ban Yaro">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Ban Yaro — Die deutschsprachige Hunde-Plattform">
|
||||
<meta name="twitter:description" content="Alles rund um deinen Hund — Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community. Kostenlos, DSGVO-konform.">
|
||||
<meta name="twitter:image" content="https://banyaro.app/icons/icon-512.png">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "MobileApplication",
|
||||
"name": "Ban Yaro",
|
||||
"alternateName": "Ban Yaro — Die Hunde-Plattform",
|
||||
"description": "Ban Yaro ist die kostenlose, deutschsprachige All-in-One Hunde-App. Digitales Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community, Hundesitting und mehr — DSGVO-konform, ohne App Store.",
|
||||
"url": "https://banyaro.app",
|
||||
"applicationCategory": "LifestyleApplication",
|
||||
"applicationSubCategory": "PetApplication",
|
||||
"operatingSystem": "iOS, Android, Web",
|
||||
"inLanguage": "de",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Ban Yaro",
|
||||
"url": "https://banyaro.app"
|
||||
},
|
||||
"featureList": [
|
||||
"Digitales Hunde-Tagebuch mit Fotos und GPS",
|
||||
"Digitaler Impfpass und Gesundheitsakte",
|
||||
"Giftköder-Alarm mit Push-Benachrichtigungen",
|
||||
"Gassi-Community und GPS-Routen",
|
||||
"Hundesitting-Vermittlung mit 8% Provision",
|
||||
"NFC-Halsband-Tags",
|
||||
"Hunde-Wiki mit Rassendatenbank",
|
||||
"Verlorener Hund Alarm",
|
||||
"Forum für Hundebesitzer",
|
||||
"Offline-Modus via Service Worker"
|
||||
],
|
||||
"areaServed": ["DE", "AT", "CH"]
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
|
|
@ -61,6 +118,13 @@
|
|||
<div class="sidebar-item" data-page="health">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> Gesundheit
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="uebungen">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg> Übungen
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="trainingsplaene">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg> Trainingspläne
|
||||
</div>
|
||||
|
||||
<span class="sidebar-section-label">Entdecken</span>
|
||||
<div class="sidebar-item" data-page="map">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-trifold"></use></svg> Karte
|
||||
|
|
@ -105,26 +169,23 @@
|
|||
<span class="sidebar-item-badge" id="lost-badge" style="display:none">0</span>
|
||||
</div>
|
||||
|
||||
<span class="sidebar-section-label">Training</span>
|
||||
<div class="sidebar-item" data-page="uebungen">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg> Übungen
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="trainingsplaene">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg> Trainingspläne
|
||||
</div>
|
||||
|
||||
<span class="sidebar-section-label">Wissen</span>
|
||||
<div class="sidebar-item" data-page="wiki">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#books"></use></svg> Wiki
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="knigge">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#handshake"></use></svg> Knigge
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="movies">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#film-slate"></use></svg> Filme
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="erste-hilfe">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> Erste Hilfe
|
||||
<button class="sidebar-section-toggle" id="wissen-toggle" aria-expanded="false">
|
||||
<span>Wissen</span>
|
||||
<svg class="ph-icon wissen-caret" aria-hidden="true"><use href="/icons/phosphor.svg#caret-right"></use></svg>
|
||||
</button>
|
||||
<div class="sidebar-section-body" id="wissen-body">
|
||||
<div class="sidebar-item" data-page="wiki">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#books"></use></svg> Wiki
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="knigge">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#handshake"></use></svg> Knigge
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="movies">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#film-slate"></use></svg> Filme
|
||||
</div>
|
||||
<div class="sidebar-item" data-page="erste-hilfe">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg> Erste Hilfe
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-item" data-page="admin" id="sidebar-admin"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '262'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '267'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
@ -103,6 +103,7 @@ const App = (() => {
|
|||
|
||||
state.page = pageId;
|
||||
UI.scrollTop();
|
||||
_expandWissenIfActive(pageId);
|
||||
|
||||
// Seiten-Modul lazy laden (einmalig)
|
||||
_loadPage(pageId, params);
|
||||
|
|
@ -309,6 +310,12 @@ const App = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Wissen-Toggle aufklappen/zuklappen
|
||||
if (e.target.closest('#wissen-toggle')) {
|
||||
_toggleWissen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sidebar-Item auf Mobile → schließen nach Navigation
|
||||
if (e.target.closest('#sidebar .sidebar-item')) {
|
||||
_closeSidebar();
|
||||
|
|
@ -346,6 +353,22 @@ const App = (() => {
|
|||
document.getElementById('sidebar-backdrop')?.classList.remove('visible');
|
||||
}
|
||||
|
||||
const _WISSEN_PAGES = new Set(['wiki', 'knigge', 'movies', 'erste-hilfe']);
|
||||
|
||||
function _toggleWissen(force) {
|
||||
const toggle = document.getElementById('wissen-toggle');
|
||||
const body = document.getElementById('wissen-body');
|
||||
if (!toggle || !body) return;
|
||||
const open = force !== undefined ? force : toggle.getAttribute('aria-expanded') !== 'true';
|
||||
toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
body.classList.toggle('open', open);
|
||||
try { localStorage.setItem('by_wissen_open', open ? '1' : '0'); } catch (_) {}
|
||||
}
|
||||
|
||||
function _expandWissenIfActive(page) {
|
||||
if (_WISSEN_PAGES.has(page)) _toggleWissen(true);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// SCHNELL-HINZUFÜGEN (+ Button)
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -744,6 +767,12 @@ const App = (() => {
|
|||
}
|
||||
|
||||
_bindNavigation();
|
||||
|
||||
// Wissen-Sektion: gespeicherten Zustand wiederherstellen
|
||||
try {
|
||||
if (localStorage.getItem('by_wissen_open') === '1') _toggleWissen(true);
|
||||
} catch (_) {}
|
||||
|
||||
await _checkAuth();
|
||||
|
||||
// Einladungslink /teilen/{token} → direkt annehmen
|
||||
|
|
|
|||
|
|
@ -220,6 +220,62 @@ window.Page_diary = (() => {
|
|||
|
||||
await _load();
|
||||
_renderList();
|
||||
_loadPraise();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// FORTSCHRITTS-LOBER
|
||||
// ----------------------------------------------------------
|
||||
async function _loadPraise() {
|
||||
const dog = _appState.activeDog;
|
||||
if (!dog) return;
|
||||
|
||||
const existing = _container.querySelector('#diary-praise-card');
|
||||
if (existing) existing.remove();
|
||||
|
||||
let data;
|
||||
try {
|
||||
const r = await fetch(`/api/praise/current?dog_id=${dog.id}`, {credentials: 'include'});
|
||||
data = r.ok ? await r.json() : null;
|
||||
} catch (_) { return; }
|
||||
|
||||
if (!data?.praise) return;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.id = 'diary-praise-card';
|
||||
card.style.cssText = `
|
||||
margin: var(--space-3) var(--space-4) 0;
|
||||
background: linear-gradient(135deg, var(--c-primary-subtle), #fdf6ef);
|
||||
border: 1px solid var(--c-primary-light, #e8c99a);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: var(--space-4) var(--space-5);
|
||||
display: flex; gap: var(--space-3); align-items: flex-start;
|
||||
`;
|
||||
card.innerHTML = `
|
||||
<div style="font-size:1.8rem;flex-shrink:0;line-height:1">🐾</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||
color:var(--c-primary-dark);text-transform:uppercase;
|
||||
letter-spacing:.06em;margin-bottom:var(--space-1)">
|
||||
Rückblick der Woche
|
||||
</div>
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text);
|
||||
line-height:1.6;margin:0">${data.praise}</p>
|
||||
</div>
|
||||
<button id="diary-praise-close"
|
||||
style="background:none;border:none;cursor:pointer;padding:2px;
|
||||
color:var(--c-text-muted);flex-shrink:0;line-height:1;font-size:1.1rem"
|
||||
aria-label="Schließen">×</button>
|
||||
`;
|
||||
|
||||
const list = _container.querySelector('#diary-list');
|
||||
if (list) _container.insertBefore(card, list);
|
||||
|
||||
card.querySelector('#diary-praise-close')?.addEventListener('click', () => {
|
||||
card.style.opacity = '0';
|
||||
card.style.transition = 'opacity .2s';
|
||||
setTimeout(() => card.remove(), 200);
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@ window.Page_trainingsplaene = (() => {
|
|||
let _activePlan = 'welpe'; // welpe | junior | erwachsen
|
||||
let _activeAdultTab = 'grundkurs'; // grundkurs | aufbaukurs
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API HELPERS
|
||||
// ----------------------------------------------------------
|
||||
function _dogId() {
|
||||
return window.App?.state?.activeDogId || null;
|
||||
}
|
||||
|
||||
async function _apiGet(url) {
|
||||
const r = await fetch(url, {credentials: 'include'});
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// HELPER
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -590,9 +602,151 @@ window.Page_trainingsplaene = (() => {
|
|||
</h2>
|
||||
${_renderPlanSelector()}
|
||||
${planContent}
|
||||
<!-- Trainingskalender -->
|
||||
<div id="tp-calendar-section" style="margin-top:var(--space-6)">
|
||||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin:0 0 var(--space-3)">
|
||||
${_icon('calendar')} Trainingskalender
|
||||
</h3>
|
||||
<div id="tp-calendar-wrap" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||||
<div style="color:var(--c-text-secondary);font-size:var(--text-sm);text-align:center;padding:var(--space-4)">
|
||||
Lade Kalender…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
_bindEvents();
|
||||
_loadCalendar();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// TRAININGSKALENDER
|
||||
// ----------------------------------------------------------
|
||||
async function _loadCalendar() {
|
||||
const dogId = _dogId();
|
||||
const wrap = _container.querySelector('#tp-calendar-wrap');
|
||||
if (!wrap) return;
|
||||
|
||||
if (!dogId) {
|
||||
wrap.innerHTML = `<p style="font-size:var(--text-sm);color:var(--c-text-secondary);text-align:center;padding:var(--space-3)">Kein Hund ausgewählt.</p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const thisYear = now.getFullYear();
|
||||
const thisMon = now.getMonth() + 1; // 1-based
|
||||
|
||||
// Letzter Monat
|
||||
const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const prevYear = prevDate.getFullYear();
|
||||
const prevMon = prevDate.getMonth() + 1;
|
||||
|
||||
const [dataCurr, dataPrev] = await Promise.all([
|
||||
_apiGet(`/api/training/calendar?dog_id=${dogId}&year=${thisYear}&month=${thisMon}`),
|
||||
_apiGet(`/api/training/calendar?dog_id=${dogId}&year=${prevYear}&month=${prevMon}`),
|
||||
]);
|
||||
|
||||
wrap.innerHTML = `
|
||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-4)">
|
||||
${_renderCalendarMonth(prevYear, prevMon, dataPrev?.days || {})}
|
||||
${_renderCalendarMonth(thisYear, thisMon, dataCurr?.days || {})}
|
||||
</div>
|
||||
<!-- Legende -->
|
||||
<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;margin-top:var(--space-2);
|
||||
font-size:var(--text-xs);color:var(--c-text-secondary);align-items:center">
|
||||
<span style="display:flex;align-items:center;gap:5px">
|
||||
<span style="width:14px;height:14px;border-radius:3px;border:1.5px solid var(--c-border);display:inline-block"></span>
|
||||
kein Training
|
||||
</span>
|
||||
<span style="display:flex;align-items:center;gap:5px">
|
||||
<span style="width:14px;height:14px;border-radius:3px;background:var(--c-primary-subtle);border:1.5px solid var(--c-primary);display:inline-block"></span>
|
||||
Training
|
||||
</span>
|
||||
<span style="display:flex;align-items:center;gap:5px">
|
||||
<span style="width:14px;height:14px;border-radius:3px;background:var(--c-primary);display:inline-block"></span>
|
||||
Top-Training
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _renderCalendarMonth(year, month, days) {
|
||||
const MONTHS = ['Januar','Februar','März','April','Mai','Juni',
|
||||
'Juli','August','September','Oktober','November','Dezember'];
|
||||
const WDAYS = ['Mo','Di','Mi','Do','Fr','Sa','So'];
|
||||
|
||||
const firstDay = new Date(year, month - 1, 1);
|
||||
// Monday = 0 offset: getDay() returns 0=Sun, so we shift
|
||||
let startOffset = firstDay.getDay(); // 0=Sun
|
||||
startOffset = startOffset === 0 ? 6 : startOffset - 1; // Mon=0 … Sun=6
|
||||
|
||||
const daysInMonth = new Date(year, month, 0).getDate();
|
||||
|
||||
// Header Wochentage
|
||||
const wdayHeader = WDAYS.map(d =>
|
||||
`<div style="text-align:center;font-size:10px;font-weight:var(--weight-semibold);
|
||||
color:var(--c-text-secondary);padding-bottom:4px">${d}</div>`
|
||||
).join('');
|
||||
|
||||
// Leere Slots am Anfang
|
||||
let cells = '';
|
||||
for (let i = 0; i < startOffset; i++) {
|
||||
cells += `<div></div>`;
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
const todayStr = `${today.getFullYear()}-${String(today.getMonth()+1).padStart(2,'0')}-${String(today.getDate()).padStart(2,'0')}`;
|
||||
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
const dateStr = `${year}-${String(month).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
|
||||
const dayData = days[dateStr];
|
||||
const isToday = dateStr === todayStr;
|
||||
|
||||
let bg = 'transparent';
|
||||
let border = '1.5px solid var(--c-border)';
|
||||
let color = 'var(--c-text-secondary)';
|
||||
let title = '';
|
||||
|
||||
if (dayData) {
|
||||
if (dayData.top) {
|
||||
bg = 'var(--c-primary)';
|
||||
border = '1.5px solid var(--c-primary)';
|
||||
color = '#fff';
|
||||
title = `Top-Training! ${dayData.count} Einheit${dayData.count !== 1 ? 'en' : ''}`;
|
||||
} else {
|
||||
bg = 'var(--c-primary-subtle)';
|
||||
border = '1.5px solid var(--c-primary)';
|
||||
color = 'var(--c-primary)';
|
||||
title = `${dayData.count} Einheit${dayData.count !== 1 ? 'en' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
const todayRing = isToday ? 'box-shadow:0 0 0 2px var(--c-primary);' : '';
|
||||
|
||||
cells += `
|
||||
<div title="${title}"
|
||||
style="aspect-ratio:1;border-radius:4px;background:${bg};border:${border};
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:10px;font-weight:${dayData ? 'var(--weight-semibold)' : '400'};
|
||||
color:${color};${todayRing}cursor:default">
|
||||
${d}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div style="flex:1;min-width:220px">
|
||||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">
|
||||
${_esc(MONTHS[month - 1])} ${year}
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:3px">
|
||||
${wdayHeader}
|
||||
${cells}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -9,6 +9,34 @@ window.Page_uebungen = (() => {
|
|||
let _appState = null;
|
||||
let _activeTab = 'grundkommandos';
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API HELPERS
|
||||
// ----------------------------------------------------------
|
||||
function _dogId() {
|
||||
return window.App?.state?.activeDogId || null;
|
||||
}
|
||||
|
||||
async function _apiPost(url, body) {
|
||||
const r = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
async function _apiGet(url) {
|
||||
const r = await fetch(url, {credentials: 'include'});
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STATS STATE
|
||||
// ----------------------------------------------------------
|
||||
let _statsData = null; // cached stats from /api/training/stats
|
||||
let _badgesData = null; // cached badges from /api/achievements
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// DATEN
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -387,10 +415,29 @@ window.Page_uebungen = (() => {
|
|||
API.training.getSuggestions().then(suggestions => {
|
||||
if (suggestions.length) _showSuggestions(suggestions);
|
||||
}).catch(() => {});
|
||||
|
||||
// Stats + Badges laden
|
||||
_loadStatsAndBadges();
|
||||
}
|
||||
|
||||
async function _loadStatsAndBadges() {
|
||||
const dogId = _dogId();
|
||||
if (!dogId) return;
|
||||
const [stats, achievements] = await Promise.all([
|
||||
_apiGet(`/api/training/stats?dog_id=${dogId}`),
|
||||
_apiGet('/api/achievements'),
|
||||
]);
|
||||
_statsData = stats;
|
||||
_badgesData = achievements;
|
||||
_renderStatsBanner();
|
||||
}
|
||||
|
||||
function refresh() {}
|
||||
function onDogChange() {}
|
||||
function onDogChange() {
|
||||
_statsData = null;
|
||||
_badgesData = null;
|
||||
_loadStatsAndBadges();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// HAUPT-RENDER
|
||||
|
|
@ -399,12 +446,64 @@ window.Page_uebungen = (() => {
|
|||
_container.innerHTML = `
|
||||
<div id="ueb-wrap">
|
||||
${_renderTabs()}
|
||||
<div id="ueb-stats-banner" style="padding:0 var(--space-4);margin-bottom:var(--space-2)"></div>
|
||||
<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();
|
||||
_renderStatsBanner();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STATS BANNER
|
||||
// ----------------------------------------------------------
|
||||
function _renderStatsBanner() {
|
||||
const el = _container && _container.querySelector('#ueb-stats-banner');
|
||||
if (!el) return;
|
||||
if (!_statsData || !_statsData.total_sessions) { el.innerHTML = ''; return; }
|
||||
|
||||
const s = _statsData;
|
||||
const streakHtml = (s.streak_days >= 2)
|
||||
? ` · ${s.streak_days}-Tage-Streak 🔥`
|
||||
: '';
|
||||
const avgHtml = s.avg_erfolgsquote != null
|
||||
? ` · Ø ${Math.round(s.avg_erfolgsquote)}% Erfolg`
|
||||
: '';
|
||||
|
||||
// Training-Badges filtern
|
||||
let badgesHtml = '';
|
||||
if (_badgesData && Array.isArray(_badgesData.user_badges)) {
|
||||
const trainingBadges = _badgesData.user_badges.filter(b => b.badge_id && b.badge_id.startsWith('training_'));
|
||||
if (trainingBadges.length) {
|
||||
const visible = trainingBadges.slice(0, 3);
|
||||
const rest = trainingBadges.length - visible.length;
|
||||
const pills = visible.map(b => `
|
||||
<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;
|
||||
border-radius:var(--radius-full,999px);
|
||||
background:var(--c-primary-subtle);color:var(--c-primary);
|
||||
font-size:var(--text-xs);font-weight:var(--weight-semibold)">
|
||||
${b.icon || '🏅'} ${_esc(b.name || b.badge_id)}
|
||||
</span>
|
||||
`).join('');
|
||||
const more = rest > 0
|
||||
? `<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">+${rest} weitere</span>`
|
||||
: '';
|
||||
badgesHtml = `<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-2)">${pills}${more}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="background:var(--c-surface-2);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);
|
||||
font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
<span style="font-weight:var(--weight-semibold);color:var(--c-text)">
|
||||
${s.total_sessions} Einheit${s.total_sessions !== 1 ? 'en' : ''}
|
||||
</span>${avgHtml}${streakHtml}
|
||||
${badgesHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _renderTabs() {
|
||||
|
|
@ -492,7 +591,8 @@ window.Page_uebungen = (() => {
|
|||
}
|
||||
_bindAccordions();
|
||||
_bindStatusButtons();
|
||||
if (_activeTab === 'ki-trainer') _bindKiTrainer();
|
||||
_bindLogButtons();
|
||||
if (_activeTab === 'ki-trainer') _loadKiTrainerFeedback();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -523,6 +623,21 @@ window.Page_uebungen = (() => {
|
|||
${_esc(u.name)}
|
||||
</span>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||||
<!-- Log-Button -->
|
||||
<button class="ueb-log-btn"
|
||||
data-tab="${_esc(_activeTab)}"
|
||||
data-name="${_esc(u.name)}"
|
||||
title="Trainingseinheit loggen"
|
||||
style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
|
||||
color:var(--c-primary);cursor:pointer;padding:2px 7px;
|
||||
display:flex;align-items:center;gap:3px;
|
||||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||
border-radius:var(--radius-sm)">
|
||||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#plus"></use>
|
||||
</svg>
|
||||
Einheit
|
||||
</button>
|
||||
<!-- Status-Button -->
|
||||
<button class="ueb-status-btn"
|
||||
data-tab="${_esc(_activeTab)}"
|
||||
|
|
@ -652,143 +767,432 @@ window.Page_uebungen = (() => {
|
|||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-TRAINER
|
||||
// LOG SESSION MODAL
|
||||
// ----------------------------------------------------------
|
||||
function _bindLogButtons() {
|
||||
_container.querySelectorAll('.ueb-log-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
_openLogModal(btn.dataset.tab, btn.dataset.name);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _openLogModal(tab, exerciseName) {
|
||||
// Build the modal HTML
|
||||
const modalId = 'ueb-log-modal';
|
||||
const formId = 'ueb-log-form';
|
||||
// Remove existing if present
|
||||
document.getElementById(modalId)?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = modalId;
|
||||
overlay.style.cssText = `
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
display:flex;align-items:flex-end;justify-content:center;
|
||||
background:rgba(0,0,0,0.45);
|
||||
`;
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
width:100%;max-width:480px;max-height:92vh;overflow-y:auto;
|
||||
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
|
||||
<!-- Handle -->
|
||||
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
|
||||
|
||||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
|
||||
margin:0 0 var(--space-4);text-align:center">
|
||||
Einheit loggen: ${_esc(exerciseName)}
|
||||
</h3>
|
||||
|
||||
<form id="${formId}" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||||
|
||||
<!-- Wiederholungen -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Wiederholungen</label>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);justify-content:center">
|
||||
<button type="button" id="ueb-rep-minus"
|
||||
style="width:36px;height:36px;border-radius:50%;border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);font-size:1.2rem;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;color:var(--c-text)">−</button>
|
||||
<span id="ueb-rep-val" style="font-size:var(--text-xl);font-weight:700;color:var(--c-text);min-width:32px;text-align:center">5</span>
|
||||
<button type="button" id="ueb-rep-plus"
|
||||
style="width:36px;height:36px;border-radius:50%;border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);font-size:1.2rem;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;color:var(--c-text)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wie lief's -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Wie lief's?</label>
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:center;flex-wrap:wrap">
|
||||
${[['😓','0'],['😐','25'],['🙂','50'],['😊','75'],['🎉','100']].map(([emoji, val]) => `
|
||||
<button type="button" class="ueb-erfolg-btn"
|
||||
data-val="${val}"
|
||||
style="font-size:1.5rem;padding:var(--space-2) var(--space-3);
|
||||
border-radius:var(--radius-md);border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);cursor:pointer;transition:all 0.15s"
|
||||
title="${val}%">${emoji}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stimmung des Hundes -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes</label>
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:center;flex-wrap:wrap">
|
||||
${[['🎯','aufmerksam'],['😴','müde'],['🌪️','abgelenkt'],['⚡','super']].map(([emoji, val]) => `
|
||||
<button type="button" class="ueb-stimmung-btn"
|
||||
data-val="${val}"
|
||||
style="display:flex;flex-direction:column;align-items:center;gap:2px;
|
||||
font-size:1.2rem;padding:var(--space-2);min-width:60px;
|
||||
border-radius:var(--radius-md);border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);cursor:pointer;transition:all 0.15s">
|
||||
${emoji}
|
||||
<span style="font-size:9px;color:var(--c-text-secondary)">${_esc(val)}</span>
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zufriedenheit -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Wie zufrieden bist du?</label>
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:center">
|
||||
${[1,2,3,4,5].map(n => `
|
||||
<button type="button" class="ueb-stern-btn"
|
||||
data-val="${n}"
|
||||
style="font-size:1.5rem;background:none;border:none;cursor:pointer;
|
||||
padding:2px;opacity:0.35;transition:opacity 0.15s">⭐</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notiz -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Notiz</label>
|
||||
<textarea id="ueb-log-notiz" rows="2" placeholder="Optional: Was ist aufgefallen?"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;resize:none;
|
||||
background:var(--c-surface);color:var(--c-text);line-height:1.5"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Meilenstein-Checkbox (initially hidden) -->
|
||||
<label id="ueb-log-milestone-wrap" hidden
|
||||
style="display:flex;align-items:flex-start;gap:var(--space-2);cursor:pointer;
|
||||
padding:var(--space-3);background:var(--c-primary-subtle);
|
||||
border:1px solid var(--c-primary-light);border-radius:var(--radius-md)">
|
||||
<input type="checkbox" id="ueb-log-milestone"
|
||||
style="margin-top:2px;flex-shrink:0;accent-color:var(--c-primary);width:16px;height:16px">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
|
||||
📖 Als Meilenstein ins Tagebuch eintragen
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Footer Buttons -->
|
||||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
|
||||
<button id="ueb-log-cancel"
|
||||
style="flex:1;padding:var(--space-3);border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);cursor:pointer;color:var(--c-text)">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button id="ueb-log-save" form="${formId}"
|
||||
class="btn btn-primary" style="flex:2"
|
||||
type="button">
|
||||
Einheit speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// State
|
||||
let wiederholungen = 5;
|
||||
let erfolgsquote = null; // must be selected
|
||||
let stimmung = null;
|
||||
let zufriedenheit = null;
|
||||
|
||||
// Close helpers
|
||||
function _closeModal() { overlay.remove(); }
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _closeModal(); });
|
||||
overlay.querySelector('#ueb-log-cancel').addEventListener('click', _closeModal);
|
||||
|
||||
// Stepper
|
||||
const repVal = overlay.querySelector('#ueb-rep-val');
|
||||
overlay.querySelector('#ueb-rep-minus').addEventListener('click', () => {
|
||||
if (wiederholungen > 1) { wiederholungen--; repVal.textContent = wiederholungen; }
|
||||
});
|
||||
overlay.querySelector('#ueb-rep-plus').addEventListener('click', () => {
|
||||
wiederholungen++;
|
||||
repVal.textContent = wiederholungen;
|
||||
});
|
||||
|
||||
// Erfolg-Buttons
|
||||
overlay.querySelectorAll('.ueb-erfolg-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
erfolgsquote = parseInt(btn.dataset.val, 10);
|
||||
overlay.querySelectorAll('.ueb-erfolg-btn').forEach(b => {
|
||||
b.style.background = 'var(--c-surface-2)';
|
||||
b.style.borderColor = 'var(--c-border)';
|
||||
b.style.transform = '';
|
||||
});
|
||||
btn.style.background = 'var(--c-primary-subtle)';
|
||||
btn.style.borderColor = 'var(--c-primary)';
|
||||
btn.style.transform = 'scale(1.15)';
|
||||
_checkMilestoneVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
// Stimmung-Buttons
|
||||
overlay.querySelectorAll('.ueb-stimmung-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
stimmung = btn.dataset.val;
|
||||
overlay.querySelectorAll('.ueb-stimmung-btn').forEach(b => {
|
||||
b.style.background = 'var(--c-surface-2)';
|
||||
b.style.borderColor = 'var(--c-border)';
|
||||
});
|
||||
btn.style.background = 'var(--c-primary-subtle)';
|
||||
btn.style.borderColor = 'var(--c-primary)';
|
||||
});
|
||||
});
|
||||
|
||||
// Stern-Buttons
|
||||
overlay.querySelectorAll('.ueb-stern-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
zufriedenheit = parseInt(btn.dataset.val, 10);
|
||||
overlay.querySelectorAll('.ueb-stern-btn').forEach(b => {
|
||||
b.style.opacity = parseInt(b.dataset.val, 10) <= zufriedenheit ? '1' : '0.35';
|
||||
});
|
||||
_checkMilestoneVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
function _checkMilestoneVisibility() {
|
||||
const wrap = overlay.querySelector('#ueb-log-milestone-wrap');
|
||||
if (!wrap) return;
|
||||
const show = erfolgsquote != null && erfolgsquote >= 75 && zufriedenheit != null && zufriedenheit >= 4;
|
||||
wrap.hidden = !show;
|
||||
}
|
||||
|
||||
// Save
|
||||
overlay.querySelector('#ueb-log-save').addEventListener('click', async () => {
|
||||
const dogId = _dogId();
|
||||
if (!dogId) { UI.toast('Kein Hund ausgewählt.', 'warning'); return; }
|
||||
if (erfolgsquote === null) { UI.toast('Bitte wähle aus, wie es gelaufen ist.', 'warning'); return; }
|
||||
|
||||
const saveBtn = overlay.querySelector('#ueb-log-save');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichern…';
|
||||
|
||||
const exerciseId = `${tab}_${exerciseName.replace(/[\s/]+/g, '_')}`;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const tagebuch = !overlay.querySelector('#ueb-log-milestone-wrap').hidden &&
|
||||
overlay.querySelector('#ueb-log-milestone').checked;
|
||||
|
||||
const body = {
|
||||
dog_id: dogId,
|
||||
exercise_id: exerciseId,
|
||||
exercise_name: exerciseName,
|
||||
datum: today,
|
||||
wiederholungen: wiederholungen,
|
||||
erfolgsquote: erfolgsquote,
|
||||
hund_stimmung: stimmung || null,
|
||||
zufriedenheit: zufriedenheit || null,
|
||||
notiz: overlay.querySelector('#ueb-log-notiz').value.trim() || null,
|
||||
tagebuch_eintrag: tagebuch,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await _apiPost('/api/training/sessions', body);
|
||||
_closeModal();
|
||||
if (!resp) { UI.toast.error('Speichern fehlgeschlagen.'); return; }
|
||||
|
||||
if (resp.ist_top) {
|
||||
UI.toast.success('🎉 Top-Training! Das war eine eurer besten Einheiten.');
|
||||
} else {
|
||||
UI.toast.success('Einheit gespeichert!');
|
||||
}
|
||||
|
||||
if (resp.badges && resp.badges.length) {
|
||||
resp.badges.forEach((badge, idx) => {
|
||||
setTimeout(() => {
|
||||
UI.toast.success(`🏅 Neues Abzeichen: "${badge.name || badge}"!`);
|
||||
}, 1000 * (idx + 1));
|
||||
});
|
||||
}
|
||||
|
||||
if (resp.diary_entry_id) {
|
||||
setTimeout(() => {
|
||||
UI.toast.success('📖 Als Meilenstein im Tagebuch gespeichert.');
|
||||
}, resp.badges?.length ? (resp.badges.length + 1) * 1000 : 1000);
|
||||
}
|
||||
|
||||
// Stats-Banner aktualisieren
|
||||
_statsData = null;
|
||||
_loadStatsAndBadges();
|
||||
} catch (err) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Einheit speichern';
|
||||
UI.toast.error('Speichern fehlgeschlagen.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-TRAINER (neu: hundebasiertes Feedback)
|
||||
// ----------------------------------------------------------
|
||||
function _renderKiTrainer() {
|
||||
return `
|
||||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
|
||||
const dogId = _dogId();
|
||||
if (!dogId) {
|
||||
return `
|
||||
<div style="padding:var(--space-6) var(--space-4);text-align:center;color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" style="width:40px;height:40px;margin-bottom:var(--space-3);color:var(--c-border)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#robot"></use>
|
||||
</svg>
|
||||
<p style="font-size:var(--text-sm);margin:0">Wähle einen Hund aus um den KI-Trainer zu nutzen.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
<!-- 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">
|
||||
return `
|
||||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)" id="ki-trainer-panel">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
<div style="width:48px;height:48px;border-radius:50%;background:var(--c-primary-subtle);
|
||||
display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:26px;height:26px;color:var(--c-primary)" 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 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||||
KI-Trainer
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
Personalisiertes Feedback basierend auf deinen Trainingseinheiten
|
||||
</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>
|
||||
<!-- Lade-Spinner -->
|
||||
<div id="ki-loading" style="text-align:center;padding:var(--space-6);color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" style="width:32px;height:32px;animation:spin 1s linear infinite;margin-bottom:var(--space-2)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#spinner-gap"></use>
|
||||
</svg>
|
||||
<p style="font-size:var(--text-sm);margin:0">Lade KI-Feedback…</p>
|
||||
</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
|
||||
<!-- Kein Sessions-Hinweis -->
|
||||
<div id="ki-no-sessions" hidden
|
||||
style="text-align:center;padding:var(--space-6) var(--space-4);color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" style="width:40px;height:40px;margin-bottom:var(--space-3);color:var(--c-border)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#clipboard-text"></use>
|
||||
</svg>
|
||||
<p style="font-size:var(--text-sm);margin:0">
|
||||
Logge deine erste Trainingseinheit um KI-Feedback zu erhalten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feedback-Card -->
|
||||
<div id="ki-feedback-card" hidden>
|
||||
<div class="card" style="border-left:3px solid var(--c-primary);background:var(--c-surface)">
|
||||
<div id="ki-feedback-text"
|
||||
style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;white-space:pre-wrap"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:var(--space-3)">
|
||||
<span id="ki-feedback-meta" style="font-size:var(--text-xs);color:var(--c-text-muted)"></span>
|
||||
<button id="ki-regenerate"
|
||||
style="font-size:var(--text-xs);padding:var(--space-1) var(--space-3);
|
||||
border-radius:var(--radius-md);border:1px solid var(--c-border);
|
||||
background:var(--c-surface-2);cursor:pointer;color:var(--c-text-secondary)">
|
||||
Neu generieren
|
||||
</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.
|
||||
KI-Tipps ersetzen keinen professionellen Hundetrainer.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
async function _loadKiTrainerFeedback(forceRefresh) {
|
||||
const loading = _container.querySelector('#ki-loading');
|
||||
const noSessions = _container.querySelector('#ki-no-sessions');
|
||||
const feedbackCard = _container.querySelector('#ki-feedback-card');
|
||||
const feedbackText = _container.querySelector('#ki-feedback-text');
|
||||
const feedbackMeta = _container.querySelector('#ki-feedback-meta');
|
||||
const regenBtn = _container.querySelector('#ki-regenerate');
|
||||
if (!loading) return; // not on ki-trainer tab
|
||||
|
||||
textarea.addEventListener('input', () => {
|
||||
charCount.textContent = `${textarea.value.length} / 1000`;
|
||||
});
|
||||
const dogId = _dogId();
|
||||
if (!dogId) return;
|
||||
|
||||
submitBtn.addEventListener('click', async () => {
|
||||
const problem = textarea.value.trim();
|
||||
if (problem.length < 10) {
|
||||
UI.toast('Bitte beschreibe das Problem etwas genauer.', 'warning');
|
||||
// Show loading
|
||||
loading.hidden = false;
|
||||
noSessions.hidden = true;
|
||||
feedbackCard.hidden = true;
|
||||
|
||||
// Check if there are any sessions
|
||||
const stats = _statsData || await _apiGet(`/api/training/stats?dog_id=${dogId}`);
|
||||
if (!stats || !stats.total_sessions) {
|
||||
loading.hidden = true;
|
||||
noSessions.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await _apiPost('/api/training/ki-feedback', { dog_id: dogId });
|
||||
loading.hidden = true;
|
||||
if (!resp || !resp.feedback) {
|
||||
noSessions.hidden = false;
|
||||
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`;
|
||||
feedbackText.textContent = resp.feedback;
|
||||
const cachedInfo = resp.cached ? 'Gespeichertes Feedback' : 'Gerade generiert';
|
||||
const sessionInfo = stats.total_sessions
|
||||
? ` · Basiert auf ${stats.total_sessions} Einheit${stats.total_sessions !== 1 ? 'en' : ''}`
|
||||
: '';
|
||||
feedbackMeta.textContent = `Aktualisiert alle 6 Stunden · ${cachedInfo}${sessionInfo}`;
|
||||
if (regenBtn) {
|
||||
regenBtn.textContent = resp.cached ? 'Neu generieren' : 'Aktualisieren';
|
||||
regenBtn.style.opacity = resp.cached ? '0.6' : '1';
|
||||
}
|
||||
});
|
||||
feedbackCard.hidden = false;
|
||||
} catch (err) {
|
||||
loading.hidden = true;
|
||||
noSessions.hidden = false;
|
||||
}
|
||||
|
||||
// Bind regenerate button
|
||||
if (regenBtn) {
|
||||
regenBtn.addEventListener('click', async () => {
|
||||
regenBtn.disabled = true;
|
||||
regenBtn.textContent = 'Generiere…';
|
||||
loading.hidden = false;
|
||||
feedbackCard.hidden = true;
|
||||
await _loadKiTrainerFeedback(true);
|
||||
regenBtn.disabled = false;
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
function _bindAccordions() {
|
||||
|
|
|
|||
|
|
@ -70,27 +70,41 @@ window.Page_welcome = (() => {
|
|||
|
||||
<!-- Features: Mein Hund -->
|
||||
${_featureCard('Mein Hund', [
|
||||
['book-open', 'Tagebuch', 'Momente, Fotos und Meilensteine festhalten', 'diary'],
|
||||
['syringe', 'Gesundheit', 'Impfungen, Tierarztbesuche & Medikamente', 'health'],
|
||||
['target', 'Training', 'Übungen, Pläne und KI-Trainer', 'uebungen'],
|
||||
['books', 'Wiki & Wissen', 'Rassen, Ernährung, Erste Hilfe', 'wiki'],
|
||||
])}
|
||||
|
||||
<!-- Features: Community -->
|
||||
${_featureCard('Community', [
|
||||
['users', 'Freunde', 'Verbinde dich mit anderen Hundebesitzern', 'friends'],
|
||||
['chat-circle-dots', 'Nachrichten', 'Private Chats mit deinen Freunden', 'chat'],
|
||||
['push-pin', 'Forum', 'Diskussionen, Tipps und Austausch', 'forum'],
|
||||
['paw-print', 'Gassi-Treffen', 'Hunde-Dates mit anderen Besitzern', 'walks'],
|
||||
['house-line', 'Sitting', 'Dogsitter finden oder selbst anbieten', 'sitting'],
|
||||
['magnifying-glass', 'Verlorene Hunde','Hilf gesuchte Hunde zu finden', 'lost'],
|
||||
['book-open', 'Tagebuch', 'Momente, Fotos und Meilensteine festhalten', 'diary'],
|
||||
['first-aid', 'Gesundheit', 'Impfungen, Tierarztbesuche & Medikamente', 'health'],
|
||||
['target', 'Übungen', 'Trainingsübungen mit KI-Unterstützung', 'uebungen'],
|
||||
['clipboard-text', 'Trainingspläne', 'Strukturierte Pläne für jedes Lernziel', 'trainingsplaene'],
|
||||
])}
|
||||
|
||||
<!-- Features: Entdecken -->
|
||||
${_featureCard('Entdecken', [
|
||||
['map-trifold', 'Karte & Routen', 'Hundefreundliche Orte und Spazierwege', 'map'],
|
||||
['calendar-dots', 'Events', 'Veranstaltungen in deiner Nähe', 'events'],
|
||||
['warning-octagon','Giftköder-Alarm', 'Community-Warnungen in deiner Nähe', 'poison'],
|
||||
['map-trifold', 'Karte', 'Orte, Routen und Meldungen in der Nähe', 'map'],
|
||||
['path', 'Routen', 'GPS-Routen aufzeichnen und bewerten', 'routes'],
|
||||
['calendar-dots', 'Events', 'Turniere und Veranstaltungen', 'events'],
|
||||
])}
|
||||
|
||||
<!-- Features: Soziales -->
|
||||
${_featureCard('Soziales', [
|
||||
['users', 'Freunde', 'Verbinde dich mit anderen Hundebesitzern', 'friends'],
|
||||
['chat-circle-dots', 'Nachrichten', 'Private Chats mit deinen Freunden', 'chat'],
|
||||
['bell', 'Aktuelles', 'Benachrichtigungen und Neuigkeiten', 'notifications'],
|
||||
])}
|
||||
|
||||
<!-- Features: Community -->
|
||||
${_featureCard('Community', [
|
||||
['warning-octagon', 'Giftköder-Alarm', 'Warnungen sofort melden und empfangen', 'poison'],
|
||||
['paw-print', 'Gassi-Treffen', 'Hunde-Dates mit anderen Besitzern', 'walks'],
|
||||
['house-line', 'Sitting', 'Sitter finden oder selbst anbieten', 'sitting'],
|
||||
['push-pin', 'Forum', 'Diskussionen, Tipps und Austausch', 'forum'],
|
||||
['magnifying-glass', 'Verlorene Hunde', 'Hilf vermisste Hunde zu finden', 'lost'],
|
||||
])}
|
||||
|
||||
<!-- Features: Wissen -->
|
||||
${_featureCard('Wissen', [
|
||||
['books', 'Wiki', 'Rassendatenbank, Gesundheits-Wiki, Quiz', 'wiki'],
|
||||
['handshake', 'Knigge', 'Regeln, Begegnungen, Leinenpflicht', 'knigge'],
|
||||
['film-slate', 'Filme', 'Stirbt der Hund? Die wichtigste Frage', 'movies'],
|
||||
['first-aid', 'Erste Hilfe','Notfallratgeber für häufige Situationen', 'erste-hilfe'],
|
||||
])}
|
||||
|
||||
<!-- App installieren -->
|
||||
|
|
|
|||
|
|
@ -393,6 +393,313 @@ window.Page_wiki = (() => {
|
|||
`;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API-Funktionen: Interesse / Stats / Züchter
|
||||
// ----------------------------------------------------------
|
||||
async function _fetchStats(slug) {
|
||||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/stats`, { credentials: 'include' });
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
async function _setInteresse(slug, typ) {
|
||||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/interesse`, {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ typ }),
|
||||
});
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
async function _deleteInteresse(slug) {
|
||||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/interesse`, {
|
||||
method: 'DELETE', credentials: 'include',
|
||||
});
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
async function _fetchZuchter(slug) {
|
||||
const r = await fetch(`/api/wiki/rassen/${encodeURIComponent(slug)}/zuchter`);
|
||||
return r.ok ? r.json() : [];
|
||||
}
|
||||
|
||||
async function _submitZuchter(data) {
|
||||
const r = await fetch('/api/wiki/zuchter', {
|
||||
method: 'POST', credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return r.ok;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Render-Helfer: Steckbrief-Grid
|
||||
// ----------------------------------------------------------
|
||||
function _renderSteckbriefGrid(rasse) {
|
||||
const gewicht = (rasse.gewicht_min_kg && rasse.gewicht_max_kg)
|
||||
? `${rasse.gewicht_min_kg}–${rasse.gewicht_max_kg} kg`
|
||||
: (rasse.gewicht_max_kg ? `bis ${rasse.gewicht_max_kg} kg` : '—');
|
||||
|
||||
const kinderLabel = rasse.kinder_geeignet === true
|
||||
? `<span style="color:var(--c-success)">✓ Ja</span>`
|
||||
: rasse.kinder_geeignet === false
|
||||
? `<span style="color:var(--c-warning)">⚡ Bedingt</span>`
|
||||
: '—';
|
||||
|
||||
const wohnungLabel = rasse.wohnung_geeignet
|
||||
? `<span style="color:var(--c-success)">✓ Ja</span>`
|
||||
: `<span style="color:var(--c-text-secondary)">✗ Besser Garten</span>`;
|
||||
|
||||
const rows = [
|
||||
['Größe', _groesseLabel(rasse.groesse) || '—'],
|
||||
['Gewicht', gewicht],
|
||||
['Lebensdauer', _esc(rasse.lebensdauer) || '—'],
|
||||
['Aktivität', _aktivLabel(rasse.aktivitaet) || '—'],
|
||||
['Eignung', _erfahrungLabel(rasse.erfahrung) || '—'],
|
||||
['Kinder', kinderLabel],
|
||||
['Wohnung', wohnungLabel],
|
||||
['FCI-Gruppe', _esc(rasse.gruppe) || '—'],
|
||||
];
|
||||
|
||||
return `
|
||||
<div class="wiki-steckbrief-grid">
|
||||
${rows.map(([label, val]) => `
|
||||
<div class="wiki-steckbrief-item">
|
||||
<span class="wiki-steckbrief-label">${label}</span>
|
||||
<span class="wiki-steckbrief-value">${val}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Render-Helfer: Interesse-Section (Social)
|
||||
// ----------------------------------------------------------
|
||||
function _renderInteresseSection(stats, slug) {
|
||||
const hatCount = stats?.dogs_count ?? '–';
|
||||
const willCount = stats?.will_count ?? '–';
|
||||
const interest = stats?.user_interest ?? null;
|
||||
const isLoggedIn = !!_appState.user;
|
||||
|
||||
const hatActive = interest === 'hat';
|
||||
const willActive = interest === 'will';
|
||||
|
||||
const hatStyle = hatActive ? `background:var(--c-primary);color:#fff;border-color:var(--c-primary)` : '';
|
||||
const willStyle = willActive ? `background:var(--c-primary);color:#fff;border-color:var(--c-primary)` : '';
|
||||
|
||||
return `
|
||||
<div class="wiki-detail-section wiki-interesse-section" id="wiki-interesse-section">
|
||||
<div class="wiki-detail-label">In der Community</div>
|
||||
<div class="wiki-interesse-counts" style="display:flex;gap:var(--space-4);margin-bottom:var(--space-3);font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
<span id="wiki-hat-count">🐕 <strong>${hatCount}</strong> haben diesen Hund</span>
|
||||
<span id="wiki-will-count">❤️ <strong>${willCount}</strong> möchten ihn</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
<button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-hat"
|
||||
style="flex:1;${hatStyle}"
|
||||
data-slug="${_esc(slug)}" data-typ="hat">
|
||||
${isLoggedIn ? '' : '🔒 '}Ich hab einen
|
||||
</button>
|
||||
<button class="btn btn-sm wiki-interesse-btn" id="wiki-btn-will"
|
||||
style="flex:1;${willStyle}"
|
||||
data-slug="${_esc(slug)}" data-typ="will">
|
||||
${isLoggedIn ? '' : '🔒 '}Ich will einen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _bindInteresseButtons(slug) {
|
||||
document.querySelectorAll('.wiki-interesse-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!_appState.user) {
|
||||
UI.toast('Bitte melde dich an, um diese Funktion zu nutzen.', 'info');
|
||||
return;
|
||||
}
|
||||
const typ = btn.dataset.typ;
|
||||
const hatBtn = document.getElementById('wiki-btn-hat');
|
||||
const willBtn = document.getElementById('wiki-btn-will');
|
||||
|
||||
// Determine current state
|
||||
const isActive = btn.style.background.includes('var(--c-primary)') || btn.style.backgroundColor;
|
||||
const currentActive = (hatBtn?.style.background || '').includes('var(--c-primary)') ? 'hat'
|
||||
: (willBtn?.style.background || '').includes('var(--c-primary)') ? 'will' : null;
|
||||
|
||||
// Optimistic disable
|
||||
btn.disabled = true;
|
||||
try {
|
||||
if (currentActive === typ) {
|
||||
await _deleteInteresse(slug);
|
||||
} else {
|
||||
await _setInteresse(slug, typ);
|
||||
}
|
||||
// Reload stats and re-render counts + button states
|
||||
const stats = await _fetchStats(slug);
|
||||
if (stats) {
|
||||
const hatCount = stats.dogs_count ?? '–';
|
||||
const willCount = stats.will_count ?? '–';
|
||||
const interest = stats.user_interest ?? null;
|
||||
|
||||
const hatEl = document.getElementById('wiki-hat-count');
|
||||
const willEl = document.getElementById('wiki-will-count');
|
||||
if (hatEl) hatEl.innerHTML = `🐕 <strong>${hatCount}</strong> haben diesen Hund`;
|
||||
if (willEl) willEl.innerHTML = `❤️ <strong>${willCount}</strong> möchten ihn`;
|
||||
|
||||
const activeStyle = `background:var(--c-primary);color:#fff;border-color:var(--c-primary)`;
|
||||
if (hatBtn) { hatBtn.removeAttribute('style'); if (interest === 'hat') hatBtn.style.cssText = activeStyle; }
|
||||
if (willBtn) { willBtn.removeAttribute('style'); if (interest === 'will') willBtn.style.cssText = activeStyle; }
|
||||
}
|
||||
} catch {
|
||||
UI.toast.error('Aktion fehlgeschlagen.');
|
||||
}
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Render-Helfer: Züchter-Sektion
|
||||
// ----------------------------------------------------------
|
||||
function _renderZuchterSection(zuchter, slug) {
|
||||
const DE_BUNDESLAENDER = [
|
||||
'Baden-Württemberg','Bayern','Berlin','Brandenburg','Bremen','Hamburg',
|
||||
'Hessen','Mecklenburg-Vorpommern','Niedersachsen','Nordrhein-Westfalen',
|
||||
'Rheinland-Pfalz','Saarland','Sachsen','Sachsen-Anhalt',
|
||||
'Schleswig-Holstein','Thüringen',
|
||||
];
|
||||
|
||||
const listHtml = zuchter.length === 0
|
||||
? `<p style="color:var(--c-text-secondary);font-size:var(--text-sm)">Noch keine Züchter eingetragen.</p>`
|
||||
: zuchter.map(z => `
|
||||
<div class="wiki-zuchter-card" style="padding:var(--space-3);border-radius:var(--radius-md);background:var(--c-surface-2);margin-bottom:var(--space-2)">
|
||||
<div style="font-weight:var(--weight-semibold)">${_esc(z.name)}
|
||||
${z.zwingername ? `<em style="font-weight:normal;color:var(--c-text-secondary)"> „${_esc(z.zwingername)}“</em>` : ''}
|
||||
${z.vdh_mitglied ? `<span class="badge badge-sm" style="margin-left:var(--space-1);background:var(--c-primary);color:#fff">VDH</span>` : ''}
|
||||
</div>
|
||||
${(z.ort || z.bundesland) ? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">${[z.ort, z.bundesland].filter(Boolean).map(_esc).join(', ')}</div>` : ''}
|
||||
${z.beschreibung ? `<p style="font-size:var(--text-sm);margin-top:var(--space-1)">${_esc(z.beschreibung)}</p>` : ''}
|
||||
${z.website ? `<a href="${_esc(z.website)}" target="_blank" rel="noopener" style="font-size:var(--text-sm);color:var(--c-primary)">${_esc(z.website)}</a>` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const formHtml = _appState.user ? `
|
||||
<div id="wiki-zuchter-form-wrap" style="display:none;margin-top:var(--space-3)">
|
||||
<form id="wiki-zuchter-form" autocomplete="off">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
|
||||
<div class="form-group" style="grid-column:1/-1">
|
||||
<label class="form-label">Name *</label>
|
||||
<input class="form-control" name="name" required maxlength="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Zwingername</label>
|
||||
<input class="form-control" name="zwingername" maxlength="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ort</label>
|
||||
<input class="form-control" name="ort" maxlength="80">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">PLZ</label>
|
||||
<input class="form-control" name="plz" maxlength="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Bundesland</label>
|
||||
<select class="form-control" name="bundesland">
|
||||
<option value="">— bitte wählen —</option>
|
||||
${DE_BUNDESLAENDER.map(bl => `<option value="${_esc(bl)}">${_esc(bl)}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="grid-column:1/-1">
|
||||
<label class="form-label">Website</label>
|
||||
<input class="form-control" name="website" type="url" maxlength="200" placeholder="https://…">
|
||||
</div>
|
||||
<div class="form-group" style="grid-column:1/-1">
|
||||
<label class="form-label">Telefon</label>
|
||||
<input class="form-control" name="telefon" type="tel" maxlength="30">
|
||||
</div>
|
||||
<div class="form-group" style="grid-column:1/-1">
|
||||
<label class="form-label">Kurzbeschreibung</label>
|
||||
<textarea class="form-control" name="beschreibung" rows="3" maxlength="500"></textarea>
|
||||
</div>
|
||||
<div class="form-group" style="grid-column:1/-1;display:flex;align-items:center;gap:var(--space-2)">
|
||||
<input type="checkbox" id="wiki-zuchter-vdh" name="vdh_mitglied" value="1" style="width:auto">
|
||||
<label for="wiki-zuchter-vdh" style="margin:0;font-size:var(--text-sm)">VDH-Mitglied</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="wiki-zuchter-success" style="display:none;color:var(--c-success);padding:var(--space-3);text-align:center;font-size:var(--text-sm)">
|
||||
Vielen Dank! Dein Eintrag wird geprüft.
|
||||
</div>
|
||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
|
||||
<button type="button" class="btn btn-ghost flex-1" id="wiki-zuchter-cancel">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary flex-1" id="wiki-zuchter-submit">Eintragen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm" id="wiki-zuchter-add-btn" style="margin-top:var(--space-3)">
|
||||
+ Züchter eintragen
|
||||
</button>
|
||||
` : '';
|
||||
|
||||
return `
|
||||
<div class="wiki-detail-section" id="wiki-zuchter-section">
|
||||
<div class="wiki-detail-label">Züchter</div>
|
||||
<div id="wiki-zuchter-list">${listHtml}</div>
|
||||
${formHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _bindZuchterForm(slug) {
|
||||
const addBtn = document.getElementById('wiki-zuchter-add-btn');
|
||||
const cancelBtn = document.getElementById('wiki-zuchter-cancel');
|
||||
const formWrap = document.getElementById('wiki-zuchter-form-wrap');
|
||||
const form = document.getElementById('wiki-zuchter-form');
|
||||
|
||||
addBtn?.addEventListener('click', () => {
|
||||
formWrap.style.display = '';
|
||||
addBtn.style.display = 'none';
|
||||
});
|
||||
|
||||
cancelBtn?.addEventListener('click', () => {
|
||||
formWrap.style.display = 'none';
|
||||
addBtn.style.display = '';
|
||||
});
|
||||
|
||||
form?.addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.getElementById('wiki-zuchter-submit');
|
||||
const fd = new FormData(form);
|
||||
const data = {
|
||||
rasse_slug: slug,
|
||||
name: fd.get('name'),
|
||||
zwingername: fd.get('zwingername') || null,
|
||||
ort: fd.get('ort') || null,
|
||||
plz: fd.get('plz') || null,
|
||||
bundesland: fd.get('bundesland') || null,
|
||||
vdh_mitglied: fd.get('vdh_mitglied') === '1',
|
||||
website: fd.get('website') || null,
|
||||
telefon: fd.get('telefon') || null,
|
||||
beschreibung: fd.get('beschreibung') || null,
|
||||
};
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Wird gesendet…';
|
||||
|
||||
const ok = await _submitZuchter(data);
|
||||
if (ok) {
|
||||
form.reset();
|
||||
document.getElementById('wiki-zuchter-success').style.display = '';
|
||||
// Hide submit row
|
||||
submitBtn.closest('div[style*="flex"]').style.display = 'none';
|
||||
} else {
|
||||
UI.toast.error('Fehler beim Einsenden. Bitte versuche es erneut.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = 'Eintragen';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function _openBreedDetail(slug) {
|
||||
let rasse;
|
||||
try {
|
||||
|
|
@ -402,54 +709,69 @@ window.Page_wiki = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug);
|
||||
|
||||
// Temperament chips
|
||||
const chips = rasse.temperament
|
||||
? rasse.temperament.split(',').map(t => `<span class="wiki-trait-chip">${_esc(t.trim())}</span>`).join('')
|
||||
: '';
|
||||
|
||||
// Stats row
|
||||
const gewicht = (rasse.gewicht_min_kg && rasse.gewicht_max_kg)
|
||||
? `${rasse.gewicht_min_kg}–${rasse.gewicht_max_kg} kg`
|
||||
: (rasse.gewicht_max_kg ? `bis ${rasse.gewicht_max_kg} kg` : '—');
|
||||
|
||||
const photoHtml = rasse.foto_url
|
||||
? `<img class="wiki-detail-photo" src="${_esc(rasse.foto_url)}" alt="${_esc(rasse.name)}" onerror="this.style.display='none'">`
|
||||
? `<div class="wiki-detail-hero-photo-wrap">
|
||||
<img class="wiki-detail-photo" src="${_esc(rasse.foto_url)}" alt="${_esc(rasse.name)}" onerror="this.parentElement.style.display='none'">
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const berichteHtml = _renderBerichteHtml(rasse.berichte || [], slug);
|
||||
|
||||
const body = `
|
||||
${photoHtml}
|
||||
<div class="wiki-detail-badges">
|
||||
<span class="wiki-badge-groesse wiki-badge-groesse--${_esc(rasse.groesse)}">${_groesseLabel(rasse.groesse)}</span>
|
||||
<span class="wiki-badge-aktivitaet wiki-badge-aktivitaet--${_esc(rasse.aktivitaet)}">${_aktivLabel(rasse.aktivitaet)}</span>
|
||||
<span class="wiki-badge-erfahrung wiki-badge-erfahrung--${_esc(rasse.erfahrung)}">${_erfahrungLabel(rasse.erfahrung)}</span>
|
||||
${rasse.gruppe ? `<span class="badge">${_esc(rasse.gruppe)}</span>` : ''}
|
||||
${/* 1. Hero */ ''}
|
||||
<div class="wiki-detail-hero" style="text-align:center;margin-bottom:var(--space-4)">
|
||||
${photoHtml}
|
||||
<h1 style="font-size:var(--text-xl);font-weight:var(--weight-bold);margin:var(--space-2) 0 var(--space-1)">${_esc(rasse.name)}</h1>
|
||||
${rasse.herkunft ? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">${UI.icon('map-pin')} ${_esc(rasse.herkunft)}</div>` : ''}
|
||||
${rasse.gruppe ? `<div style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:2px">${_esc(rasse.gruppe)}</div>` : ''}
|
||||
</div>
|
||||
${rasse.herkunft || rasse.bred_for ? `
|
||||
<div class="wiki-detail-section">
|
||||
${rasse.herkunft ? `<div class="wiki-detail-label">Herkunft</div><p>${_esc(rasse.herkunft)}</p>` : ''}
|
||||
${rasse.bred_for ? `<div class="wiki-detail-label">Ursprüngliche Aufgabe</div><p>${_esc(rasse.bred_for)}</p>` : ''}
|
||||
</div>` : ''}
|
||||
${chips ? `
|
||||
|
||||
${/* 2. Charakter-Badges */ chips ? `
|
||||
<div class="wiki-detail-section">
|
||||
<div class="wiki-detail-label">Charakter</div>
|
||||
<div class="wiki-trait-chips">${chips}</div>
|
||||
</div>` : ''}
|
||||
<div class="wiki-stat-row">
|
||||
<div class="wiki-stat-item">
|
||||
<span class="wiki-stat-label">Gewicht</span>
|
||||
<span class="wiki-stat-value">${gewicht}</span>
|
||||
|
||||
${/* 3. Beschreibung */ rasse.beschreibung ? `
|
||||
<div class="wiki-detail-section">
|
||||
<div class="wiki-detail-label">Beschreibung</div>
|
||||
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.beschreibung)}</p>
|
||||
</div>` : (rasse.bred_for ? `
|
||||
<div class="wiki-detail-section">
|
||||
<div class="wiki-detail-label">Ursprüngliche Aufgabe</div>
|
||||
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.bred_for)}</p>
|
||||
</div>` : '')}
|
||||
|
||||
${/* 4. Steckbrief */ _renderSteckbriefGrid(rasse)}
|
||||
|
||||
${/* 5. Vorkommen */ rasse.vorkommen_de ? `
|
||||
<div class="wiki-detail-section">
|
||||
<div class="wiki-detail-label">Vorkommen in Deutschland</div>
|
||||
<p style="line-height:1.6;color:var(--c-text)">${_esc(rasse.vorkommen_de)}</p>
|
||||
</div>` : ''}
|
||||
|
||||
${/* 6. Interesse — wird async befüllt */ `
|
||||
<div id="wiki-interesse-placeholder">
|
||||
<div class="wiki-detail-section" style="opacity:0.5">
|
||||
<div class="wiki-detail-label">In der Community</div>
|
||||
<div style="height:60px;background:var(--c-surface-2);border-radius:var(--radius-md)"></div>
|
||||
</div>
|
||||
<div class="wiki-stat-item">
|
||||
<span class="wiki-stat-label">Lebenserwartung</span>
|
||||
<span class="wiki-stat-value">${_esc(rasse.lebensdauer || '—')}</span>
|
||||
</div>`}
|
||||
|
||||
${/* 7. Züchter — wird async befüllt */ `
|
||||
<div id="wiki-zuchter-placeholder">
|
||||
<div class="wiki-detail-section" style="opacity:0.5">
|
||||
<div class="wiki-detail-label">Züchter</div>
|
||||
<div style="height:40px;background:var(--c-surface-2);border-radius:var(--radius-md)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wiki-fit-row">
|
||||
<span>${UI.icon('house-line')} Wohnung: ${rasse.wohnung_geeignet ? UI.icon('check') : UI.icon('x')}</span>
|
||||
<span>${UI.icon('users')} Kinder: ${rasse.kinder_geeignet ? UI.icon('check') : UI.icon('x')}</span>
|
||||
</div>
|
||||
</div>`}
|
||||
|
||||
${/* 8. Community-Berichte */ `
|
||||
<div class="wiki-detail-section" id="wiki-berichte-section">
|
||||
<div class="wiki-detail-label">Community-Berichte</div>
|
||||
${berichteHtml}
|
||||
|
|
@ -465,11 +787,30 @@ window.Page_wiki = (() => {
|
|||
<button class="btn btn-ghost w-full" id="wiki-foto-submit-btn" style="font-size:var(--text-sm)">
|
||||
${UI.icon('camera')} ${rasse.foto_url ? 'Besseres Foto vorschlagen' : 'Foto hinzufügen'}
|
||||
</button>
|
||||
</div>` : ''}
|
||||
</div>` : ''}`}
|
||||
`;
|
||||
|
||||
UI.modal.open({ title: _esc(rasse.name), body });
|
||||
|
||||
// Async: load stats + züchter in parallel
|
||||
Promise.all([_fetchStats(slug), _fetchZuchter(slug)]).then(([stats, zuchter]) => {
|
||||
const interessePlaceholder = document.getElementById('wiki-interesse-placeholder');
|
||||
if (interessePlaceholder) {
|
||||
interessePlaceholder.outerHTML = _renderInteresseSection(stats, slug);
|
||||
_bindInteresseButtons(slug);
|
||||
}
|
||||
|
||||
const zuchterPlaceholder = document.getElementById('wiki-zuchter-placeholder');
|
||||
if (zuchterPlaceholder) {
|
||||
zuchterPlaceholder.outerHTML = _renderZuchterSection(zuchter || [], slug);
|
||||
_bindZuchterForm(slug);
|
||||
}
|
||||
}).catch(() => {
|
||||
// Silently remove placeholders on error
|
||||
document.getElementById('wiki-interesse-placeholder')?.remove();
|
||||
document.getElementById('wiki-zuchter-placeholder')?.remove();
|
||||
});
|
||||
|
||||
document.getElementById('wiki-bericht-add-btn')?.addEventListener('click', () => {
|
||||
UI.modal.close();
|
||||
setTimeout(() => _showBerichtForm(slug, rasse.name), 350);
|
||||
|
|
|
|||
717
backend/static/landing.html
Normal file
717
backend/static/landing.html
Normal file
|
|
@ -0,0 +1,717 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ban Yaro — Die deutschsprachige Hunde-Plattform</title>
|
||||
<meta name="description" content="Ban Yaro ist die kostenlose All-in-One Hunde-App für Deutschland, Österreich und die Schweiz. Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community, Hundesitting — DSGVO-konform, ohne App Store.">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://banyaro.app/info">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Ban Yaro — Die deutschsprachige Hunde-Plattform">
|
||||
<meta property="og:description" content="Alles rund um deinen Hund — Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community, Hundesitting. Kostenlos, DSGVO-konform, ohne App Store.">
|
||||
<meta property="og:url" content="https://banyaro.app/info">
|
||||
<meta property="og:image" content="https://banyaro.app/icons/icon-512.png">
|
||||
<meta property="og:locale" content="de_DE">
|
||||
<meta property="og:site_name" content="Ban Yaro">
|
||||
|
||||
<!-- Twitter Card -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Ban Yaro — Die deutschsprachige Hunde-Plattform">
|
||||
<meta name="twitter:description" content="Alles rund um deinen Hund — Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community. Kostenlos, DSGVO-konform.">
|
||||
<meta name="twitter:image" content="https://banyaro.app/icons/icon-512.png">
|
||||
|
||||
<!-- Structured Data -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "MobileApplication",
|
||||
"name": "Ban Yaro",
|
||||
"alternateName": "Ban Yaro — Die Hunde-Plattform",
|
||||
"description": "Ban Yaro ist die kostenlose, deutschsprachige All-in-One Hunde-App. Digitales Tagebuch, Impfpass, Giftköder-Alarm, Gassi-Community, Hundesitting und mehr — DSGVO-konform, ohne App Store.",
|
||||
"url": "https://banyaro.app",
|
||||
"applicationCategory": "LifestyleApplication",
|
||||
"applicationSubCategory": "PetApplication",
|
||||
"operatingSystem": "iOS, Android, Web",
|
||||
"inLanguage": "de",
|
||||
"availableOnDevice": "Smartphone, Tablet",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "0",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Ban Yaro",
|
||||
"url": "https://banyaro.app"
|
||||
},
|
||||
"featureList": [
|
||||
"Digitales Hunde-Tagebuch mit Fotos und GPS",
|
||||
"Digitaler Impfpass und Gesundheitsakte",
|
||||
"Giftköder-Alarm mit Push-Benachrichtigungen",
|
||||
"Gassi-Community und GPS-Routen",
|
||||
"Hundesitting-Vermittlung",
|
||||
"NFC-Halsband-Tags",
|
||||
"Hunde-Wiki mit Rassendatenbank",
|
||||
"Verlorener Hund Alarm",
|
||||
"Forum für Hundebesitzer",
|
||||
"Offline-Modus via Service Worker"
|
||||
],
|
||||
"screenshot": "https://banyaro.app/icons/icon-512.png",
|
||||
"softwareVersion": "2.0",
|
||||
"datePublished": "2026-04-01",
|
||||
"areaServed": ["DE", "AT", "CH"],
|
||||
"audience": {
|
||||
"@type": "Audience",
|
||||
"audienceType": "Hundebesitzer, Hundeschulen, Tierärzte, Züchter"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--primary: #C4843A;
|
||||
--primary-dark: #a86e2e;
|
||||
--primary-light: #f5e6d3;
|
||||
--text: #1a1a1a;
|
||||
--text-secondary: #555;
|
||||
--text-muted: #888;
|
||||
--bg: #FAF7F2;
|
||||
--surface: #fff;
|
||||
--border: #e8ddd0;
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a { color: var(--primary); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
.container { max-width: 900px; margin: 0 auto; padding: 0 1.5rem; }
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background: linear-gradient(135deg, #C4843A 0%, #e8a857 100%);
|
||||
color: white;
|
||||
padding: 3rem 0 4rem;
|
||||
text-align: center;
|
||||
}
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.header-logo img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.2);
|
||||
}
|
||||
.header-logo .logo-name {
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
header h1 {
|
||||
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
header p {
|
||||
font-size: 1.15rem;
|
||||
opacity: 0.92;
|
||||
max-width: 600px;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
.header-badges {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.badge {
|
||||
background: rgba(255,255,255,.2);
|
||||
border: 1px solid rgba(255,255,255,.4);
|
||||
border-radius: 999px;
|
||||
padding: 0.4rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cta-btn {
|
||||
display: inline-block;
|
||||
background: white;
|
||||
color: var(--primary-dark);
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
padding: 0.85rem 2.5rem;
|
||||
border-radius: 999px;
|
||||
margin-top: 2rem;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.15);
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.cta-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 28px rgba(0,0,0,.2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
nav {
|
||||
background: white;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 0.75rem 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
nav .container {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
nav a {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
nav a:hover { color: var(--primary); text-decoration: none; }
|
||||
nav .nav-brand {
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
margin-right: auto;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
section { padding: 4rem 0; }
|
||||
section:nth-child(even) { background: white; }
|
||||
h2 {
|
||||
font-size: clamp(1.4rem, 3vw, 2rem);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.section-intro {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.05rem;
|
||||
margin-bottom: 2.5rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* Feature Groups */
|
||||
.feature-group { margin-bottom: 2.5rem; }
|
||||
.feature-group-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--primary-dark);
|
||||
background: var(--primary-light);
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.feature-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
section:nth-child(even) .feature-card { background: var(--bg); }
|
||||
.feature-icon {
|
||||
font-size: 1.6rem;
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.feature-card h3 {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
color: var(--text);
|
||||
}
|
||||
.feature-card p {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
.feature-tag {
|
||||
display: inline-block;
|
||||
background: var(--primary-light);
|
||||
color: var(--primary-dark);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
margin-top: 0.4rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* Comparison Table */
|
||||
.table-wrap { overflow-x: auto; margin-top: 2rem; }
|
||||
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; min-width: 500px; }
|
||||
th {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
th:first-child { border-radius: var(--radius) 0 0 0; }
|
||||
th:last-child { border-radius: 0 var(--radius) 0 0; }
|
||||
td { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tr:nth-child(even) td { background: var(--bg); }
|
||||
.check { color: #16a34a; font-weight: 700; }
|
||||
.cross { color: #dc2626; }
|
||||
|
||||
/* Pricing */
|
||||
.pricing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.pricing-card {
|
||||
border: 2px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem 1.5rem;
|
||||
background: white;
|
||||
}
|
||||
.pricing-card.featured {
|
||||
border-color: var(--primary);
|
||||
position: relative;
|
||||
}
|
||||
.pricing-card.featured::before {
|
||||
content: "Empfohlen";
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.pricing-card h3 { font-size: 1.2rem; font-weight: 700; margin-bottom: 0.25rem; }
|
||||
.pricing-price { font-size: 2rem; font-weight: 800; color: var(--primary); margin: 0.75rem 0; }
|
||||
.pricing-price span { font-size: 1rem; font-weight: 400; color: var(--text-muted); }
|
||||
.pricing-card ul { list-style: none; margin-top: 1rem; }
|
||||
.pricing-card ul li { padding: 0.35rem 0; font-size: 0.9rem; color: var(--text-secondary); }
|
||||
.pricing-card ul li::before { content: "✓ "; color: #16a34a; font-weight: 700; }
|
||||
|
||||
/* USP Strip */
|
||||
.usp-strip {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.usp-item { display: flex; align-items: flex-start; gap: 0.75rem; }
|
||||
.usp-icon { font-size: 1.5rem; flex-shrink: 0; margin-top: 0.1rem; }
|
||||
.usp-item h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.2rem; }
|
||||
.usp-item p { font-size: 0.85rem; color: var(--text-secondary); }
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background: #1a1a1a;
|
||||
color: #aaa;
|
||||
padding: 2.5rem 0;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
footer a { color: var(--primary); }
|
||||
footer .footer-links { margin-top: 0.75rem; display: flex; gap: 1.5rem; justify-content: center; flex-wrap: wrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<div class="header-logo">
|
||||
<img src="/icons/icon-180.png" alt="Ban Yaro App Icon">
|
||||
<span class="logo-name">Ban Yaro</span>
|
||||
</div>
|
||||
<h1>Die deutschsprachige Hunde-Plattform</h1>
|
||||
<p>Alles rund um deinen Hund — von Welpe bis Opa. Kostenlos, DSGVO-konform, ohne App Store.</p>
|
||||
<div class="header-badges">
|
||||
<span class="badge">Kostenlos nutzbar</span>
|
||||
<span class="badge">DSGVO-konform</span>
|
||||
<span class="badge">Kein App Store nötig</span>
|
||||
<span class="badge">Made in Germany</span>
|
||||
<span class="badge">Offline-fähig</span>
|
||||
</div>
|
||||
<a href="/" class="cta-btn">Jetzt kostenlos starten</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav>
|
||||
<div class="container">
|
||||
<span class="nav-brand">Ban Yaro</span>
|
||||
<a href="#funktionen">Funktionen</a>
|
||||
<a href="#vergleich">Vergleich</a>
|
||||
<a href="#preise">Preise</a>
|
||||
<a href="#warum">Warum Ban Yaro?</a>
|
||||
<a href="/wiki/rassen">Rassen-Wiki</a>
|
||||
<a href="/knigge">Knigge</a>
|
||||
<a href="/">App öffnen</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<section id="funktionen">
|
||||
<div class="container">
|
||||
<h2>Alles für Hundebesitzer in einer App</h2>
|
||||
<p class="section-intro">Ban Yaro vereint alle wichtigen Hunde-Tools — ohne Werbung, ohne Datenweitergabe an US-Konzerne, ohne monatliche Pflichtkosten.</p>
|
||||
|
||||
<div class="feature-group">
|
||||
<div class="feature-group-label">Mein Hund</div>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📓</span>
|
||||
<div><h3>Tagebuch</h3><p>Fotos, Videos, Texte und GPS-Orte — alle Momente mit deinem Hund. Kategorien wie Spaziergänge, Meilensteine, Lustiges.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">💉</span>
|
||||
<div><h3>Gesundheit & Impfpass</h3><p>Impfungen, Tierarztbesuche, Medikamente digital verwalten. Automatische Erinnerungen per Push-Notification.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🎯</span>
|
||||
<div><h3>Training & Übungen</h3><p>Tägliches Trainings-Tagebuch, Übungen und Pläne. KI-gestützte Mustererkennung — wann lernt dein Hund am besten?</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🏥</span>
|
||||
<div><h3>Symptom-Checker</h3><p>KI-gestützte Ersteinschätzung: beobachten, Tierarzt oder Notfall? Orientierung wann es wirklich dringend ist.</p><span class="feature-tag">Plus</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📄</span>
|
||||
<div><h3>Digitaler Heimtierausweis</h3><p>Alle Gesundheitsdaten als druckbares Dokument — für Tierarzt, Tierpension oder Auslandsreise.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🆔</span>
|
||||
<div><h3>NFC-Halsband-Tags</h3><p>Öffentliche Profilseite für jeden Hund. Finder kontaktiert dich anonym — ohne deine Nummer preiszugeben.</p><span class="feature-tag">Kostenlos + Shop</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-group">
|
||||
<div class="feature-group-label">Entdecken</div>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🗺️</span>
|
||||
<div><h3>Karte & Umgebung</h3><p>Interaktive Karte mit Giftköder-Meldungen, Gassi-Treffen, Routen und hundefreundlichen Orten in der Nähe.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🐾</span>
|
||||
<div><h3>GPS-Routen</h3><p>Routen aufzeichnen, teilen und bewerten — Untergrund, Schatten, Leinenpflicht, Sicherheit.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📅</span>
|
||||
<div><h3>Events & Turniere</h3><p>Agility-Turniere, Hundeausstellungen und lokale Veranstaltungen in deiner Region.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📍</span>
|
||||
<div><h3>Hundefreundliche Orte</h3><p>Crowd-sourced Datenbank: Restaurants, Cafés, Parks, Geschäfte — mit echten Bewertungen von Hundebesitzern.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-group">
|
||||
<div class="feature-group-label">Community</div>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">⚠️</span>
|
||||
<div><h3>Giftköder-Alarm</h3><p>Meldungen mit GPS und Foto. Alle Nutzer im Umkreis bekommen sofort eine Push-Benachrichtigung.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🚨</span>
|
||||
<div><h3>Verlorener Hund</h3><p>Sofortalarm für alle Nutzer in der Nähe — mit Foto, letzter GPS-Position und direktem Kontakt.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🐕</span>
|
||||
<div><h3>Gassi-Treffen</h3><p>Spontane oder geplante Gassi-Treffen erstellen und finden. Mit Hunde-Profilen der Teilnehmer.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🏠</span>
|
||||
<div><h3>Hundesitting</h3><p>Vertrauenswürdige Sitter finden — nur 8% Provision statt 20% bei Rover oder Pawshake.</p><span class="feature-tag">8% Provision</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">💬</span>
|
||||
<div><h3>Forum</h3><p>Rassen-basierte Foren, KI-Zusammenfassungen langer Threads, Experten-Badge für Tierärzte und Trainer.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-group">
|
||||
<div class="feature-group-label">Wissen</div>
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">📚</span>
|
||||
<div><h3><a href="/wiki/rassen">Hunde-Wiki</a></h3><p>Rassendatenbank mit Charakter, Gesundheit, Pflege. "Passt diese Rasse zu mir?" Quiz für angehende Hundebesitzer.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🤝</span>
|
||||
<div><h3><a href="/knigge">Hunde-Knigge</a></h3><p>Begegnungen mit fremden Hunden, Kindern, Radfahrern. ÖPNV-Regeln, Leinenpflicht, Haftpflicht.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🎬</span>
|
||||
<div><h3>Hundefilme</h3><p>Filmdatenbank mit der wichtigsten Frage: "Stirbt der Hund?" Nie wieder unvorbereitet in einen Film stolpern.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<span class="feature-icon">🩹</span>
|
||||
<div><h3>Erste Hilfe</h3><p>Notfallratgeber für häufige Situationen — Vergiftung, Wunden, Hitzschlag. Mit klaren Handlungsschritten.</p><span class="feature-tag">Kostenlos</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="vergleich">
|
||||
<div class="container">
|
||||
<h2>Ban Yaro vs. Konkurrenz</h2>
|
||||
<p class="section-intro">Andere Apps decken einzelne Bereiche ab — Ban Yaro vereint alles in einer DSGVO-konformen Plattform ohne US-Datenweitergabe.</p>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Funktion</th>
|
||||
<th>Ban Yaro</th>
|
||||
<th>Dogorama</th>
|
||||
<th>Tractive</th>
|
||||
<th>PetDesk</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Kostenlos nutzbar</td>
|
||||
<td class="check">✓ Ja</td>
|
||||
<td>Begrenzt</td>
|
||||
<td class="cross">✗ Abo</td>
|
||||
<td class="cross">✗ Nein</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>DSGVO / EU-Hosting</td>
|
||||
<td class="check">✓ Ja</td>
|
||||
<td class="cross">✗ Nein</td>
|
||||
<td>Teilweise</td>
|
||||
<td class="cross">✗ USA</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kein App Store nötig</td>
|
||||
<td class="check">✓ PWA</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Giftköder-Alarm</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Digitaler Impfpass</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="check">✓</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gassi-Community</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hundesitting</td>
|
||||
<td class="check">✓ (8%)</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NFC-Halsband-Tag</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Verlorener Hund Alarm</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="check">✓ (GPS)</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rassen-Wiki</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Offline-Modus</td>
|
||||
<td class="check">✓</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
<td class="cross">✗</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="preise">
|
||||
<div class="container">
|
||||
<h2>Preise</h2>
|
||||
<p class="section-intro">Ban Yaro ist kostenlos nutzbar — für immer. Ban Yaro Plus erweitert die Möglichkeiten für engagierte Hundebesitzer.</p>
|
||||
<div class="pricing-grid">
|
||||
<div class="pricing-card">
|
||||
<h3>Kostenlos</h3>
|
||||
<div class="pricing-price">0 € <span>/ Monat</span></div>
|
||||
<ul>
|
||||
<li>1 Hunde-Profil</li>
|
||||
<li>Tagebuch (unbegrenzte Einträge)</li>
|
||||
<li>Giftköder-Alarm</li>
|
||||
<li>Verlorener Hund Alarm</li>
|
||||
<li>Wiki & Knigge</li>
|
||||
<li>Forum & Community</li>
|
||||
<li>Gassi-Treffen & Routen</li>
|
||||
<li>NFC-Halsband-Profil</li>
|
||||
<li>Heimtierausweis (Druck)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pricing-card featured">
|
||||
<h3>Ban Yaro Plus</h3>
|
||||
<div class="pricing-price">4,99 € <span>/ Monat</span></div>
|
||||
<ul>
|
||||
<li>Alles aus Kostenlos</li>
|
||||
<li>Unbegrenzte Hunde-Profile</li>
|
||||
<li>Tagebuch-Export (PDF/Fotobuch)</li>
|
||||
<li>Jahresrückblick</li>
|
||||
<li>Symptom-Checker unlimitiert</li>
|
||||
<li>Futter-Barcode Scanner</li>
|
||||
<li>EU-Reisepass Checkliste</li>
|
||||
<li>KI-Erziehungsassistent</li>
|
||||
<li>Smart Collar Integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pricing-card">
|
||||
<h3>NFC-Tags</h3>
|
||||
<div class="pricing-price">ab 6 €</div>
|
||||
<ul>
|
||||
<li>Physisches NFC-Tag für Halsband</li>
|
||||
<li>Scan → öffentliches Hunde-Profil</li>
|
||||
<li>"Gefunden"-Benachrichtigung</li>
|
||||
<li>Anonymer Kontakt ohne Telefon</li>
|
||||
<li>Wetter- und kratzfest</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="warum">
|
||||
<div class="container">
|
||||
<h2>Warum Ban Yaro?</h2>
|
||||
<p class="section-intro">Ban Yaro wurde von Hundebesitzern für Hundebesitzer entwickelt — mit einem klaren Standpunkt zu Datenschutz und Fairness.</p>
|
||||
<div class="usp-strip">
|
||||
<div class="usp-item">
|
||||
<span class="usp-icon">🇩🇪</span>
|
||||
<div>
|
||||
<h3>Deutsche Plattform</h3>
|
||||
<p>Hosting in Deutschland, deutschsprachiger Support, auf DACH-Nutzer zugeschnitten.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usp-item">
|
||||
<span class="usp-icon">🔒</span>
|
||||
<div>
|
||||
<h3>DSGVO-konform</h3>
|
||||
<p>Keine Datenweitergabe an US-Konzerne. Cookielose Analytics (Umami). Transparente Datennutzung.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usp-item">
|
||||
<span class="usp-icon">📱</span>
|
||||
<div>
|
||||
<h3>Kein App Store</h3>
|
||||
<p>Als Progressive Web App direkt über den Browser installierbar — auf iOS und Android. Sofort updatebar.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usp-item">
|
||||
<span class="usp-icon">📡</span>
|
||||
<div>
|
||||
<h3>Offline-fähig</h3>
|
||||
<p>Service Worker sorgt dafür dass die App auch ohne Internet funktioniert — beim Gassi gehen in der Natur.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usp-item">
|
||||
<span class="usp-icon">💸</span>
|
||||
<div>
|
||||
<h3>Faire Provision</h3>
|
||||
<p>Hundesitting nur 8% Provision — Rover und Pawshake nehmen 20%. Mehr Geld bleibt beim Sitter.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usp-item">
|
||||
<span class="usp-icon">🗺️</span>
|
||||
<div>
|
||||
<h3>OpenStreetMap</h3>
|
||||
<p>Karten von OpenStreetMap statt Google — keine Tracking-Cookies, kein API-Lock-in, günstiger für alle.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="container" style="text-align:center">
|
||||
<h2>Jetzt kostenlos starten</h2>
|
||||
<p class="section-intro" style="margin:1rem auto 2rem">Einfach banyaro.app im Browser öffnen und "Zum Homescreen hinzufügen" — fertig. Kein App Store, keine Kreditkarte.</p>
|
||||
<a href="/" class="cta-btn" style="background:var(--primary);color:white;box-shadow:0 4px 20px rgba(196,132,58,.4)">Ban Yaro öffnen</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p><strong style="color:white">Ban Yaro</strong> — Die deutschsprachige Hunde-Plattform</p>
|
||||
<p style="margin-top:0.5rem">banyaro.app · banyaro.de · DSGVO-konform · Hosting in Deutschland</p>
|
||||
<div class="footer-links">
|
||||
<a href="/">App öffnen</a>
|
||||
<a href="/info">Über Ban Yaro</a>
|
||||
<a href="/api/wiki/rassen">Hunde-Rassen</a>
|
||||
<a href="/api/events">Events</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
167
backend/static/llms.txt
Normal file
167
backend/static/llms.txt
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# Ban Yaro — Die deutschsprachige Hunde-Plattform
|
||||
# https://banyaro.app
|
||||
# Letzte Aktualisierung: 2026-04-21
|
||||
|
||||
## Was ist Ban Yaro?
|
||||
|
||||
Ban Yaro (banyaro.app) ist eine deutschsprachige All-in-One Hunde-Plattform als Progressive Web App (PWA).
|
||||
Kein App Store, kein US-Konzern, DSGVO-konform, selbst gehostet in Deutschland.
|
||||
|
||||
Das Kern-Versprechen: "Alles rund um deinen Hund — von Welpe bis Opa."
|
||||
|
||||
Ban Yaro ist kostenlos nutzbar (Freemium-Modell). Die App ist auf allen Smartphones installierbar
|
||||
(iOS und Android) direkt über den Browser — ohne App Store.
|
||||
|
||||
## Zielgruppe
|
||||
|
||||
- Deutschsprachige Hundebesitzer (Deutschland, Österreich, Schweiz)
|
||||
- Hundeschulen und Hundetrainer
|
||||
- Tierärzte und Praxen
|
||||
- Züchter
|
||||
|
||||
## Funktionen (aktuell verfügbar)
|
||||
|
||||
### Hunde-Profil & Tagebuch
|
||||
- Digitales Hunde-Profil (Name, Rasse, Geburtstag, Foto, Chip-Nummer)
|
||||
- Tagebuch mit Fotos, Videos, GPS-Ort, Kategorien
|
||||
- Öffentliches Profil für NFC-Halsband-Tags (banyaro.app/hund/{id})
|
||||
- Mehrere Hunde pro Account verwaltbar
|
||||
- Hunde-Profil teilen (Mitbesitzer, Hundesitter)
|
||||
|
||||
### Gesundheit & Impfpass
|
||||
- Digitaler Impfpass (Impfungen, Entwurmungen, Tierarztbesuche)
|
||||
- Medikamenten-Reminder per Push-Notification
|
||||
- Gewichtsverlauf
|
||||
- Symptom-Checker (KI-gestützt: beobachten / Tierarzt / Notfall)
|
||||
- Tierarzt-Verzeichnis
|
||||
- Printbarer Heimtierausweis (PDF)
|
||||
|
||||
### Giftköder-Alarm
|
||||
- Giftköder-Meldungen mit GPS-Koordinaten und Foto
|
||||
- Push-Benachrichtigung für alle Nutzer im konfigurierbaren Umkreis
|
||||
- Interaktive Karte (OpenStreetMap/Leaflet)
|
||||
- Automatisches Ablaufdatum nach 7 Tagen
|
||||
|
||||
### Sicherheit & Community-Alerts
|
||||
- Verlorener Hund: Alert mit Foto und letzter GPS-Position
|
||||
- Nearby-Alerts: Push-Benachrichtigungen für Ereignisse in der Nähe
|
||||
|
||||
### NFC-Halsband-Tags
|
||||
- Jeder Hund hat eine öffentliche URL (ohne Login sichtbar)
|
||||
- "Ich habe diesen Hund gefunden"-Button → Besitzer bekommt Push-Benachrichtigung
|
||||
- Notfallkontakt ohne Telefonnummer preiszugeben
|
||||
- Physische NFC-Tags erhältlich (Shop)
|
||||
|
||||
### Gassi-Community
|
||||
- Gassi-Treffen erstellen und beitreten
|
||||
- GPS-Routen aufzeichnen und teilen
|
||||
- Routen bewerten (Untergrund, Schatten, Leinenpflicht)
|
||||
- Beliebte Routen entdecken
|
||||
|
||||
### Hundesitting-Netzwerk
|
||||
- Sitter-Profile mit Erfahrung und Bewertungen
|
||||
- Buchungsanfragen und Kalender
|
||||
- Nur 8% Provision (vs. 20% bei Rover/Pawshake)
|
||||
- Bewertungen verifizierter Buchungen
|
||||
|
||||
### Forum
|
||||
- Rassen-basierte Foren
|
||||
- KI-Zusammenfassung langer Threads
|
||||
- Experten-Badge (Tierarzt, Trainer)
|
||||
|
||||
### Hunde-Wiki (Wissensdatenbank)
|
||||
- Rassen-Datenbank mit Charakter, Gesundheit, Pflege
|
||||
- "Passt diese Rasse zu mir?" Quiz
|
||||
- Gesundheits-Wiki (Zecken, Vergiftungen, Erste Hilfe)
|
||||
|
||||
### Hunde-Knigge
|
||||
- Ratgeber für Begegnungen (fremder Hund, Kinder, Radfahrer)
|
||||
- Regeln in ÖPNV und öffentlichen Orten
|
||||
- Haftpflicht-Ratgeber
|
||||
|
||||
### Events & Kultur
|
||||
- Agility-Turniere und Hundeausstellungen
|
||||
- Hundefilme-Datenbank mit "Stirbt der Hund?"-Rubrik
|
||||
- Veranstaltungskalender
|
||||
|
||||
### Hundefreundliche Orte
|
||||
- Crowd-sourced Datenbank hundefreundlicher Orte
|
||||
- Restaurants, Parks, Geschäfte
|
||||
- Detaillierte Bewertungen
|
||||
|
||||
### Training
|
||||
- Tägliches Trainings-Tagebuch
|
||||
- Trainingsübungen und -pläne
|
||||
- KI-gestützte Mustererkennung
|
||||
|
||||
## Technologie
|
||||
|
||||
- Progressive Web App (PWA) — installierbar ohne App Store
|
||||
- Offline-fähig via Service Worker
|
||||
- Backend: Python/FastAPI + SQLite
|
||||
- Karten: Leaflet.js + OpenStreetMap (kein Google Maps)
|
||||
- Hosting: Deutschland (DSGVO-konform)
|
||||
- Analytics: Umami (cookieless, DSGVO-konform)
|
||||
- KI-Integration: Lokale Sprachmodelle + Claude API
|
||||
|
||||
## Monetarisierung
|
||||
|
||||
**Kostenlos (immer):**
|
||||
- Hunde-Profil (1 Hund)
|
||||
- Giftköder-Alarm
|
||||
- Verlorener Hund
|
||||
- Wiki & Knigge
|
||||
- Forum
|
||||
- Grundfunktionen Tagebuch
|
||||
|
||||
**Ban Yaro Plus (ca. 4,99 €/Monat):**
|
||||
- Unbegrenzte Hunde-Profile
|
||||
- Tagebuch-Export als PDF/Printbook
|
||||
- Jahresrückblick
|
||||
- Symptom-Checker unlimitiert
|
||||
- Futter-Barcode Scanner
|
||||
- EU-Reisepass Checkliste
|
||||
- KI-Erziehungsassistent
|
||||
|
||||
**Provisionen:**
|
||||
- Hundesitting: 8% Provision
|
||||
|
||||
**Physische Produkte:**
|
||||
- NFC-Halsband-Tags (ca. 6 €)
|
||||
- Gedruckte Fotobücher (Partner)
|
||||
|
||||
## Vergleich mit Konkurrenz
|
||||
|
||||
| Funktion | Ban Yaro | Dogorama | PetDesk | Tractive |
|
||||
|----------|----------|----------|---------|----------|
|
||||
| Kostenlos nutzbar | Ja | Begrenzt | Nein | Nein |
|
||||
| DSGVO / EU-Hosting | Ja | Nein | Nein | Teilweise |
|
||||
| Giftköder-Alarm | Ja | Nein | Nein | Nein |
|
||||
| Gassi-Community | Ja | Ja | Nein | Nein |
|
||||
| Hundesitting | Ja | Nein | Nein | Nein |
|
||||
| Digitaler Impfpass | Ja | Nein | Ja | Nein |
|
||||
| NFC-Halsband-Tag | Ja | Nein | Nein | Nein |
|
||||
| Offline-Modus | Ja | Nein | Nein | Nein |
|
||||
| Kein App Store | Ja | Nein | Nein | Nein |
|
||||
| Sitting-Provision | 8% | – | – | – |
|
||||
|
||||
## Domains
|
||||
|
||||
- https://banyaro.app (primäre Domain)
|
||||
- https://banyaro.de (Weiterleitung auf banyaro.app)
|
||||
|
||||
## Kontakt
|
||||
|
||||
Website: https://banyaro.app
|
||||
E-Mail: Über das Kontaktformular in der App
|
||||
|
||||
## Öffentliche Daten-APIs (keine Authentifizierung nötig)
|
||||
|
||||
- GET https://banyaro.app/api/wiki/rassen — Liste aller Hunderassen
|
||||
- GET https://banyaro.app/api/wiki/rassen/{slug} — Details zu einer Rasse
|
||||
- GET https://banyaro.app/api/events — Aktuelle Hundeevents
|
||||
- GET https://banyaro.app/api/poison — Aktuelle Giftköder-Meldungen
|
||||
- GET https://banyaro.app/api/lost — Aktuelle Vermisst-Meldungen
|
||||
- GET https://banyaro.app/api/knigge/articles — Hunde-Knigge Artikel
|
||||
- GET https://banyaro.app/api/movies/list — Hundefilme-Datenbank
|
||||
- GET https://banyaro.app/api/stats — Community-Statistiken
|
||||
32
backend/static/robots.txt
Normal file
32
backend/static/robots.txt
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
Allow: /info
|
||||
Allow: /hund/
|
||||
Allow: /api/wiki/rassen
|
||||
Allow: /api/wiki/rassen/
|
||||
Allow: /api/events
|
||||
Allow: /api/knigge/articles
|
||||
Allow: /api/movies/list
|
||||
Allow: /api/forum/
|
||||
Allow: /api/lost
|
||||
Allow: /api/poison
|
||||
Allow: /api/stats
|
||||
Disallow: /api/auth/
|
||||
Disallow: /api/admin/
|
||||
Disallow: /api/dogs/
|
||||
Disallow: /api/diary/
|
||||
Disallow: /api/health/
|
||||
Disallow: /api/chat/
|
||||
Disallow: /api/friends/
|
||||
Disallow: /api/push/
|
||||
Disallow: /api/widget/
|
||||
Disallow: /api/notifications/
|
||||
Disallow: /api/alerts/
|
||||
Disallow: /api/ki/
|
||||
Disallow: /api/import/
|
||||
Disallow: /api/sitting-access/
|
||||
Disallow: /ausweis/
|
||||
Disallow: /teilen/
|
||||
Disallow: /media/
|
||||
|
||||
Sitemap: https://banyaro.app/sitemap.xml
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v272';
|
||||
const CACHE_VERSION = 'by-v279';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue