feat: relative weight sliders for focus areas
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:
parent
1fdf91cb50
commit
92cc309489
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user