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 type { Metadata, Viewport } from 'next'
|
||||
import type { ReactNode } from 'react'
|
||||
import { AppHeader } from '@/components/app-header'
|
||||
import './globals.css'
|
||||
|
||||
const APP_NAME = 'flashcards'
|
||||
@@ -55,6 +56,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
<html lang="fr">
|
||||
<body>
|
||||
<SerwistProvider swUrl="/serwist/sw.js" disable={process.env.NODE_ENV === 'development'}>
|
||||
<AppHeader />
|
||||
{children}
|
||||
</SerwistProvider>
|
||||
</body>
|
||||
|
||||
+12
-2
@@ -3,8 +3,18 @@ import { describe, expect, it } from 'vitest'
|
||||
import Home from './page'
|
||||
|
||||
describe('Home', () => {
|
||||
it('affiche le contenu minimal', () => {
|
||||
it('affiche un titre et un lien vers le jeu', () => {
|
||||
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() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Hello world</h1>
|
||||
<main className="mx-auto max-w-2xl px-4 py-16">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -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 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'
|
||||
|
||||
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' }))
|
||||
expect(await screen.findByText('Cest quoi un rebase ?')).toBeInTheDocument()
|
||||
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' }))
|
||||
expect(screen.getByText(/R\u00e9appliquer des commits sur une autre base/)).toBeInTheDocument()
|
||||
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 () => {
|
||||
@@ -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 () => {
|
||||
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 />)
|
||||
await userEvent.selectOptions(await screen.findByLabelText('Thème'), 'vide')
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Tirer une carte' }))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ApiClientError, cardsApi } from '@/lib/api-client'
|
||||
import type { Card, CardInput } from '@/domain/card'
|
||||
import type { Card } from '@/domain/card'
|
||||
|
||||
const EMPTY_THEME = ''
|
||||
|
||||
@@ -40,8 +40,7 @@ export function PlayView() {
|
||||
if (e instanceof ApiClientError) {
|
||||
setError(e.message)
|
||||
setCard(null)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
setError('Impossible de tirer une carte')
|
||||
setCard(null)
|
||||
}
|
||||
@@ -82,7 +81,9 @@ export function PlayView() {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="text-red-600">{error}</p>
|
||||
<p role="alert" className="text-red-600">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{card && (
|
||||
|
||||
Reference in new issue
Block a user