banyaro/backend/static/js/pages/welcome.js
rene 55069d246b Feature: Welten-Onboarding, Wetter-Motivation, UX-Fixes (SW by-v715)
Welten (worlds.js):
- Swipe-Hints beim ersten Öffnen (JETZT ← → WELT animiert, einmalig)
- Kein-Hund-Onboarding: Feature-Preview-Grid statt leerer Karte
- Hintergrund-Foto-Hint: Kamera-Karte wenn noch kein Tagebuchfoto
- worlds-back: navigiert zu Welcome wenn kein User eingeloggt
- Nach Logout: worlds-back Button sofort ausgeblendet

Wetter (wetter.js):
- Standort-Fehlerseite zu Motivations-Seite umgebaut
- Feature-Preview: Gassi-Score, 7-Tage, Regenradar, Rekorde
- CTA: Standort freigeben + Registrieren (nur für Gäste)

Settings (settings.js):
- Logo in Auth-Form: display:block + margin:0 auto zentriert
- Header bleibt sichtbar (FAB/Zurück-Navigation funktioniert)

Jobs (jobs.js):
- 2-Spalten-Grid auf Mobile: auto-fit statt festes 1fr 1fr
- Kein doppeltes Padding im Wrapper

Backend:
- weather.py, achievements.py: diary JOIN fix (d.user_id → dogs JOIN)
- Neue Wetter-Badges: wetter_tapfer, jahreszeiten, schnee
- Ernährungs-, Reise-, Ausgaben-Seite: diverse UX-Verbesserungen
- Presse-Seite erweitert
- Ban Yaro Foto-Assets (WebP + HIRES JPG)
2026-05-05 17:32:03 +02:00

