fix(lint): 4 erreurs ESLint corrigées — split SimulationFlowProvider, hook conditionnel, ref render, setState effect

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-23 03:05:14 +03:00
parent de69b3ff16
commit 79bbbdc4e8
10 changed files with 82 additions and 50 deletions

View file

@ -30,8 +30,11 @@ export function AppLayout({ children }: AppLayoutProps) {
const { data } = usePlan() const { data } = usePlan()
const plan: Plan = data?.plan ?? 'free' 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(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsMobileMenuOpen(false) setIsMobileMenuOpen(false)
}, [location.pathname]) }, [location.pathname])

View file

@ -33,11 +33,14 @@ function gaugeColor(score: number): string {
} }
export function MonProfilPreparation({ plan }: Props) { export function MonProfilPreparation({ plan }: Props) {
// Garde explicite (cohérent avec la logique du hook qui a déjà `enabled`). // Hook appelé inconditionnellement (règle React). Il court-circuite la
if (!hasAccess(plan, 'pattern_analysis')) return null // requête backend via `enabled: hasAccess(plan, 'pattern_analysis')`,
// donc aucun appel parasite pour Free/Standard.
const { data, isLoading, isError } = usePatterns(plan) 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) { if (isLoading || isError || !data) {
return ( return (
<Card variant="default" className="p-4"> <Card variant="default" className="p-4">

View file

@ -5,7 +5,7 @@
* Le hook `usePatterns` est mocké pour isoler la présentation. * 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 { render, screen, cleanup } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom' import { MemoryRouter } from 'react-router-dom'
@ -16,6 +16,16 @@ vi.mock('@/features/progression/hooks/usePatterns', () => ({
import { usePatterns } from '@/features/progression/hooks/usePatterns' import { usePatterns } from '@/features/progression/hooks/usePatterns'
import { MonProfilPreparation } from '../MonProfilPreparation' 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<typeof usePatterns>)
})
afterEach(cleanup) afterEach(cleanup)
function renderWithRouter(ui: React.ReactNode) { function renderWithRouter(ui: React.ReactNode) {

View file

@ -23,7 +23,7 @@ import { countWords, getSimulationConfig } from '../lib/simulationConfig'
import { useTimer } from '../hooks/useTimer' import { useTimer } from '../hooks/useTimer'
import { useIdees } from '../hooks/useIdees' import { useIdees } from '../hooks/useIdees'
import { useAutosave } from '../hooks/useAutosave' import { useAutosave } from '../hooks/useAutosave'
import type { SimulationStep } from '../state/SimulationFlowProvider' import type { SimulationStep } from '../state/simulationFlow'
import { SujetDisplay } from './SujetDisplay' import { SujetDisplay } from './SujetDisplay'
import { SpecialCharsKeyboard } from './SpecialCharsKeyboard' import { SpecialCharsKeyboard } from './SpecialCharsKeyboard'
import { TimerDisplay } from './TimerDisplay' import { TimerDisplay } from './TimerDisplay'

View file

@ -17,7 +17,8 @@ import { MemoryRouter } from 'react-router-dom'
import React from 'react' import React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useSimulation } from '../useSimulation' import { useSimulation } from '../useSimulation'
import { SimulationFlowProvider, useSimulationFlow } from '../../state/SimulationFlowProvider' import { SimulationFlowProvider } from '../../state/SimulationFlowProvider'
import { useSimulationFlow } from '../../state/simulationFlow'
import { import {
createSimulation, createSimulation,
getSimulationState, getSimulationState,

View file

@ -10,7 +10,7 @@
*/ */
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useSimulationFlow } from '../state/SimulationFlowProvider' import { useSimulationFlow } from '../state/simulationFlow'
export function useSimulation() { export function useSimulation() {
const navigate = useNavigate() const navigate = useNavigate()

View file

@ -22,7 +22,9 @@ export function useTimer(dureeMinutes: number, active: boolean): TimerState {
const dureeSecondes = Math.max(0, Math.floor(dureeMinutes * 60)) const dureeSecondes = Math.max(0, Math.floor(dureeMinutes * 60))
const [secondesRestantes, setSecondesRestantes] = useState(dureeSecondes) const [secondesRestantes, setSecondesRestantes] = useState(dureeSecondes)
const dureeRef = useRef(dureeSecondes) const dureeRef = useRef(dureeSecondes)
dureeRef.current = dureeSecondes useEffect(() => {
dureeRef.current = dureeSecondes
}, [dureeSecondes])
useEffect(() => { useEffect(() => {
if (!active) return if (!active) return

View file

@ -18,7 +18,7 @@ import { Shuffle } from 'lucide-react'
import { Button } from '@/shared/ui/Button' import { Button } from '@/shared/ui/Button'
import { formatTache } from '@/entities/production/lib' import { formatTache } from '@/entities/production/lib'
import type { SujetData } from '@/entities/production/types' import type { SujetData } from '@/entities/production/types'
import { useSimulationFlow } from '../state/SimulationFlowProvider' import { useSimulationFlow } from '../state/simulationFlow'
import { useSujets } from '../hooks/useSujets' import { useSujets } from '../hooks/useSujets'
import { SujetCard } from '../components/SujetCard' import { SujetCard } from '../components/SujetCard'

View file

@ -8,7 +8,7 @@
* Règle H : aucune logique métier les mutations s'appuient sur entities/. * 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 { useLocation, useNavigate } from 'react-router-dom'
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { import {
@ -17,43 +17,15 @@ import {
updateSujet as updateSujetApi, updateSujet as updateSujetApi,
} from '@/entities/production/api' } from '@/entities/production/api'
import { correctEe } from '@/entities/report/api' import { correctEe } from '@/entities/report/api'
import type { import type { CreateSimulationPayload, Production, Tache } from '@/entities/production/types'
CreateSimulationPayload,
Production,
SujetData,
Tache,
} from '@/entities/production/types'
import type { Report } from '@/entities/report/types' import type { Report } from '@/entities/report/types'
import type { ApiError } from '@/shared/types/api' import type { ApiError } from '@/shared/types/api'
import type { SujetData } from '@/entities/production/types'
export type SimulationStep = import { SimulationFlowContext, type FlowValue, type SimulationStep } from './simulationFlow'
| 'idle'
| 'choosing-subject'
| 'task-selected'
| 'correcting'
| 'done'
const TACHES_SANS_CATALOGUE: Tache[] = ['EO_T1'] const TACHES_SANS_CATALOGUE: Tache[] = ['EO_T1']
const LS_SIMULATION_ID_KEY = 'expria_simulation_id' 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<FlowValue | null>(null)
export function SimulationFlowProvider({ children }: { children: ReactNode }) { export function SimulationFlowProvider({ children }: { children: ReactNode }) {
const [step, setStep] = useState<SimulationStep>('idle') const [step, setStep] = useState<SimulationStep>('idle')
const [production, setProduction] = useState<Production | null>(null) const [production, setProduction] = useState<Production | null>(null)
@ -178,11 +150,3 @@ export function SimulationFlowProvider({ children }: { children: ReactNode }) {
<SimulationFlowContext.Provider value={value}>{children}</SimulationFlowContext.Provider> <SimulationFlowContext.Provider value={value}>{children}</SimulationFlowContext.Provider>
) )
} }
export function useSimulationFlow(): FlowValue {
const ctx = useContext(SimulationFlowContext)
if (!ctx) {
throw new Error('useSimulationFlow doit être utilisé dans un <SimulationFlowProvider>.')
}
return ctx
}

View file

@ -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 -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<FlowValue | null>(null)
export function useSimulationFlow(): FlowValue {
const ctx = useContext(SimulationFlowContext)
if (!ctx) {
throw new Error('useSimulationFlow doit être utilisé dans un <SimulationFlowProvider>.')
}
return ctx
}