6 Commits

20 changed files with 568 additions and 110 deletions
+164 -35
View File
@@ -22,6 +22,88 @@
--track-accent: rgba(139, 228, 255, 0.34); --track-accent: rgba(139, 228, 255, 0.34);
--track-surface: rgba(255, 255, 255, 0.08); --track-surface: rgba(255, 255, 255, 0.08);
--track-glow: rgba(139, 228, 255, 0.18); --track-glow: rgba(139, 228, 255, 0.18);
--body-radial-one: radial-gradient(circle at 18% 18%, rgba(76, 171, 255, 0.24), transparent 34%);
--body-radial-two: radial-gradient(circle at 82% 12%, rgba(113, 255, 210, 0.18), transparent 28%);
--body-gradient: linear-gradient(145deg, #07111b 0%, #0b1e2e 35%, #13273b 65%, #08111b 100%);
--panel-gradient-top: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.08));
--panel-gradient-accent: radial-gradient(circle at top left, rgba(255, 255, 255, 0.2), transparent 48%);
--pill-bg: rgba(255, 255, 255, 0.12);
--pill-border: rgba(255, 255, 255, 0.12);
--input-bg: rgba(255, 255, 255, 0.09);
--input-bg-focus: rgba(255, 255, 255, 0.12);
--input-border: rgba(255, 255, 255, 0.14);
--input-border-soft: rgba(255, 255, 255, 0.12);
--nav-hover-bg: rgba(255, 255, 255, 0.13);
--user-chip-bg: rgba(255, 255, 255, 0.1);
--eyebrow-color: rgba(239, 247, 255, 0.62);
--chart-axis-color: rgba(255, 255, 255, 0.1);
--chart-label-color: rgba(239, 247, 255, 0.65);
--chart-value-color: rgba(239, 247, 255, 0.9);
--line-point-stroke: rgba(7, 17, 27, 0.9);
--bar-grid-color: rgba(255, 255, 255, 0.08);
--calendar-detail-bg: rgba(255, 255, 255, 0.08);
--calendar-detail-border: rgba(255, 255, 255, 0.1);
--calendar-detail-eyebrow: rgba(239, 247, 255, 0.58);
--calendar-selected-stroke: rgba(255, 255, 255, 0.9);
--control-soft-bg: rgba(255, 255, 255, 0.08);
--control-soft-border: rgba(255, 255, 255, 0.16);
--brand-shadow: 0 10px 24px rgba(9, 25, 40, 0.22);
}
@media (prefers-color-scheme: light) {
:root {
--bg: #eef4fa;
--bg-soft: #dce8f2;
--surface: rgba(255, 255, 255, 0.72);
--surface-strong: rgba(255, 255, 255, 0.86);
--surface-border: rgba(120, 146, 172, 0.24);
--text: #12304b;
--muted: rgba(18, 48, 75, 0.66);
--shadow: 0 24px 60px rgba(78, 105, 130, 0.16);
--primary: #5abcf2;
--primary-strong: #1494de;
--accent: #63d9b4;
--warm: #ee9f63;
--danger: #db6b6b;
--good: #45c98d;
--track-accent: rgba(67, 153, 212, 0.22);
--track-surface: rgba(255, 255, 255, 0.46);
--track-glow: rgba(104, 201, 255, 0.16);
--body-radial-one: radial-gradient(circle at 18% 18%, rgba(115, 196, 255, 0.28), transparent 34%);
--body-radial-two: radial-gradient(circle at 82% 12%, rgba(129, 232, 212, 0.24), transparent 28%);
--body-gradient: linear-gradient(145deg, #eef5fb 0%, #e4eff7 40%, #d8e9f4 72%, #edf5fa 100%);
--panel-gradient-top: linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(255, 255, 255, 0.58));
--panel-gradient-accent: radial-gradient(circle at top left, rgba(123, 190, 255, 0.18), transparent 48%);
--pill-bg: rgba(255, 255, 255, 0.54);
--pill-border: rgba(130, 158, 185, 0.22);
--input-bg: rgba(255, 255, 255, 0.62);
--input-bg-focus: rgba(255, 255, 255, 0.82);
--input-border: rgba(123, 153, 182, 0.26);
--input-border-soft: rgba(123, 153, 182, 0.22);
--nav-hover-bg: rgba(255, 255, 255, 0.56);
--user-chip-bg: rgba(255, 255, 255, 0.54);
--eyebrow-color: rgba(18, 48, 75, 0.5);
--chart-axis-color: rgba(18, 48, 75, 0.12);
--chart-label-color: rgba(18, 48, 75, 0.58);
--chart-value-color: rgba(18, 48, 75, 0.86);
--line-point-stroke: rgba(255, 255, 255, 0.95);
--bar-grid-color: rgba(18, 48, 75, 0.08);
--calendar-detail-bg: rgba(255, 255, 255, 0.6);
--calendar-detail-border: rgba(120, 146, 172, 0.18);
--calendar-detail-eyebrow: rgba(18, 48, 75, 0.48);
--calendar-selected-stroke: rgba(18, 48, 75, 0.52);
--control-soft-bg: rgba(255, 255, 255, 0.58);
--control-soft-border: rgba(123, 153, 182, 0.22);
--brand-shadow: 0 10px 24px rgba(82, 111, 138, 0.14);
}
html {
color-scheme: light;
}
select {
color-scheme: light;
}
} }
*, *,
@@ -40,9 +122,9 @@ body {
min-height: 100vh; min-height: 100vh;
font-family: var(--font-ui); font-family: var(--font-ui);
background: background:
radial-gradient(circle at 18% 18%, rgba(76, 171, 255, 0.24), transparent 34%), var(--body-radial-one),
radial-gradient(circle at 82% 12%, rgba(113, 255, 210, 0.18), transparent 28%), var(--body-radial-two),
linear-gradient(145deg, #07111b 0%, #0b1e2e 35%, #13273b 65%, #08111b 100%); var(--body-gradient);
color: var(--text); color: var(--text);
} }
@@ -103,8 +185,8 @@ button {
.glass-panel { .glass-panel {
border: 1px solid var(--surface-border); border: 1px solid var(--surface-border);
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.08)), var(--panel-gradient-top),
radial-gradient(circle at top left, rgba(255, 255, 255, 0.2), transparent 48%); var(--panel-gradient-accent);
backdrop-filter: blur(var(--panel-blur)) saturate(150%); backdrop-filter: blur(var(--panel-blur)) saturate(150%);
-webkit-backdrop-filter: blur(var(--panel-blur)) saturate(150%); -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(150%);
box-shadow: var(--shadow); box-shadow: var(--shadow);
@@ -149,9 +231,9 @@ button {
align-items: center; align-items: center;
padding: 0.48rem 0.8rem; padding: 0.48rem 0.8rem;
border-radius: 999px; border-radius: 999px;
background: rgba(255, 255, 255, 0.12); background: var(--pill-bg);
color: var(--muted); color: var(--muted);
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid var(--pill-border);
font-size: 0.88rem; font-size: 0.88rem;
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
@@ -167,10 +249,10 @@ button {
.topbar-date-input { .topbar-date-input {
width: auto; width: auto;
min-height: 2.2rem; min-height: 2.2rem;
border: 1px solid rgba(255, 255, 255, 0.12); border: 1px solid var(--input-border-soft);
border-radius: 999px; border-radius: 999px;
padding: 0.45rem 0.9rem; padding: 0.45rem 0.9rem;
background: rgba(255, 255, 255, 0.08); background: var(--control-soft-bg);
color: var(--text); color: var(--text);
} }
@@ -195,7 +277,7 @@ button {
place-items: center; place-items: center;
border-radius: 18px; border-radius: 18px;
overflow: hidden; overflow: hidden;
box-shadow: 0 10px 24px rgba(9, 25, 40, 0.22); box-shadow: var(--brand-shadow);
} }
.brand-mark img { .brand-mark img {
@@ -222,7 +304,7 @@ button {
margin: 0 0 0.28rem; margin: 0 0 0.28rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.14em; letter-spacing: 0.14em;
color: rgba(239, 247, 255, 0.62); color: var(--eyebrow-color);
font-size: 0.74rem; font-size: 0.74rem;
} }
@@ -244,7 +326,7 @@ button {
.main-nav a:hover, .main-nav a:hover,
.main-nav a.active { .main-nav a.active {
background: rgba(255, 255, 255, 0.13); background: var(--nav-hover-bg);
color: var(--text); color: var(--text);
transform: translateX(2px); transform: translateX(2px);
} }
@@ -269,7 +351,7 @@ button {
gap: 0.75rem; gap: 0.75rem;
padding: 0.9rem 1rem; padding: 0.9rem 1rem;
border-radius: 18px; border-radius: 18px;
background: rgba(255, 255, 255, 0.1); background: var(--user-chip-bg);
} }
.user-chip__name { .user-chip__name {
@@ -415,8 +497,8 @@ button {
margin-bottom: 1rem; margin-bottom: 1rem;
padding: 0.9rem 1rem; padding: 0.9rem 1rem;
border-radius: 18px; border-radius: 18px;
background: rgba(255, 255, 255, 0.08); background: var(--calendar-detail-bg);
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--calendar-detail-border);
} }
.calendar-detail__meta { .calendar-detail__meta {
@@ -425,7 +507,7 @@ button {
} }
.calendar-detail__eyebrow { .calendar-detail__eyebrow {
color: rgba(239, 247, 255, 0.58); color: var(--calendar-detail-eyebrow);
font-size: 0.78rem; font-size: 0.78rem;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
@@ -537,7 +619,7 @@ button {
} }
.calendar-cell--selected { .calendar-cell--selected {
stroke: rgba(255, 255, 255, 0.9); stroke: var(--calendar-selected-stroke);
stroke-width: 1.2; stroke-width: 1.2;
filter: brightness(1.08); filter: brightness(1.08);
} }
@@ -566,7 +648,7 @@ button {
} }
.line-point { .line-point {
stroke: rgba(7, 17, 27, 0.9); stroke: var(--line-point-stroke);
stroke-width: 1.5; stroke-width: 1.5;
} }
@@ -579,7 +661,7 @@ button {
} }
.chart-axis { .chart-axis {
stroke: rgba(255, 255, 255, 0.1); stroke: var(--chart-axis-color);
stroke-width: 1; stroke-width: 1;
} }
@@ -589,12 +671,12 @@ button {
} }
.chart-label { .chart-label {
fill: rgba(239, 247, 255, 0.65); fill: var(--chart-label-color);
font-size: 11px; font-size: 11px;
} }
.chart-value { .chart-value {
fill: rgba(239, 247, 255, 0.9); fill: var(--chart-value-color);
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
} }
@@ -607,7 +689,7 @@ button {
} }
.bar-grid { .bar-grid {
fill: rgba(255, 255, 255, 0.08); fill: var(--bar-grid-color);
} }
.bar-segment--sport { .bar-segment--sport {
@@ -619,12 +701,12 @@ button {
} }
.bar-label { .bar-label {
fill: rgba(239, 247, 255, 0.62); fill: var(--chart-label-color);
font-size: 11px; font-size: 11px;
} }
.bar-value { .bar-value {
fill: rgba(239, 247, 255, 0.82); fill: var(--chart-value-color);
font-size: 10px; font-size: 10px;
text-anchor: middle; text-anchor: middle;
} }
@@ -723,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);
@@ -733,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));
@@ -773,9 +861,9 @@ input[type="date"],
select, select,
textarea { textarea {
width: 100%; width: 100%;
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid var(--input-border);
border-radius: 16px; border-radius: 16px;
background: rgba(255, 255, 255, 0.09); background: var(--input-bg);
color: var(--text); color: var(--text);
padding: 0.9rem 1rem; padding: 0.9rem 1rem;
outline: none; outline: none;
@@ -788,7 +876,7 @@ select {
option, option,
optgroup { optgroup {
background: #10253a; background: var(--bg-soft);
color: var(--text); color: var(--text);
} }
@@ -796,7 +884,7 @@ input:focus,
select:focus, select:focus,
textarea:focus { textarea:focus {
border-color: rgba(139, 228, 255, 0.5); border-color: rgba(139, 228, 255, 0.5);
background: rgba(255, 255, 255, 0.12); background: var(--input-bg-focus);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -920,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 {
@@ -950,8 +1044,8 @@ input[type="range"] {
.ghost-button, .ghost-button,
.ghost-link { .ghost-link {
color: var(--text); color: var(--text);
border: 1px solid rgba(255, 255, 255, 0.16); border: 1px solid var(--control-soft-border);
background: rgba(255, 255, 255, 0.08); background: var(--control-soft-bg);
} }
.ghost-link { .ghost-link {
@@ -1092,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;
} }
+8
View File
@@ -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

+8
View File
@@ -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

+6
View File
@@ -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

+8
View File
@@ -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

+8
View File
@@ -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

+7
View File
@@ -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

+8
View File
@@ -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

+82 -20
View File
@@ -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", () => {
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
+30 -5
View File
@@ -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;
@@ -594,6 +618,7 @@ final class App
header('Referrer-Policy: strict-origin-when-cross-origin'); header('Referrer-Policy: strict-origin-when-cross-origin');
header('X-Frame-Options: DENY'); header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff'); header('X-Content-Type-Options: nosniff');
header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
header('Cross-Origin-Opener-Policy: same-origin'); header('Cross-Origin-Opener-Policy: same-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()'); header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
header("Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'"); header("Content-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'");
+19 -4
View File
@@ -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);
} }
} }
+67 -17
View File
@@ -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,
+2 -16
View File
@@ -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();
+73 -2
View File
@@ -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']),
+1
View File
@@ -18,6 +18,7 @@ $brandSubtitle = match ($page) {
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0b1e2e"> <meta name="theme-color" content="#0b1e2e">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
<title><?= e($pageTitle) ?> · Mood</title> <title><?= e($pageTitle) ?> · Mood</title>
<link rel="icon" type="image/svg+xml" href="/assets/branding/favicon.svg"> <link rel="icon" type="image/svg+xml" href="/assets/branding/favicon.svg">
<link rel="shortcut icon" href="/assets/branding/favicon.svg"> <link rel="shortcut icon" href="/assets/branding/favicon.svg">
+2 -2
View File
@@ -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>
+5
View File
@@ -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>
+65 -9
View File
@@ -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">
+3
View File
@@ -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; ?>