feat(ui): vue jeu branchée avec accueil et navigation (P3, F-01 à F-04, F-10)
This commit is contained in:
1 parent
0233be7fff
commit
61eef32c74
9 files changed
+136
-12
No files matched your search
@@ -1,6 +1,7 @@
|
|||||||
import { SerwistProvider } from '@serwist/turbopack/react'
|
import { SerwistProvider } from '@serwist/turbopack/react'
|
||||||
import type { Metadata, Viewport } from 'next'
|
import type { Metadata, Viewport } from 'next'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import { AppHeader } from '@/components/app-header'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
const APP_NAME = 'flashcards'
|
const APP_NAME = 'flashcards'
|
||||||
@@ -55,6 +56,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<body>
|
<body>
|
||||||
<SerwistProvider swUrl="/serwist/sw.js" disable={process.env.NODE_ENV === 'development'}>
|
<SerwistProvider swUrl="/serwist/sw.js" disable={process.env.NODE_ENV === 'development'}>
|
||||||
|
<AppHeader />
|
||||||
{children}
|
{children}
|
||||||
</SerwistProvider>
|
</SerwistProvider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+12
-2
@@ -3,8 +3,18 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import Home from './page'
|
import Home from './page'
|
||||||
|
|
||||||
describe('Home', () => {
|
describe('Home', () => {
|
||||||
it('affiche le contenu minimal', () => {
|
it('affiche un titre et un lien vers le jeu', () => {
|
||||||
render(<Home />)
|
render(<Home />)
|
||||||
expect(screen.getByRole('heading', { level: 1, name: 'Hello world' })).toBeInTheDocument()
|
expect(
|
||||||
|
screen.getByRole('heading', { level: 1, name: /R\u00e9viser tes cartes/ }),
|
||||||
|
).toBeInTheDocument()
|
||||||
|
const playLink = screen.getByRole('link', { name: /Jouer/ })
|
||||||
|
expect(playLink).toHaveAttribute('href', '/play')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche un lien vers la gestion des cartes', () => {
|
||||||
|
render(<Home />)
|
||||||
|
const cardsLink = screen.getByRole('link', { name: /G\u00e9rer les cartes/ })
|
||||||
|
expect(cardsLink).toHaveAttribute('href', '/cards')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
+30
-2
@@ -1,7 +1,35 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main className="mx-auto max-w-2xl px-4 py-16">
|
||||||
<h1>Hello world</h1>
|
<h1 className="text-3xl font-bold text-slate-900">
|
||||||
|
Réviser tes cartes, une carte à la fois.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-slate-600">
|
||||||
|
Tire une carte au hasard, révèle la réponse quand tu es prêt, ou choisis un thème pour
|
||||||
|
cibler ta révision.
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 grid gap-4 sm:grid-cols-2">
|
||||||
|
<Link
|
||||||
|
href="/play"
|
||||||
|
className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition hover:border-slate-300 hover:shadow-md"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-semibold text-slate-900">Jouer</span>
|
||||||
|
<span className="mt-1 block text-sm text-slate-600">
|
||||||
|
Tirage aléatoire d'une carte, par thème ou globalement.
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/cards"
|
||||||
|
className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm transition hover:border-slate-300 hover:shadow-md"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-semibold text-slate-900">Gérer les cartes</span>
|
||||||
|
<span className="mt-1 block text-sm text-slate-600">
|
||||||
|
Créer, modifier, consulter et supprimer tes cartes.
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import PlayPage from './page'
|
||||||
|
|
||||||
|
vi.mock('@/lib/api-client', () => ({
|
||||||
|
ApiClientError: class extends Error {
|
||||||
|
status = 0
|
||||||
|
},
|
||||||
|
cardsApi: { themes: vi.fn(), random: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { cardsApi } from '@/lib/api-client'
|
||||||
|
|
||||||
|
describe('PlayPage', () => {
|
||||||
|
it('monte le jeu avec le sélecteur de thème et le tirage (F-01 à F-04, F-10)', async () => {
|
||||||
|
vi.mocked(cardsApi.themes).mockResolvedValue(['git'])
|
||||||
|
render(<PlayPage />)
|
||||||
|
expect(screen.getByRole('button', { name: 'Tirer une carte' })).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText('Th\u00e8me')).toBeInTheDocument()
|
||||||
|
await screen.findByRole('option', { name: 'git' })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { PlayView } from '@/components/play-view'
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Jouer',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PlayPage() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<PlayView />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { AppHeader } from './app-header'
|
||||||
|
|
||||||
|
describe('AppHeader', () => {
|
||||||
|
it('affiche la marque et les liens de navigation', () => {
|
||||||
|
render(<AppHeader />)
|
||||||
|
expect(screen.getByRole('link', { name: 'Flashcards' })).toHaveAttribute('href', '/')
|
||||||
|
expect(screen.getByRole('link', { name: 'Jouer' })).toHaveAttribute('href', '/play')
|
||||||
|
expect(screen.getByRole('link', { name: 'G\u00e9rer les cartes' })).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'/cards',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export function AppHeader() {
|
||||||
|
return (
|
||||||
|
<header className="border-b border-slate-200 bg-white">
|
||||||
|
<nav className="mx-auto flex max-w-5xl items-center gap-6 px-4 py-3">
|
||||||
|
<Link href="/" className="text-lg font-bold text-slate-900">
|
||||||
|
Flashcards
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/play"
|
||||||
|
className="text-sm font-medium text-slate-600 underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
Jouer
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/cards"
|
||||||
|
className="text-sm font-medium text-slate-600 underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
Gérer les cartes
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, expect, it, vi } from 'vitest'
|
||||||
import { PlayView } from './play-view'
|
import { PlayView } from './play-view'
|
||||||
|
|
||||||
vi.mock('@/lib/api-client', () => {
|
vi.mock('@/lib/api-client', () => {
|
||||||
@@ -42,11 +42,15 @@ it('tire une carte au hasard globalement (F-01) puis masque la réponse (F-02, F
|
|||||||
await userEvent.click(await screen.findByRole('button', { name: 'Tirer une carte' }))
|
await userEvent.click(await screen.findByRole('button', { name: 'Tirer une carte' }))
|
||||||
expect(await screen.findByText('Cest quoi un rebase ?')).toBeInTheDocument()
|
expect(await screen.findByText('Cest quoi un rebase ?')).toBeInTheDocument()
|
||||||
expect(cardsApi.random).toHaveBeenCalledWith(undefined)
|
expect(cardsApi.random).toHaveBeenCalledWith(undefined)
|
||||||
expect(screen.queryByText(/R\u00e9appliquer des commits sur une autre base/)).not.toBeInTheDocument()
|
expect(
|
||||||
|
screen.queryByText(/R\u00e9appliquer des commits sur une autre base/),
|
||||||
|
).not.toBeInTheDocument()
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Voir la réponse' }))
|
await userEvent.click(screen.getByRole('button', { name: 'Voir la réponse' }))
|
||||||
expect(screen.getByText(/R\u00e9appliquer des commits sur une autre base/)).toBeInTheDocument()
|
expect(screen.getByText(/R\u00e9appliquer des commits sur une autre base/)).toBeInTheDocument()
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Masquer la réponse' }))
|
await userEvent.click(screen.getByRole('button', { name: 'Masquer la réponse' }))
|
||||||
expect(screen.queryByText(/R\u00e9appliquer des commits sur une autre base/)).not.toBeInTheDocument()
|
expect(
|
||||||
|
screen.queryByText(/R\u00e9appliquer des commits sur une autre base/),
|
||||||
|
).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tire uniquement dans le thème choisi (F-04) et gère un tirage vide (F-10 erreur)', async () => {
|
it('tire uniquement dans le thème choisi (F-04) et gère un tirage vide (F-10 erreur)', async () => {
|
||||||
@@ -59,7 +63,10 @@ it('tire uniquement dans le thème choisi (F-04) et gère un tirage vide (F-10 e
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('affiche un message si le thème ne contient aucune carte', async () => {
|
it('affiche un message si le thème ne contient aucune carte', async () => {
|
||||||
vi.mocked(cardsApi.random).mockRejectedValue(new ApiClientError(404, 'NotFoundError', 'Aucune carte'))
|
vi.mocked(cardsApi.themes).mockResolvedValue(['git', 'javascript', 'vide'])
|
||||||
|
vi.mocked(cardsApi.random).mockRejectedValue(
|
||||||
|
new ApiClientError(404, 'NotFoundError', 'Aucune carte'),
|
||||||
|
)
|
||||||
render(<PlayView />)
|
render(<PlayView />)
|
||||||
await userEvent.selectOptions(await screen.findByLabelText('Thème'), 'vide')
|
await userEvent.selectOptions(await screen.findByLabelText('Thème'), 'vide')
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Tirer une carte' }))
|
await userEvent.click(screen.getByRole('button', { name: 'Tirer une carte' }))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { ApiClientError, cardsApi } from '@/lib/api-client'
|
import { ApiClientError, cardsApi } from '@/lib/api-client'
|
||||||
import type { Card, CardInput } from '@/domain/card'
|
import type { Card } from '@/domain/card'
|
||||||
|
|
||||||
const EMPTY_THEME = ''
|
const EMPTY_THEME = ''
|
||||||
|
|
||||||
@@ -40,8 +40,7 @@ export function PlayView() {
|
|||||||
if (e instanceof ApiClientError) {
|
if (e instanceof ApiClientError) {
|
||||||
setError(e.message)
|
setError(e.message)
|
||||||
setCard(null)
|
setCard(null)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
setError('Impossible de tirer une carte')
|
setError('Impossible de tirer une carte')
|
||||||
setCard(null)
|
setCard(null)
|
||||||
}
|
}
|
||||||
@@ -82,7 +81,9 @@ export function PlayView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" className="text-red-600">{error}</p>
|
<p role="alert" className="text-red-600">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{card && (
|
{card && (
|
||||||
|
|||||||
Reference in new issue
Block a user