workout-session.store.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. import { create } from "zustand";
  2. import { workoutSessionLocal } from "@/shared/lib/workout-session/workout-session.local";
  3. import { WorkoutSession } from "@/shared/lib/workout-session/types/workout-session";
  4. import { WorkoutSessionExercise, WorkoutSet } from "@/features/workout-session/types/workout-set";
  5. import { useWorkoutBuilderStore } from "@/features/workout-builder/model/workout-builder.store";
  6. import { ExerciseWithAttributes } from "../../workout-builder/types";
  7. interface WorkoutSessionProgress {
  8. exerciseId: string;
  9. sets: {
  10. reps: number;
  11. weight?: number;
  12. duration?: number;
  13. }[];
  14. completed: boolean;
  15. }
  16. interface WorkoutSessionState {
  17. session: WorkoutSession | null;
  18. progress: Record<string, WorkoutSessionProgress>;
  19. elapsedTime: number;
  20. isTimerRunning: boolean;
  21. isWorkoutActive: boolean;
  22. currentExerciseIndex: number;
  23. currentExercise: WorkoutSessionExercise | null;
  24. // Progression
  25. exercisesCompleted: number;
  26. totalExercises: number;
  27. progressPercent: number;
  28. // Actions
  29. startWorkout: (exercises: ExerciseWithAttributes[], equipment: any[], muscles: any[]) => void;
  30. quitWorkout: () => void;
  31. completeWorkout: () => void;
  32. toggleTimer: () => void;
  33. resetTimer: () => void;
  34. updateExerciseProgress: (exerciseId: string, progressData: Partial<WorkoutSessionProgress>) => void;
  35. addSet: () => void;
  36. updateSet: (exerciseIndex: number, setIndex: number, data: Partial<WorkoutSet>) => void;
  37. removeSet: (exerciseIndex: number, setIndex: number) => void;
  38. finishSet: (exerciseIndex: number, setIndex: number) => void;
  39. goToNextExercise: () => void;
  40. goToPrevExercise: () => void;
  41. goToExercise: (targetIndex: number) => void;
  42. formatElapsedTime: () => string;
  43. getExercisesCompleted: () => number;
  44. getTotalExercises: () => number;
  45. loadSessionFromLocal: () => void;
  46. }
  47. export const useWorkoutSessionStore = create<WorkoutSessionState>((set, get) => ({
  48. session: null,
  49. progress: {},
  50. elapsedTime: 0,
  51. isTimerRunning: false,
  52. isWorkoutActive: false,
  53. currentExerciseIndex: 0,
  54. currentExercise: null,
  55. exercisesCompleted: 0,
  56. totalExercises: 0,
  57. progressPercent: 0,
  58. startWorkout: (exercises, _equipment, _muscles) => {
  59. const sessionExercises: WorkoutSessionExercise[] = exercises.map((ex, idx) => ({
  60. ...ex,
  61. order: idx,
  62. sets: [
  63. {
  64. id: `${ex.id}-set-1`,
  65. setIndex: 0,
  66. types: ["REPS", "WEIGHT"],
  67. valuesInt: [],
  68. valuesSec: [],
  69. units: [],
  70. completed: false,
  71. },
  72. ],
  73. }));
  74. const newSession: WorkoutSession = {
  75. id: Date.now().toString(),
  76. userId: "local",
  77. startedAt: new Date().toISOString(),
  78. exercises: sessionExercises,
  79. status: "active",
  80. };
  81. workoutSessionLocal.add(newSession);
  82. workoutSessionLocal.setCurrent(newSession.id);
  83. set({
  84. session: newSession,
  85. elapsedTime: 0,
  86. isTimerRunning: false,
  87. isWorkoutActive: true,
  88. currentExercise: sessionExercises[0],
  89. });
  90. },
  91. quitWorkout: () => {
  92. const { session } = get();
  93. if (session) {
  94. workoutSessionLocal.remove(session.id);
  95. }
  96. set({
  97. session: null,
  98. progress: {},
  99. elapsedTime: 0,
  100. isTimerRunning: false,
  101. isWorkoutActive: false,
  102. currentExerciseIndex: 0,
  103. currentExercise: null,
  104. });
  105. },
  106. completeWorkout: () => {
  107. const { session } = get();
  108. if (session) {
  109. workoutSessionLocal.update(session.id, { status: "completed", endedAt: new Date().toISOString() });
  110. set({
  111. session: { ...session, status: "completed", endedAt: new Date().toISOString() },
  112. progress: {},
  113. elapsedTime: 0,
  114. isTimerRunning: false,
  115. isWorkoutActive: false,
  116. });
  117. }
  118. useWorkoutBuilderStore.getState().setStep(1);
  119. },
  120. toggleTimer: () => {
  121. set((state) => {
  122. const newIsRunning = !state.isTimerRunning;
  123. if (state.session) {
  124. workoutSessionLocal.update(state.session.id, { isActive: newIsRunning });
  125. }
  126. return { isTimerRunning: newIsRunning };
  127. });
  128. },
  129. resetTimer: () => {
  130. set((state) => {
  131. if (state.session) {
  132. workoutSessionLocal.update(state.session.id, { duration: 0 });
  133. }
  134. return { elapsedTime: 0 };
  135. });
  136. },
  137. updateExerciseProgress: (exerciseId, progressData) => {
  138. set((state) => ({
  139. progress: {
  140. ...state.progress,
  141. [exerciseId]: {
  142. ...state.progress[exerciseId],
  143. exerciseId,
  144. sets: [],
  145. completed: false,
  146. ...progressData,
  147. },
  148. },
  149. }));
  150. },
  151. addSet: () => {
  152. const { session, currentExerciseIndex } = get();
  153. if (!session) return;
  154. const exIdx = currentExerciseIndex;
  155. const sets = session.exercises[exIdx].sets;
  156. const newSet: WorkoutSet = {
  157. id: `${session.exercises[exIdx].id}-set-${sets.length + 1}`,
  158. setIndex: sets.length,
  159. types: ["REPS"],
  160. valuesInt: [],
  161. valuesSec: [],
  162. units: [],
  163. completed: false,
  164. };
  165. const updatedExercises = session.exercises.map((ex, idx) => (idx === exIdx ? { ...ex, sets: [...ex.sets, newSet] } : ex));
  166. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  167. set({
  168. session: { ...session, exercises: updatedExercises },
  169. currentExercise: { ...updatedExercises[exIdx] },
  170. });
  171. },
  172. updateSet: (exerciseIndex, setIndex, data) => {
  173. const { session } = get();
  174. if (!session) return;
  175. const targetExercise = session.exercises[exerciseIndex];
  176. if (!targetExercise) return;
  177. const updatedSets = targetExercise.sets.map((set, idx) => (idx === setIndex ? { ...set, ...data } : set));
  178. const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));
  179. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  180. set({
  181. session: { ...session, exercises: updatedExercises },
  182. currentExercise: { ...updatedExercises[exerciseIndex] },
  183. });
  184. // handle exercisesCompleted
  185. },
  186. removeSet: (exerciseIndex, setIndex) => {
  187. const { session } = get();
  188. if (!session) return;
  189. const targetExercise = session.exercises[exerciseIndex];
  190. if (!targetExercise) return;
  191. const updatedSets = targetExercise.sets.filter((_, idx) => idx !== setIndex);
  192. const updatedExercises = session.exercises.map((ex, idx) => (idx === exerciseIndex ? { ...ex, sets: updatedSets } : ex));
  193. workoutSessionLocal.update(session.id, { exercises: updatedExercises });
  194. set({
  195. session: { ...session, exercises: updatedExercises },
  196. currentExercise: { ...updatedExercises[exerciseIndex] },
  197. });
  198. },
  199. finishSet: (exerciseIndex, setIndex) => {
  200. get().updateSet(exerciseIndex, setIndex, { completed: true });
  201. // if has completed all sets, go to next exercise
  202. const { session } = get();
  203. if (!session) return;
  204. const exercise = session.exercises[exerciseIndex];
  205. if (!exercise) return;
  206. if (exercise.sets.every((set) => set.completed)) {
  207. get().goToNextExercise();
  208. // update exercisesCompleted
  209. const exercisesCompleted = get().exercisesCompleted;
  210. set({ exercisesCompleted: exercisesCompleted + 1 });
  211. }
  212. },
  213. goToNextExercise: () => {
  214. const { session, currentExerciseIndex } = get();
  215. if (!session) return;
  216. const idx = currentExerciseIndex;
  217. if (idx < session.exercises.length - 1) {
  218. workoutSessionLocal.update(session.id, { currentExerciseIndex: idx + 1 });
  219. set({
  220. currentExerciseIndex: idx + 1,
  221. currentExercise: session.exercises[idx + 1],
  222. });
  223. }
  224. },
  225. goToPrevExercise: () => {
  226. const { session, currentExerciseIndex } = get();
  227. if (!session) return;
  228. const idx = currentExerciseIndex;
  229. if (idx > 0) {
  230. workoutSessionLocal.update(session.id, { currentExerciseIndex: idx - 1 });
  231. set({
  232. currentExerciseIndex: idx - 1,
  233. currentExercise: session.exercises[idx - 1],
  234. });
  235. }
  236. },
  237. goToExercise: (targetIndex) => {
  238. const { session } = get();
  239. if (!session) return;
  240. if (targetIndex >= 0 && targetIndex < session.exercises.length) {
  241. workoutSessionLocal.update(session.id, { currentExerciseIndex: targetIndex });
  242. set({
  243. currentExerciseIndex: targetIndex,
  244. currentExercise: session.exercises[targetIndex],
  245. });
  246. }
  247. },
  248. getExercisesCompleted: () => {
  249. const { session } = get();
  250. if (!session) return 0;
  251. // only count exercises with at least one set
  252. return session.exercises
  253. .filter((exercise) => exercise.sets.length > 0)
  254. .filter((exercise) => exercise.sets.every((set) => set.completed)).length;
  255. },
  256. getTotalExercises: () => {
  257. const { session } = get();
  258. if (!session) return 0;
  259. return session.exercises.length;
  260. },
  261. formatElapsedTime: () => {
  262. const { elapsedTime } = get();
  263. const hours = Math.floor(elapsedTime / 3600);
  264. const minutes = Math.floor((elapsedTime % 3600) / 60);
  265. const secs = elapsedTime % 60;
  266. if (hours > 0) {
  267. return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
  268. }
  269. return `${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
  270. },
  271. loadSessionFromLocal: () => {
  272. const currentId = workoutSessionLocal.getCurrent();
  273. console.log("currentId:", currentId);
  274. if (currentId) {
  275. const session = workoutSessionLocal.getById(currentId);
  276. if (session && session.status === "active") {
  277. set({
  278. session,
  279. isWorkoutActive: true,
  280. currentExerciseIndex: session.currentExerciseIndex ?? 0,
  281. currentExercise: session.exercises[session.currentExerciseIndex ?? 0],
  282. elapsedTime: 0,
  283. isTimerRunning: false,
  284. });
  285. }
  286. }
  287. },
  288. }));