style: prettier format

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hermann_Kitio 2026-04-23 03:17:16 +03:00
parent 79bbbdc4e8
commit 99617f117c
45 changed files with 229 additions and 302 deletions

View file

@ -79,7 +79,10 @@ export function IdeesSuggestions({ idees, isLoading, error, isOpen, onClose }: P
<ul className="space-y-2 text-sm text-ink-2">
{idees.map((idee, i) => (
<li key={i} className="flex gap-2">
<span className="mt-[0.4em] size-1.5 shrink-0 rounded-full bg-expria" aria-hidden="true" />
<span
className="mt-[0.4em] size-1.5 shrink-0 rounded-full bg-expria"
aria-hidden="true"
/>
<span>{idee}</span>
</li>
))}

View file

@ -61,9 +61,7 @@ export function NclcCibleSelector({ value, onChange, disabled = false }: Props)
)
})}
</div>
<p className="text-xs text-ink-4">
{OPTIONS.find((o) => o.value === value)?.hint}
</p>
<p className="text-xs text-ink-4">{OPTIONS.find((o) => o.value === value)?.hint}</p>
</fieldset>
)
}

View file

@ -117,11 +117,7 @@ export function SimulationForm({
const tipsAllowed = hasAccess(plan, 'tips')
const ideesDisabled =
isSubmitting ||
idees.isLoading ||
!sujet ||
!tipsAllowed ||
wordCount < MIN_WORDS_IDEES
isSubmitting || idees.isLoading || !sujet || !tipsAllowed || wordCount < MIN_WORDS_IDEES
const ideesTitle = !tipsAllowed
? 'Disponible en Standard'
: wordCount < MIN_WORDS_IDEES
@ -311,11 +307,7 @@ export function SimulationForm({
className="w-full resize-none overflow-y-hidden rounded-md border border-line bg-surface p-3 text-sm text-ink-1 placeholder:text-ink-5 focus:border-expria focus:outline-none focus:shadow-focus disabled:cursor-not-allowed disabled:opacity-50"
/>
<WordCountBar count={wordCount} config={config} />
<NclcCibleSelector
value={nclcCible}
onChange={setNclcCible}
disabled={isSubmitting}
/>
<NclcCibleSelector value={nclcCible} onChange={setNclcCible} disabled={isSubmitting} />
{autosave.savedAt && !fieldError && (
<p className="text-xs text-ink-4" aria-live="polite">

View file

@ -13,8 +13,36 @@
*/
const SPECIAL_CHARS = [
'à', 'â', 'é', 'è', 'ê', 'ë', 'î', 'ï', 'ô', 'ù', 'û', 'ü', 'ç', 'œ', 'æ',
'À', 'Â', 'É', 'È', 'Ê', 'Ë', 'Î', 'Ï', 'Ô', 'Ù', 'Û', 'Ü', 'Ç', 'Œ', 'Æ',
'à',
'â',
'é',
'è',
'ê',
'ë',
'î',
'ï',
'ô',
'ù',
'û',
'ü',
'ç',
'œ',
'æ',
'À',
'Â',
'É',
'È',
'Ê',
'Ë',
'Î',
'Ï',
'Ô',
'Ù',
'Û',
'Ü',
'Ç',
'Œ',
'Æ',
] as const
interface Props {

View file

@ -24,9 +24,7 @@ function DocumentBlock({ titre, texte }: { titre: string | null; texte: string |
return (
<article className="rounded-md border border-line bg-canvas-2 p-3">
{titre && <h4 className="mb-2 text-sm font-semibold text-ink-1">{titre}</h4>}
{texte && (
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-2">{texte}</p>
)}
{texte && <p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-2">{texte}</p>}
</article>
)
}
@ -45,9 +43,7 @@ export function SujetDisplay({ sujet }: Props) {
)}
{sujet.contexte && (
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-3">
{sujet.contexte}
</p>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-3">{sujet.contexte}</p>
)}
<div>

View file

@ -44,7 +44,13 @@ const EE_CARDS: readonly TaskCard[] = [
const EO_CARDS: readonly TaskCard[] = [
{ key: 'EO_T1', tache: 'EO_T1', label: 'Expression Orale', sublabel: 'Entretien' },
{ key: 'EO_T3', tache: 'EO_T3', label: 'Expression Orale', sublabel: 'Point de vue' },
{ key: 'EO_T2_LIVE', tache: null, label: 'Expression Orale', sublabel: 'Tâche 2 — Live', lockLabel: 'Exclusivité Premium' },
{
key: 'EO_T2_LIVE',
tache: null,
label: 'Expression Orale',
sublabel: 'Tâche 2 — Live',
lockLabel: 'Exclusivité Premium',
},
]
export function TaskSelector({ type, plan, simulationsUsed, isLoading, onSelect }: Props) {
@ -56,9 +62,7 @@ export function TaskSelector({ type, plan, simulationsUsed, isLoading, onSelect
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-ink-1">Choisir une tâche</h2>
<p className="mt-1 text-sm text-ink-3">
Sélectionnez la tâche que vous souhaitez simuler.
</p>
<p className="mt-1 text-sm text-ink-3">Sélectionnez la tâche que vous souhaitez simuler.</p>
</div>
{quotaBlocked && (
@ -81,11 +85,7 @@ export function TaskSelector({ type, plan, simulationsUsed, isLoading, onSelect
if (locked) {
return (
<Card
key={card.key}
variant="default"
className="flex flex-col p-4 opacity-60"
>
<Card key={card.key} variant="default" className="flex flex-col p-4 opacity-60">
{card.tache === null && (
<Lock className="mb-2 size-4 text-ink-4" aria-hidden="true" />
)}

View file

@ -21,13 +21,9 @@ export function ConseilNclcCallout({ conseil }: Props) {
<h2 className="mb-3 text-base font-semibold text-ink-1">Plan d'action NCLC</h2>
<Card variant="raised" className="space-y-3 p-4">
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-1">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Objectif
</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Objectif</p>
<p className="text-sm font-semibold text-ink-1">{conseil.nclc_cible}</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Écart
</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Écart</p>
<p className="text-sm text-ink-2">{conseil.ecart}</p>
</div>
<div className="space-y-1.5 rounded-md border border-expria/30 bg-expria-50 p-3">

View file

@ -44,9 +44,7 @@ export function CritereCard({ critere, erreursCodes }: Props) {
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Exemple tiré de votre texte
</p>
<p className="italic text-sm leading-relaxed text-ink-2">
« {critere.exemple} »
</p>
<p className="italic text-sm leading-relaxed text-ink-2">« {critere.exemple} »</p>
</div>
)}
@ -61,7 +59,9 @@ export function CritereCard({ critere, erreursCodes }: Props) {
{critere.astuce && (
<div className="flex gap-2 text-sm text-ink-3">
<span className="shrink-0 text-expria" aria-hidden="true">💡</span>
<span className="shrink-0 text-expria" aria-hidden="true">
💡
</span>
<span>{critere.astuce}</span>
</div>
)}

View file

@ -17,14 +17,10 @@ interface Props {
export function DiagnosticCallout({ diagnostic }: Props) {
return (
<section aria-label="Frein principal">
<h2 className="mb-3 text-base font-semibold text-ink-1">
Ce qui freine votre progression
</h2>
<h2 className="mb-3 text-base font-semibold text-ink-1">Ce qui freine votre progression</h2>
<Card variant="default" className="border-l-4 border-l-expria p-4">
<div className="text-sm leading-relaxed text-ink-1">
<ReactMarkdown disallowedElements={['script', 'iframe']}>
{diagnostic}
</ReactMarkdown>
<ReactMarkdown disallowedElements={['script', 'iframe']}>{diagnostic}</ReactMarkdown>
</div>
</Card>
</section>

View file

@ -52,9 +52,7 @@ export function ExerciceInteractive({ exercice }: Props) {
{exercice.consigne && (
<div className="space-y-1.5 rounded-md border border-line bg-canvas-2 p-3">
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Consigne
</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Consigne</p>
<p className="text-sm leading-relaxed text-ink-1">{exercice.consigne}</p>
</div>
)}
@ -95,7 +93,7 @@ export function ExerciceInteractive({ exercice }: Props) {
size="sm"
disabled={!canRevealCorrection || correctionRevealed}
onClick={() => setCorrectionRevealed(true)}
title={!canRevealCorrection ? 'Écrivez d\'abord votre tentative' : undefined}
title={!canRevealCorrection ? "Écrivez d'abord votre tentative" : undefined}
>
Voir la correction
</Button>
@ -106,9 +104,7 @@ export function ExerciceInteractive({ exercice }: Props) {
className="space-y-1 rounded-md border border-warning/30 bg-warning-bg p-3"
aria-live="polite"
>
<p className="text-[11px] font-semibold uppercase tracking-widest text-warning">
Indice
</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-warning">Indice</p>
<p className="text-sm leading-relaxed text-ink-1">{exercice.indice}</p>
</div>
)}

View file

@ -30,9 +30,7 @@ export function ProductionModeleSection({ modele }: Props) {
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Version restructurée NCLC 9+
</p>
<Badge variant="nclc">
{modele.tcf_word_count ?? ''} mots
</Badge>
<Badge variant="nclc">{modele.tcf_word_count ?? ''} mots</Badge>
</div>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-ink-1">
{modele.production_modele_propre}

View file

@ -18,7 +18,7 @@ interface Props {
const SECTIONS: { key: keyof Revelation; titre: string; ton: 'ink' | 'warning' | 'danger' }[] = [
{ key: 'croyance', titre: 'Ce que vous croyez', ton: 'ink' },
{ key: 'realite', titre: 'Ce qu\'observe le correcteur', ton: 'warning' },
{ key: 'realite', titre: "Ce qu'observe le correcteur", ton: 'warning' },
{ key: 'consequence', titre: 'Conséquence sur la note', ton: 'danger' },
]
@ -35,7 +35,9 @@ export function RevelationCards({ revelation }: Props) {
<div className="grid gap-3 sm:grid-cols-3">
{SECTIONS.map(({ key, titre, ton }) => (
<Card key={key} variant="default" className="p-4">
<p className={`mb-2 text-[11px] font-semibold uppercase tracking-widest ${TON_CLASS[ton]}`}>
<p
className={`mb-2 text-[11px] font-semibold uppercase tracking-widest ${TON_CLASS[ton]}`}
>
{titre}
</p>
<div className="text-sm leading-relaxed text-ink-2">

View file

@ -13,8 +13,8 @@ import { ecartVsCible } from '@/entities/report/lib'
import type { NclcCible } from '@/entities/report/types'
interface Props {
score: number // /20
nclc: number // NCLC atteint
score: number // /20
nclc: number // NCLC atteint
nclcCible: NclcCible
}
@ -30,9 +30,7 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
<Card variant="raised" className="space-y-4 p-6">
<div className="flex flex-wrap items-end gap-8">
<div>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Score
</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Score</p>
<p className="mt-1 tabular-nums text-ink-1">
<span className="text-5xl font-bold">{score}</span>
<span className="text-2xl font-medium text-ink-4">/20</span>
@ -47,9 +45,7 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
</Badge>
</div>
<div>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Objectif
</p>
<p className="text-[11px] font-semibold uppercase tracking-widest text-ink-5">Objectif</p>
<Badge variant="neutral" className="mt-2">
NCLC {nclcCible}
</Badge>
@ -67,9 +63,7 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
aria-label={`Score ${score} sur 20`}
>
<div
className={`h-full transition-all duration-300 ${
atteint ? 'bg-success' : 'bg-expria'
}`}
className={`h-full transition-all duration-300 ${atteint ? 'bg-success' : 'bg-expria'}`}
style={{ width: `${percent}%` }}
/>
{/* Marqueur du seuil NCLC cible */}
@ -82,7 +76,9 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
</div>
<div className="flex justify-between text-xs text-ink-4 tabular-nums">
<span>0</span>
<span className="font-medium">Seuil NCLC {nclcCible} : {seuilCible}/20</span>
<span className="font-medium">
Seuil NCLC {nclcCible} : {seuilCible}/20
</span>
<span>20</span>
</div>
</div>
@ -94,9 +90,7 @@ export function ScoreHero({ score, nclc, nclcCible }: Props) {
</p>
) : (
<p className="rounded-md border border-warning/30 bg-warning-bg px-3 py-2 text-sm text-warning">
{points === 1
? '1 point avant NCLC '
: `${points} points avant NCLC `}
{points === 1 ? '1 point avant NCLC ' : `${points} points avant NCLC `}
{nclcCible}+
</p>
)}

View file

@ -21,7 +21,7 @@ const EXERCICE: Exercice = {
extrait: 'les enfants joue',
indice: 'Pluriel du sujet ?',
correction: 'les enfants jouent',
explication: 'Le verbe s\'accorde en nombre avec le sujet.',
explication: "Le verbe s'accorde en nombre avec le sujet.",
}
describe('ExerciceInteractive', () => {

View file

@ -31,7 +31,7 @@ describe('useAutosave', () => {
vi.useRealTimers()
})
it('debounce 30 s : pas d\'appel avant, appel après', async () => {
it("debounce 30 s : pas d'appel avant, appel après", async () => {
const { rerender } = renderHook(
({ contenu }: { contenu: string }) => useAutosave('sim-1', contenu, true),
{ initialProps: { contenu: '' } },
@ -112,7 +112,7 @@ describe('useAutosave', () => {
expect(mocked).not.toHaveBeenCalled()
})
it('dédoublonnage : pas de second appel si le contenu n\'a pas changé', async () => {
it("dédoublonnage : pas de second appel si le contenu n'a pas changé", async () => {
const { rerender } = renderHook(
({ contenu }: { contenu: string }) => useAutosave('sim-1', contenu, true),
{ initialProps: { contenu: '' } },

View file

@ -19,11 +19,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useSimulation } from '../useSimulation'
import { SimulationFlowProvider } from '../../state/SimulationFlowProvider'
import { useSimulationFlow } from '../../state/simulationFlow'
import {
createSimulation,
getSimulationState,
updateSujet,
} from '@/entities/production/api'
import { createSimulation, getSimulationState, updateSujet } from '@/entities/production/api'
import { correctEe } from '@/entities/report/api'
import type { Production } from '@/entities/production/types'
import type { Report } from '@/entities/report/types'
@ -134,7 +130,12 @@ describe('useSimulation — selectTask', () => {
it('isCreating = true pendant la mutation createSimulation', async () => {
let resolveCreate!: (p: Production) => void
mockCreateSimulation.mockImplementation(() => new Promise(r => { resolveCreate = r }))
mockCreateSimulation.mockImplementation(
() =>
new Promise((r) => {
resolveCreate = r
}),
)
const { result } = renderHook(() => useSimulation(), { wrapper: createWrapper() })
@ -154,7 +155,12 @@ describe('useSimulation — submitText', () => {
mockCreateSimulation.mockResolvedValue(mockProduction)
let resolveCorrect!: (r: Report) => void
mockCorrectEe.mockImplementation(() => new Promise(r => { resolveCorrect = r }))
mockCorrectEe.mockImplementation(
() =>
new Promise((r) => {
resolveCorrect = r
}),
)
const { result } = renderHook(
() => {
@ -202,8 +208,7 @@ describe('useSimulation — submitText', () => {
})
await waitFor(() => expect(result.current.step).toBe('task-selected'))
act(() => result.current.submitText('Mon texte.')
)
act(() => result.current.submitText('Mon texte.'))
await waitFor(() => expect(result.current.step).toBe('task-selected'))
expect(result.current.report).toBeNull()
})

View file

@ -46,7 +46,9 @@ export function countWords(texte: string): number {
/** Formate un nombre de secondes en MM:SS (ex. 125 → "02:05"). */
export function formatTimer(secondes: number): string {
const safe = Math.max(0, Math.floor(secondes))
const mm = Math.floor(safe / 60).toString().padStart(2, '0')
const mm = Math.floor(safe / 60)
.toString()
.padStart(2, '0')
const ss = (safe % 60).toString().padStart(2, '0')
return `${mm}:${ss}`
}

View file

@ -20,11 +20,7 @@ import { useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Lock } from 'lucide-react'
import { usePlan } from '@/features/dashboard/hooks/usePlan'
import {
isSectionVisible,
groupErreursByCritere,
critereCodeFromNom,
} from '@/entities/report/lib'
import { isSectionVisible, groupErreursByCritere, critereCodeFromNom } from '@/entities/report/lib'
import type { Report } from '@/entities/report/types'
import { useRapport } from '../hooks/useRapport'
import { useSimulation } from '../hooks/useSimulation'
@ -171,11 +167,7 @@ export function RapportPage() {
const { reset } = useSimulation()
const {
data: planData,
isLoading: isPlanLoading,
isError: isPlanError,
} = usePlan()
const { data: planData, isLoading: isPlanLoading, isError: isPlanError } = usePlan()
// FTD-21 — si la simulation n'est pas encore corrigée, rediriger vers /simulation/ee.
useEffect(() => {
@ -206,7 +198,9 @@ export function RapportPage() {
Simulations
</button>
<span aria-hidden="true"></span>
<span aria-current="page" className="text-ink-2">Rapport</span>
<span aria-current="page" className="text-ink-2">
Rapport
</span>
</nav>
{(isLoading || isPlanLoading) && <RapportSkeleton />}
@ -232,11 +226,7 @@ export function RapportPage() {
{rapport && planData && (
<>
<ScoreHero
score={rapport.score}
nclc={rapport.nclc}
nclcCible={rapport.nclc_cible}
/>
<ScoreHero score={rapport.score} nclc={rapport.nclc} nclcCible={rapport.nclc_cible} />
<RevelationCards revelation={rapport.revelation} />
@ -258,10 +248,7 @@ export function RapportPage() {
<ExercicesSection rapport={rapport} />
</BlurredSection>
<BlurredSection
visible={isSectionVisible(planData.plan, 'modele')}
onUpgrade={onUpgrade}
>
<BlurredSection visible={isSectionVisible(planData.plan, 'modele')} onUpgrade={onUpgrade}>
<ModeleSection rapport={rapport} />
</BlurredSection>

View file

@ -80,23 +80,21 @@ export function SimulationPage() {
/>
)}
{planData &&
(step === 'task-selected' || step === 'correcting') &&
production && (
<SimulationForm
tache={production.tache}
sujet={sujet}
plan={planData.plan}
simulationId={production.id}
initialContenu={production.contenu}
step={step}
isSubmitting={isCorrecting}
error={correctError}
onSubmit={submitText}
onBack={reset}
onChangeSujet={goToSubjectPicker}
/>
)}
{planData && (step === 'task-selected' || step === 'correcting') && production && (
<SimulationForm
tache={production.tache}
sujet={sujet}
plan={planData.plan}
simulationId={production.id}
initialContenu={production.contenu}
step={step}
isSubmitting={isCorrecting}
error={correctError}
onSubmit={submitText}
onBack={reset}
onChangeSujet={goToSubjectPicker}
/>
)}
</main>
)
}

View file

@ -46,10 +46,12 @@ export function SujetsPage() {
if (shouldRedirect) navigate('/simulation/ee', { replace: true })
}, [shouldRedirect, navigate])
const { data: sujets, isLoading, isError, refetch } = useSujets(
production?.tache ?? 'EE_T1',
!!production && !shouldRedirect,
)
const {
data: sujets,
isLoading,
isError,
refetch,
} = useSujets(production?.tache ?? 'EE_T1', !!production && !shouldRedirect)
if (shouldRedirect || !production) return null
@ -61,9 +63,7 @@ export function SujetsPage() {
function handleRandom() {
if (!sujets || sujets.length === 0) return
const pool = production?.sujet
? sujets.filter((s) => s.id !== production.sujet?.id)
: sujets
const pool = production?.sujet ? sujets.filter((s) => s.id !== production.sujet?.id) : sujets
const list = pool.length > 0 ? pool : sujets
const pick = list[Math.floor(Math.random() * list.length)]
if (pick) handleSelect(pick)
@ -117,11 +117,7 @@ export function SujetsPage() {
className="mb-4 rounded-md border border-danger/40 bg-danger-bg px-3 py-2 text-sm text-danger"
>
Impossible de charger les sujets.{' '}
<button
type="button"
onClick={() => refetch()}
className="underline underline-offset-2"
>
<button type="button" onClick={() => refetch()} className="underline underline-offset-2">
Réessayer
</button>
</div>

View file

@ -146,7 +146,5 @@ export function SimulationFlowProvider({ children }: { children: ReactNode }) {
reset,
}
return (
<SimulationFlowContext.Provider value={value}>{children}</SimulationFlowContext.Provider>
)
return <SimulationFlowContext.Provider value={value}>{children}</SimulationFlowContext.Provider>
}

View file

@ -7,20 +7,11 @@
*/
import { createContext, useContext } from 'react'
import type {
CreateSimulationPayload,
Production,
SujetData,
} from '@/entities/production/types'
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 type SimulationStep = 'idle' | 'choosing-subject' | 'task-selected' | 'correcting' | 'done'
export interface FlowValue {
step: SimulationStep