Make score and sport settings fully account-specific
This commit is contained in:
@@ -1174,6 +1174,41 @@ input[type="range"] {
|
|||||||
gap: 0.9rem;
|
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 {
|
.sport-type-card {
|
||||||
gap: 0.9rem;
|
gap: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-19
@@ -752,11 +752,14 @@
|
|||||||
const list = document.querySelector("[data-sport-type-list]");
|
const list = document.querySelector("[data-sport-type-list]");
|
||||||
const addButton = document.querySelector("[data-add-sport-type]");
|
const addButton = document.querySelector("[data-add-sport-type]");
|
||||||
const template = document.querySelector("#sport-type-row-template");
|
const template = document.querySelector("#sport-type-row-template");
|
||||||
|
const presetButtons = [...document.querySelectorAll("[data-sport-preset]")];
|
||||||
|
|
||||||
if (!list || !addButton || !template) {
|
if (!list || !addButton || !template) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getRows = () => [...list.querySelectorAll("[data-sport-type-row]")];
|
||||||
|
|
||||||
const syncPreview = row => {
|
const syncPreview = row => {
|
||||||
const labelInput = row.querySelector('input[data-name-template$="[label]"]');
|
const labelInput = row.querySelector('input[data-name-template$="[label]"]');
|
||||||
const iconSelect = row.querySelector('select[data-name-template$="[icon]"]');
|
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));
|
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();
|
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 => {
|
list.addEventListener("click", event => {
|
||||||
@@ -797,32 +863,16 @@
|
|||||||
return;
|
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();
|
row.remove();
|
||||||
renumber();
|
renumber();
|
||||||
|
syncPresets();
|
||||||
});
|
});
|
||||||
|
|
||||||
list.addEventListener("input", event => {
|
list.addEventListener("input", event => {
|
||||||
const row = event.target.closest("[data-sport-type-row]");
|
const row = event.target.closest("[data-sport-type-row]");
|
||||||
if (row) {
|
if (row) {
|
||||||
syncPreview(row);
|
syncPreview(row);
|
||||||
|
syncPresets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -830,10 +880,12 @@
|
|||||||
const row = event.target.closest("[data-sport-type-row]");
|
const row = event.target.closest("[data-sport-type-row]");
|
||||||
if (row) {
|
if (row) {
|
||||||
syncPreview(row);
|
syncPreview(row);
|
||||||
|
syncPresets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
renumber();
|
renumber();
|
||||||
|
syncPresets();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
|
|||||||
+19
-3
@@ -311,12 +311,25 @@ final class App
|
|||||||
{
|
{
|
||||||
$user = $this->requireUser();
|
$user = $this->requireUser();
|
||||||
$settings = $this->hydrateSettings($this->settings->forUser($user['username']));
|
$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', [
|
View::render('options', [
|
||||||
'pageTitle' => 'Optionen',
|
'pageTitle' => 'Optionen',
|
||||||
'page' => 'options',
|
'page' => 'options',
|
||||||
'authUser' => $user,
|
'authUser' => $user,
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
|
'sportTypePresets' => $sportTypePresets,
|
||||||
'users' => $user['is_admin'] ? $this->users->all() : [],
|
'users' => $user['is_admin'] ? $this->users->all() : [],
|
||||||
'maxScore' => $this->scoring->evaluate([
|
'maxScore' => $this->scoring->evaluate([
|
||||||
'mood' => 10,
|
'mood' => 10,
|
||||||
@@ -345,7 +358,7 @@ final class App
|
|||||||
if ($form === 'settings') {
|
if ($form === 'settings') {
|
||||||
$settings = $this->sanitizeSettings($_POST['settings'] ?? []);
|
$settings = $this->sanitizeSettings($_POST['settings'] ?? []);
|
||||||
$this->settings->saveForUser($user['username'], $settings);
|
$this->settings->saveForUser($user['username'], $settings);
|
||||||
flash('success', 'Die Bewertungslogik wurde aktualisiert.');
|
flash('success', 'Deine persönlichen Optionen wurden aktualisiert.');
|
||||||
redirect('/options');
|
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([
|
$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']
|
? $input['sport_types']
|
||||||
: $defaults['sport_types'],
|
: ($sportTypesProvided ? [] : $defaults['sport_types']),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $settings;
|
return $settings;
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
<article class="glass-panel form-panel form-panel--wide">
|
<article class="glass-panel form-panel form-panel--wide">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Bewertungslogik</p>
|
<p class="eyebrow">Dein Account</p>
|
||||||
<h3>Score und Schutzregeln anpassen</h3>
|
<h3>Score und Sportarten persönlich anpassen</h3>
|
||||||
|
<p class="helper-text">Alle Einstellungen auf dieser Seite gelten nur für deinen eigenen Account und beeinflussen keine anderen Nutzer.</p>
|
||||||
</div>
|
</div>
|
||||||
<span class="chart-chip">Maximal <?= e(format_points((float) $maxScore)) ?> Punkte</span>
|
<span class="chart-chip">Maximal <?= e(format_points((float) $maxScore)) ?> Punkte</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,11 +65,34 @@
|
|||||||
<div class="section-head section-head--compact">
|
<div class="section-head section-head--compact">
|
||||||
<div>
|
<div>
|
||||||
<h4>Sportarten und Bonuspunkte</h4>
|
<h4>Sportarten und Bonuspunkte</h4>
|
||||||
<p class="helper-text">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.</p>
|
<p class="helper-text">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.</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="ghost-button" type="button" data-add-sport-type>Sportart hinzufügen</button>
|
<button class="ghost-button" type="button" data-add-sport-type>Sportart hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="settings[sport_types_present]" value="1">
|
||||||
|
|
||||||
|
<?php if (!empty($sportTypePresets)): ?>
|
||||||
|
<div class="preset-list">
|
||||||
|
<?php foreach ($sportTypePresets as $preset): ?>
|
||||||
|
<button
|
||||||
|
class="preset-pill"
|
||||||
|
type="button"
|
||||||
|
data-sport-preset
|
||||||
|
data-id="<?= e($preset['id']) ?>"
|
||||||
|
data-label="<?= e($preset['label']) ?>"
|
||||||
|
data-icon="<?= e($preset['icon']) ?>"
|
||||||
|
data-recovery-group="<?= e($preset['recovery_group']) ?>"
|
||||||
|
data-bonus-points="<?= e((string) $preset['bonus_points']) ?>"
|
||||||
|
data-allow-consecutive="<?= !empty($preset['allow_consecutive']) ? '1' : '0' ?>"
|
||||||
|
>
|
||||||
|
<img src="<?= e(sport_icon_path($preset['icon'])) ?>" alt="">
|
||||||
|
<span><?= e($preset['label']) ?></span>
|
||||||
|
</button>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="sport-type-list" data-sport-type-list>
|
<div class="sport-type-list" data-sport-type-list>
|
||||||
<?php foreach ($settings['sport_types'] as $index => $sportType): ?>
|
<?php foreach ($settings['sport_types'] as $index => $sportType): ?>
|
||||||
<div class="sport-type-card band-card" data-sport-type-row>
|
<div class="sport-type-card band-card" data-sport-type-row>
|
||||||
@@ -164,6 +188,7 @@
|
|||||||
|
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h4>Bewertungsskala</h4>
|
<h4>Bewertungsskala</h4>
|
||||||
|
<p class="helper-text">Diese Score-Grenzen und Schutzregeln gelten ebenfalls nur für deinen eigenen Account.</p>
|
||||||
<div class="band-grid">
|
<div class="band-grid">
|
||||||
<?php foreach ($settings['ratings'] as $index => $rating): ?>
|
<?php foreach ($settings['ratings'] as $index => $rating): ?>
|
||||||
<div class="band-card">
|
<div class="band-card">
|
||||||
|
|||||||
Reference in New Issue
Block a user