diff --git a/apps/web/src/features/workout/workout-card.tsx b/apps/web/src/features/workout/workout-card.tsx index c9a5dd8..f8b81de 100644 --- a/apps/web/src/features/workout/workout-card.tsx +++ b/apps/web/src/features/workout/workout-card.tsx @@ -5,11 +5,8 @@ import { CardContent, CardFooter, CardHeader, - CardTitle, } from '@/shared/components/ui/card'; import { WORKOUT_ELEMENT_TYPES, WorkoutDto, TrainingSessionDto } from '@dropit/schemas'; -import { getCategoryBadgeVariant } from '@/shared/utils'; -import { Separator } from '@/shared/components/ui/separator'; interface WorkoutCardProps { workout: WorkoutDto; @@ -17,76 +14,119 @@ interface WorkoutCardProps { onWorkoutClick: (id: string) => void; } +// Fonction pour obtenir la couleur pastel selon la catégorie (jaune ou rouge pâle) +const getCategoryColor = (category: string) => { + const colors: Record = { + Force: 'bg-red-50 text-red-700 border-red-200', + Technique: 'bg-yellow-50 text-yellow-700 border-yellow-200', + Endurance: 'bg-red-50 text-red-700 border-red-200', + Mobilité: 'bg-yellow-50 text-yellow-700 border-yellow-200', + Conditioning: 'bg-red-50 text-red-700 border-red-200', + }; + return colors[category] || 'bg-yellow-50 text-yellow-700 border-yellow-200'; +}; + export function WorkoutCard({ workout, trainingSessions, onWorkoutClick }: WorkoutCardProps) { return ( onWorkoutClick(workout.id)} + className="border border-gray-100 rounded-2xl" > - - {workout.title} - - {workout.workoutCategory || 'Sans catégorie'} - + +
+

+ {workout.title} +

+ {workout.workoutCategory && ( + + {workout.workoutCategory} + + )} +
- - {workout.elements.map((element) => { - const isExercise = element.type === WORKOUT_ELEMENT_TYPES.EXERCISE; - const categoryName = isExercise - ? element.exercise.exerciseCategory?.name - : element.complex.complexCategory?.name; - return ( -
-
- - {element.type.toUpperCase()} - - {categoryName && ( -
- - - {categoryName} - + +
+ {workout.elements.map((element) => { + const isExercise = element.type === WORKOUT_ELEMENT_TYPES.EXERCISE; + + return ( +
+ {/* Header avec type et sets/reps */} +
+ + {isExercise ? 'exercise' : 'complex'} + + + {element.sets}x{element.reps} {('intensity' in element && element.intensity) ? `${element.intensity}%` : ''} + +
+ + {/* Contenu */} + {isExercise ? ( +
+ {element.exercise.name} +
+ ) : ( +
+ {element.complex.exercises && element.complex.exercises.length > 0 && ( +
+ {element.complex.exercises.map((ex, idx) => ( +
+ {ex.name} +
+ ))} +
+ )}
)}
- {/* Contenu */} - {isExercise ? ( - - {element.exercise.name} - - ) : ( -
- {element.complex.exercises?.map((ex) => ( - - {ex.name} - - ))} -
- )} -
- ); - })} + ); + })} +
- -
- - {trainingSessions.length > 0 + + +
+ 0 ? 'text-emerald-600' : 'text-gray-400' + }`}> + {trainingSessions.length > 0 ? `${trainingSessions.length} session${trainingSessions.length > 1 ? 's' : ''} planifiée${trainingSessions.length > 1 ? 's' : ''}` : 'Non planifié' }
- +
- -
diff --git a/apps/web/src/features/workout/workout-grid.tsx b/apps/web/src/features/workout/workout-grid.tsx index dd6c84b..46060c8 100644 --- a/apps/web/src/features/workout/workout-grid.tsx +++ b/apps/web/src/features/workout/workout-grid.tsx @@ -9,12 +9,12 @@ interface WorkoutGridProps { export function WorkoutGrid({ workouts, trainingSessions, onWorkoutClick }: WorkoutGridProps) { return ( -
+
{workouts.map((workout) => { const workoutSessions = trainingSessions.filter( session => session.workout.id === workout.id ); - + return ( {/* Main Content */} -
+
{t('common:no_results')}
; return ( -
+
+
Hello "/__home/help"!
+ return
Hello "/__home/help"!
} diff --git a/apps/web/src/routes/__home.library.complex.tsx b/apps/web/src/routes/__home.library.complex.tsx index ebbdc46..ea1704f 100644 --- a/apps/web/src/routes/__home.library.complex.tsx +++ b/apps/web/src/routes/__home.library.complex.tsx @@ -1,5 +1,6 @@ import { api } from '@/lib/api' import { DetailsPanel } from '@/shared/components/ui/details-panel' +import { Spinner } from '@/shared/components/ui/spinner' import { useTranslation } from '@dropit/i18n' import { useQuery, useQueryClient } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' @@ -9,6 +10,7 @@ import { ComplexDetail } from '../features/complex/complex-detail' import { ComplexFilters } from '../features/complex/complex-filters' import { ComplexGrid } from '../features/complex/complex-grid' import { DialogCreation } from '../features/exercises/dialog-creation' +import { HeaderPage } from '../shared/components/layout/header-page' export const Route = createFileRoute('/__home/library/complex')({ component: ComplexPage, @@ -40,7 +42,7 @@ function ComplexPage() { }, }) - const { data: complexDetails } = useQuery({ + const { data: complexDetails, isLoading: complexDetailsLoading } = useQuery({ queryKey: ['complex', selectedComplex], queryFn: async () => { if (!selectedComplex) return null @@ -69,35 +71,38 @@ function ComplexPage() { } return ( -
-
- setCreateComplexModalOpen(true)} - categories={categories} - disabled={isLoading || !complexes?.length} +
+
+ - {isLoading ? ( -
- {t('common.loading')} -
- ) : !complexes?.length ? ( -
-

{t('complex.filters.no_results')}

-

{t('common.start_create')}

-
- ) : ( - setSelectedComplex(complexId)} +
+ setCreateComplexModalOpen(true)} + categories={categories} + disabled={isLoading || !complexes?.length} /> - )} + + {isLoading ? ( +
+ {t('common.loading')} +
+ ) : !complexes?.length ? ( +
+

{t('complex.filters.no_results')}

+

{t('common.start_create')}

+
+ ) : ( + setSelectedComplex(complexId)} + /> + )} +
setSelectedComplex(null)} title={t('complex.details.title')} > - {complexDetails && } + {complexDetailsLoading ? ( +
+ +
+ ) : complexDetails ? ( + + ) : null}
{ if (!selectedExercise) return null @@ -53,32 +55,35 @@ function ExercisesPage() { if (!exercises) return
{t('exercise.filters.no_results')}
return ( -
-
- {exercisesLoading ? ( -
- {t('common.loading')} -
- ) : !exercises?.length ? ( -
-

{t('exercise.filters.no_results')}

-

{t('common.start_create')}

- -
- ) : ( - setSelectedExercise(exerciseId)} - /> - )} +
+
+ + +
+ {exercisesLoading ? ( +
+ {t('common.loading')} +
+ ) : !exercises?.length ? ( +
+

{t('exercise.filters.no_results')}

+

{t('common.start_create')}

+ +
+ ) : ( + setSelectedExercise(exerciseId)} + /> + )} +
setSelectedExercise(null)} title={t('exercise.details.title')} > - {exerciseDetails && } + {exerciseDetailsLoading ? ( +
+ +
+ ) : exerciseDetails ? ( + + ) : null}
- -
- -
- -
- -
- - ); + return ; } diff --git a/apps/web/src/routes/__home.library.workouts.tsx b/apps/web/src/routes/__home.library.workouts.tsx index d9e50e0..0713e14 100644 --- a/apps/web/src/routes/__home.library.workouts.tsx +++ b/apps/web/src/routes/__home.library.workouts.tsx @@ -5,6 +5,7 @@ import { useTranslation } from '@dropit/i18n'; import { useQuery } from '@tanstack/react-query'; import { Outlet, createFileRoute, useMatches } from '@tanstack/react-router'; import { useState } from 'react'; +import { HeaderPage } from '../shared/components/layout/header-page'; export const Route = createFileRoute('/__home/library/workouts')({ component: WorkoutPage, @@ -72,8 +73,13 @@ function WorkoutPage() { // Sinon on affiche la grille des workouts return ( -
-
+
+ + +
-
- {isLoading ? ( -
- {t('common.loading')} -
- ) : !workouts?.length ? ( -
-

{t('workout.filters.no_results')}

-

{t('common.start_create')}

-
- ) : ( -
-
- -
-
- )} -
+ {isLoading ? ( +
+ {t('common.loading')} +
+ ) : !workouts?.length ? ( +
+

{t('workout.filters.no_results')}

+

{t('common.start_create')}

+
+ ) : ( + + )}
); } diff --git a/apps/web/src/routes/__home.planning.tsx b/apps/web/src/routes/__home.planning.tsx index bf710cd..8516038 100644 --- a/apps/web/src/routes/__home.planning.tsx +++ b/apps/web/src/routes/__home.planning.tsx @@ -150,7 +150,7 @@ function PlanningPage() { if (calendarEventsLoading) return
{t('common:loading')}
; return ( -
+
{ - try { - // Appeler directement l'API pour se déconnecter - // Avec credentials: 'include', les cookies seront automatiquement envoyés - await authClient.signOut(); - - // Rediriger vers la page de connexion - toast({ - title: 'Logout successful', - description: 'You have been logged out successfully', - }); - - navigate({ to: '/', replace: true }); - } catch (error) { - console.error('Erreur lors de la déconnexion:', error); - - toast({ - title: 'Logout issue', - description: - 'You have been logged out but there was an issue contacting the server', - variant: 'destructive', - }); - - navigate({ to: '/', replace: true }); - } - }; - - return <> -
Hello
- - - + return ( +
+
Profile page
+
+ ) } diff --git a/apps/web/src/routes/__home.settings.tsx b/apps/web/src/routes/__home.settings.tsx index 9aa8ff0..07bb41d 100644 --- a/apps/web/src/routes/__home.settings.tsx +++ b/apps/web/src/routes/__home.settings.tsx @@ -5,5 +5,5 @@ export const Route = createFileRoute('/__home/settings')({ }) function RouteComponent() { - return
Hello "/__home/settings"!
+ return
Hello "/__home/settings"!
} diff --git a/apps/web/src/routes/__home.tsx b/apps/web/src/routes/__home.tsx index 614820d..ff1e456 100644 --- a/apps/web/src/routes/__home.tsx +++ b/apps/web/src/routes/__home.tsx @@ -1,8 +1,8 @@ -import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'; +import { Outlet, createFileRoute, redirect, useMatches } from '@tanstack/react-router'; import { AppSidebar } from '../shared/components/layout/app-sidebar'; -//import { Breadcrumbs } from '../shared/components/layout/breadcrumbs'; -import { SidebarProvider } from '../shared/components/ui/sidebar'; +import { AppHeader } from '../shared/components/layout/app-header'; import { authClient } from '../lib/auth-client'; +import { useTranslation } from '@dropit/i18n'; export const Route = createFileRoute('/__home')({ beforeLoad: async () => { @@ -20,12 +20,12 @@ export const Route = createFileRoute('/__home')({ // Vérifier le rôle de l'utilisateur - seuls les coachs peuvent accéder au dashboard const userRole = activeMember.data.role; - + // Si c'est un membre (athlète), rediriger vers download-app if (userRole === 'member') { throw redirect({ to: '/download-app' }); } - + // Les owner et admin peuvent continuer if (userRole !== 'owner' && userRole !== 'admin') { throw redirect({ to: '/download-app' }); @@ -39,20 +39,36 @@ export const Route = createFileRoute('/__home')({ }); function HomeLayout() { + const matches = useMatches(); + const { t } = useTranslation(); + const currentPath = matches[matches.length - 1]?.pathname || ''; + + // Définir les tabs selon la route active + const getTabs = () => { + if (currentPath.startsWith('/library')) { + return [ + { label: t('library.tabs.workouts'), path: '/library/workouts' }, + { label: t('library.tabs.complex'), path: '/library/complex' }, + { label: t('library.tabs.exercises'), path: '/library/exercises' }, + ]; + } + // On peut ajouter d'autres conditions pour d'autres sections avec tabs + return undefined; + }; + return ( - -
+
+ - +
+ -
-
-
- -
+
+
+
-
-
- +
+ +
); } diff --git a/apps/web/src/shared/components/layout/app-header.tsx b/apps/web/src/shared/components/layout/app-header.tsx new file mode 100644 index 0000000..8bd6bcd --- /dev/null +++ b/apps/web/src/shared/components/layout/app-header.tsx @@ -0,0 +1,101 @@ +import { Link, useMatches, useRouter } from '@tanstack/react-router'; +import { ChevronLeft, Bell } from 'lucide-react'; +import { authClient } from '@/lib/auth-client'; +import { Avatar, AvatarFallback } from '@/shared/components/ui/avatar'; +import { Button } from '@/shared/components/ui/button'; + +interface Tab { + label: string; + path: string; +} + +interface AppHeaderProps { + tabs?: Tab[]; + showBackButton?: boolean; + onBackClick?: () => void; +} + +export function AppHeader({ tabs, showBackButton = false, onBackClick }: AppHeaderProps) { + const matches = useMatches(); + const router = useRouter(); + const { data: session } = authClient.useSession(); + + // Function to get user initials from name + const getUserInitials = (name?: string) => { + if (!name) return 'U'; + const names = name.trim().split(' '); + if (names.length === 1) { + return names[0].charAt(0).toUpperCase(); + } + return names[0].charAt(0).toUpperCase() + names[names.length - 1].charAt(0).toUpperCase(); + }; + + const handleBackClick = () => { + if (onBackClick) { + onBackClick(); + } else { + router.history.back(); + } + }; + + const currentPath = matches[matches.length - 1]?.pathname || ''; + + return ( +
+ {/* Left side: Back button and Tabs */} +
+ {showBackButton && ( + + )} + + {tabs && tabs.length > 0 && ( + + )} +
+ + {/* Right side: Notifications and User Menu */} +
+ {/* Notifications */} + + + {/* User Profile */} + + + +
+
+ ); +} diff --git a/apps/web/src/shared/components/layout/app-sidebar.tsx b/apps/web/src/shared/components/layout/app-sidebar.tsx index c8118ec..e7ce2a0 100644 --- a/apps/web/src/shared/components/layout/app-sidebar.tsx +++ b/apps/web/src/shared/components/layout/app-sidebar.tsx @@ -1,56 +1,52 @@ import { authClient } from '@/lib/auth-client'; -import { Avatar, AvatarFallback } from '@/shared/components/ui/avatar'; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupContent, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarSeparator, - SidebarTrigger, - useSidebar, -} from '@/shared/components/ui/sidebar'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/shared/components/ui/tooltip'; +import { useToast } from '@/shared/hooks/use-toast'; import { useTranslation } from '@dropit/i18n'; import { Link, useMatches, useNavigate } from '@tanstack/react-router'; import { BicepsFlexed, Calendar, - ChevronRight, GraduationCap, Home, LayoutDashboard, CircleQuestionMark, Settings, + LogOut, } from 'lucide-react'; export function AppSidebar() { const { t } = useTranslation(); const navigate = useNavigate(); const matches = useMatches(); - const { data: session } = authClient.useSession(); - const { state } = useSidebar(); + const { toast } = useToast(); + + const handleLogout = async () => { + try { + // Appeler directement l'API pour se déconnecter + // Avec credentials: 'include', les cookies seront automatiquement envoyés + await authClient.signOut(); + + // Rediriger vers la page de connexion + toast({ + title: 'Logout successful', + description: 'You have been logged out successfully', + }); - // Function to get user initials from name - const getUserInitials = (name?: string) => { - if (!name) return 'U'; - const names = name.trim().split(' '); - if (names.length === 1) { - return names[0].charAt(0).toUpperCase(); + navigate({ to: '/', replace: true }); + } catch (error) { + console.error('Erreur lors de la déconnexion:', error); + + toast({ + title: 'Logout issue', + description: + 'You have been logged out but there was an issue contacting the server', + variant: 'destructive', + }); + + navigate({ to: '/', replace: true }); } - return names[0].charAt(0).toUpperCase() + names[names.length - 1].charAt(0).toUpperCase(); }; - const mainItems = [ + const items = [ { title: t('sidebar.menu.dashboard'), url: '/dashboard', @@ -71,9 +67,6 @@ export function AppSidebar() { url: '/athletes', icon: GraduationCap, }, - ]; - - const footerItems = [ { title: t('sidebar.menu.help'), url: '/help', @@ -107,149 +100,53 @@ export function AppSidebar() { }; return ( - - - - - - - - - Dropit - - - - - - - - - - - {mainItems.map((item) => { - const isActive = isActiveItem(item.url); - const menuButton = ( - - - - - {item.title} - - - - ); + ); } diff --git a/apps/web/src/shared/components/ui/button.tsx b/apps/web/src/shared/components/ui/button.tsx index 65d4fcd..4ebe75f 100644 --- a/apps/web/src/shared/components/ui/button.tsx +++ b/apps/web/src/shared/components/ui/button.tsx @@ -10,7 +10,7 @@ const buttonVariants = cva( variants: { variant: { default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", + "bg-slate-700 text-primary-foreground shadow hover:bg-slate-800", destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", outline: diff --git a/apps/web/src/shared/components/ui/details-panel.tsx b/apps/web/src/shared/components/ui/details-panel.tsx index d13d5fb..deacd1b 100644 --- a/apps/web/src/shared/components/ui/details-panel.tsx +++ b/apps/web/src/shared/components/ui/details-panel.tsx @@ -17,49 +17,46 @@ export function DetailsPanel({ children, className, }: DetailsPanelProps) { + if (!open) return null; + return ( <> {/* Mobile overlay */} - {open && ( -
{ - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - onClose?.(); - } - }} - role="button" - tabIndex={0} - aria-label="Fermer le panneau" - /> - )} - - {/* Panel */} +
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClose?.(); + } + }} + role="button" + tabIndex={0} + aria-label="Fermer le panneau" + /> +
-
-
-

{title}

- -
+
+
+

{title}

+ +
-
{children}
+
{children}
+
-
); } diff --git a/apps/web/src/shared/components/ui/spinner.tsx b/apps/web/src/shared/components/ui/spinner.tsx new file mode 100644 index 0000000..3781ce1 --- /dev/null +++ b/apps/web/src/shared/components/ui/spinner.tsx @@ -0,0 +1,16 @@ +import { Loader2Icon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Spinner({ className, ...props }: React.ComponentPropsWithoutRef<"svg">) { + return ( + + ) +} + +export { Spinner } diff --git a/packages/i18n/src/locales/en/common.json b/packages/i18n/src/locales/en/common.json index 5d9b189..5353cba 100644 --- a/packages/i18n/src/locales/en/common.json +++ b/packages/i18n/src/locales/en/common.json @@ -30,7 +30,7 @@ }, "sidebar": { "menu": { - "dashboard": "Dashboard", + "dashboard": "Home", "library": "Library", "calendar": "Calendar", "athletes": "Athletes", diff --git a/packages/i18n/src/locales/fr/common.json b/packages/i18n/src/locales/fr/common.json index 8435a96..ed5f3ce 100644 --- a/packages/i18n/src/locales/fr/common.json +++ b/packages/i18n/src/locales/fr/common.json @@ -30,7 +30,7 @@ }, "sidebar": { "menu": { - "dashboard": "Tableau de bord", + "dashboard": "Accueil", "library": "Bibliothèque", "calendar": "Calendrier", "athletes": "Athlètes",