Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dabf1df6b9 | |||
| e8ac8f0c54 | |||
| 0d9f8dc539 | |||
| 4197f99116 | |||
| 092a020059 | |||
| 8708686994 | |||
| 0d37efd227 | |||
| 1cf4bed83b | |||
| 0ba4e5599e | |||
| 587e81816c | |||
| 8359f21e07 | |||
| e3c02a6fc8 | |||
| a595750194 | |||
| 00e90aaa0c | |||
| 009ac7f5f7 | |||
| 9080b8e67e | |||
| 179f259c3d | |||
| bfd1f600de | |||
| 52213fce0b | |||
| 7117aba838 | |||
| 010ec9c11a | |||
| de47c293b8 | |||
| 4ae227d79d | |||
| 8f3451adc0 | |||
| 9916b02ef6 | |||
| 1aa83b0911 | |||
| abbd3ee95d | |||
| efa6fc49eb | |||
| e33acdf151 | |||
| ccafc24f72 | |||
| c811c373d5 | |||
| 1f9a1e9af8 | |||
| e438373370 | |||
| 2c0f30f0a2 | |||
| 4008ef4a9b | |||
| b99dcd8592 | |||
| 4e1a0b24f4 | |||
| 9318d1c28b | |||
| 038c52efc0 |
Binary file not shown.
@@ -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*
|
||||
@@ -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)
|
||||
Generated
+17341
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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 |
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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 Returns from './pages/Returns';
|
||||
import Subscription from './pages/Subscription';
|
||||
import Loans from './pages/Loans';
|
||||
import GroupOrders from './pages/GroupOrders';
|
||||
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="returns" element={<RequireAdmin><Returns /></RequireAdmin>} />
|
||||
<Route path="subscription" element={<RequireAuth><Subscription /></RequireAuth>} />
|
||||
<Route path="loans" element={<RequireAuth><Loans /></RequireAuth>} />
|
||||
<Route path="group-orders" element={<RequireAuth><GroupOrders /></RequireAuth>} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: 'http://localhost:8080',
|
||||
});
|
||||
|
||||
export default client;
|
||||
@@ -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 } });
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import client from './client';
|
||||
|
||||
export function createOrder(order) {
|
||||
return client.post('/api/orders', order);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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?.role === 'user' && <li><Link to="/subscription">Mon abonnement</Link></li>}
|
||||
{user?.role === 'user' && <li><Link to="/loans">Mes prêts</Link></li>}
|
||||
{user && <li><Link to="/group-orders">Commandes groupées</Link></li>}
|
||||
{user && <li><Link to="/profile">Mon compte</Link></li>}
|
||||
{user?.role === 'admin' && <li><Link to="/customers">Clients</Link></li>}
|
||||
{user?.role === 'admin' && <li><Link to="/returns">Retours</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>
|
||||
);
|
||||
}
|
||||
@@ -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,59 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const GroupOrderContext = createContext(null);
|
||||
|
||||
export function GroupOrderProvider({ children }) {
|
||||
const [groups, setGroups] = useState(() => {
|
||||
const saved = localStorage.getItem('groupOrders');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
function createGroup(creatorPhone) {
|
||||
const group = {
|
||||
id: crypto.randomUUID(),
|
||||
creatorPhone,
|
||||
orders: [],
|
||||
status: 'OPEN',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...groups, group];
|
||||
setGroups(updated);
|
||||
localStorage.setItem('groupOrders', JSON.stringify(updated));
|
||||
return group;
|
||||
}
|
||||
|
||||
function addOrderToGroup(groupId, phoneNumber, bookId, bookTitle, quantity) {
|
||||
const updated = groups.map(g => {
|
||||
if (g.id !== groupId) return g;
|
||||
const order = {
|
||||
orderId: crypto.randomUUID(),
|
||||
phoneNumber,
|
||||
bookId,
|
||||
bookTitle,
|
||||
quantity: Number(quantity),
|
||||
addedAt: new Date().toISOString(),
|
||||
};
|
||||
return { ...g, orders: [...g.orders, order] };
|
||||
});
|
||||
setGroups(updated);
|
||||
localStorage.setItem('groupOrders', JSON.stringify(updated));
|
||||
}
|
||||
|
||||
function closeGroup(groupId) {
|
||||
const updated = groups.map(g =>
|
||||
g.id === groupId ? { ...g, status: 'CLOSED' } : g
|
||||
);
|
||||
setGroups(updated);
|
||||
localStorage.setItem('groupOrders', JSON.stringify(updated));
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupOrderContext.Provider value={{ groups, createGroup, addOrderToGroup, closeGroup }}>
|
||||
{children}
|
||||
</GroupOrderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useGroupOrders() {
|
||||
return useContext(GroupOrderContext);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const LoanContext = createContext(null);
|
||||
|
||||
export function LoanProvider({ children }) {
|
||||
const [loans, setLoans] = useState(() => {
|
||||
const saved = localStorage.getItem('loans');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
function addLoan(book, borrowerPhone, dueDate) {
|
||||
const loan = {
|
||||
loanId: crypto.randomUUID(),
|
||||
bookId: book.isbn,
|
||||
bookTitle: book.title,
|
||||
bookAuthor: book.author,
|
||||
borrowerPhone,
|
||||
dueDate,
|
||||
status: 'ACTIVE',
|
||||
loanedAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...loans, loan];
|
||||
setLoans(updated);
|
||||
localStorage.setItem('loans', JSON.stringify(updated));
|
||||
return loan;
|
||||
}
|
||||
|
||||
function returnLoan(loanId) {
|
||||
const updated = loans.map(l =>
|
||||
l.loanId === loanId ? { ...l, status: 'RETURNED' } : l
|
||||
);
|
||||
setLoans(updated);
|
||||
localStorage.setItem('loans', JSON.stringify(updated));
|
||||
}
|
||||
|
||||
return (
|
||||
<LoanContext.Provider value={{ loans, addLoan, returnLoan }}>
|
||||
{children}
|
||||
</LoanContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLoans() {
|
||||
return useContext(LoanContext);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const ReturnContext = createContext(null);
|
||||
|
||||
export function ReturnProvider({ children }) {
|
||||
const [returns, setReturns] = useState(() => {
|
||||
const saved = localStorage.getItem('returns');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
function addReturn(bookId, bookTitle, customerPhone, reason) {
|
||||
const bookReturn = {
|
||||
returnId: crypto.randomUUID(),
|
||||
bookId,
|
||||
bookTitle,
|
||||
customerPhone,
|
||||
reason,
|
||||
status: 'PROCESSED',
|
||||
returnedAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...returns, bookReturn];
|
||||
setReturns(updated);
|
||||
localStorage.setItem('returns', JSON.stringify(updated));
|
||||
return bookReturn;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReturnContext.Provider value={{ returns, addReturn }}>
|
||||
{children}
|
||||
</ReturnContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useReturns() {
|
||||
return useContext(ReturnContext);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const ReviewContext = createContext(null);
|
||||
|
||||
export function ReviewProvider({ children }) {
|
||||
const [reviews, setReviews] = useState(() => {
|
||||
const saved = localStorage.getItem('reviews');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
function addReview(bookId, bookTitle, username, rating, comment) {
|
||||
const review = {
|
||||
reviewId: crypto.randomUUID(),
|
||||
bookId,
|
||||
bookTitle,
|
||||
username,
|
||||
rating,
|
||||
comment,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...reviews, review];
|
||||
setReviews(updated);
|
||||
localStorage.setItem('reviews', JSON.stringify(updated));
|
||||
return review;
|
||||
}
|
||||
|
||||
function getReviewsByBook(bookId) {
|
||||
return reviews.filter(r => String(r.bookId) === String(bookId));
|
||||
}
|
||||
|
||||
return (
|
||||
<ReviewContext.Provider value={{ reviews, addReview, getReviewsByBook }}>
|
||||
{children}
|
||||
</ReviewContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useReviews() {
|
||||
return useContext(ReviewContext);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const SubscriptionContext = createContext(null);
|
||||
|
||||
const PLANS = [
|
||||
{ id: 'basic', name: 'Basic', price: 9.99, description: '2 livres par mois' },
|
||||
{ id: 'standard', name: 'Standard', price: 14.99, description: '5 livres par mois' },
|
||||
{ id: 'premium', name: 'Premium', price: 24.99, description: 'Livres illimités par mois' },
|
||||
];
|
||||
|
||||
export { PLANS };
|
||||
|
||||
export function SubscriptionProvider({ children }) {
|
||||
const [subscription, setSubscription] = useState(() => {
|
||||
const saved = localStorage.getItem('subscription');
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
});
|
||||
|
||||
function subscribe(planId, phoneNumber) {
|
||||
const plan = PLANS.find(p => p.id === planId);
|
||||
const newSubscription = {
|
||||
subscriptionId: crypto.randomUUID(),
|
||||
planId,
|
||||
planName: plan.name,
|
||||
price: plan.price,
|
||||
phoneNumber,
|
||||
status: 'ACTIVE',
|
||||
startDate: new Date().toISOString(),
|
||||
};
|
||||
setSubscription(newSubscription);
|
||||
localStorage.setItem('subscription', JSON.stringify(newSubscription));
|
||||
return newSubscription;
|
||||
}
|
||||
|
||||
function cancelSubscription() {
|
||||
const updated = { ...subscription, status: 'CANCELLED' };
|
||||
setSubscription(updated);
|
||||
localStorage.setItem('subscription', JSON.stringify(updated));
|
||||
}
|
||||
|
||||
return (
|
||||
<SubscriptionContext.Provider value={{ subscription, subscribe, cancelSubscription, PLANS }}>
|
||||
{children}
|
||||
</SubscriptionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSubscription() {
|
||||
return useContext(SubscriptionContext);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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 { ReviewProvider } from './context/ReviewContext';
|
||||
import { ReturnProvider } from './context/ReturnContext';
|
||||
import { SubscriptionProvider } from './context/SubscriptionContext';
|
||||
import { LoanProvider } from './context/LoanContext';
|
||||
import { GroupOrderProvider } from './context/GroupOrderContext';
|
||||
import App from './App';
|
||||
import './styles/global.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<ReservationProvider>
|
||||
<ReviewProvider>
|
||||
<ReturnProvider>
|
||||
<SubscriptionProvider>
|
||||
<LoanProvider>
|
||||
<GroupOrderProvider>
|
||||
<App />
|
||||
</GroupOrderProvider>
|
||||
</LoanProvider>
|
||||
</SubscriptionProvider>
|
||||
</ReturnProvider>
|
||||
</ReviewProvider>
|
||||
</ReservationProvider>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.6 KiB |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
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';
|
||||
import { useReviews } from '../context/ReviewContext';
|
||||
import { useSubscription } from '../context/SubscriptionContext';
|
||||
|
||||
export default function BookDetail() {
|
||||
const { user } = useAuth();
|
||||
const { subscription } = useSubscription();
|
||||
const { addReservation, reservations } = useReservations();
|
||||
const { addReview, getReviewsByBook } = useReviews();
|
||||
const [rating, setRating] = useState(5);
|
||||
const [comment, setComment] = useState('');
|
||||
const [reviewStatus, setReviewStatus] = useState(null);
|
||||
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('');
|
||||
}
|
||||
|
||||
function handleReview(e) {
|
||||
e.preventDefault();
|
||||
addReview(book.isbn, book.title, user.username, Number(rating), comment);
|
||||
setReviewStatus({ success: true, message: 'Avis publié avec succès !' });
|
||||
setComment('');
|
||||
setRating(5);
|
||||
}
|
||||
|
||||
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>}
|
||||
|
||||
<section>
|
||||
<h2>Avis des lecteurs</h2>
|
||||
{getReviewsByBook(book.isbn).length === 0 ? (
|
||||
<p>Aucun avis pour le moment.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{getReviewsByBook(book.isbn).map(r => (
|
||||
<li key={r.reviewId}>
|
||||
<strong>{r.username}</strong> — {'*'.repeat(r.rating)}
|
||||
<br />
|
||||
{r.comment}
|
||||
<br />
|
||||
<small>{new Date(r.createdAt).toLocaleDateString('fr-FR')}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{user?.role === 'user' && (
|
||||
<form onSubmit={handleReview}>
|
||||
<h3>Laisser un avis</h3>
|
||||
<label>
|
||||
Note :
|
||||
<select value={rating} onChange={e => setRating(e.target.value)}>
|
||||
<option value={1}>*</option>
|
||||
<option value={2}>**</option>
|
||||
<option value={3}>***</option>
|
||||
<option value={4}>****</option>
|
||||
<option value={5}>*****</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Commentaire :
|
||||
<textarea value={comment} onChange={e => setComment(e.target.value)} required />
|
||||
</label>
|
||||
<button type="submit">Publier l'avis</button>
|
||||
</form>
|
||||
)}
|
||||
{reviewStatus && (
|
||||
<p style={{ color: reviewStatus.success ? 'green' : 'red' }}>
|
||||
{reviewStatus.message}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{user?.role === 'user' && (
|
||||
<section>
|
||||
<h2>Réserver ce livre</h2>
|
||||
{!subscription || subscription.status !== 'ACTIVE' ? (
|
||||
<p>
|
||||
Vous devez avoir un abonnement actif pour réserver un livre.{' '}
|
||||
<Link to="/subscription">S'abonner</Link>
|
||||
</p>
|
||||
) : (() => {
|
||||
const QUOTAS = { basic: 2, standard: 5, premium: Infinity };
|
||||
const quota = QUOTAS[subscription.planId];
|
||||
const thisMonthStart = new Date();
|
||||
thisMonthStart.setDate(1);
|
||||
thisMonthStart.setHours(0, 0, 0, 0);
|
||||
const usedThisMonth = reservations.filter(r =>
|
||||
new Date(r.reservedAt) >= thisMonthStart
|
||||
).length;
|
||||
const remaining = quota - usedThisMonth;
|
||||
|
||||
if (remaining <= 0) {
|
||||
return (
|
||||
<p style={{ color: 'orange' }}>
|
||||
Vous avez atteint votre quota de {quota} réservation(s) ce mois-ci.
|
||||
<Link to="/subscription"> Passer au plan supérieur</Link>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>Réservations restantes ce mois : <strong>{remaining === Infinity ? 'illimitées' : remaining}</strong></p>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { useState } from 'react';
|
||||
import { useGroupOrders } from '../context/GroupOrderContext';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
export default function GroupOrders() {
|
||||
const { groups, createGroup, addOrderToGroup, closeGroup } = useGroupOrders();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [creatorPhone, setCreatorPhone] = useState('');
|
||||
const [joinGroupId, setJoinGroupId] = useState('');
|
||||
const [joinForm, setJoinForm] = useState({ phoneNumber: '', bookId: '', bookTitle: '', quantity: 1 });
|
||||
const [joinStatus, setJoinStatus] = useState({});
|
||||
const [createStatus, setCreateStatus] = useState(null);
|
||||
|
||||
function handleCreateGroup(e) {
|
||||
e.preventDefault();
|
||||
const group = createGroup(creatorPhone);
|
||||
setCreateStatus(`Groupe créé ! ID : ${group.id}`);
|
||||
setCreatorPhone('');
|
||||
}
|
||||
|
||||
function handleJoinGroup(e) {
|
||||
e.preventDefault();
|
||||
const group = groups.find(g => g.id === joinGroupId);
|
||||
if (!group) {
|
||||
setJoinStatus(prev => ({ ...prev, [joinGroupId]: 'Groupe introuvable.' }));
|
||||
return;
|
||||
}
|
||||
if (group.status === 'CLOSED') {
|
||||
setJoinStatus(prev => ({ ...prev, [joinGroupId]: 'Ce groupe est fermé.' }));
|
||||
return;
|
||||
}
|
||||
addOrderToGroup(joinGroupId, joinForm.phoneNumber, joinForm.bookId, joinForm.bookTitle, joinForm.quantity);
|
||||
setJoinStatus(prev => ({ ...prev, [joinGroupId]: 'Commande ajoutée au groupe !' }));
|
||||
setJoinForm({ phoneNumber: '', bookId: '', bookTitle: '', quantity: 1 });
|
||||
}
|
||||
|
||||
const openGroups = groups.filter(g => g.status === 'OPEN');
|
||||
const closedGroups = groups.filter(g => g.status === 'CLOSED');
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Commandes groupées</h1>
|
||||
|
||||
<section>
|
||||
<h2>Créer un groupe</h2>
|
||||
<form onSubmit={handleCreateGroup}>
|
||||
<label>
|
||||
Votre téléphone :
|
||||
<input
|
||||
type="tel"
|
||||
value={creatorPhone}
|
||||
onChange={e => setCreatorPhone(e.target.value)}
|
||||
placeholder="0612345678"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Créer un groupe</button>
|
||||
</form>
|
||||
{createStatus && <p style={{ color: 'green' }}>{createStatus}</p>}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Rejoindre un groupe</h2>
|
||||
<form onSubmit={handleJoinGroup}>
|
||||
<label>
|
||||
ID du groupe :
|
||||
<input
|
||||
value={joinGroupId}
|
||||
onChange={e => setJoinGroupId(e.target.value)}
|
||||
placeholder="Collez l'ID du groupe ici"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Votre téléphone :
|
||||
<input
|
||||
type="tel"
|
||||
value={joinForm.phoneNumber}
|
||||
onChange={e => setJoinForm(prev => ({ ...prev, phoneNumber: e.target.value }))}
|
||||
placeholder="0612345678"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
ISBN du livre :
|
||||
<input
|
||||
value={joinForm.bookId}
|
||||
onChange={e => setJoinForm(prev => ({ ...prev, bookId: e.target.value }))}
|
||||
placeholder="9782070361038"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Titre du livre :
|
||||
<input
|
||||
value={joinForm.bookTitle}
|
||||
onChange={e => setJoinForm(prev => ({ ...prev, bookTitle: e.target.value }))}
|
||||
placeholder="Nom du livre"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Quantité :
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={joinForm.quantity}
|
||||
onChange={e => setJoinForm(prev => ({ ...prev, quantity: e.target.value }))}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Rejoindre</button>
|
||||
</form>
|
||||
{joinStatus[joinGroupId] && (
|
||||
<p style={{ color: joinStatus[joinGroupId].includes('introuvable') || joinStatus[joinGroupId].includes('fermé') ? 'red' : 'green' }}>
|
||||
{joinStatus[joinGroupId]}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Groupes ouverts ({openGroups.length})</h2>
|
||||
{openGroups.length === 0 ? (
|
||||
<p>Aucun groupe ouvert.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{openGroups.map(g => (
|
||||
<li key={g.id}>
|
||||
<strong>Groupe {g.id.slice(0, 8)}…</strong>
|
||||
<br />
|
||||
Créé par : {g.creatorPhone}
|
||||
<br />
|
||||
Créé le : {new Date(g.createdAt).toLocaleDateString('fr-FR')}
|
||||
<br />
|
||||
Participants : {g.orders.length}
|
||||
{g.orders.length > 0 && (
|
||||
<ul>
|
||||
{g.orders.map(o => (
|
||||
<li key={o.orderId}>
|
||||
{o.phoneNumber} , {o.bookTitle} (x{o.quantity})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<button onClick={() => closeGroup(g.id)}>Fermer le groupe</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{closedGroups.length > 0 && (
|
||||
<section>
|
||||
<h2>Groupes fermés ({closedGroups.length})</h2>
|
||||
<ul>
|
||||
{closedGroups.map(g => (
|
||||
<li key={g.id}>
|
||||
<strong>Groupe {g.id.slice(0, 8)}…</strong> , {g.orders.length} commande(s) , <small>Fermé ✓</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import '../styles/home.css';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useLoans } from '../context/LoanContext';
|
||||
|
||||
export default function Loans() {
|
||||
const { loans, returnLoan } = useLoans();
|
||||
|
||||
const activeLoans = loans.filter(l => l.status === 'ACTIVE');
|
||||
const returnedLoans = loans.filter(l => l.status === 'RETURNED');
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Mes prêts</h1>
|
||||
|
||||
<section>
|
||||
<h2>Prêts en cours ({activeLoans.length})</h2>
|
||||
{activeLoans.length === 0 ? (
|
||||
<p>Aucun prêt en cours.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{activeLoans.map(l => (
|
||||
<li key={l.loanId}>
|
||||
<strong>{l.bookTitle}</strong> — {l.bookAuthor}
|
||||
<br />
|
||||
Prêté à : {l.borrowerPhone}
|
||||
<br />
|
||||
À rendre avant le : {new Date(l.dueDate).toLocaleDateString('fr-FR')}
|
||||
<br />
|
||||
Prêté le : {new Date(l.loanedAt).toLocaleDateString('fr-FR')}
|
||||
<br />
|
||||
<button onClick={() => returnLoan(l.loanId)}>Marquer comme rendu</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{returnedLoans.length > 0 && (
|
||||
<section>
|
||||
<h2>Prêts terminés ({returnedLoans.length})</h2>
|
||||
<ul>
|
||||
{returnedLoans.map(l => (
|
||||
<li key={l.loanId}>
|
||||
<strong>{l.bookTitle}</strong> — {l.bookAuthor}
|
||||
<br />
|
||||
Prêté à : {l.borrowerPhone}
|
||||
<br />
|
||||
<small>Rendu ✓</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function NotFound() {
|
||||
return <main><h1>404 Page introuvable</h1></main>;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
import { createOrder } from '../api/orders';
|
||||
|
||||
export default function Orders() {
|
||||
const [customerId, setCustomerId] = useState('');
|
||||
const [paymentMethod, setPaymentMethod] = useState('CREDIT_CARD');
|
||||
const [address, setAddress] = useState({ street: '', city: '', postalCode: '', country: '' });
|
||||
const [lines, setLines] = useState([{ bookId: '', quantity: 1 }]);
|
||||
const [message, setMessage] = useState(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
function handleAddressChange(e) {
|
||||
const { name, value } = e.target;
|
||||
setAddress((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
|
||||
function handleLineChange(index, field, value) {
|
||||
setLines((prev) => prev.map((line, i) => (i === index ? { ...line, [field]: value } : line)));
|
||||
}
|
||||
function addLine() {
|
||||
setLines((prev) => [...prev, { bookId: '', quantity: 1 }]);
|
||||
}
|
||||
function removeLine(index) {
|
||||
setLines((prev) => prev.filter((_, i) => i !== index));
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setMessage(null);
|
||||
|
||||
const payload = {
|
||||
customerId,
|
||||
paymentMethod,
|
||||
address,
|
||||
orderLineDtos: lines.map((line) => ({
|
||||
bookId: Number(line.bookId),
|
||||
quantity: Number(line.quantity),
|
||||
})),
|
||||
};
|
||||
|
||||
createOrder(payload)
|
||||
.then((response) => setMessage('Commande créée'))
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setMessage(error.response?.data?.message || 'Erreur lors de la commande.');
|
||||
})
|
||||
.finally(() => setSubmitting(false));
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Passer une commande</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>ID client
|
||||
<input value={customerId} onChange={(e) => setCustomerId(e.target.value)} required />
|
||||
</label>
|
||||
|
||||
<h2>Livres</h2>
|
||||
{lines.map((line, index) => (
|
||||
<div key={index}>
|
||||
<input type="number" placeholder="ISBN du livre" value={line.bookId}
|
||||
onChange={(e) => handleLineChange(index, 'bookId', e.target.value)} required />
|
||||
<input type="number" placeholder="Quantité" value={line.quantity}
|
||||
onChange={(e) => handleLineChange(index, 'quantity', e.target.value)} required />
|
||||
{lines.length > 1 && (
|
||||
<button type="button" onClick={() => removeLine(index)}>Retirer</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button type="button" onClick={addLine}>+ Ajouter un livre</button>
|
||||
|
||||
<h2>Adresse de livraison</h2>
|
||||
<input name="street" placeholder="Rue" value={address.street} onChange={handleAddressChange} required />
|
||||
<input name="city" placeholder="Ville" value={address.city} onChange={handleAddressChange} required />
|
||||
<input name="postalCode" placeholder="Code postal" value={address.postalCode} onChange={handleAddressChange} required />
|
||||
<input name="country" placeholder="Pays" value={address.country} onChange={handleAddressChange} required />
|
||||
|
||||
<h2>Paiement</h2>
|
||||
<select value={paymentMethod} onChange={(e) => setPaymentMethod(e.target.value)}>
|
||||
<option value="CREDIT_CARD">Carte bancaire</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" disabled={submitting}>
|
||||
{submitting ? 'Envoi…' : 'Valider la commande'}
|
||||
</button>
|
||||
</form>
|
||||
{message && <p>{message}</p>}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function Profile() {
|
||||
return <main><h1>Mon compte</h1></main>;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
import { useReservations } from '../context/ReservationContext';
|
||||
import { useLoans } from '../context/LoanContext';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export default function Reservations() {
|
||||
const { reservations, cancelReservation } = useReservations();
|
||||
const { addLoan } = useLoans();
|
||||
const [loanForms, setLoanForms] = useState({});
|
||||
const [loanStatuses, setLoanStatuses] = useState({});
|
||||
|
||||
function handleLoanChange(reservationId, field, value) {
|
||||
setLoanForms(prev => ({
|
||||
...prev,
|
||||
[reservationId]: { ...prev[reservationId], [field]: value }
|
||||
}));
|
||||
}
|
||||
|
||||
function handleLoan(e, reservation) {
|
||||
e.preventDefault();
|
||||
const form = loanForms[reservation.reservationId] || {};
|
||||
addLoan(
|
||||
{ isbn: reservation.bookId, title: reservation.bookTitle, author: reservation.bookAuthor },
|
||||
form.borrowerPhone,
|
||||
form.dueDate
|
||||
);
|
||||
setLoanStatuses(prev => ({ ...prev, [reservation.reservationId]: 'Prêt enregistré avec succès !' }));
|
||||
setLoanForms(prev => ({ ...prev, [reservation.reservationId]: {} }));
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<details>
|
||||
<summary>Prêter ce livre à un autre lecteur</summary>
|
||||
<form onSubmit={e => handleLoan(e, r)}>
|
||||
<label>
|
||||
Téléphone de l'emprunteur :
|
||||
<input
|
||||
type="tel"
|
||||
value={loanForms[r.reservationId]?.borrowerPhone || ''}
|
||||
onChange={e => handleLoanChange(r.reservationId, 'borrowerPhone', e.target.value)}
|
||||
placeholder="0612345678"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Date de retour prévue :
|
||||
<input
|
||||
type="date"
|
||||
value={loanForms[r.reservationId]?.dueDate || ''}
|
||||
onChange={e => handleLoanChange(r.reservationId, 'dueDate', e.target.value)}
|
||||
min={new Date().toISOString().split('T')[0]}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Prêter</button>
|
||||
</form>
|
||||
{loanStatuses[r.reservationId] && (
|
||||
<p style={{ color: 'green' }}>{loanStatuses[r.reservationId]}</p>
|
||||
)}
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useState } from 'react';
|
||||
import { useReturns } from '../context/ReturnContext';
|
||||
import { getBooks } from '../api/books';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function Returns() {
|
||||
const { returns, addReturn } = useReturns();
|
||||
const [books, setBooks] = useState([]);
|
||||
const [form, setForm] = useState({ bookId: '', customerPhone: '', reason: '' });
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
getBooks(0, 100).then(res => setBooks(res.data.content)).catch(console.error);
|
||||
}, []);
|
||||
|
||||
function handleChange(e) {
|
||||
const { name, value } = e.target;
|
||||
setForm(f => ({ ...f, [name]: value }));
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const book = books.find(b => String(b.isbn) === String(form.bookId));
|
||||
const bookTitle = book ? book.title : form.bookId;
|
||||
addReturn(form.bookId, bookTitle, form.customerPhone, form.reason);
|
||||
setMessage({ success: true, text: 'Retour enregistré avec succès !' });
|
||||
setForm({ bookId: '', customerPhone: '', reason: '' });
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Gestion des retours</h1>
|
||||
|
||||
<section>
|
||||
<h2>Enregistrer un retour</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<label>
|
||||
Livre :
|
||||
<select name="bookId" value={form.bookId} onChange={handleChange} required>
|
||||
<option value="">-- Choisir un livre --</option>
|
||||
{books.map(b => (
|
||||
<option key={b.isbn} value={b.isbn}>{b.title} — {b.author}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Téléphone du client :
|
||||
<input
|
||||
name="customerPhone"
|
||||
type="tel"
|
||||
value={form.customerPhone}
|
||||
onChange={handleChange}
|
||||
placeholder="0612345678"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Motif :
|
||||
<textarea
|
||||
name="reason"
|
||||
value={form.reason}
|
||||
onChange={handleChange}
|
||||
placeholder="Motif du retour..."
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button type="submit">Enregistrer le retour</button>
|
||||
</form>
|
||||
{message && <p style={{ color: message.success ? 'green' : 'red' }}>{message.text}</p>}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Historique des retours</h2>
|
||||
{returns.length === 0 ? (
|
||||
<p>Aucun retour enregistré.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{returns.map(r => (
|
||||
<li key={r.returnId}>
|
||||
<strong>{r.bookTitle}</strong>
|
||||
<br />
|
||||
Client : {r.customerPhone}
|
||||
<br />
|
||||
Motif : {r.reason}
|
||||
<br />
|
||||
Statut : {r.status}
|
||||
<br />
|
||||
<small>{new Date(r.returnedAt).toLocaleDateString('fr-FR')}</small>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useState } from 'react';
|
||||
import { useSubscription } from '../context/SubscriptionContext';
|
||||
|
||||
export default function Subscription() {
|
||||
const { subscription, subscribe, cancelSubscription, PLANS } = useSubscription();
|
||||
const [selectedPlan, setSelectedPlan] = useState('basic');
|
||||
const [phoneNumber, setPhoneNumber] = useState('');
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
function handleSubscribe(e) {
|
||||
e.preventDefault();
|
||||
subscribe(selectedPlan, phoneNumber);
|
||||
setMessage({ success: true, text: 'Abonnement créé avec succès !' });
|
||||
setPhoneNumber('');
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
cancelSubscription();
|
||||
setMessage({ success: false, text: 'Abonnement annulé.' });
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Mon abonnement</h1>
|
||||
|
||||
{subscription && subscription.status === 'ACTIVE' ? (
|
||||
<section>
|
||||
<h2>Abonnement actif</h2>
|
||||
<p>Plan : <strong>{subscription.planName}</strong></p>
|
||||
<p>Prix : <strong>{subscription.price} € / mois</strong></p>
|
||||
<p>Téléphone : {subscription.phoneNumber}</p>
|
||||
<p>Depuis le : {new Date(subscription.startDate).toLocaleDateString('fr-FR')}</p>
|
||||
<p>Statut : <strong style={{ color: 'green' }}>{subscription.status}</strong></p>
|
||||
<button onClick={handleCancel}>Annuler l'abonnement</button>
|
||||
</section>
|
||||
) : (
|
||||
<section>
|
||||
<h2>Choisir un abonnement</h2>
|
||||
<form onSubmit={handleSubscribe}>
|
||||
<div>
|
||||
{PLANS.map(plan => (
|
||||
<label key={plan.id} style={{ display: 'block', marginBottom: '8px' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name="plan"
|
||||
value={plan.id}
|
||||
checked={selectedPlan === plan.id}
|
||||
onChange={() => setSelectedPlan(plan.id)}
|
||||
/>
|
||||
{' '}<strong>{plan.name}</strong> , {plan.price} € / mois , {plan.description}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<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">S'abonner</button>
|
||||
</form>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{message && (
|
||||
<p style={{ color: message.success ? 'green' : 'red' }}>{message.text}</p>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user