New exercise name autofill. Log Set button animation.

This commit is contained in:
AG
2025-12-12 21:48:46 +02:00
parent c7f5c4048c
commit f169c7c4d3
8 changed files with 67 additions and 19 deletions

View File

@@ -15,15 +15,23 @@ interface ExerciseModalProps {
onSave: (exercise: ExerciseDef) => Promise<void> | void; onSave: (exercise: ExerciseDef) => Promise<void> | void;
lang: Language; lang: Language;
existingExercises?: ExerciseDef[]; existingExercises?: ExerciseDef[];
initialName?: string;
} }
const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave, lang, existingExercises = [] }) => { const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave, lang, existingExercises = [], initialName = '' }) => {
const [newName, setNewName] = useState(''); const [newName, setNewName] = useState(initialName);
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 [isUnilateral, setIsUnilateral] = useState(false); const [isUnilateral, setIsUnilateral] = useState(false);
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
// Update newName when modal opens or initialName changes
React.useEffect(() => {
if (isOpen) {
setNewName(initialName);
}
}, [isOpen, initialName]);
const exerciseTypeLabels: Record<ExerciseType, string> = { const exerciseTypeLabels: Record<ExerciseType, string> = {
[ExerciseType.STRENGTH]: t('type_strength', lang), [ExerciseType.STRENGTH]: t('type_strength', lang),
[ExerciseType.BODYWEIGHT]: t('type_bodyweight', lang), [ExerciseType.BODYWEIGHT]: t('type_bodyweight', lang),
@@ -69,12 +77,12 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
isOpen={isOpen} isOpen={isOpen}
onClose={() => { onClose={() => {
console.log('ExerciseModal onClose'); console.log('ExerciseModal onClose');
setNewName(''); // Reset on close if desired
onClose(); onClose();
}} }}
title={t('create_exercise', lang)} title={t('create_exercise', lang)}
width="md" width="md"
> >
{console.log('ExerciseModal Rendering. isOpen:', isOpen)}
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<FilledInput <FilledInput

View File

@@ -75,18 +75,21 @@ const FilledInput: React.FC<FilledInputProps> = ({
<button <button
type="button" type="button"
onClick={handleClear} onClick={handleClear}
className={`absolute top-1/2 -translate-y-1/2 p-2 text-on-surface-variant hover:text-on-surface rounded-full opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity ${rightElement ? 'right-12' : 'right-2'}`} aria-label="Clear input"
className={`absolute top-1/2 -translate-y-1/2 p-2 text-on-surface-variant hover:text-on-surface rounded-full transition-opacity ${rightElement ? 'right-12' : 'right-2'}`}
tabIndex={-1} tabIndex={-1}
> >
<X size={16} /> <X size={16} />
</button> </button>
)} )}
{rightElement && ( {
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10"> rightElement && (
{rightElement} <div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
</div> {rightElement}
)} </div>
</div> )
}
</div >
); );
}; };

View File

@@ -50,7 +50,7 @@ interface SortablePlanStepProps {
lang: Language; lang: Language;
} }
const SortablePlanStep = ({ step, index, toggleWeighted, updateRestTime, removeStep, lang }: SortablePlanStepProps) => { const SortablePlanStep: React.FC<SortablePlanStepProps> = ({ step, index, toggleWeighted, updateRestTime, removeStep, lang }) => {
const { const {
attributes, attributes,
listeners, listeners,

View File

@@ -299,6 +299,7 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
onSave={handleCreateExercise} onSave={handleCreateExercise}
lang={lang} lang={lang}
existingExercises={exercises} existingExercises={exercises}
initialName={tracker.searchQuery}
/> />
)} )}

View File

@@ -8,7 +8,7 @@ import { useTracker } from './useTracker';
interface SetLoggerProps { interface SetLoggerProps {
tracker: ReturnType<typeof useTracker>; tracker: ReturnType<typeof useTracker>;
lang: Language; lang: Language;
onLogSet: () => void; onLogSet: () => Promise<void> | void;
isSporadic?: boolean; isSporadic?: boolean;
} }
@@ -39,7 +39,31 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
setUnilateralSide setUnilateralSide
} = tracker; } = tracker;
const [localSuccess, setLocalSuccess] = React.useState(false);
React.useEffect(() => {
if (localSuccess) {
const timer = setTimeout(() => {
setLocalSuccess(false);
}, 750);
return () => clearTimeout(timer);
}
}, [localSuccess]);
const handleLogClick = async () => {
try {
const result = onLogSet();
if (result instanceof Promise) {
await result;
}
setLocalSuccess(true);
} catch (error) {
console.error("Failed to log set:", error);
}
};
const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length; const isPlanFinished = activePlan && currentStepIndex >= activePlan.steps.length;
const showSuccess = (isSporadic && sporadicSuccess) || localSuccess;
return ( return (
<div className="space-y-6"> <div className="space-y-6">
@@ -174,14 +198,14 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
</div> </div>
<button <button
onClick={onLogSet} onClick={handleLogClick}
className={`w-full h-14 font-medium text-lg rounded-full shadow-elevation-2 hover:shadow-elevation-3 active:scale-[0.98] transition-all flex items-center justify-center gap-2 ${isSporadic && sporadicSuccess className={`w-full h-14 font-medium text-lg rounded-full shadow-elevation-2 hover:shadow-elevation-3 active:scale-[0.98] transition-all flex items-center justify-center gap-2 ${showSuccess
? 'bg-green-500 text-white' ? 'bg-green-500 text-white'
: 'bg-primary-container text-on-primary-container' : 'bg-primary-container text-on-primary-container'
}`} }`}
> >
{isSporadic && sporadicSuccess ? <CheckCircle size={24} /> : <Plus size={24} />} {showSuccess ? <CheckCircle size={24} className="animate-in zoom-in spin-in-90 duration-300" /> : <Plus size={24} />}
<span>{isSporadic && sporadicSuccess ? t('saved', lang) : t('log_set', lang)}</span> <span>{showSuccess ? t('saved', lang) : t('log_set', lang)}</span>
</button> </button>
</div> </div>
)} )}

