chore: add ansible backend docker frontend-prep
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
flex: 1;
|
||||
margin-left: 0;
|
||||
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import Sidebar from './Sidebar';
|
||||
import './AdminLayout.css';
|
||||
|
||||
interface AdminLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function AdminLayout({ children }: AdminLayoutProps) {
|
||||
return (
|
||||
<div className="admin-layout">
|
||||
<Sidebar />
|
||||
<main className="admin-content">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminLayout;
|
||||
@@ -0,0 +1,388 @@
|
||||
/* ============================================
|
||||
CONFIRM MODAL - STYLES PROFESSIONNELS
|
||||
============================================ */
|
||||
|
||||
.confirm-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
padding: 1rem;
|
||||
animation: overlayFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes overlayFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CONTAINER
|
||||
============================================ */
|
||||
.confirm-modal-container {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(30, 30, 40, 0.98) 0%,
|
||||
rgba(20, 20, 30, 0.98) 100%
|
||||
);
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.5),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
animation: modalSlideIn 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
transform: scale(0.9) translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HEADER
|
||||
============================================ */
|
||||
.confirm-modal-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem 2rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.confirm-modal-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
animation: iconPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-modal-icon-danger {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(239, 68, 68, 0.2),
|
||||
rgba(220, 38, 38, 0.1)
|
||||
);
|
||||
border: 2px solid rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
box-shadow: 0 0 30px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-icon-warning {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(245, 158, 11, 0.2),
|
||||
rgba(217, 119, 6, 0.1)
|
||||
);
|
||||
border: 2px solid rgba(245, 158, 11, 0.3);
|
||||
color: #f59e0b;
|
||||
box-shadow: 0 0 30px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-icon-info {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(59, 130, 246, 0.2),
|
||||
rgba(37, 99, 235, 0.1)
|
||||
);
|
||||
border: 2px solid rgba(59, 130, 246, 0.3);
|
||||
color: #3b82f6;
|
||||
box-shadow: 0 0 30px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-icon-success {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(16, 185, 129, 0.2),
|
||||
rgba(5, 150, 105, 0.1)
|
||||
);
|
||||
border: 2px solid rgba(16, 185, 129, 0.3);
|
||||
color: #10b981;
|
||||
box-shadow: 0 0 30px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-close {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #888;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.confirm-modal-close:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.confirm-modal-close:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CONTENT
|
||||
============================================ */
|
||||
.confirm-modal-content {
|
||||
padding: 0 2rem 2rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirm-modal-title {
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem 0;
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.confirm-modal-message {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
max-width: 380px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FOOTER
|
||||
============================================ */
|
||||
.confirm-modal-footer {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 0 2rem 2rem 2rem;
|
||||
}
|
||||
|
||||
.confirm-modal-btn {
|
||||
flex: 1;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.confirm-modal-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Cancel Button */
|
||||
.confirm-modal-btn-cancel {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-cancel:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Confirm Buttons - Variants */
|
||||
.confirm-modal-btn-confirm {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.confirm-modal-btn-danger {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-danger:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #f87171, #ef4444);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-warning {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-warning:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-info {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
border-color: rgba(59, 130, 246, 0.3);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-info:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #60a5fa, #3b82f6);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.5);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-success {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.confirm-modal-btn-success:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #34d399, #10b981);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 32px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LOADING STATE
|
||||
============================================ */
|
||||
.confirm-modal-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
@media (max-width: 640px) {
|
||||
.confirm-modal-container {
|
||||
max-width: 100%;
|
||||
margin: 1rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.confirm-modal-header {
|
||||
padding: 1.5rem 1.5rem 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.confirm-modal-content {
|
||||
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.confirm-modal-footer {
|
||||
flex-direction: column;
|
||||
padding: 0 1.5rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.confirm-modal-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.confirm-modal-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.confirm-modal-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.confirm-modal-message {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.confirm-modal-close {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.confirm-modal-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.confirm-modal-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Touch Optimization */
|
||||
@media (hover: none) {
|
||||
.confirm-modal-btn:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.confirm-modal-close:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { X, AlertTriangle, Trash2, CheckCircle, Info } from "lucide-react";
|
||||
import "./ConfirmModal.css";
|
||||
|
||||
interface ConfirmModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title: string;
|
||||
message: string;
|
||||
type?: "danger" | "warning" | "info" | "success";
|
||||
confirmText?: string;
|
||||
cancelText?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function ConfirmModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
type = "warning",
|
||||
confirmText = "Confirmer",
|
||||
cancelText = "Annuler",
|
||||
isLoading = false,
|
||||
}: ConfirmModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case "danger":
|
||||
return <Trash2 size={28} />;
|
||||
case "warning":
|
||||
return <AlertTriangle size={28} />;
|
||||
case "success":
|
||||
return <CheckCircle size={28} />;
|
||||
case "info":
|
||||
return <Info size={28} />;
|
||||
default:
|
||||
return <AlertTriangle size={28} />;
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!isLoading) {
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (!isLoading) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="confirm-modal-overlay" onClick={handleCancel}>
|
||||
<div
|
||||
className="confirm-modal-container"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="confirm-modal-header">
|
||||
<div
|
||||
className={`confirm-modal-icon confirm-modal-icon-${type}`}
|
||||
>
|
||||
{getIcon()}
|
||||
</div>
|
||||
<button
|
||||
className="confirm-modal-close"
|
||||
onClick={handleCancel}
|
||||
disabled={isLoading}
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="confirm-modal-content">
|
||||
<h2 className="confirm-modal-title">{title}</h2>
|
||||
<p className="confirm-modal-message">{message}</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="confirm-modal-footer">
|
||||
<button
|
||||
className="confirm-modal-btn confirm-modal-btn-cancel"
|
||||
onClick={handleCancel}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
<button
|
||||
className={`confirm-modal-btn confirm-modal-btn-confirm confirm-modal-btn-${type}`}
|
||||
onClick={handleConfirm}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="confirm-modal-loading">
|
||||
<span className="spinner" />
|
||||
Chargement...
|
||||
</span>
|
||||
) : (
|
||||
confirmText
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmModal;
|
||||
@@ -0,0 +1,429 @@
|
||||
/* ============================================
|
||||
EditUserModal.css
|
||||
============================================ */
|
||||
|
||||
/* ✅ Modal overlay - PAS DE Z-INDEX */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
/* ✅ PAS DE Z-INDEX pour ne pas cacher la modal */
|
||||
backdrop-filter: blur(8px);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-user-modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
background: linear-gradient(135deg, #1a1a1a, #0f0f0f);
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 20px;
|
||||
z-index: 9999; /* ✅ Seule la modal a un z-index */
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.8);
|
||||
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -45%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 1rem 1.2rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
animation: slideInDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(220, 38, 38, 0.1));
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.2), rgba(5, 150, 105, 0.1));
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
/* Form sections */
|
||||
.form-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-section:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-section h3 {
|
||||
color: white;
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 1rem 0;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid rgba(124, 58, 237, 0.3);
|
||||
padding-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
color: #888;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group label svg {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem 1rem;
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
|
||||
border-color: rgba(124, 58, 237, 0.4);
|
||||
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
|
||||
}
|
||||
|
||||
.form-group input::placeholder {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.form-group input[type="number"] {
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
.form-group input[type="number"]::-webkit-inner-spin-button,
|
||||
.form-group input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.role-select {
|
||||
cursor: pointer;
|
||||
background: #1a1a1a !important;
|
||||
width: 100%;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
.custom-select-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-select-wrapper .select-icon {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: #7c3aed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.custom-select-wrapper .select-arrow {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: #888;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.role-select {
|
||||
padding-left: 3rem !important;
|
||||
}
|
||||
|
||||
.role-select option {
|
||||
background-color: #1a1a1a !important;
|
||||
color: white !important;
|
||||
padding: 0.8rem !important;
|
||||
}
|
||||
|
||||
.role-select option:hover,
|
||||
.role-select option:focus,
|
||||
.role-select option:active {
|
||||
background-color: #0f0f0f !important;
|
||||
background: #0f0f0f !important;
|
||||
color: white !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.role-select option:checked {
|
||||
background-color: #7c3aed !important;
|
||||
background: #7c3aed !important;
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.role-select option:hover {
|
||||
background-color: #0f0f0f !important;
|
||||
}
|
||||
}
|
||||
|
||||
.role-select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.role-select option::selection {
|
||||
background: #0f0f0f !important;
|
||||
}
|
||||
|
||||
.role-select option::-moz-selection {
|
||||
background: #0f0f0f !important;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal header et actions - Styles manquants */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
color: white;
|
||||
font-size: 1.5rem;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-modal {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0.5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.close-modal:hover {
|
||||
color: white;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.modal-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(124, 58, 237, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 2rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.9rem 1.5rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.secondary:hover {
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.04));
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.action-button.primary {
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.action-button.primary:hover {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.4);
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.edit-user-modal {
|
||||
width: 95%;
|
||||
max-height: 95vh;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
grid-column: 1 / -1 !important;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-actions {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.form-section h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
padding: 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
// ============================================
|
||||
// EditUserModal.tsx
|
||||
// ============================================
|
||||
// Modal de modification des profils utilisateurs (Admin)
|
||||
|
||||
import { useState } from 'react';
|
||||
import { XCircle, Save, User, Lock, Shield, Phone, Crown, Building2, Truck } from 'lucide-react';
|
||||
import { updateClientByAdmin, updateUserByAdmin } from '../api/api_admin';
|
||||
import './EditUserModal.css';
|
||||
|
||||
interface EditUserModalProps {
|
||||
user: {
|
||||
id: number;
|
||||
username: string;
|
||||
role: 'client' | 'livreur' | 'admin' | 'cabine';
|
||||
nom?: string;
|
||||
prenom?: string;
|
||||
telephone?: string;
|
||||
adresse?: string;
|
||||
command?: number;
|
||||
point?: number;
|
||||
points_zipette?: number; // ✅ AJOUTÉ
|
||||
amende?: number;
|
||||
};
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
function EditUserModal({ user, onClose, onSuccess }: EditUserModalProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
// État du formulaire pour CLIENT
|
||||
const [clientForm, setClientForm] = useState({
|
||||
username: user.username || '',
|
||||
password: '',
|
||||
nom: user.nom || '',
|
||||
prenom: user.prenom || '',
|
||||
telephone: user.telephone || '',
|
||||
point: user.point || 0,
|
||||
points_zipette: user.points_zipette || 0, // ✅ AJOUTÉ
|
||||
amende: user.amende || 0,
|
||||
command: user.command || 0,
|
||||
});
|
||||
|
||||
// État du formulaire pour USER (admin/cabine/livreur)
|
||||
const [userForm, setUserForm] = useState({
|
||||
username: user.username || '',
|
||||
password: '',
|
||||
role: user.role as 'admin' | 'cabine' | 'livreur',
|
||||
});
|
||||
|
||||
const isClient = user.role === 'client';
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
if (isClient) {
|
||||
// ✅ Modification CLIENT
|
||||
const updates: any = {};
|
||||
|
||||
// N'envoyer que les champs modifiés
|
||||
if (clientForm.username && clientForm.username !== user.username) {
|
||||
updates.username = clientForm.username;
|
||||
}
|
||||
if (clientForm.password) {
|
||||
updates.password = clientForm.password;
|
||||
}
|
||||
if (clientForm.nom && clientForm.nom !== user.nom) {
|
||||
updates.nom = clientForm.nom;
|
||||
}
|
||||
if (clientForm.prenom && clientForm.prenom !== user.prenom) {
|
||||
updates.prenom = clientForm.prenom;
|
||||
}
|
||||
if (clientForm.telephone && clientForm.telephone !== user.telephone) {
|
||||
updates.telephone = clientForm.telephone;
|
||||
}
|
||||
if (clientForm.point !== user.point) {
|
||||
updates.point = clientForm.point;
|
||||
}
|
||||
if (clientForm.points_zipette !== user.points_zipette) { // ✅ AJOUTÉ
|
||||
updates.points_zipette = clientForm.points_zipette;
|
||||
}
|
||||
if (clientForm.amende !== user.amende) {
|
||||
updates.amende = clientForm.amende;
|
||||
}
|
||||
if (clientForm.command !== user.command) {
|
||||
updates.command = clientForm.command;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
setError('Aucune modification détectée');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📝 [EDIT_CLIENT] Mise à jour:', updates);
|
||||
|
||||
const result = await updateClientByAdmin(user.id, updates);
|
||||
|
||||
if (result.success) {
|
||||
setSuccess(result.message || 'Client mis à jour avec succès');
|
||||
setTimeout(() => {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}, 1500);
|
||||
}
|
||||
} else {
|
||||
// ✅ Modification USER (admin/cabine/livreur)
|
||||
const updates: any = {};
|
||||
|
||||
if (userForm.username && userForm.username !== user.username) {
|
||||
updates.username = userForm.username;
|
||||
}
|
||||
if (userForm.password) {
|
||||
updates.password = userForm.password;
|
||||
}
|
||||
if (userForm.role && userForm.role !== user.role) {
|
||||
updates.role = userForm.role;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
setError('Aucune modification détectée');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('📝 [EDIT_USER] Mise à jour:', updates);
|
||||
|
||||
const result = await updateUserByAdmin(user.id, updates);
|
||||
|
||||
if (result.success) {
|
||||
setSuccess(result.message || 'Utilisateur mis à jour avec succès');
|
||||
setTimeout(() => {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ [EDIT_MODAL] Erreur:', err);
|
||||
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="modal-overlay" onClick={onClose} />
|
||||
<div className="edit-user-modal">
|
||||
<div className="modal-header">
|
||||
<h2>
|
||||
{isClient ? 'Modifier le client' : 'Modifier l\'utilisateur'}
|
||||
</h2>
|
||||
<button className="close-modal" onClick={onClose}>
|
||||
<XCircle size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="modal-content">
|
||||
{/* Messages de succès/erreur */}
|
||||
{error && (
|
||||
<div className="alert alert-error">
|
||||
❌ {error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="alert alert-success">
|
||||
✅ {success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isClient ? (
|
||||
// ============================================
|
||||
// FORMULAIRE CLIENT
|
||||
// ============================================
|
||||
<>
|
||||
<div className="form-section">
|
||||
<h3>Informations personnelles</h3>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<User size={16} />
|
||||
<span>Nom d'utilisateur</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={clientForm.username}
|
||||
onChange={(e) => setClientForm({ ...clientForm, username: e.target.value })}
|
||||
placeholder="Nom d'utilisateur"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Lock size={16} />
|
||||
<span>Nouveau mot de passe</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={clientForm.password}
|
||||
onChange={(e) => setClientForm({ ...clientForm, password: e.target.value })}
|
||||
placeholder="Laisser vide pour ne pas changer"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<User size={16} />
|
||||
<span>Nom</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={clientForm.nom}
|
||||
onChange={(e) => setClientForm({ ...clientForm, nom: e.target.value })}
|
||||
placeholder="Nom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<User size={16} />
|
||||
<span>Prénom</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={clientForm.prenom}
|
||||
onChange={(e) => setClientForm({ ...clientForm, prenom: e.target.value })}
|
||||
placeholder="Prénom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group full-width">
|
||||
<label>
|
||||
<Phone size={16} />
|
||||
<span>Téléphone</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={clientForm.telephone}
|
||||
onChange={(e) => setClientForm({ ...clientForm, telephone: e.target.value })}
|
||||
placeholder="+33612345678"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-section">
|
||||
<h3>Statistiques (Admin uniquement)</h3>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<span>📦</span>
|
||||
<span>Commandes</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={clientForm.command}
|
||||
onChange={(e) => setClientForm({ ...clientForm, command: parseInt(e.target.value) || 0 })}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<span>🍃</span>
|
||||
<span>Points Weed/Hash</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={clientForm.point}
|
||||
onChange={(e) => setClientForm({ ...clientForm, point: parseInt(e.target.value) || 0 })}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<span>💨</span>
|
||||
<span>Points Zipette</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={clientForm.points_zipette}
|
||||
onChange={(e) => setClientForm({ ...clientForm, points_zipette: parseInt(e.target.value) || 0 })}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<span>⚠️</span>
|
||||
<span>Amendes</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={clientForm.amende}
|
||||
onChange={(e) => setClientForm({ ...clientForm, amende: parseFloat(e.target.value) || 0 })}
|
||||
min="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
// ============================================
|
||||
// FORMULAIRE USER (admin/cabine/livreur)
|
||||
// ============================================
|
||||
<div className="form-section">
|
||||
<h3>Informations utilisateur</h3>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<User size={16} />
|
||||
<span>Nom d'utilisateur</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={userForm.username}
|
||||
onChange={(e) => setUserForm({ ...userForm, username: e.target.value })}
|
||||
placeholder="Nom d'utilisateur"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Lock size={16} />
|
||||
<span>Nouveau mot de passe</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={userForm.password}
|
||||
onChange={(e) => setUserForm({ ...userForm, password: e.target.value })}
|
||||
placeholder="Laisser vide pour ne pas changer"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group full-width">
|
||||
<label>
|
||||
<Shield size={16} />
|
||||
<span>Rôle</span>
|
||||
</label>
|
||||
<div className="custom-select-wrapper">
|
||||
<select
|
||||
value={userForm.role}
|
||||
onChange={(e) => setUserForm({ ...userForm, role: e.target.value as any })}
|
||||
className="role-select"
|
||||
>
|
||||
<option value="admin">Administrateur</option>
|
||||
<option value="cabine">Opérateur Cabine</option>
|
||||
<option value="livreur">Livreur</option>
|
||||
</select>
|
||||
<div className="select-icon">
|
||||
{userForm.role === 'admin' && <Crown size={16} />}
|
||||
{userForm.role === 'cabine' && <Building2 size={16} />}
|
||||
{userForm.role === 'livreur' && <Truck size={16} />}
|
||||
</div>
|
||||
<div className="select-arrow">▼</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="action-button primary"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span className="spinner" />
|
||||
<span>Enregistrement...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save size={18} />
|
||||
<span>Enregistrer les modifications</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditUserModal;
|
||||
@@ -0,0 +1,537 @@
|
||||
/* ============================================
|
||||
ITEMS MODAL - PROFESSIONAL DARK THEME
|
||||
============================================ */
|
||||
|
||||
/* Overlay */
|
||||
.items-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(12px);
|
||||
padding: 1.5rem;
|
||||
animation: items-fadeIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes items-fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.items-modal-content {
|
||||
background: linear-gradient(to bottom, #1a1a1a, #0f0f0f);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(124, 58, 237, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: items-slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes items-slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.98);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.items-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: #ffffff;
|
||||
font-size: 1.25rem;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.items-modal-header h3 svg {
|
||||
color: #a78bfa;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.items-modal-header h3 span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.command-badge {
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
color: #c4b5fd;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
.items-modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #6b7280;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 400;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.items-modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
.items-modal-body {
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
background: #0a0a0a;
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar-thumb {
|
||||
background: rgba(124, 58, 237, 0.3);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.items-modal-body::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.items-modal-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #6b7280;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-empty svg {
|
||||
color: #374151;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.items-modal-empty p {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Summary */
|
||||
.items-modal-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.summary-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
}
|
||||
|
||||
.summary-item svg {
|
||||
color: #a78bfa;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.summary-item span {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-item::before {
|
||||
content: attr(data-label);
|
||||
color: #9ca3af;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Items List */
|
||||
.items-modal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Item Card */
|
||||
.items-modal-card {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-card:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(124, 58, 237, 0.4);
|
||||
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.15);
|
||||
}
|
||||
|
||||
/* Item Number */
|
||||
.item-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
color: #a78bfa;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
/* Item Content */
|
||||
.item-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
color: #f9fafb;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Item Status */
|
||||
.item-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-status.status-pending {
|
||||
background: rgba(251, 146, 60, 0.15);
|
||||
color: #fdba74;
|
||||
border: 1px solid rgba(251, 146, 60, 0.3);
|
||||
}
|
||||
|
||||
.item-status.status-prepared,
|
||||
.item-status.status-packed {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
color: #93c5fd;
|
||||
border: 1px solid rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.item-status.status-delivered {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* Item Details */
|
||||
.item-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.item-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #9ca3af;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #f9fafb;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value.detail-total {
|
||||
color: #34d399;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* Item Meta */
|
||||
.item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
margin-top: 0.75rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.item-id {
|
||||
color: #6b7280;
|
||||
font-size: 0.8125rem;
|
||||
font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Total */
|
||||
.items-modal-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1), rgba(124, 58, 237, 0.05));
|
||||
border: 2px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.items-modal-total span {
|
||||
color: #9ca3af;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.items-modal-total strong {
|
||||
color: #c4b5fd;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
/* Tablet */
|
||||
@media (max-width: 768px) {
|
||||
.items-modal-overlay {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-content {
|
||||
max-height: 92vh;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
|
||||
.items-modal-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.command-badge {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.items-modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.items-modal-summary {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-card {
|
||||
padding: 1.25rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-total {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-total strong {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 480px) {
|
||||
.items-modal-overlay {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.items-modal-header {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
font-size: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.command-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.625rem;
|
||||
}
|
||||
|
||||
.items-modal-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.items-modal-card {
|
||||
padding: 1rem;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.item-number {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.detail-value.detail-total {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.items-modal-total {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.items-modal-total strong {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small Mobile */
|
||||
@media (max-width: 360px) {
|
||||
.items-modal-content {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.items-modal-header h3 {
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch Devices */
|
||||
@media (hover: none) {
|
||||
.items-modal-card:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.summary-item:hover {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import React from "react";
|
||||
import { Package, Clock, CheckCircle } from "lucide-react";
|
||||
import "./Items.css";
|
||||
|
||||
interface OrderItem {
|
||||
id: number;
|
||||
command_id: number;
|
||||
product_id: number;
|
||||
produit: string; // ✅ API utilise "produit" pas "product_name"
|
||||
quantite: number; // ✅ API utilise "quantite" pas "quantity"
|
||||
prix: number; // ✅ API utilise "prix" pas "price"
|
||||
status: string;
|
||||
created_at: string;
|
||||
// Champs additionnels de l'API
|
||||
client_nom?: string;
|
||||
client_prenom?: string;
|
||||
client_telephone?: string;
|
||||
client_username?: string;
|
||||
command_address?: string;
|
||||
delivery_address?: string;
|
||||
livreur_assign?: string;
|
||||
total_prix?: number;
|
||||
}
|
||||
|
||||
interface ItemsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
items: OrderItem[];
|
||||
commandNumber?: string;
|
||||
}
|
||||
|
||||
const ItemsModal: React.FC<ItemsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
items,
|
||||
commandNumber,
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Debug: Afficher les items reçus
|
||||
console.log("🔍 [ITEMS_MODAL] Items reçus:", items);
|
||||
console.log("🔍 [ITEMS_MODAL] Nombre items:", items.length);
|
||||
if (items.length > 0) {
|
||||
console.log("🔍 [ITEMS_MODAL] Premier item:", items[0]);
|
||||
}
|
||||
|
||||
const getItemStatusLabel = (status: string): string => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "pending":
|
||||
return "En attente";
|
||||
case "prepared":
|
||||
return "Préparé";
|
||||
case "packed":
|
||||
return "Emballé";
|
||||
case "delivered":
|
||||
return "Livré";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
const getItemStatusIcon = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case "pending":
|
||||
return <Clock size={14} />;
|
||||
case "prepared":
|
||||
case "packed":
|
||||
return <Package size={14} />;
|
||||
case "delivered":
|
||||
return <CheckCircle size={14} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const calculateTotal = (): number => {
|
||||
return items.reduce((sum, item) => {
|
||||
const price = item.prix || 0; // ✅ Utiliser "prix"
|
||||
return sum + price;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleOverlayClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="items-modal-overlay" onClick={handleOverlayClick}>
|
||||
<div
|
||||
className="items-modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="items-modal-header">
|
||||
<h3>
|
||||
<Package size={24} />
|
||||
<span>
|
||||
Articles de la commande
|
||||
{commandNumber && (
|
||||
<span className="command-badge">
|
||||
{commandNumber}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</h3>
|
||||
<button
|
||||
className="items-modal-close"
|
||||
onClick={onClose}
|
||||
aria-label="Fermer"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="items-modal-body">
|
||||
{items.length === 0 ? (
|
||||
<div className="items-modal-empty">
|
||||
<Package size={48} />
|
||||
<p>Aucun article trouvé</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Items List */}
|
||||
<div className="items-modal-list">
|
||||
{items.map((item, index) => {
|
||||
// Valeurs sécurisées - utiliser les vrais noms de champs de l'API
|
||||
const itemPrice = item.prix || 0;
|
||||
const itemQuantity = item.quantite || 0;
|
||||
const itemName =
|
||||
item.produit || "Produit inconnu";
|
||||
const itemTotal = itemPrice;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="items-modal-card"
|
||||
>
|
||||
<div className="item-number">
|
||||
#{index + 1}
|
||||
</div>
|
||||
|
||||
<div className="item-content">
|
||||
<div className="item-header">
|
||||
<h4 className="item-name">
|
||||
{itemName}
|
||||
</h4>
|
||||
<div
|
||||
className={`item-status status-${(item.status || "pending").toLowerCase()}`}
|
||||
>
|
||||
{getItemStatusIcon(
|
||||
item.status ||
|
||||
"pending",
|
||||
)}
|
||||
<span>
|
||||
{getItemStatusLabel(
|
||||
item.status ||
|
||||
"pending",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="item-details">
|
||||
<div className="item-detail">
|
||||
<span className="detail-label">
|
||||
Quantité
|
||||
</span>
|
||||
<span className="detail-value">
|
||||
{itemQuantity}g
|
||||
</span>
|
||||
</div>
|
||||
<div className="item-detail">
|
||||
<span className="detail-label">
|
||||
Prix unitaire
|
||||
</span>
|
||||
<span className="detail-value">
|
||||
{itemPrice.toFixed(
|
||||
2,
|
||||
)}
|
||||
€
|
||||
</span>
|
||||
</div>
|
||||
<div className="item-detail">
|
||||
<span className="detail-label">
|
||||
Total
|
||||
</span>
|
||||
<span className="detail-value detail-total">
|
||||
{itemTotal.toFixed(
|
||||
2,
|
||||
)}
|
||||
€
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="items-modal-total">
|
||||
<span>Total de la commande</span>
|
||||
<strong>{calculateTotal().toFixed(2)}€</strong>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemsModal;
|
||||
@@ -0,0 +1,419 @@
|
||||
.navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background-color: #1a1a1a;
|
||||
border-bottom: 2px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.hamburger-menu {
|
||||
background: transparent;
|
||||
border: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.hamburger-menu:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.hamburger-line {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: white;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hamburger-line.open:nth-child(1) {
|
||||
transform: rotate(45deg) translate(6px, 6px);
|
||||
}
|
||||
|
||||
.hamburger-line.open:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hamburger-line.open:nth-child(3) {
|
||||
transform: rotate(-45deg) translate(6px, -6px);
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
color: white;
|
||||
font-size: clamp(1.2rem, 4vw, 1.5rem);
|
||||
margin: 0 0 0 1rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cart-button {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
border: none;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: all 0.2s;
|
||||
margin-left: 330px;
|
||||
}
|
||||
|
||||
.cart-button:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.cart-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.cart-count {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background-color: #ff0000;
|
||||
color: white;
|
||||
font-size: 0.7rem;
|
||||
font-weight: bold;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Menu latéral */
|
||||
.side-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -280px;
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
background-color: #0a0a0a;
|
||||
border-right: 2px solid #333;
|
||||
z-index: 200;
|
||||
transition: left 0.3s ease;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.side-menu.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.side-menu-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1rem;
|
||||
border-bottom: 2px solid #333;
|
||||
}
|
||||
|
||||
.side-menu-header h3 {
|
||||
color: white;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.close-menu {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-menu:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.side-menu-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
border-bottom: 1px solid #222;
|
||||
transition: all 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background-color: #1a1a1a;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.menu-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
/* Désactiver hover sur tactile */
|
||||
@media (hover: none) {
|
||||
.menu-item:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sélecteur de couleur */
|
||||
.color-picker-section {
|
||||
margin-top: 0;
|
||||
border-top: 2px solid #333;
|
||||
padding: 1rem;
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
||||
.color-picker-toggle {
|
||||
width: 100%;
|
||||
background-color: #1a1a1a;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 0.8rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.color-picker-toggle:active {
|
||||
transform: scale(0.98);
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.color-preview {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.color-picker-toggle span {
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.arrow.up {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.color-options {
|
||||
margin-top: 0.8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.color-option {
|
||||
background-color: #0a0a0a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 0.8rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.color-option:active {
|
||||
transform: scale(0.98);
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.color-option.active {
|
||||
border-color: #4ade80;
|
||||
background-color: #1a2a1a;
|
||||
}
|
||||
|
||||
.color-circle {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.color-option span {
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.check {
|
||||
color: #4ade80;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
/* Bouton panier flottant */
|
||||
.cart-button-float {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1001;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.2),
|
||||
rgba(109, 40, 217, 0.1)
|
||||
);
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 50%;
|
||||
color: #7c3aed;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.cart-button-float:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.3),
|
||||
rgba(109, 40, 217, 0.15)
|
||||
);
|
||||
border-color: rgba(124, 58, 237, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.cart-button-float:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.cart-icon-fa {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.cart-count {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2px;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||
border: 2px solid #0f0f0f;
|
||||
}
|
||||
|
||||
/* Bouton Telegram */
|
||||
.telegram-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
padding: 0.9rem 1rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(37, 161, 244, 0.1),
|
||||
rgba(32, 139, 220, 0.05)
|
||||
);
|
||||
border: 1px solid rgba(37, 161, 244, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #25a1f4;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.telegram-button:hover:not(:disabled) {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(37, 161, 244, 0.15),
|
||||
rgba(32, 139, 220, 0.08)
|
||||
);
|
||||
border-color: rgba(37, 161, 244, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(37, 161, 244, 0.3);
|
||||
}
|
||||
|
||||
.telegram-button:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.telegram-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Footer styles */
|
||||
.sidebar-footer {
|
||||
margin-top: auto;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useCart } from "../context/CartContext";
|
||||
import "./Navbar.css";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faHome,
|
||||
faBox,
|
||||
faShoppingCart,
|
||||
faTruck,
|
||||
faClockRotateLeft,
|
||||
faSignOutAlt,
|
||||
faBars,
|
||||
faTimes,
|
||||
faChevronRight,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faTelegram } from "@fortawesome/free-brands-svg-icons";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: IconDefinition;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function Navbar() {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState<boolean>(false);
|
||||
const { cartCount } = useCart();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
id: "accueil",
|
||||
label: "Accueil",
|
||||
icon: faHome,
|
||||
path: "/user/accueil",
|
||||
},
|
||||
{
|
||||
id: "produits",
|
||||
label: "Nos Produits",
|
||||
icon: faBox,
|
||||
path: "/user/nos-produits",
|
||||
},
|
||||
{
|
||||
id: "panier",
|
||||
label: "Passer Commande",
|
||||
icon: faShoppingCart,
|
||||
path: "/user/panier",
|
||||
},
|
||||
{
|
||||
id: "suivi",
|
||||
label: "Suivi Livraison",
|
||||
icon: faTruck,
|
||||
path: "/user/suivi-livraison",
|
||||
},
|
||||
{
|
||||
id: "historique",
|
||||
label: "Historique",
|
||||
icon: faClockRotateLeft,
|
||||
path: "/user/consultation-historique",
|
||||
},
|
||||
];
|
||||
|
||||
const toggleMenu = (): void => {
|
||||
setIsMenuOpen(!isMenuOpen);
|
||||
};
|
||||
|
||||
const closeMenu = (): void => {
|
||||
setIsMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleNavigation = (path: string): void => {
|
||||
navigate(path);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const handleLogout = async (): Promise<void> => {
|
||||
if (isLoggingOut) return;
|
||||
setIsLoggingOut(true);
|
||||
console.log("🚪 [LOGOUT] Déconnexion en cours...");
|
||||
|
||||
try {
|
||||
const token = sessionStorage.getItem("admin_token");
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/v2/admin/auth/logout",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
console.log("✅ [LOGOUT] Déconnexion backend réussie");
|
||||
} else {
|
||||
console.warn("⚠️ [LOGOUT] Erreur backend (ignorée)");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"⚠️ [LOGOUT] Erreur réseau backend (ignorée):",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sessionStorage.removeItem("admin_token");
|
||||
sessionStorage.removeItem("admin_username");
|
||||
console.log("✅ [LOGOUT] SessionStorage nettoyé");
|
||||
|
||||
navigate("/login/client", { replace: true });
|
||||
} catch (error) {
|
||||
console.error("❌ [LOGOUT] Erreur:", error);
|
||||
|
||||
sessionStorage.removeItem("admin_token");
|
||||
sessionStorage.removeItem("admin_username");
|
||||
navigate("/login/client", { replace: true });
|
||||
} finally {
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTelegram = (): void => {
|
||||
window.open("https://t.me/milieu_nantais", "_blank");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Bouton Toggle */}
|
||||
<button
|
||||
className={`sidebar-toggle ${isMenuOpen ? "sidebar-open" : ""}`}
|
||||
onClick={toggleMenu}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<FontAwesomeIcon icon={isMenuOpen ? faTimes : faBars} />
|
||||
</button>
|
||||
|
||||
{/* Overlay */}
|
||||
{isMenuOpen && (
|
||||
<div className="sidebar-overlay" onClick={closeMenu} />
|
||||
)}
|
||||
|
||||
{/* Bouton Panier (flottant en haut à droite) */}
|
||||
<button
|
||||
className="cart-button-float"
|
||||
onClick={() => navigate("/user/panier")}
|
||||
aria-label="Panier"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faShoppingCart}
|
||||
className="cart-icon-fa"
|
||||
/>
|
||||
{cartCount > 0 && (
|
||||
<span className="cart-count">{cartCount}</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className={`sidebar ${isMenuOpen ? "open" : ""}`}>
|
||||
{/* Header */}
|
||||
<div className="sidebar-header">
|
||||
<div className="sidebar-logo">
|
||||
<div className="logo-icon">
|
||||
<FontAwesomeIcon icon={faShoppingCart} size="lg" />
|
||||
</div>
|
||||
<div className="logo-text">
|
||||
<h2>Milieu-Nantais</h2>
|
||||
<p>User Panel</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Menu */}
|
||||
<nav className="sidebar-nav">
|
||||
<ul className="menu-list">
|
||||
{menuItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
className={`menu-item ${location.pathname === item.path ? "active" : ""}`}
|
||||
onClick={() => handleNavigation(item.path)}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<span className="menu-icon">
|
||||
<FontAwesomeIcon icon={item.icon} />
|
||||
</span>
|
||||
<span className="menu-label">
|
||||
{item.label}
|
||||
</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className="menu-arrow"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Footer avec boutons Telegram et déconnexion */}
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
className="telegram-button"
|
||||
onClick={handleTelegram}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTelegram} />
|
||||
<span>Telegram</span>
|
||||
</button>
|
||||
<button
|
||||
className="logout-button"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||
<span>
|
||||
{isLoggingOut ? "Déconnexion..." : "Déconnexion"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Navbar;
|
||||
@@ -0,0 +1,715 @@
|
||||
.product-card {
|
||||
background-color: #1a1a1a;
|
||||
border: 2px solid white;
|
||||
border-radius: 15px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: all 0.3s ease;
|
||||
cursor: default; /* ✅ Pas de cursor pointer */
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 8px 25px rgba(255, 255, 255, 0.2);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.product-card:active {
|
||||
transform: scale(0.97);
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
|
||||
.product-card-content {
|
||||
cursor: default; /* ✅ Pas de cursor pointer */
|
||||
}
|
||||
|
||||
.product-image-container {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background-color: #2a2a2a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sold-out-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: rgba(255, 0, 0, 0.95);
|
||||
border: 6px solid rgba(255, 0, 0, 0.95);
|
||||
padding: clamp(0.5rem, 2vw, 0.8rem) clamp(1.5rem, 5vw, 2.5rem);
|
||||
font-size: clamp(1.3rem, 5.5vw, 2.2rem);
|
||||
font-weight: 900;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 0 8px rgba(255, 0, 0, 0.6);
|
||||
text-shadow:
|
||||
2px 2px 6px rgba(0, 0, 0, 0.9),
|
||||
-2px -2px 6px rgba(0, 0, 0, 0.9),
|
||||
0 0 10px rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.product-card.out-of-stock {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.product-card.out-of-stock .product-image {
|
||||
filter: grayscale(100%) brightness(0.6) blur(1.5px);
|
||||
}
|
||||
|
||||
.product-image3 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: clamp(0.8rem, 3vw, 1.2rem);
|
||||
background-color: #1a1a1a;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
color: white;
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: clamp(1rem, 3.5vw, 1.2rem);
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
color: #4ade80;
|
||||
margin: 0;
|
||||
font-size: clamp(1.1rem, 4vw, 1.4rem);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product-stock {
|
||||
color: #cccccc;
|
||||
font-size: clamp(0.85rem, 2.5vw, 1rem);
|
||||
text-align: center;
|
||||
padding: clamp(0.6rem, 2vw, 0.8rem);
|
||||
background-color: #0a0a0a;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.quick-add-section {
|
||||
padding: clamp(0.8rem, 3vw, 1rem);
|
||||
background-color: #0a0a0a;
|
||||
border-top: 1px solid #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.quick-add-btn {
|
||||
width: 100%;
|
||||
padding: clamp(0.7rem, 2.5vw, 0.9rem) clamp(1rem, 3vw, 1.5rem);
|
||||
background: linear-gradient(135deg, #4ade80 0%, #22c55e 100%);
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: clamp(0.9rem, 3vw, 1.05rem);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 8px rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
|
||||
.quick-add-btn:hover {
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(74, 222, 128, 0.4);
|
||||
}
|
||||
|
||||
.quick-add-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(74, 222, 128, 0.3);
|
||||
}
|
||||
|
||||
/* ===== BOUTON "AJOUTER RAPIDEMENT" PAR CATÉGORIE ===== */
|
||||
|
||||
/* Catégorie: weed&hash - VERT */
|
||||
div[data-category="weed&hash"] .quick-add-btn {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
div[data-category="weed&hash"] .quick-add-btn:hover {
|
||||
background: linear-gradient(135deg, #059669 0%, #047857 100%);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="weed&hash"] .quick-add-btn:active {
|
||||
box-shadow: 0 1px 4px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
/* Catégorie: tous - VIOLET */
|
||||
div[data-category="tous"] .quick-add-btn {
|
||||
background: linear-gradient(135deg, #9333ea 0%, #7e22ce 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(147, 51, 234, 0.4);
|
||||
}
|
||||
|
||||
div[data-category="tous"] .quick-add-btn:hover {
|
||||
background: linear-gradient(135deg, #7e22ce 0%, #6b21a8 100%);
|
||||
box-shadow: 0 4px 12px rgba(147, 51, 234, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="tous"] .quick-add-btn:active {
|
||||
box-shadow: 0 1px 4px rgba(147, 51, 234, 0.4);
|
||||
}
|
||||
|
||||
/* Catégorie: zipette&co - BLANC CASSÉ */
|
||||
div[data-category="zipette&co"] .quick-add-btn {
|
||||
background: linear-gradient(135deg, #f5f5f0 0%, #e8e8e0 100%);
|
||||
color: #1a1a1a;
|
||||
box-shadow: 0 2px 8px rgba(245, 245, 240, 0.4);
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .quick-add-btn:hover {
|
||||
background: linear-gradient(135deg, #e8e8e0 0%, #dcdcd0 100%);
|
||||
box-shadow: 0 4px 12px rgba(245, 245, 240, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .quick-add-btn:active {
|
||||
box-shadow: 0 1px 4px rgba(245, 245, 240, 0.4);
|
||||
}
|
||||
|
||||
/* Catégorie: gros&semi - BLEU CIEL */
|
||||
div[data-category="gros&semi"] .quick-add-btn {
|
||||
background: linear-gradient(135deg, #3dc2f7 0%, #0ea5e9 100%);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(61, 194, 247, 0.4);
|
||||
}
|
||||
|
||||
div[data-category="gros&semi"] .quick-add-btn:hover {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
box-shadow: 0 4px 12px rgba(61, 194, 247, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="gros&semi"] .quick-add-btn:active {
|
||||
box-shadow: 0 1px 4px rgba(61, 194, 247, 0.4);
|
||||
}
|
||||
|
||||
.quick-add-btn.disabled {
|
||||
background: linear-gradient(135deg, #666 0%, #444 100%);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.quick-add-btn.disabled:hover {
|
||||
transform: none;
|
||||
background: linear-gradient(135deg, #666 0%, #444 100%);
|
||||
}
|
||||
|
||||
.quantity-select {
|
||||
width: 100%;
|
||||
padding: clamp(0.7rem, 2.5vw, 0.9rem);
|
||||
background-color: #2a2a2a;
|
||||
color: white;
|
||||
border: 2px solid #4ade80;
|
||||
border-radius: 8px;
|
||||
font-size: clamp(0.9rem, 3vw, 1.05rem);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%234ade80' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.8rem center;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
.quantity-select:hover {
|
||||
border-color: #22c55e;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.quantity-select:focus {
|
||||
outline: none;
|
||||
border-color: #22c55e;
|
||||
box-shadow: 0 0 0 3px rgba(74, 222, 128, 0.2);
|
||||
}
|
||||
|
||||
/* ===== SELECT "CHOISIR UNE QUANTITÉ" PAR CATÉGORIE ===== */
|
||||
|
||||
/* Catégorie: weed&hash - VERT */
|
||||
div[data-category="weed&hash"] .quantity-select {
|
||||
border-color: #10b981;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2310b981' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
div[data-category="weed&hash"] .quantity-select:hover {
|
||||
border-color: #059669;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
div[data-category="weed&hash"] .quantity-select:focus {
|
||||
border-color: #059669;
|
||||
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
/* Catégorie: tous - VIOLET */
|
||||
div[data-category="tous"] .quantity-select {
|
||||
border-color: #9333ea;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239333ea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
div[data-category="tous"] .quantity-select:hover {
|
||||
border-color: #7e22ce;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
div[data-category="tous"] .quantity-select:focus {
|
||||
border-color: #7e22ce;
|
||||
box-shadow: 0 0 0 3px rgba(147, 51, 234, 0.3);
|
||||
}
|
||||
|
||||
/* Catégorie: zipette&co - BLANC CASSÉ */
|
||||
div[data-category="zipette&co"] .quantity-select {
|
||||
border-color: #f5f5f0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23F5F5F0' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .quantity-select:hover {
|
||||
border-color: #e8e8e0;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .quantity-select:focus {
|
||||
border-color: #e8e8e0;
|
||||
box-shadow: 0 0 0 3px rgba(245, 245, 240, 0.3);
|
||||
}
|
||||
|
||||
/* Catégorie: gros&semi - BLEU CIEL */
|
||||
div[data-category="gros&semi"] .quantity-select {
|
||||
border-color: #3dc2f7;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%233dc2f7' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
div[data-category="gros&semi"] .quantity-select:hover {
|
||||
border-color: #0ea5e9;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
div[data-category="gros&semi"] .quantity-select:focus {
|
||||
border-color: #0ea5e9;
|
||||
box-shadow: 0 0 0 3px rgba(61, 194, 247, 0.3);
|
||||
}
|
||||
|
||||
.quantity-select option {
|
||||
background-color: #2a2a2a;
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
width: 100%;
|
||||
padding: clamp(0.7rem, 2.5vw, 0.9rem);
|
||||
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: clamp(0.9rem, 3vw, 1.05rem);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
animation: successPulse 0.5s ease;
|
||||
}
|
||||
|
||||
/* ===== MESSAGE DE SUCCÈS "✓ AJOUTÉ !" PAR CATÉGORIE ===== */
|
||||
|
||||
/* Catégorie: weed&hash - VERT */
|
||||
div[data-category="weed&hash"] .success-message {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Catégorie: tous - VIOLET */
|
||||
div[data-category="tous"] .success-message {
|
||||
background: linear-gradient(135deg, #9333ea 0%, #7e22ce 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Catégorie: zipette&co - BLANC CASSÉ */
|
||||
div[data-category="zipette&co"] .success-message {
|
||||
background: linear-gradient(135deg, #f5f5f0 0%, #e8e8e0 100%);
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Catégorie: gros&semi - BLEU CIEL */
|
||||
div[data-category="gros&semi"] .success-message {
|
||||
background: linear-gradient(135deg, #3dc2f7 0%, #0ea5e9 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== ICÔNE MÉDIA (CAMÉRA) - CLIQUABLE ===== */
|
||||
.media-indicator {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 3;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-indicator:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
transform: scale(1.15);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.media-indicator:active {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.media-indicator i {
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
animation: cameraPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes cameraPulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Variantes par catégorie pour l'icône caméra */
|
||||
div[data-category="weed&hash"] .media-indicator {
|
||||
background: rgba(16, 185, 129, 0.9);
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="weed&hash"] .media-indicator:hover {
|
||||
background: rgba(16, 185, 129, 1);
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
div[data-category="tous"] .media-indicator {
|
||||
background: rgba(147, 51, 234, 0.9);
|
||||
border-color: rgba(147, 51, 234, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="tous"] .media-indicator:hover {
|
||||
background: rgba(147, 51, 234, 1);
|
||||
border-color: #9333ea;
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .media-indicator {
|
||||
background: rgba(245, 245, 240, 0.9);
|
||||
border-color: rgba(245, 245, 240, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .media-indicator:hover {
|
||||
background: rgba(245, 245, 240, 1);
|
||||
border-color: #f5f5f0;
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .media-indicator i {
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
div[data-category="gros&semi"] .media-indicator {
|
||||
background: rgba(61, 194, 247, 0.9);
|
||||
border-color: rgba(61, 194, 247, 0.5);
|
||||
}
|
||||
|
||||
div[data-category="gros&semi"] .media-indicator:hover {
|
||||
background: rgba(61, 194, 247, 1);
|
||||
border-color: #3dc2f7;
|
||||
}
|
||||
|
||||
/* ===== BOUTON DÉTAILS ===== */
|
||||
.details-button {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(10px);
|
||||
color: white;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 25px;
|
||||
padding: 8px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
z-index: 4;
|
||||
transition: all 0.3s ease;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.product-image-container:hover .details-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.details-button:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
color: #1a1a1a;
|
||||
border-color: white;
|
||||
transform: translateX(-50%) scale(1.05);
|
||||
}
|
||||
|
||||
.details-button:active {
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.details-button i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.details-button span {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Variantes par catégorie pour le bouton détails */
|
||||
div[data-category="weed&hash"] .details-button:hover {
|
||||
background: #10b981;
|
||||
color: white;
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
div[data-category="tous"] .details-button:hover {
|
||||
background: #9333ea;
|
||||
color: white;
|
||||
border-color: #9333ea;
|
||||
}
|
||||
|
||||
div[data-category="zipette&co"] .details-button:hover {
|
||||
background: #f5f5f0;
|
||||
color: #1a1a1a;
|
||||
border-color: #f5f5f0;
|
||||
}
|
||||
|
||||
div[data-category="gros&semi"] .details-button:hover {
|
||||
background: #3dc2f7;
|
||||
color: white;
|
||||
border-color: #3dc2f7;
|
||||
}
|
||||
|
||||
/* ===== MODAL VIDÉO ===== */
|
||||
.video-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.video-modal-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||
animation: scaleIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0.9);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.video-close-btn {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.video-close-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.video-close-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.video-close-btn i {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* ===== RESPONSIVE ===== */
|
||||
|
||||
/* Version mobile - bouton détails toujours visible */
|
||||
@media (max-width: 768px) {
|
||||
.details-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
padding: 6px 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.details-button i {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.details-button span {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.video-modal {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.video-modal-content {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.video-close-btn {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.video-close-btn i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
max-height: 70vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Petits écrans */
|
||||
@media (max-width: 480px) {
|
||||
.details-button {
|
||||
padding: 5px 14px;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.details-button i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.details-button span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.media-indicator {
|
||||
width: 36px;
|
||||
height: 49px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.media-indicator i {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Désactiver hover sur tactile */
|
||||
@media (hover: none) {
|
||||
.product-card:hover {
|
||||
transform: none;
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.quick-add-btn:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useCart } from "../context/CartContext";
|
||||
import "./ProductCard.css";
|
||||
|
||||
interface ProductCardProps {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
unit: string;
|
||||
image: string;
|
||||
stock: number;
|
||||
category: string;
|
||||
prices?: Array<{ quantity: number; price: number }>;
|
||||
hasVideo?: boolean;
|
||||
videoUrl?: string; // ✨ Nouveau prop pour l'URL de la vidéo
|
||||
}
|
||||
|
||||
function ProductCard({
|
||||
id,
|
||||
name,
|
||||
price,
|
||||
image,
|
||||
stock,
|
||||
category = "autre",
|
||||
prices,
|
||||
hasVideo = false,
|
||||
videoUrl,
|
||||
}: ProductCardProps) {
|
||||
const navigate = useNavigate();
|
||||
const { addToCart } = useCart();
|
||||
|
||||
const [showQuantitySelect, setShowQuantitySelect] = useState(false);
|
||||
const [selectedQuantity, setSelectedQuantity] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [showSuccess, setShowSuccess] = useState(false);
|
||||
const [showVideo, setShowVideo] = useState(false); // ✨ État pour afficher/masquer la vidéo
|
||||
|
||||
const isOutOfStock = stock === 0;
|
||||
const normalizedCategory = (category || "autre").toLowerCase().trim();
|
||||
|
||||
const handleDetailsClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/user/product/${id}`);
|
||||
};
|
||||
|
||||
// ✨ Handler pour afficher/masquer la vidéo
|
||||
const handleVideoToggle = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setShowVideo(!showVideo);
|
||||
};
|
||||
|
||||
// ✨ Handler pour fermer la vidéo
|
||||
const handleCloseVideo = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setShowVideo(false);
|
||||
};
|
||||
|
||||
const handleQuickAddClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
console.log("🔘 Bouton cliqué", {
|
||||
id,
|
||||
name,
|
||||
category: normalizedCategory,
|
||||
isOutOfStock,
|
||||
prices: prices?.length || 0,
|
||||
showQuantitySelect,
|
||||
});
|
||||
|
||||
if (!isOutOfStock && prices && prices.length > 0) {
|
||||
setShowQuantitySelect(!showQuantitySelect);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuantitySelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedGrams = Number(e.target.value);
|
||||
setSelectedQuantity(selectedGrams);
|
||||
|
||||
const priceOption = prices?.find((p) => p.quantity === selectedGrams);
|
||||
if (priceOption) {
|
||||
console.log("💾 [ProductCard] Ajout au panier:", {
|
||||
id,
|
||||
name,
|
||||
category: normalizedCategory,
|
||||
quantity: selectedGrams,
|
||||
price: priceOption.price,
|
||||
});
|
||||
|
||||
addToCart({
|
||||
product_id: id,
|
||||
name_product: name,
|
||||
price: priceOption.price,
|
||||
quantity: selectedGrams,
|
||||
category: normalizedCategory,
|
||||
image: image,
|
||||
});
|
||||
|
||||
setShowSuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowSuccess(false);
|
||||
setShowQuantitySelect(false);
|
||||
setSelectedQuantity(null);
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`product-card ${isOutOfStock ? "out-of-stock" : ""}`}>
|
||||
<div className="product-card-content">
|
||||
<div className="product-image-container">
|
||||
<img
|
||||
src={image}
|
||||
alt={name}
|
||||
className="product-image3"
|
||||
loading="lazy"
|
||||
/>
|
||||
{/* ✨ Icône caméra cliquable */}
|
||||
{hasVideo && !isOutOfStock && videoUrl && (
|
||||
<button
|
||||
className="media-indicator"
|
||||
onClick={handleVideoToggle}
|
||||
aria-label="Voir la vidéo du produit"
|
||||
>
|
||||
<i className="fas fa-camera"></i>
|
||||
</button>
|
||||
)}
|
||||
{/* ✨ Bouton "Détails" */}
|
||||
<button
|
||||
className="details-button"
|
||||
onClick={handleDetailsClick}
|
||||
aria-label="Voir les détails du produit"
|
||||
>
|
||||
<i className="fas fa-info-circle"></i>
|
||||
<span>Détails</span>
|
||||
</button>
|
||||
{isOutOfStock && (
|
||||
<div className="sold-out-overlay">SOLD OUT</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="product-info">
|
||||
<h3 className="product-name">{name}</h3>
|
||||
<p className="product-price">
|
||||
{price > 0
|
||||
? `${price.toFixed(2)} €`
|
||||
: "Prix non disponible"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="quick-add-section"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{showSuccess ? (
|
||||
<div className="success-message">✓ Ajouté !</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className={`quick-add-btn ${isOutOfStock ? "disabled" : ""}`}
|
||||
onClick={handleQuickAddClick}
|
||||
disabled={isOutOfStock}
|
||||
>
|
||||
{isOutOfStock
|
||||
? "Rupture de stock"
|
||||
: "Ajouter rapidement"}
|
||||
</button>
|
||||
|
||||
{showQuantitySelect && prices && prices.length > 0 && (
|
||||
<select
|
||||
className="quantity-select"
|
||||
value={selectedQuantity ?? ""}
|
||||
onChange={handleQuantitySelect}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<option value="">Choisir une quantité</option>
|
||||
{prices.map((priceOption) => (
|
||||
<option
|
||||
key={priceOption.quantity}
|
||||
value={priceOption.quantity}
|
||||
>
|
||||
{priceOption.quantity}g -{" "}
|
||||
{priceOption.price.toFixed(2)} €
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ✨ Modal vidéo */}
|
||||
{showVideo && videoUrl && (
|
||||
<div className="video-modal" onClick={handleCloseVideo}>
|
||||
<div
|
||||
className="video-modal-content"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="video-close-btn"
|
||||
onClick={handleCloseVideo}
|
||||
aria-label="Fermer la vidéo"
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
<video
|
||||
src={videoUrl}
|
||||
controls
|
||||
autoPlay
|
||||
className="video-player"
|
||||
>
|
||||
Votre navigateur ne supporte pas la lecture de
|
||||
vidéos.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProductCard;
|
||||
@@ -0,0 +1,340 @@
|
||||
// ============================================
|
||||
// components/admin/ProductCreateModal.tsx - VERSION AVEC BOUTONS
|
||||
// ============================================
|
||||
// ✅ Boutons stylisés au lieu de select natif (comme AdminUsers)
|
||||
// ✅ Pas de hover gris natif !
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { X, Plus, Trash2, Upload, DollarSign } from 'lucide-react';
|
||||
import { createProductAdmin } from '../api/api_admin';
|
||||
import type { CreateProductData, ProductPrice} from '../api/api_admin_types';
|
||||
import './ProductModal.css';
|
||||
|
||||
interface ProductCreateModalProps {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ProductCreateModal: React.FC<ProductCreateModalProps> = ({ onClose, onSuccess }) => {
|
||||
// ============================================
|
||||
// 📝 STATE
|
||||
// ============================================
|
||||
const [name, setName] = useState('');
|
||||
const [category, setCategory] = useState('weed&hash');
|
||||
const [description, setDescription] = useState('');
|
||||
const [stock, setStock] = useState<number>(0);
|
||||
const [prices, setPrices] = useState<ProductPrice[]>([
|
||||
{ quantity: 1, price: 0 }
|
||||
]);
|
||||
const [mediaFiles, setMediaFiles] = useState<File[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// ✅ Options de catégories
|
||||
const categoryOptions = [
|
||||
{ value: 'weed&hash', label: 'Weed & Hash' },
|
||||
{ value: 'zipette&co', label: 'Zipette & Co' },
|
||||
{ value: 'gros&semi', label: 'Gros & Semi' }
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 💰 GESTION DES PRIX
|
||||
// ============================================
|
||||
const addPriceRow = () => {
|
||||
setPrices([...prices, { quantity: 1, price: 0 }]);
|
||||
};
|
||||
|
||||
const removePriceRow = (index: number) => {
|
||||
if (prices.length > 1) {
|
||||
setPrices(prices.filter((_, i) => i !== index));
|
||||
}
|
||||
};
|
||||
|
||||
const updatePrice = (index: number, field: 'quantity' | 'price', value: number) => {
|
||||
const newPrices = [...prices];
|
||||
newPrices[index][field] = value;
|
||||
setPrices(newPrices);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 📁 GESTION DES FICHIERS
|
||||
// ============================================
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
setMediaFiles([...mediaFiles, ...newFiles]);
|
||||
console.log(`📁 ${newFiles.length} fichier(s) ajouté(s)`);
|
||||
}
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
setMediaFiles(mediaFiles.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ✅ VALIDATION
|
||||
// ============================================
|
||||
const validateForm = (): string | null => {
|
||||
if (!name.trim()) return 'Le nom est requis';
|
||||
if (!description.trim()) return 'La description est requise';
|
||||
if (stock < 0) return 'Le stock ne peut pas être négatif';
|
||||
if (prices.length === 0) return 'Au moins un prix est requis';
|
||||
|
||||
for (const price of prices) {
|
||||
if (price.quantity <= 0) return 'Toutes les quantités doivent être positives';
|
||||
if (price.price <= 0) return 'Tous les prix doivent être positifs';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 📤 SOUMISSION
|
||||
// ============================================
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const validationError = validateForm();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log('📤 [CREATE_MODAL] Envoi du formulaire...');
|
||||
|
||||
const productData: CreateProductData = {
|
||||
name: name.trim(),
|
||||
category,
|
||||
description: description.trim(),
|
||||
stock,
|
||||
prices,
|
||||
};
|
||||
|
||||
const response = await createProductAdmin(productData, mediaFiles);
|
||||
|
||||
if (response.success) {
|
||||
console.log('✅ [CREATE_MODAL] Produit créé avec succès');
|
||||
onSuccess();
|
||||
} else {
|
||||
setError(response.error || 'Erreur lors de la création du produit');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [CREATE_MODAL] Erreur:', error);
|
||||
setError(error instanceof Error ? error.message : 'Erreur inconnue');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 🎨 RENDER
|
||||
// ============================================
|
||||
return (
|
||||
<>
|
||||
<div className="modal-overlay" onClick={onClose} />
|
||||
<div className="product-modal">
|
||||
{/* Header */}
|
||||
<div className="modal-header">
|
||||
<h2>Créer un Nouveau Produit</h2>
|
||||
<button className="close-modal" onClick={onClose}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<form onSubmit={handleSubmit} className="modal-content">
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informations de base */}
|
||||
<div className="form-section">
|
||||
<h3>Informations de Base</h3>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Nom du Produit *</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Ex: OG Kush Premium"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ✅ CATÉGORIE AVEC BOUTONS AU LIEU DE SELECT */}
|
||||
<div className="form-group">
|
||||
<label>Catégorie *</label>
|
||||
<div className="filter-buttons">
|
||||
{categoryOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={category === option.value ? 'active' : ''}
|
||||
onClick={() => setCategory(option.value)}
|
||||
disabled={loading}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description *</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Décrivez le produit en détail..."
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="stock">Stock (en grammes) *</label>
|
||||
<input
|
||||
id="stock"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={stock}
|
||||
onChange={(e) => setStock(parseFloat(e.target.value))}
|
||||
placeholder="100"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prix */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<h3>Tarifs</h3>
|
||||
<button type="button" className="add-price-btn" onClick={addPriceRow}>
|
||||
<Plus size={18} />
|
||||
Ajouter un Prix
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="prices-grid">
|
||||
{prices.map((price, index) => (
|
||||
<div key={index} className="price-row">
|
||||
<div className="price-input-group">
|
||||
<label>Quantité (g)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={price.quantity}
|
||||
onChange={(e) => updatePrice(index, 'quantity', parseInt(e.target.value))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="price-input-group">
|
||||
<label>Prix (€)</label>
|
||||
<div className="price-input-wrapper">
|
||||
<DollarSign size={18} />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={price.price}
|
||||
onChange={(e) => updatePrice(index, 'price', parseFloat(e.target.value))}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{prices.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="remove-price-btn"
|
||||
onClick={() => removePriceRow(index)}
|
||||
title="Supprimer ce prix"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Médias */}
|
||||
<div className="form-section">
|
||||
<h3>Médias (Images & Vidéos)</h3>
|
||||
|
||||
<div className="upload-zone">
|
||||
<label htmlFor="media-upload" className="upload-label">
|
||||
<Upload size={32} />
|
||||
<p>Cliquez pour ajouter des fichiers</p>
|
||||
<span>Images (JPG, PNG, WebP) ou Vidéos (MP4, WebM)</span>
|
||||
</label>
|
||||
<input
|
||||
id="media-upload"
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{mediaFiles.length > 0 && (
|
||||
<div className="files-list">
|
||||
{mediaFiles.map((file, index) => (
|
||||
<div key={index} className="file-item">
|
||||
<div className="file-info">
|
||||
<span className="file-name">{file.name}</span>
|
||||
<span className="file-size">
|
||||
{(file.size / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-file-btn"
|
||||
onClick={() => removeFile(index)}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="action-button primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Création...' : 'Créer le Produit'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCreateModal;
|
||||
@@ -0,0 +1,300 @@
|
||||
// ============================================
|
||||
// components/admin/ProductDetailsModal.tsx
|
||||
// ============================================
|
||||
// ✅ Modal d'affichage détaillé d'un produit
|
||||
// ✅ Galerie de médias (images/vidéos)
|
||||
// ✅ Informations complètes
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
X,
|
||||
Package,
|
||||
DollarSign,
|
||||
Box,
|
||||
Calendar,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Image as ImageIcon,
|
||||
Video as VideoIcon,
|
||||
Play
|
||||
} from 'lucide-react';
|
||||
import type { Product } from '../api/api_admin_types'; // ✅ CORRIGÉ
|
||||
import './ProductModal.css';
|
||||
|
||||
interface ProductDetailsModalProps {
|
||||
product: Product;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ProductDetailsModal: React.FC<ProductDetailsModalProps> = ({ product, onClose }) => {
|
||||
// ============================================
|
||||
// 📝 STATE
|
||||
// ============================================
|
||||
const [currentMediaIndex, setCurrentMediaIndex] = useState(0);
|
||||
|
||||
// ============================================
|
||||
// 🎨 HELPER FUNCTIONS
|
||||
// ============================================
|
||||
const getCategoryLabel = (category: string): string => {
|
||||
switch (category) {
|
||||
case 'weed&hash': return 'Weed & Hash';
|
||||
case 'zipette&co': return 'Zipette & Co';
|
||||
case 'gros&semi': return 'Gros & Semi';
|
||||
default: return category;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateString?: string): string => {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const hasMedia = product.media && product.media.length > 0;
|
||||
const currentMedia = hasMedia && product.media ? product.media[currentMediaIndex] : null;
|
||||
|
||||
const nextMedia = () => {
|
||||
if (hasMedia && product.media && currentMediaIndex < product.media.length - 1) {
|
||||
setCurrentMediaIndex(currentMediaIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const prevMedia = () => {
|
||||
if (hasMedia && currentMediaIndex > 0) {
|
||||
setCurrentMediaIndex(currentMediaIndex - 1);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 🎨 RENDER
|
||||
// ============================================
|
||||
return (
|
||||
<>
|
||||
<div className="modal-overlay" onClick={onClose} />
|
||||
<div className="product-modal details-modal">
|
||||
{/* Header */}
|
||||
<div className="modal-header">
|
||||
<div className="header-title-section">
|
||||
<Package size={24} className="header-icon" />
|
||||
<div>
|
||||
<h2>{product.name}</h2>
|
||||
<p className="modal-subtitle">{getCategoryLabel(product.category)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="close-modal" onClick={onClose}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="modal-content details-content">
|
||||
{/* Galerie de médias */}
|
||||
{hasMedia && (
|
||||
<div className="media-gallery">
|
||||
<div className="media-viewer">
|
||||
{currentMedia?.type === 'image' ? (
|
||||
<img
|
||||
src={`http://localhost:8080${currentMedia.url}`}
|
||||
alt={product.name}
|
||||
className="media-display"
|
||||
/>
|
||||
) : currentMedia?.type === 'video' ? (
|
||||
<div className="video-container">
|
||||
<video
|
||||
src={`http://localhost:8080${currentMedia.url}`}
|
||||
controls
|
||||
className="media-display"
|
||||
>
|
||||
Votre navigateur ne supporte pas la lecture de vidéos.
|
||||
</video>
|
||||
</div>
|
||||
) : (
|
||||
<div className="media-placeholder">
|
||||
<Package size={64} />
|
||||
<p>Aucun média disponible</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
{product.media && product.media.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="media-nav prev"
|
||||
onClick={prevMedia}
|
||||
disabled={currentMediaIndex === 0}
|
||||
>
|
||||
<ChevronLeft size={24} />
|
||||
</button>
|
||||
<button
|
||||
className="media-nav next"
|
||||
onClick={nextMedia}
|
||||
disabled={currentMediaIndex === product.media.length - 1}
|
||||
>
|
||||
<ChevronRight size={24} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Compteur */}
|
||||
<div className="media-counter">
|
||||
{currentMediaIndex + 1} / {product.media?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Miniatures */}
|
||||
{product.media && product.media.length > 1 && (
|
||||
<div className="media-thumbnails">
|
||||
{product.media.map((media, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`thumbnail ${index === currentMediaIndex ? 'active' : ''}`}
|
||||
onClick={() => setCurrentMediaIndex(index)}
|
||||
>
|
||||
{media.type === 'image' ? (
|
||||
<img
|
||||
src={`http://localhost:8080${media.url}`}
|
||||
alt={`Media ${index + 1}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="video-thumb">
|
||||
<Play size={20} />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pas de médias */}
|
||||
{!hasMedia && (
|
||||
<div className="no-media-placeholder">
|
||||
<ImageIcon size={64} />
|
||||
<p>Aucun média disponible pour ce produit</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informations détaillées */}
|
||||
<div className="details-grid">
|
||||
{/* Description */}
|
||||
<div className="detail-section full-width">
|
||||
<h3 className="section-title">
|
||||
<Package size={18} />
|
||||
Description
|
||||
</h3>
|
||||
<p className="detail-description">{product.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Stock */}
|
||||
<div className="detail-section">
|
||||
<h3 className="section-title">
|
||||
<Box size={18} />
|
||||
Stock Disponible
|
||||
</h3>
|
||||
<div className={`stock-badge ${product.stock > 0 ? 'in-stock' : 'out-of-stock'}`}>
|
||||
{product.stock}g
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Catégorie */}
|
||||
<div className="detail-section">
|
||||
<h3 className="section-title">
|
||||
<Package size={18} />
|
||||
Catégorie
|
||||
</h3>
|
||||
<div className="category-badge">
|
||||
{getCategoryLabel(product.category)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prix */}
|
||||
<div className="detail-section full-width">
|
||||
<h3 className="section-title">
|
||||
<DollarSign size={18} />
|
||||
Tarifs
|
||||
</h3>
|
||||
<div className="prices-table">
|
||||
{product.prices && product.prices.length > 0 ? (
|
||||
product.prices.map((price, index) => (
|
||||
<div key={index} className="price-row-display">
|
||||
<div className="price-quantity">
|
||||
<span className="quantity-value">{price.quantity}g</span>
|
||||
</div>
|
||||
<div className="price-arrow">→</div>
|
||||
<div className="price-amount">
|
||||
<span className="amount-value">{price.price.toFixed(2)}€</span>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="no-data">Aucun tarif défini</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistiques médias */}
|
||||
{hasMedia && product.media && (
|
||||
<div className="detail-section full-width">
|
||||
<h3 className="section-title">
|
||||
<ImageIcon size={18} />
|
||||
Médias
|
||||
</h3>
|
||||
<div className="media-stats">
|
||||
<div className="media-stat">
|
||||
<ImageIcon size={20} />
|
||||
<span>{product.media.filter(m => m.type === 'image').length} Image{product.media.filter(m => m.type === 'image').length > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
<div className="media-stat">
|
||||
<VideoIcon size={20} />
|
||||
<span>{product.media.filter(m => m.type === 'video').length} Vidéo{product.media.filter(m => m.type === 'video').length > 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dates */}
|
||||
{product.created_at && (
|
||||
<div className="detail-section">
|
||||
<h3 className="section-title">
|
||||
<Calendar size={18} />
|
||||
Créé le
|
||||
</h3>
|
||||
<p className="date-text">{formatDate(product.created_at)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{product.updated_at && (
|
||||
<div className="detail-section">
|
||||
<h3 className="section-title">
|
||||
<Calendar size={18} />
|
||||
Modifié le
|
||||
</h3>
|
||||
<p className="date-text">{formatDate(product.updated_at)}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button primary full-width"
|
||||
onClick={onClose}
|
||||
>
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductDetailsModal;
|
||||
@@ -0,0 +1,462 @@
|
||||
// ============================================
|
||||
// components/admin/ProductEditModal.tsx
|
||||
// ============================================
|
||||
// ✅ Gestion correcte de la mise à jour des produits
|
||||
// ✅ Envoi en JSON (pas FormData)
|
||||
// ✅ Upload séparé des médias
|
||||
// ✅ Boutons au lieu de select (comme AdminUsers)
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { X, Plus, Trash2, Upload, DollarSign, Image as ImageIcon } from 'lucide-react';
|
||||
import { updateProductAdmin, deleteProductMediaAdmin, uploadProductMediaAdmin } from '../api/api_admin';
|
||||
import type { Product, ProductPrice } from '../api/api_admin_types';
|
||||
import './ProductModal.css';
|
||||
|
||||
interface ProductEditModalProps {
|
||||
product: Product;
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
const ProductEditModal: React.FC<ProductEditModalProps> = ({ product, onClose, onSuccess }) => {
|
||||
// ============================================
|
||||
// 📝 STATE
|
||||
// ============================================
|
||||
const [name, setName] = useState(product.name);
|
||||
const [category, setCategory] = useState(product.category);
|
||||
const [description, setDescription] = useState(product.description);
|
||||
const [stock, setStock] = useState(product.stock);
|
||||
const [prices, setPrices] = useState<ProductPrice[]>(
|
||||
product.prices && product.prices.length > 0
|
||||
? product.prices
|
||||
: [{ quantity: 1, price: 0 }]
|
||||
);
|
||||
const [existingMedia, setExistingMedia] = useState(product.media || []);
|
||||
const [newMediaFiles, setNewMediaFiles] = useState<File[]>([]);
|
||||
const [mediaToDelete, setMediaToDelete] = useState<number[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// ✅ Options de catégories
|
||||
const categoryOptions = [
|
||||
{ value: 'weed&hash', label: 'Weed & Hash' },
|
||||
{ value: 'zipette&co', label: 'Zipette & Co' },
|
||||
{ value: 'gros&semi', label: 'Gros & Semi' }
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// 💰 GESTION DES PRIX
|
||||
// ============================================
|
||||
const addPriceRow = () => {
|
||||
setPrices([...prices, { quantity: 1, price: 0 }]);
|
||||
};
|
||||
|
||||
const removePriceRow = (index: number) => {
|
||||
if (prices.length > 1) {
|
||||
setPrices(prices.filter((_, i) => i !== index));
|
||||
}
|
||||
};
|
||||
|
||||
const updatePrice = (index: number, field: 'quantity' | 'price', value: number) => {
|
||||
const newPrices = [...prices];
|
||||
newPrices[index][field] = value;
|
||||
setPrices(newPrices);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 📁 GESTION DES MÉDIAS
|
||||
// ============================================
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
const newFiles = Array.from(e.target.files);
|
||||
setNewMediaFiles([...newMediaFiles, ...newFiles]);
|
||||
console.log(`📁 ${newFiles.length} nouveau(x) fichier(s) ajouté(s)`);
|
||||
}
|
||||
};
|
||||
|
||||
const removeNewFile = (index: number) => {
|
||||
setNewMediaFiles(newMediaFiles.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const markMediaForDeletion = (mediaId: number) => {
|
||||
setMediaToDelete([...mediaToDelete, mediaId]);
|
||||
setExistingMedia(existingMedia.filter(m => m.id !== mediaId));
|
||||
console.log(`🗑️ Média ${mediaId} marqué pour suppression`);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// ✅ VALIDATION
|
||||
// ============================================
|
||||
const validateForm = (): string | null => {
|
||||
if (!name.trim()) return 'Le nom est requis';
|
||||
if (!description.trim()) return 'La description est requise';
|
||||
if (stock < 0) return 'Le stock ne peut pas être négatif';
|
||||
if (prices.length === 0) return 'Au moins un prix est requis';
|
||||
|
||||
for (const price of prices) {
|
||||
if (price.quantity <= 0) return 'Toutes les quantités doivent être positives';
|
||||
if (price.price <= 0) return 'Tous les prix doivent être positifs';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 📤 SOUMISSION - VERSION CORRIGÉE
|
||||
// ============================================
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const validationError = validateForm();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!product.id) {
|
||||
setError('ID du produit manquant');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
console.log('📤 [EDIT_MODAL] Début de la mise à jour du produit:', product.id);
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 1: Supprimer les médias marqués
|
||||
// ============================================
|
||||
if (mediaToDelete.length > 0) {
|
||||
console.log(`🗑️ Suppression de ${mediaToDelete.length} média(s)...`);
|
||||
|
||||
for (const mediaId of mediaToDelete) {
|
||||
try {
|
||||
await deleteProductMediaAdmin(product.id, mediaId);
|
||||
console.log(`✅ Média ${mediaId} supprimé`);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Erreur suppression média ${mediaId}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 2: Préparer les données de mise à jour (JSON)
|
||||
// ============================================
|
||||
const updateData: Partial<Product> = {
|
||||
name: name.trim(),
|
||||
category,
|
||||
description: description.trim(),
|
||||
stock,
|
||||
prices: prices.map(p => ({
|
||||
quantity: p.quantity,
|
||||
price: p.price
|
||||
}))
|
||||
};
|
||||
|
||||
console.log('📝 [EDIT_MODAL] Données de mise à jour:', updateData);
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 3: Mettre à jour le produit
|
||||
// ============================================
|
||||
const response = await updateProductAdmin(product.id, updateData);
|
||||
|
||||
if (!response.success) {
|
||||
console.error('❌ [EDIT_MODAL] Erreur API:', response.error);
|
||||
setError(response.error || 'Erreur lors de la mise à jour du produit');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ [EDIT_MODAL] Produit mis à jour avec succès');
|
||||
|
||||
// ============================================
|
||||
// ÉTAPE 4: Upload des nouveaux médias
|
||||
// ============================================
|
||||
if (newMediaFiles.length > 0) {
|
||||
console.log(`📤 [EDIT_MODAL] Upload de ${newMediaFiles.length} nouveau(x) média(s)...`);
|
||||
|
||||
for (const file of newMediaFiles) {
|
||||
try {
|
||||
const fileType = file.type.startsWith('image/') ? 'image' : 'video';
|
||||
const uploadResponse = await uploadProductMediaAdmin(product.id, file, fileType);
|
||||
|
||||
if (uploadResponse.success) {
|
||||
console.log(`✅ Média uploadé: ${file.name}`);
|
||||
} else {
|
||||
console.warn(`⚠️ Erreur upload ${file.name}:`, uploadResponse.error);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Erreur upload ${file.name}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎉 [EDIT_MODAL] Toutes les modifications appliquées');
|
||||
|
||||
// Fermer et rafraîchir
|
||||
onSuccess();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ [EDIT_MODAL] Erreur:', error);
|
||||
setError(error instanceof Error ? error.message : 'Erreur inconnue');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// 🎨 RENDER
|
||||
// ============================================
|
||||
return (
|
||||
<>
|
||||
<div className="modal-overlay" onClick={onClose} />
|
||||
<div className="product-modal edit-modal">
|
||||
{/* Header */}
|
||||
<div className="modal-header">
|
||||
<h2>Modifier le Produit</h2>
|
||||
<button className="close-modal" onClick={onClose} disabled={loading}>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<form onSubmit={handleSubmit} className="modal-content">
|
||||
{error && (
|
||||
<div className="error-message">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Informations de base */}
|
||||
<div className="form-section">
|
||||
<h3>Informations de Base</h3>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Nom du Produit *</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Ex: OG Kush Premium"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ✅ CATÉGORIE AVEC BOUTONS AU LIEU DE SELECT */}
|
||||
<div className="form-group">
|
||||
<label>Catégorie *</label>
|
||||
<div className="filter-buttons">
|
||||
{categoryOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={category === option.value ? 'active' : ''}
|
||||
onClick={() => setCategory(option.value)}
|
||||
disabled={loading}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="description">Description *</label>
|
||||
<textarea
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Décrivez le produit en détail..."
|
||||
rows={4}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="stock">Stock (en grammes) *</label>
|
||||
<input
|
||||
id="stock"
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={stock}
|
||||
onChange={(e) => setStock(parseFloat(e.target.value) || 0)}
|
||||
placeholder="100"
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prix */}
|
||||
<div className="form-section">
|
||||
<div className="section-header">
|
||||
<h3>Tarifs</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="add-price-btn"
|
||||
onClick={addPriceRow}
|
||||
disabled={loading}
|
||||
>
|
||||
<Plus size={18} />
|
||||
Ajouter un Prix
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="prices-grid">
|
||||
{prices.map((price, index) => (
|
||||
<div key={index} className="price-row">
|
||||
<div className="price-input-group">
|
||||
<label>Quantité (g)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value={price.quantity}
|
||||
onChange={(e) => updatePrice(index, 'quantity', parseInt(e.target.value) || 1)}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="price-input-group">
|
||||
<label>Prix (€)</label>
|
||||
<div className="price-input-wrapper">
|
||||
<DollarSign size={18} />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={price.price}
|
||||
onChange={(e) => updatePrice(index, 'price', parseFloat(e.target.value) || 0)}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{prices.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="remove-price-btn"
|
||||
onClick={() => removePriceRow(index)}
|
||||
disabled={loading}
|
||||
title="Supprimer ce prix"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Médias existants */}
|
||||
{existingMedia.length > 0 && (
|
||||
<div className="form-section">
|
||||
<h3>Médias Actuels</h3>
|
||||
<div className="existing-media-grid">
|
||||
{existingMedia.map((media) => (
|
||||
<div key={media.id} className="existing-media-item">
|
||||
{media.type === 'image' ? (
|
||||
<img
|
||||
src={`http://localhost:8080${media.url}`}
|
||||
alt="Media"
|
||||
className="media-thumbnail"
|
||||
/>
|
||||
) : (
|
||||
<div className="video-thumbnail">
|
||||
<ImageIcon size={32} />
|
||||
<span>Vidéo</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="delete-media-btn"
|
||||
onClick={() => markMediaForDeletion(media.id!)}
|
||||
disabled={loading}
|
||||
title="Supprimer ce média"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Nouveaux médias */}
|
||||
<div className="form-section">
|
||||
<h3>Ajouter de Nouveaux Médias</h3>
|
||||
|
||||
<div className="upload-zone">
|
||||
<label htmlFor="media-upload" className="upload-label">
|
||||
<Upload size={32} />
|
||||
<p>Cliquez pour ajouter des fichiers</p>
|
||||
<span>Images (JPG, PNG, WebP) ou Vidéos (MP4, WebM)</span>
|
||||
</label>
|
||||
<input
|
||||
id="media-upload"
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
multiple
|
||||
onChange={handleFileSelect}
|
||||
disabled={loading}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{newMediaFiles.length > 0 && (
|
||||
<div className="files-list">
|
||||
{newMediaFiles.map((file, index) => (
|
||||
<div key={index} className="file-item">
|
||||
<div className="file-info">
|
||||
<span className="file-name">{file.name}</span>
|
||||
<span className="file-size">
|
||||
{(file.size / 1024).toFixed(1)} KB
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-file-btn"
|
||||
onClick={() => removeNewFile(index)}
|
||||
disabled={loading}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="action-button secondary"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="action-button primary"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Mise à jour...' : 'Mettre à Jour'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductEditModal;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,477 @@
|
||||
/* Sidebar Toggle Button */
|
||||
.sidebar-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 1001;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.2),
|
||||
rgba(109, 40, 217, 0.1)
|
||||
);
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #7c3aed;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.sidebar-toggle.sidebar-open {
|
||||
left: calc(280px + 1rem);
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.3),
|
||||
rgba(109, 40, 217, 0.15)
|
||||
);
|
||||
border-color: rgba(124, 58, 237, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.sidebar-toggle:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 999;
|
||||
backdrop-filter: blur(4px);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar Container */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 280px;
|
||||
background: linear-gradient(180deg, #0f0f0f 0%, #1a1a1a 100%);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.3);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: rgba(124, 58, 237, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
|
||||
/* Sidebar Header */
|
||||
.sidebar-header {
|
||||
padding: 2rem 1.5rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-text h2 {
|
||||
color: white;
|
||||
font-size: 1.3rem;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.logo-text p {
|
||||
color: #888;
|
||||
font-size: 0.75rem;
|
||||
margin: 0.2rem 0 0 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1.5rem 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
color: #888;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(124, 58, 237, 0.1),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.menu-item:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-badge {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 20px;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(-8px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.1),
|
||||
rgba(109, 40, 217, 0.05)
|
||||
);
|
||||
border-color: rgba(124, 58, 237, 0.2);
|
||||
color: white;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-icon {
|
||||
transform: scale(1.1);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(124, 58, 237, 0.15),
|
||||
rgba(109, 40, 217, 0.08)
|
||||
);
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.menu-item.active .menu-icon {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item.active .menu-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
transform: scale(0.98) translateX(4px);
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.sidebar-footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.03),
|
||||
rgba(255, 255, 255, 0.01)
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
margin: 0.2rem 0 0 0;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
padding: 0.9rem 1rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(239, 68, 68, 0.1),
|
||||
rgba(220, 38, 38, 0.05)
|
||||
);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #ef4444;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(239, 68, 68, 0.15),
|
||||
rgba(220, 38, 38, 0.08)
|
||||
);
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.logout-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Bouton Telegram */
|
||||
.telegram-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
padding: 0.9rem 1rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(37, 161, 244, 0.1),
|
||||
rgba(32, 139, 220, 0.05)
|
||||
);
|
||||
border: 1px solid rgba(37, 161, 244, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #25a1f4;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.telegram-button:hover:not(:disabled) {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(37, 161, 244, 0.15),
|
||||
rgba(32, 139, 220, 0.08)
|
||||
);
|
||||
border-color: rgba(37, 161, 244, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(37, 161, 244, 0.3);
|
||||
}
|
||||
|
||||
.telegram-button:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.telegram-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.sidebar-toggle.sidebar-open {
|
||||
left: calc(280px + 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.sidebar-toggle.sidebar-open {
|
||||
left: calc(260px + 1rem);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.5rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.logo-text h2 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.menu-list li {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 0.8rem 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.menu-item:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-icon {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import "./Sidebar.css";
|
||||
|
||||
// Importation de Font Awesome
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faChartLine,
|
||||
faShoppingCart,
|
||||
faBox,
|
||||
faUsers,
|
||||
faSignOutAlt,
|
||||
faBars,
|
||||
faTimes,
|
||||
faChevronRight,
|
||||
faBoxOpen,
|
||||
faMotorcycle,
|
||||
faExclamationCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faTelegram } from "@fortawesome/free-brands-svg-icons";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: IconDefinition;
|
||||
path: string;
|
||||
badge?: number;
|
||||
}
|
||||
|
||||
function Sidebar() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
id: "dashboard",
|
||||
label: "Tableau de Bord",
|
||||
icon: faChartLine,
|
||||
path: "/admin/dashboard",
|
||||
},
|
||||
{
|
||||
id: "orders",
|
||||
label: "Commandes",
|
||||
icon: faShoppingCart,
|
||||
path: "/admin/dashboard/orders",
|
||||
},
|
||||
{
|
||||
id: "products",
|
||||
label: "Produits",
|
||||
icon: faBox,
|
||||
path: "/admin/dashboard/products",
|
||||
},
|
||||
{
|
||||
id: "delivery-persons",
|
||||
label: "Livreurs",
|
||||
icon: faMotorcycle,
|
||||
path: "/admin/dashboard/delivery",
|
||||
},
|
||||
{
|
||||
id: "users",
|
||||
label: "Utilisateurs",
|
||||
icon: faUsers,
|
||||
path: "/admin/dashboard/users",
|
||||
},
|
||||
{
|
||||
id: "alerts",
|
||||
label: "Alertes",
|
||||
icon: faExclamationCircle,
|
||||
path: "/admin/dashboard/alerts",
|
||||
},
|
||||
];
|
||||
|
||||
const handleNavigation = (path: string) => {
|
||||
navigate(path);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (isLoggingOut) return; // Éviter les double-clics
|
||||
|
||||
setIsLoggingOut(true);
|
||||
console.log("🚪 [LOGOUT] Déconnexion en cours...");
|
||||
|
||||
try {
|
||||
const token = sessionStorage.getItem("admin_token");
|
||||
|
||||
if (token) {
|
||||
// ✅ Appel API de logout (via admin API car cabine utilise le même token)
|
||||
try {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/v2/admin/auth/logout",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
console.log("✅ [LOGOUT] Déconnexion backend réussie");
|
||||
} else {
|
||||
console.warn("⚠️ [LOGOUT] Erreur backend (ignorée)");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"⚠️ [LOGOUT] Erreur réseau backend (ignorée):",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CRITIQUE: Nettoyer sessionStorage
|
||||
sessionStorage.removeItem("admin_token");
|
||||
sessionStorage.removeItem("admin_username");
|
||||
console.log("✅ [LOGOUT] SessionStorage nettoyé");
|
||||
|
||||
navigate("/login-admin/admin", { replace: true });
|
||||
} catch (error) {
|
||||
console.error("❌ [LOGOUT] Erreur:", error);
|
||||
|
||||
// ✅ Même en cas d'erreur, nettoyer et rediriger
|
||||
sessionStorage.removeItem("admin_token");
|
||||
sessionStorage.removeItem("admin_username");
|
||||
navigate("/login-admin/admin", { replace: true });
|
||||
} finally {
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTelegram = () => {
|
||||
window.open("https://t.me/milieu_nantais", "_blank");
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Menu Toggle Button */}
|
||||
<button
|
||||
className={`sidebar-toggle ${isOpen ? "sidebar-open" : ""}`}
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<FontAwesomeIcon icon={isOpen ? faTimes : faBars} />
|
||||
</button>
|
||||
|
||||
{/* Overlay */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="sidebar-overlay"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className={`sidebar ${isOpen ? "open" : ""}`}>
|
||||
{/* Logo / Brand */}
|
||||
<div className="sidebar-header">
|
||||
<div className="sidebar-logo">
|
||||
<div className="logo-icon">
|
||||
<FontAwesomeIcon icon={faBoxOpen} size="lg" />
|
||||
</div>
|
||||
<div className="logo-text">
|
||||
<h2>Milieu-Nantais</h2>
|
||||
<p>Admin Panel</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Menu */}
|
||||
<nav className="sidebar-nav">
|
||||
<ul className="menu-list">
|
||||
{menuItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
className={`menu-item ${location.pathname === item.path ? "active" : ""}`}
|
||||
onClick={() => handleNavigation(item.path)}
|
||||
>
|
||||
<span className="menu-icon">
|
||||
<FontAwesomeIcon icon={item.icon} />
|
||||
</span>
|
||||
<span className="menu-label">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.badge && (
|
||||
<span className="menu-badge">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className="menu-arrow"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* User Profile / Logout */}
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
className="telegram-button"
|
||||
onClick={handleTelegram}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTelegram} />
|
||||
<span>Telegram</span>
|
||||
</button>
|
||||
<button
|
||||
className="logout-button"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||
<span>
|
||||
{isLoggingOut ? "Déconnexion..." : "Déconnexion"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,401 @@
|
||||
/* Sidebar Toggle Button */
|
||||
.sidebar-toggle {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
z-index: 1001;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.2), rgba(109, 40, 217, 0.1));
|
||||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #7c3aed;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.sidebar-toggle.sidebar-open {
|
||||
left: calc(280px + 1rem);
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.3), rgba(109, 40, 217, 0.15));
|
||||
border-color: rgba(124, 58, 237, 0.5);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.sidebar-toggle:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Overlay */
|
||||
.sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 999;
|
||||
backdrop-filter: blur(4px);
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar Container */
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 280px;
|
||||
background: linear-gradient(180deg, #0f0f0f 0%, #1a1a1a 100%);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.3);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb {
|
||||
background: rgba(124, 58, 237, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sidebar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(124, 58, 237, 0.5);
|
||||
}
|
||||
|
||||
/* Sidebar Header */
|
||||
.sidebar-header {
|
||||
padding: 2rem 1.5rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: linear-gradient(135deg, #7c3aed, #6d28d9);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
box-shadow: 0 8px 24px rgba(124, 58, 237, 0.4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-text h2 {
|
||||
color: white;
|
||||
font-size: 1.3rem;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.logo-text p {
|
||||
color: #888;
|
||||
font-size: 0.75rem;
|
||||
margin: 0.2rem 0 0 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 1.5rem 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-list li {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
color: #888;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -100%;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(124, 58, 237, 0.1), transparent);
|
||||
transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.menu-item:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-badge {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 20px;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transform: translateX(-8px);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1), rgba(109, 40, 217, 0.05));
|
||||
border-color: rgba(124, 58, 237, 0.2);
|
||||
color: white;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-icon {
|
||||
transform: scale(1.1);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item.active {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.15), rgba(109, 40, 217, 0.08));
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(124, 58, 237, 0.2);
|
||||
}
|
||||
|
||||
.menu-item.active .menu-icon {
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item.active .menu-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
transform: scale(0.98) translateX(4px);
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.sidebar-footer {
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.user-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01));
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: white;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
color: #888;
|
||||
font-size: 0.8rem;
|
||||
margin: 0.2rem 0 0 0;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
padding: 0.9rem 1rem;
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(220, 38, 38, 0.05));
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
border-radius: 12px;
|
||||
color: #ef4444;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(220, 38, 38, 0.08));
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.logout-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.sidebar-toggle.sidebar-open {
|
||||
left: calc(280px + 1rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.sidebar-toggle.sidebar-open {
|
||||
left: calc(260px + 1rem);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.5rem 1rem 1rem;
|
||||
}
|
||||
|
||||
.logo-text h2 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.menu-list li {
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
padding: 0.8rem 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.menu-item:hover {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.menu-item:hover .menu-icon {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.logout-button:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import "./Sidebar.css";
|
||||
// Importation de Font Awesome
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faChartLine,
|
||||
faShoppingCart,
|
||||
faUsers,
|
||||
faSignOutAlt,
|
||||
faBars,
|
||||
faTimes,
|
||||
faChevronRight,
|
||||
faBoxOpen,
|
||||
faMotorcycle,
|
||||
faExclamationCircle,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faTelegram } from "@fortawesome/free-brands-svg-icons";
|
||||
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: IconDefinition;
|
||||
path: string;
|
||||
badge?: number;
|
||||
}
|
||||
|
||||
function SidebarCabine() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
id: "dashboard",
|
||||
label: "Tableau de Bord",
|
||||
icon: faChartLine,
|
||||
path: "/cabine/dashboard",
|
||||
},
|
||||
{
|
||||
id: "orders",
|
||||
label: "Commandes",
|
||||
icon: faShoppingCart,
|
||||
path: "/cabine/dashboard/orders",
|
||||
},
|
||||
{
|
||||
id: "delivery-persons",
|
||||
label: "Livreurs",
|
||||
icon: faMotorcycle,
|
||||
path: "/cabine/dashboard/delivery",
|
||||
},
|
||||
{
|
||||
id: "users",
|
||||
label: "Utilisateurs",
|
||||
icon: faUsers,
|
||||
path: "/cabine/dashboard/users",
|
||||
},
|
||||
{
|
||||
id: "alerts",
|
||||
label: "Alertes Livreur",
|
||||
icon: faExclamationCircle,
|
||||
path: "/cabine/dashboard/alerts",
|
||||
},
|
||||
];
|
||||
|
||||
const handleNavigation = (path: string) => {
|
||||
navigate(path);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* ✅ Fonction de déconnexion complète
|
||||
*/
|
||||
const handleLogout = async () => {
|
||||
if (isLoggingOut) return; // Éviter les double-clics
|
||||
|
||||
setIsLoggingOut(true);
|
||||
console.log("🚪 [LOGOUT] Déconnexion en cours...");
|
||||
|
||||
try {
|
||||
const token = sessionStorage.getItem("admin_token");
|
||||
|
||||
if (token) {
|
||||
// ✅ Appel API de logout (via admin API car cabine utilise le même token)
|
||||
try {
|
||||
const response = await fetch(
|
||||
"http://localhost:8080/api/v2/admin/auth/logout",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
console.log("✅ [LOGOUT] Déconnexion backend réussie");
|
||||
} else {
|
||||
console.warn("⚠️ [LOGOUT] Erreur backend (ignorée)");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"⚠️ [LOGOUT] Erreur réseau backend (ignorée):",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ CRITIQUE: Nettoyer sessionStorage
|
||||
sessionStorage.removeItem("admin_token");
|
||||
sessionStorage.removeItem("admin_username");
|
||||
console.log("✅ [LOGOUT] SessionStorage nettoyé");
|
||||
|
||||
navigate("/login-cabine/cabine", { replace: true });
|
||||
} catch (error) {
|
||||
console.error("❌ [LOGOUT] Erreur:", error);
|
||||
|
||||
// ✅ Même en cas d'erreur, nettoyer et rediriger
|
||||
sessionStorage.removeItem("admin_token");
|
||||
sessionStorage.removeItem("admin_username");
|
||||
navigate("/login", { replace: true });
|
||||
} finally {
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTelegram = () => {
|
||||
window.open("https://t.me/milieu_nantais", "_blank");
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Menu Toggle Button */}
|
||||
<button
|
||||
className={`sidebar-toggle ${isOpen ? "sidebar-open" : ""}`}
|
||||
onClick={toggleSidebar}
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<FontAwesomeIcon icon={isOpen ? faTimes : faBars} />
|
||||
</button>
|
||||
|
||||
{/* Overlay */}
|
||||
{isOpen && (
|
||||
<div
|
||||
className="sidebar-overlay"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside className={`sidebar ${isOpen ? "open" : ""}`}>
|
||||
{/* Logo / Brand */}
|
||||
<div className="sidebar-header">
|
||||
<div className="sidebar-logo">
|
||||
<div className="logo-icon">
|
||||
<FontAwesomeIcon icon={faBoxOpen} size="lg" />
|
||||
</div>
|
||||
<div className="logo-text">
|
||||
<h2>Milieu-Nantais</h2>
|
||||
<p>Cabine Panel</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Menu */}
|
||||
<nav className="sidebar-nav">
|
||||
<ul className="menu-list">
|
||||
{menuItems.map((item) => (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
className={`menu-item ${location.pathname === item.path ? "active" : ""}`}
|
||||
onClick={() => handleNavigation(item.path)}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<span className="menu-icon">
|
||||
<FontAwesomeIcon icon={item.icon} />
|
||||
</span>
|
||||
<span className="menu-label">
|
||||
{item.label}
|
||||
</span>
|
||||
{item.badge && (
|
||||
<span className="menu-badge">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
icon={faChevronRight}
|
||||
className="menu-arrow"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* User Profile / Logout */}
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
className="telegram-button"
|
||||
onClick={handleTelegram}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTelegram} />
|
||||
<span>Telegram</span>
|
||||
</button>
|
||||
<button
|
||||
className="logout-button"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoggingOut}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSignOutAlt} />
|
||||
<span>
|
||||
{isLoggingOut ? "Déconnexion..." : "Déconnexion"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarCabine;
|
||||
@@ -0,0 +1,170 @@
|
||||
/* ===== TOAST PROFESSIONAL STYLES ===== */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 28px;
|
||||
right: 28px;
|
||||
min-width: 360px;
|
||||
max-width: 450px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow:
|
||||
0 10px 40px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||
animation: toastSlideIn 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
@keyframes toastSlideIn {
|
||||
from {
|
||||
transform: translateX(480px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* SUCCESS - VIOLET */
|
||||
.toast-success {
|
||||
background: linear-gradient(135deg, rgba(124, 58, 237, 0.1) 0%, rgba(124, 58, 237, 0.05) 100%);
|
||||
border-color: rgba(124, 58, 237, 0.3);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.toast-success .toast-icon {
|
||||
color: #7c3aed;
|
||||
background: rgba(124, 58, 237, 0.15);
|
||||
border: 1px solid rgba(124, 58, 237, 0.4);
|
||||
}
|
||||
|
||||
.toast-success .toast-progress {
|
||||
background: linear-gradient(90deg, #7c3aed, #a78bfa, transparent);
|
||||
}
|
||||
|
||||
/* ERROR */
|
||||
.toast-error {
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.08) 0%, rgba(220, 38, 38, 0.04) 100%);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.toast-error .toast-icon {
|
||||
color: #dc2626;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||
}
|
||||
|
||||
.toast-error .toast-progress {
|
||||
background: linear-gradient(90deg, #dc2626, #b91c1c, transparent);
|
||||
}
|
||||
|
||||
/* WARNING */
|
||||
.toast-warning {
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(217, 119, 6, 0.04) 100%);
|
||||
border-color: rgba(245, 158, 11, 0.2);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.toast-warning .toast-icon {
|
||||
color: #d97706;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
|
||||
.toast-warning .toast-progress {
|
||||
background: linear-gradient(90deg, #d97706, #b45309, transparent);
|
||||
}
|
||||
|
||||
/* INFO */
|
||||
.toast-info {
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08) 0%, rgba(37, 99, 235, 0.04) 100%);
|
||||
border-color: rgba(59, 130, 246, 0.2);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.toast-info .toast-icon {
|
||||
color: #2563eb;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.toast-info .toast-progress {
|
||||
background: linear-gradient(90deg, #2563eb, #1d4ed8, transparent);
|
||||
}
|
||||
|
||||
.toast-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
min-width: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.toast:hover .toast-icon {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
letter-spacing: 0.1px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.toast-progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 2px;
|
||||
animation: toastProgressBar linear forwards;
|
||||
}
|
||||
|
||||
@keyframes toastProgressBar {
|
||||
from {
|
||||
width: 100%;
|
||||
}
|
||||
to {
|
||||
width: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 640px) {
|
||||
.toast {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
left: 20px;
|
||||
min-width: auto;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
.toast {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.toast:hover .toast-icon {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import "./Toast.css";
|
||||
|
||||
interface ToastProps {
|
||||
message: string;
|
||||
type?: "success" | "error" | "warning" | "info";
|
||||
duration?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function Toast({
|
||||
message,
|
||||
type = "success",
|
||||
duration = 3000,
|
||||
onClose,
|
||||
}: ToastProps) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
onClose?.();
|
||||
}, duration);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [duration, onClose]);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
const getIcon = () => {
|
||||
const icons = {
|
||||
success: "✓",
|
||||
error: "✕",
|
||||
warning: "⚠",
|
||||
info: "ℹ",
|
||||
};
|
||||
return icons[type];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`toast toast-${type}`}>
|
||||
<div className="toast-content">
|
||||
<div className="toast-icon">{getIcon()}</div>
|
||||
<div className="toast-message">{message}</div>
|
||||
</div>
|
||||
<div
|
||||
className="toast-progress"
|
||||
style={{ animationDuration: `${duration}ms` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Toast;
|
||||
Reference in New Issue
Block a user