Alternating option for Unilateral exercises

This commit is contained in:
AG
2025-12-10 18:40:54 +02:00
parent 95a5e37748
commit 9243fec947
10 changed files with 59 additions and 8 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -542,9 +542,10 @@ Comprehensive test plan for the GymFlow web application, covering authentication
4. Select 'Left' from the Side selector.
5. Click 'Log Set'.
6. Repeat for 'Right' side.
7. Repeat for 'Alternately' side.
**Expected Results:**
- Sets are logged with the correct 'Left'/'Right' indicators visible in the history.
- Sets are logged with the correct 'Left'/'Right'/'Alternately' indicators visible in the history.
#### 3.15. C. Active Session - Log Special Type Set

View File

@@ -66,7 +66,7 @@ Users can structure their training via Plans.
* `PLYOMETRIC`: Requires **Reps**.
* **3.3.2 Custom Exercises**
* User can create new exercises.
* **Unilateral Flag**: Boolean flag `isUnilateral`. If true, sets recorded for this exercise can specify a `side` (LEFT/RIGHT).
* **Unilateral Flag**: Boolean flag `isUnilateral`. If true, sets recorded for this exercise can specify a `side` (LEFT/RIGHT/ALTERNATELY).
* **Bodyweight %**: For bodyweight-based calculations.
### 3.4. Workout Tracking (The "Tracker")

View File

@@ -76,7 +76,7 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
}
};
const handleUpdateSet = (setId: string, field: keyof WorkoutSet, value: number) => {
const handleUpdateSet = (setId: string, field: keyof WorkoutSet, value: number | string) => {
if (!editingSession) return;
const updatedSets = editingSession.sets.map(s =>
s.id === setId ? { ...s, [field]: value } : s
@@ -454,6 +454,26 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</div>
)}
</div>
{/* Side Selector - Full width on mobile, 1 col on desktop if space */}
{set.side && (
<div className="bg-surface-container-high rounded px-2 py-1 col-span-2 sm:col-span-1 border border-outline-variant/30">
<label className="text-[10px] text-on-surface-variant font-bold block mb-1">{t('unilateral', lang)}</label>
<div className="flex bg-surface-container-low rounded p-0.5">
{(['LEFT', 'RIGHT', 'ALTERNATELY'] as const).map((sideOption) => (
<button
key={sideOption}
onClick={() => handleUpdateSet(set.id, 'side', sideOption)}
className={`flex-1 text-[10px] py-1 rounded transition-colors ${set.side === sideOption
? 'bg-primary/10 text-primary font-bold'
: 'text-on-surface-variant hover:bg-surface-container'
}`}
>
{t(sideOption.toLowerCase() as any, lang).slice(0, 3)}
</button>
))}
</div>
</div>
)}
</div>
))}
</div>
@@ -466,8 +486,9 @@ const History: React.FC<HistoryProps> = ({ lang }) => {
</div>
</div>
</Modal>
)}
</div>
)
}
</div >
);
};

View File

@@ -106,6 +106,13 @@ const SetLogger: React.FC<SetLoggerProps> = ({ tracker, lang, onLogSet, isSporad
>
{t('left', lang)}
</button>
<button
onClick={() => setUnilateralSide('ALTERNATELY')}
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'ALTERNATELY' ? 'bg-tertiary-container text-on-tertiary-container' : 'text-on-surface-variant hover:bg-surface-container-high'
}`}
>
{t('alternately', lang) || 'Alternately'}
</button>
<button
onClick={() => setUnilateralSide('RIGHT')}
className={`w-full text-center px-4 py-2 rounded-full text-sm font-medium transition-colors ${unilateralSide === 'RIGHT' ? 'bg-secondary-container text-on-secondary-container' : 'text-on-surface-variant hover:bg-surface-container-high'

View File

@@ -17,7 +17,7 @@ export const useWorkoutForm = ({ userId, onSetAdded, onUpdateSet }: UseWorkoutFo
const [bwPercentage, setBwPercentage] = useState<string>('100');
// Unilateral State
const [unilateralSide, setUnilateralSide] = useState<'LEFT' | 'RIGHT'>('LEFT');
const [unilateralSide, setUnilateralSide] = useState<'LEFT' | 'RIGHT' | 'ALTERNATELY'>('ALTERNATELY');
// Editing State
const [editingSetId, setEditingSetId] = useState<string | null>(null);

View File

@@ -176,6 +176,7 @@ const translations = {
same_values_both_sides: 'Same values for both sides',
left: 'Left',
right: 'Right',
alternately: 'Alternately',
},
ru: {
// Tabs
@@ -344,6 +345,7 @@ const translations = {
same_values_both_sides: 'Одинаковые значения для обеих сторон',
left: 'Левая',
right: 'Правая',
alternately: 'Попеременно',
},
};

View File

@@ -21,7 +21,7 @@ export interface WorkoutSet {
height?: number;
bodyWeightPercentage?: number; // Percentage of bodyweight used (e.g. 65 for pushups)
timestamp: number;
side?: 'LEFT' | 'RIGHT'; // For unilateral exercises
side?: 'LEFT' | 'RIGHT' | 'ALTERNATELY'; // For unilateral exercises
completed: boolean;
}

View File

@@ -404,6 +404,26 @@ test.describe('III. Workout Tracking', () => {
// Verify Side and Metrics
await expect(page.getByText('Left', { exact: true })).toBeVisible();
await expect(page.getByText('20 kg x 10 reps')).toBeVisible();
// Log Right
await page.getByText('Right').first().click();
await page.getByLabel('Weight (kg)').fill('20');
await page.getByLabel('Reps').first().fill('10');
await page.getByRole('button', { name: /Log Set/i }).click();
await expect(page.getByText('Right', { exact: true })).toBeVisible();
// Log Alternately
if (await page.getByText('Alternately').count() > 0) {
await page.getByText('Alternately').first().click();
} else {
// Fallback for i18n or exact text match if needed
await page.getByRole('button', { name: /Alternately|Alt/i }).click();
}
await page.getByLabel('Weight (kg)').fill('20');
await page.getByLabel('Reps').first().fill('10');
await page.getByRole('button', { name: /Log Set/i }).click();
await expect(page.getByText(/Alternately|Alt/i).last()).toBeVisible();
});
test('3.15 C. Active Session - Log Special Type Set', async ({ page, createUniqueUser, request }) => {