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>
|
</div>
|
||||||
<p style={{ color: 'var(--text2)', fontSize: 14, marginBottom: 16 }}>
|
<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 && (
|
{focusAreas && !focusAreas.custom && (
|
||||||
<span style={{ display: 'block', marginTop: 4, fontStyle: 'italic' }}>
|
<span style={{ display: 'block', marginTop: 4, fontStyle: 'italic' }}>
|
||||||
ℹ️ Aktuell abgeleitet aus Trainingsmodus "{goalMode}" - klicke "Anpassen" für individuelle Gewichtung
|
ℹ️ Aktuell abgeleitet aus Trainingsmodus "{goalMode}" - klicke "Anpassen" für individuelle Gewichtung
|
||||||
|
|
@ -305,87 +305,68 @@ export default function GoalsPage() {
|
||||||
{ key: 'endurance_pct', label: 'Ausdauer', icon: '🏃', color: '#1D9E75' },
|
{ key: 'endurance_pct', label: 'Ausdauer', icon: '🏃', color: '#1D9E75' },
|
||||||
{ key: 'flexibility_pct', label: 'Beweglichkeit', icon: '🤸', color: '#E67E22' },
|
{ key: 'flexibility_pct', label: 'Beweglichkeit', icon: '🤸', color: '#E67E22' },
|
||||||
{ key: 'health_pct', label: 'Gesundheit', icon: '❤️', color: '#F59E0B' }
|
{ key: 'health_pct', label: 'Gesundheit', icon: '❤️', color: '#F59E0B' }
|
||||||
].map(area => (
|
].map(area => {
|
||||||
<div key={area.key}>
|
const weight = Math.round(focusTemp[area.key] / 10)
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
const actualPercent = sum > 0 ? Math.round(focusTemp[area.key] / sum * 100) : 0
|
||||||
<span style={{ fontSize: 20 }}>{area.icon}</span>
|
|
||||||
<span style={{ fontWeight: 500 }}>{area.label}</span>
|
return (
|
||||||
|
<div key={area.key}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<span style={{ fontSize: 20 }}>{area.icon}</span>
|
||||||
|
<span style={{ fontWeight: 500 }}>{area.label}</span>
|
||||||
|
</div>
|
||||||
|
<span style={{
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: area.color,
|
||||||
|
minWidth: 80,
|
||||||
|
textAlign: 'right'
|
||||||
|
}}>
|
||||||
|
{weight} → {actualPercent}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span style={{
|
<input
|
||||||
fontSize: 18,
|
type="range"
|
||||||
fontWeight: 600,
|
min="0"
|
||||||
color: area.color,
|
max="10"
|
||||||
minWidth: 50,
|
step="1"
|
||||||
textAlign: 'right'
|
value={weight}
|
||||||
}}>
|
onChange={e => setFocusTemp(f => ({ ...f, [area.key]: parseInt(e.target.value) * 10 }))}
|
||||||
{focusTemp[area.key]}%
|
style={{
|
||||||
</span>
|
width: '100%',
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
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>
|
||||||
<input
|
)
|
||||||
type="range"
|
})}
|
||||||
min="0"
|
|
||||||
max="100"
|
|
||||||
step="5"
|
|
||||||
value={focusTemp[area.key]}
|
|
||||||
onChange={e => setFocusTemp(f => ({ ...f, [area.key]: parseInt(e.target.value) }))}
|
|
||||||
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%)`,
|
|
||||||
outline: 'none',
|
|
||||||
cursor: 'pointer'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sum Display */}
|
{/* Weight Total Display */}
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 12,
|
padding: 12,
|
||||||
background: (() => {
|
background: 'var(--surface2)',
|
||||||
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
|
border: '1px solid var(--border)',
|
||||||
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'
|
|
||||||
})(),
|
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
marginBottom: 16
|
marginBottom: 16
|
||||||
}}>
|
}}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span style={{
|
<span style={{ fontWeight: 600, color: 'var(--text2)' }}>
|
||||||
fontWeight: 600,
|
Gewichtung gesamt:
|
||||||
color: (() => {
|
|
||||||
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
|
|
||||||
if (sum === 100) return 'white'
|
|
||||||
return '#DC2626'
|
|
||||||
})()
|
|
||||||
}}>
|
|
||||||
Summe:
|
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
<span style={{ fontSize: 18, fontWeight: 600, color: 'var(--text1)' }}>
|
||||||
fontSize: 20,
|
{Object.values(focusTemp).reduce((a, b) => a + b, 0) / 10}
|
||||||
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{Object.values(focusTemp).reduce((a, b) => a + b, 0) !== 100 && (
|
<div style={{ fontSize: 12, marginTop: 4, color: 'var(--text3)' }}>
|
||||||
<div style={{ fontSize: 12, marginTop: 4, color: '#DC2626' }}>
|
💡 Die Prozentanteile werden automatisch berechnet
|
||||||
⚠️ Summe muss 100% ergeben
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons */}
|
{/* Action Buttons */}
|
||||||
|
|
@ -394,12 +375,27 @@ export default function GoalsPage() {
|
||||||
className="btn-primary"
|
className="btn-primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const sum = Object.values(focusTemp).reduce((a, b) => a + b, 0)
|
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
|
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 {
|
try {
|
||||||
await api.updateFocusAreas(focusTemp)
|
await api.updateFocusAreas(normalized)
|
||||||
showToast('✓ Fokus-Bereiche aktualisiert')
|
showToast('✓ Fokus-Bereiche aktualisiert')
|
||||||
await loadData()
|
await loadData()
|
||||||
setFocusEditing(false)
|
setFocusEditing(false)
|
||||||
|
|
@ -408,7 +404,6 @@ export default function GoalsPage() {
|
||||||
setError(err.message || 'Fehler beim Speichern')
|
setError(err.message || 'Fehler beim Speichern')
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={Object.values(focusTemp).reduce((a, b) => a + b, 0) !== 100}
|
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
>
|
>
|
||||||
Speichern
|
Speichern
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user