Files
krafttrainer/frontend/src/components/sets/SetForm.tsx
Christoph K. 4db170b467 init
2026-04-07 09:49:17 +02:00

199 lines
7.3 KiB
TypeScript
Executable File

import { useState, useEffect } from 'react';
import { useExerciseStore } from '../../stores/exerciseStore';
import type { TrainingSet, MuscleGroup } from '../../types';
import { MUSCLE_GROUPS, MUSCLE_GROUP_LABELS } from '../../types';
interface SetFormProps {
/** Vorhandenes Set beim Bearbeiten; `null`/`undefined` beim Erstellen. */
trainingSet?: TrainingSet | null;
onSubmit: (name: string, exerciseIds: number[]) => void;
onCancel: () => void;
}
/**
* Formular zum Erstellen und Bearbeiten eines Trainings-Sets.
* Übungen können nach Muskelgruppe gefiltert und per Checkbox ausgewählt werden.
* Die Reihenfolge der ausgewählten Übungen ist per Pfeil-Buttons sortierbar.
*/
export function SetForm({ trainingSet, onSubmit, onCancel }: SetFormProps) {
const { exercises, fetchExercises } = useExerciseStore();
const [name, setName] = useState('');
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [filterMg, setFilterMg] = useState<MuscleGroup | ''>('');
useEffect(() => {
// Alle Übungen ohne aktiven Filter laden
useExerciseStore.getState().setFilter({ muscleGroup: '', query: '' });
fetchExercises();
}, [fetchExercises]);
useEffect(() => {
if (trainingSet) {
setName(trainingSet.name);
setSelectedIds(trainingSet.exercises?.map((e) => e.id) || []);
} else {
setName('');
setSelectedIds([]);
}
}, [trainingSet]);
const toggleExercise = (id: number) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
);
};
const moveUp = (index: number) => {
if (index === 0) return;
setSelectedIds((prev) => {
const next = [...prev];
[next[index - 1], next[index]] = [next[index], next[index - 1]];
return next;
});
};
const moveDown = (index: number) => {
if (index >= selectedIds.length - 1) return;
setSelectedIds((prev) => {
const next = [...prev];
[next[index], next[index + 1]] = [next[index + 1], next[index]];
return next;
});
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSubmit(name.trim(), selectedIds);
};
const filteredExercises = filterMg
? exercises.filter((e) => e.muscle_group === filterMg)
: exercises;
const isValid = name.trim().length > 0 && selectedIds.length > 0;
/** Map für effizienten Zugriff auf Übungsnamen in der Reihenfolge-Ansicht. */
const exerciseMap = new Map(exercises.map((e) => [e.id, e]));
return (
<form onSubmit={handleSubmit} className="bg-gray-900 border border-gray-800 rounded-xl p-4 space-y-4">
<h3 className="text-lg font-semibold">
{trainingSet ? 'Set bearbeiten' : 'Neues Set'}
</h3>
<div>
<label className="block text-sm text-gray-400 mb-1">Name *</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
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]"
placeholder="Set-Name"
maxLength={100}
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Übungen auswählen *</label>
<select
value={filterMg}
onChange={(e) => setFilterMg(e.target.value as MuscleGroup | '')}
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-2"
>
<option value="">Alle Muskelgruppen</option>
{MUSCLE_GROUPS.map((mg) => (
<option key={mg.value} value={mg.value}>
{mg.label}
</option>
))}
</select>
<div className="max-h-48 overflow-y-auto space-y-1 border border-gray-700 rounded-lg p-2">
{filteredExercises.length === 0 ? (
<p className="text-sm text-gray-500 py-2 text-center">Keine Übungen gefunden</p>
) : (
filteredExercises.map((ex) => (
<label
key={ex.id}
className="flex items-center gap-2 px-2 py-2 rounded hover:bg-gray-800 cursor-pointer min-h-[44px]"
>
<input
type="checkbox"
checked={selectedIds.includes(ex.id)}
onChange={() => toggleExercise(ex.id)}
className="w-5 h-5 accent-blue-500"
/>
<span className="flex-1 text-gray-200">{ex.name}</span>
<span className="text-xs text-gray-500">
{MUSCLE_GROUP_LABELS[ex.muscle_group]}
</span>
</label>
))
)}
</div>
</div>
{selectedIds.length > 0 && (
<div>
<label className="block text-sm text-gray-400 mb-1">
Reihenfolge ({selectedIds.length} ausgewählt)
</label>
<div className="space-y-1 border border-gray-700 rounded-lg p-2">
{selectedIds.map((id, index) => {
const ex = exerciseMap.get(id);
return (
<div
key={id}
className="flex items-center gap-2 bg-gray-800 rounded-lg px-3 py-2"
>
<span className="text-gray-500 text-sm w-6">{index + 1}.</span>
<span className="flex-1 text-gray-200 text-sm">
{ex?.name || `Übung #${id}`}
</span>
<button
type="button"
onClick={() => moveUp(index)}
disabled={index === 0}
className="min-h-[36px] min-w-[36px] flex items-center justify-center text-gray-400 hover:text-gray-200 disabled:opacity-30"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
</svg>
</button>
<button
type="button"
onClick={() => moveDown(index)}
disabled={index === selectedIds.length - 1}
className="min-h-[36px] min-w-[36px] flex items-center justify-center text-gray-400 hover:text-gray-200 disabled:opacity-30"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
);
})}
</div>
</div>
)}
<div className="flex gap-3 justify-end pt-2">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-gray-200 min-h-[44px]"
>
Abbrechen
</button>
<button
type="submit"
disabled={!isValid}
className="px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white min-h-[44px] disabled:opacity-50 disabled:cursor-not-allowed"
>
{trainingSet ? 'Speichern' : 'Erstellen'}
</button>
</div>
</form>
);
}