mise en place de l'application de base

This commit is contained in:
felix-vi
2026-03-22 18:45:37 +01:00
commit eddb103755
18 changed files with 6038 additions and 0 deletions
+90
View File
@@ -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>
)
}
+102
View File
@@ -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>
)
}