diff --git a/src/features/simulations/components/SimulationForm.tsx b/src/features/simulations/components/SimulationForm.tsx index 60f7fe1..1be8b3c 100644 --- a/src/features/simulations/components/SimulationForm.tsx +++ b/src/features/simulations/components/SimulationForm.tsx @@ -3,18 +3,26 @@ * * SEC-04 : validation Zod avant envoi (texte non vide, max 5 000 caractères). * SEC-05 : aucun dangerouslySetInnerHTML — le texte utilisateur est rendu comme texte. - * Règle H : aucune logique métier — le composant reçoit tache, handlers et états par props. + * Règle H : aucune logique métier — délègue à simulationConfig + useTimer. + * + * Minuteur et cibles de mots : cf. getSimulationConfig(tache). + * À l'expiration du timer, soumission automatique si mots ≥ motsMin, + * sinon message explicite demandant d'atteindre le seuil. */ import { useEffect, useRef, useState, type FormEvent } from 'react' -import { Loader2 } from 'lucide-react' +import { Clock, Loader2 } from 'lucide-react' import { z } from 'zod' import { Button } from '@/shared/components/ui/button' import { formatTache } from '@/entities/production/lib' import type { SujetData, Tache } from '@/entities/production/types' import type { ApiError } from '@/shared/types/api' +import { countWords, getSimulationConfig } from '../lib/simulationConfig' +import { useTimer } from '../hooks/useTimer' import { SujetDisplay } from './SujetDisplay' import { SpecialCharsKeyboard } from './SpecialCharsKeyboard' +import { TimerDisplay } from './TimerDisplay' +import { WordCountBar } from './WordCountBar' const textSchema = z.object({ texte: z @@ -49,9 +57,16 @@ interface Props { export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, onBack }: Props) { const textareaRef = useRef(null) + const hasAutoSubmittedRef = useRef(false) const [texte, setTexte] = useState('') const [fieldError, setFieldError] = useState(null) + const config = getSimulationConfig(tache) + const wordCount = countWords(texte) + const canSubmit = wordCount >= config.motsMin + + const timer = useTimer(config.dureeMinutes, !isSubmitting) + useEffect(() => { const el = textareaRef.current if (!el) return @@ -59,6 +74,16 @@ export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, on el.style.height = `${el.scrollHeight}px` }, [texte]) + useEffect(() => { + if (!timer.isExpired) return + if (hasAutoSubmittedRef.current) return + if (isSubmitting) return + if (wordCount < config.motsMin) return + + hasAutoSubmittedRef.current = true + onSubmit(texte) + }, [timer.isExpired, wordCount, config.motsMin, isSubmitting, texte, onSubmit]) + function handleInsert(char: string) { const el = textareaRef.current if (!el) { @@ -86,11 +111,17 @@ export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, on return } + if (wordCount < config.motsMin) { + setFieldError(`Écrivez au moins ${config.motsMin} mots pour soumettre.`) + return + } + onSubmit(parsed.data.texte) } const apiError = mapCorrectError(error) - const charCount = texte.length + const expiredBelowMin = timer.isExpired && wordCount < config.motsMin + const submitDisabled = isSubmitting || !canSubmit return (
@@ -103,7 +134,7 @@ export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, on > ← Retour -

{formatTache(tache)}

+

{formatTache(tache)}

@@ -117,12 +148,48 @@ export function SimulationForm({ tache, sujet, isSubmitting, error, onSubmit, on )} + {expiredBelowMin && ( +
+ Temps écoulé. Écrivez au moins {config.motsMin} mots pour soumettre. +
+ )} +
-
+
+
+