Initial commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
193
frontend/src/components/sets/SetForm.tsx
Executable file
193
frontend/src/components/sets/SetForm.tsx
Executable file
@@ -0,0 +1,193 @@
|
||||
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 {
|
||||
trainingSet?: TrainingSet | null;
|
||||
onSubmit: (name: string, exerciseIds: number[]) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
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(() => {
|
||||
// Lade alle Übungen ohne Filter
|
||||
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;
|
||||
|
||||
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>
|
||||
|
||||
{/* Übungsauswahl */}
|
||||
<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>
|
||||
|
||||
{/* Sortierbare ausgewählte Übungen */}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user