+ {book.year} · {Number(book.price ?? 10).toFixed(2)} €
+
Genre
diff --git a/src/pages/CommandePage.jsx b/src/pages/CommandePage.jsx
new file mode 100644
index 0000000..f00c76e
--- /dev/null
+++ b/src/pages/CommandePage.jsx
@@ -0,0 +1,211 @@
+import { useMemo, useState } from 'react'
+import { useBooks } from '../context/BooksContext.jsx'
+import { useOrders } from '../context/OrdersContext.jsx'
+import { usePromotions } from '../context/PromotionsContext.jsx'
+
+function formatEUR(amount) {
+ const n = Number(amount) || 0
+ return new Intl.NumberFormat('fr-FR', {
+ style: 'currency',
+ currency: 'EUR',
+ maximumFractionDigits: 2,
+ }).format(n)
+}
+
+function toQtyMap(items) {
+ const map = new Map()
+ for (const it of items) map.set(it.bookId, it.qty)
+ return map
+}
+
+export default function CommandePage() {
+ const { books } = useBooks()
+ const { createOrder } = useOrders()
+ const { getActivePromotionByCode, normalizeCode } = usePromotions()
+
+ const [qtyByBookId, setQtyByBookId] = useState(() => ({}))
+ const [promoInput, setPromoInput] = useState('')
+ const [notice, setNotice] = useState(null)
+ const [error, setError] = useState(null)
+
+ const items = useMemo(() => {
+ return books
+ .map((b) => {
+ const qty = Math.max(0, Math.trunc(Number(qtyByBookId[b.id]) || 0))
+ const unitPrice = Number(b.price)
+ return {
+ bookId: b.id,
+ title: b.title,
+ unitPrice: Number.isFinite(unitPrice) ? unitPrice : 10,
+ qty,
+ }
+ })
+ .filter((it) => it.qty > 0)
+ }, [books, qtyByBookId])
+
+ const subtotal = useMemo(() => {
+ return items.reduce((sum, it) => sum + it.unitPrice * it.qty, 0)
+ }, [items])
+
+ const promo = useMemo(() => {
+ const code = normalizeCode(promoInput)
+ if (!code) return null
+ return getActivePromotionByCode(code)
+ }, [promoInput, getActivePromotionByCode, normalizeCode])
+
+ const discount = useMemo(() => {
+ if (!promo) return 0
+ return (subtotal * promo.value) / 100
+ }, [promo, subtotal])
+
+ const total = useMemo(
+ () => Math.max(0, subtotal - discount),
+ [subtotal, discount],
+ )
+
+ function setQty(bookId, nextQty) {
+ setQtyByBookId((prev) => ({ ...prev, [bookId]: nextQty }))
+ }
+
+ function handleSubmit() {
+ setError(null)
+ setNotice(null)
+ try {
+ const order = createOrder({
+ items,
+ promotion: promo
+ ? { code: promo.code, type: promo.type, value: promo.value }
+ : null,
+ })
+ setQtyByBookId({})
+ setPromoInput('')
+ const map = toQtyMap(order.items)
+ setNotice(
+ `Commande ${order.id.slice(0, 8)} créée (${map.size} livre${
+ map.size > 1 ? 's' : ''
+ }). Total: ${formatEUR(order.total)}.`,
+ )
+ } catch (e) {
+ setError(e?.message || 'Impossible de créer la commande.')
+ }
+ }
+
+ return (
+
+
Passer une commande
+
+ Équivalent local de POST /api/orders (aucun backend).
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+ {notice ?
{notice}
: null}
+
+ {books.length === 0 ? (
+
+ Aucun livre dans le catalogue. Ajoute un livre dans “Mes livres”
+ d’abord.
+
+ ) : (
+
+
+ Catalogue
+
+ {books.map((b) => (
+ -
+
+
+
{b.title}
+
+ {b.author} · {formatEUR(b.price ?? 10)}
+
+
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ )
+}
+
diff --git a/src/pages/PromotionsPage.jsx b/src/pages/PromotionsPage.jsx
new file mode 100644
index 0000000..b931cc0
--- /dev/null
+++ b/src/pages/PromotionsPage.jsx
@@ -0,0 +1,118 @@
+import { useMemo, useState } from 'react'
+import { usePromotions } from '../context/PromotionsContext.jsx'
+
+export default function PromotionsPage() {
+ const { promotions, createPromotion, removePromotion, setPromotionActive } =
+ usePromotions()
+
+ const [code, setCode] = useState('')
+ const [value, setValue] = useState(10)
+ const [notice, setNotice] = useState(null)
+ const [error, setError] = useState(null)
+
+ const sorted = useMemo(() => {
+ return [...promotions].sort((a, b) => {
+ if (a.active !== b.active) return a.active ? -1 : 1
+ return String(a.code).localeCompare(String(b.code))
+ })
+ }, [promotions])
+
+ function handleSubmit(e) {
+ e.preventDefault()
+ setError(null)
+ setNotice(null)
+ try {
+ createPromotion({ code, value })
+ setNotice('Promotion créée.')
+ setCode('')
+ setValue(10)
+ } catch (err) {
+ setError(err?.message || 'Impossible de créer la promotion.')
+ }
+ }
+
+ return (
+
+
Promotions
+
+ Équivalent local de POST /api/promotions (aucun backend).
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+ {notice ?
{notice}
: null}
+
+
+
+ Créer une promotion
+
+
+ Astuce : le code est normalisé (majuscules, sans espaces).
+
+
+
+
+ Promotions existantes
+ {sorted.length === 0 ? (
+ Aucune promotion créée.
+ ) : (
+
+ {sorted.map((p) => (
+ -
+
+
+
{p.code}
+
−{p.value}%
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ )
+}
+
diff --git a/src/pages/SearchPage.jsx b/src/pages/SearchPage.jsx
index e158f6b..1897229 100644
--- a/src/pages/SearchPage.jsx
+++ b/src/pages/SearchPage.jsx
@@ -15,6 +15,7 @@ function docToBookPayload(doc) {
? doc.first_publish_year
: new Date().getFullYear(),
genre: Array.isArray(doc.subject) ? doc.subject[0] : '',
+ price: 10,
read: false,
}
}