- Exercise number (UF#): optional field on exercises, displayed in cards, training, and sets - Import training plan numbers via migration 005 (UPDATE by name) - Exercise images: JPG upload with multi-image support per exercise (migration 006) - Version endpoint (GET /api/v1/version) with ldflags injection in Makefile and Dockerfile - Version displayed on settings page - Session resume: GET /api/v1/sessions/active endpoint, auto-resume on training page load - Block new session while one is active (409 Conflict) - e1RM sparkline chart per exercise during training (Epley formula) - Fix CORS: add X-User-ID to allowed headers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
60 lines
2.6 KiB
TypeScript
Executable File
60 lines
2.6 KiB
TypeScript
Executable File
import type { Exercise } from '../../types';
|
|
import { MUSCLE_GROUP_LABELS, MUSCLE_GROUP_COLORS } from '../../types';
|
|
|
|
interface ExerciseCardProps {
|
|
exercise: Exercise;
|
|
onEdit: (exercise: Exercise) => void;
|
|
onDelete: (exercise: Exercise) => void;
|
|
}
|
|
|
|
export function ExerciseCard({ exercise, onEdit, onDelete }: ExerciseCardProps) {
|
|
const label = MUSCLE_GROUP_LABELS[exercise.muscle_group] || exercise.muscle_group;
|
|
const color = MUSCLE_GROUP_COLORS[exercise.muscle_group] || 'bg-gray-600';
|
|
|
|
return (
|
|
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="font-semibold text-gray-100 truncate">
|
|
{exercise.exercise_number != null && (
|
|
<span className="text-blue-400 mr-1.5">#{exercise.exercise_number}</span>
|
|
)}
|
|
{exercise.name}
|
|
</h3>
|
|
{exercise.description && (
|
|
<p className="text-sm text-gray-400 mt-1">{exercise.description}</p>
|
|
)}
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<span className={`${color} text-white text-xs px-2 py-1 rounded-full`}>
|
|
{label}
|
|
</span>
|
|
<span className="text-xs text-gray-500">
|
|
Schritt: {exercise.weight_step_kg} kg
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-1">
|
|
<button
|
|
onClick={() => onEdit(exercise)}
|
|
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-gray-400 hover:text-blue-400 rounded-lg hover:bg-gray-800"
|
|
title="Bearbeiten"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => onDelete(exercise)}
|
|
className="min-h-[44px] min-w-[44px] flex items-center justify-center text-gray-400 hover:text-red-400 rounded-lg hover:bg-gray-800"
|
|
title="Löschen"
|
|
>
|
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|