workout-session-set.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. import { Plus, Minus, Trash2 } from "lucide-react";
  2. import { useI18n } from "locales/client";
  3. import { WorkoutSet, WorkoutSetType, WorkoutSetUnit } from "@/features/workout-session/types/workout-set";
  4. import { Button } from "@/components/ui/button";
  5. interface WorkoutSetRowProps {
  6. set: WorkoutSet;
  7. setIndex: number;
  8. onChange: (setIndex: number, data: Partial<WorkoutSet>) => void;
  9. onFinish: () => void;
  10. onRemove: () => void;
  11. }
  12. const SET_TYPES: WorkoutSetType[] = ["REPS", "WEIGHT", "TIME", "BODYWEIGHT", "NA"];
  13. const UNITS: WorkoutSetUnit[] = ["kg", "lbs"];
  14. export function WorkoutSessionSet({ set, setIndex, onChange, onFinish, onRemove }: WorkoutSetRowProps) {
  15. const t = useI18n();
  16. // On utilise un tableau de types pour gérer plusieurs colonnes
  17. const types = set.types || [];
  18. const maxColumns = 4;
  19. // Handlers pour chaque champ
  20. const handleTypeChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLSelectElement>) => {
  21. const newTypes = [...types];
  22. newTypes[columnIndex] = e.target.value as WorkoutSetType;
  23. onChange(setIndex, { types: newTypes });
  24. };
  25. const handleValueIntChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
  26. const newValuesInt = Array.isArray(set.valuesInt) ? [...set.valuesInt] : [];
  27. newValuesInt[columnIndex] = e.target.value ? parseInt(e.target.value, 10) : 0;
  28. onChange(setIndex, { valuesInt: newValuesInt });
  29. };
  30. const handleValueSecChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLInputElement>) => {
  31. const newValuesSec = Array.isArray(set.valuesSec) ? [...set.valuesSec] : [];
  32. newValuesSec[columnIndex] = e.target.value ? parseInt(e.target.value, 10) : 0;
  33. onChange(setIndex, { valuesSec: newValuesSec });
  34. };
  35. const handleUnitChange = (columnIndex: number) => (e: React.ChangeEvent<HTMLSelectElement>) => {
  36. const newUnits = Array.isArray(set.units) ? [...set.units] : [];
  37. newUnits[columnIndex] = e.target.value as WorkoutSetUnit;
  38. onChange(setIndex, { units: newUnits });
  39. };
  40. const addColumn = () => {
  41. if (types.length < maxColumns) {
  42. const newTypes = [...types, "REPS" as WorkoutSetType];
  43. onChange(setIndex, { types: newTypes });
  44. }
  45. };
  46. const removeColumn = (columnIndex: number) => {
  47. const newTypes = types.filter((_, idx) => idx !== columnIndex);
  48. const newValuesInt = Array.isArray(set.valuesInt) ? set.valuesInt.filter((_, idx) => idx !== columnIndex) : [];
  49. const newValuesSec = Array.isArray(set.valuesSec) ? set.valuesSec.filter((_, idx) => idx !== columnIndex) : [];
  50. const newUnits = Array.isArray(set.units) ? set.units.filter((_, idx) => idx !== columnIndex) : [];
  51. onChange(setIndex, {
  52. types: newTypes,
  53. valuesInt: newValuesInt,
  54. valuesSec: newValuesSec,
  55. units: newUnits,
  56. });
  57. };
  58. const handleEdit = () => {
  59. onChange(setIndex, { completed: false });
  60. };
  61. const renderInputForType = (type: WorkoutSetType, columnIndex: number) => {
  62. const valuesInt = Array.isArray(set.valuesInt) ? set.valuesInt : [set.valueInt];
  63. const valuesSec = Array.isArray(set.valuesSec) ? set.valuesSec : [set.valueSec];
  64. const units = Array.isArray(set.units) ? set.units : [set.unit];
  65. switch (type) {
  66. case "TIME":
  67. return (
  68. <div className="flex gap-1 w-full">
  69. <input
  70. className="border border-black rounded px-1 py-1 w-1/2 text-sm text-center font-bold"
  71. disabled={set.completed}
  72. min={0}
  73. onChange={handleValueIntChange(columnIndex)}
  74. placeholder="min"
  75. type="number"
  76. value={valuesInt[columnIndex] ?? ""}
  77. />
  78. <input
  79. className="border border-black rounded px-1 py-1 w-1/2 text-sm text-center font-bold"
  80. disabled={set.completed}
  81. max={59}
  82. min={0}
  83. onChange={handleValueSecChange(columnIndex)}
  84. placeholder="sec"
  85. type="number"
  86. value={valuesSec[columnIndex] ?? ""}
  87. />
  88. </div>
  89. );
  90. case "WEIGHT":
  91. return (
  92. <div className="flex gap-1 w-full items-center">
  93. <input
  94. className="border border-black rounded px-1 py-1 w-1/2 text-sm text-center font-bold"
  95. disabled={set.completed}
  96. min={0}
  97. onChange={handleValueIntChange(columnIndex)}
  98. placeholder=""
  99. type="number"
  100. value={valuesInt[columnIndex] ?? ""}
  101. />
  102. <select
  103. className="border border-black rounded px-1 py-1 w-1/2 text-sm font-bold bg-white"
  104. disabled={set.completed}
  105. onChange={handleUnitChange(columnIndex)}
  106. value={units[columnIndex] ?? "kg"}
  107. >
  108. <option value="kg">kg</option>
  109. <option value="lbs">lbs</option>
  110. </select>
  111. </div>
  112. );
  113. case "REPS":
  114. return (
  115. <input
  116. className="border border-black rounded px-1 py-1 w-full text-sm text-center font-bold"
  117. disabled={set.completed}
  118. min={0}
  119. onChange={handleValueIntChange(columnIndex)}
  120. placeholder=""
  121. type="number"
  122. value={valuesInt[columnIndex] ?? ""}
  123. />
  124. );
  125. case "BODYWEIGHT":
  126. return (
  127. <input
  128. className="border border-black rounded px-1 py-1 w-full text-sm text-center font-bold"
  129. disabled={set.completed}
  130. placeholder=""
  131. readOnly
  132. value="✔"
  133. />
  134. );
  135. default:
  136. return null;
  137. }
  138. };
  139. return (
  140. <div className="w-full py-4 flex flex-col gap-2 bg-slate-50 border border-slate-200 rounded-xl shadow-sm mb-3 relative px-2 sm:px-4">
  141. <div className="flex items-center justify-between mb-2">
  142. <div className="bg-blue-500 text-white text-xs font-bold px-3 py-1 rounded-full shadow">SET {setIndex + 1}</div>
  143. <Button
  144. aria-label="Supprimer la série"
  145. className="bg-red-100 hover:bg-red-200 text-red-600 rounded-full p-1 h-8 w-8 flex items-center justify-center shadow transition"
  146. disabled={set.completed}
  147. onClick={onRemove}
  148. type="button"
  149. >
  150. <Trash2 className="h-4 w-4" />
  151. </Button>
  152. </div>
  153. {/* Colonnes de types, stack vertical on mobile, horizontal on md+ */}
  154. <div className="flex flex-col md:flex-row gap-2 w-full">
  155. {types.map((type, columnIndex) => (
  156. <div className="flex flex-col w-full md:w-auto" key={columnIndex}>
  157. <div className="flex items-center w-full gap-1 mb-1">
  158. <select
  159. className="border border-black rounded font-bold px-1 py-1 text-sm w-full bg-white min-w-0"
  160. disabled={set.completed}
  161. onChange={handleTypeChange(columnIndex)}
  162. value={type}
  163. >
  164. <option value="TIME">{t("workout_builder.session.time")}</option>
  165. <option value="WEIGHT">{t("workout_builder.session.weight")}</option>
  166. <option value="REPS">{t("workout_builder.session.reps")}</option>
  167. <option value="BODYWEIGHT">{t("workout_builder.session.bodyweight")}</option>
  168. </select>
  169. {types.length > 1 && (
  170. <Button
  171. className="p-1 h-auto bg-red-500 hover:bg-red-600 flex-shrink-0"
  172. onClick={() => removeColumn(columnIndex)}
  173. size="small"
  174. variant="destructive"
  175. >
  176. <Minus className="h-3 w-3" />
  177. </Button>
  178. )}
  179. </div>
  180. {renderInputForType(type, columnIndex)}
  181. </div>
  182. ))}
  183. </div>
  184. {/* Bouton pour ajouter une colonne, sous les colonnes */}
  185. {types.length < maxColumns && (
  186. <div className="flex w-full justify-start mt-1">
  187. <Button
  188. className="bg-green-500 hover:bg-green-600 text-white font-bold px-4 py-2 text-sm rounded w-full md:w-auto mt-2"
  189. disabled={set.completed}
  190. onClick={addColumn}
  191. >
  192. <Plus className="h-4 w-4" />
  193. {t("workout_builder.session.add_column")}
  194. </Button>
  195. </div>
  196. )}
  197. {/* Finish & Edit buttons, full width on mobile */}
  198. <div className="flex gap-2 w-full md:w-auto mt-2">
  199. <Button
  200. className="bg-blue-500 hover:bg-blue-600 text-white font-bold px-4 py-2 text-sm rounded flex-1"
  201. disabled={set.completed}
  202. onClick={onFinish}
  203. >
  204. {t("workout_builder.session.finish_set")}
  205. </Button>
  206. {set.completed && (
  207. <Button
  208. className="bg-gray-100 hover:bg-gray-200 text-gray-700 font-bold px-4 py-2 text-sm rounded flex-1 border border-gray-300"
  209. onClick={handleEdit}
  210. variant="outline"
  211. >
  212. {t("commons.edit")}
  213. </Button>
  214. )}
  215. </div>
  216. </div>
  217. );
  218. }