Compare commits
4 Commits
0e00dfdc40
...
2cd00b1bf6
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cd00b1bf6 | |||
| 1080dd9d82 | |||
| 80f649c547 | |||
| a1135d37d8 |
@@ -805,9 +805,9 @@ button {
|
|||||||
|
|
||||||
.sport-choice__card {
|
.sport-choice__card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.35rem;
|
gap: 0.5rem;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: 0.85rem 0.95rem;
|
padding: 1rem 1.05rem;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
@@ -815,17 +815,23 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sport-choice__card img {
|
.sport-choice__card img {
|
||||||
width: 1.2rem;
|
width: 1.7rem;
|
||||||
height: 1.2rem;
|
height: 1.7rem;
|
||||||
opacity: 0.96;
|
opacity: 0.96;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sport-choice__card strong {
|
.sport-choice__card strong {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 0.92rem;
|
font-size: 1rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sport-choice__card small {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
.sport-choice input:checked + .sport-choice__card {
|
.sport-choice input:checked + .sport-choice__card {
|
||||||
border-color: rgba(139, 228, 255, 0.44);
|
border-color: rgba(139, 228, 255, 0.44);
|
||||||
background: linear-gradient(180deg, rgba(96, 184, 255, 0.18), rgba(255, 255, 255, 0.08));
|
background: linear-gradient(180deg, rgba(96, 184, 255, 0.18), rgba(255, 255, 255, 0.08));
|
||||||
@@ -1002,6 +1008,12 @@ input[type="range"] {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
.primary-button,
|
.primary-button,
|
||||||
.ghost-button,
|
.ghost-button,
|
||||||
.button-link {
|
.button-link {
|
||||||
@@ -1174,6 +1186,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="18" cy="44" r="8" stroke="#8BE4FF" stroke-width="3"/>
|
||||||
|
<circle cx="46" cy="44" r="8" stroke="#8BE4FF" stroke-width="3"/>
|
||||||
|
<path d="M18 44L28 28H37L46 44" stroke="#EFF7FF" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M28 28L24 21H17" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M33 20H41" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M31 28L38 36" stroke="#EFF7FF" stroke-width="3.5" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 636 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="34" cy="14" r="5" fill="#EFF7FF"/>
|
||||||
|
<path d="M28 27L34 19L42 24" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M34 19L31 33L40 39" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M31 33L20 39" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M40 39L47 51" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M17 50C22 46.5 27 45 32 45C37 45 42 46.5 47 50" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.78"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 681 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M20 14H28L24 27H34L21 50L25 36H16L20 14Z" fill="#EFF7FF"/>
|
||||||
|
<path d="M39 18V46" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M47 22V42" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M55 26V38" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 416 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="32" cy="15" r="5" fill="#EFF7FF"/>
|
||||||
|
<path d="M28 28L33 21L39 26" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M33 21L30 34L24 45" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M30 34L39 41L45 51" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M18 51H50" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.75"/>
|
||||||
|
<path d="M44 22V41" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 671 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="10" y="24" width="8" height="16" rx="3" fill="#8BE4FF" fill-opacity="0.86"/>
|
||||||
|
<rect x="18" y="27" width="6" height="10" rx="2" fill="#8BE4FF" fill-opacity="0.72"/>
|
||||||
|
<rect x="40" y="27" width="6" height="10" rx="2" fill="#8BE4FF" fill-opacity="0.72"/>
|
||||||
|
<rect x="46" y="24" width="8" height="16" rx="3" fill="#8BE4FF" fill-opacity="0.86"/>
|
||||||
|
<rect x="24" y="29" width="16" height="6" rx="3" fill="#EFF7FF"/>
|
||||||
|
<path d="M22 47C25 44.5 28.3 43.2 32 43.2C35.7 43.2 39 44.5 42 47" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 651 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="39" cy="20" r="5" fill="#EFF7FF"/>
|
||||||
|
<path d="M25 28C29 23 36 22 43 24" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M27 35C31 31 36 30 42 31" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M11 43C15 40 18 40 22 43C26 46 29 46 33 43C37 40 40 40 44 43C48 46 51 46 55 43" stroke="#7FF3BB" stroke-width="3" stroke-linecap="round"/>
|
||||||
|
<path d="M9 50C13 47 16 47 20 50C24 53 27 53 31 50C35 47 38 47 42 50C46 53 49 53 53 50" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 657 B |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="32" cy="18" r="5" fill="#EFF7FF"/>
|
||||||
|
<path d="M24 31C27 27.5 29.6 26 32 26C34.4 26 37 27.5 40 31" stroke="#8BE4FF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M32 26V41" stroke="#EFF7FF" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M32 41L22 48" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M32 41L42 48" stroke="#7FF3BB" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<path d="M16 50H48" stroke="#8BE4FF" stroke-width="3" stroke-linecap="round" opacity="0.8"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 618 B |
@@ -752,19 +752,27 @@
|
|||||||
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]"]');
|
||||||
|
const locationSelect = row.querySelector('select[data-name-template$="[location]"]');
|
||||||
const previewText = row.querySelector(".sport-pill span:last-child");
|
const previewText = row.querySelector(".sport-pill span:last-child");
|
||||||
const previewImage = row.querySelector(".sport-pill img");
|
const previewImage = row.querySelector(".sport-pill img");
|
||||||
|
|
||||||
if (previewText) {
|
if (previewText) {
|
||||||
previewText.textContent = (labelInput && labelInput.value.trim()) || "Neue Sportart";
|
const label = (labelInput && labelInput.value.trim()) || "Neue Sportart";
|
||||||
|
const location = locationSelect && locationSelect.value
|
||||||
|
? locationSelect.options[locationSelect.selectedIndex]?.textContent || ""
|
||||||
|
: "";
|
||||||
|
previewText.textContent = location ? `${label} · ${location}` : label;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previewImage && iconSelect) {
|
if (previewImage && iconSelect) {
|
||||||
@@ -781,9 +789,77 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
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 locationSelect = row.querySelector('select[data-name-template$="[location]"]');
|
||||||
|
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 (locationSelect) {
|
||||||
|
locationSelect.value = values.location || "";
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
location: button.dataset.location || "",
|
||||||
|
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 +873,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 +890,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", () => {
|
||||||
|
|||||||
@@ -170,7 +170,9 @@ final class App
|
|||||||
redirect('/login');
|
redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->auth->attempt($username, $password)) {
|
$remember = isset($_POST['remember_me']) && $_POST['remember_me'] === '1';
|
||||||
|
|
||||||
|
if (!$this->auth->attempt($username, $password, $remember)) {
|
||||||
$this->throttle->hit($throttleKey);
|
$this->throttle->hit($throttleKey);
|
||||||
flash('error', 'Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.');
|
flash('error', 'Login fehlgeschlagen. Bitte prüfe Benutzername und Passwort.');
|
||||||
redirect('/login');
|
redirect('/login');
|
||||||
@@ -309,12 +311,26 @@ 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,
|
||||||
|
'sportLocationOptions' => sport_location_options(),
|
||||||
'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,
|
||||||
@@ -343,7 +359,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');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +482,12 @@ final class App
|
|||||||
'sport' => $entry['sport_minutes'],
|
'sport' => $entry['sport_minutes'],
|
||||||
'walk' => $entry['walk_minutes'],
|
'walk' => $entry['walk_minutes'],
|
||||||
'sport_labels' => array_values(array_filter(array_map(
|
'sport_labels' => array_values(array_filter(array_map(
|
||||||
static fn (array $type): string => (string) ($type['label'] ?? ''),
|
static function (array $type): string {
|
||||||
|
$label = (string) ($type['label'] ?? '');
|
||||||
|
$location = sport_location_label((string) ($type['location'] ?? ''));
|
||||||
|
|
||||||
|
return $location !== '' ? $label . ' · ' . $location : $label;
|
||||||
|
},
|
||||||
$entry['sport_type_meta'] ?? []
|
$entry['sport_type_meta'] ?? []
|
||||||
))),
|
))),
|
||||||
'sport_icons' => array_values(array_filter(array_map(
|
'sport_icons' => array_values(array_filter(array_map(
|
||||||
@@ -549,10 +570,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;
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ final class Auth
|
|||||||
|
|
||||||
$username = $_SESSION['user']['username'] ?? null;
|
$username = $_SESSION['user']['username'] ?? null;
|
||||||
|
|
||||||
return is_string($username) && $username !== '';
|
$valid = is_string($username) && $username !== '';
|
||||||
|
|
||||||
|
if ($valid && !empty($_SESSION['remember_me'])) {
|
||||||
|
setcookie(session_name(), session_id(), session_cookie_options_for(time() + remember_me_lifetime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function user(): ?array
|
public function user(): ?array
|
||||||
@@ -28,7 +34,7 @@ final class Auth
|
|||||||
return $_SESSION['user'];
|
return $_SESSION['user'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function attempt(string $username, string $password): bool
|
public function attempt(string $username, string $password, bool $remember = false): bool
|
||||||
{
|
{
|
||||||
$user = $this->users->verify($username, $password);
|
$user = $this->users->verify($username, $password);
|
||||||
|
|
||||||
@@ -36,12 +42,12 @@ final class Auth
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->login($user);
|
$this->login($user, $remember);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function login(array $user): void
|
public function login(array $user, bool $remember = false): void
|
||||||
{
|
{
|
||||||
if (!isset($user['username']) || !is_string($user['username']) || $user['username'] === '') {
|
if (!isset($user['username']) || !is_string($user['username']) || $user['username'] === '') {
|
||||||
throw new RuntimeException('Der Benutzer konnte nicht angemeldet werden.');
|
throw new RuntimeException('Der Benutzer konnte nicht angemeldet werden.');
|
||||||
@@ -53,11 +59,20 @@ final class Auth
|
|||||||
'username' => $user['username'],
|
'username' => $user['username'],
|
||||||
'is_admin' => (bool) ($user['is_admin'] ?? false),
|
'is_admin' => (bool) ($user['is_admin'] ?? false),
|
||||||
];
|
];
|
||||||
|
$_SESSION['remember_me'] = $remember;
|
||||||
|
|
||||||
|
if ($remember) {
|
||||||
|
setcookie(session_name(), session_id(), session_cookie_options_for(time() + remember_me_lifetime()));
|
||||||
|
} else {
|
||||||
|
setcookie(session_name(), session_id(), session_cookie_options_for());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function logout(): void
|
public function logout(): void
|
||||||
{
|
{
|
||||||
unset($_SESSION['user']);
|
unset($_SESSION['user']);
|
||||||
|
unset($_SESSION['remember_me']);
|
||||||
|
setcookie(session_name(), '', session_cookie_options_for(time() - 3600));
|
||||||
session_regenerate_id(true);
|
session_regenerate_id(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,42 +17,92 @@ final class Defaults
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'sport_types' => [
|
'sport_types' => [
|
||||||
[
|
|
||||||
'id' => 'strength-home',
|
|
||||||
'label' => 'Kraftsport (Keller)',
|
|
||||||
'icon' => 'strength-home',
|
|
||||||
'recovery_group' => 'kraftsport',
|
|
||||||
'bonus_points' => 2,
|
|
||||||
'allow_consecutive' => false,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'id' => 'strength-gym',
|
|
||||||
'label' => 'Kraftsport (Gym)',
|
|
||||||
'icon' => 'strength-gym',
|
|
||||||
'recovery_group' => 'kraftsport',
|
|
||||||
'bonus_points' => 2,
|
|
||||||
'allow_consecutive' => false,
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'id' => 'running',
|
'id' => 'running',
|
||||||
'label' => 'Joggen',
|
'label' => 'Joggen',
|
||||||
'icon' => 'run',
|
'icon' => 'run',
|
||||||
|
'location' => '',
|
||||||
'recovery_group' => 'joggen',
|
'recovery_group' => 'joggen',
|
||||||
'bonus_points' => 2,
|
'bonus_points' => 2,
|
||||||
'allow_consecutive' => false,
|
'allow_consecutive' => false,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'id' => 'cycling',
|
||||||
|
'label' => 'Radfahren',
|
||||||
|
'icon' => 'bike',
|
||||||
|
'location' => '',
|
||||||
|
'recovery_group' => 'radfahren',
|
||||||
|
'bonus_points' => 2,
|
||||||
|
'allow_consecutive' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'strength',
|
||||||
|
'label' => 'Krafttraining',
|
||||||
|
'icon' => 'strength',
|
||||||
|
'location' => '',
|
||||||
|
'recovery_group' => 'kraftsport',
|
||||||
|
'bonus_points' => 2,
|
||||||
|
'allow_consecutive' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'hiking',
|
||||||
|
'label' => 'Wandern',
|
||||||
|
'icon' => 'hike',
|
||||||
|
'location' => '',
|
||||||
|
'recovery_group' => 'wandern',
|
||||||
|
'bonus_points' => 2,
|
||||||
|
'allow_consecutive' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'swimming',
|
||||||
|
'label' => 'Schwimmen',
|
||||||
|
'icon' => 'swim',
|
||||||
|
'location' => '',
|
||||||
|
'recovery_group' => 'schwimmen',
|
||||||
|
'bonus_points' => 2,
|
||||||
|
'allow_consecutive' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'yoga',
|
||||||
|
'label' => 'Yoga',
|
||||||
|
'icon' => 'yoga',
|
||||||
|
'location' => '',
|
||||||
|
'recovery_group' => 'yoga',
|
||||||
|
'bonus_points' => 2,
|
||||||
|
'allow_consecutive' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'hiit-workout',
|
||||||
|
'label' => 'HIIT / Workout',
|
||||||
|
'icon' => 'hiit',
|
||||||
|
'location' => '',
|
||||||
|
'recovery_group' => 'hiit',
|
||||||
|
'bonus_points' => 2,
|
||||||
|
'allow_consecutive' => false,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'id' => 'rowing',
|
'id' => 'rowing',
|
||||||
'label' => 'Rudergerät',
|
'label' => 'Rudern',
|
||||||
'icon' => 'row',
|
'icon' => 'row',
|
||||||
|
'location' => '',
|
||||||
'recovery_group' => 'rudern',
|
'recovery_group' => 'rudern',
|
||||||
'bonus_points' => 2,
|
'bonus_points' => 2,
|
||||||
'allow_consecutive' => false,
|
'allow_consecutive' => false,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'id' => 'dance',
|
||||||
|
'label' => 'Tanzen',
|
||||||
|
'icon' => 'dance',
|
||||||
|
'location' => '',
|
||||||
|
'recovery_group' => 'tanzen',
|
||||||
|
'bonus_points' => 2,
|
||||||
|
'allow_consecutive' => false,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'id' => 'core',
|
'id' => 'core',
|
||||||
'label' => 'Core',
|
'label' => 'Core',
|
||||||
'icon' => 'core',
|
'icon' => 'core',
|
||||||
|
'location' => '',
|
||||||
'recovery_group' => 'core',
|
'recovery_group' => 'core',
|
||||||
'bonus_points' => 2,
|
'bonus_points' => 2,
|
||||||
'allow_consecutive' => true,
|
'allow_consecutive' => true,
|
||||||
|
|||||||
@@ -15,26 +15,12 @@ require __DIR__ . '/App.php';
|
|||||||
|
|
||||||
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Berlin');
|
date_default_timezone_set($_ENV['APP_TIMEZONE'] ?? 'Europe/Berlin');
|
||||||
|
|
||||||
$forwardedProto = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
|
|
||||||
$forwardedSsl = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? ''));
|
|
||||||
$isSecure = (
|
|
||||||
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
|
||||||
|| $forwardedProto === 'https'
|
|
||||||
|| $forwardedSsl === 'on'
|
|
||||||
);
|
|
||||||
|
|
||||||
ini_set('session.use_only_cookies', '1');
|
ini_set('session.use_only_cookies', '1');
|
||||||
ini_set('session.use_strict_mode', '1');
|
ini_set('session.use_strict_mode', '1');
|
||||||
|
ini_set('session.gc_maxlifetime', (string) remember_me_lifetime());
|
||||||
|
|
||||||
session_name('mood_session');
|
session_name('mood_session');
|
||||||
session_set_cookie_params([
|
session_set_cookie_params(session_cookie_params_for());
|
||||||
'lifetime' => 0,
|
|
||||||
'path' => '/',
|
|
||||||
'domain' => '',
|
|
||||||
'secure' => $isSecure,
|
|
||||||
'httponly' => true,
|
|
||||||
'samesite' => 'Lax',
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
session_start();
|
session_start();
|
||||||
|
|||||||
@@ -176,6 +176,47 @@ function mood_icon_path(string $sentiment): string
|
|||||||
return icon_path('mood-' . $sentiment);
|
return icon_path('mood-' . $sentiment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function request_is_secure(): bool
|
||||||
|
{
|
||||||
|
$forwardedProto = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? ''));
|
||||||
|
$forwardedSsl = strtolower((string) ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? ''));
|
||||||
|
|
||||||
|
return (
|
||||||
|
(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
||||||
|
|| $forwardedProto === 'https'
|
||||||
|
|| $forwardedSsl === 'on'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remember_me_lifetime(): int
|
||||||
|
{
|
||||||
|
return 60 * 60 * 24 * 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
function session_cookie_params_for(int $lifetime = 0): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'lifetime' => $lifetime,
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => '',
|
||||||
|
'secure' => request_is_secure(),
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function session_cookie_options_for(int $expires = 0): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'expires' => $expires,
|
||||||
|
'path' => '/',
|
||||||
|
'domain' => '',
|
||||||
|
'secure' => request_is_secure(),
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function sport_icon_path(string $icon): string
|
function sport_icon_path(string $icon): string
|
||||||
{
|
{
|
||||||
return icon_path('sport-' . $icon);
|
return icon_path('sport-' . $icon);
|
||||||
@@ -184,14 +225,38 @@ function sport_icon_path(string $icon): string
|
|||||||
function sport_icon_options(): array
|
function sport_icon_options(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'strength-home' => 'Kraftsport daheim',
|
'strength' => 'Krafttraining',
|
||||||
'strength-gym' => 'Kraftsport im Gym',
|
'bike' => 'Radfahren',
|
||||||
'run' => 'Joggen',
|
'run' => 'Joggen',
|
||||||
|
'hike' => 'Wandern',
|
||||||
|
'swim' => 'Schwimmen',
|
||||||
|
'yoga' => 'Yoga',
|
||||||
|
'hiit' => 'HIIT / Workout',
|
||||||
'row' => 'Rudergerät',
|
'row' => 'Rudergerät',
|
||||||
|
'dance' => 'Tanzen',
|
||||||
'core' => 'Core',
|
'core' => 'Core',
|
||||||
|
'strength-home' => 'Krafttraining Zuhause',
|
||||||
|
'strength-gym' => 'Krafttraining Auswärts',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sport_location_options(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'' => 'ohne Angabe',
|
||||||
|
'home' => 'Zuhause',
|
||||||
|
'away' => 'Auswärts',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function sport_location_label(?string $value): string
|
||||||
|
{
|
||||||
|
$options = sport_location_options();
|
||||||
|
$value = is_string($value) ? trim($value) : '';
|
||||||
|
|
||||||
|
return $options[$value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
function normalize_sport_type_id(string $value): string
|
function normalize_sport_type_id(string $value): string
|
||||||
{
|
{
|
||||||
$value = trim(strtr($value, [
|
$value = trim(strtr($value, [
|
||||||
@@ -252,6 +317,11 @@ function normalized_sport_types(array $settings): array
|
|||||||
$icon = 'run';
|
$icon = 'run';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$location = trim((string) ($type['location'] ?? ''));
|
||||||
|
if (!array_key_exists($location, sport_location_options())) {
|
||||||
|
$location = '';
|
||||||
|
}
|
||||||
|
|
||||||
$group = trim((string) ($type['recovery_group'] ?? ''));
|
$group = trim((string) ($type['recovery_group'] ?? ''));
|
||||||
if ($group === '') {
|
if ($group === '') {
|
||||||
$group = $id;
|
$group = $id;
|
||||||
@@ -261,6 +331,7 @@ function normalized_sport_types(array $settings): array
|
|||||||
'id' => $id,
|
'id' => $id,
|
||||||
'label' => $label,
|
'label' => $label,
|
||||||
'icon' => $icon,
|
'icon' => $icon,
|
||||||
|
'location' => $location,
|
||||||
'recovery_group' => normalize_sport_type_id($group) ?: $id,
|
'recovery_group' => normalize_sport_type_id($group) ?: $id,
|
||||||
'bonus_points' => max(0, min(20, (int) ($type['bonus_points'] ?? 0))),
|
'bonus_points' => max(0, min(20, (int) ($type['bonus_points'] ?? 0))),
|
||||||
'allow_consecutive' => !empty($type['allow_consecutive']),
|
'allow_consecutive' => !empty($type['allow_consecutive']),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<?php foreach ($entry['sport_type_meta'] as $sportType): ?>
|
<?php foreach ($entry['sport_type_meta'] as $sportType): ?>
|
||||||
<span class="sport-pill">
|
<span class="sport-pill">
|
||||||
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
||||||
<span><?= e($sportType['label']) ?></span>
|
<span><?= e($sportType['label']) ?><?= !empty($sportType['location']) ? ' · ' . e(sport_location_label((string) $sportType['location'])) : '' ?></span>
|
||||||
</span>
|
</span>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</span>
|
</span>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<?php foreach ($selectedEntry['sport_type_meta'] as $sportType): ?>
|
<?php foreach ($selectedEntry['sport_type_meta'] as $sportType): ?>
|
||||||
<span class="sport-pill sport-pill--inline">
|
<span class="sport-pill sport-pill--inline">
|
||||||
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
||||||
<span><?= e($sportType['label']) ?></span>
|
<span><?= e($sportType['label']) ?><?= !empty($sportType['location']) ? ' · ' . e(sport_location_label((string) $sportType['location'])) : '' ?></span>
|
||||||
</span>
|
</span>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
<input type="password" name="password" autocomplete="current-password" required>
|
<input type="password" name="password" autocomplete="current-password" required>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="checkbox-row">
|
||||||
|
<input type="checkbox" name="remember_me" value="1">
|
||||||
|
<span>Angemeldet bleiben</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<button class="primary-button" type="submit">Anmelden</button>
|
<button class="primary-button" type="submit">Anmelden</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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,35 @@
|
|||||||
<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 im Tracking auswählbar sind. Der Bonus gilt nur, wenn am Vortag keine gleiche Erholungsgruppe trainiert wurde.</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-location="<?= e($preset['location'] ?? '') ?>"
|
||||||
|
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>
|
||||||
@@ -90,10 +115,21 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>Erholungsgruppe</span>
|
<span>Ort</span>
|
||||||
<input type="text" name="settings[sport_types][<?= e((string) $index) ?>][recovery_group]" value="<?= e($sportType['recovery_group']) ?>" data-name-template="settings[sport_types][__INDEX__][recovery_group]">
|
<select name="settings[sport_types][<?= e((string) $index) ?>][location]" data-name-template="settings[sport_types][__INDEX__][location]">
|
||||||
|
<?php foreach ($sportLocationOptions as $locationValue => $locationLabel): ?>
|
||||||
|
<option value="<?= e($locationValue) ?>" <?= ($sportType['location'] ?? '') === $locationValue ? 'selected' : '' ?>><?= e($locationLabel) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span>Erholungsgruppe optional</span>
|
||||||
|
<input type="text" name="settings[sport_types][<?= e((string) $index) ?>][recovery_group]" value="<?= e($sportType['recovery_group']) ?>" data-name-template="settings[sport_types][__INDEX__][recovery_group]">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-grid field-grid--four">
|
||||||
<label>
|
<label>
|
||||||
<span>Bonuspunkte</span>
|
<span>Bonuspunkte</span>
|
||||||
<input type="number" name="settings[sport_types][<?= e((string) $index) ?>][bonus_points]" value="<?= e((string) $sportType['bonus_points']) ?>" min="0" max="20" data-name-template="settings[sport_types][__INDEX__][bonus_points]">
|
<input type="number" name="settings[sport_types][<?= e((string) $index) ?>][bonus_points]" value="<?= e((string) $sportType['bonus_points']) ?>" min="0" max="20" data-name-template="settings[sport_types][__INDEX__][bonus_points]">
|
||||||
@@ -105,10 +141,12 @@
|
|||||||
<span>Darf an aufeinanderfolgenden Tagen Bonus geben</span>
|
<span>Darf an aufeinanderfolgenden Tagen Bonus geben</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<p class="helper-text">Die Erholungsgruppe brauchst du nur, wenn mehrere Varianten dieselbe Belastung teilen sollen, zum Beispiel Krafttraining Zuhause und Krafttraining Auswärts. Wenn du nichts Besonderes einträgst, reicht die Sportart selbst.</p>
|
||||||
|
|
||||||
<div class="sport-type-card__actions">
|
<div class="sport-type-card__actions">
|
||||||
<span class="sport-pill sport-pill--soft">
|
<span class="sport-pill sport-pill--soft">
|
||||||
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
||||||
<span><?= e($sportType['label']) ?></span>
|
<span><?= e($sportType['label']) ?><?= !empty($sportType['location']) ? ' · ' . e(sport_location_label((string) $sportType['location'])) : '' ?></span>
|
||||||
</span>
|
</span>
|
||||||
<button class="ghost-button ghost-button--small" type="button" data-remove-sport-type>Entfernen</button>
|
<button class="ghost-button ghost-button--small" type="button" data-remove-sport-type>Entfernen</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -116,6 +154,10 @@
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section-actions">
|
||||||
|
<button class="primary-button" type="submit">Sportarten speichern</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template id="sport-type-row-template">
|
<template id="sport-type-row-template">
|
||||||
<div class="sport-type-card band-card" data-sport-type-row>
|
<div class="sport-type-card band-card" data-sport-type-row>
|
||||||
<input type="hidden" value="" data-name-template="settings[sport_types][__INDEX__][id]">
|
<input type="hidden" value="" data-name-template="settings[sport_types][__INDEX__][id]">
|
||||||
@@ -123,7 +165,7 @@
|
|||||||
<div class="field-grid field-grid--four">
|
<div class="field-grid field-grid--four">
|
||||||
<label>
|
<label>
|
||||||
<span>Bezeichnung</span>
|
<span>Bezeichnung</span>
|
||||||
<input type="text" value="" placeholder="z. B. Mobility" data-name-template="settings[sport_types][__INDEX__][label]">
|
<input type="text" value="" placeholder="z. B. Krafttraining Zuhause" data-name-template="settings[sport_types][__INDEX__][label]">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
@@ -136,10 +178,21 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<label>
|
||||||
<span>Erholungsgruppe</span>
|
<span>Ort</span>
|
||||||
<input type="text" value="" placeholder="z. B. kraftsport" data-name-template="settings[sport_types][__INDEX__][recovery_group]">
|
<select data-name-template="settings[sport_types][__INDEX__][location]">
|
||||||
|
<?php foreach ($sportLocationOptions as $locationValue => $locationLabel): ?>
|
||||||
|
<option value="<?= e($locationValue) ?>"><?= e($locationLabel) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span>Erholungsgruppe optional</span>
|
||||||
|
<input type="text" value="" placeholder="z. B. krafttraining" data-name-template="settings[sport_types][__INDEX__][recovery_group]">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-grid field-grid--four">
|
||||||
<label>
|
<label>
|
||||||
<span>Bonuspunkte</span>
|
<span>Bonuspunkte</span>
|
||||||
<input type="number" value="2" min="0" max="20" data-name-template="settings[sport_types][__INDEX__][bonus_points]">
|
<input type="number" value="2" min="0" max="20" data-name-template="settings[sport_types][__INDEX__][bonus_points]">
|
||||||
@@ -151,6 +204,8 @@
|
|||||||
<span>Darf an aufeinanderfolgenden Tagen Bonus geben</span>
|
<span>Darf an aufeinanderfolgenden Tagen Bonus geben</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<p class="helper-text">Die Erholungsgruppe ist nur dann nützlich, wenn mehrere Varianten sportlich gleich belastend sind und denselben Bonusrhythmus teilen sollen.</p>
|
||||||
|
|
||||||
<div class="sport-type-card__actions">
|
<div class="sport-type-card__actions">
|
||||||
<span class="sport-pill sport-pill--soft">
|
<span class="sport-pill sport-pill--soft">
|
||||||
<img src="<?= e(sport_icon_path('run')) ?>" alt="">
|
<img src="<?= e(sport_icon_path('run')) ?>" alt="">
|
||||||
@@ -164,6 +219,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">
|
||||||
|
|||||||
@@ -76,6 +76,9 @@
|
|||||||
<span class="sport-choice__card">
|
<span class="sport-choice__card">
|
||||||
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
<img src="<?= e(sport_icon_path($sportType['icon'])) ?>" alt="">
|
||||||
<strong><?= e($sportType['label']) ?></strong>
|
<strong><?= e($sportType['label']) ?></strong>
|
||||||
|
<?php if (!empty($sportType['location'])): ?>
|
||||||
|
<small><?= e(sport_location_label((string) $sportType['location'])) ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||