199 lines
7.3 KiB
TypeScript
Executable File
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>
|
|
);
|
|
}
|