mise en place de l'application de base
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
const emptyForm = () => ({
|
||||
title: '',
|
||||
author: '',
|
||||
year: new Date().getFullYear(),
|
||||
genre: '',
|
||||
read: false,
|
||||
})
|
||||
|
||||
export function BookForm({ onSubmit }) {
|
||||
const [form, setForm] = useState(() => emptyForm())
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
if (!form.title.trim() || !form.author.trim()) return
|
||||
onSubmit(form)
|
||||
setForm(emptyForm())
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="book-form" onSubmit={handleSubmit}>
|
||||
<h2>Enregistrer un nouveau livre</h2>
|
||||
<p className="book-form-story">Je veux enregistrer un nouveau livre</p>
|
||||
<p className="book-form-api">
|
||||
<code>POST /api/books</code>
|
||||
<span className="book-form-api-note">
|
||||
{' '}
|
||||
(dans cette appli : enregistrement local, pas de serveur)
|
||||
</span>
|
||||
</p>
|
||||
<div className="form-grid">
|
||||
<label>
|
||||
Titre
|
||||
<input
|
||||
required
|
||||
value={form.title}
|
||||
onChange={(e) => setForm((f) => ({ ...f, title: e.target.value }))}
|
||||
placeholder="Ex. Les Misérables"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Auteur·ice
|
||||
<input
|
||||
required
|
||||
value={form.author}
|
||||
onChange={(e) => setForm((f) => ({ ...f, author: e.target.value }))}
|
||||
placeholder="Ex. Victor Hugo"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Année
|
||||
<input
|
||||
type="number"
|
||||
min={1000}
|
||||
max={2100}
|
||||
value={form.year}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({
|
||||
...f,
|
||||
year: Number(e.target.value) || f.year,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Genre
|
||||
<input
|
||||
value={form.genre}
|
||||
onChange={(e) => setForm((f) => ({ ...f, genre: e.target.value }))}
|
||||
placeholder="Roman, essai…"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="checkbox-row">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.read}
|
||||
onChange={(e) => setForm((f) => ({ ...f, read: e.target.checked }))}
|
||||
/>
|
||||
Déjà lu
|
||||
</label>
|
||||
<div className="form-actions">
|
||||
<button type="submit" className="btn primary">
|
||||
Enregistrer (POST)
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const TABS = [
|
||||
['all', 'Tous'],
|
||||
['read', 'Lus'],
|
||||
['unread', 'À lire'],
|
||||
]
|
||||
|
||||
export function BookList({
|
||||
books,
|
||||
filter,
|
||||
query,
|
||||
onFilterChange,
|
||||
onQueryChange,
|
||||
onToggleRead,
|
||||
onRemove,
|
||||
}) {
|
||||
const q = query.trim().toLowerCase()
|
||||
const filtered = books.filter((b) => {
|
||||
if (filter === 'read' && !b.read) return false
|
||||
if (filter === 'unread' && b.read) return false
|
||||
if (!q) return true
|
||||
return (
|
||||
b.title.toLowerCase().includes(q) ||
|
||||
b.author.toLowerCase().includes(q) ||
|
||||
(b.genre && b.genre.toLowerCase().includes(q))
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<section className="book-list-section">
|
||||
<div className="toolbar">
|
||||
<input
|
||||
type="search"
|
||||
className="search"
|
||||
placeholder="Rechercher par titre, auteur ou genre…"
|
||||
value={query}
|
||||
onChange={(e) => onQueryChange(e.target.value)}
|
||||
aria-label="Recherche"
|
||||
/>
|
||||
<div className="filter-tabs" role="tablist">
|
||||
{TABS.map(([value, label]) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={filter === value}
|
||||
className={filter === value ? 'tab active' : 'tab'}
|
||||
onClick={() => onFilterChange(value)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className="empty-state">
|
||||
{books.length === 0
|
||||
? 'Aucun livre enregistré. Utilisez le formulaire à gauche : Je veux enregistrer un nouveau livre (POST /api/books).'
|
||||
: 'Aucun résultat pour ces critères.'}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="book-grid">
|
||||
{filtered.map((book) => (
|
||||
<li key={book.id} className="book-card">
|
||||
<div className="book-card-top">
|
||||
<span className={`badge ${book.read ? 'read' : 'unread'}`}>
|
||||
{book.read ? 'Lu' : 'À lire'}
|
||||
</span>
|
||||
{book.genre ? <span className="genre">{book.genre}</span> : null}
|
||||
</div>
|
||||
<h3>
|
||||
<Link className="book-title-link" to={`/${book.id}`}>
|
||||
{book.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="author">{book.author}</p>
|
||||
<p className="year">{book.year}</p>
|
||||
<div className="card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn small"
|
||||
onClick={() => onToggleRead(book.id)}
|
||||
>
|
||||
{book.read ? 'Marquer non lu' : 'Marquer lu'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn small danger"
|
||||
onClick={() => onRemove(book.id)}
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user