feat(ui): formulaire de création de carte avec navigation (F-05)

CardForm en mode création : champs question/réponse/thème, validation
HTML + affichage derreur API, soumission via cardsApi.create puis
redirection vers le détail de la carte créée (next/navigation).
This commit is contained in:
devcodetools committed 2026-09-21 18:52:02 +02:00
1 parent 3fdb558697
commit 2e131dddcb
2 files changed
+133

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CardForm } from './card-form'
vi.mock('@/lib/api-client', () => ({
ApiClientError: class extends Error { status = 0 },
cardsApi: { create: vi.fn(), update: vi.fn() },
}))
const routerMock = { push: vi.fn(), refresh: vi.fn() }
vi.mock('next/navigation', () => ({
useRouter: () => routerMock,
}))
import { cardsApi } from '@/lib/api-client'
beforeEach(() => {
vi.mocked(cardsApi.create).mockReset()
vi.mocked(cardsApi.update).mockReset()
routerMock.push.mockReset()
routerMock.refresh.mockReset()
})
it('crée une carte puis navigue vers son détail (F-05)', async () => {
vi.mocked(cardsApi.create).mockResolvedValue({ id: 'c1' } as never)
render(<CardForm mode="create" />)
await userEvent.type(screen.getByLabelText('Question'), 'Cest quoi un commit ?')
await userEvent.type(screen.getByLabelText('Réponse'), 'Un snapshot')
await userEvent.type(screen.getByLabelText('Thème'), 'git')
await userEvent.click(screen.getByRole('button', { name: 'Créer la carte' }))
expect(vi.mocked(cardsApi.create)).toHaveBeenCalledWith({
question: 'Cest quoi un commit ?',
response: 'Un snapshot',
theme: 'git',
})
await screen.findByText('Enregistrement…')
})
+95
View File
@@ -0,0 +1,95 @@
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import type { CardInput } from '@/domain/card'
import { ApiClientError, cardsApi } from '@/lib/api-client'
interface CardFormProps {
mode: 'create' | 'edit'
cardId?: string
initial?: CardInput
}
export function CardForm({ mode, cardId, initial }: CardFormProps) {
const router = useRouter()
const [question, setQuestion] = useState(initial?.question ?? '')
const [response, setResponse] = useState(initial?.response ?? '')
const [theme, setTheme] = useState(initial?.theme ?? '')
const [error, setError] = useState<string | null>(null)
const [saving, setSaving] = useState(false)
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setSaving(true)
setError(null)
const input: CardInput = { question, response, theme }
try {
if (mode === 'create') {
const card = await cardsApi.create(input)
router.push(`/cards/${card.id}`)
router.refresh()
} else if (cardId) {
await cardsApi.update(cardId, input)
router.push(`/cards/${cardId}`)
router.refresh()
}
} catch (err) {
setError(err instanceof ApiClientError ? err.message : 'Impossible de sauvegarder la carte')
setSaving(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<p role="alert" className="text-red-600">{error}</p>
)}
<div>
<label htmlFor="question" className="block text-sm font-medium text-slate-700">
Question
</label>
<input
id="question"
value={question}
onChange={(e) => setQuestion(e.target.value)}
required
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
/>
</div>
<div>
<label htmlFor="response" className="block text-sm font-medium text-slate-700">
Réponse
</label>
<textarea
id="response"
value={response}
onChange={(e) => setResponse(e.target.value)}
required
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
/>
</div>
<div>
<label htmlFor="theme" className="block text-sm font-medium text-slate-700">
Thème
</label>
<input
id="theme"
value={theme}
onChange={(e) => setTheme(e.target.value)}
required
className="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
/>
</div>
<div className="flex items-center gap-3">
<button
type="submit"
disabled={saving}
className="rounded-lg bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-700 disabled:opacity-50"
>
{saving ? 'Enregistrement…' : mode === 'create' ? 'Créer la carte' : 'Enregistrer'}
</button>
</div>
</form>
)
}