Initial commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-03-21 15:03:55 +01:00
commit dfd66e43c6
78 changed files with 6219 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import type { TrainingSet } from '../../types';
import { MUSCLE_GROUP_LABELS, MUSCLE_GROUP_COLORS } from '../../types';
interface SetDetailProps {
trainingSet: TrainingSet;
}
export function SetDetail({ trainingSet }: SetDetailProps) {
return (
<div className="bg-gray-900 border border-gray-800 rounded-xl p-4">
<h3 className="font-semibold text-gray-100 text-lg mb-3">{trainingSet.name}</h3>
{(!trainingSet.exercises || trainingSet.exercises.length === 0) ? (
<p className="text-gray-500 text-sm">Keine Übungen in diesem Set.</p>
) : (
<div className="space-y-2">
{trainingSet.exercises.map((ex, index) => {
const label = MUSCLE_GROUP_LABELS[ex.muscle_group] || ex.muscle_group;
const color = MUSCLE_GROUP_COLORS[ex.muscle_group] || 'bg-gray-600';
return (
<div key={ex.id} className="flex items-center gap-3 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">{ex.name}</span>
<span className={`${color} text-white text-xs px-2 py-1 rounded-full`}>
{label}
</span>
</div>
);
})}
</div>
)}
</div>
);
}

View 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>
);
}

View File

@@ -0,0 +1,81 @@
import { useEffect } from 'react';
import { useSetStore } from '../../stores/setStore';
import type { TrainingSet } from '../../types';
interface SetListProps {
onEdit: (set: TrainingSet) => void;
onDelete: (set: TrainingSet) => void;
}
export function SetList({ onEdit, onDelete }: SetListProps) {
const { sets, loading, fetchSets } = useSetStore();
useEffect(() => {
fetchSets();
}, [fetchSets]);
if (loading) {
return <div className="text-center text-gray-500 py-8">Laden...</div>;
}
if (sets.length === 0) {
return (
<div className="text-center text-gray-500 py-12">
<p className="text-lg">Noch keine Sets angelegt</p>
<p className="text-sm mt-1">Erstelle dein erstes Set mit dem Button oben.</p>
</div>
);
}
return (
<div className="grid gap-3">
{sets.map((set) => (
<div
key={set.id}
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">{set.name}</h3>
<p className="text-sm text-gray-400 mt-1">
{set.exercises?.length || 0} Übungen
</p>
{set.exercises && set.exercises.length > 0 && (
<div className="flex flex-wrap gap-1 mt-2">
{set.exercises.map((ex) => (
<span
key={ex.id}
className="text-xs bg-gray-800 text-gray-300 px-2 py-1 rounded"
>
{ex.name}
</span>
))}
</div>
)}
</div>
<div className="flex gap-1">
<button
onClick={() => onEdit(set)}
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(set)}
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>
))}
</div>
);
}