diff --git a/README.md b/README.md index f9b5c63..503a971 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,26 @@ Application React (Vite) : livres enregistrés localement (équivalent métier `POST /api/books`), React Router, `useContext`, Axios (Open Library). +## Ce qu’on a rajouté (branche Patrick_commande_promo, tout en français courant) + +En gros : on a continué sans brancher **aucun backend** du cours. Les “POST” sont simulés dans le navigateur avec du **React (Context)** et du **`localStorage`**. + +- **Commande** (`/commande`) : tu choisis des livres avec une quantité, tu peux entrer un code promo si tu en as créé une, et tu passes la commande. Ça revient métier à un `POST /api/orders`, sauf que tout est enregistré localement sous la clé `librairie-orders`. +- **Promotions** (`/promotions`) : tu crées des codes promo (pour l’instant c’est une remise en %), tu peux les activer / désactiver / supprimer. C’est le pendant local d’un `POST /api/promotions`, stocké sous `librairie-promotions`. +- **Prix des livres** : chaque livre a un champ **prix (€)** (formulaire, fiche détail, liste ; les anciens livres sans prix prennent 10 € par défaut). Les résultats ajoutés depuis Open Library partent aussi sur un prix par défaut. +- **Navigation** : dans le menu en haut, liens **Commande** et **Promotions** en plus de Mes livres / Recherche. + +## Ce qu’on a rajouté (branche Patrick_reserve_retourne__avis_livre, tout en français courant) + +Même logique que la branche d’au-dessus : **pas de backend** du projet, on simule les endpoints avec du **Context** + **`localStorage`**, comme si on faisait des `POST` / `GET` mais que tout reste dans le navigateur. + +- **Réserver un livre** (`POST /api/books/:id/reservations`) : tu vas sur la fiche d’un livre (`/` dans l’URL, ou en cliquant sur un titre), tu tapes sur **Réserver**. Une réservation **active** à la fois pour un même livre (sinon ça bloque, logique biblio pas folle sinon). Ça vit dans **`librairie-reservations`**. +- **Gérer le retour** (`POST /api/returns`) : page dédiée **Retours** (`/retours`) — vue “biblio qui récupère les exemplaires”. Tu vois les réservations pas encore rendues, tu cliques **Enregistrer le retour**, et ça part dans l’historique. Toujours le même stockage **`librairie-reservations`** que pour réserver : on ajoute juste une date de retour à l’entrée. +- **Avis sur un livre** (`POST /api/books/:id/reviews`) : toujours sur la fiche du livre, tu mets une **note sur 5** + un **petit pavé de texte**, tu publies, et tes avis s’affichent en dessous. Stocké sous **`librairie-reviews`** (un livre peut en avoir plusieurs, pas de problème). +- **Navigation** : on a rajouté le lien **Retours** dans la barre du haut avec le reste. + +Les données qui tournaient déjà (`librairie-books` pour le catalogue, puis `librairie-orders` et `librairie-promotions` si tu viens de l’autre branche) : **toujours zéro serveur**, tout est chez toi dans le navigateur, pas sur l’infra du cours. + ## Les membres du projet Marvin Aubert, Maxime Lebreton et Patrick Felix-Vimalaratnam @@ -49,5 +69,8 @@ npm run build **Tests manuels dans le navigateur** (après `npm run dev`) : 1. **Mes livres** (`/`) : enregistrer un nouveau livre (libellés + `POST /api/books` côté UI), supprimer, filtrer (Tous / Lus / À lire), rechercher dans la liste. -2. **Fiche livre** : cliquer sur le titre d’un livre ou aller sur `/` ; vérifier lu / non lu et suppression avec retour à **Mes livres**. +2. **Fiche livre** (`/`) : lu / non lu, suppression, **Réserver**, publier un **avis** puis vérifier l’historique sous le formulaire. 3. **Recherche** (`/recherche`) : recherche Open Library (Internet), enregistrer un résultat (même logique que `POST /api/books` en local), puis vérifier qu’il apparaît sous **Mes livres**. +4. **Promotions** (`/promotions`) : créer un code promo (ex. `BUT10` avec −10 %), puis vérifier que tu peux le désactiver / le réactiver / le supprimer. +5. **Commande** (`/commande`) : mettre au moins un livre avec une quantité strictement positive, tester sans promo puis avec le code créé avant ; passer la commande et vérifier le message + que le sous-total, la remise et le total sont cohérents (toujours en local). +6. **Retours** (`/retours`) : après une réservation, vérifier que le livre apparaît puis **Enregistrer le retour** ; vérifier l’historique et que tu peux de nouveau réserver le même titre. diff --git a/src/App.css b/src/App.css index b790e26..66a82fa 100644 --- a/src/App.css +++ b/src/App.css @@ -593,3 +593,345 @@ flex-wrap: wrap; gap: 0.5rem; } + +.page-title { + font-family: var(--font-display); + margin: 0 0 0.5rem; + color: var(--ink); +} + +.page-lead { + margin: 0 0 1.25rem; + color: var(--ink-muted); + max-width: 60ch; +} + +.form-error { + color: var(--danger); + margin: 0 0 1rem; +} + +.form-notice { + margin: 0 0 1rem; + color: var(--ink); +} + +.order-grid, +.promo-grid { + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: 1.25rem; + align-items: start; +} + +@media (max-width: 900px) { + .order-grid, + .promo-grid { + grid-template-columns: 1fr; + } +} + +.order-panel { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem 1.1rem; + box-shadow: var(--shadow); +} + +.panel-title { + font-family: var(--font-display); + margin: 0 0 0.75rem; + font-size: 1.1rem; +} + +.order-catalog, +.promo-list, +.order-summary { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.order-catalog-item, +.promo-item { + border: 1px solid var(--border); + border-radius: 12px; + padding: 0.75rem 0.85rem; + background: var(--paper); +} + +.order-catalog-main, +.promo-item-main { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.order-book-title, +.promo-code { + margin: 0; + font-weight: 700; +} + +.order-book-meta, +.promo-meta { + margin: 0.2rem 0 0; + color: var(--ink-muted); + font-size: 0.9rem; +} + +.qty { + display: grid; + gap: 0.25rem; + align-items: center; + justify-items: end; +} + +.qty-label { + font-size: 0.75rem; + color: var(--ink-muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.qty input, +.promo-input, +.promo-form input { + font: inherit; + padding: 0.45rem 0.55rem; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--card); + color: var(--ink); + width: 7rem; +} + +.promo-input { + width: 100%; +} + +.order-summary-item { + display: flex; + justify-content: space-between; + gap: 1rem; + color: var(--ink); +} + +.promo-box { + margin: 1rem 0; + padding: 0.9rem; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--paper); +} + +.promo-label { + display: grid; + gap: 0.35rem; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--ink-muted); +} + +.promo-hint { + margin: 0.55rem 0 0; + font-size: 0.9rem; + color: var(--ink-muted); +} + +.promo-warn { + margin: 0.55rem 0 0; + color: var(--danger); +} + +.promo-ok { + margin: 0.55rem 0 0; +} + +.totals { + border-top: 1px solid var(--border); + padding-top: 0.9rem; + margin-top: 0.9rem; + margin-bottom: 0.9rem; +} + +.totals-row { + display: flex; + justify-content: space-between; + margin: 0.25rem 0; + color: var(--ink-muted); +} + +.totals-row.total { + color: var(--ink); + font-weight: 800; + margin-top: 0.6rem; +} + +.promo-form { + display: grid; + gap: 0.75rem; +} + +.promo-form label { + display: grid; + gap: 0.35rem; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--ink-muted); +} + +.promo-actions { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + justify-content: flex-end; +} + +.book-detail .badge + .badge { + margin-left: 0.35rem; +} + +.book-section { + margin: 1.5rem 0; + padding: 1rem 1.1rem; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + max-width: 520px; + text-align: left; +} + +.book-section-title { + font-family: var(--font-display); + font-size: 1.1rem; + margin: 0 0 0.35rem; + color: var(--ink); +} + +.book-section-lead { + margin: 0 0 1rem; + font-size: 0.88rem; + color: var(--ink-muted); +} + +.book-section-lead code { + font-size: 0.85em; +} + +.book-section-link { + display: inline-block; + margin-top: 0.75rem; +} + +.review-form { + display: grid; + gap: 0.75rem; +} + +.review-form label { + display: grid; + gap: 0.35rem; + font-size: 0.8rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--ink-muted); +} + +.review-form select, +.review-form textarea { + font: inherit; + padding: 0.55rem 0.65rem; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--paper); + color: var(--ink); +} + +.book-reviews-empty { + margin: 1rem 0 0; +} + +.reviews-list { + list-style: none; + margin: 1rem 0 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.reviews-item { + padding: 0.75rem 0.85rem; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--paper); +} + +.reviews-meta { + margin: 0 0 0.35rem; + font-size: 0.85rem; + color: var(--ink-muted); +} + +.reviews-text { + margin: 0; + font-size: 0.95rem; + white-space: pre-wrap; +} + +.retours-page { + text-align: left; +} + +.retours-panel { + margin-bottom: 1.25rem; +} + +.retours-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.65rem; +} + +.retours-item { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.75rem 0.85rem; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--paper); +} + +.retours-item.muted { + opacity: 0.82; +} + +.retours-book { + margin: 0; + font-weight: 700; +} + +.retours-meta, +.retours-id { + margin: 0.2rem 0 0; + font-size: 0.88rem; + color: var(--ink-muted); +} diff --git a/src/App.jsx b/src/App.jsx index c69dcfb..5d362fe 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,10 @@ import { Route, Routes } from 'react-router-dom' import { RootLayout } from './layout/RootLayout.jsx' import BookDetailPage from './pages/BookDetailPage.jsx' +import CommandePage from './pages/CommandePage.jsx' import MesLivresPage from './pages/MesLivresPage.jsx' +import PromotionsPage from './pages/PromotionsPage.jsx' +import RetoursPage from './pages/RetoursPage.jsx' import SearchPage from './pages/SearchPage.jsx' import './App.css' @@ -11,6 +14,9 @@ export default function App() { }> } /> } /> + } /> + } /> + } /> } /> diff --git a/src/components/BookForm.jsx b/src/components/BookForm.jsx index 9a44a28..b15acce 100644 --- a/src/components/BookForm.jsx +++ b/src/components/BookForm.jsx @@ -5,6 +5,7 @@ const emptyForm = () => ({ author: '', year: new Date().getFullYear(), genre: '', + price: 10, read: false, }) @@ -71,6 +72,22 @@ export function BookForm({ onSubmit }) { placeholder="Roman, essai…" /> +