Check exercise name uniqueness when creating

This commit is contained in:
AG
2025-11-26 22:26:45 +02:00
parent 53d11c6665
commit 94f0a9a17a
4 changed files with 35 additions and 9 deletions

View File

@@ -9,12 +9,14 @@ interface ExerciseModalProps {
onClose: () => void; onClose: () => void;
onSave: (exercise: ExerciseDef) => Promise<void> | void; onSave: (exercise: ExerciseDef) => Promise<void> | void;
lang: Language; lang: Language;
existingExercises?: ExerciseDef[];
} }
const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave, lang }) => { const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave, lang, existingExercises = [] }) => {
const [newName, setNewName] = useState(''); const [newName, setNewName] = useState('');
const [newType, setNewType] = useState<ExerciseType>(ExerciseType.STRENGTH); const [newType, setNewType] = useState<ExerciseType>(ExerciseType.STRENGTH);
const [newBwPercentage, setNewBwPercentage] = useState<string>('100'); const [newBwPercentage, setNewBwPercentage] = useState<string>('100');
const [error, setError] = useState<string>('');
const exerciseTypeLabels: Record<ExerciseType, string> = { const exerciseTypeLabels: Record<ExerciseType, string> = {
[ExerciseType.STRENGTH]: t('type_strength', lang), [ExerciseType.STRENGTH]: t('type_strength', lang),
@@ -28,9 +30,21 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
const handleCreateExercise = async () => { const handleCreateExercise = async () => {
if (!newName.trim()) return; if (!newName.trim()) return;
// Check for duplicate name (case-insensitive)
const trimmedName = newName.trim();
const isDuplicate = existingExercises.some(
ex => ex.name.toLowerCase() === trimmedName.toLowerCase()
);
if (isDuplicate) {
setError(t('exercise_name_exists', lang) || 'An exercise with this name already exists');
return;
}
const newEx: ExerciseDef = { const newEx: ExerciseDef = {
id: crypto.randomUUID(), id: crypto.randomUUID(),
name: newName.trim(), name: trimmedName,
type: newType, type: newType,
...(newType === ExerciseType.BODYWEIGHT && { bodyWeightPercentage: parseFloat(newBwPercentage) || 100 }) ...(newType === ExerciseType.BODYWEIGHT && { bodyWeightPercentage: parseFloat(newBwPercentage) || 100 })
}; };
@@ -38,6 +52,7 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
setNewName(''); setNewName('');
setNewType(ExerciseType.STRENGTH); setNewType(ExerciseType.STRENGTH);
setNewBwPercentage('100'); setNewBwPercentage('100');
setError('');
onClose(); onClose();
}; };
@@ -52,13 +67,21 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<FilledInput <div>
label={t('ex_name', lang)} <FilledInput
value={newName} label={t('ex_name', lang)}
onChange={(e: any) => setNewName(e.target.value)} value={newName}
type="text" onChange={(e: any) => {
autoFocus setNewName(e.target.value);
/> setError(''); // Clear error when user types
}}
type="text"
autoFocus
/>
{error && (
<p className="text-xs text-error mt-2 ml-3">{error}</p>
)}
</div>
<div> <div>
<label className="block text-xs text-on-surface-variant font-medium mb-3">{t('ex_type', lang)}</label> <label className="block text-xs text-on-surface-variant font-medium mb-3">{t('ex_type', lang)}</label>

View File

@@ -514,6 +514,7 @@ const Profile: React.FC<ProfileProps> = ({ user, onLogout, lang, onLanguageChang
onClose={() => setIsCreatingEx(false)} onClose={() => setIsCreatingEx(false)}
onSave={handleCreateExercise} onSave={handleCreateExercise}
lang={lang} lang={lang}
existingExercises={exercises}
/> />
)} )}

Binary file not shown.

View File

@@ -155,6 +155,7 @@ const translations = {
show_archived: 'Show Archived', show_archived: 'Show Archived',
filter_by_name: 'Filter by name', filter_by_name: 'Filter by name',
type_to_filter: 'Type to filter...', type_to_filter: 'Type to filter...',
exercise_name_exists: 'An exercise with this name already exists',
profile_saved: 'Profile saved successfully', profile_saved: 'Profile saved successfully',
}, },
ru: { ru: {
@@ -304,6 +305,7 @@ const translations = {
show_archived: 'Показать архивные', show_archived: 'Показать архивные',
filter_by_name: 'Фильтр по названию', filter_by_name: 'Фильтр по названию',
type_to_filter: 'Введите для фильтрации...', type_to_filter: 'Введите для фильтрации...',
exercise_name_exists: 'Упражнение с таким названием уже существует',
profile_saved: 'Профиль успешно сохранен', profile_saved: 'Профиль успешно сохранен',
} }
}; };