mirror of
https://github.com/DocNR/POWR.git
synced 2025-04-23 01:01:27 +00:00
285 lines
9.0 KiB
TypeScript
285 lines
9.0 KiB
TypeScript
// contexts/WorkoutContext.tsx
|
|
import React, { createContext, useContext, useReducer, useEffect } from 'react';
|
|
import { BaseExercise, WorkoutExercise, WorkoutSet } from '@/types/exercise';
|
|
import { WorkoutState } from '@/types/workout';
|
|
import { WorkoutTemplate } from '@/types/template';
|
|
import { generateId } from '@/utils/ids';
|
|
|
|
type WorkoutAction =
|
|
| { type: 'START_WORKOUT'; payload: { title: string } }
|
|
| { type: 'END_WORKOUT' }
|
|
| { type: 'PAUSE_WORKOUT' }
|
|
| { type: 'RESUME_WORKOUT' }
|
|
| { type: 'ADD_EXERCISE'; payload: BaseExercise }
|
|
| { type: 'UPDATE_EXERCISE'; payload: Partial<WorkoutExercise> & { id: string } }
|
|
| { type: 'REMOVE_EXERCISE'; payload: { id: string } }
|
|
| { type: 'ADD_SET'; payload: { exerciseId: string; set: WorkoutSet } }
|
|
| { type: 'UPDATE_SET'; payload: { exerciseId: string; setId: string; updates: Partial<WorkoutSet> } }
|
|
| { type: 'COMPLETE_SET'; payload: { exerciseId: string; setId: string } }
|
|
| { type: 'UPDATE_NOTES'; payload: string }
|
|
| { type: 'UPDATE_TITLE'; payload: string }
|
|
| { type: 'START_FROM_TEMPLATE'; payload: WorkoutTemplate }
|
|
| { type: 'RESTORE_STATE'; payload: WorkoutState }
|
|
| { type: 'SAVE_TEMPLATE'; payload: WorkoutTemplate };
|
|
|
|
interface WorkoutContextType extends WorkoutState {
|
|
startWorkout: (title: string) => void;
|
|
endWorkout: () => Promise<void>;
|
|
pauseWorkout: () => void;
|
|
resumeWorkout: () => void;
|
|
addExercise: (exercise: BaseExercise) => void;
|
|
updateExercise: (exerciseId: string, updates: Partial<WorkoutExercise>) => void;
|
|
removeExercise: (exerciseId: string) => void;
|
|
addSet: (exerciseId: string, set: WorkoutSet) => void;
|
|
updateSet: (exerciseId: string, setId: string, updates: Partial<WorkoutSet>) => void;
|
|
completeSet: (exerciseId: string, setId: string) => void;
|
|
updateNotes: (notes: string) => void;
|
|
updateTitle: (title: string) => void;
|
|
startFromTemplate: (template: WorkoutTemplate) => void;
|
|
saveTemplate: (template: WorkoutTemplate) => Promise<void>;
|
|
}
|
|
|
|
const initialState: WorkoutState = {
|
|
id: generateId(),
|
|
title: '',
|
|
startTime: null,
|
|
endTime: null,
|
|
isActive: false,
|
|
exercises: [],
|
|
notes: '',
|
|
totalTime: 0,
|
|
isPaused: false,
|
|
created_at: Date.now(),
|
|
totalWeight: 0,
|
|
availability: {
|
|
source: ['local']
|
|
}
|
|
};
|
|
|
|
const WorkoutContext = createContext<WorkoutContextType | null>(null);
|
|
|
|
function workoutReducer(state: WorkoutState, action: WorkoutAction): WorkoutState {
|
|
switch (action.type) {
|
|
case 'START_WORKOUT':
|
|
return {
|
|
...initialState,
|
|
id: generateId(),
|
|
title: action.payload.title,
|
|
startTime: new Date(),
|
|
isActive: true,
|
|
created_at: Date.now(),
|
|
availability: {
|
|
source: ['local']
|
|
}
|
|
};
|
|
|
|
case 'END_WORKOUT': {
|
|
const endTime = new Date();
|
|
const totalTime = state.startTime
|
|
? Math.floor((endTime.getTime() - state.startTime.getTime()) / 1000)
|
|
: 0;
|
|
|
|
return {
|
|
...state,
|
|
isActive: false,
|
|
endTime,
|
|
totalTime
|
|
};
|
|
}
|
|
|
|
case 'PAUSE_WORKOUT':
|
|
return { ...state, isPaused: true };
|
|
|
|
case 'RESUME_WORKOUT':
|
|
return { ...state, isPaused: false };
|
|
|
|
case 'ADD_EXERCISE': {
|
|
// Convert BaseExercise to WorkoutExercise
|
|
const workoutExercise: WorkoutExercise = {
|
|
...action.payload,
|
|
sets: [],
|
|
};
|
|
return {
|
|
...state,
|
|
exercises: [...state.exercises, workoutExercise]
|
|
};
|
|
}
|
|
|
|
case 'UPDATE_EXERCISE':
|
|
return {
|
|
...state,
|
|
exercises: state.exercises.map(ex =>
|
|
ex.id === action.payload.id
|
|
? { ...ex, ...action.payload }
|
|
: ex
|
|
)
|
|
};
|
|
|
|
case 'REMOVE_EXERCISE':
|
|
return {
|
|
...state,
|
|
exercises: state.exercises.filter(ex => ex.id !== action.payload.id),
|
|
totalWeight: state.exercises.reduce((total, ex) => {
|
|
if (ex.id === action.payload.id) {
|
|
return total - ex.sets.reduce((setTotal, set) =>
|
|
setTotal + (set.weight || 0) * (set.reps || 0), 0);
|
|
}
|
|
return total;
|
|
}, state.totalWeight)
|
|
};
|
|
|
|
case 'ADD_SET': {
|
|
const newSet = action.payload.set;
|
|
const setWeight = (newSet.weight || 0) * (newSet.reps || 0);
|
|
|
|
return {
|
|
...state,
|
|
exercises: state.exercises.map(ex =>
|
|
ex.id === action.payload.exerciseId
|
|
? { ...ex, sets: [...ex.sets, action.payload.set] }
|
|
: ex
|
|
),
|
|
totalWeight: state.totalWeight + setWeight
|
|
};
|
|
}
|
|
|
|
case 'UPDATE_SET': {
|
|
const exercise = state.exercises.find(ex => ex.id === action.payload.exerciseId);
|
|
const oldSet = exercise?.sets.find(set => set.id === action.payload.setId);
|
|
const oldWeight = oldSet ? (oldSet.weight || 0) * (oldSet.reps || 0) : 0;
|
|
|
|
const newWeight = action.payload.updates.weight !== undefined && action.payload.updates.reps !== undefined
|
|
? (action.payload.updates.weight || 0) * (action.payload.updates.reps || 0)
|
|
: action.payload.updates.weight !== undefined
|
|
? (action.payload.updates.weight || 0) * ((oldSet?.reps || 0))
|
|
: action.payload.updates.reps !== undefined
|
|
? ((oldSet?.weight || 0)) * (action.payload.updates.reps || 0)
|
|
: oldWeight;
|
|
|
|
return {
|
|
...state,
|
|
exercises: state.exercises.map(ex =>
|
|
ex.id === action.payload.exerciseId
|
|
? {
|
|
...ex,
|
|
sets: ex.sets.map(set =>
|
|
set.id === action.payload.setId
|
|
? { ...set, ...action.payload.updates }
|
|
: set
|
|
)
|
|
}
|
|
: ex
|
|
),
|
|
totalWeight: state.totalWeight - oldWeight + newWeight
|
|
};
|
|
}
|
|
|
|
case 'COMPLETE_SET':
|
|
return {
|
|
...state,
|
|
exercises: state.exercises.map(ex =>
|
|
ex.id === action.payload.exerciseId
|
|
? {
|
|
...ex,
|
|
sets: ex.sets.map(set =>
|
|
set.id === action.payload.setId
|
|
? { ...set, isCompleted: !set.isCompleted }
|
|
: set
|
|
)
|
|
}
|
|
: ex
|
|
)
|
|
};
|
|
|
|
case 'UPDATE_NOTES':
|
|
return { ...state, notes: action.payload };
|
|
|
|
case 'UPDATE_TITLE':
|
|
return { ...state, title: action.payload };
|
|
|
|
case 'START_FROM_TEMPLATE':
|
|
const template = action.payload;
|
|
return {
|
|
...initialState,
|
|
id: generateId(),
|
|
title: template.title,
|
|
startTime: new Date(),
|
|
isActive: true,
|
|
created_at: Date.now(),
|
|
templateSource: {
|
|
id: template.id,
|
|
title: template.title,
|
|
category: template.category
|
|
},
|
|
availability: {
|
|
source: ['local']
|
|
}
|
|
};
|
|
|
|
case 'RESTORE_STATE':
|
|
return action.payload;
|
|
|
|
case 'SAVE_TEMPLATE':
|
|
return state;
|
|
|
|
default:
|
|
return state;
|
|
}
|
|
}
|
|
|
|
export function WorkoutProvider({ children }: { children: React.ReactNode }) {
|
|
const [state, dispatch] = useReducer(workoutReducer, initialState);
|
|
|
|
useEffect(() => {
|
|
if (state.isActive) {
|
|
const saveState = async () => {
|
|
try {
|
|
// Save to AsyncStorage implementation pending
|
|
} catch (error) {
|
|
console.error('Error auto-saving workout:', error);
|
|
}
|
|
};
|
|
|
|
const interval = setInterval(saveState, 30000);
|
|
return () => clearInterval(interval);
|
|
}
|
|
}, [state]);
|
|
|
|
const contextValue: WorkoutContextType = {
|
|
...state,
|
|
startWorkout: (title) => dispatch({ type: 'START_WORKOUT', payload: { title } }),
|
|
endWorkout: async () => {
|
|
dispatch({ type: 'END_WORKOUT' });
|
|
},
|
|
pauseWorkout: () => dispatch({ type: 'PAUSE_WORKOUT' }),
|
|
resumeWorkout: () => dispatch({ type: 'RESUME_WORKOUT' }),
|
|
addExercise: (exercise) => dispatch({ type: 'ADD_EXERCISE', payload: exercise }),
|
|
updateExercise: (id, updates) => dispatch({ type: 'UPDATE_EXERCISE', payload: { id, ...updates } }),
|
|
removeExercise: (id) => dispatch({ type: 'REMOVE_EXERCISE', payload: { id } }),
|
|
addSet: (exerciseId, set) => dispatch({ type: 'ADD_SET', payload: { exerciseId, set } }),
|
|
updateSet: (exerciseId, setId, updates) =>
|
|
dispatch({ type: 'UPDATE_SET', payload: { exerciseId, setId, updates } }),
|
|
completeSet: (exerciseId, setId) =>
|
|
dispatch({ type: 'COMPLETE_SET', payload: { exerciseId, setId } }),
|
|
updateNotes: (notes) => dispatch({ type: 'UPDATE_NOTES', payload: notes }),
|
|
updateTitle: (title) => dispatch({ type: 'UPDATE_TITLE', payload: title }),
|
|
startFromTemplate: (template) => dispatch({ type: 'START_FROM_TEMPLATE', payload: template }),
|
|
saveTemplate: async (template) => {
|
|
dispatch({ type: 'SAVE_TEMPLATE', payload: template });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<WorkoutContext.Provider value={contextValue}>
|
|
{children}
|
|
</WorkoutContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useWorkout() {
|
|
const context = useContext(WorkoutContext);
|
|
if (!context) {
|
|
throw new Error('useWorkout must be used within a WorkoutProvider');
|
|
}
|
|
return context;
|
|
} |