123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- import { useState, useEffect, useCallback, useMemo } from "react";
- import { Loader2, Plus } from "lucide-react";
- import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
- import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
- import { DndContext, closestCenter, TouchSensor, MouseSensor, useSensor, useSensors, DragEndEvent } from "@dnd-kit/core";
- import { useI18n } from "locales/client";
- import { useWorkoutStepper } from "../model/use-workout-stepper";
- import { ExerciseListItem } from "./exercise-list-item";
- import type { ExerciseWithAttributes } from "../types";
- interface ExercisesSelectionProps {
- isLoading: boolean;
- exercisesByMuscle: { muscle: string; exercises: ExerciseWithAttributes[] }[];
- error: any;
- onShuffle: (exerciseId: string, muscle: string) => void;
- onPick: (exerciseId: string) => void;
- onDelete: (exerciseId: string, muscle: string) => void;
- onAdd: () => void;
- shufflingExerciseId?: string | null;
- }
- export const ExercisesSelection = ({
- isLoading,
- exercisesByMuscle,
- error,
- onShuffle,
- onPick,
- onDelete,
- onAdd,
- shufflingExerciseId,
- }: ExercisesSelectionProps) => {
- const t = useI18n();
- const [flatExercises, setFlatExercises] = useState<{ id: string; muscle: string; exercise: ExerciseWithAttributes }[]>([]);
- const { setExercisesOrder, exercisesOrder } = useWorkoutStepper();
- const sensors = useSensors(
- useSensor(MouseSensor, {
- activationConstraint: {
- distance: 5,
- },
- }),
- useSensor(TouchSensor, {
- activationConstraint: {
- delay: 100,
- tolerance: 5,
- },
- }),
- );
- const sortableItems = useMemo(() => flatExercises.map((item) => item.id), [flatExercises]);
- const flatExercisesComputed = useMemo(() => {
- if (exercisesByMuscle.length === 0) return [];
- const flat = exercisesByMuscle.flatMap((group) =>
- group.exercises.map((exercise) => ({
- id: exercise.id,
- muscle: group.muscle,
- exercise,
- })),
- );
- if (exercisesOrder.length === 0) return flat;
- const exerciseMap = new Map(flat.map((item) => [item.id, item]));
- const orderedFlat = exercisesOrder.map((id) => exerciseMap.get(id)).filter(Boolean) as typeof flat;
- const newExercises = flat.filter((item) => !exercisesOrder.includes(item.id));
- return [...orderedFlat, ...newExercises];
- }, [exercisesByMuscle, exercisesOrder]);
- useEffect(() => {
- setFlatExercises(flatExercisesComputed);
- }, [flatExercisesComputed]);
- const handleDragEnd = useCallback(
- (event: DragEndEvent) => {
- const { active, over } = event;
- if (active.id !== over?.id) {
- setFlatExercises((items) => {
- const oldIndex = items.findIndex((item) => item.id === active.id);
- const newIndex = items.findIndex((item) => item.id === over?.id);
- const newOrder = arrayMove(items, oldIndex, newIndex);
- setExercisesOrder(newOrder.map((item) => item.id));
- return newOrder;
- });
- }
- },
- [setExercisesOrder],
- );
- if (isLoading) {
- return (
- <div className="space-y-6">
- <div className="text-center">
- <Loader2 className="h-8 w-8 animate-spin mx-auto text-blue-600" />
- <p className="mt-4 text-slate-600 dark:text-slate-400">{t("workout_builder.loading.exercises")}</p>
- </div>
- </div>
- );
- }
- return (
- <div className="space-y-6">
- {flatExercises.length > 0 ? (
- <div className="max-w-4xl mx-auto">
- {/* Liste des exercices drag and drop */}
- <DndContext collisionDetection={closestCenter} modifiers={[restrictToVerticalAxis]} onDragEnd={handleDragEnd} sensors={sensors}>
- <SortableContext items={sortableItems} strategy={verticalListSortingStrategy}>
- <div className="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 overflow-hidden">
- {flatExercises.map((item) => (
- <ExerciseListItem
- exercise={item.exercise}
- isShuffling={shufflingExerciseId === item.exercise.id}
- key={item.id}
- muscle={item.muscle}
- onDelete={onDelete}
- onPick={onPick}
- onShuffle={onShuffle}
- />
- ))}
- <div className="border-t border-slate-200 dark:border-slate-800">
- <button
- 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"
- onClick={onAdd}
- >
- <div className="h-8 w-8 rounded-full bg-blue-600 flex items-center justify-center">
- <Plus className="h-4 w-4 text-white" />
- </div>
- <span className="font-medium">{t("commons.add")}</span>
- </button>
- </div>
- </div>
- </SortableContext>
- </DndContext>
- </div>
- ) : error ? (
- <div className="text-center py-20">
- <p className="text-red-600 dark:text-red-400">{t("workout_builder.error.loading_exercises")}</p>
- </div>
- ) : (
- <div className="text-center py-20">
- <p className="text-slate-600 dark:text-slate-400">{t("workout_builder.no_exercises_found")}</p>
- </div>
- )}
- </div>
- );
- };
|