workout-session-list.tsx 7.9 KB

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