diff --git a/apps/api/src/seeders/complex.seeder.ts b/apps/api/src/seeders/complex.seeder.ts index 2ec4161..ee02c63 100644 --- a/apps/api/src/seeders/complex.seeder.ts +++ b/apps/api/src/seeders/complex.seeder.ts @@ -12,20 +12,16 @@ export async function seedComplexes( const complexCategories = [ { - name: 'EMOM', - description: 'Exercices à exécuter toutes les minutes', - }, - { - name: 'Technique Arraché', + name: 'Arraché', description: "Exercices focalisés sur la technique de l'arraché", }, { - name: 'Technique Épaulé-Jeté', + name: 'Épaulé', description: "Exercices focalisés sur la technique de l'épaulé-jeté", }, { - name: 'TABATA', - description: 'Exercices en intervalles courts (20s effort / 10s repos)', + name: 'Renforcement', + description: 'Exercices de musculation spécifiques', }, ]; @@ -39,9 +35,13 @@ export async function seedComplexes( console.log('Complex category created:', categoryToCreate); } + const ARRACHE_CATEGORY_INDEX = 0; + const EPAULE_CATEGORY_INDEX = 1; + const RENFORCEMENT_CATEGORY_INDEX = 2; + const complexesToCreate = [ { - category: 'EMOM', + category: complexCategories[ARRACHE_CATEGORY_INDEX].name, description: "Focus sur la technique de l'arraché", exercises: [ { @@ -59,8 +59,8 @@ export async function seedComplexes( ], }, { - category: 'Technique Épaulé-Jeté', - description: "Focus sur la technique de l'épaulé-jeté", + category: complexCategories[EPAULE_CATEGORY_INDEX].name, + description: "EMOM", exercises: [ { name: 'Épaulé Debout', @@ -81,8 +81,8 @@ export async function seedComplexes( ], }, { - category: 'TABATA', - description: 'Focus sur la force', + category: complexCategories[RENFORCEMENT_CATEGORY_INDEX].name, + description: 'On le fait en TABATA', exercises: [ { name: 'Squat Nuque', @@ -99,8 +99,8 @@ export async function seedComplexes( ], }, { - category: 'Technique Arraché', - description: "Focus sur la technique de l'arraché", + category: complexCategories[ARRACHE_CATEGORY_INDEX].name, + description: "Focus sur la technique de l'arraché, EMOM", exercises: [ { name: 'Arraché Debout', @@ -117,8 +117,8 @@ export async function seedComplexes( ], }, { - category: 'EMOM', - description: "Focus sur l'épaulé", + category: complexCategories[EPAULE_CATEGORY_INDEX].name, + description: "On se concentre sur la technique", exercises: [ { name: 'Épaulé Debout', diff --git a/apps/api/src/seeders/exercise.seeder.ts b/apps/api/src/seeders/exercise.seeder.ts index 531a880..a308ade 100644 --- a/apps/api/src/seeders/exercise.seeder.ts +++ b/apps/api/src/seeders/exercise.seeder.ts @@ -36,7 +36,7 @@ export async function seedExerciseCategories( export async function seedExercises( em: EntityManager ): Promise> { - const types = ['Haltérophilie', 'Endurance', 'Cardio', 'Musculation']; + const types = ['Technique', 'Endurance', 'Cardio', 'Renforcement']; const categories: Record = {}; for (const exerciseCategory of types) { @@ -52,79 +52,79 @@ export async function seedExercises( const exercises = [ { name: 'Squat Clavicule', - category: 'Haltérophilie', + category: 'Technique', englishName: 'Front Squat', shortName: 'Squat Clav', }, { name: 'Épaulé Debout', - category: 'Haltérophilie', + category: 'Technique', englishName: 'Power Clean', shortName: 'PC', }, { name: 'Arraché Debout', - category: 'Haltérophilie', + category: 'Technique', englishName: 'Power Snatch', shortName: 'PS', }, { name: 'Jeté Fente', - category: 'Haltérophilie', + category: 'Technique', englishName: 'Split Jerk', shortName: 'SJ', }, { name: 'Arraché', - category: 'Haltérophilie', + category: 'Technique', englishName: 'snatch', shortName: 'SN', }, { name: 'Squat Nuque', - category: 'Haltérophilie', + category: 'Technique', englishName: 'Back Squat', shortName: 'BS', }, { name: 'Tirage Nuque', - category: 'Haltérophilie', + category: 'Technique', englishName: 'Snatch Pull', shortName: 'SP', }, { name: 'Développé Militaire', - category: 'Musculation', + category: 'Renforcement', englishName: 'Military Press', shortName: 'MP', }, { name: 'Soulevé de Terre', - category: 'Musculation', + category: 'Renforcement', englishName: 'Deadlift', shortName: 'DL', }, { name: 'Tirage Menton', - category: 'Musculation', + category: 'Renforcement', englishName: 'Upright Row', shortName: 'UR', }, { name: 'Développé Couché', - category: 'Musculation', + category: 'Renforcement', englishName: 'Bench Press', shortName: 'BP', }, { name: 'Épaulé-Jeté', - category: 'Haltérophilie', + category: 'Technique', englishName: 'cleanAndJerk', shortName: 'C&J', }, { name: 'Tirage Planche', - category: 'Musculation', + category: 'Renforcement', englishName: 'Bent Over Row', shortName: 'BOR', }, diff --git a/apps/web/src/features/athletes/athlete-detail.tsx b/apps/web/src/features/athletes/athlete-detail.tsx index dfe9d49..6ece2ae 100644 --- a/apps/web/src/features/athletes/athlete-detail.tsx +++ b/apps/web/src/features/athletes/athlete-detail.tsx @@ -86,7 +86,7 @@ export function AthleteDetail({
{/* Profile Header with Avatar */}
- + [] = [ { id: 'select', header: ({ table }) => ( +
[] = [ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" /> +
), cell: ({ row }) => ( +
row.toggleSelected(!!value)} aria-label="Select row" /> +
), enableSorting: false, enableHiding: false, @@ -35,7 +39,7 @@ export const columns: ColumnDef[] = [ id: 'name', header: () => { const { t } = useTranslation(['athletes']); - return t('athletes.columns.name'); + return t('columns.name'); }, cell: ({ row }) => { const firstName = row.original.firstName; @@ -45,7 +49,7 @@ export const columns: ColumnDef[] = [ return (
- + {`${firstName[0]}${lastName[0]}`} diff --git a/apps/web/src/features/athletes/data-table.tsx b/apps/web/src/features/athletes/data-table.tsx index ce25d88..512a66b 100644 --- a/apps/web/src/features/athletes/data-table.tsx +++ b/apps/web/src/features/athletes/data-table.tsx @@ -36,7 +36,7 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; import { useState } from 'react'; interface DataTableProps { @@ -104,53 +104,58 @@ export function DataTable({ return (
+ {/* Filters */}
-
- setGlobalFilter(event.target.value)} - className="max-w-sm" - /> -
-
- - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - - - +
+
+ + setGlobalFilter(event.target.value)} + className="pl-8 bg-sidebar max-w-lg" + /> +
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + + + +
+ {/* Table */}
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { return ( @@ -198,6 +203,7 @@ export function DataTable({
+ {/* Pagination */}
diff --git a/apps/web/src/features/complex/complex-card.tsx b/apps/web/src/features/complex/complex-card.tsx index 3377fa7..3183450 100644 --- a/apps/web/src/features/complex/complex-card.tsx +++ b/apps/web/src/features/complex/complex-card.tsx @@ -15,21 +15,23 @@ import { } from '@/shared/components/ui/dropdown-menu'; import { ComplexDto } from '@dropit/schemas'; import { MoreHorizontal } from 'lucide-react'; +import { getCategoryBadgeVariant } from '@/shared/utils'; interface ComplexCardProps { complex: ComplexDto; onClick?: () => void; } + export function ComplexCard({ complex, onClick }: ComplexCardProps) { return ( - {complex.complexCategory?.name || 'Sans catégorie'} + {complex.exercises?.map(ex => ex.name).join(', ') || 'Aucun exercice'} @@ -51,7 +53,9 @@ export function ComplexCard({ complex, onClick }: ComplexCardProps) { {complex.description || 'Pas de description'}
- + {complex.complexCategory?.name || 'Sans catégorie'} diff --git a/apps/web/src/features/complex/complex-detail.tsx b/apps/web/src/features/complex/complex-detail.tsx index 3704e6e..02f0bc8 100644 --- a/apps/web/src/features/complex/complex-detail.tsx +++ b/apps/web/src/features/complex/complex-detail.tsx @@ -45,13 +45,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { fr } from 'date-fns/locale'; -import { GripVertical, PlusCircle, Trash2, Edit } from 'lucide-react'; +import { GripVertical, PlusCircle, Trash2 } from 'lucide-react'; import { useState } from 'react'; import { UseFormReturn, useFieldArray, useForm } from 'react-hook-form'; import { z } from 'zod'; import { DialogCreation } from '../exercises/dialog-creation'; import { ExerciseCreationForm } from '../exercises/exercise-creation-form'; import { ComplexCategoryCreationForm } from './complex-category-creation-form'; +import { getCategoryBadgeVariant } from '@/shared/utils'; interface ComplexDetailProps { complex: ComplexDto; @@ -511,28 +512,11 @@ export function ComplexDetail({ complex }: ComplexDetailProps) { {/* Informations principales */} -
-
-
-

- Complex -

-

- {complex.complexCategory?.name || 'Sans catégorie'} -

-
- -
- -
+

{complex.description || 'Pas de description'}

-
@@ -540,9 +524,13 @@ export function ComplexDetail({ complex }: ComplexDetailProps) { {/* Catégorie dans une Card séparée */} -
+
- {complex.complexCategory.name} + + {complex.complexCategory.name} +
diff --git a/apps/web/src/features/complex/complex-filters.tsx b/apps/web/src/features/complex/complex-filters.tsx index 3fef10e..f912589 100644 --- a/apps/web/src/features/complex/complex-filters.tsx +++ b/apps/web/src/features/complex/complex-filters.tsx @@ -10,6 +10,7 @@ import { import { Separator } from '@/shared/components/ui/separator'; import { ComplexCategoryDto } from '@dropit/schemas'; import { useTranslation } from '@dropit/i18n'; +import { Search } from 'lucide-react'; interface ComplexFiltersProps { onFilterChange: (value: string) => void; @@ -30,30 +31,35 @@ export function ComplexFilters({ return (
-
- onFilterChange(e.target.value)} - className="max-w-sm" - disabled={disabled} - /> -
-
- - - +
+ {/* Search */} +
+ + onFilterChange(e.target.value)} + disabled={disabled} + className="pl-8 bg-sidebar max-w-lg" + /> +
+ {/* Filters */} +
+ + + +
); diff --git a/apps/web/src/features/exercises/columns.tsx b/apps/web/src/features/exercises/columns.tsx index b47c5a0..3d760e0 100644 --- a/apps/web/src/features/exercises/columns.tsx +++ b/apps/web/src/features/exercises/columns.tsx @@ -7,10 +7,12 @@ import { DropdownMenuLabel, DropdownMenuTrigger, } from '@/shared/components/ui/dropdown-menu'; +import { Badge } from '@/shared/components/ui/badge'; import { ExerciseDto } from '@dropit/schemas'; import { ColumnDef } from '@tanstack/react-table'; import { MoreHorizontal } from 'lucide-react'; import { ArrowUpDown } from 'lucide-react'; +import { getCategoryBadgeVariant } from '@/shared/utils'; type Exercise = ExerciseDto; @@ -18,21 +20,25 @@ export const columns: ColumnDef[] = [ { id: 'select', header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - /> +
+ table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> +
), cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - /> +
+ row.toggleSelected(!!value)} + aria-label="Select row" + /> +
), enableSorting: false, enableHiding: false, @@ -44,6 +50,7 @@ export const columns: ColumnDef[] = [ ); }, - }, - { - accessorKey: 'shortName', - header: 'Abréviation', - cell: ({ row }) => row.getValue('shortName') || '—', - }, - { - accessorKey: 'englishName', - header: 'Nom Anglais', - cell: ({ row }) => row.getValue('englishName') || '—', + cell: ({ row }) => { + const categoryName = row.original.exerciseCategory?.name || '—'; + + const badgeVariant = getCategoryBadgeVariant(categoryName); + + return ( + + {categoryName} + + ); + }, }, { id: 'actions', - cell: ({ row }) => { - const exercise = row.original; + cell: () => { return ( - - - - - - Actions - navigator.clipboard.writeText(exercise.id)} - > - Copier l'ID - - Voir les détails - Modifier - Supprimer - - +
+ + + + + + Actions + Voir les détails + Modifier + Supprimer + + +
); }, }, diff --git a/apps/web/src/features/exercises/data-table.tsx b/apps/web/src/features/exercises/data-table.tsx index 8684b86..304a119 100644 --- a/apps/web/src/features/exercises/data-table.tsx +++ b/apps/web/src/features/exercises/data-table.tsx @@ -28,7 +28,7 @@ import { getSortedRowModel, useReactTable, } from '@tanstack/react-table'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; import { useState } from 'react'; interface DataTableProps { @@ -70,55 +70,60 @@ export function DataTable({ return (
+ {/* Filters */}
-
- - table.getColumn('name')?.setFilterValue(event.target.value) - } - className="max-w-sm" - /> -
-
- - - - - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - - column.toggleVisibility(!!value) - } - > - {column.id} - - ); - })} - - - - +
+
+ + + table.getColumn('name')?.setFilterValue(event.target.value) + } + className="pl-8 bg-sidebar max-w-lg" + /> +
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + + + +
+ {/* Table */}
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { return ( @@ -166,6 +171,7 @@ export function DataTable({
+ {/* Pagination */}
{t('exercise.table.selected_rows', { diff --git a/apps/web/src/features/exercises/exercise-detail.tsx b/apps/web/src/features/exercises/exercise-detail.tsx index e49c0f7..a20d766 100644 --- a/apps/web/src/features/exercises/exercise-detail.tsx +++ b/apps/web/src/features/exercises/exercise-detail.tsx @@ -29,6 +29,8 @@ import { fr } from 'date-fns/locale'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { Badge } from '@/shared/components/ui/badge'; +import { getCategoryBadgeVariant } from '@/shared/utils'; interface ExerciseDetailProps { exercise: { @@ -308,9 +310,13 @@ export function ExerciseDetail({ exercise }: ExerciseDetailProps) {
-
+
-

{exercise.exerciseCategory.name}

+ + {exercise.exerciseCategory.name} +
diff --git a/apps/web/src/features/planning/planning-calendar.tsx b/apps/web/src/features/planning/planning-calendar.tsx index 463ca61..08fb792 100644 --- a/apps/web/src/features/planning/planning-calendar.tsx +++ b/apps/web/src/features/planning/planning-calendar.tsx @@ -1,4 +1,6 @@ import { cn } from '@/lib/utils'; +import { Button } from '@/shared/components/ui/button'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select'; import { useToast } from '@/shared/hooks/use-toast'; import { useTranslation } from '@dropit/i18n'; import { TrainingSessionDto } from '@dropit/schemas'; @@ -9,7 +11,8 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin, { DateClickArg } from '@fullcalendar/interaction'; import multiMonthPlugin from '@fullcalendar/multimonth'; import FullCalendar from '@fullcalendar/react'; -import { useState } from 'react'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { useRef, useState } from 'react'; interface EventDropInfo { event: EventApi; @@ -36,6 +39,8 @@ export function PlanningCalendar({ const { t, i18n } = useTranslation('planning'); const { toast } = useToast(); const [events, setEvents] = useState(initialEvents); + const [currentView, setCurrentView] = useState('dayGridMonth'); + const calendarRef = useRef(null); const currentLocale = i18n.language === 'fr' ? frLocale : enLocale; const handleEventClick = (info: EventClickArg) => { @@ -75,33 +80,131 @@ export function PlanningCalendar({ } }; + const handlePrev = () => { + const calendarApi = calendarRef.current?.getApi(); + if (calendarApi) { + calendarApi.prev(); + } + }; + + const handleNext = () => { + const calendarApi = calendarRef.current?.getApi(); + if (calendarApi) { + calendarApi.next(); + } + }; + + const handleToday = () => { + const calendarApi = calendarRef.current?.getApi(); + if (calendarApi) { + calendarApi.today(); + } + }; + + const handleViewChange = (newView: string) => { + const calendarApi = calendarRef.current?.getApi(); + if (calendarApi) { + calendarApi.changeView(newView); + setCurrentView(newView); + } + }; + + const getViewLabel = (view: string) => { + switch (view) { + case 'dayGridMonth': + return t('month'); + case 'dayGridWeek': + return t('week'); + case 'multiMonthYear': + return t('year'); + default: + return t('month'); + } + }; + return ( -
- ({ - id: event.id, - title: event.workout.title, - start: event.scheduledDate, - end: event.scheduledDate, - }))} - eventClick={handleEventClick} - dateClick={handleDateClick} - eventDrop={handleEventDrop} - height="75vh" - /> +
+ {/* Custom Toolbar */} +
+ {/* Navigation Controls */} +
+ + + + + +
+ + {/* View Selector */} +
+ +
+
+ + {/* Calendar */} +
+ ({ + id: event.id, + title: event.workout.title, + start: event.scheduledDate, + end: event.scheduledDate, + }))} + eventClick={handleEventClick} + dateClick={handleDateClick} + eventDrop={handleEventDrop} + height="75vh" + /> +
); } diff --git a/apps/web/src/features/workout/steps/workout-elements-step.tsx b/apps/web/src/features/workout/steps/workout-elements-step.tsx index c5dc7ce..7aa2fa9 100644 --- a/apps/web/src/features/workout/steps/workout-elements-step.tsx +++ b/apps/web/src/features/workout/steps/workout-elements-step.tsx @@ -184,7 +184,7 @@ export function WorkoutElementsStep({
{activeTab === 'exercise' && (
-
+
Créer un nouvel exercice @@ -241,7 +241,7 @@ export function WorkoutElementsStep({ {activeTab === 'complex' && (
-
+
Créer un nouveau complexe @@ -311,9 +311,6 @@ export function WorkoutElementsStep({
- {fields.length > 0 && ( -
- )} {fields.length === 0 ? (
@@ -323,7 +320,7 @@ export function WorkoutElementsStep({
) : ( -
+
{fields.map((field, index) => (
+ key={field.id} + className="w-[49%]" + > { @@ -53,71 +60,184 @@ export function WorkoutInfoStep({ }, }); + const { data: workouts } = useQuery({ + queryKey: ['workouts'], + queryFn: async () => { + const response = await api.workout.getWorkouts(); + if (response.status !== 200) throw new Error('Failed to load workouts'); + return response.body; + }, + }); + + const handleUseTemplate = (templateWorkout: WorkoutDto) => { + form.setValue('title', `${templateWorkout.title} (copie)`); + form.setValue('description', templateWorkout.description || ''); + + // Trouver la catégorie correspondante par son nom pour obtenir l'ID + const category = categories?.find(cat => cat.name === templateWorkout.workoutCategory); + if (category) { + form.setValue('workoutCategory', category.id); + } + + if (templateWorkout.elements) { + form.setValue('elements', templateWorkout.elements); + } + }; + + const filteredWorkouts = workouts?.filter(workout => + workout.title.toLowerCase().includes(templateSearch.toLowerCase()) + ) || []; + return ( -
-
-

Informations générales

-
- ( - - Nom - - - - - - )} - /> +
+ {/* Layout principal : 2 colonnes */} +
+ + {/* Colonne gauche : Formulaire d'informations */} +
+
+

Informations générales

+

+ Renseignez les détails de votre entraînement +

+
- ( - - Description - -