View File

@@ -153,6 +153,7 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
onSave={handleCreateExercise} onSave={handleCreateExercise}
lang={lang} lang={lang}
existingExercises={exercises} existingExercises={exercises}
initialName={tracker.searchQuery}
/> />
)} )}

View File

@@ -414,6 +414,10 @@ test.describe('V. User & System Management', () => {
await expect(blockButton).toBeVisible(); await expect(blockButton).toBeVisible();
await blockButton.click(); await blockButton.click();
// Handle Block Confirmation Modal
await expect(page.getByText('Block User?').or(page.getByText('Заблокировать?'))).toBeVisible();
await page.getByRole('button', { name: /Confirm|Подтвердить/i }).click();
await expect(userRow.getByText(/Blocked|Block/i)).toBeVisible(); await expect(userRow.getByText(/Blocked|Block/i)).toBeVisible();
// 5. Verify Blocked User Cannot Login // 5. Verify Blocked User Cannot Login
@@ -458,6 +462,10 @@ test.describe('V. User & System Management', () => {
const userRowAfter = listContainer.locator('.bg-surface-container-high').filter({ hasText: regularUser.email }).first(); const userRowAfter = listContainer.locator('.bg-surface-container-high').filter({ hasText: regularUser.email }).first();
await expect(userRowAfter).toBeVisible(); await expect(userRowAfter).toBeVisible();
await userRowAfter.getByRole('button', { name: 'Unblock', exact: true }).click(); await userRowAfter.getByRole('button', { name: 'Unblock', exact: true }).click();
// Handle Unblock Modal
await expect(page.getByText('Unblock User?').or(page.getByText('Разблокировать?'))).toBeVisible();
await page.getByRole('button', { name: /Confirm|Подтвердить/i }).click();
// Wait for UI to update (block icon/text should disappear or change style) // Wait for UI to update (block icon/text should disappear or change style)
// Ideally we check API response or UI change. Assuming "Blocked" text goes away or button changes. // Ideally we check API response or UI change. Assuming "Blocked" text goes away or button changes.
// The original code checked for not.toBeVisible of blocked text, let's stick to that or button state // The original code checked for not.toBeVisible of blocked text, let's stick to that or button state
@@ -594,9 +602,12 @@ test.describe('V. User & System Management', () => {
const userRow = listContainer.locator('.bg-surface-container-high').filter({ hasText: userToDelete.email }).first(); const userRow = listContainer.locator('.bg-surface-container-high').filter({ hasText: userToDelete.email }).first();
await expect(userRow).toBeVisible(); await expect(userRow).toBeVisible();
page.once('dialog', dialog => dialog.accept());
await userRow.getByRole('button', { name: /Delete/i }).click(); await userRow.getByRole('button', { name: /Delete/i }).click();
// Handle Delete Confirmation Modal
await expect(page.getByText('Delete User?').or(page.getByText('Удалить пользователя?'))).toBeVisible();
await page.getByRole('button', { name: /Confirm|Подтвердить/i }).click();
await expect(page.getByText(userToDelete.email)).not.toBeVisible(); await expect(page.getByText(userToDelete.email)).not.toBeVisible();
}); });
}); });

View File

@@ -400,7 +400,7 @@ test.describe('III. Workout Tracking', () => {
// Helper to log a set // Helper to log a set
const logSet = async (side: 'L' | 'R' | 'A') => { const logSet = async (side: 'L' | 'R' | 'A') => {
// Find the logger container (has 'Log Set' button) // Find the logger container (has 'Log Set' button)
const logger = page.locator('div').filter({ has: page.getByRole('button', { name: /Log Set/i }) }).last(); const logger = page.locator('div').filter({ has: page.getByRole('button', { name: /Log Set|Saved/i }) }).last();
await expect(logger).toBeVisible(); await expect(logger).toBeVisible();
// Select side // Select side
@@ -416,7 +416,7 @@ test.describe('III. Workout Tracking', () => {
// Reps - handle potential multiples if strict, but scoped should be unique // Reps - handle potential multiples if strict, but scoped should be unique
await logger.getByLabel('Reps').fill('10'); await logger.getByLabel('Reps').fill('10');
await logger.getByRole('button', { name: /Log Set/i }).click(); await logger.getByRole('button', { name: /Log Set|Saved/i }).click();
}; };
// Log Left (L) // Log Left (L)