New exercise name autofill. Log Set button animation.
This commit is contained in:
@@ -15,15 +15,23 @@ interface ExerciseModalProps {
|
||||
onSave: (exercise: ExerciseDef) => Promise<void> | void;
|
||||
lang: Language;
|
||||
existingExercises?: ExerciseDef[];
|
||||
initialName?: string;
|
||||
}
|
||||
|
||||
const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave, lang, existingExercises = [] }) => {
|
||||
const [newName, setNewName] = useState('');
|
||||
const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave, lang, existingExercises = [], initialName = '' }) => {
|
||||
const [newName, setNewName] = useState(initialName);
|
||||
const [newType, setNewType] = useState<ExerciseType>(ExerciseType.STRENGTH);
|
||||
const [newBwPercentage, setNewBwPercentage] = useState<string>('100');
|
||||
const [isUnilateral, setIsUnilateral] = useState(false);
|
||||
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> = {
|
||||
[ExerciseType.STRENGTH]: t('type_strength', lang),
|
||||
[ExerciseType.BODYWEIGHT]: t('type_bodyweight', lang),
|
||||
@@ -69,12 +77,12 @@ const ExerciseModal: React.FC<ExerciseModalProps> = ({ isOpen, onClose, onSave,
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
console.log('ExerciseModal onClose');
|
||||
setNewName(''); // Reset on close if desired
|
||||
onClose();
|
||||
}}
|
||||
title={t('create_exercise', lang)}
|
||||
width="md"
|
||||
>
|
||||
{console.log('ExerciseModal Rendering. isOpen:', isOpen)}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<FilledInput
|
||||
|
||||
@@ -75,17 +75,20 @@ const FilledInput: React.FC<FilledInputProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
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}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
)}
|
||||
{rightElement && (
|
||||
{
|
||||
rightElement && (
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 z-10">
|
||||
{rightElement}
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ interface SortablePlanStepProps {
|
||||
lang: Language;
|
||||
}
|
||||
|
||||
const SortablePlanStep = ({ step, index, toggleWeighted, updateRestTime, removeStep, lang }: SortablePlanStepProps) => {
|
||||
const SortablePlanStep: React.FC<SortablePlanStepProps> = ({ step, index, toggleWeighted, updateRestTime, removeStep, lang }) => {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
|
||||
@@ -299,6 +299,7 @@ const ActiveSessionView: React.FC<ActiveSessionViewProps> = ({ tracker, activeSe
|
||||
onSave={handleCreateExercise}
|
||||
lang={lang}
|
||||
existingExercises={exercises}
|
||||
initialName={tracker.searchQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useTracker } from './useTracker';
|
||||
interface SetLoggerProps {
|
||||
tracker: ReturnType<typeof useTracker>;
|
||||
lang: Language;
|
||||
onLogSet: () => void;
|
||||
onLogSet: () => Promise<void> | void;
|
||||
isSporadic?: boolean;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,31 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
|
||||
setUnilateralSide
|
||||
} = 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 showSuccess = (isSporadic && sporadicSuccess) || localSuccess;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -174,14 +198,14 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onLogSet}
|
||||
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
|
||||
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 ${showSuccess
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-primary-container text-on-primary-container'
|
||||
}`}
|
||||
>
|
||||
{isSporadic && sporadicSuccess ? <CheckCircle size={24} /> : <Plus size={24} />}
|
||||
<span>{isSporadic && sporadicSuccess ? t('saved', lang) : t('log_set', lang)}</span>
|
||||
{showSuccess ? <CheckCircle size={24} className="animate-in zoom-in spin-in-90 duration-300" /> : <Plus size={24} />}
|
||||
<span>{showSuccess ? t('saved', lang) : t('log_set', lang)}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -153,6 +153,7 @@ const SporadicView: React.FC<SporadicViewProps> = ({ tracker, lang }) => {
|
||||
onSave={handleCreateExercise}
|
||||
lang={lang}
|
||||
existingExercises={exercises}
|
||||
initialName={tracker.searchQuery}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -414,6 +414,10 @@ test.describe('V. User & System Management', () => {
|
||||
await expect(blockButton).toBeVisible();
|
||||
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();
|
||||
|
||||
// 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();
|
||||
await expect(userRowAfter).toBeVisible();
|
||||
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)
|
||||
// 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
|
||||
@@ -594,9 +602,12 @@ test.describe('V. User & System Management', () => {
|
||||
const userRow = listContainer.locator('.bg-surface-container-high').filter({ hasText: userToDelete.email }).first();
|
||||
await expect(userRow).toBeVisible();
|
||||
|
||||
page.once('dialog', dialog => dialog.accept());
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -400,7 +400,7 @@ test.describe('III. Workout Tracking', () => {
|
||||
// Helper to log a set
|
||||
const logSet = async (side: 'L' | 'R' | 'A') => {
|
||||
// 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();
|
||||
|
||||
// Select side
|
||||
@@ -416,7 +416,7 @@ test.describe('III. Workout Tracking', () => {
|
||||
// Reps - handle potential multiples if strict, but scoped should be unique
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user