From 79bbbdc4e85966cf57528bde45f75c6cda2f6d0d Mon Sep 17 00:00:00 2001 From: Hermann_Kitio Date: Thu, 23 Apr 2026 03:05:14 +0300 Subject: [PATCH] =?UTF-8?q?fix(lint):=204=20erreurs=20ESLint=20corrig?= =?UTF-8?q?=C3=A9es=20=E2=80=94=20split=20SimulationFlowProvider,=20hook?= =?UTF-8?q?=20conditionnel,=20ref=20render,=20setState=20effect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/AppLayout.tsx | 5 +- .../components/MonProfilPreparation.tsx | 9 ++-- .../__tests__/MonProfilPreparation.test.tsx | 12 ++++- .../simulations/components/SimulationForm.tsx | 2 +- .../hooks/__tests__/useSimulation.test.tsx | 3 +- .../simulations/hooks/useSimulation.ts | 2 +- src/features/simulations/hooks/useTimer.ts | 4 +- src/features/simulations/pages/SujetsPage.tsx | 2 +- .../state/SimulationFlowProvider.tsx | 44 ++--------------- .../simulations/state/simulationFlow.ts | 49 +++++++++++++++++++ 10 files changed, 82 insertions(+), 50 deletions(-) create mode 100644 src/features/simulations/state/simulationFlow.ts diff --git a/src/app/AppLayout.tsx b/src/app/AppLayout.tsx index e026f20..65d53f9 100644 --- a/src/app/AppLayout.tsx +++ b/src/app/AppLayout.tsx @@ -30,8 +30,11 @@ export function AppLayout({ children }: AppLayoutProps) { const { data } = usePlan() const plan: Plan = data?.plan ?? 'free' - // Ferme le drawer à chaque changement de route + // Ferme le drawer à chaque changement de route. + // Synchronisation UI → router state : pattern légitime (source externe = React + // Router). Bail-out React si déjà fermé = zéro cascading render en pratique. useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setIsMobileMenuOpen(false) }, [location.pathname]) diff --git a/src/features/dashboard/components/MonProfilPreparation.tsx b/src/features/dashboard/components/MonProfilPreparation.tsx index 97ff571..6f0691a 100644 --- a/src/features/dashboard/components/MonProfilPreparation.tsx +++ b/src/features/dashboard/components/MonProfilPreparation.tsx @@ -33,11 +33,14 @@ function gaugeColor(score: number): string { } export function MonProfilPreparation({ plan }: Props) { - // Garde explicite (cohérent avec la logique du hook qui a déjà `enabled`). - if (!hasAccess(plan, 'pattern_analysis')) return null - + // Hook appelé inconditionnellement (règle React). Il court-circuite la + // requête backend via `enabled: hasAccess(plan, 'pattern_analysis')`, + // donc aucun appel parasite pour Free/Standard. const { data, isLoading, isError } = usePatterns(plan) + // Garde explicite après le hook pour éviter un flash de contenu. + if (!hasAccess(plan, 'pattern_analysis')) return null + if (isLoading || isError || !data) { return ( diff --git a/src/features/dashboard/components/__tests__/MonProfilPreparation.test.tsx b/src/features/dashboard/components/__tests__/MonProfilPreparation.test.tsx index 66107fb..0f0f85f 100644 --- a/src/features/dashboard/components/__tests__/MonProfilPreparation.test.tsx +++ b/src/features/dashboard/components/__tests__/MonProfilPreparation.test.tsx @@ -5,7 +5,7 @@ * Le hook `usePatterns` est mocké pour isoler la présentation. */ -import { describe, it, expect, vi, afterEach } from 'vitest' +import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest' import { render, screen, cleanup } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' @@ -16,6 +16,16 @@ vi.mock('@/features/progression/hooks/usePatterns', () => ({ import { usePatterns } from '@/features/progression/hooks/usePatterns' import { MonProfilPreparation } from '../MonProfilPreparation' +beforeEach(() => { + // Mock par défaut — usePatterns est appelé inconditionnellement depuis le + // composant (Règle des hooks). Les tests Premium surchargent ce mock. + vi.mocked(usePatterns).mockReturnValue({ + data: undefined, + isLoading: false, + isError: false, + } as unknown as ReturnType) +}) + afterEach(cleanup) function renderWithRouter(ui: React.ReactNode) { diff --git a/src/features/simulations/components/SimulationForm.tsx b/src/features/simulations/components/SimulationForm.tsx index 810f12c..9a9ea85 100644 --- a/src/features/simulations/components/SimulationForm.tsx +++ b/src/features/simulations/components/SimulationForm.tsx @@ -23,7 +23,7 @@ import { countWords, getSimulationConfig } from '../lib/simulationConfig' import { useTimer } from '../hooks/useTimer' import { useIdees } from '../hooks/useIdees' import { useAutosave } from '../hooks/useAutosave' -import type { SimulationStep } from '../state/SimulationFlowProvider' +import type { SimulationStep } from '../state/simulationFlow' import { SujetDisplay } from './SujetDisplay' import { SpecialCharsKeyboard } from './SpecialCharsKeyboard' import { TimerDisplay } from './TimerDisplay' diff --git a/src/features/simulations/hooks/__tests__/useSimulation.test.tsx b/src/features/simulations/hooks/__tests__/useSimulation.test.tsx index 84c86b3..ba2ad84 100644 --- a/src/features/simulations/hooks/__tests__/useSimulation.test.tsx +++ b/src/features/simulations/hooks/__tests__/useSimulation.test.tsx @@ -17,7 +17,8 @@ import { MemoryRouter } from 'react-router-dom' import React from 'react' import { describe, it, expect, vi, beforeEach } from 'vitest' import { useSimulation } from '../useSimulation' -import { SimulationFlowProvider, useSimulationFlow } from '../../state/SimulationFlowProvider' +import { SimulationFlowProvider } from '../../state/SimulationFlowProvider' +import { useSimulationFlow } from '../../state/simulationFlow' import { createSimulation, getSimulationState, diff --git a/src/features/simulations/hooks/useSimulation.ts b/src/features/simulations/hooks/useSimulation.ts index 4732915..5310bee 100644 --- a/src/features/simulations/hooks/useSimulation.ts +++ b/src/features/simulations/hooks/useSimulation.ts @@ -10,7 +10,7 @@ */ import { useNavigate } from 'react-router-dom' -import { useSimulationFlow } from '../state/SimulationFlowProvider' +import { useSimulationFlow } from '../state/simulationFlow' export function useSimulation() { const navigate = useNavigate() diff --git a/src/features/simulations/hooks/useTimer.ts b/src/features/simulations/hooks/useTimer.ts index 49a6db5..1d5d7d8 100644 --- a/src/features/simulations/hooks/useTimer.ts +++ b/src/features/simulations/hooks/useTimer.ts @@ -22,7 +22,9 @@ export function useTimer(dureeMinutes: number, active: boolean): TimerState { const dureeSecondes = Math.max(0, Math.floor(dureeMinutes * 60)) const [secondesRestantes, setSecondesRestantes] = useState(dureeSecondes) const dureeRef = useRef(dureeSecondes) - dureeRef.current = dureeSecondes + useEffect(() => { + dureeRef.current = dureeSecondes + }, [dureeSecondes]) useEffect(() => { if (!active) return diff --git a/src/features/simulations/pages/SujetsPage.tsx b/src/features/simulations/pages/SujetsPage.tsx index 5a7993c..c3cba74 100644 --- a/src/features/simulations/pages/SujetsPage.tsx +++ b/src/features/simulations/pages/SujetsPage.tsx @@ -18,7 +18,7 @@ import { Shuffle } from 'lucide-react' import { Button } from '@/shared/ui/Button' import { formatTache } from '@/entities/production/lib' import type { SujetData } from '@/entities/production/types' -import { useSimulationFlow } from '../state/SimulationFlowProvider' +import { useSimulationFlow } from '../state/simulationFlow' import { useSujets } from '../hooks/useSujets' import { SujetCard } from '../components/SujetCard' diff --git a/src/features/simulations/state/SimulationFlowProvider.tsx b/src/features/simulations/state/SimulationFlowProvider.tsx index b0a7e25..94565b6 100644 --- a/src/features/simulations/state/SimulationFlowProvider.tsx +++ b/src/features/simulations/state/SimulationFlowProvider.tsx @@ -8,7 +8,7 @@ * Règle H : aucune logique métier — les mutations s'appuient sur entities/. */ -import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from 'react' +import { useEffect, useRef, useState, type ReactNode } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { useMutation } from '@tanstack/react-query' import { @@ -17,43 +17,15 @@ import { updateSujet as updateSujetApi, } from '@/entities/production/api' import { correctEe } from '@/entities/report/api' -import type { - CreateSimulationPayload, - Production, - SujetData, - Tache, -} from '@/entities/production/types' +import type { CreateSimulationPayload, Production, Tache } from '@/entities/production/types' import type { Report } from '@/entities/report/types' import type { ApiError } from '@/shared/types/api' - -export type SimulationStep = - | 'idle' - | 'choosing-subject' - | 'task-selected' - | 'correcting' - | 'done' +import type { SujetData } from '@/entities/production/types' +import { SimulationFlowContext, type FlowValue, type SimulationStep } from './simulationFlow' const TACHES_SANS_CATALOGUE: Tache[] = ['EO_T1'] const LS_SIMULATION_ID_KEY = 'expria_simulation_id' -interface FlowValue { - step: SimulationStep - production: Production | null - sujet: SujetData | null - report: Report | null - isCreating: boolean - isCorrecting: boolean - createError: ApiError | null - correctError: ApiError | null - selectTask: (payload: CreateSimulationPayload) => void - submitText: (texte: string, nclcCible?: 9 | 10) => void - changeSubject: (sujet: SujetData) => void - setStep: (step: SimulationStep) => void - reset: () => void -} - -const SimulationFlowContext = createContext(null) - export function SimulationFlowProvider({ children }: { children: ReactNode }) { const [step, setStep] = useState('idle') const [production, setProduction] = useState(null) @@ -178,11 +150,3 @@ export function SimulationFlowProvider({ children }: { children: ReactNode }) { {children} ) } - -export function useSimulationFlow(): FlowValue { - const ctx = useContext(SimulationFlowContext) - if (!ctx) { - throw new Error('useSimulationFlow doit être utilisé dans un .') - } - return ctx -} diff --git a/src/features/simulations/state/simulationFlow.ts b/src/features/simulations/state/simulationFlow.ts new file mode 100644 index 0000000..3a4b987 --- /dev/null +++ b/src/features/simulations/state/simulationFlow.ts @@ -0,0 +1,49 @@ +/** + * Types, contexte et hook du flux simulation. + * + * Extrait de SimulationFlowProvider.tsx pour respecter la règle + * `react-refresh/only-export-components` : un fichier de composant ne peut + * pas ré-exporter types / hooks / contextes. + */ + +import { createContext, useContext } from 'react' +import type { + CreateSimulationPayload, + Production, + SujetData, +} from '@/entities/production/types' +import type { Report } from '@/entities/report/types' +import type { ApiError } from '@/shared/types/api' + +export type SimulationStep = + | 'idle' + | 'choosing-subject' + | 'task-selected' + | 'correcting' + | 'done' + +export interface FlowValue { + step: SimulationStep + production: Production | null + sujet: SujetData | null + report: Report | null + isCreating: boolean + isCorrecting: boolean + createError: ApiError | null + correctError: ApiError | null + selectTask: (payload: CreateSimulationPayload) => void + submitText: (texte: string, nclcCible?: 9 | 10) => void + changeSubject: (sujet: SujetData) => void + setStep: (step: SimulationStep) => void + reset: () => void +} + +export const SimulationFlowContext = createContext(null) + +export function useSimulationFlow(): FlowValue { + const ctx = useContext(SimulationFlowContext) + if (!ctx) { + throw new Error('useSimulationFlow doit être utilisé dans un .') + } + return ctx +}