workout-session-list.tsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import { useRouter } from "next/navigation";
  2. import { Play, Repeat2, Trash2 } from "lucide-react";
  3. import { useCurrentLocale, useI18n } from "locales/client";
  4. import { useWorkoutSessionService } from "@/shared/lib/workout-session/use-workout-session.service";
  5. import { useWorkoutSessions } from "@/features/workout-session/model/use-workout-sessions";
  6. import { useWorkoutBuilderStore } from "@/features/workout-builder/model/workout-builder.store";
  7. import { Link } from "@/components/ui/link";
  8. import { Button } from "@/components/ui/button";
  9. const BADGE_COLORS = [
  10. "bg-blue-100 text-blue-700 border-blue-300 dark:bg-blue-900 dark:text-blue-100 dark:border-blue-800",
  11. "bg-green-100 text-green-700 border-green-300 dark:bg-green-900 dark:text-green-100 dark:border-green-800",
  12. "bg-red-100 text-red-700 border-red-300 dark:bg-red-900 dark:text-red-300 dark:border-red-700",
  13. "bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-900 dark:text-purple-100 dark:border-purple-800",
  14. "bg-orange-100 text-orange-700 border-orange-300 dark:bg-orange-900 dark:text-orange-100 dark:border-orange-800",
  15. "bg-pink-100 text-pink-700 border-pink-300 dark:bg-pink-900 dark:text-pink-100 dark:border-pink-800",
  16. ];
  17. export function WorkoutSessionList() {
  18. const locale = useCurrentLocale();
  19. const t = useI18n();
  20. const router = useRouter();
  21. const loadFromSession = useWorkoutBuilderStore((s) => s.loadFromSession);
  22. const { remove } = useWorkoutSessionService();
  23. const { data: sessions = [], refetch } = useWorkoutSessions();
  24. const activeSession = sessions.find((s) => s.status === "active");
  25. const handleDelete = async (id: string) => {
  26. const confirmed = window.confirm(t("workout_builder.confirm_delete"));
  27. if (!confirmed) return;
  28. try {
  29. await remove(id);
  30. refetch();
  31. } catch (error) {
  32. console.error("Error deleting session:", error);
  33. alert("Error deleting session");
  34. }
  35. };
  36. const handleRepeat = (id: string) => {
  37. const sessionToCopy = sessions.find((s) => s.id === id);
  38. if (!sessionToCopy) return;
  39. const allEquipment = Array.from(
  40. new Set(
  41. sessionToCopy.exercises
  42. .flatMap((ex) =>
  43. ex.attributes?.filter((attr) => attr.attributeName?.name === "EQUIPMENT").map((attr) => attr.attributeValue.value),
  44. )
  45. .filter(Boolean),
  46. ),
  47. );
  48. // Utilise les muscles stockés dans la session, sinon fallback sur les muscles primaires des exercices
  49. const allMuscles =
  50. sessionToCopy.muscles && sessionToCopy.muscles.length > 0
  51. ? sessionToCopy.muscles
  52. : Array.from(
  53. new Set(
  54. sessionToCopy.exercises
  55. .flatMap((ex) =>
  56. ex.attributes?.filter((attr) => attr.attributeName?.name === "PRIMARY_MUSCLE").map((attr) => attr.attributeValue.value),
  57. )
  58. .filter(Boolean),
  59. ),
  60. );
  61. console.log("allMuscles:", allMuscles);
  62. const exercisesByMuscle = allMuscles.map((muscle) => ({
  63. muscle,
  64. exercises: sessionToCopy.exercises
  65. .filter((ex) =>
  66. ex.attributes?.some((attr) => attr.attributeName?.name === "PRIMARY_MUSCLE" && attr.attributeValue.value === muscle),
  67. )
  68. .map((ex) => ({
  69. ...ex,
  70. id: ex.id,
  71. workoutSessionId: sessionToCopy.id,
  72. exerciseId: ex.id,
  73. order: ex.order,
  74. })),
  75. }));
  76. const exercisesOrder = sessionToCopy.exercises.map((ex) => ex.id);
  77. // 5. inject in the builder and go step 3
  78. loadFromSession({
  79. equipment: allEquipment,
  80. muscles: allMuscles,
  81. exercisesByMuscle,
  82. exercisesOrder,
  83. });
  84. router.push("/?fromSession=1");
  85. };
  86. return (
  87. <div className="space-y-4 mt-10">
  88. <h2 className="text-xl font-bold mt-5 mb-2 text-slate-900 dark:text-slate-200">
  89. {t("workout_builder.session.history", { count: sessions.length })}
  90. </h2>
  91. {sessions.length === 0 && <div className="text-slate-500 dark:text-slate-400">{t("workout_builder.session.no_workout_yet")}</div>}
  92. <ul className="divide-y divide-slate-200 dark:divide-slate-700/50">
  93. {sessions.map((session) => {
  94. const isActive = session.status === "active";
  95. return (
  96. <li
  97. className="px-2 flex flex-col sm:flex-row items-start sm:items-center justify-between py-4 gap-2 sm:gap-0 hover:bg-slate-50 dark:hover:bg-slate-800/70 rounded-lg space-x-4"
  98. key={session.id}
  99. >
  100. <div className="flex items-center flex-col">
  101. <span className="font-bold text-base tabular-nums text-slate-900 dark:text-slate-200">
  102. {new Date(session.startedAt).toLocaleDateString(locale)}
  103. </span>
  104. <span className="text-xs text-slate-700 dark:text-slate-300 tabular-nums">
  105. {t("workout_builder.session.start") || "start"}
  106. {" : "}
  107. {new Date(session.startedAt).toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" })}
  108. </span>
  109. {session.endedAt && (
  110. <span className="text-xs text-slate-500 dark:text-slate-400 tabular-nums">
  111. {t("workout_builder.session.end") || "end"}
  112. {" : "}
  113. {new Date(session.endedAt).toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" })}
  114. </span>
  115. )}
  116. {session.muscles && session.muscles.length > 0 && (
  117. <div className="flex flex-wrap gap-1 mt-1 justify-center">
  118. {session.muscles.map((muscle, idx) => (
  119. <span
  120. // eslint-disable-next-line max-len
  121. className={`inline-block border rounded-full px-2 py-0.5 text-xs font-semibold ${BADGE_COLORS[idx % BADGE_COLORS.length]}`}
  122. key={muscle}
  123. >
  124. {t(("workout_builder.muscles." + muscle.toLowerCase()) as keyof typeof t)}
  125. </span>
  126. ))}
  127. </div>
  128. )}
  129. {session.status === "active" && (
  130. <div className="relative mt-1">
  131. <span className="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 border border-emerald-300 text-xs font-semibold">
  132. {t("commons.in_progress")}
  133. </span>
  134. <span className="absolute top-0 right-0 w-2 h-2 bg-emerald-500 rounded-full animate-ping"></span>
  135. </div>
  136. )}
  137. </div>
  138. <div className="flex flex-wrap gap-2 flex-1">
  139. {session.exercises?.map((ex, idx) => {
  140. const exerciseName = locale === "fr" ? ex.name : ex.nameEn;
  141. return (
  142. <span
  143. className={`inline-block border rounded-full px-1 text-xs font-semibold ${BADGE_COLORS[idx % BADGE_COLORS.length]}`}
  144. key={ex.id}
  145. >
  146. {exerciseName?.toUpperCase() || t("workout_builder.session.exercise")}
  147. </span>
  148. );
  149. })}
  150. </div>
  151. <div className="flex gap-2 items-center mt-2 sm:mt-0">
  152. {isActive && (
  153. <Link className="w-auto flex items-center gap-2 flex-col" href="/" variant="nav">
  154. <Play className="w-7 h-7 text-blue-500 dark:text-blue-400" />
  155. <span className="sr-only">{t("workout_builder.session.back_to_workout")}</span>
  156. <span>{t("workout_builder.session.back_to_workout")}</span>
  157. </Link>
  158. )}
  159. {!isActive && (
  160. <div
  161. className="tooltip tooltip-left"
  162. data-tip={
  163. activeSession ? t("workout_builder.session.already_have_a_active_session") : t("workout_builder.session.repeat")
  164. }
  165. >
  166. <Button
  167. aria-label={t("workout_builder.session.repeat")}
  168. className="w-12 h-12"
  169. disabled={!!activeSession}
  170. onClick={() => handleRepeat(session.id)}
  171. size="icon"
  172. variant="ghost"
  173. >
  174. <Repeat2 className="w-7 h-7 text-blue-500 dark:text-blue-400" />
  175. </Button>
  176. </div>
  177. )}
  178. {!isActive && (
  179. <div className="tooltip" data-tip={t("workout_builder.session.delete")}>
  180. <Button
  181. aria-label={t("workout_builder.session.delete")}
  182. onClick={() => handleDelete(session.id)}
  183. size="icon"
  184. variant="ghost"
  185. >
  186. <Trash2 className="w-7 h-7 text-red-500 dark:text-red-400" />
  187. </Button>
  188. </div>
  189. )}
  190. </div>
  191. </li>
  192. );
  193. })}
  194. </ul>
  195. {/* TODO: Ajouter un bouton pour créer une nouvelle séance (redirige vers le builder sans session courante) */}
  196. </div>
  197. );
  198. }