feat: relative weight sliders for focus areas
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

Improved UX for focus area configuration:
- Sliders now use relative weights (0-10) instead of percentages
- System automatically normalizes to percentages (sum=100%)
- Live preview shows "weight → percent%" (e.g., "5 → 50%")
- No more manual balancing required from user

User sets: Kraft=5, Ausdauer=3, Flexibilität=2
System calculates: 50%, 30%, 20%

Addresses user feedback: "Summe muss 100% sein" not user-friendly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-27 12:10:56 +01:00
parent 1fdf91cb50
commit 92cc309489

View File

@ -286,7 +286,7 @@ export default function GoalsPage() {
)}
</div>
<p style={{ color: 'var(--text2)', fontSize: 14, marginBottom: 16 }}>
Gewichte deine Trainingsziele individuell. Die Summe muss 100% ergeben.
Setze relative Gewichte für deine Trainingsziele. Das System berechnet automatisch die Prozentanteile.
{focusAreas && !focusAreas.custom && (
<span style={{ display: 'block', marginTop: 4, fontStyle: 'italic' }}>
Aktuell abgeleitet aus Trainingsmodus "{goalMode}" - klicke "Anpassen" für individuelle Gewichtung
@ -305,7 +305,12 @@ export default function GoalsPage() {
{ key: 'endurance_pct', label: 'Ausdauer', icon: '🏃', color: '#1D9E75' },
{ key: 'flexibility_pct', label: 'Beweglichkeit', icon: '🤸', color: '#E67E22' },
{ key: 'health_pct', label: 'Gesundheit', icon: '❤️', color: '#F59E0B' }
].map(area => (
].map(area => {
const weight = Math.round(focusTemp[area.key] / 10)
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
const actualPercent = sum > 0 ? Math.round(focusTemp[area.key] / sum * 100) : 0
return (
<div key={area.key}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
@ -313,79 +318,55 @@ export default function GoalsPage() {
<span style={{ fontWeight: 500 }}>{area.label}</span>
</div>
<span style={{
fontSize: 18,
fontSize: 16,
fontWeight: 600,
color: area.color,
minWidth: 50,
minWidth: 80,
textAlign: 'right'
}}>
{focusTemp[area.key]}%
{weight} {actualPercent}%
</span>
</div>
<input
type="range"
min="0"
max="100"
step="5"
value={focusTemp[area.key]}
onChange={e => setFocusTemp(f => ({ ...f, [area.key]: parseInt(e.target.value) }))}
max="10"
step="1"
value={weight}
onChange={e => setFocusTemp(f => ({ ...f, [area.key]: parseInt(e.target.value) * 10 }))}
style={{
width: '100%',
height: 8,
borderRadius: 4,
background: `linear-gradient(to right, ${area.color} 0%, ${area.color} ${focusTemp[area.key]}%, var(--border) ${focusTemp[area.key]}%, var(--border) 100%)`,
background: `linear-gradient(to right, ${area.color} 0%, ${area.color} ${weight * 10}%, var(--border) ${weight * 10}%, var(--border) 100%)`,
outline: 'none',
cursor: 'pointer'
}}
/>
</div>
))}
)
})}
</div>
{/* Sum Display */}
{/* Weight Total Display */}
<div style={{
padding: 12,
background: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return 'var(--accent)'
return '#FEF2F2'
})(),
border: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return '1px solid var(--accent)'
return '1px solid #FCA5A5'
})(),
background: 'var(--surface2)',
border: '1px solid var(--border)',
borderRadius: 8,
marginBottom: 16
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{
fontWeight: 600,
color: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return 'white'
return '#DC2626'
})()
}}>
Summe:
<span style={{ fontWeight: 600, color: 'var(--text2)' }}>
Gewichtung gesamt:
</span>
<span style={{
fontSize: 20,
fontWeight: 700,
color: (() => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum === 100) return 'white'
return '#DC2626'
})()
}}>
{Object.values(focusTemp).reduce((a, b) => a + b, 0)}%
<span style={{ fontSize: 18, fontWeight: 600, color: 'var(--text1)' }}>
{Object.values(focusTemp).reduce((a, b) => a + b, 0) / 10}
</span>
</div>
{Object.values(focusTemp).reduce((a, b) => a + b, 0) !== 100 && (
<div style={{ fontSize: 12, marginTop: 4, color: '#DC2626' }}>
Summe muss 100% ergeben
<div style={{ fontSize: 12, marginTop: 4, color: 'var(--text3)' }}>
💡 Die Prozentanteile werden automatisch berechnet
</div>
)}
</div>
{/* Action Buttons */}
@ -394,12 +375,27 @@ export default function GoalsPage() {
className="btn-primary"
onClick={async () => {
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
if (sum !== 100) {
setError('Summe muss 100% ergeben')
if (sum === 0) {
setError('Mindestens ein Bereich muss gewichtet sein')
return
}
// Normalize to percentages
const normalized = {}
Object.keys(focusTemp).forEach(key => {
normalized[key] = Math.round(focusTemp[key] / sum * 100)
})
// Ensure sum is exactly 100 (adjust largest value if needed due to rounding)
const normalizedSum = Object.values(normalized).reduce((a, b) => a + b, 0)
if (normalizedSum !== 100) {
const largest = Object.entries(normalized).reduce((max, [k, v]) => v > max[1] ? [k, v] : max, ['', 0])
normalized[largest[0]] += (100 - normalizedSum)
}
try {
await api.updateFocusAreas(focusTemp)
await api.updateFocusAreas(normalized)
showToast('✓ Fokus-Bereiche aktualisiert')
await loadData()
setFocusEditing(false)
@ -408,7 +404,6 @@ export default function GoalsPage() {
setError(err.message || 'Fehler beim Speichern')
}
}}
disabled={Object.values(focusTemp).reduce((a, b) => a + b, 0) !== 100}
style={{ flex: 1 }}
>
Speichern