From d82a82baf353abd3de30f2c269c3b9f9a7a2c6a4 Mon Sep 17 00:00:00 2001 From: gilles Date: Mon, 21 Sep 2026 18:43:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20liste=20des=20cartes=20cliquable=20?= =?UTF-8?q?avec=20th=C3=A8me=20(F-08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composant CardList : charge les cartes via cardsApi.list(), affiche question + thème, chaque entrée est un lien vers le détail. Gère les états chargement, vide et erreur (ApiClientError). --- src/components/card-list.test.tsx | 65 +++++++++++++++++++++++++++++ src/components/card-list.tsx | 68 +++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/components/card-list.test.tsx create mode 100644 src/components/card-list.tsx diff --git a/src/components/card-list.test.tsx b/src/components/card-list.test.tsx new file mode 100644 index 0000000..cd8f0f4 --- /dev/null +++ b/src/components/card-list.test.tsx @@ -0,0 +1,65 @@ +import { render, screen, within } from '@testing-library/react' +import { describe, expect, it, vi, beforeEach } from 'vitest' +import { CardList } from './card-list' + +vi.mock('@/lib/api-client', () => { + class ApiClientErrorMock extends Error { + status: number + constructor(status: number, name: string, message: string) { + super(message) + this.name = name + this.status = status + } + } + return { + ApiClientError: ApiClientErrorMock, + cardsApi: { + list: vi.fn(), + }, + } +}) + +import { cardsApi, ApiClientError } from '@/lib/api-client' + +const CARDS = [ + { id: '1', question: 'Cest quoi un commit ?', response: 'Un snapshot du projet', theme: 'git', tags: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, + { id: '2', question: 'Cest quoi une closure ?', response: 'Une fonction + son scope', theme: 'javascript', tags: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }, +] + +beforeEach(() => { + vi.mocked(cardsApi.list).mockReset() +}) + +it('affiche le nom de chaque carte', async () => { + vi.mocked(cardsApi.list).mockResolvedValue(CARDS) + render() + const question = await screen.findByText('Cest quoi un commit ?') + expect(question).toBeInTheDocument() + expect(screen.getByText('Cest quoi une closure ?')).toBeInTheDocument() +}) + +it('affiche un lien vers le détail de chaque carte', async () => { + vi.mocked(cardsApi.list).mockResolvedValue(CARDS) + render() + const link = await screen.findByRole('link', { name: /Cest quoi un commit \?/ }) + expect(link).toHaveAttribute('href', '/cards/1') +}) + +it("affiche le thème de chaque carte", async () => { + vi.mocked(cardsApi.list).mockResolvedValue(CARDS) + render() + const item = (await screen.findByText('Cest quoi un commit ?')).closest('li') + expect(within(item as HTMLElement).getByText('git')).toBeInTheDocument() +}) + +it('affiche un message si aucune carte', async () => { + vi.mocked(cardsApi.list).mockResolvedValue([]) + render() + expect(await screen.findByRole('status')).toHaveTextContent('Aucune carte') +}) + +it('affiche un message derreur si le chargement échoue', async () => { + vi.mocked(cardsApi.list).mockRejectedValue(new ApiClientError(500, 'InternalServerError', 'Boom')) + render() + expect(await screen.findByRole('alert')).toHaveTextContent('Boom') +}) diff --git a/src/components/card-list.tsx b/src/components/card-list.tsx new file mode 100644 index 0000000..777a77a --- /dev/null +++ b/src/components/card-list.tsx @@ -0,0 +1,68 @@ +import Link from 'next/link' +import { useEffect, useState } from 'react' +import { ApiClientError, cardsApi } from '@/lib/api-client' + +interface ListedCard { + id: string + question: string + theme: string +} + +export function CardList() { + const [cards, setCards] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + cardsApi + .list() + .then((data) => { + if (!cancelled) { + setCards( + data.map((card) => ({ + id: card.id, + question: card.question, + theme: card.theme, + })), + ) + } + }) + .catch((e: unknown) => { + if (!cancelled) { + setError(e instanceof ApiClientError ? e.message : 'Impossible de charger les cartes') + setCards([]) + } + }) + return () => { + cancelled = true + } + }, []) + + if (error) { + return

{error}

+ } + if (cards === null) { + return

Chargement…

+ } + if (cards.length === 0) { + return

Aucune carte pour le moment.

+ } + + return ( +
    + {cards.map((card) => ( +
  • + + {card.question} + + {card.theme} + + +
  • + ))} +
+ ) +}