1262 lines
55 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/* ============================================================
BAN YARO — Willkommensseite
============================================================ */
window.Page_welcome = (() => {
let _container = null;
let _appState = null;
let _showInstall = false;
let _heroInterval = null;
// ----------------------------------------------------------
// HERO-SLIDES — rotieren alle 4 Sekunden
// ----------------------------------------------------------
const HERO_SLIDES = [
{ headline: 'Jeder Moment zählt.', sub: 'Fotos, Notizen, Stimmungen — das Tagebuch deines Hundes.', screen: '/img/screenshots/screen-1.jpg' },
{ headline: 'Deine Gegend. Sein Revier.', sub: 'Hundeparks, Gassi-Spots und mehr — alles auf der Karte.', screen: '/img/screenshots/screen-2.jpg' },
{ headline: 'Keinen Termin verpassen.', sub: 'Impfungen, Gewicht, Tierarzt — mit KI, individuell auf deinen Hund angepasst.', screen: '/img/screenshots/screen-3.jpg' },
{ headline: 'Wir achten auf deinen Hund.', sub: 'Gefahren in deiner Nähe — damit ihr gezielt aus dem Weg gehen könnt.', screen: '/img/screenshots/screen-4.jpg' },
{ headline: 'Wie eine App. Nur ohne App Store.', sub: 'Einmal auf „Zum Homescreen" — fertig. Kein Store, keine Updates.', screen: '/img/screenshots/screen-5.jpg' },
{ headline: 'Lieblingsrouten für immer.', sub: 'Speichere eure besten Strecken — und entdecke neue in der Nähe.', screen: '/img/screenshots/screen-6.jpg' },
{ headline: 'Gassi ist kein Solosport.', sub: 'Triff andere Hundebesitzer — spontan, in deiner Umgebung.', screen: '/img/screenshots/screen-7.jpg' },
{ headline: 'Dein virtueller Trainer.', sub: '100+ Übungen, Schritt für Schritt — individuell auf deinen Hund abgestimmt.', screen: '/img/screenshots/screen-8.jpg' },
{ headline: 'Frag nach. Du bist nicht allein.', sub: 'Erfahrungen, Tipps, Hilfe — von Hundebesitzern für Hundebesitzer.', screen: '/img/screenshots/screen-9.jpg' },
];
async function init(container, appState, params = {}) {
_container = container;
_appState = appState;
_showInstall = !!params.install;
_render();
if (_showInstall) {
setTimeout(() => {
_container.querySelector('.wc-install-card')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
}
}
function refresh() { _render(); }
function onDogChange() {}
// ----------------------------------------------------------
// FEATURES — Icon, Titel, Beschreibung, Zielseite
// ----------------------------------------------------------
const FEATURES = [
{ icon: 'book-open', label: 'Tagebuch', page: 'diary' },
{ icon: 'first-aid', label: 'Gesundheit', page: 'health' },
{ icon: 'map-trifold', label: 'Karte', page: 'map' },
{ icon: 'path', label: 'Routen', page: 'routes' },
{ icon: 'target', label: 'Training', page: 'uebungen' },
{ icon: 'warning-octagon', label: 'Giftköder', page: 'poison' },
{ icon: 'users', label: 'Freunde', page: 'friends' },
{ icon: 'chat-circle-dots', label: 'Nachrichten', page: 'chat' },
{ icon: 'paw-print', label: 'Gassi-Treffen', page: 'walks' },
{ icon: 'house-line', label: 'Sitting', page: 'sitting' },
{ icon: 'push-pin', label: 'Forum', page: 'forum' },
{ icon: 'books', label: 'Rassen-Wiki', page: 'wiki' },
{ icon: 'calendar-dots', label: 'Events', page: 'events' },
{ icon: 'bell', label: 'Neuigkeiten', page: 'notifications' },
{ icon: 'handshake', label: 'Knigge', page: 'knigge' },
{ icon: 'magnifying-glass', label: 'Vermisste', page: 'lost' },
];
// ----------------------------------------------------------
// RENDER
// ----------------------------------------------------------
function _render() {
if (_heroInterval) { clearInterval(_heroInterval); _heroInterval = null; }
const isInstalled = window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone === true;
const user = _appState?.user;
_injectStyles();
if (user) {
_renderLoggedIn(isInstalled);
} else {
_renderLanding(isInstalled);
}
_bindEvents();
_pulseMenuBtn();
}
// ----------------------------------------------------------
// LANDING PAGE — nicht eingeloggte Besucher
// ----------------------------------------------------------
function _renderLanding(isInstalled) {
const hasPrompt = !!App.getInstallPrompt();
_container.innerHTML = `
<div class="wc-landing">
<!-- ── Hero ─────────────────────────────────────────── -->
<div class="wc-lhero">
<img src="/icons/icon-180.png" alt="Ban Yaro" class="wc-lhero-icon">
<h1 class="wc-lhero-headline" id="wc-hero-headline">${HERO_SLIDES[0].headline}</h1>
<p class="wc-lhero-sub" id="wc-hero-sub">${HERO_SLIDES[0].sub}</p>
<span class="wc-hero-counter" id="wc-hero-counter">1 / ${HERO_SLIDES.length}</span>
<div class="wc-phone-frame">
<img class="wc-phone-screen" id="wc-phone-screen"
src="${HERO_SLIDES[0].screen}" alt="" loading="eager">
</div>
<div class="wc-lhero-cta">
${hasPrompt ? `
<button class="btn wc-btn-hero" id="welcome-install-hero-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
App installieren
</button>
` : `
<button class="btn wc-btn-hero" id="welcome-register-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>
Kostenlos loslegen
</button>
`}
<button class="btn wc-btn-login" id="welcome-login-btn">
Schon dabei? Anmelden
</button>
</div>
<div class="wc-trust-strip">
<span>
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-circle"></use></svg>
Kostenlos
</span>
<span>
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#device-mobile"></use></svg>
Kein App Store
</span>
<span>
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield-check"></use></svg>
Daten in Deutschland
</span>
</div>
</div>
<!-- ── Feature 1: Tagebuch ───────────────────────────── -->
<div class="wc-feature wc-feature--a">
<div class="wc-feature-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
</div>
<div class="wc-feature-text">
<h2>Tagebuch & Erinnerungen</h2>
<p>Halte jeden gemeinsamen Moment fest — Fotos, Einträge, Stimmungen. Nur für dich, privat und sicher.</p>
</div>
</div>
<!-- ── Feature 2: Gesundheit ─────────────────────────── -->
<div class="wc-feature wc-feature--b">
<div class="wc-feature-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>
</div>
<div class="wc-feature-text">
<h2>Gesundheit im Blick</h2>
<p>Impfungen, Gewicht, Tierarzttermine — alles an einem Ort. Du siehst immer, wann was ansteht.</p>
</div>
</div>
<!-- ── Feature 3: Community ──────────────────────────── -->
<div class="wc-feature wc-feature--c">
<div class="wc-feature-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-trifold"></use></svg>
</div>
<div class="wc-feature-text">
<h2>Community vor Ort</h2>
<p>Giftköder-Warnungen, Gassi-Treffen, Events — was in deiner Gegend gerade passiert.</p>
</div>
</div>
<!-- ── Feature 4: Training ──────────────────────────── -->
<div class="wc-feature wc-feature--d">
<div class="wc-feature-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg>
</div>
<div class="wc-feature-text">
<h2>Training & KI-Trainer</h2>
<p>Über 100 Übungen mit Schritt-für-Schritt-Anleitungen. Mit KI-Unterstützung, die deinen Hund kennt.</p>
</div>
</div>
<!-- ── Privacy Block ─────────────────────────────────── -->
<div class="wc-privacy">
<div class="wc-privacy-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield-check"></use></svg>
</div>
<h2 class="wc-privacy-title">Deine Daten gehören dir.</h2>
<p class="wc-privacy-sub">
Kein Facebook. Kein Google. Keine Werbung.<br>
Ban Yaro läuft auf einem eigenen Server in Deutschland —
dein Tagebuch, deine Routen, deine Gesundheitsdaten
bleiben privat.
</p>
</div>
<!-- ── Und noch mehr (einklappbar) ─────────────────────── -->
<div class="wc-more">
<button class="wc-more-toggle" id="wc-more-toggle" aria-expanded="false">
<span>Und noch viel mehr</span>
<svg class="ph-icon wc-more-chevron" aria-hidden="true"><use href="/icons/phosphor.svg#caret-down"></use></svg>
</button>
<div class="wc-grid wc-grid--collapsed" id="wc-more-grid">
${FEATURES.map(f => `
<div class="wc-tile wc-tile--static">
<div class="wc-tile-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${f.icon}"></use></svg>
</div>
<span class="wc-tile-label">${f.label}</span>
</div>
`).join('')}
</div>
</div>
<!-- ── Bottom CTA ────────────────────────────────────── -->
<div class="wc-bottom-cta">
${hasPrompt ? `
<button class="btn wc-btn-hero" id="welcome-install-hero-btn2">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
App installieren — kostenlos
</button>
` : `
<button class="btn wc-btn-hero" id="welcome-register-btn2">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>
Jetzt kostenlos starten
</button>
`}
<p class="wc-bottom-hint">Kein App Store · Direkt auf den Home-Bildschirm</p>
${!isInstalled ? `
<button class="wc-install-link" id="welcome-install-link">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
Installationsanleitung
</button>
` : ''}
</div>
<!-- ── Install Card (via Einstellungen) ──────────────── -->
${_showInstall ? `
<div class="page-container" style="padding:0 var(--space-4) var(--space-6)">
<div class="card wc-install-card">
<div class="wc-install-header">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
App installieren
</div>
<div style="padding:var(--space-4)">${_installHTML()}</div>
</div>
</div>
` : ''}
<p class="wc-footer">Ban Yaro · Deine Daten auf eigenem Server in Deutschland</p>
</div>
`;
}
// ----------------------------------------------------------
// HILFSFUNKTIONEN (eingeloggte Ansicht)
// ----------------------------------------------------------
function _relDate(dateStr) {
if (!dateStr) return null;
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86400000);
if (diff === 0) return 'Heute';
if (diff === 1) return 'Gestern';
if (diff > 1 && diff < 14) return `vor ${diff} Tagen`;
if (diff === -1) return 'Morgen';
if (diff < 0 && diff > -14) return `in ${-diff} Tagen`;
return dateStr;
}
function _greeting() {
const h = new Date().getHours();
if (h >= 5 && h < 11) return 'Guten Morgen';
if (h >= 11 && h < 18) return 'Moin';
if (h >= 18 && h < 22) return 'Guten Abend';
return 'Hey';
}
function _appointmentIcon(typ) {
if (!typ) return 'calendar-check';
const t = typ.toLowerCase();
if (t === 'impfung') return 'syringe';
if (t === 'tierarzt') return 'stethoscope';
if (t === 'medikament') return 'pill';
return 'calendar-check';
}
// Die 4 Feature-Karten für die eingeloggte Ansicht
const FEATURE_CARDS = [
{ icon: 'book-open', label: 'Tagebuch', desc: 'Fotos, Notizen, Erinnerungen', page: 'diary' },
{ icon: 'first-aid', label: 'Gesundheit', desc: 'Impfungen, Gewicht, Termine', page: 'health' },
{ icon: 'map-trifold', label: 'Karte', desc: 'Spots & Alerts in deiner Nähe', page: 'map' },
{ icon: 'target', label: 'Training', desc: 'Übungen mit KI-Trainer', page: 'uebungen' },
];
const FEATURE_CARD_PAGES = new Set(FEATURE_CARDS.map(f => f.page));
function _heroHTML(dog, dashData) {
const photoUrl = dashData?.random_photo?.preview_url || dashData?.random_photo?.url || null;
const avatarUrl = dog?.foto_url || null;
const dogName = dog ? UI.escape(dog.name) : 'Dein Hund';
const dogRasse = dog?.rasse ? UI.escape(dog.rasse) : '';
const greeting = _greeting();
if (photoUrl) {
return `
<div class="wc-hero wc-hero--photo" id="wc-hero-box">
<img src="${UI.escape(photoUrl)}" class="wc-hero-bg-img" alt="">
<div class="wc-hero-overlay"></div>
${avatarUrl ? `<img src="${UI.escape(avatarUrl)}" class="wc-hero-avatar" alt="${dogName}">` : ''}
<div class="wc-hero-center">
<span class="wc-hero-greeting">${UI.escape(greeting)}</span>
<h1 class="wc-hero-title">${dogName}</h1>
${dogRasse ? `<p class="wc-hero-rasse">${dogRasse}</p>` : ''}
</div>
</div>`;
}
// Fallback: Gradient-Hero
return `
<div class="wc-hero wc-hero--gradient" id="wc-hero-box">
<img src="/icons/icon-180.png" alt="Ban Yaro" class="wc-hero-icon">
<div class="wc-hero-center">
<span class="wc-hero-greeting">${UI.escape(greeting)}</span>
<h1 class="wc-hero-title">${dogName}</h1>
${dogRasse ? `<p class="wc-hero-rasse">${dogRasse}</p>` : ''}
</div>
</div>`;
}
function _chip2HTML(dashData) {
const appt = dashData?.next_appointment;
if (!appt) return ''; // kein Termin → Chip fehlt; Bank kommt ggf. async
const apptLabel = UI.escape(appt.bezeichnung);
const apptDate = _relDate(appt.naechstes) || appt.naechstes || '—';
const apptIcon = _appointmentIcon(appt.typ);
return `
<div class="wc-chip" id="wc-chip-mid" data-nav="health">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${apptIcon}"></use></svg>
<span class="wc-chip-label">Nächster Termin</span>
<span class="wc-chip-val">${apptLabel} · ${apptDate}</span>
</div>`;
}
function _chipsHTML(dashData) {
const diaryDate = dashData?.last_diary?.datum;
const diaryText = diaryDate ? (_relDate(diaryDate) || diaryDate) : '—';
const weight = dashData?.last_weight;
const weightVal = weight ? `${weight.wert} ${weight.einheit}` : '—';
const ex = dashData?.daily_exercise;
return `
<div class="wc-chips" id="wc-chips-row">
<div class="wc-chip" data-nav="diary">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>
<span class="wc-chip-label">Tagebuch</span>
<span class="wc-chip-val${diaryDate ? '' : ' wc-chip-val--empty'}">${diaryText}</span>
</div>
${_chip2HTML(dashData)}
<div class="wc-chip" data-nav="health">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>
<span class="wc-chip-label">Gewicht</span>
<span class="wc-chip-val${weight ? '' : ' wc-chip-val--empty'}">${weightVal}</span>
</div>
${ex ? `
<div class="wc-chip" id="wc-chip-exercise"
data-exercise-name="${UI.escape(ex.name)}"
data-exercise-kat="${UI.escape(ex.kategorie || '')}"
data-exercise-id="${UI.escape(ex.exercise_id || '')}">
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg>
<span class="wc-chip-label">Übung des Tages</span>
<span class="wc-chip-val">${UI.escape(ex.name)}</span>
</div>` : ''}
</div>`;
}
// Berechnet async eine Tages-Gassirunde via ORS und ersetzt Chip 2
async function _tryRouteChip(dashData) {
if (dashData?.next_appointment) return; // Termin hat Vorrang
let loc;
try { loc = await API.getLocation({ timeout: 5000, maximumAge: 300000 }); }
catch { return; }
// Täglich stabile, aber rotierende Distanz + Variante
const dayIdx = Math.floor(Date.now() / 86400000);
const km = [2, 4, 6][dayIdx % 3];
const seed = dayIdx % 5;
// Tages-Cache prüfen — ORS nur einmal pro Tag anfragen
const today = new Date().toISOString().slice(0, 10); // 'YYYY-MM-DD'
const cacheKey = 'by_daily_route_' + today;
const cached = localStorage.getItem(cacheKey);
if (cached) {
try {
const result = JSON.parse(cached);
_applyRouteChip(result, km, seed);
return;
} catch {}
}
let result;
try {
result = await API.post('/routes/suggest', { lat: loc.lat, lon: loc.lon, distance_km: km, seed });
} catch { return; }
if (!result?.gps_track?.length) return;
// Ergebnis cachen und alte Einträge aufräumen
localStorage.setItem(cacheKey, JSON.stringify(result));
Object.keys(localStorage)
.filter(k => k.startsWith('by_daily_route_') && k !== cacheKey)
.forEach(k => localStorage.removeItem(k));
_applyRouteChip(result, km, seed);
}
function _applyRouteChip(result, km, seed) {
const chipsRow = _container.querySelector('#wc-chips-row');
if (!chipsRow) return;
let chip2 = _container.querySelector('#wc-chip-mid');
if (!chip2) {
chip2 = document.createElement('div');
chip2.className = 'wc-chip';
chip2.id = 'wc-chip-mid';
const first = chipsRow.querySelector('.wc-chip');
first ? first.after(chip2) : chipsRow.prepend(chip2);
}
const durStr = result.dauer_min < 60
? `${result.dauer_min} min`
: `${Math.floor(result.dauer_min / 60)}h ${result.dauer_min % 60}min`;
chip2.innerHTML = `
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
<span class="wc-chip-label">Gassirunde</span>
<span class="wc-chip-val">${result.distanz_km} km · ${durStr}</span>`;
chip2.addEventListener('click', () => {
App.navigate('routes', true, { _suggestResult: result, _suggestKm: km, _suggestSeed: seed });
});
}
// ----------------------------------------------------------
// EINGELOGGTE ANSICHT — visueller Überblick
// ----------------------------------------------------------
function _renderLoggedIn(isInstalled) {
const dog = _appState?.activeDog || null;
_container.innerHTML = `
<div class="welcome-layout" id="wc-logged-root">
${_heroHTML(dog, null)}
${_chipsHTML(null)}
<div class="page-container wc-li-body">
<div class="wc-feature-grid">
${FEATURE_CARDS.map(f => `
<button class="wc-fcard" data-nav="${f.page}">
<div class="wc-fcard-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${f.icon}"></use></svg>
</div>
<span class="wc-fcard-title">${f.label}</span>
<span class="wc-fcard-desc">${f.desc}</span>
</button>
`).join('')}
</div>
${dog?.id ? `<div id="wc-streak-widget"></div>` : ''}
<h2 class="wc-section-title" style="margin-top:var(--space-6)">Mehr entdecken</h2>
<div class="wc-grid">
${FEATURES.filter(f => !FEATURE_CARD_PAGES.has(f.page)).map(f => `
<button class="wc-tile" data-nav="${f.page}">
<div class="wc-tile-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${f.icon}"></use></svg>
</div>
<span class="wc-tile-label">${f.label}</span>
</button>
`).join('')}
</div>
${(!isInstalled || _showInstall) ? `
<div class="card wc-install-card">
<div class="wc-install-header">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
App installieren
</div>
<div style="padding:var(--space-4)">${_installHTML()}</div>
</div>
` : ''}
<p class="wc-footer">Ban Yaro · Deine Daten auf eigenem Server in Deutschland</p>
</div>
</div>
`;
// Asynchrones Dashboard laden
if (dog?.id) {
API.dogs.welcomeDashboard(dog.id).then(dash => {
_updateHeroFromDash(dash, dog);
_updateChipsFromDash(dash);
_tryRouteChip(dash);
}).catch(() => { /* Skeleton bleibt sichtbar */ });
// Hero-Foto stündlich auffrischen (Tageswechsel um Mitternacht sichtbar ohne Reload)
setInterval(() => {
API.dogs.welcomeDashboard(dog.id)
.then(dash => _updateHeroFromDash(dash, dog))
.catch(() => {});
}, 60 * 60 * 1000);
// Streak-Widget asynchron laden
_loadStreakWidget(dog.id);
}
}
// ----------------------------------------------------------
// STREAK-WIDGET
// ----------------------------------------------------------
async function _loadStreakWidget(dogId) {
const slot = _container.querySelector('#wc-streak-widget');
if (!slot) return;
let streak;
try {
streak = await API.get(`/streak/${dogId}`);
} catch { return; }
if (!streak || (streak.current_streak === 0 && streak.longest_streak === 0)) return;
slot.innerHTML = _streakWidgetHTML(streak);
slot.querySelector('#wc-streak-leaderboard-btn')?.addEventListener('click', async () => {
const modalEl = UI.modal.open({
title: '🔥 Trainings-Bestenliste',
body: '<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-4)">Wird geladen…</p>',
});
let board;
try { board = await API.get('/streak/leaderboard'); } catch { board = []; }
const bodyEl = modalEl?.querySelector('.modal-body');
if (bodyEl) bodyEl.innerHTML = _leaderboardHTML(board);
});
}
function _streakWidgetHTML(s) {
const cur = s.current_streak || 0;
const best = s.longest_streak || 0;
return `
<div class="wc-streak-card">
<div class="wc-streak-flame-wrap">
<span class="wc-streak-flame">🔥</span>
<span class="wc-streak-number">${cur}</span>
</div>
<div class="wc-streak-info">
<div class="wc-streak-label">Tage in Folge trainiert</div>
<div class="wc-streak-best">Rekord: ${best} ${best === 1 ? 'Tag' : 'Tage'}</div>
</div>
<button class="wc-streak-lb-btn" id="wc-streak-leaderboard-btn" title="Bestenliste">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trophy"></use></svg>
</button>
</div>`;
}
function _leaderboardHTML(rows) {
if (!rows || !rows.length) {
return '<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-4)">Noch keine Einträge.</p>';
}
const medals = ['🥇', '🥈', '🥉'];
return `
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
${rows.map((r, i) => `
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--c-border-subtle)">
<span style="font-size:1.4rem;width:28px;text-align:center;flex-shrink:0">${medals[i] || (i + 1) + '.'}</span>
${r.foto_url
? `<img src="${UI.escape(r.foto_url)}" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0" alt="">`
: `<div style="width:36px;height:36px;border-radius:50%;background:var(--c-primary-subtle);flex-shrink:0"></div>`}
<div style="flex:1;min-width:0">
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${UI.escape(r.dog_name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.escape(r.rasse || '')}${r.user_name ? ' · ' + UI.escape(r.user_name) : ''}</div>
</div>
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
<span style="font-size:1.1rem">🔥</span>
<span style="font-weight:var(--weight-bold);font-size:var(--text-base);color:var(--c-primary)">${r.current_streak}</span>
</div>
</div>
`).join('')}
</div>`;
}
function _updateHeroFromDash(dash, dog) {
const heroBox = _container.querySelector('#wc-hero-box');
if (!heroBox) return;
const photoUrl = dash?.random_photo?.preview_url || dash?.random_photo?.url;
const avatarUrl = dog?.foto_url;
const dogName = dog ? UI.escape(dog.name) : 'Dein Hund';
const dogRasse = dog?.rasse ? UI.escape(dog.rasse) : '';
const greeting = _greeting();
if (photoUrl) {
heroBox.className = 'wc-hero wc-hero--photo';
heroBox.innerHTML = `
<img src="${UI.escape(photoUrl)}" class="wc-hero-bg-img" alt="">
<div class="wc-hero-overlay"></div>
${avatarUrl ? `<img src="${UI.escape(avatarUrl)}" class="wc-hero-avatar" alt="${dogName}">` : ''}
<div class="wc-hero-center">
<span class="wc-hero-greeting">${UI.escape(greeting)}</span>
<h1 class="wc-hero-title">${dogName}</h1>
${dogRasse ? `<p class="wc-hero-rasse">${dogRasse}</p>` : ''}
</div>`;
}
}
function _updateChipsFromDash(dash) {
const chipsRow = _container.querySelector('#wc-chips-row');
if (!chipsRow) return;
chipsRow.outerHTML = _chipsHTML(dash);
_container.querySelectorAll('#wc-chips-row [data-nav]').forEach(el => {
el.addEventListener('click', () => App.navigate(el.dataset.nav));
});
// Exercise-Chip hat kein data-nav — separat binden
const exChip = _container.querySelector('#wc-chip-exercise');
if (exChip) {
exChip.addEventListener('click', () => {
App.navigate('uebungen', true, {
name: exChip.dataset.exerciseName,
kategorie: exChip.dataset.exerciseKat,
exercise_id: exChip.dataset.exerciseId,
});
});
}
}
// ----------------------------------------------------------
// STYLES
// ----------------------------------------------------------
function _injectStyles() {
if (document.getElementById('wc-styles')) return;
const s = document.createElement('style');
s.id = 'wc-styles';
s.textContent = `
/* ── Landing Page ──────────────────────────────────────── */
.wc-landing { display: flex; flex-direction: column; }
/* Hero */
.wc-lhero {
background: linear-gradient(160deg, var(--c-primary) 0%, color-mix(in srgb, var(--c-primary) 70%, transparent) 60%, var(--c-bg) 100%);
text-align: center;
padding: var(--space-8) var(--space-5) var(--space-8);
position: relative;
overflow: hidden;
}
.wc-lhero::before {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse at 50% 0%, rgba(255,255,255,0.18) 0%, transparent 65%);
pointer-events: none;
}
.wc-lhero-icon {
width: 80px; height: 80px;
border-radius: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
display: block; margin: 0 auto var(--space-5);
position: relative;
}
.wc-lhero-headline {
font-size: 2.6rem; font-weight: 800; line-height: 1.1;
color: #fff;
margin: 0 0 var(--space-4);
text-shadow: 0 2px 12px rgba(0,0,0,0.15);
letter-spacing: -0.02em;
position: relative;
transition: opacity 0.4s ease;
}
.wc-lhero-sub {
font-size: var(--text-base); color: rgba(255,255,255,0.88);
line-height: 1.6; margin: 0 0 var(--space-3);
position: relative;
transition: opacity 0.4s ease;
}
.wc-hero-counter {
display: block; text-align: center;
font-size: var(--text-xs); color: rgba(255,255,255,0.55);
font-weight: var(--weight-semibold); letter-spacing: 0.08em;
margin-bottom: var(--space-5); position: relative;
}
.wc-phone-frame {
width: 188px;
border-radius: 30px;
border: 3px solid rgba(255,255,255,0.28);
box-shadow: 0 24px 64px rgba(0,0,0,0.45), 0 0 0 1px rgba(0,0,0,0.12);
overflow: hidden;
margin: 0 auto var(--space-5);
position: relative;
background: #111;
}
.wc-phone-screen {
width: 100%;
display: block;
margin-top: -24px; /* iOS-Statusleiste abschneiden */
transition: opacity 0.4s ease;
}
.wc-lhero-cta {
display: flex; flex-direction: column;
align-items: center; gap: var(--space-3);
margin-bottom: var(--space-6);
position: relative;
}
.wc-btn-hero {
background: #fff; color: var(--c-primary);
font-size: var(--text-base); font-weight: var(--weight-bold);
padding: 14px var(--space-6); border-radius: 100px;
border: none; width: 100%; max-width: 320px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
display: flex; align-items: center; justify-content: center; gap: var(--space-2);
transition: transform 0.1s, box-shadow 0.1s;
}
.wc-btn-hero:active { transform: scale(0.97); box-shadow: 0 2px 10px rgba(0,0,0,0.15); }
.wc-btn-login {
background: rgba(255,255,255,0.15);
color: #fff;
font-size: var(--text-sm); font-weight: var(--weight-semibold);
padding: 12px var(--space-6); border-radius: 100px;
border: 1.5px solid rgba(255,255,255,0.65);
width: 100%; max-width: 320px;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s;
}
.wc-btn-login:hover { background: rgba(255,255,255,0.22); }
.wc-btn-login:active { background: rgba(255,255,255,0.28); }
.wc-trust-strip {
display: flex; justify-content: center;
gap: var(--space-3); flex-wrap: nowrap;
position: relative;
}
.wc-trust-strip span {
display: flex; align-items: center; gap: 4px;
font-size: 0.7rem; color: rgba(255,255,255,0.8);
font-weight: var(--weight-semibold); white-space: nowrap;
}
.wc-trust-strip .ph-icon { width: 12px; height: 12px; flex-shrink: 0; }
/* Feature Cards */
.wc-feature {
display: flex; align-items: flex-start; gap: var(--space-4);
padding: var(--space-5) var(--space-5);
border-bottom: 1px solid var(--c-border-light);
background: var(--c-bg);
}
.wc-feature--a, .wc-feature--b, .wc-feature--c, .wc-feature--d { background: var(--c-bg); }
.wc-feature-icon {
width: 36px; height: 36px; flex-shrink: 0; margin-top: 2px;
display: flex; align-items: center; justify-content: center;
}
.wc-feature-icon .ph-icon { width: 24px; height: 24px; color: var(--c-primary); opacity: 0.8; }
.wc-feature-text h2 {
font-size: var(--text-sm); font-weight: var(--weight-semibold);
color: var(--c-text); margin: 0 0 var(--space-1);
}
.wc-feature-text p {
font-size: var(--text-sm); color: var(--c-text-secondary);
line-height: 1.6; margin: 0;
}
/* Privacy Block */
.wc-privacy {
background: var(--c-primary);
padding: var(--space-8) var(--space-5);
text-align: center;
}
.wc-privacy-icon {
width: 56px; height: 56px; border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex; align-items: center; justify-content: center;
margin: 0 auto var(--space-4);
}
.wc-privacy-icon .ph-icon { width: 28px; height: 28px; color: #fff; }
.wc-privacy-title {
font-size: var(--text-xl); font-weight: var(--weight-bold);
color: #fff; margin: 0 0 var(--space-3);
}
.wc-privacy-sub {
font-size: var(--text-sm); color: rgba(255,255,255,0.85);
line-height: 1.7; margin: 0; max-width: 360px; margin: 0 auto;
}
/* Und noch mehr */
.wc-more {
padding: var(--space-4) var(--space-4);
background: var(--c-bg);
border-top: 1px solid var(--c-border-light);
}
.wc-more-toggle {
display: flex; align-items: center; justify-content: center;
gap: var(--space-2); width: 100%;
background: none; border: none; cursor: pointer;
font-size: var(--text-xs); font-weight: var(--weight-semibold);
color: var(--c-text-secondary); text-transform: uppercase;
letter-spacing: 0.07em; padding: var(--space-2) 0;
margin-bottom: var(--space-3);
}
.wc-more-chevron {
width: 14px; height: 14px;
transition: transform 0.25s ease;
}
.wc-grid.wc-grid--collapsed { display: none; }
/* Bottom CTA */
.wc-bottom-cta {
padding: var(--space-8) var(--space-5) var(--space-6);
display: flex; flex-direction: column; align-items: center;
gap: var(--space-3); background: var(--c-bg);
border-top: 1px solid var(--c-border-light);
}
.wc-bottom-hint {
font-size: var(--text-xs); color: var(--c-text-muted); margin: 0;
}
.wc-install-link {
background: none; border: none; cursor: pointer;
font-size: var(--text-xs); color: var(--c-text-secondary);
display: flex; align-items: center; gap: 4px;
padding: var(--space-2); margin-top: var(--space-1);
}
.wc-install-link .ph-icon { width: 12px; height: 12px; }
/* ── Logged-in Hero ─────────────────────────────────────── */
.wc-hero {
position: relative;
min-height: 220px;
display: flex; align-items: flex-end; justify-content: center;
padding: var(--space-6) var(--space-4) var(--space-5);
overflow: hidden;
border-bottom: 1px solid var(--c-border-light);
text-align: center;
}
/* Gradient fallback */
.wc-hero--gradient {
background: linear-gradient(160deg, var(--c-primary) 0%, color-mix(in srgb, var(--c-primary) 60%, var(--c-bg)) 100%);
flex-direction: column; align-items: center; justify-content: center;
}
/* Photo variant */
.wc-hero--photo {
min-height: 260px;
flex-direction: column; align-items: center; justify-content: flex-end;
}
.wc-hero-bg-img {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: cover; object-position: center center;
z-index: 0;
}
.wc-hero-overlay {
position: absolute; inset: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.08) 0%, rgba(0,0,0,0.55) 100%);
pointer-events: none;
}
.wc-hero-avatar {
position: absolute; top: var(--space-4); right: var(--space-4);
width: 52px; height: 52px; border-radius: 50%;
border: 2.5px solid rgba(255,255,255,0.7);
object-fit: cover;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 2;
}
.wc-hero-center {
position: relative; z-index: 2;
display: flex; flex-direction: column; align-items: center;
gap: 2px;
}
.wc-hero-greeting {
font-size: var(--text-sm); font-weight: var(--weight-semibold);
color: rgba(255,255,255,0.82); letter-spacing: 0.04em;
text-transform: uppercase;
}
.wc-hero-title {
font-size: var(--text-2xl); font-weight: var(--weight-bold);
color: #fff; margin: 0;
text-shadow: 0 2px 8px rgba(0,0,0,0.25);
}
.wc-hero-rasse {
font-size: var(--text-sm); color: rgba(255,255,255,0.72);
margin: 0; line-height: 1.4;
}
.wc-hero-icon {
width: 72px; height: 72px; border-radius: 18px;
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
display: block; margin: 0 auto var(--space-3);
position: relative; z-index: 2;
}
/* ── Stats-Chips ─────────────────────────────────────────── */
.wc-chips {
display: flex; gap: var(--space-3);
padding: var(--space-3) var(--space-4);
overflow-x: auto; scrollbar-width: none;
background: var(--c-surface);
border-bottom: 1px solid var(--c-border-light);
-webkit-overflow-scrolling: touch;
}
.wc-chips::-webkit-scrollbar { display: none; }
.wc-chip {
display: flex; flex-direction: column; gap: 2px;
flex-shrink: 0;
background: var(--c-bg); border: 1px solid var(--c-border-light);
border-radius: var(--radius-lg);
padding: var(--space-3) var(--space-4);
cursor: pointer; min-width: 130px;
transition: background 0.15s;
}
.wc-chip:hover { background: var(--c-surface-2, var(--c-surface)); }
.wc-chip:active { transform: scale(0.97); }
.wc-chip-icon { width: 18px; height: 18px; color: var(--c-primary); margin-bottom: 2px; }
.wc-chip-label {
font-size: var(--text-xs); color: var(--c-text-muted);
font-weight: var(--weight-semibold); text-transform: uppercase;
letter-spacing: 0.05em; white-space: nowrap;
}
.wc-chip-val {
font-size: var(--text-sm); color: var(--c-text);
font-weight: var(--weight-semibold); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; max-width: 180px;
}
.wc-chip-val--empty { color: var(--c-text-muted); font-weight: normal; }
/* ── Feature-Karten (2×2) ────────────────────────────────── */
.wc-li-body { padding: var(--space-5) var(--space-4); }
.wc-feature-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-3);
margin-bottom: var(--space-2);
}
.wc-fcard {
display: flex; flex-direction: column; align-items: flex-start;
gap: var(--space-2); padding: var(--space-4);
background: var(--c-surface); border: 1px solid var(--c-border-light);
border-radius: var(--radius-lg); cursor: pointer; text-align: left;
transition: background 0.15s, transform 0.1s;
}
.wc-fcard:hover { background: var(--c-surface-2, var(--c-surface)); }
.wc-fcard:active { transform: scale(0.97); }
.wc-fcard-icon {
width: 44px; height: 44px; border-radius: var(--radius-md);
background: var(--c-primary-subtle);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.wc-fcard-icon .ph-icon { width: 22px; height: 22px; color: var(--c-primary); }
.wc-fcard-title {
font-size: var(--text-sm); font-weight: var(--weight-bold);
color: var(--c-text); line-height: 1.2;
}
.wc-fcard-desc {
font-size: var(--text-xs); color: var(--c-text-secondary);
line-height: 1.4;
}
/* ── Shared ────────────────────────────────────────────── */
.wc-section-title {
font-size: var(--text-xs); font-weight: var(--weight-semibold);
color: var(--c-text-secondary); text-transform: uppercase;
letter-spacing: 0.07em; margin: 0 0 var(--space-4); text-align: center;
}
.wc-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.wc-tile {
display: flex; flex-direction: column; align-items: center;
gap: var(--space-2); padding: var(--space-4) var(--space-2);
background: var(--c-surface); border: 1px solid var(--c-border-light);
border-radius: var(--radius-lg); cursor: pointer;
transition: background var(--transition-fast), transform 0.1s;
}
.wc-tile:hover { background: var(--c-surface-2); }
.wc-tile:active { transform: scale(0.96); }
.wc-tile--static { cursor: default; }
.wc-tile--static:hover { background: var(--c-surface); }
.wc-tile--static:active { transform: none; }
.wc-tile-icon {
width: 40px; height: 40px; border-radius: var(--radius-md);
background: var(--c-primary-subtle);
display: flex; align-items: center; justify-content: center;
}
.wc-tile-icon .ph-icon { width: 20px; height: 20px; color: var(--c-primary); }
.wc-tile-label {
font-size: var(--text-xs); font-weight: var(--weight-semibold);
color: var(--c-text); text-align: center; line-height: 1.3;
}
.wc-install-card { margin-bottom: var(--space-5); }
.wc-install-header {
display: flex; align-items: center; gap: var(--space-2);
padding: var(--space-3) var(--space-4);
font-size: var(--text-xs); font-weight: 600;
color: var(--c-text-secondary); text-transform: uppercase;
letter-spacing: 0.05em; border-bottom: 1px solid var(--c-border);
}
.wc-install-header .ph-icon { width: 14px; height: 14px; }
.wc-footer {
text-align: center; font-size: var(--text-xs);
color: var(--c-text-muted); padding: var(--space-4);
}
@media (min-width: 640px) {
.wc-lhero-headline { font-size: 3.2rem; }
.wc-lhero-cta { flex-direction: row; justify-content: center; }
.wc-btn-hero { width: auto; }
.wc-grid { grid-template-columns: repeat(4, 1fr); }
.wc-phone-frame { width: 220px; }
}
@media (min-width: 1024px) {
.wc-lhero { padding: var(--space-8) var(--space-4) var(--space-10); }
.wc-feature { max-width: 680px; margin: 0 auto; }
.wc-grid { grid-template-columns: repeat(8, 1fr); }
}
@keyframes wc-pulse {
0%,100% { transform: scale(1); box-shadow: none; }
50% { transform: scale(1.25); box-shadow: 0 0 0 6px var(--c-primary-subtle); }
}
.wc-pulsing { animation: wc-pulse 0.6s ease-in-out 3; border-radius: var(--radius-md); }
`;
document.head.appendChild(s);
}
// ----------------------------------------------------------
// HERO-ROTATION
// ----------------------------------------------------------
function _startHeroRotation() {
let idx = 0;
const headline = _container.querySelector('#wc-hero-headline');
const sub = _container.querySelector('#wc-hero-sub');
const counter = _container.querySelector('#wc-hero-counter');
const screen = _container.querySelector('#wc-phone-screen');
if (!headline || !sub) return;
// nächste Screenshots vorladen
HERO_SLIDES.forEach(s => { const i = new Image(); i.src = s.screen; });
_heroInterval = setInterval(() => {
headline.style.opacity = '0';
sub.style.opacity = '0';
if (screen) screen.style.opacity = '0';
setTimeout(() => {
idx = (idx + 1) % HERO_SLIDES.length;
const slide = HERO_SLIDES[idx];
headline.textContent = slide.headline;
sub.textContent = slide.sub;
if (screen) screen.src = slide.screen;
headline.style.opacity = '1';
sub.style.opacity = '1';
if (screen) screen.style.opacity = '1';
if (counter) counter.textContent = `${idx + 1} / ${HERO_SLIDES.length}`;
}, 400);
}, 6000);
}
// ----------------------------------------------------------
// INSTALLATIONS-ANLEITUNG
// ----------------------------------------------------------
function _installHTML() {
const ua = navigator.userAgent;
const isIOS = /iPad|iPhone|iPod/.test(ua) && !window.MSStream;
const isSafari = /^((?!chrome|android).)*safari/i.test(ua);
const isAndroid = /android/i.test(ua);
const hasPrompt = !!App.getInstallPrompt();
if (hasPrompt) {
return `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3);line-height:1.5">
Kein App Store nötig — direkt auf den Home-Bildschirm.
</p>
<button class="btn btn-primary" id="install-android-btn" style="width:100%;margin-bottom:var(--space-2)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
Ban Yaro installieren
</button>`;
}
if (isIOS && isSafari) {
return `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-4);line-height:1.5">
So kommt Ban Yaro auf deinen Home-Bildschirm:
</p>
${_steps([
['share', 'Tippe unten in Safari auf das <strong>Teilen-Symbol</strong> <svg class="ph-icon" style="width:14px;height:14px;vertical-align:-2px"><use href="/icons/phosphor.svg#share"></use></svg>'],
['rows-plus-top', 'Wähle <strong>„Zum Home-Bildschirm"</strong> aus der Liste'],
['check', 'Tippe oben rechts auf <strong>„Hinzufügen"</strong>'],
])}
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:var(--space-4) 0 0">
Funktioniert nur in Safari — nicht in Chrome oder Firefox auf iOS.
</p>`;
}
if (isIOS && !isSafari) {
return `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-4);line-height:1.5">
Auf dem iPhone geht die Installation nur über <strong>Safari</strong>.
</p>
${_steps([
['safari-logo', 'Öffne <strong>Safari</strong> auf deinem iPhone'],
['arrow-square-in','Gib <strong>banyaro.app</strong> in die Adressleiste ein'],
['share', 'Tippe auf das Teilen-Symbol und wähle <strong>„Zum Home-Bildschirm"</strong>'],
])}
<button class="btn btn-ghost btn-sm" id="install-copy-btn"
style="margin-top:var(--space-3);width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#copy"></use></svg>
Link kopieren
</button>`;
}
if (isAndroid) {
return `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-4);line-height:1.5">
Am einfachsten geht es mit <strong>Chrome</strong>:
</p>
${_steps([
['arrow-square-in', 'Öffne <strong>banyaro.app</strong> in Chrome'],
['dots-three-circle','Tippe auf das <strong>Menü ⋮</strong> oben rechts'],
['download-simple', 'Wähle <strong>„App installieren"</strong> oder<br>„Zum Startbildschirm hinzufügen"'],
])}
<button class="btn btn-ghost btn-sm" id="install-copy-btn"
style="margin-top:var(--space-3);width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#copy"></use></svg>
Link kopieren
</button>`;
}
return `
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4)">
<button class="btn btn-sm" id="inst-tab-android"
style="flex:1;background:var(--c-primary);color:#fff;border:none">
Android
</button>
<button class="btn btn-sm btn-ghost" id="inst-tab-ios" style="flex:1">
iPhone / iPad
</button>
</div>
<div id="inst-panel-android">
${_steps([
['arrow-square-in', 'Öffne <strong>banyaro.app</strong> in Chrome oder Edge'],
['monitor', 'Klicke auf das <strong>Installations-Symbol</strong> in der Adressleiste'],
['check', 'Bestätigen — fertig!'],
])}
</div>
<div id="inst-panel-ios" style="display:none">
${_steps([
['arrow-square-in', 'Öffne <strong>banyaro.app</strong> in Safari auf dem iPhone'],
['share', 'Tippe auf das <strong>Teilen-Symbol</strong> <svg class="ph-icon" style="width:14px;height:14px;vertical-align:-2px"><use href="/icons/phosphor.svg#share"></use></svg>'],
['rows-plus-top', 'Wähle <strong>„Zum Home-Bildschirm"</strong> → <strong>„Hinzufügen"</strong>'],
])}
</div>`;
}
function _steps(list) {
return `
<ol style="margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--space-3)">
${list.map(([icon, text]) => `
<li style="display:flex;gap:var(--space-3);align-items:flex-start">
<div style="width:32px;height:32px;border-radius:var(--radius-md);flex-shrink:0;
background:var(--c-surface-2);display:flex;align-items:center;justify-content:center">
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-primary)" aria-hidden="true">
<use href="/icons/phosphor.svg#${icon}"></use>
</svg>
</div>
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5;padding-top:6px">${text}</span>
</li>
`).join('')}
</ol>`;
}
// ----------------------------------------------------------
// EVENTS
// ----------------------------------------------------------
function _bindEvents() {
// Install-Prompt Buttons
const installBtn = async () => {
const prompt = App.getInstallPrompt();
if (!prompt) return;
prompt.prompt();
const { outcome } = await prompt.userChoice;
if (outcome === 'accepted') { UI.toast.success('Ban Yaro wird installiert!'); _render(); }
};
_container.querySelector('#install-android-btn')?.addEventListener('click', installBtn);
_container.querySelector('#welcome-install-hero-btn')?.addEventListener('click', installBtn);
_container.querySelector('#welcome-install-hero-btn2')?.addEventListener('click', installBtn);
// Register / Login
const toSettings = () => App.navigate('settings');
_container.querySelector('#welcome-register-btn')?.addEventListener('click', toSettings);
_container.querySelector('#welcome-register-btn2')?.addEventListener('click', toSettings);
_container.querySelector('#welcome-login-btn')?.addEventListener('click', toSettings);
// Installationsanleitung Link
_container.querySelector('#welcome-install-link')?.addEventListener('click', () => {
App.navigate('welcome', true, { install: true });
});
// Link kopieren
_container.querySelector('#install-copy-btn')?.addEventListener('click', async () => {
await navigator.clipboard.writeText('https://banyaro.app');
UI.toast.success('Link kopiert!');
});
// Desktop-Tabs Android / iOS
_container.querySelector('#inst-tab-android')?.addEventListener('click', () => {
_container.querySelector('#inst-panel-android').style.display = '';
_container.querySelector('#inst-panel-ios').style.display = 'none';
_container.querySelector('#inst-tab-android').style.cssText += ';background:var(--c-primary);color:#fff;border:none';
_container.querySelector('#inst-tab-ios').className = 'btn btn-sm btn-ghost';
_container.querySelector('#inst-tab-ios').style.cssText = 'flex:1';
});
_container.querySelector('#inst-tab-ios')?.addEventListener('click', () => {
_container.querySelector('#inst-panel-android').style.display = 'none';
_container.querySelector('#inst-panel-ios').style.display = '';
_container.querySelector('#inst-tab-ios').style.cssText += ';background:var(--c-primary);color:#fff;border:none';
_container.querySelector('#inst-tab-android').className = 'btn btn-sm btn-ghost';
_container.querySelector('#inst-tab-android').style.cssText = 'flex:1';
});
// "Und noch mehr"-Toggle
const moreToggle = _container.querySelector('#wc-more-toggle');
const moreGrid = _container.querySelector('#wc-more-grid');
moreToggle?.addEventListener('click', () => {
const open = moreToggle.getAttribute('aria-expanded') === 'true';
moreToggle.setAttribute('aria-expanded', String(!open));
moreGrid.classList.toggle('wc-grid--collapsed', open);
moreToggle.querySelector('.wc-more-chevron').style.transform = open ? '' : 'rotate(180deg)';
});
// Feature-Tiles (nur eingeloggte Ansicht)
_container.querySelectorAll('[data-nav]').forEach(btn => {
btn.addEventListener('click', () => App.navigate(btn.dataset.nav));
});
// Exercise-Chip: navigiert direkt zur spezifischen Übung
const exChip = _container.querySelector('#wc-chip-exercise');
if (exChip) {
exChip.addEventListener('click', () => {
App.navigate('uebungen', true, {
name: exChip.dataset.exerciseName,
kategorie: exChip.dataset.exerciseKat,
exercise_id: exChip.dataset.exerciseId,
});
});
}
// Hero-Rotation starten (nur Landing)
if (!_appState?.user) _startHeroRotation();
}
function _pulseMenuBtn() {
const btn = document.getElementById('header-menu-btn');
if (!btn) return;
setTimeout(() => {
btn.classList.add('wc-pulsing');
btn.addEventListener('animationend', () => btn.classList.remove('wc-pulsing'), { once: true });
}, 1200);
}
return { init, refresh, onDogChange };
})();