feat(simulations): hook useSujets + composant SujetSelector
This commit is contained in:
parent
4245d0bcf1
commit
477477b6a6
2 changed files with 174 additions and 0 deletions
153
src/features/simulations/components/SujetSelector.tsx
Normal file
153
src/features/simulations/components/SujetSelector.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
/**
|
||||||
|
* Écran de choix du sujet avant de démarrer la rédaction.
|
||||||
|
*
|
||||||
|
* Deux modes :
|
||||||
|
* - `choice` : propose "Sujet aléatoire" ou "Choisir dans la liste"
|
||||||
|
* - `list` : affiche les sujets actifs renvoyés par GET /sujets
|
||||||
|
*
|
||||||
|
* Règle H : aucune logique métier — le composant reçoit tache et handlers,
|
||||||
|
* et consomme `useSujets` pour la lecture du catalogue.
|
||||||
|
* Règle L : tokens Direction H uniquement (primitives Card / Button).
|
||||||
|
*
|
||||||
|
* Pour EO_T1, cet écran n'est pas rendu (flux direct SimulationForm) —
|
||||||
|
* cf. SimulationPage.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { ArrowLeft, Loader2, Shuffle, List } from 'lucide-react'
|
||||||
|
import { Button } from '@/shared/ui/Button'
|
||||||
|
import { Card } from '@/shared/ui/Card'
|
||||||
|
import type { SujetData, Tache } from '@/entities/production/types'
|
||||||
|
import { useSujets } from '../hooks/useSujets'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tache: Tache
|
||||||
|
currentSujetId: string | null
|
||||||
|
onSelect: (sujet: SujetData) => void
|
||||||
|
onRandom: (sujets: SujetData[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type ViewMode = 'choice' | 'list'
|
||||||
|
|
||||||
|
export function SujetSelector({ tache, currentSujetId, onSelect, onRandom }: Props) {
|
||||||
|
const [mode, setMode] = useState<ViewMode>('choice')
|
||||||
|
const { data: sujets, isLoading, isError, refetch } = useSujets(tache)
|
||||||
|
|
||||||
|
if (mode === 'choice') {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-ink-1">Choisir un sujet</h2>
|
||||||
|
<p className="mt-1 text-sm text-ink-4">
|
||||||
|
Lancez-vous avec un sujet aléatoire ou parcourez le catalogue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
variant="interactive"
|
||||||
|
onClick={() => onRandom(sujets ?? [])}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3 p-4 text-left">
|
||||||
|
<Shuffle className="mt-0.5 size-5 shrink-0 text-expria" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-ink-1">Sujet aléatoire</div>
|
||||||
|
<p className="mt-0.5 text-xs text-ink-4">
|
||||||
|
On tire un sujet au hasard dans le catalogue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
variant="interactive"
|
||||||
|
onClick={() => setMode('list')}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3 p-4 text-left">
|
||||||
|
<List className="mt-0.5 size-5 shrink-0 text-expria" aria-hidden="true" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-ink-1">Choisir dans la liste</div>
|
||||||
|
<p className="mt-0.5 text-xs text-ink-4">
|
||||||
|
Parcourir tous les sujets disponibles pour cette tâche.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
icon={<ArrowLeft className="size-4" aria-hidden="true" />}
|
||||||
|
onClick={() => setMode('choice')}
|
||||||
|
>
|
||||||
|
Retour
|
||||||
|
</Button>
|
||||||
|
<h2 className="text-lg font-semibold text-ink-1">Liste des sujets</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
className="flex items-center gap-2 text-sm text-ink-4"
|
||||||
|
>
|
||||||
|
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
|
||||||
|
Chargement des sujets…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p role="alert" className="text-sm text-danger">
|
||||||
|
Impossible de charger la liste. Réessayez dans quelques instants.
|
||||||
|
</p>
|
||||||
|
<Button variant="secondary" size="sm" onClick={() => refetch()}>
|
||||||
|
Réessayer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sujets && sujets.length === 0 && (
|
||||||
|
<p className="text-sm text-ink-4">
|
||||||
|
Aucun sujet disponible pour cette tâche pour le moment.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sujets && sujets.length > 0 && (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{sujets.map((sujet) => {
|
||||||
|
const isCurrent = sujet.id === currentSujetId
|
||||||
|
return (
|
||||||
|
<li key={sujet.id}>
|
||||||
|
<Card
|
||||||
|
variant="interactive"
|
||||||
|
onClick={() => onSelect(sujet)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-current={isCurrent ? 'true' : undefined}
|
||||||
|
className={`p-4 text-left ${isCurrent ? 'ring-2 ring-expria/40' : ''}`}
|
||||||
|
>
|
||||||
|
{sujet.role && (
|
||||||
|
<div className="mb-1 text-xs font-semibold uppercase tracking-wide text-ink-4">
|
||||||
|
Rôle : {sujet.role}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="line-clamp-2 text-sm text-ink-1">{sujet.consigne}</p>
|
||||||
|
{isCurrent && (
|
||||||
|
<div className="mt-2 text-xs font-medium text-expria">Sujet actuel</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/features/simulations/hooks/useSujets.ts
Normal file
21
src/features/simulations/hooks/useSujets.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Hook de chargement des sujets disponibles pour une tâche.
|
||||||
|
*
|
||||||
|
* Règle H : aucune logique métier ici — mapping Tache → filtres DB dans
|
||||||
|
* `entities/production/api.ts` (getSujets). Le hook ne fait qu'encapsuler
|
||||||
|
* la requête TanStack Query avec un staleTime généreux (le catalogue de
|
||||||
|
* sujets change rarement).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { getSujets } from '@/entities/production/api'
|
||||||
|
import type { SujetData, Tache } from '@/entities/production/types'
|
||||||
|
|
||||||
|
export function useSujets(tache: Tache, enabled: boolean = true) {
|
||||||
|
return useQuery<SujetData[]>({
|
||||||
|
queryKey: ['sujets', tache],
|
||||||
|
queryFn: () => getSujets(tache),
|
||||||
|
staleTime: 10 * 60 * 1000,
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue