exercises-selection.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import { useState, useEffect } from "react";
  2. import { Loader2, Plus } from "lucide-react";
  3. import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
  4. import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
  5. import { DndContext, closestCenter, PointerSensor, useSensor, useSensors, DragEndEvent } from "@dnd-kit/core";
  6. import { useWorkoutStepper } from "../model/use-workout-stepper";
  7. import { ExerciseListItem } from "./exercise-list-item";
  8. import type { ExerciseWithAttributes } from "../types";
  9. import type { TFunction } from "../../../../locales/client";
  10. interface ExercisesSelectionProps {
  11. isLoading: boolean;
  12. exercisesByMuscle: { muscle: string; exercises: ExerciseWithAttributes[] }[];
  13. error: any;
  14. onShuffle: (exerciseId: string, muscle: string) => void;
  15. onPick: (exerciseId: string) => void;
  16. onDelete: (exerciseId: string, muscle: string) => void;
  17. onAdd: () => void;
  18. onStartWorkout: (exercises: ExerciseWithAttributes[]) => void;
  19. t: TFunction;
  20. }
  21. export const ExercisesSelection = ({
  22. isLoading,
  23. exercisesByMuscle,
  24. error,
  25. onShuffle,
  26. onPick,
  27. onDelete,
  28. onAdd,
  29. onStartWorkout,
  30. t,
  31. }: ExercisesSelectionProps) => {
  32. const [flatExercises, setFlatExercises] = useState<{ id: string; muscle: string; exercise: ExerciseWithAttributes }[]>([]);
  33. const { setExercisesOrder } = useWorkoutStepper();
  34. const sensors = useSensors(
  35. useSensor(PointerSensor, {
  36. activationConstraint: {
  37. distance: 5,
  38. },
  39. }),
  40. );
  41. useEffect(() => {
  42. if (exercisesByMuscle.length > 0) {
  43. const flat = exercisesByMuscle.flatMap((group) =>
  44. group.exercises.map((exercise) => ({
  45. id: exercise.id,
  46. muscle: group.muscle,
  47. exercise,
  48. })),
  49. );
  50. setFlatExercises(flat);
  51. } else {
  52. setFlatExercises([]);
  53. }
  54. }, [exercisesByMuscle]);
  55. const handleDragEnd = (event: DragEndEvent) => {
  56. const { active, over } = event;
  57. if (active.id !== over?.id) {
  58. setFlatExercises((items) => {
  59. const oldIndex = items.findIndex((item) => item.id === active.id);
  60. const newIndex = items.findIndex((item) => item.id === over?.id);
  61. const newOrder = arrayMove(items, oldIndex, newIndex);
  62. setExercisesOrder(newOrder.map((item) => item.id));
  63. return newOrder;
  64. });
  65. }
  66. };
  67. const handleStartWorkout = () => {
  68. const allExercises = flatExercises.map((item) => item.exercise);
  69. if (allExercises.length > 0) {
  70. onStartWorkout(allExercises);
  71. }
  72. };
  73. return (
  74. <div className="space-y-6">
  75. {isLoading ? (
  76. <div className="text-center">
  77. <Loader2 className="h-8 w-8 animate-spin mx-auto text-blue-600" />
  78. <p className="mt-4 text-slate-600 dark:text-slate-400">{t("workout_builder.loading.exercises")}</p>
  79. </div>
  80. ) : flatExercises.length > 0 ? (
  81. <div className="max-w-4xl mx-auto">
  82. {/* Liste des exercices drag and drop */}
  83. <DndContext collisionDetection={closestCenter} modifiers={[restrictToVerticalAxis]} onDragEnd={handleDragEnd} sensors={sensors}>
  84. <SortableContext items={flatExercises.map((item) => item.id)} strategy={verticalListSortingStrategy}>
  85. <div className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 overflow-hidden">
  86. {flatExercises.map((item) => (
  87. <ExerciseListItem
  88. exercise={item.exercise}
  89. key={item.id}
  90. muscle={item.muscle}
  91. onDelete={onDelete}
  92. onPick={onPick}
  93. onShuffle={onShuffle}
  94. />
  95. ))}
  96. <div className="border-t border-slate-200 dark:border-slate-800">
  97. <button
  98. className="w-full flex items-center gap-3 py-4 px-4 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-950 transition-colors"
  99. onClick={onAdd}
  100. >
  101. <div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center">
  102. <Plus className="h-4 w-4 text-white" />
  103. </div>
  104. <span className="font-medium">Add</span>
  105. </button>
  106. </div>
  107. </div>
  108. </SortableContext>
  109. </DndContext>
  110. </div>
  111. ) : error ? (
  112. <div className="text-center py-20">
  113. <p className="text-red-600 dark:text-red-400">{t("workout_builder.error.loading_exercises")}</p>
  114. </div>
  115. ) : (
  116. <div className="text-center py-20">
  117. <p className="text-slate-600 dark:text-slate-400">{t("workout_builder.no_exercises_found")}</p>
  118. </div>
  119. )}
  120. </div>
  121. );
  122. };