/* ============================================================ 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 = `
Ban Yaro

${HERO_SLIDES[0].headline}

${HERO_SLIDES[0].sub}

1 / ${HERO_SLIDES.length}
${hasPrompt ? ` ` : ` `}
Kostenlos Kein App Store Daten in Deutschland

Tagebuch & Erinnerungen

Halte jeden gemeinsamen Moment fest — Fotos, Einträge, Stimmungen. Nur für dich, privat und sicher.

Gesundheit im Blick

Impfungen, Gewicht, Tierarzttermine — alles an einem Ort. Du siehst immer, wann was ansteht.

Community vor Ort

Giftköder-Warnungen, Gassi-Treffen, Events — was in deiner Gegend gerade passiert.

Training & KI-Trainer

Über 100 Übungen mit Schritt-für-Schritt-Anleitungen. Mit KI-Unterstützung, die deinen Hund kennt.

Deine Daten gehören dir.

Kein Facebook. Kein Google. Keine Werbung.
Ban Yaro läuft auf einem eigenen Server in Deutschland — dein Tagebuch, deine Routen, deine Gesundheitsdaten bleiben privat.

${FEATURES.map(f => `
${f.label}
`).join('')}
${hasPrompt ? ` ` : ` `}

Kein App Store · Direkt auf den Home-Bildschirm

${!isInstalled ? ` ` : ''}
${_showInstall ? `
Immer griffbereit — kein App Store
${_installHTML()}
` : ''}
`; } // ---------------------------------------------------------- // 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 `
${avatarUrl ? `${dogName}` : ''}
${UI.escape(greeting)}

${dogName}

${dogRasse ? `

${dogRasse}

` : ''}
`; } // Fallback: Gradient-Hero return `
Ban Yaro
${UI.escape(greeting)}

${dogName}

${dogRasse ? `

${dogRasse}

` : ''}
`; } 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 `
Nächster Termin ${apptLabel} · ${apptDate}
`; } 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 `
Tagebuch ${diaryText}
${_chip2HTML(dashData)}
Gewicht ${weightVal}
${ex ? `
Übung des Tages ${UI.escape(ex.name)}
` : ''}
`; } // 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 = ` Gassirunde ${result.distanz_km} km · ${durStr}`; 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 = `
${_heroHTML(dog, null)} ${_chipsHTML(null)}
${FEATURE_CARDS.map(f => ` `).join('')}
${dog?.id ? `
` : ''}

Mehr entdecken

${FEATURES.filter(f => !FEATURE_CARD_PAGES.has(f.page)).map(f => ` `).join('')}
${(!isInstalled || _showInstall) ? `
Immer griffbereit — kein App Store
${_installHTML()}
` : ''}
`; // 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: '

Wird geladen…

', }); 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 `
🔥 ${cur}
Tage in Folge trainiert
Rekord: ${best} ${best === 1 ? 'Tag' : 'Tage'}
`; } function _leaderboardHTML(rows) { if (!rows || !rows.length) { return '

Noch keine Einträge.

'; } const medals = ['🥇', '🥈', '🥉']; return `
${rows.map((r, i) => `
${medals[i] || (i + 1) + '.'} ${r.foto_url ? `` : `
`}
${UI.escape(r.dog_name)}
${UI.escape(r.rasse || '')}${r.user_name ? ' · ' + UI.escape(r.user_name) : ''}
🔥 ${r.current_streak}
`).join('')}
`; } 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 = `
${avatarUrl ? `${dogName}` : ''}
${UI.escape(greeting)}

${dogName}

${dogRasse ? `

${dogRasse}

` : ''}
`; } } 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 `

Ban Yaro immer griffbereit — einmal hinzufügen, dann direkt vom Home-Bildschirm öffnen.

`; } if (isIOS && isSafari) { return `

So kommt Ban Yaro auf deinen Home-Bildschirm:

${_steps([ ['share', 'Tippe unten in Safari auf das Teilen-Symbol '], ['rows-plus-top', 'Wähle „Zum Home-Bildschirm" aus der Liste'], ['check', 'Tippe oben rechts auf „Hinzufügen"'], ])}

Funktioniert nur in Safari — nicht in Chrome oder Firefox auf iOS.

`; } if (isIOS && !isSafari) { return `

Auf dem iPhone funktioniert das Hinzufügen nur über Safari.

${_steps([ ['safari-logo', 'Öffne Safari auf deinem iPhone'], ['arrow-square-in','Gib banyaro.app in die Adressleiste ein'], ['share', 'Tippe auf das Teilen-Symbol und wähle „Zum Home-Bildschirm"'], ])} `; } if (isAndroid) { return `

Am einfachsten geht es mit Chrome:

${_steps([ ['arrow-square-in', 'Öffne banyaro.app in Chrome'], ['dots-three-circle','Tippe auf das Menü ⋮ oben rechts'], ['download-simple', 'Wähle „App installieren" oder
„Zum Startbildschirm hinzufügen"'], ])} `; } return `
${_steps([ ['arrow-square-in', 'Öffne banyaro.app in Chrome oder Edge'], ['monitor', 'Klicke auf das Installations-Symbol in der Adressleiste'], ['check', 'Bestätigen — fertig!'], ])}
`; } function _steps(list) { return `
    ${list.map(([icon, text]) => `
  1. ${text}
  2. `).join('')}
`; } // ---------------------------------------------------------- // 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 }; })();