mise en place de l'application de base
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useBooks } from '../context/BooksContext.jsx'
|
||||
|
||||
export default function BookDetailPage() {
|
||||
const { id } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const { books, toggleRead, removeBook } = useBooks()
|
||||
const book = books.find((b) => b.id === id)
|
||||
|
||||
if (!book) {
|
||||
return (
|
||||
<div className="book-detail">
|
||||
<p className="book-detail-missing">Livre introuvable.</p>
|
||||
<Link to="/" className="nav-link">
|
||||
← Retour à mes livres
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function handleDelete() {
|
||||
removeBook(book.id)
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="book-detail">
|
||||
<Link to="/" className="nav-link book-detail-back">
|
||||
← Catalogue
|
||||
</Link>
|
||||
<span className={`badge ${book.read ? 'read' : 'unread'}`}>
|
||||
{book.read ? 'Lu' : 'À lire'}
|
||||
</span>
|
||||
<h2 className="book-detail-title">{book.title}</h2>
|
||||
<p className="book-detail-author">{book.author}</p>
|
||||
<dl className="book-detail-meta">
|
||||
<div>
|
||||
<dt>Année</dt>
|
||||
<dd>{book.year}</dd>
|
||||
</div>
|
||||
{book.genre ? (
|
||||
<div>
|
||||
<dt>Genre</dt>
|
||||
<dd>{book.genre}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<dt>Identifiant</dt>
|
||||
<dd>
|
||||
<code>{book.id}</code>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="book-detail-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
onClick={() => toggleRead(book.id)}
|
||||
>
|
||||
{book.read ? 'Marquer non lu' : 'Marquer lu'}
|
||||
</button>
|
||||
<button type="button" className="btn danger" onClick={handleDelete}>
|
||||
Supprimer de mes livres
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { BookForm } from '../components/BookForm.jsx'
|
||||
import { BookList } from '../components/BookList.jsx'
|
||||
import { useBooks } from '../context/BooksContext.jsx'
|
||||
|
||||
export default function MesLivresPage() {
|
||||
const { books, postBook, removeBook, toggleRead } = useBooks()
|
||||
const [filter, setFilter] = useState('all')
|
||||
const [query, setQuery] = useState('')
|
||||
|
||||
const stats = useMemo(() => {
|
||||
const read = books.filter((b) => b.read).length
|
||||
return { total: books.length, read, unread: books.length - read }
|
||||
}, [books])
|
||||
|
||||
return (
|
||||
<div className="mes-livres-page">
|
||||
<div className="mes-livres-intro">
|
||||
<p className="tagline mes-livres-tagline">
|
||||
{stats.total} livre{stats.total !== 1 ? 's' : ''} enregistré
|
||||
{stats.total !== 1 ? 's' : ''} — {stats.read} lu
|
||||
{stats.read !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="app-main">
|
||||
<aside className="sidebar">
|
||||
<BookForm onSubmit={postBook} />
|
||||
</aside>
|
||||
<BookList
|
||||
books={books}
|
||||
filter={filter}
|
||||
query={query}
|
||||
onFilterChange={setFilter}
|
||||
onQueryChange={setQuery}
|
||||
onToggleRead={toggleRead}
|
||||
onRemove={removeBook}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import axios from 'axios'
|
||||
import { useState } from 'react'
|
||||
import { useBooks } from '../context/BooksContext.jsx'
|
||||
|
||||
const OPEN_LIBRARY_SEARCH = 'https://openlibrary.org/search.json'
|
||||
|
||||
function docToBookPayload(doc) {
|
||||
return {
|
||||
title: doc.title || 'Sans titre',
|
||||
author: Array.isArray(doc.author_name)
|
||||
? doc.author_name[0]
|
||||
: 'Auteur inconnu',
|
||||
year:
|
||||
typeof doc.first_publish_year === 'number'
|
||||
? doc.first_publish_year
|
||||
: new Date().getFullYear(),
|
||||
genre: Array.isArray(doc.subject) ? doc.subject[0] : '',
|
||||
read: false,
|
||||
}
|
||||
}
|
||||
|
||||
function resultKey(doc, index) {
|
||||
return doc.key || doc.cover_edition_key || `${doc.title || 'work'}-${index}`
|
||||
}
|
||||
|
||||
export default function SearchPage() {
|
||||
const { postBook } = useBooks()
|
||||
const [q, setQ] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [docs, setDocs] = useState([])
|
||||
const [addedKeys, setAddedKeys] = useState(() => new Set())
|
||||
const [lastQuery, setLastQuery] = useState('')
|
||||
|
||||
async function handleSearch(e) {
|
||||
e.preventDefault()
|
||||
const term = q.trim()
|
||||
if (!term) return
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setDocs([])
|
||||
setAddedKeys(() => new Set())
|
||||
setLastQuery(term)
|
||||
try {
|
||||
const { data } = await axios.get(OPEN_LIBRARY_SEARCH, {
|
||||
params: { q: term, limit: 15 },
|
||||
timeout: 15000,
|
||||
})
|
||||
setDocs(Array.isArray(data.docs) ? data.docs : [])
|
||||
} catch (err) {
|
||||
setError(
|
||||
err.response?.data?.error ||
|
||||
err.message ||
|
||||
'Impossible de contacter Open Library.',
|
||||
)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd(doc, index) {
|
||||
postBook(docToBookPayload(doc))
|
||||
const k = resultKey(doc, index)
|
||||
setAddedKeys((prev) => new Set(prev).add(k))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="search-page">
|
||||
<h2 className="search-page-title">Recherche Open Library</h2>
|
||||
<p className="search-page-lead">
|
||||
Interroge l’API publique Open Library avec Axios, puis ajoute un
|
||||
ouvrage à ton catalogue local.
|
||||
</p>
|
||||
<form className="search-form" onSubmit={handleSearch}>
|
||||
<input
|
||||
type="search"
|
||||
className="search search-wide"
|
||||
placeholder="Titre, auteur, sujet…"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
aria-label="Requête Open Library"
|
||||
/>
|
||||
<button type="submit" className="btn primary" disabled={loading}>
|
||||
{loading ? 'Recherche…' : 'Rechercher'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error ? (
|
||||
<p className="search-error" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!loading && docs.length === 0 && !error && !lastQuery ? (
|
||||
<p className="search-hint">Saisis un mot-clé et lance la recherche.</p>
|
||||
) : null}
|
||||
|
||||
{!loading && docs.length === 0 && !error && lastQuery ? (
|
||||
<p className="search-hint">Aucun résultat pour « {lastQuery} ».</p>
|
||||
) : null}
|
||||
|
||||
{docs.length > 0 ? (
|
||||
<ul className="ol-results">
|
||||
{docs.map((doc, i) => {
|
||||
const key = resultKey(doc, i)
|
||||
const year =
|
||||
typeof doc.first_publish_year === 'number'
|
||||
? doc.first_publish_year
|
||||
: '—'
|
||||
const authors = Array.isArray(doc.author_name)
|
||||
? doc.author_name.slice(0, 2).join(', ')
|
||||
: '—'
|
||||
const justAdded = addedKeys.has(key)
|
||||
return (
|
||||
<li key={key} className="ol-result-card">
|
||||
<h3 className="ol-result-title">
|
||||
{doc.title || 'Sans titre'}
|
||||
</h3>
|
||||
<p className="ol-result-meta">
|
||||
{authors} · {year}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="btn small primary"
|
||||
onClick={() => handleAdd(doc, i)}
|
||||
disabled={justAdded}
|
||||
>
|
||||
{justAdded ? 'Enregistré' : 'Enregistrer (POST /api/books)'}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user