Add PWA reminders and flexible walk scoring

This commit is contained in:
2026-04-12 19:40:40 +02:00
parent 2cd00b1bf6
commit cd7526bd80
19 changed files with 1561 additions and 33 deletions
+32 -2
View File
@@ -6,7 +6,7 @@ $brandSubtitle = match ($page) {
'dashboard' => 'Statistiken und Verlauf',
'track' => 'Tag erfassen und bewerten',
'archive' => 'Rückblick auf vergangene Tage',
'options' => 'Logik, Sicherheit und Accounts',
'options' => 'Logik, Erinnerungen, Sicherheit und Accounts',
'login' => 'Geschützter Zugang',
'setup' => 'Erstkonfiguration',
default => 'Stimmungstracker',
@@ -19,13 +19,22 @@ $brandSubtitle = match ($page) {
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0b1e2e">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Mood-Board">
<meta name="csrf-token" content="<?= e(csrf_token()) ?>">
<?php if (!empty($pushPublicKey)): ?>
<meta name="mood-push-public-key" content="<?= e((string) $pushPublicKey) ?>">
<?php endif; ?>
<title><?= e($pageTitle) ?> · Mood</title>
<link rel="icon" type="image/svg+xml" href="/assets/branding/favicon.svg">
<link rel="shortcut icon" href="/assets/branding/favicon.svg">
<link rel="apple-touch-icon" href="/assets/branding/apple-touch-icon.svg">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="stylesheet" href="/assets/css/app.css">
<script defer src="/assets/js/app.js"></script>
</head>
<body class="app-body page-<?= e($page) ?>"<?= isset($trackMood) ? ' data-track-mood="' . e($trackMood) . '"' : '' ?>>
<body class="app-body page-<?= e($page) ?><?= $authUser !== null ? ' is-authenticated' : '' ?>" data-authenticated="<?= $authUser !== null ? '1' : '0' ?>"<?= isset($trackMood) ? ' data-track-mood="' . e($trackMood) . '"' : '' ?>>
<div class="aurora aurora-one"></div>
<div class="aurora aurora-two"></div>
<div class="shell">
@@ -101,6 +110,27 @@ $brandSubtitle = match ($page) {
<?= $content ?>
</main>
<?php if ($authUser !== null): ?>
<nav class="mobile-nav glass-panel" aria-label="Mobile Hauptnavigation">
<a class="<?= is_active_path('/') ? 'active' : '' ?>" href="/">
<img class="nav-icon" src="<?= e(icon_path('dashboard')) ?>" alt="">
<span>Dashboard</span>
</a>
<a class="<?= is_active_path('/track') ? 'active' : '' ?>" href="/track">
<img class="nav-icon" src="<?= e(icon_path('track')) ?>" alt="">
<span>Tracken</span>
</a>
<a class="<?= is_active_path('/archive') ? 'active' : '' ?>" href="/archive">
<img class="nav-icon" src="<?= e(icon_path('archive')) ?>" alt="">
<span>Archiv</span>
</a>
<a class="<?= is_active_path('/options') ? 'active' : '' ?>" href="/options">
<img class="nav-icon" src="<?= e(icon_path('options')) ?>" alt="">
<span>Optionen</span>
</a>
</nav>
<?php endif; ?>
</div>
</body>
</html>
+1 -1
View File
@@ -75,7 +75,7 @@
</dd>
</div>
<div><dt>Sportbonus</dt><dd><?= e(format_points((float) ($selectedEntry['evaluation']['components']['sport_bonus'] ?? 0))) ?></dd></div>
<div><dt>Spaziergang</dt><dd><?= e((string) $selectedEntry['walk_minutes']) ?> min</dd></div>
<div><dt>Spaziergang</dt><dd><?= e(format_walk_value($selectedEntry)) ?></dd></div>
</dl>
<div class="note-box">
+1 -1
View File
@@ -79,7 +79,7 @@
<p class="eyebrow">Aktivität</p>
<h3>Sport und Spaziergang</h3>
</div>
<span class="chart-chip chart-chip--cool">Minuten pro Tag</span>
<span class="chart-chip chart-chip--cool">Aktivität pro Tag</span>
</div>
<div class="bar-chart" data-chart-type="bars" data-series="sport" data-payload="<?= e($chartPayload) ?>"></div>
</article>
+81 -9
View File
@@ -49,16 +49,39 @@
</div>
<div class="settings-section">
<h4>Spaziergang-Bänder</h4>
<div class="band-grid">
<?php foreach ($settings['scoring']['walk_bands'] as $index => $band): ?>
<div class="band-card">
<label><span>Min</span><input type="number" name="settings[scoring][walk_bands][<?= e((string) $index) ?>][min]" value="<?= e((string) $band['min']) ?>"></label>
<label><span>Max</span><input type="number" name="settings[scoring][walk_bands][<?= e((string) $index) ?>][max]" value="<?= e((string) $band['max']) ?>"></label>
<label><span>Punkte</span><input type="number" name="settings[scoring][walk_bands][<?= e((string) $index) ?>][points]" value="<?= e((string) $band['points']) ?>"></label>
</div>
<?php endforeach; ?>
<div class="section-head section-head--compact">
<div>
<h4>Spaziergang</h4>
<p class="helper-text">Du kannst Spaziergänge pro Account entweder über Zeit oder über Schritte bewerten lassen.</p>
</div>
</div>
<label>
<span>Spaziergang auswerten nach</span>
<select name="settings[walk][mode]">
<?php foreach ($walkModeOptions as $modeValue => $modeLabel): ?>
<option value="<?= e($modeValue) ?>" <?= ($settings['walk']['mode'] ?? 'time') === $modeValue ? 'selected' : '' ?>><?= e($modeLabel) ?></option>
<?php endforeach; ?>
</select>
</label>
<?php if (($settings['walk']['mode'] ?? 'time') === 'steps'): ?>
<div class="band-card">
<h5>Schritte mit Bestwert bei 10.000</h5>
<p class="helper-text">Bei Schritten liegt der beste Wert bei 10.000. Darunter steigt die Punktzahl schrittweise an, darüber fällt sie wieder sanft ab.</p>
<p class="helper-text">Aktueller Verlauf: 0 / 3.000 / 5.000 / 7.500 / 10.000 / 12.500 / 15.000 / 20.000 Schritte</p>
</div>
<?php else: ?>
<div class="band-grid">
<?php foreach ($settings['scoring']['walk_bands'] as $index => $band): ?>
<div class="band-card">
<label><span>Min</span><input type="number" name="settings[scoring][walk_bands][<?= e((string) $index) ?>][min]" value="<?= e((string) $band['min']) ?>"></label>
<label><span>Max</span><input type="number" name="settings[scoring][walk_bands][<?= e((string) $index) ?>][max]" value="<?= e((string) $band['max']) ?>"></label>
<label><span>Punkte</span><input type="number" name="settings[scoring][walk_bands][<?= e((string) $index) ?>][points]" value="<?= e((string) $band['points']) ?>"></label>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="settings-section">
@@ -217,6 +240,55 @@
</template>
</div>
<div class="settings-section">
<div class="section-head section-head--compact">
<div>
<h4>Erinnerungen</h4>
<p class="helper-text">Diese Erinnerungen gelten nur für deinen Account. Auf dem iPhone funktioniert Push nach dem Hinzufügen zum Home-Bildschirm.</p>
</div>
<span class="chart-chip"><?= e((string) $pushSubscriptionCount) ?> Gerät<?= $pushSubscriptionCount === 1 ? '' : 'e' ?></span>
</div>
<div class="field-grid field-grid--two">
<label class="checkbox-row checkbox-row--panel">
<input type="checkbox" name="settings[notifications][enabled]" value="1" <?= !empty($settings['notifications']['enabled']) ? 'checked' : '' ?>>
<span>Tägliche Push-Erinnerung aktivieren</span>
</label>
<label>
<span>Uhrzeit der Erinnerung</span>
<input type="time" name="settings[notifications][time]" value="<?= e((string) ($settings['notifications']['time'] ?? '20:30')) ?>">
</label>
</div>
<div
class="push-panel band-card"
data-push-panel
data-push-ready="<?= !empty($pushAvailable) && !empty($pushPublicKey) ? '1' : '0' ?>"
>
<div>
<h5>Push auf diesem Gerät</h5>
<p class="helper-text" data-push-status>
<?php if (!empty($pushAvailable) && !empty($pushPublicKey)): ?>
Installiere die App auf Wunsch als PWA und aktiviere dann Push direkt auf diesem Gerät.
<?php else: ?>
Push ist auf diesem Server gerade noch nicht verfügbar.
<?php endif; ?>
</p>
</div>
<div class="push-actions">
<button class="ghost-button" type="button" data-push-enable <?= empty($pushAvailable) || empty($pushPublicKey) ? 'disabled' : '' ?>>Push aktivieren</button>
<button class="ghost-button" type="button" data-push-disable <?= empty($pushAvailable) || empty($pushPublicKey) ? 'disabled' : '' ?>>Auf diesem Gerät entfernen</button>
<button class="ghost-button" type="button" data-push-test <?= empty($pushAvailable) || empty($pushPublicKey) ? 'disabled' : '' ?>>Test senden</button>
</div>
</div>
<div class="section-actions">
<button class="primary-button" type="submit">Erinnerungen speichern</button>
</div>
</div>
<div class="settings-section">
<h4>Bewertungsskala</h4>
<p class="helper-text">Diese Score-Grenzen und Schutzregeln gelten ebenfalls nur für deinen eigenen Account.</p>
+12 -2
View File
@@ -14,6 +14,8 @@
<form method="post" action="/track" class="tracker-form" id="tracker-form" autocomplete="off">
<?= csrf_field() ?>
<input type="hidden" name="date" value="<?= e($entry['date']) ?>">
<?php $walkMode = ($entry['walk_mode'] ?? ($settings['walk']['mode'] ?? 'time')) === 'steps' ? 'steps' : 'time'; ?>
<input type="hidden" name="walk_mode" value="<?= e($walkMode) ?>">
<div class="field-grid field-grid--three">
<label class="range-card">
@@ -60,8 +62,16 @@
</label>
<label>
<span>Spaziergang in Minuten</span>
<input type="number" min="0" max="1440" step="1" name="walk_minutes" value="<?= e((string) $entry['walk_minutes']) ?>" required>
<span><?= $walkMode === 'steps' ? 'Spaziergang in Schritten' : 'Spaziergang in Minuten' ?></span>
<input
type="number"
min="0"
max="<?= $walkMode === 'steps' ? '50000' : '1440' ?>"
step="1"
name="<?= $walkMode === 'steps' ? 'walk_steps' : 'walk_minutes' ?>"
value="<?= e((string) ($walkMode === 'steps' ? ($entry['walk_steps'] ?? 0) : $entry['walk_minutes'])) ?>"
required
>
</label>
</div>