21 Commits

Author SHA1 Message Date
AISSI-JUDE-CHRIST 52213fce0b re Correction page login 2026-06-12 21:37:12 +02:00
AISSI-JUDE-CHRIST 7117aba838 Correction page login 2026-06-12 21:35:40 +02:00
AISSI-JUDE-CHRIST 010ec9c11a reservation livres 2026-06-11 23:08:21 +02:00
AISSI-JUDE-CHRIST de47c293b8 correction affiche livre 2026-06-11 22:36:47 +02:00
AISSI-JUDE-CHRIST 4ae227d79d ajout authentification admin/user 2026-06-11 22:23:39 +02:00
Adrien DICK 8f3451adc0 Merge pull request 'Gestion client' (#5) from GestionClient into main
Reviewed-on: #5
2026-06-11 16:49:36 +02:00
Riad KARA-MOSTEFA 9916b02ef6 Merge pull request 'Api java' (#4) from api_java into main
Reviewed-on: #4
2026-06-11 14:28:28 +02:00
RKaraMos_EVO 1aa83b0911 ajout enum Category et record BookInfo 2026-06-11 14:23:15 +02:00
RKaraMos_EVO abbd3ee95d ajout du module customer fourni par le prof 2026-06-11 14:22:59 +02:00
RKaraMos_EVO efa6fc49eb ajout du pom et du gitignore pour le module 2026-06-11 14:22:13 +02:00
Adrien DICK e33acdf151 Ajouter et Retirer des points de fidélité 2026-06-11 07:59:03 -04:00
Adrien DICK ccafc24f72 Ajouter un client 2026-06-11 07:27:37 -04:00
Adrien DICK c811c373d5 Merge pull request ' Voir le detail d'un livre' (#3) from DetailLivre into main
Reviewed-on: #3
2026-06-11 11:55:29 +02:00
Adrien DICK 1f9a1e9af8 Voir le detail d'un livre 2026-06-11 05:52:26 -04:00
Adrien DICK e438373370 Merge pull request ' Enregistrer un livre' (#2) from EnregistrerLivre into main
Reviewed-on: #2
2026-06-11 11:35:23 +02:00
Adrien DICK 2c0f30f0a2 Enregistrer un livre 2026-06-11 05:15:30 -04:00
Adrien DICK 4008ef4a9b Merge pull request 'Remise au propre du projet' (#1) from dick into main
Reviewed-on: #1
2026-06-11 10:34:19 +02:00
Adrien DICK b99dcd8592 Get api/books pour la page catalogue 2026-06-11 04:32:20 -04:00
Adrien DICK 4e1a0b24f4 🎨 Remise au propre 2026-06-10 16:24:37 -04:00
Riad KARA-MOSTEFA 9318d1c28b Merge branch 'main' of https://grond.iut-fbleau.fr/aissi/2026-DEV-BUT3 2026-03-16 16:45:01 +01:00
Riad KARA-MOSTEFA 038c52efc0 debut projet 2026-03-16 16:43:17 +01:00
66 changed files with 20593 additions and 0 deletions
Binary file not shown.
+23
View File
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+70
View File
@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
+17341
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
{
"name": "my-library",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.17.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+14
View File
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Biblio</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400;1,600&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+25
View File
@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+3
View File
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
+38
View File
@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+44
View File
@@ -0,0 +1,44 @@
import { Routes, Route, Navigate } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import Books from './pages/Books';
import Orders from './pages/Orders';
import Profile from './pages/Profile';
import NotFound from './pages/NotFound';
import AddBook from './pages/AddBook';
import BookDetail from './pages/BookDetail';
import Customers from './pages/Customers';
import Login from './pages/Login';
import Reservations from './pages/Reservations';
import { useAuth } from './context/AuthContext';
function RequireAuth({ children }) {
const { user } = useAuth();
return user ? children : <Navigate to="/login" replace />;
}
function RequireAdmin({ children }) {
const { user } = useAuth();
if (!user) return <Navigate to="/login" replace />;
if (user.role !== 'admin') return <Navigate to="/" replace />;
return children;
}
export default function App() {
return (
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="books" element={<Books />} />
<Route path="books/:bookId" element={<RequireAuth><BookDetail /></RequireAuth>} />
<Route path="books/new" element={<RequireAdmin><AddBook /></RequireAdmin>} />
<Route path="orders" element={<RequireAuth><Orders /></RequireAuth>} />
<Route path="reservations" element={<RequireAuth><Reservations /></RequireAuth>} />
<Route path="profile" element={<RequireAuth><Profile /></RequireAuth>} />
<Route path="customers" element={<RequireAdmin><Customers /></RequireAdmin>} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
+8
View File
@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+17
View File
@@ -0,0 +1,17 @@
import client from './client';
export function getBooks(page = 0, size = 20) {
return client.get('/api/books', { params: { page, size } });
}
export function registerBook(book) {
return client.post('/api/books', book);
}
export function getBookById(id) {
return client.get(`/api/books/${id}`);
}
export function reserveBook(bookId, reservation) {
return client.post(`/api/books/${bookId}/reservations`, reservation);
}
+7
View File
@@ -0,0 +1,7 @@
import axios from 'axios';
const client = axios.create({
baseURL: 'http://localhost:8080',
});
export default client;
+17
View File
@@ -0,0 +1,17 @@
import client from './client';
export function registerCustomer(customer) {
return client.post('/api/customers', customer);
}
export function findCustomerByPhone(phoneNumber) {
return client.get(`/api/customers/phone/${encodeURIComponent(phoneNumber)}`);
}
export function addLoyaltyPoints(customerId, points) {
return client.post(`/api/customers/${customerId}/loyalty/add`, null, { params: { points } });
}
export function subtractLoyaltyPoints(customerId, points) {
return client.post(`/api/customers/${customerId}/loyalty/subtract`, null, { params: { points } });
}
+11
View File
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom';
import Navbar from './Navbar';
export default function Layout() {
return (
<>
<Navbar />
<Outlet />
</>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import '../styles/navbar.css';
export default function Navbar() {
const { user, logout } = useAuth();
const navigate = useNavigate();
function handleLogout() {
logout();
navigate('/login');
}
return (
<nav className="navbar">
<Link to="/" className="navbar__logo">Biblio</Link>
<ul className="navbar__links">
<li><Link to="/">Accueil</Link></li>
<li><Link to="/books">Catalogue</Link></li>
{user && <li><Link to="/orders">Commandes</Link></li>}
{user?.role === 'user' && <li><Link to="/reservations">Mes réservations</Link></li>}
{user && <li><Link to="/profile">Mon compte</Link></li>}
{user?.role === 'admin' && <li><Link to="/customers">Clients</Link></li>}
</ul>
<div className="navbar__actions">
{user ? (
<>
<span>{user.username} ({user.role})</span>
<button onClick={handleLogout}>Déconnexion</button>
</>
) : (
<Link to="/login"><button>Connexion</button></Link>
)}
</div>
</nav>
);
}
+39
View File
@@ -0,0 +1,39 @@
import { createContext, useContext, useState } from 'react';
const USERS = [
{ username: 'admin', password: 'admin', role: 'admin' },
{ username: 'alice', password: 'bob', role: 'user' },
];
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(() => {
const saved = localStorage.getItem('auth');
return saved ? JSON.parse(saved) : null;
});
function login(username, password) {
const found = USERS.find(u => u.username === username && u.password === password);
if (!found) return false;
const { password: _, ...safe } = found;
setUser(safe);
localStorage.setItem('auth', JSON.stringify(safe));
return true;
}
function logout() {
setUser(null);
localStorage.removeItem('auth');
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
return useContext(AuthContext);
}
@@ -0,0 +1,42 @@
import { createContext, useContext, useState } from 'react';
const ReservationContext = createContext(null);
export function ReservationProvider({ children }) {
const [reservations, setReservations] = useState(() => {
const saved = localStorage.getItem('reservations');
return saved ? JSON.parse(saved) : [];
});
function addReservation(book, phoneNumber) {
const reservation = {
reservationId: crypto.randomUUID(),
bookId: book.isbn,
bookTitle: book.title,
bookAuthor: book.author,
phoneNumber,
status: 'CONFIRMED',
reservedAt: new Date().toISOString(),
};
const updated = [...reservations, reservation];
setReservations(updated);
localStorage.setItem('reservations', JSON.stringify(updated));
return reservation;
}
function cancelReservation(reservationId) {
const updated = reservations.filter(r => r.reservationId !== reservationId);
setReservations(updated);
localStorage.setItem('reservations', JSON.stringify(updated));
}
return (
<ReservationContext.Provider value={{ reservations, addReservation, cancelReservation }}>
{children}
</ReservationContext.Provider>
);
}
export function useReservations() {
return useContext(ReservationContext);
}
View File
+20
View File
@@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ReservationProvider } from './context/ReservationContext';
import App from './App';
import './styles/global.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<ReservationProvider>
<App />
</ReservationProvider>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 KiB

+105
View File
@@ -0,0 +1,105 @@
import { useState } from 'react';
import { registerBook } from '../api/books';
const CATEGORIES = ['FICTION', 'NON_FICTION', 'SCIENCE_FICTION', 'FANTASY', 'MYSTERY', 'THRILLER', 'ROMANCE', 'BIOGRAPHY', 'HISTORY', 'POETRY', 'CHILDRENS', 'YOUNG_ADULT', 'SCIENCE', 'PHILOSOPHY', 'SELF_HELP', 'TRAVEL', 'COOKING', 'ART', 'RELIGION', 'REFERENCE'];
const initialForm = {
isbn: '', title: '', author: '', publisher: '', publicationDate: '', price: '', quantity: '', categories: [], description: '', language: '',
};
export default function AddBook() {
const [form, setForm] = useState(initialForm);
const [message, setMessage] = useState(null);
const [submitting, setSubmitting] = useState(false);
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleCategoryChange(e) {
setForm((prev) => ({ ...prev, categories: e.target.value ? [e.target.value] : [] }));
}
function handleSubmit(e) {
e.preventDefault();
setSubmitting(true);
setMessage(null);
const payload = {
...form,
isbn: Number(form.isbn),
price: Number(form.price),
quantity: Number(form.quantity),
};
registerBook(payload)
.then((response) => {
setMessage({ type: 'success', text: `Livre créé (id : ${response.data})` });
setForm(initialForm);
})
.catch((error) => {
console.error(error);
const status = error.response?.status;
if (status === 406) setMessage({ type: 'error', text: 'Ce livre existe déjà.' });
else if (status === 400) setMessage({ type: 'error', text: 'Données invalides, vérifie les champs.' });
else setMessage({ type: 'error', text: 'Erreur lors de la création.' });
})
.finally(() => setSubmitting(false));
}
return (
<main>
<h1>Ajouter un livre</h1>
<form onSubmit={handleSubmit}>
<table>
<tr>
<td>ISBN</td>
<td><input name="isbn" type="number" value={form.isbn} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Titre</td>
<td><input name="title" value={form.title} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Auteur</td>
<td><input name="author" value={form.author} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Éditeur</td>
<td><input name="publisher" value={form.publisher} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Date de publication</td>
<td><input name="publicationDate" type="date" value={form.publicationDate} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Prix</td>
<td><input name="price" type="number" step="0.01" value={form.price} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Quantité</td>
<td><input name="quantity" type="number" value={form.quantity} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Langue</td>
<td><input name="language" value={form.language} onChange={handleChange} required /></td>
</tr>
<tr>
<td>Catégorie</td>
<td><select value={form.categories[0] || ''} onChange={handleCategoryChange} required><option value=""> choisir </option>{CATEGORIES.map((c) => <option key={c} value={c}>{c}</option>)}</select></td>
</tr>
<tr>
<td>Description</td>
<td><textarea name="description" value={form.description} onChange={handleChange} /></td>
</tr>
</table>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Ajouter le livre'}
</button>
</form>
{message && <p>{message.text}</p>}
</main>
);
}
+66
View File
@@ -0,0 +1,66 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import { getBookById } from '../api/books';
import { useAuth } from '../context/AuthContext';
import { useReservations } from '../context/ReservationContext';
export default function BookDetail() {
const { user } = useAuth();
const { addReservation } = useReservations();
const { bookId } = useParams();
const [book, setBook] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [phoneNumber, setPhoneNumber] = useState('');
const [reservationStatus, setReservationStatus] = useState(null);
useEffect(() => { getBookById(bookId).then((response) => setBook(response.data)).catch((err) => { console.error(err); setError('Livre introuvable.'); }).finally(() => setLoading(false)); }, [bookId]);
function handleReservation(e) {
e.preventDefault();
addReservation(book, phoneNumber);
setReservationStatus({ success: true, message: 'Réservation effectuée avec succès !' });
setPhoneNumber('');
}
if (loading) return <main><p>Chargement</p></main>;
if (error) return <main><p>{error}</p><Link to="/books"> Retour au catalogue</Link></main>;
return (
<main>
<Link to="/books"> Retour au catalogue</Link>
<h1>{book.title}</h1>
<p>Auteur : {book.author}</p>
<p>ISBN : {book.isbn}</p>
<p>Éditeur : {book.publisher}</p>
<p>Publié le : {book.publicationDate}</p>
<p>Prix : {book.price} </p>
<p>Stock : {book.quantity}</p>
<p>Langue : {book.language}</p>
<p>Catégories : {book.categories?.join(', ')}</p>
{book.description && <p>{book.description}</p>}
{user?.role === 'user' && <section>
<h2>Réserver ce livre</h2>
<form onSubmit={handleReservation}>
<label>
Numéro de téléphone :
<input
type="tel"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="0612345678"
required
/>
</label>
<button type="submit">Réserver</button>
</form>
{reservationStatus && (
<p style={{ color: reservationStatus.success ? 'green' : 'red' }}>
{reservationStatus.message}
</p>
)}
</section>}
</main>
);
}
+45
View File
@@ -0,0 +1,45 @@
import { useState, useEffect } from 'react';
import { getBooks } from '../api/books';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export default function Books() {
const { user } = useAuth();
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
getBooks()
.then((response) => {
console.log(response.data);
setBooks(response.data.content);
})
.catch((err) => {
console.error(err);
setError("Impossible de charger les livres.");
})
.finally(() => {
setLoading(false);
});
}, []);
if (loading) return <main><p>Chargement</p></main>;
if (error) return <main><p>{error}</p></main>;
return (
<main>
<h1>Catalogue</h1>
{user?.role === 'admin' && <Link to="/books/new">+ Ajouter un livre</Link>}
<ul>
{books.map((book) => (
<li key={book.isbn}>
<Link to={`/books/${book.isbn}`}>
<strong>{book.title}</strong> - {book.author}
</Link>
</li>
))}
</ul>
</main>
);
}
+109
View File
@@ -0,0 +1,109 @@
import { useState } from 'react';
import { registerCustomer, findCustomerByPhone, addLoyaltyPoints, subtractLoyaltyPoints } from '../api/customers';
const initialForm = { firstName: '', lastName: '', phoneNumber: '' };
export default function Customers() {
const [form, setForm] = useState(initialForm);
const [registerMsg, setRegisterMsg] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [pointsInput, setPointsInput] = useState('');
function handleChange(e) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
}
function handleRegister(e) {
e.preventDefault();
setSubmitting(true);
setRegisterMsg(null);
registerCustomer(form)
.then((response) => {
setRegisterMsg(`Client créé (id : ${response.data})`);
setForm(initialForm);
})
.catch((error) => {
console.error(error);
setRegisterMsg('Erreur lors de la création du client.');
})
.finally(() => setSubmitting(false));
}
const [phone, setPhone] = useState('');
const [customer, setCustomer] = useState(null);
const [searchError, setSearchError] = useState(null);
function handleSearch(e) {
e.preventDefault();
setSearchError(null);
setCustomer(null);
findCustomerByPhone(phone)
.then((response) => setCustomer(response.data))
.catch((error) => {
console.error(error);
setSearchError('Aucun client trouvé avec ce numéro.');
});
}
function applyLoyalty(operation) {
const points = Number(pointsInput);
const action = operation === 'add' ? addLoyaltyPoints : subtractLoyaltyPoints;
action(customer.id, points)
.then((response) => {
setCustomer((prev) => ({ ...prev, loyaltyPoints: response.data }));
setPointsInput('');
})
.catch((error) => {
console.error(error);
if (error.response?.status === 400) setSearchError('Pas assez de points pour ce retrait.');
else setSearchError('Erreur lors de la mise à jour des points.');
});
}
return (
<main>
<h1>Gestion des clients</h1>
<section>
<h2>Enregistrer un client</h2>
<form onSubmit={handleRegister}>
<label>Prénom
<input name="firstName" value={form.firstName} onChange={handleChange} required />
</label>
<label>Nom
<input name="lastName" value={form.lastName} onChange={handleChange} required />
</label>
<label>Téléphone
<input name="phoneNumber" value={form.phoneNumber} onChange={handleChange} required />
</label>
<button type="submit" disabled={submitting}>
{submitting ? 'Envoi…' : 'Créer le client'}
</button>
</form>
{registerMsg && <p>{registerMsg}</p>}
</section>
<section>
<h2>Rechercher un client</h2>
<form onSubmit={handleSearch}>
<label>Téléphone
<input value={phone} onChange={(e) => setPhone(e.target.value)} required />
</label>
<button type="submit">Rechercher</button>
</form>
{searchError && <p>{searchError}</p>}
{customer && (
<div>
<p>{customer.firstName} {customer.lastName}</p>
<p>Téléphone : {customer.phoneNumber}</p>
<p><strong>Points de fidélité : {customer.loyaltyPoints}</strong></p>
<input type="number"value={pointsInput}onChange={(e) => setPointsInput(e.target.value)}placeholder="Nombre de points"/>
<button type="button" onClick={() => applyLoyalty('add')}>Ajouter</button>
<button type="button" onClick={() => applyLoyalty('subtract')}>Retirer</button>
</div>
)}
</section>
</main>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { Link } from 'react-router-dom';
import '../styles/home.css';
export default function Home() {
return (
<main>
</main>
);
}
+57
View File
@@ -0,0 +1,57 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
export default function Login() {
const { login } = useAuth();
const navigate = useNavigate();
const [form, setForm] = useState({ username: '', password: '' });
const [error, setError] = useState(null);
function handleSubmit(e) {
e.preventDefault();
const ok = login(form.username, form.password);
if (ok) {
navigate('/');
} else {
setError('Identifiants incorrects.');
}
}
return (
<main>
<h1>Connexion</h1>
<form onSubmit={handleSubmit}>
<div>
<label>
Nom d'utilisateur :
<input
type="text"
value={form.username}
onChange={e => setForm(f => ({ ...f, username: e.target.value }))}
required
/>
</label>
</div>
<div>
<label>
Mot de passe :
<input
type="password"
value={form.password}
onChange={e => setForm(f => ({ ...f, password: e.target.value }))}
required
/>
</label>
</div>
<button type="submit">Se connecter</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
<p>Comptes disponibles :</p>
<ul>
<li><strong>admin</strong> / admin (administrateur)</li>
<li><strong>alice (utilisateur)</strong> / bob (utilisateur)</li>
</ul>
</main>
);
}
+3
View File
@@ -0,0 +1,3 @@
export default function NotFound() {
return <main><h1>404 Page introuvable</h1></main>;
}
+3
View File
@@ -0,0 +1,3 @@
export default function Orders() {
return <main><h1>Mes commandes</h1></main>;
}
+3
View File
@@ -0,0 +1,3 @@
export default function Profile() {
return <main><h1>Mon compte</h1></main>;
}
+37
View File
@@ -0,0 +1,37 @@
import { useReservations } from '../context/ReservationContext';
import { Link } from 'react-router-dom';
export default function Reservations() {
const { reservations, cancelReservation } = useReservations();
if (reservations.length === 0) {
return (
<main>
<h1>Mes réservations</h1>
<p>Aucune réservation pour le moment.</p>
<Link to="/books"> Retour au catalogue</Link>
</main>
);
}
return (
<main>
<h1>Mes réservations</h1>
<ul>
{reservations.map(r => (
<li key={r.reservationId}>
<strong>{r.bookTitle}</strong> {r.bookAuthor}
<br />
Téléphone : {r.phoneNumber}
<br />
Réservé le : {new Date(r.reservedAt).toLocaleDateString('fr-FR')}
<br />
Statut : {r.status}
<br />
<button onClick={() => cancelReservation(r.reservationId)}>Annuler</button>
</li>
))}
</ul>
</main>
);
}
+13
View File
@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+5
View File
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
View File
View File
View File
Binary file not shown.
+38
View File
@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
+151
View File
@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>fr.iut_fbleau.but3.dev62</groupId>
<artifactId>mylibrary</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- Your java version-->
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Main dependencies -->
<lombok.version>1.18.36</lombok.version>
<!-- Test Verisons-->
<junit.version>5.11.4</junit.version>
<junit.platform.version>1.11.4</junit.platform.version>
<cucumber.version>7.21.1</cucumber.version>
<mockito.version>5.16.0</mockito.version>
<!-- Maven build version -->
<maven.compiler.version>3.13.0</maven.compiler.version>
<maven.surefire.version>3.5.2</maven.surefire.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-bom</artifactId>
<version>${cucumber.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit-platform-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-engine</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.version}</version>
<configuration>
<properties>
<!-- Work around. Surefire does not include enough
information to disambiguate between different
examples and scenarios. -->
<configurationParameters>
cucumber.junit-platform.naming-strategy=long
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,19 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book;
import fr.iut_fbleau.but3.dev62.mylibrary.book.entity.Category;
import java.time.LocalDate;
import java.util.List;
public record BookInfo(
long isbn,
String title,
String author,
String publisher,
LocalDate publicationDate,
double price,
int quantity,
List<Category> categories,
String description,
String language
) {
}
@@ -0,0 +1,24 @@
package fr.iut_fbleau.but3.dev62.mylibrary.book.entity;
public enum Category {
FICTION,
NON_FICTION,
SCIENCE_FICTION,
FANTASY,
MYSTERY,
THRILLER,
ROMANCE,
BIOGRAPHY,
HISTORY,
POETRY,
CHILDRENS,
YOUNG_ADULT,
SCIENCE,
PHILOSOPHY,
SELF_HELP,
TRAVEL,
COOKING,
ART,
RELIGION,
REFERENCE
}
@@ -0,0 +1,15 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer;
import java.util.UUID;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class CustomerDTO {
private final UUID id;
private final String firstName;
private final String lastName;
private final String phoneNumber;
private final int loyaltyPoints;
}
@@ -0,0 +1,4 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer;
public record CustomerInfo(String firstName, String lastName, String phoneNumber) {
}
@@ -0,0 +1,30 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer.converter;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.CustomerDTO;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.CustomerInfo;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.entity.Customer;
public final class CustomerConverter {
private CustomerConverter(){
}
public static Customer toDomain(CustomerInfo newCustomer) {
return Customer.builder()
.firstName(newCustomer.firstName())
.lastName(newCustomer.lastName())
.phoneNumber(newCustomer.phoneNumber())
.loyaltyPoints(0)
.build();
}
public static CustomerDTO toDTO(Customer customer) {
return CustomerDTO.builder()
.id(customer.getId())
.firstName(customer.getFirstName())
.lastName(customer.getLastName())
.phoneNumber(customer.getPhoneNumber())
.loyaltyPoints(customer.getLoyaltyPoints())
.build();
}
}
@@ -0,0 +1,30 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer.entity;
import fr.iut_fbleau.but3.dev62.mylibrary.customer.exception.IllegalCustomerPointException;
import java.util.UUID;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class Customer {
private UUID id;
private String firstName;
private String lastName;
private String phoneNumber;
private int loyaltyPoints;
public void setRandomUUID() {
this.id = UUID.randomUUID();
}
public void addLoyaltyPoints(int loyaltyPointToAdd) {
this.loyaltyPoints += loyaltyPointToAdd;
}
public void removeLoyaltyPoints(int loyaltyPointToRemove) throws IllegalCustomerPointException {
if (loyaltyPointToRemove > this.loyaltyPoints) throw new IllegalCustomerPointException(loyaltyPointToRemove, this.loyaltyPoints);
this.loyaltyPoints -= loyaltyPointToRemove;
}
}
@@ -0,0 +1,13 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer.exception;
import java.text.MessageFormat;
import java.util.UUID;
public class CustomerNotFoundException extends Exception {
public static final String THE_CUSTOMER_WITH_ID_DOES_NOT_EXIST_MESSAGE = "The customer with id {0} does not exist";
public CustomerNotFoundException(UUID uuid) {
super(MessageFormat.format(THE_CUSTOMER_WITH_ID_DOES_NOT_EXIST_MESSAGE, uuid));
}
}
@@ -0,0 +1,13 @@
package fr.iut_fbleau.but3.dev62.mylibrary.customer.exception;
import java.text.MessageFormat;
public class IllegalCustomerPointException extends Exception {
public static final String CANNOT_REMOVE_LOYALTY_POINTS = "Cannot remove {0} points from {1} points";
public IllegalCustomerPointException(int needed, int actual) {
super(MessageFormat.format(CANNOT_REMOVE_LOYALTY_POINTS, needed,
actual));
}
}

Some files were not shown because too many files have changed in this diff Show More