Plan state is persistent during session
This commit is contained in:
32
App.tsx
32
App.tsx
@@ -9,7 +9,7 @@ import Plans from './components/Plans';
|
|||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
import Profile from './components/Profile';
|
import Profile from './components/Profile';
|
||||||
import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from './types';
|
import { TabView, WorkoutSession, WorkoutSet, WorkoutPlan, User, Language } from './types';
|
||||||
import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession } from './services/storage';
|
import { getSessions, saveSession, deleteSession, getPlans, getActiveSession, updateActiveSession, deleteActiveSession, updateSetInActiveSession, deleteSetFromActiveSession } from './services/storage';
|
||||||
import { getCurrentUserProfile, getMe } from './services/auth';
|
import { getCurrentUserProfile, getMe } from './services/auth';
|
||||||
import { getSystemLanguage } from './services/i18n';
|
import { getSystemLanguage } from './services/i18n';
|
||||||
import { generateId } from './utils/uuid';
|
import { generateId } from './utils/uuid';
|
||||||
@@ -132,39 +132,35 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddSet = async (set: WorkoutSet) => {
|
const handleAddSet = (set: WorkoutSet) => {
|
||||||
if (activeSession && currentUser) {
|
if (activeSession && currentUser) {
|
||||||
const updatedSession = {
|
const updatedSession = {
|
||||||
...activeSession,
|
...activeSession,
|
||||||
sets: [...activeSession.sets, set]
|
sets: [...activeSession.sets, set]
|
||||||
};
|
};
|
||||||
setActiveSession(updatedSession);
|
setActiveSession(updatedSession);
|
||||||
// Save to database
|
|
||||||
await updateActiveSession(currentUser.id, updatedSession);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveSetFromActive = async (setId: string) => {
|
const handleRemoveSetFromActive = async (setId: string) => {
|
||||||
if (activeSession && currentUser) {
|
if (activeSession && currentUser) {
|
||||||
const updatedSession = {
|
await deleteSetFromActiveSession(currentUser.id, setId);
|
||||||
...activeSession,
|
const updatedSession = {
|
||||||
sets: activeSession.sets.filter(s => s.id !== setId)
|
...activeSession,
|
||||||
};
|
sets: activeSession.sets.filter(s => s.id !== setId)
|
||||||
setActiveSession(updatedSession);
|
};
|
||||||
// Save to database
|
setActiveSession(updatedSession);
|
||||||
await updateActiveSession(currentUser.id, updatedSession);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateSetInActive = async (updatedSet: WorkoutSet) => {
|
const handleUpdateSetInActive = async (updatedSet: WorkoutSet) => {
|
||||||
if (activeSession && currentUser) {
|
if (activeSession && currentUser) {
|
||||||
const updatedSession = {
|
const response = await updateSetInActiveSession(currentUser.id, updatedSet.id, updatedSet);
|
||||||
...activeSession,
|
const updatedSession = {
|
||||||
sets: activeSession.sets.map(s => s.id === updatedSet.id ? updatedSet : s)
|
...activeSession,
|
||||||
};
|
sets: activeSession.sets.map(s => s.id === updatedSet.id ? response : s)
|
||||||
setActiveSession(updatedSession);
|
};
|
||||||
// Save to database
|
setActiveSession(updatedSession);
|
||||||
await updateActiveSession(currentUser.id, updatedSession);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getExercises, getLastSetForExercise, saveExercise, getPlans } from '../
|
|||||||
import { getCurrentUserProfile } from '../services/auth';
|
import { getCurrentUserProfile } from '../services/auth';
|
||||||
import { t } from '../services/i18n';
|
import { t } from '../services/i18n';
|
||||||
import { generateId } from '../utils/uuid';
|
import { generateId } from '../utils/uuid';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
|
||||||
interface TrackerProps {
|
interface TrackerProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -99,6 +100,32 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [activeSession]);
|
}, [activeSession]);
|
||||||
|
|
||||||
|
// Recalculate current step when sets change
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeSession && activePlan) {
|
||||||
|
const performedCounts = new Map<string, number>();
|
||||||
|
for (const set of activeSession.sets) {
|
||||||
|
performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextStepIndex = activePlan.steps.length; // Default to finished
|
||||||
|
const plannedCounts = new Map<string, number>();
|
||||||
|
for (let i = 0; i < activePlan.steps.length; i++) {
|
||||||
|
const step = activePlan.steps[i];
|
||||||
|
const exerciseId = step.exerciseId;
|
||||||
|
plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1);
|
||||||
|
const performedCount = performedCounts.get(exerciseId) || 0;
|
||||||
|
|
||||||
|
if (performedCount < plannedCounts.get(exerciseId)!) {
|
||||||
|
nextStepIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentStepIndex(nextStepIndex);
|
||||||
|
}
|
||||||
|
}, [activeSession, activePlan]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeSession && activePlan && exercises.length > 0 && activePlan.steps.length > 0) {
|
if (activeSession && activePlan && exercises.length > 0 && activePlan.steps.length > 0) {
|
||||||
if (currentStepIndex < activePlan.steps.length) {
|
if (currentStepIndex < activePlan.steps.length) {
|
||||||
@@ -166,54 +193,61 @@ const Tracker: React.FC<TrackerProps> = ({ userId, userWeight, activeSession, ac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddSet = () => {
|
const handleAddSet = async () => {
|
||||||
if (!activeSession || !selectedExercise) return;
|
if (!activeSession || !selectedExercise) return;
|
||||||
|
|
||||||
const newSet: WorkoutSet = {
|
const setData: Partial<WorkoutSet> = {
|
||||||
id: generateId(),
|
|
||||||
exerciseId: selectedExercise.id,
|
exerciseId: selectedExercise.id,
|
||||||
exerciseName: selectedExercise.name,
|
|
||||||
type: selectedExercise.type,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (selectedExercise.type) {
|
switch (selectedExercise.type) {
|
||||||
case ExerciseType.STRENGTH:
|
case ExerciseType.STRENGTH:
|
||||||
if (weight) newSet.weight = parseFloat(weight);
|
if (weight) setData.weight = parseFloat(weight);
|
||||||
if (reps) newSet.reps = parseInt(reps);
|
if (reps) setData.reps = parseInt(reps);
|
||||||
break;
|
break;
|
||||||
case ExerciseType.BODYWEIGHT:
|
case ExerciseType.BODYWEIGHT:
|
||||||
if (weight) newSet.weight = parseFloat(weight);
|
if (weight) setData.weight = parseFloat(weight);
|
||||||
if (reps) newSet.reps = parseInt(reps);
|
if (reps) setData.reps = parseInt(reps);
|
||||||
newSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||||
break;
|
break;
|
||||||
case ExerciseType.CARDIO:
|
case ExerciseType.CARDIO:
|
||||||
if (duration) newSet.durationSeconds = parseInt(duration);
|
if (duration) setData.durationSeconds = parseInt(duration);
|
||||||
if (distance) newSet.distanceMeters = parseFloat(distance);
|
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||||
break;
|
break;
|
||||||
case ExerciseType.STATIC:
|
case ExerciseType.STATIC:
|
||||||
if (duration) newSet.durationSeconds = parseInt(duration);
|
if (duration) setData.durationSeconds = parseInt(duration);
|
||||||
newSet.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
setData.bodyWeightPercentage = parseFloat(bwPercentage) || 100;
|
||||||
break;
|
break;
|
||||||
case ExerciseType.HIGH_JUMP:
|
case ExerciseType.HIGH_JUMP:
|
||||||
if (height) newSet.height = parseFloat(height);
|
if (height) setData.height = parseFloat(height);
|
||||||
break;
|
break;
|
||||||
case ExerciseType.LONG_JUMP:
|
case ExerciseType.LONG_JUMP:
|
||||||
if (distance) newSet.distanceMeters = parseFloat(distance);
|
if (distance) setData.distanceMeters = parseFloat(distance);
|
||||||
break;
|
break;
|
||||||
case ExerciseType.PLYOMETRIC:
|
case ExerciseType.PLYOMETRIC:
|
||||||
if (reps) newSet.reps = parseInt(reps);
|
if (reps) setData.reps = parseInt(reps);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSetAdded(newSet);
|
try {
|
||||||
|
const response = await api.post('/sessions/active/log-set', setData);
|
||||||
|
if (response.success) {
|
||||||
|
const { newSet, activeExerciseId } = response;
|
||||||
|
onSetAdded(newSet);
|
||||||
|
|
||||||
if (activePlan) {
|
if (activePlan && activeExerciseId) {
|
||||||
const currentStep = activePlan.steps[currentStepIndex];
|
const nextStepIndex = activePlan.steps.findIndex(step => step.exerciseId === activeExerciseId);
|
||||||
if (currentStep && currentStep.exerciseId === selectedExercise.id) {
|
if (nextStepIndex !== -1) {
|
||||||
const nextIndex = currentStepIndex + 1;
|
setCurrentStepIndex(nextStepIndex);
|
||||||
setCurrentStepIndex(nextIndex);
|
}
|
||||||
|
} else if (activePlan && !activeExerciseId) {
|
||||||
|
// Plan is finished
|
||||||
|
setCurrentStepIndex(activePlan.steps.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to log set:", error);
|
||||||
|
// Optionally, show an error message to the user
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -219,6 +219,163 @@ router.put('/active', async (req: any, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Log a set to the active session
|
||||||
|
router.post('/active/log-set', async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.userId;
|
||||||
|
const { exerciseId, reps, weight, distanceMeters, durationSeconds } = req.body;
|
||||||
|
|
||||||
|
// Find active session
|
||||||
|
const activeSession = await prisma.workoutSession.findFirst({
|
||||||
|
where: { userId, endTime: null },
|
||||||
|
include: { sets: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activeSession) {
|
||||||
|
return res.status(404).json({ error: 'No active session found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the highest order value from the existing sets
|
||||||
|
const maxOrder = activeSession.sets.reduce((max, set) => Math.max(max, set.order), -1);
|
||||||
|
|
||||||
|
// Create the new set
|
||||||
|
const newSet = await prisma.workoutSet.create({
|
||||||
|
data: {
|
||||||
|
sessionId: activeSession.id,
|
||||||
|
exerciseId,
|
||||||
|
order: maxOrder + 1,
|
||||||
|
reps: reps ? parseInt(reps) : null,
|
||||||
|
weight: weight ? parseFloat(weight) : null,
|
||||||
|
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
|
||||||
|
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
|
||||||
|
completed: true
|
||||||
|
},
|
||||||
|
include: { exercise: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recalculate active step
|
||||||
|
if (activeSession.planId) {
|
||||||
|
const plan = await prisma.workoutPlan.findUnique({
|
||||||
|
where: { id: activeSession.planId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (plan) {
|
||||||
|
const planExercises: { id: string }[] = JSON.parse(plan.exercises);
|
||||||
|
const allPerformedSets = await prisma.workoutSet.findMany({
|
||||||
|
where: { sessionId: activeSession.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
const performedCounts = new Map<string, number>();
|
||||||
|
for (const set of allPerformedSets) {
|
||||||
|
performedCounts.set(set.exerciseId, (performedCounts.get(set.exerciseId) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeExerciseId = null;
|
||||||
|
const plannedCounts = new Map<string, number>();
|
||||||
|
for (const planExercise of planExercises) {
|
||||||
|
const exerciseId = planExercise.id;
|
||||||
|
plannedCounts.set(exerciseId, (plannedCounts.get(exerciseId) || 0) + 1);
|
||||||
|
const performedCount = performedCounts.get(exerciseId) || 0;
|
||||||
|
|
||||||
|
if (performedCount < plannedCounts.get(exerciseId)!) {
|
||||||
|
activeExerciseId = exerciseId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappedNewSet = {
|
||||||
|
...newSet,
|
||||||
|
exerciseName: newSet.exercise.name,
|
||||||
|
type: newSet.exercise.type
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.json({ success: true, newSet: mappedNewSet, activeExerciseId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no plan or plan not found, just return the new set
|
||||||
|
const mappedNewSet = {
|
||||||
|
...newSet,
|
||||||
|
exerciseName: newSet.exercise.name,
|
||||||
|
type: newSet.exercise.type
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({ success: true, newSet: mappedNewSet, activeExerciseId: null });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update a set in the active session
|
||||||
|
router.put('/active/set/:setId', async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.userId;
|
||||||
|
const { setId } = req.params;
|
||||||
|
const { reps, weight, distanceMeters, durationSeconds } = req.body;
|
||||||
|
|
||||||
|
// Find active session
|
||||||
|
const activeSession = await prisma.workoutSession.findFirst({
|
||||||
|
where: { userId, endTime: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activeSession) {
|
||||||
|
return res.status(404).json({ error: 'No active session found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSet = await prisma.workoutSet.update({
|
||||||
|
where: { id: setId },
|
||||||
|
data: {
|
||||||
|
reps: reps ? parseInt(reps) : null,
|
||||||
|
weight: weight ? parseFloat(weight) : null,
|
||||||
|
distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null,
|
||||||
|
durationSeconds: durationSeconds ? parseInt(durationSeconds) : null,
|
||||||
|
},
|
||||||
|
include: { exercise: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const mappedUpdatedSet = {
|
||||||
|
...updatedSet,
|
||||||
|
exerciseName: updatedSet.exercise.name,
|
||||||
|
type: updatedSet.exercise.type
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json({ success: true, updatedSet: mappedUpdatedSet });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete a set from the active session
|
||||||
|
router.delete('/active/set/:setId', async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.userId;
|
||||||
|
const { setId } = req.params;
|
||||||
|
|
||||||
|
// Find active session
|
||||||
|
const activeSession = await prisma.workoutSession.findFirst({
|
||||||
|
where: { userId, endTime: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!activeSession) {
|
||||||
|
return res.status(404).json({ error: 'No active session found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.workoutSet.delete({
|
||||||
|
where: { id: setId }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
res.status(500).json({ error: 'Server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Delete active session (quit without saving)
|
// Delete active session (quit without saving)
|
||||||
router.delete('/active', async (req: any, res) => {
|
router.delete('/active', async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ export const updateActiveSession = async (userId: string, session: WorkoutSessio
|
|||||||
await api.put('/sessions/active', session);
|
await api.put('/sessions/active', session);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteSetFromActiveSession = async (userId: string, setId: string): Promise<void> => {
|
||||||
|
await api.delete(`/sessions/active/set/${setId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSetInActiveSession = async (userId: string, setId: string, setData: Partial<WorkoutSet>): Promise<WorkoutSet> => {
|
||||||
|
const response = await api.put(`/sessions/active/set/${setId}`, setData);
|
||||||
|
return response.updatedSet;
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteActiveSession = async (userId: string): Promise<void> => {
|
export const deleteActiveSession = async (userId: string): Promise<void> => {
|
||||||
await api.delete('/sessions/active');
|
await api.delete('/sessions/active');
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user