workout-session-set.tsx 8.7 KB

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