Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
113
frontend/src/components/history/ExerciseChart.tsx
Executable file
113
frontend/src/components/history/ExerciseChart.tsx
Executable file
@@ -0,0 +1,113 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
import { api } from '../../api/client';
|
||||
import { useExerciseStore } from '../../stores/exerciseStore';
|
||||
import type { SessionLog } from '../../types';
|
||||
|
||||
export function ExerciseChart() {
|
||||
const { exercises, fetchExercises } = useExerciseStore();
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [chartData, setChartData] = useState<{ date: string; weight: number }[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExercises();
|
||||
}, [fetchExercises]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedId) {
|
||||
setChartData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
api.exercises
|
||||
.history(selectedId, 50)
|
||||
.then((logs: SessionLog[]) => {
|
||||
// Gruppiere nach Datum, nehme max Gewicht pro Tag
|
||||
const byDate = new Map<string, number>();
|
||||
for (const log of logs) {
|
||||
const date = new Date(log.logged_at).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
});
|
||||
const current = byDate.get(date) || 0;
|
||||
if (log.weight_kg > current) {
|
||||
byDate.set(date, log.weight_kg);
|
||||
}
|
||||
}
|
||||
const data = Array.from(byDate.entries())
|
||||
.map(([date, weight]) => ({ date, weight }))
|
||||
.reverse();
|
||||
setChartData(data);
|
||||
})
|
||||
.catch(() => setChartData([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, [selectedId]);
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
|
||||
<h3 className="text-lg font-semibold text-gray-100 mb-3">Gewichtsverlauf</h3>
|
||||
|
||||
<select
|
||||
value={selectedId ?? ''}
|
||||
onChange={(e) =>
|
||||
setSelectedId(e.target.value ? parseInt(e.target.value) : null)
|
||||
}
|
||||
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-gray-100 focus:outline-none focus:border-blue-500 min-h-[44px] mb-4"
|
||||
>
|
||||
<option value="">Übung auswählen...</option>
|
||||
{exercises.map((ex) => (
|
||||
<option key={ex.id} value={ex.id}>
|
||||
{ex.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center text-gray-500 py-8">Laden...</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
{selectedId
|
||||
? 'Keine Daten vorhanden'
|
||||
: 'Wähle eine Übung aus'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="date" stroke="#9CA3AF" fontSize={12} />
|
||||
<YAxis stroke="#9CA3AF" fontSize={12} unit=" kg" />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1F2937',
|
||||
border: '1px solid #374151',
|
||||
borderRadius: '8px',
|
||||
color: '#F3F4F6',
|
||||
}}
|
||||
formatter={(value) => [`${value} kg`, 'Max. Gewicht']}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="weight"
|
||||
stroke="#3B82F6"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#3B82F6', r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user