From 80f649c5473b59fc4a32710a973aa1f27db4d82a Mon Sep 17 00:00:00 2001 From: Florian Heinz Date: Sun, 12 Apr 2026 11:52:57 +0200 Subject: [PATCH] Make score and sport settings fully account-specific --- assets/css/app.css | 35 +++++++++++++++ assets/js/app.js | 90 +++++++++++++++++++++++++++++-------- src/App.php | 22 +++++++-- templates/pages/options.php | 31 +++++++++++-- 4 files changed, 153 insertions(+), 25 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 1b24383..ec760a2 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1174,6 +1174,41 @@ input[type="range"] { gap: 0.9rem; } +.preset-list { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; + margin-bottom: 0.95rem; +} + +.preset-pill { + display: inline-flex; + align-items: center; + gap: 0.55rem; + padding: 0.65rem 0.9rem; + border-radius: 999px; + border: 1px solid rgba(138, 210, 235, 0.22); + background: rgba(255, 255, 255, 0.08); + color: var(--text); + backdrop-filter: blur(20px); + transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; +} + +.preset-pill:hover { + transform: translateY(-1px); + border-color: rgba(139, 228, 255, 0.4); + background: rgba(255, 255, 255, 0.12); +} + +.preset-pill img { + width: 1rem; + height: 1rem; +} + +.preset-pill[hidden] { + display: none; +} + .sport-type-card { gap: 0.9rem; } diff --git a/assets/js/app.js b/assets/js/app.js index 61bc90a..110f3d2 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -752,11 +752,14 @@ const list = document.querySelector("[data-sport-type-list]"); const addButton = document.querySelector("[data-add-sport-type]"); const template = document.querySelector("#sport-type-row-template"); + const presetButtons = [...document.querySelectorAll("[data-sport-preset]")]; if (!list || !addButton || !template) { return; } + const getRows = () => [...list.querySelectorAll("[data-sport-type-row]")]; + const syncPreview = row => { const labelInput = row.querySelector('input[data-name-template$="[label]"]'); const iconSelect = row.querySelector('select[data-name-template$="[icon]"]'); @@ -781,9 +784,72 @@ }); }; - addButton.addEventListener("click", () => { + const syncPresets = () => { + const selectedIds = new Set( + getRows() + .map(row => row.querySelector('input[data-name-template$="[id]"]')) + .filter(Boolean) + .map(input => String(input.value || "").trim()) + .filter(Boolean) + ); + + presetButtons.forEach(button => { + button.hidden = selectedIds.has(button.dataset.id || ""); + }); + }; + + const createRow = values => { list.append(template.content.cloneNode(true)); + const row = list.querySelector("[data-sport-type-row]:last-child"); + if (!row) { + return; + } + + const idInput = row.querySelector('input[data-name-template$="[id]"]'); + const labelInput = row.querySelector('input[data-name-template$="[label]"]'); + const iconSelect = row.querySelector('select[data-name-template$="[icon]"]'); + const groupInput = row.querySelector('input[data-name-template$="[recovery_group]"]'); + const bonusInput = row.querySelector('input[data-name-template$="[bonus_points]"]'); + const consecutiveInput = row.querySelector('input[data-name-template$="[allow_consecutive]"]'); + + if (idInput) { + idInput.value = values.id || ""; + } + if (labelInput) { + labelInput.value = values.label || ""; + } + if (iconSelect) { + iconSelect.value = values.icon || "run"; + } + if (groupInput) { + groupInput.value = values.recovery_group || ""; + } + if (bonusInput) { + bonusInput.value = values.bonus_points || "2"; + } + if (consecutiveInput) { + consecutiveInput.checked = Boolean(values.allow_consecutive); + } + renumber(); + syncPresets(); + }; + + addButton.addEventListener("click", () => { + createRow({}); + }); + + presetButtons.forEach(button => { + button.addEventListener("click", () => { + createRow({ + id: button.dataset.id || "", + label: button.dataset.label || "", + icon: button.dataset.icon || "run", + recovery_group: button.dataset.recoveryGroup || "", + bonus_points: button.dataset.bonusPoints || "2", + allow_consecutive: button.dataset.allowConsecutive === "1", + }); + }); }); list.addEventListener("click", event => { @@ -797,32 +863,16 @@ return; } - const rows = list.querySelectorAll("[data-sport-type-row]"); - if (rows.length <= 1) { - row.querySelectorAll("input[type='text'], input[type='hidden']").forEach(input => { - input.value = ""; - }); - row.querySelectorAll("input[type='number']").forEach(input => { - input.value = "2"; - }); - row.querySelectorAll("input[type='checkbox']").forEach(input => { - input.checked = false; - }); - row.querySelectorAll("select").forEach(select => { - select.value = "run"; - }); - syncPreview(row); - return; - } - row.remove(); renumber(); + syncPresets(); }); list.addEventListener("input", event => { const row = event.target.closest("[data-sport-type-row]"); if (row) { syncPreview(row); + syncPresets(); } }); @@ -830,10 +880,12 @@ const row = event.target.closest("[data-sport-type-row]"); if (row) { syncPreview(row); + syncPresets(); } }); renumber(); + syncPresets(); } window.addEventListener("resize", () => { diff --git a/src/App.php b/src/App.php index 5e8de75..24fe773 100644 --- a/src/App.php +++ b/src/App.php @@ -311,12 +311,25 @@ final class App { $user = $this->requireUser(); $settings = $this->hydrateSettings($this->settings->forUser($user['username'])); + $sportTypePresets = array_values(array_filter( + Defaults::settings()['sport_types'], + static function (array $preset) use ($settings): bool { + foreach (normalized_sport_types($settings) as $type) { + if (($type['id'] ?? '') === ($preset['id'] ?? '')) { + return false; + } + } + + return true; + } + )); View::render('options', [ 'pageTitle' => 'Optionen', 'page' => 'options', 'authUser' => $user, 'settings' => $settings, + 'sportTypePresets' => $sportTypePresets, 'users' => $user['is_admin'] ? $this->users->all() : [], 'maxScore' => $this->scoring->evaluate([ 'mood' => 10, @@ -345,7 +358,7 @@ final class App if ($form === 'settings') { $settings = $this->sanitizeSettings($_POST['settings'] ?? []); $this->settings->saveForUser($user['username'], $settings); - flash('success', 'Die Bewertungslogik wurde aktualisiert.'); + flash('success', 'Deine persönlichen Optionen wurden aktualisiert.'); redirect('/options'); } @@ -551,10 +564,13 @@ final class App ]; } + $sportTypesProvided = array_key_exists('sport_types_present', $input) + || array_key_exists('sport_types', $input); + $settings['sport_types'] = normalized_sport_types([ - 'sport_types' => is_array($input['sport_types'] ?? null) + 'sport_types' => $sportTypesProvided && is_array($input['sport_types'] ?? null) ? $input['sport_types'] - : $defaults['sport_types'], + : ($sportTypesProvided ? [] : $defaults['sport_types']), ]); return $settings; diff --git a/templates/pages/options.php b/templates/pages/options.php index 71ac527..9c92271 100644 --- a/templates/pages/options.php +++ b/templates/pages/options.php @@ -2,8 +2,9 @@
-

Bewertungslogik

-

Score und Schutzregeln anpassen

+

Dein Account

+

Score und Sportarten persönlich anpassen

+

Alle Einstellungen auf dieser Seite gelten nur für deinen eigenen Account und beeinflussen keine anderen Nutzer.

Maximal Punkte
@@ -64,11 +65,34 @@

Sportarten und Bonuspunkte

-

Lege fest, welche Sportarten nur für deinen eigenen Account im Tracking auswählbar sind. Wenn du lieber Varianten wie Krafttraining Zuhause oder Krafttraining Auswärts nutzen möchtest, kannst du sie hier einfach selbst anlegen.

+

Lege fest, welche Sportarten nur in deinem eigenen Tracking auswählbar sind. Entfernte Sportarten kannst du darunter jederzeit wieder für deinen Account hinzufügen.

+ + + +
+ + + +
+ +
$sportType): ?>
@@ -164,6 +188,7 @@

Bewertungsskala

+

Diese Score-Grenzen und Schutzregeln gelten ebenfalls nur für deinen eigenen Account.

$rating): ?>