2 Commits

Author SHA1 Message Date
AISSI-JUDE-CHRIST e8ac8f0c54 prets entre utilisateurs 2026-06-14 18:23:57 +02:00
AISSI-JUDE-CHRIST 0d9f8dc539 Créer un abonnement 2026-06-14 16:12:47 +02:00
9 changed files with 347 additions and 23 deletions
+4
View File
@@ -11,6 +11,8 @@ 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 { useAuth } from './context/AuthContext';
function RequireAuth({ children }) {
@@ -39,6 +41,8 @@ export default function App() {
<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="*" element={<NotFound />} />
</Route>
</Routes>
+2
View File
@@ -20,6 +20,8 @@ export default function Navbar() {
<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="/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>}
+45
View File
@@ -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,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);
}
+7 -1
View File
@@ -5,6 +5,8 @@ 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 App from './App';
import './styles/global.css';
@@ -16,7 +18,11 @@ root.render(
<ReservationProvider>
<ReviewProvider>
<ReturnProvider>
<App />
<SubscriptionProvider>
<LoanProvider>
<App />
</LoanProvider>
</SubscriptionProvider>
</ReturnProvider>
</ReviewProvider>
</ReservationProvider>
+57 -22
View File
@@ -4,10 +4,12 @@ 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 { addReservation } = useReservations();
const { subscription } = useSubscription();
const { addReservation, reservations } = useReservations();
const { addReview, getReviewsByBook } = useReviews();
const [rating, setRating] = useState(5);
const [comment, setComment] = useState('');
@@ -97,27 +99,60 @@ export default function BookDetail() {
)}
</section>
{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>}
{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>
);
}
+54
View File
@@ -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>
);
}
+54
View File
@@ -1,8 +1,32 @@
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 (
@@ -29,6 +53,36 @@ export default function Reservations() {
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>
+74
View File
@@ -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>
);
}