banyaro/backend/static/js/pages/welcome.js
rene 459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
PHASE 1 — Sofort-Cleanup ohne Risiko:
- Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen:
  * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary
  * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3
  * flex-between, flex-1-min, mb-1/3, mt-1/3
  * icon-xs/sm/md/lg, label-block, caption
- index.html bindet utilities.css ein
- mb-3/mt-3 ergänzt (waren in design-system.css unvollständig)

PHASE 2 — .by-tab Modifier für Vereinheitlichung:
- .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.)
- .by-tabs.sticky (Desktop vertikale Tabs für Admin)
- .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll)
- .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border)

PHASE 3 — Inline-Style → Klassen-Migration (Python-Script):
- 948 Inline-Styles entfernt (5101 → 4153, -18%)
- 962 Migrationen über 47 Page-Dateien
- Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67),
  litters.js (62), settings.js (61), zuchthunde.js (51)
- Patterns: text-muted, text-secondary, text-danger, text-xs-muted,
  text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3,
  p-3/4, mb-2/3/4, hidden, w-full, flex-1, ...
- Bewahrt bestehende class-Attribute (mergt korrekt)

Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
2026-05-27 07:11:27 +02:00

1232 lines
54 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 isPWA = window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone === true;
if (!isPWA && !sessionStorage.getItem('by_stay_in_app')) {
window.location.replace('/info');
return;
}
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">
<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 registrieren
</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>
<!-- ── Install-Block ─────────────────────────────────── -->
${!isInstalled ? `
<div class="wc-install-block">
<div class="wc-install-block-header">
<div class="wc-install-block-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#device-mobile"></use></svg>
</div>
<div>
<div class="wc-install-block-title">Kein App Store nötig</div>
<div class="wc-install-block-sub">Füge Ban Yaro zum Home-Bildschirm hinzu — einmal, dann immer griffbereit</div>
</div>
</div>
<p class="wc-install-block-why">
Ban Yaro ist eine Web-App (PWA). Das bedeutet: kein App-Store-Download, automatische Updates ohne dein Zutun, und sie verhält sich genau wie eine native App — mit Icon, Vollbild und Offline-Modus.
</p>
${hasPrompt ? `
<button class="btn btn-primary wc-install-block-btn" id="welcome-install-hero-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
Jetzt zum Home-Bildschirm hinzufügen
</button>
` : `
<div class="wc-install-block-steps">${_installHTML()}</div>
`}
</div>
` : ''}
<p class="wc-footer">
<a href="/#impressum" class="text-muted">Impressum</a>
&nbsp;·&nbsp;
<a href="/#datenschutz" class="text-muted">Datenschutz</a>
&nbsp;·&nbsp;
<a href="/#agb" class="text-muted">AGB</a>
</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>
Immer griffbereit — kein App Store
</div>
<div class="p-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 class="flex-col-gap-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 class="flex-1-min">
<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 class="text-xs-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; }
/* Install Block */
.wc-install-block {
background: var(--c-bg-card);
border-top: 3px solid var(--c-primary);
padding: var(--space-5) var(--space-5) var(--space-6);
}
.wc-install-block-header {
display: flex; align-items: flex-start; gap: var(--space-3);
margin-bottom: var(--space-3);
}
.wc-install-block-icon {
width: 44px; height: 44px; border-radius: var(--radius-md);
background: var(--c-primary-subtle); flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.wc-install-block-icon .ph-icon { width: 22px; height: 22px; color: var(--c-primary); }
.wc-install-block-title {
font-size: var(--text-base); font-weight: var(--weight-bold);
color: var(--c-text); margin-bottom: 2px;
}
.wc-install-block-sub {
font-size: var(--text-sm); color: var(--c-text-secondary); line-height: 1.4;
}
.wc-install-block-why {
font-size: var(--text-sm); color: var(--c-text-secondary);
line-height: 1.6; margin: 0 0 var(--space-4);
padding: var(--space-3) var(--space-4);
background: var(--c-surface); border-radius: var(--radius-md);
border-left: 3px solid var(--c-primary);
}
.wc-install-block-btn {
width: 100%; font-size: var(--text-base);
padding: 14px; border-radius: var(--radius-lg);
display: flex; align-items: center; justify-content: center; gap: var(--space-2);
}
.wc-install-block-steps { margin-top: var(--space-2); }
/* 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">
Ban Yaro immer griffbereit — einmal hinzufügen, dann direkt vom Home-Bildschirm öffnen.
</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>
Zum Home-Bildschirm hinzufügen
</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 funktioniert das Hinzufügen 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"'],
])}
<div style="display:flex;gap:var(--space-2);align-items:flex-start;
background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-3) var(--space-3);margin-top:var(--space-4)">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:2px;color:var(--c-text-secondary)"
aria-hidden="true"><use href="/icons/phosphor.svg#shield-warning"></use></svg>
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin:0;line-height:1.5">
Falls eine Sicherheitswarnung erscheint: Das ist normal für neuere Webseiten — unabhängig vom Inhalt. Ban Yaro läuft auf einem deutschen Server, ist DSGVO-konform und enthält keine Schadsoftware. Tippe auf <strong>„Trotzdem hinzufügen"</strong>.
</p>
</div>
<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" class="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 style="display:flex;gap:var(--space-2);align-items:flex-start;
background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-3);margin-top:var(--space-4)">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:2px;color:var(--c-text-secondary)"
aria-hidden="true"><use href="/icons/phosphor.svg#shield-warning"></use></svg>
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin:0;line-height:1.5">
Falls eine Sicherheitswarnung erscheint: Das ist normal für neuere Webseiten. Tippe auf <strong>„Trotzdem hinzufügen"</strong>.
</p>
</div>
</div>
<div id="inst-panel-ios" class="hidden">
${_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
_container.querySelector('#welcome-register-btn')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'register' }));
_container.querySelector('#welcome-register-btn2')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'register' }));
_container.querySelector('#welcome-login-btn')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'login' }));
_container.querySelector('#welcome-login-btn2')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'login' }));
// 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 };
})();