Feature: Hilfe/FAQ, Übungen-Content, Navigation-Fixes (SW by-v727)

Hilfe & FAQ:
- Neue Seite /hilfe mit Akkordeon + Live-Suche (6 Kategorien, 25 Artikel)
- DB-Tabelle help_articles — Inhalte admin-seitig ohne Deploy änderbar
- Admin-Tab Hilfe/FAQ zum Bearbeiten aller Artikel
- Link in Einstellungen (unter Welten einrichten, über Abmelden)
- routes/help.py: GET (public), POST/PATCH/DELETE (Admin)

Übungen:
- 110 Übungen: beschreibung (kurz), schritte (JSON 4-6 Schritte), tipp — gutes Deutsch mit Umlauten
- Admin-Tab Übungen: Inline-Editor für alle drei Felder
- PUT /training/exercises/{id} (Admin) neu
- Übung-des-Tages Chip → scrollt jetzt korrekt zur Übung (exercise_id-Feldname-Fix)

Welten-Navigation:
- hide() stellt app-header + bottom-nav wieder her (worlds-hidden wurde nie entfernt)
- init() mit _setupDone-Guard (keine doppelten Event-Listener)
- Login ruft Worlds.init(_appState) statt show() — _state war null → falscher Render
- X-Button in Welten-Konfiguration: 30×30px, Icon 17px, besser sichtbar

Wetter:
- Motivation bei blockiertem Standort: 6-Schritte-iOS-Anleitung + Flugmodus-Tipp
- Auto-locate bleibt (kein Button-Only mehr)

achievements.py:
- my_achievements(): d.user_id → JOIN dogs (zweite Funktion war noch kaputt)
This commit is contained in:
rene 2026-05-05 21:46:16 +02:00
parent 55069d246b
commit 05ecf3b94a
13 changed files with 1158 additions and 43 deletions

View file

@ -14,6 +14,7 @@ window.Worlds = (() => {
let _dogs = []; // gecachte Hundesliste
let _dogIdx = 0; // aktuell angezeigter Hund
let _hasBgPhoto = false; // Hintergrund-Foto vorhanden?
let _setupDone = false; // Swipe/Button-Listener nur einmal registrieren
// Touch-Tracking
const _t = { x:0, y:0, active:false, vert:null, moved:0 };
@ -44,13 +45,17 @@ window.Worlds = (() => {
// ── PUBLIC ──────────────────────────────────────────────────
async function init(appState) {
_state = appState;
_cur = 1; // immer HUND als Start
_setupSwipe();
_setupButtons();
_state = appState;
_lastUserId = undefined; // Neurender erzwingen
_cur = 1;
if (!_setupDone) {
_setupDone = true;
_setupSwipe();
_setupButtons();
_showSwipeHints();
}
_goTo(_cur, false);
show();
_showSwipeHints();
}
function _showSwipeHints() {
@ -133,6 +138,8 @@ window.Worlds = (() => {
ov.classList.remove('worlds-visible');
ov.style.display = 'none';
_visible = false;
document.getElementById('app-header')?.classList.remove('worlds-hidden');
document.getElementById('bottom-nav')?.classList.remove('worlds-hidden');
document.getElementById('worlds-back')?.classList.add('worlds-back-visible');
}
@ -665,11 +672,11 @@ window.Worlds = (() => {
user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none">
${!c.pinned ? `
<button class="wc-remove" data-page="${c.page}" data-zone="${w}"
style="position:absolute;top:-8px;right:-8px;width:24px;height:24px;
border-radius:50%;background:#EF4444;border:2px solid rgba(18,22,32,0.9);
style="position:absolute;top:-10px;right:-10px;width:30px;height:30px;
border-radius:50%;background:#EF4444;border:3px solid rgba(18,22,32,0.95);
cursor:pointer;display:flex;align-items:center;justify-content:center;
z-index:2;box-shadow:0 2px 6px rgba(0,0,0,0.5)">
<svg class="ph-icon" style="width:13px;height:13px;color:white">
z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.7)">
<svg class="ph-icon" style="width:17px;height:17px;color:white;stroke:white;stroke-width:1.5">
<use href="/icons/phosphor.svg#x"></use>
</svg>
</button>` : `
@ -1001,7 +1008,7 @@ window.Worlds = (() => {
<span class="wj-chip-val" id="wj-route-val"></span>
${totalKm != null ? `<span style="font-size:9px;color:rgba(255,255,255,0.4);margin-top:1px">∑ ${totalKm} km</span>` : ''}
</div>
<div class="wj-chip" data-wnav="uebungen">
<div class="wj-chip" id="wj-exercise-chip">
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:var(--c-primary)">
<use href="/icons/phosphor.svg#barbell"></use></svg>
<span class="wj-chip-label">Übung</span>
@ -1034,6 +1041,17 @@ window.Worlds = (() => {
const res = await _cachedGet(`dash_${dog.id}`, `/dogs/${dog.id}/welcome-dashboard`);
const ex = res.data?.daily_exercise;
valEl.textContent = ex?.name || '—';
// Chip-Klick mit exercise_id/name damit übungen.js direkt dorthin scrollt
const chip = document.getElementById('wj-exercise-chip');
if (chip) {
chip.style.cursor = 'pointer';
chip.onclick = () => {
hide();
if (window.App) window.App.navigate('uebungen', true,
ex ? { exercise_id: ex.exercise_id || '', name: ex.name || '' } : {}
);
};
}
} catch { valEl.textContent = '—'; }
}