second commit

This commit is contained in:
2026-04-11 19:13:40 +02:00
parent 58bcc8f0f3
commit 87f7859017
25 changed files with 488 additions and 87 deletions
+9 -7
View File
@@ -118,7 +118,7 @@ final class App
}
if ($password !== $passwordConfirm) {
flash('error', 'Die Passwoerter stimmen nicht ueberein.');
flash('error', 'Die Passwörter stimmen nicht überein.');
redirect('/setup');
}
@@ -153,12 +153,12 @@ final class App
if (!$this->auth->attempt($username, $password)) {
$this->throttle->hit($throttleKey);
flash('error', 'Login fehlgeschlagen. Bitte pruefe Benutzername und Passwort.');
flash('error', 'Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.');
redirect('/login');
}
$this->throttle->clear($throttleKey);
flash('success', 'Willkommen zurueck.');
flash('success', 'Willkommen zurück.');
redirect('/');
}
@@ -219,6 +219,8 @@ final class App
'entry' => $entry,
'evaluation' => $evaluation,
'settings' => $settings,
'trackMood' => $evaluation['sentiment'],
'topbarDate' => $entry['date'],
'trackPayload' => encode_payload([
'settings' => $settings,
'entry' => $entry,
@@ -246,7 +248,7 @@ final class App
]);
if (!$this->isValidDate($entry['date'])) {
flash('error', 'Bitte waehle ein gueltiges Datum.');
flash('error', 'Bitte wähle ein gültiges Datum.');
redirect('/track');
}
@@ -344,7 +346,7 @@ final class App
}
if ($new !== $confirm) {
flash('error', 'Die neuen Passwoerter stimmen nicht ueberein.');
flash('error', 'Die neuen Passwörter stimmen nicht überein.');
redirect('/options');
}
@@ -359,7 +361,7 @@ final class App
$isAdmin = isset($_POST['is_admin']) && $_POST['is_admin'] === '1';
if (!$this->isValidUsername($username)) {
flash('error', 'Bitte nutze fuer neue Accounts einen sauberen Benutzernamen.');
flash('error', 'Bitte nutze für neue Accounts einen sauberen Benutzernamen.');
redirect('/options');
}
@@ -538,7 +540,7 @@ final class App
{
if (!verify_csrf($_POST['_token'] ?? null)) {
http_response_code(419);
exit('Ungueltiges Formular-Token.');
exit('Ungültiges Formular-Token.');
}
}
+3 -3
View File
@@ -68,7 +68,7 @@ final class EntryRepository
'energy' => (int) ($this->extract('/^- Energie:\s*(.+)$/m', $content) ?? 5),
'stress' => (int) ($this->extract('/^- Stress:\s*(.+)$/m', $content) ?? 5),
'sleep_hours' => (float) ($this->extract('/^- Schlafdauer:\s*(.+)$/m', $content) ?? 0),
'sleep_feeling' => (int) ($this->extract('/^- Schlafgefuehl:\s*(.+)$/m', $content) ?? 3),
'sleep_feeling' => (int) ($this->extract('/^- Schlaf(?:gefühl|gefuehl):\s*(.+)$/mu', $content) ?? 3),
'sport_minutes' => (int) ($this->extract('/^- Sport:\s*(.+)$/m', $content) ?? 0),
'walk_minutes' => (int) ($this->extract('/^- Spaziergang:\s*(.+)$/m', $content) ?? 0),
'note' => $this->extractNote($content),
@@ -112,7 +112,7 @@ final class EntryRepository
'- Energie: ' . $entry['energy'],
'- Stress: ' . $entry['stress'],
'- Schlafdauer: ' . $entry['sleep_hours'],
'- Schlafgefuehl: ' . $entry['sleep_feeling'],
'- Schlafgefühl: ' . $entry['sleep_feeling'],
'- Sport: ' . $entry['sport_minutes'],
'- Spaziergang: ' . $entry['walk_minutes'],
'',
@@ -125,7 +125,7 @@ final class EntryRepository
'- Energie: ' . format_points((float) $evaluation['components']['energy']),
'- Stress: ' . format_points((float) $evaluation['components']['stress']),
'- Schlafdauer: ' . format_points((float) $evaluation['components']['sleep_hours']),
'- Schlafgefuehl: ' . format_points((float) $evaluation['components']['sleep_feeling']),
'- Schlafgefühl: ' . format_points((float) $evaluation['components']['sleep_feeling']),
'- Sport: ' . format_points((float) $evaluation['components']['sport_minutes']),
'- Spaziergang: ' . format_points((float) $evaluation['components']['walk_minutes']),
'- Notiz: ' . format_points((float) $evaluation['components']['note']),
+21 -1
View File
@@ -72,6 +72,7 @@ final class ScoringService
'max_total' => $maxTotal,
'label' => $label,
'guardrail' => $guardrail,
'sentiment' => $this->sentimentForLabel($label, $ratings),
'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0,
];
}
@@ -168,5 +169,24 @@ final class ScoringService
return $currentIndex > $capIndex ? $cap : $current;
}
}
private function sentimentForLabel(string $label, array $ratings): string
{
$order = array_values(array_map(static fn (array $rating): string => (string) $rating['label'], $ratings));
$index = array_search($label, $order, true);
if ($index === false || count($order) <= 1) {
return 'steady';
}
$ratio = $index / max(count($order) - 1, 1);
return match (true) {
$ratio <= 0.1 => 'storm',
$ratio <= 0.35 => 'heavy',
$ratio <= 0.65 => 'steady',
$ratio <= 0.9 => 'bright',
default => 'radiant',
};
}
}
+1 -2
View File
@@ -56,7 +56,7 @@ final class UserRepository
$normalized = normalize_username($username);
if ($normalized === '' || $this->find($normalized) !== null) {
throw new RuntimeException('Benutzername existiert bereits oder ist ungueltig.');
throw new RuntimeException('Benutzername existiert bereits oder ist ungültig.');
}
$users = $this->all();
@@ -100,4 +100,3 @@ final class UserRepository
);
}
}
+53
View File
@@ -122,3 +122,56 @@ function encode_payload(array $payload): string
return base64_encode((string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}
function shift_date(string $date, int $days): string
{
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
if ($current === false) {
return today();
}
return $current->modify(($days >= 0 ? '+' : '') . $days . ' day')->format('Y-m-d');
}
function format_display_date(string $date, bool $withWeekday = true): string
{
$current = DateTimeImmutable::createFromFormat('Y-m-d', $date);
if ($current === false) {
return $date;
}
$weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
$months = [
1 => 'Januar',
2 => 'Februar',
3 => 'März',
4 => 'April',
5 => 'Mai',
6 => 'Juni',
7 => 'Juli',
8 => 'August',
9 => 'September',
10 => 'Oktober',
11 => 'November',
12 => 'Dezember',
];
$label = $current->format('j.') . ' ' . $months[(int) $current->format('n')] . ' ' . $current->format('Y');
if (!$withWeekday) {
return $label;
}
return $weekdays[(int) $current->format('w')] . ', ' . $label;
}
function icon_path(string $name): string
{
return '/assets/icons/' . $name . '.svg';
}
function mood_icon_path(string $sentiment): string
{
return icon_path('mood-' . $sentiment);
}