Inline-Scripts extrahiert:
- boot-early.js: Theme + theme-color (synchron im <head>, VOR CSS)
- boot.js: Offline-Banner + Service-Worker-Registration + Update-Flow
- landing-init.js: Dark-mode + Scroll-Animationen + Live-Stats +
Stay-In-App-Handler + Details-Toggle
Inline onclick-Handler in landing.html:
- 5× sessionStorage.setItem('by_stay_in_app','1') → data-stay-in-app
- 1× Details-Toggle → data-toggle-target + data-toggle-text-open
- JS-Handler in landing-init.js binden die data-Attribute
CSP-Header (main.py):
- script-src: 'unsafe-inline' und 'unsafe-eval' entfernt
- style-src 'unsafe-inline' bleibt (Inline-Styles bleiben für jetzt,
zu viele Fundstellen)
- Umami bleibt whitelisted
SW STATIC_ASSETS erweitert um boot-early.js + boot.js.
make bump aktualisiert jetzt auch landing.html ?v= Anker.
Tests grün (19/19).
99 lines
3.6 KiB
JavaScript
99 lines
3.6 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — Landing Page Init
|
|
Dark-Mode-Check, Scroll-Animationen, Live-Stats, Stay-In-App
|
|
Extrahiert aus landing.html für CSP-Härtung
|
|
============================================================ */
|
|
|
|
// Dark Mode (CSS-Klasse)
|
|
(function() {
|
|
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
if (mq.matches) document.documentElement.classList.add('dark');
|
|
mq.addEventListener('change', function(e) {
|
|
document.documentElement.classList.toggle('dark', e.matches);
|
|
});
|
|
})();
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
// App-Links: kein Redirect-Loop (ersetzt onclick="sessionStorage.setItem(...)")
|
|
document.querySelectorAll('[data-stay-in-app]').forEach(function(el) {
|
|
el.addEventListener('click', function() {
|
|
sessionStorage.setItem('by_stay_in_app', '1');
|
|
});
|
|
});
|
|
|
|
// Hundebesitzer-Details-Toggle (ersetzt inline onclick)
|
|
document.querySelectorAll('[data-toggle-target]').forEach(function(el) {
|
|
el.addEventListener('click', function() {
|
|
var c = document.getElementById(el.dataset.toggleTarget);
|
|
if (!c) return;
|
|
c.classList.toggle('open');
|
|
var open = c.classList.contains('open');
|
|
var openTxt = el.dataset.toggleTextOpen || '▴ Weniger anzeigen';
|
|
var closeTxt = el.dataset.toggleTextClose || el.textContent;
|
|
if (!el.dataset.toggleTextClose) el.dataset.toggleTextClose = closeTxt;
|
|
el.textContent = open ? openTxt : el.dataset.toggleTextClose;
|
|
});
|
|
});
|
|
|
|
// Auch ältere App-Links erfassen (Fallback ohne data-stay-in-app)
|
|
document.querySelectorAll('a[href="/"], a[href^="/#"]').forEach(function(a) {
|
|
a.addEventListener('click', function() {
|
|
sessionStorage.setItem('by_stay_in_app', '1');
|
|
});
|
|
});
|
|
|
|
// Scroll-Animationen
|
|
var _observer = new IntersectionObserver(function(entries) {
|
|
entries.forEach(function(e) {
|
|
if (e.isIntersecting) {
|
|
e.target.classList.add('visible');
|
|
_observer.unobserve(e.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.12 });
|
|
|
|
document.querySelectorAll('.outcome-card, .feature-card, .usp-item, .pricing-card').forEach(function(el) {
|
|
el.classList.add('fade-up');
|
|
_observer.observe(el);
|
|
});
|
|
document.querySelectorAll('.fade-up').forEach(function(el) {
|
|
_observer.observe(el);
|
|
});
|
|
|
|
// Live-Zahlen
|
|
var fmt = new Intl.NumberFormat('de-DE');
|
|
fetch('/api/stats/public')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(d) {
|
|
function set(id, val) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.textContent = fmt.format(val);
|
|
}
|
|
set('big-users', d.users);
|
|
set('big-dogs', d.dogs);
|
|
set('big-km', d.km);
|
|
set('big-posts', d.forum_posts);
|
|
set('big-diary', d.diary_entries);
|
|
set('big-kotbeutel', d.kotbeutel);
|
|
|
|
var heroStats = document.getElementById('hero-stats');
|
|
if (!heroStats || !d.users) return;
|
|
|
|
var items = [
|
|
{ val: d.users, label: 'Hundemenschen' },
|
|
{ val: d.dogs, label: 'Hunde' },
|
|
{ val: d.km, label: 'km Gassi-Wege' },
|
|
{ val: d.diary_entries, label: 'Tagebuch-Einträge' },
|
|
{ val: d.kotbeutel, label: 'Mülleimer für Kotbeutel'},
|
|
];
|
|
items.sort(function(a, b) { return a.val - b.val; });
|
|
|
|
heroStats.innerHTML = items.map(function(item, i) {
|
|
return (i > 0 ? '<span class="sep">·</span>' : '') +
|
|
'<strong>' + fmt.format(item.val) + '</strong> ' + item.label;
|
|
}).join('');
|
|
heroStats.style.display = 'flex';
|
|
})
|
|
.catch(function() {});
|
|
});
|