Session 2026-04-19: Navigation, Kompass, Übungsfortschritt
Routen-Navigation:
- POI-Marker: farbige Kreise mit Phosphor-Icons (wie Hauptkarte)
- Screensaver: Navi-Pfeil dreht sich via DeviceOrientationEvent (iOS+Android)
- Pfeil-Dämpfung: EMA α=0.12 mit Wrap-Around
- GPS-Distanz-Bug: Fortschritt nur wenn <500m zur Route
- fitBounds: User-Position nur wenn <20km von Route
- Screensaver: "zur Route" vs "verbleibend" kontextabhängig
- Richtungspfeile entlang Route (blau, max 7 Stück)
- Umkehren ins Route-Detail verschoben, Detail-Map rebuildet sich
- rk-header z-index:10 (Leaflet-Tiles liefen drüber)
- 2-Sek. Screensaver-Entsperrung
km-Tracking:
- route_walks Tabelle
- POST /api/routes/{id}/walked (≥50%)
- total_km = erstellte Routes + gelaufene route_walks
- Toast bei neuem Badge
Übungsfortschritt:
- exercise_progress + training_plan_progress Tabellen
- GET/POST /api/training/progress, /plan-progress, /suggestions
- uebungen.js: API-first + localStorage-Fallback + Auto-Migration
- Empfehlungs-Banner (regelbasiert)
- Toast bei "sitzt"
This commit is contained in:
parent
390176383f
commit
9a78121a3e
25 changed files with 2487 additions and 248 deletions
|
|
@ -36,16 +36,26 @@ window.Page_uebungen = (() => {
|
|||
return `ub_status_${tab}_${name.replace(/\s+/g, '_')}`;
|
||||
}
|
||||
|
||||
// In-memory cache (loaded from API on init)
|
||||
let _progressCache = {}; // key → statusId
|
||||
|
||||
function _progressKey(tab, name) {
|
||||
return `${tab}_${name.replace(/[\s/]+/g, '_')}`;
|
||||
}
|
||||
|
||||
function _getStatus(tab, name) {
|
||||
return localStorage.getItem(_statusKey(tab, name)) || null;
|
||||
const k = _progressKey(tab, name);
|
||||
// Fallback to localStorage while API loads
|
||||
return _progressCache[k] !== undefined
|
||||
? _progressCache[k]
|
||||
: localStorage.getItem(_statusKey(tab, name)) || null;
|
||||
}
|
||||
|
||||
function _setStatus(tab, name, statusId) {
|
||||
if (statusId === null) {
|
||||
localStorage.removeItem(_statusKey(tab, name));
|
||||
} else {
|
||||
localStorage.setItem(_statusKey(tab, name), statusId);
|
||||
}
|
||||
const k = _progressKey(tab, name);
|
||||
_progressCache[k] = statusId;
|
||||
localStorage.setItem(_statusKey(tab, name), statusId || ''); // keep localStorage in sync
|
||||
API.training.setProgress(k, statusId).catch(() => {});
|
||||
}
|
||||
|
||||
function _nextStatus(currentId) {
|
||||
|
|
@ -352,6 +362,31 @@ window.Page_uebungen = (() => {
|
|||
_container = container;
|
||||
_appState = appState;
|
||||
_render();
|
||||
|
||||
// Progress vom Server laden
|
||||
API.training.getProgress().then(rows => {
|
||||
rows.forEach(r => { _progressCache[r.exercise_id] = r.status; });
|
||||
// localStorage-Daten migrieren falls noch nicht im Backend
|
||||
Object.keys(localStorage).filter(k => k.startsWith('ub_status_')).forEach(lsKey => {
|
||||
const parts = lsKey.replace('ub_status_', '').split('_');
|
||||
const tab = parts[0];
|
||||
const name = parts.slice(1).join('_');
|
||||
const apiKey = `${tab}_${name}`;
|
||||
if (_progressCache[apiKey] === undefined) {
|
||||
const val = localStorage.getItem(lsKey);
|
||||
if (val) {
|
||||
_progressCache[apiKey] = val;
|
||||
API.training.setProgress(apiKey, val).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
_renderContent(); // Re-render with loaded progress
|
||||
}).catch(() => {});
|
||||
|
||||
// Empfehlungen laden
|
||||
API.training.getSuggestions().then(suggestions => {
|
||||
if (suggestions.length) _showSuggestions(suggestions);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function refresh() {}
|
||||
|
|
@ -364,6 +399,7 @@ window.Page_uebungen = (() => {
|
|||
_container.innerHTML = `
|
||||
<div id="ueb-wrap">
|
||||
${_renderTabs()}
|
||||
<div id="ueb-suggestions" style="padding:0 var(--space-4);display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-2)"></div>
|
||||
<div id="ueb-content"></div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -384,6 +420,54 @@ window.Page_uebungen = (() => {
|
|||
`;
|
||||
}
|
||||
|
||||
function _showSuggestions(suggestions) {
|
||||
const el = _container.querySelector('#ueb-suggestions');
|
||||
if (!el || !suggestions.length) return;
|
||||
|
||||
const COLORS = {
|
||||
help: { bg: '#fef2f2', border: '#fca5a5', text: '#dc2626' },
|
||||
boost: { bg: '#fff7ed', border: '#fdba74', text: '#ea580c' },
|
||||
next: { bg: '#f0fdf4', border: '#86efac', text: '#16a34a' },
|
||||
start: { bg: 'var(--c-primary-subtle)', border: 'var(--c-primary-light)', text: 'var(--c-primary)' },
|
||||
};
|
||||
|
||||
el.innerHTML = suggestions.map(s => {
|
||||
const c = COLORS[s.type] || COLORS.start;
|
||||
return `
|
||||
<div style="background:${c.bg};border:1px solid ${c.border};border-radius:var(--radius-md);
|
||||
padding:var(--space-3) var(--space-4);display:flex;gap:var(--space-3);align-items:flex-start;cursor:pointer"
|
||||
data-action-tab="${_esc(s.action_tab || '')}"
|
||||
data-action-name="${_esc(s.action_name || '')}"
|
||||
class="ueb-suggestion-card">
|
||||
<svg class="ph-icon" style="width:20px;height:20px;flex-shrink:0;color:${c.text};margin-top:2px" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${_esc(s.icon)}"></use>
|
||||
</svg>
|
||||
<div style="min-width:0">
|
||||
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:${c.text};margin-bottom:2px">
|
||||
${_esc(s.title)}
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
|
||||
${_esc(s.text)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
el.querySelectorAll('.ueb-suggestion-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const tab = card.dataset.actionTab;
|
||||
if (tab && tab !== _activeTab) {
|
||||
_activeTab = tab;
|
||||
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(b =>
|
||||
b.classList.toggle('active', b.dataset.tab === tab)
|
||||
);
|
||||
_renderContent();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _bindTabs() {
|
||||
_container.querySelectorAll('#ueb-tabs .by-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
|
|
@ -551,6 +635,7 @@ window.Page_uebungen = (() => {
|
|||
const cur = _getStatus(tab, name);
|
||||
const next = _nextStatus(cur);
|
||||
_setStatus(tab, name, next);
|
||||
if (next === 'sitzt') UI.toast.success(`🏆 „${name}" sitzt! Gut gemacht!`);
|
||||
|
||||
// Update button in place (no full re-render)
|
||||
const sm = _statusMeta(next);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue