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).
1232 lines
54 KiB
JavaScript
1232 lines
54 KiB
JavaScript
/* ============================================================
|
||
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>
|
||
·
|
||
<a href="/#datenschutz" class="text-muted">Datenschutz</a>
|
||
·
|
||
<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 };
|
||
})();
|