diff --git a/src/app/cards/[id]/page.tsx b/src/app/cards/[id]/page.tsx new file mode 100644 index 0000000..40e775e --- /dev/null +++ b/src/app/cards/[id]/page.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from 'next' +import { CardDetail } from '@/components/card-detail' + +export const metadata: Metadata = { + title: 'Carte', +} + +export default async function CardDetailPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + const { id } = await params + return ( +
+ +
+ ) +} diff --git a/src/components/card-detail.test.tsx b/src/components/card-detail.test.tsx new file mode 100644 index 0000000..bff58eb --- /dev/null +++ b/src/components/card-detail.test.tsx @@ -0,0 +1,70 @@ +import { render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CardDetail } from './card-detail' + +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: { + get: vi.fn(), + }, + } +}) + +import { cardsApi, ApiClientError } from '@/lib/api-client' + +const CARD = { + id: '1', + question: 'Cest quoi un commit ?', + response: 'Un snapshot du projet', + theme: 'git', + tags: ['snapshot'], +} + +beforeEach(() => { + vi.mocked(cardsApi.get).mockReset() +}) + +it('affiche la question, la réponse et le thème', async () => { + vi.mocked(cardsApi.get).mockResolvedValue(CARD) + render() + expect(await screen.findByRole('heading', { name: 'Cest quoi un commit ?' })).toBeInTheDocument() + expect(screen.getByText('Un snapshot du projet')).toBeInTheDocument() + expect(screen.getByText('git')).toBeInTheDocument() +}) + +it('affiche les tags de la carte', async () => { + vi.mocked(cardsApi.get).mockResolvedValue(CARD) + render() + expect(await screen.findByText('#snapshot')).toBeInTheDocument() +}) + +it('affiche un lien modifier vers la page dedition', async () => { + vi.mocked(cardsApi.get).mockResolvedValue(CARD) + render() + const link = await screen.findByRole('link', { name: 'Modifier' }) + expect(link).toHaveAttribute('href', '/cards/1/edit') +}) + +it('affiche un lien retour vers la liste', async () => { + vi.mocked(cardsApi.get).mockResolvedValue(CARD) + render() + const link = await screen.findByRole('link', { name: 'Retour à la liste' }) + expect(link).toHaveAttribute('href', '/cards') +}) + +it('affiche un message derreur si la carte est introuvable', async () => { + vi.mocked(cardsApi.get).mockRejectedValue( + new ApiClientError(404, 'NotFoundError', 'Carte introuvable'), + ) + render() + expect(await screen.findByRole('alert')).toHaveTextContent('Carte introuvable') +}) diff --git a/src/components/card-detail.tsx b/src/components/card-detail.tsx new file mode 100644 index 0000000..e04d52e --- /dev/null +++ b/src/components/card-detail.tsx @@ -0,0 +1,69 @@ +import Link from 'next/link' +import { useEffect, useState } from 'react' +import type { Card } from '@/domain/card' +import { ApiClientError, cardsApi } from '@/lib/api-client' + +export function CardDetail({ cardId }: { cardId: string }) { + const [card, setCard] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + let cancelled = false + cardsApi + .get(cardId) + .then((data) => { + if (!cancelled) setCard(data) + }) + .catch((e: unknown) => { + if (!cancelled) { + setError(e instanceof ApiClientError ? e.message : 'Impossible de charger la carte') + } + }) + return () => { + cancelled = true + } + }, [cardId]) + + if (error) { + return

{error}

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

Chargement…

+ } + + return ( +
+
+ + {card.theme} + +

{card.question}

+
+
+

Réponse

+

{card.response}

+
+ {card.tags.length > 0 && ( +
    + {card.tags.map((tag) => ( +
  • + #{tag} +
  • + ))} +
+ )} +
+ + Modifier + + · + + Retour à la liste + +
+
+ ) +}