Implémentation des présentations - Détails techniques
Formulaires avec React Hook Form et validation Zod
// Intégration React Hook Form + Zod avec schémas partagésimport { createExerciseSchema } from '@dropit/schemas';
export function ExerciseCreationForm() { const form = useForm({ resolver: zodResolver(createExerciseSchema), // Validation Zod automatique mode: 'onChange', });
const { mutateAsync: createExercise } = useMutation({ mutationFn: (data) => api.exercise.createExercise({ body: data }), });
const onSubmit = async (data) => { await createExercise(data); toast.success('Exercice créé'); form.reset(); };
return ( <form onSubmit={form.handleSubmit(onSubmit)}> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Nom de l'exercice</FormLabel> <Input placeholder="Ex: Squat" {...field} /> <FormMessage /> </FormItem> )} />
<Button type="submit" disabled={form.formState.isSubmitting}> {form.formState.isSubmitting ? 'Création...' : 'Créer'} </Button> </form> );}Cette approche centralise la validation via les schémas Zod partagés et optimise l’expérience utilisateur avec une validation temps réel.
Synchronisation des données avec Tanstack Query
// Récupération et cache des catégories d'exercicesconst { data: exerciseCategories, isLoading } = useQuery({ queryKey: ['exercise-categories'], queryFn: async () => { const response = await api.exerciseCategory.getExerciseCategories(); if (response.status !== 200) throw new Error('Failed to load categories'); return response.body; }, staleTime: 1000 * 60 * 5, // Cache pendant 5 minutes retry: 3, // 3 tentatives en cas d'échec});
// Mutation avec invalidation automatique du cacheconst { mutateAsync: createExercise } = useMutation({ mutationFn: createExerciseRequest, onSuccess: () => { // Revalidation automatique des listes d'exercices queryClient.invalidateQueries({ queryKey: ['exercises'] }); toast.success('Exercice créé avec succès'); }, onError: (error) => { toast.error(`Erreur: ${error.message}`); },});Implémentation i18n
// Configuration du client i18nextimport { resources } from '@dropit/i18n';
// Usage dans les composantsconst CreateWorkoutButton = () => { const { t } = useTranslation('workout');
return ( <Button onClick={handleCreate}> {t('create.button')} // "Créer un programme" </Button> );};Exemple d’implémentation Tailwind
// Composants réutilisables avec Tailwindexport function WorkoutCard({ workout }) { return ( <Card className="group hover:shadow-lg transition-shadow duration-200"> <CardHeader> <CardTitle className="text-lg group-hover:text-dropit-primary-600 transition-colors"> {workout.name} </CardTitle> </CardHeader> <CardContent> <div className="flex items-center space-x-4 text-sm text-muted-foreground"> <Clock className="h-4 w-4" /> <span>{workout.estimatedDuration}min</span> </div> </CardContent> </Card> );}Exemple d’implémentation Shadcn/ui
// Import sélectif des composants Shadcn/uiimport { Button } from "@/components/ui/button"import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"import { Input } from "@/components/ui/input"
// Utilisation avec accessibilité intégréeexport function CreateExerciseDialog() { return ( <Dialog> <DialogTrigger asChild> <Button variant="outline"> <Plus className="h-4 w-4 mr-2" aria-hidden="true" /> Nouvel exercice </Button> </DialogTrigger>
<DialogContent> <DialogHeader> <DialogTitle>Créer un exercice</DialogTitle> </DialogHeader> <Input placeholder="Nom de l'exercice" aria-required="true" /> </DialogContent> </Dialog> );}Exemple d’implémentation Lucide React
// Import sélectif des icônes nécessairesimport { Plus, Dumbbell, TrendingUp, AlertTriangle } from "lucide-react"
// Usage simple avec accessibilité<Button variant="outline"> <Plus className="h-4 w-4 mr-2" aria-hidden="true" /> Nouvel exercice</Button>
// Icônes informatives avec label<div className="flex items-center gap-2"> <AlertTriangle className="h-5 w-5 text-amber-600" aria-label="Attention : exercice à risque élevé" /> <span>Épaulé-jeté 85% 1RM</span></div>
// États dynamiques<Button disabled={isLoading}> {isLoading ? ( <Loader2 className="h-4 w-4 mr-2 animate-spin" aria-hidden="true" /> ) : ( <Plus className="h-4 w-4 mr-2" aria-hidden="true" /> )} {isLoading ? "Création..." : "Créer"}</Button>Architecture mobile flux de données
Exemple d’implémentation React Native
// Même logique de validation côté mobileimport { createPerformanceSchema } from '@dropit/schemas';import { useForm } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';
export function PerformanceForm() { const form = useForm({ resolver: zodResolver(createPerformanceSchema), // Même validation que le web });
const { mutateAsync } = useMutation({ mutationFn: (data) => api.performance.create({ body: data }), // Même contrat API });
return ( <View style={styles.container}> <Text style={styles.label}>Performance</Text> <Controller control={form.control} name="weight" render={({ field }) => ( <TextInput style={styles.input} value={field.value?.toString()} onChangeText={(text) => field.onChange(parseFloat(text))} keyboardType="numeric" placeholder="Poids soulevé (kg)" /> )} /> {form.formState.errors.weight && ( <Text style={styles.error}> {form.formState.errors.weight.message} </Text> )} </View> );}Exemple d’optimisations React
Lazy loading et memoization
// Lazy loading des composants volumineuxconst WorkoutEditor = lazy(() => import('./workout-editor'));
// Memoization des calculs coûteuxconst calculatedWorkoutStats = useMemo(() => { return workoutElements.reduce((stats, element) => { stats.totalVolume += element.sets * element.reps * element.weight; return stats; }, { totalVolume: 0 });}, [workoutElements]);
// Debouncing des recherchesconst debouncedSearch = useDebounce(searchTerm, 300);Optimisation avec React.memo
// Composant optimisé avec React.memoexport const OptimizedWorkoutCard = memo(({ workout, onSelect }) => { const handleClick = useCallback(() => { onSelect(workout); }, [workout, onSelect]);
return ( <Card onClick={handleClick} className="cursor-pointer hover:shadow-md"> <CardHeader> <CardTitle>{workout.name}</CardTitle> </CardHeader> <CardContent> <Clock className="h-4 w-4" /> <span>{workout.estimatedDuration}min</span> </CardContent> </Card> );}, (prevProps, nextProps) => { // Comparaison personnalisée pour éviter les re-renders inutiles return prevProps.workout.id === nextProps.workout.id && prevProps.workout.updatedAt === nextProps.workout.updatedAt;});Optimisations debouncing
// Hook personnalisé pour le debouncingfunction useDebounce<T>(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => { // Configure un timer qui mettra à jour la valeur après le délai const handler = setTimeout(() => { setDebouncedValue(value); }, delay);
// Nettoie le timer si value change avant la fin du délai return () => { clearTimeout(handler); }; }, [value, delay]);
return debouncedValue;}
// Utilisation dans un composant de rechercheexport function ExerciseSearchInput() { const [searchTerm, setSearchTerm] = useState(''); const debouncedSearchTerm = useDebounce(searchTerm, 300); // 300ms de délai
// Déclenche la recherche uniquement quand debouncedSearchTerm change const { data: exercises } = useQuery({ queryKey: ['exercises', debouncedSearchTerm], queryFn: () => searchExercises(debouncedSearchTerm), enabled: debouncedSearchTerm.length > 2, // Recherche après 3 caractères });
return ( <div> <Input type="text" placeholder="Rechercher un exercice..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} // Mise à jour immédiate /> {exercises?.map(exercise => ( <div key={exercise.id}>{exercise.name}</div> ))} </div> );}