chore: update

This commit is contained in:
2026-03-03 23:42:23 +01:00
parent 52b059fafa
commit 0073f80a48
95 changed files with 2720 additions and 40619 deletions
+174 -135
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useEffect, useRef, useCallback } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useCart } from "../context/CartContext";
import "./Navbar.css";
@@ -12,10 +12,12 @@ import {
faSignOutAlt,
faBars,
faTimes,
faChevronRight,
faBell,
} from "@fortawesome/free-solid-svg-icons";
import { faTelegram } from "@fortawesome/free-brands-svg-icons";
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { getClientNotifications, markNotificationsRead } from "../api/api";
import type { ClientNotification } from "../api/api";
interface MenuItem {
id: string;
@@ -27,98 +29,98 @@ interface MenuItem {
function Navbar() {
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const [isLoggingOut, setIsLoggingOut] = useState<boolean>(false);
const [notifications, setNotifications] = useState<ClientNotification[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [showNotifPanel, setShowNotifPanel] = useState(false);
const seenKeysRef = useRef<Set<string>>(new Set());
const isFirstLoadRef = useRef(true);
const notifPanelRef = useRef<HTMLDivElement>(null);
const { cartCount } = useCart();
const navigate = useNavigate();
const location = useLocation();
const fetchNotifications = useCallback(async () => {
const res = await getClientNotifications();
if (!res.success) return;
setNotifications(res.notifications);
setUnreadCount(res.unread_count);
if (!isFirstLoadRef.current) {
for (const n of res.notifications) {
if (n.read) continue;
const key = `${n.command_id}-${n.type}-${n.created_at}`;
if (!seenKeysRef.current.has(key)) {
seenKeysRef.current.add(key);
}
}
} else {
for (const n of res.notifications) {
seenKeysRef.current.add(`${n.command_id}-${n.type}-${n.created_at}`);
}
isFirstLoadRef.current = false;
}
}, []);
useEffect(() => {
fetchNotifications();
const interval = setInterval(fetchNotifications, 15000);
return () => clearInterval(interval);
}, [fetchNotifications]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (notifPanelRef.current && !notifPanelRef.current.contains(e.target as Node)) {
setShowNotifPanel(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleNotifBellClick = async () => {
setShowNotifPanel((prev) => !prev);
if (!showNotifPanel && unreadCount > 0) {
await markNotificationsRead();
setUnreadCount(0);
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
}
};
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",
},
{ id: "accueil", label: "Accueil", icon: faHome, path: "/user/accueil" },
{ id: "produits", label: "Nos Produits", icon: faBox, path: "/user/nos-produits" },
{ id: "panier", label: "Mon Panier", 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 toggleMenu = () => setIsMenuOpen((v) => !v);
const closeMenu = () => setIsMenuOpen(false);
const closeMenu = (): void => {
setIsMenuOpen(false);
};
const handleNavigation = (path: string): void => {
const handleNavigation = (path: string) => {
navigate(path);
closeMenu();
};
const handleLogout = async (): Promise<void> => {
const handleLogout = async () => {
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}`,
},
await fetch("/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,
);
}
});
} catch (_) {}
}
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);
} catch (_) {
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login/client", { replace: true });
@@ -127,86 +129,125 @@ function Navbar() {
}
};
const handleTelegram = (): void => {
window.open("https://t.me/milieu_nantais", "_blank");
};
const handleTelegram = () => 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>
{/* ── Top Bar ─────────────────────────────────── */}
<header className="topbar">
<button
className={`topbar-toggle ${isMenuOpen ? "is-open" : ""}`}
onClick={toggleMenu}
aria-label="Menu"
>
<FontAwesomeIcon icon={isMenuOpen ? faTimes : faBars} />
</button>
{/* Overlay */}
{isMenuOpen && (
<div className="sidebar-overlay" onClick={closeMenu} />
)}
<span className="topbar-brand">MilieuNantais</span>
{/* 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>
<div className="topbar-actions">
{/* Notifications */}
<div className="notif-wrapper" ref={notifPanelRef}>
<button
className="topbar-icon-btn"
onClick={handleNotifBellClick}
aria-label="Notifications"
>
<FontAwesomeIcon icon={faBell} />
{unreadCount > 0 && (
<span className="topbar-badge">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)}
</button>
{/* Sidebar */}
{showNotifPanel && (
<div className="notif-panel">
<div className="notif-panel-header">Notifications</div>
{notifications.length === 0 ? (
<div className="notif-empty">Aucune notification</div>
) : (
<ul className="notif-list">
{notifications.map((n, i) => (
<li
key={i}
className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`}
>
<span className="notif-message">{n.message}</span>
<span className="notif-time">
{new Date(n.created_at).toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
})}
</span>
</li>
))}
</ul>
)}
</div>
)}
</div>
{/* Panier */}
<button
className="topbar-icon-btn"
onClick={() => navigate("/user/panier")}
aria-label="Panier"
>
<FontAwesomeIcon icon={faShoppingCart} />
{cartCount > 0 && (
<span className="topbar-badge">{cartCount}</span>
)}
</button>
</div>
</header>
{/* ── Overlay ─────────────────────────────────── */}
{isMenuOpen && <div className="sidebar-overlay" onClick={closeMenu} />}
{/* ── 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 className="sidebar-brand">
<div className="sidebar-brand-icon">
<FontAwesomeIcon icon={faShoppingCart} />
</div>
<div className="logo-text">
<h2>Milieu-Nantais</h2>
<p>User Panel</p>
<div>
<p className="sidebar-brand-name">Milieu-Nantais</p>
<p className="sidebar-brand-sub">Mon espace</p>
</div>
</div>
<button className="sidebar-close" onClick={closeMenu} aria-label="Fermer">
<FontAwesomeIcon icon={faTimes} />
</button>
</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>
))}
{menuItems.map((item) => {
const isActive = location.pathname === item.path;
return (
<li key={item.id}>
<button
className={`menu-item ${isActive ? "active" : ""}`}
onClick={() => handleNavigation(item.path)}
disabled={isLoggingOut}
>
<span className="menu-icon">
<FontAwesomeIcon icon={item.icon} />
</span>
<span className="menu-label">{item.label}</span>
{isActive && <span className="menu-dot" />}
</button>
</li>
);
})}
</ul>
</nav>
{/* Footer avec boutons Telegram et déconnexion */}
<div className="sidebar-footer">
<button
className="telegram-button"
className="sidebar-footer-btn telegram"
onClick={handleTelegram}
disabled={isLoggingOut}
>
@@ -214,14 +255,12 @@ function Navbar() {
<span>Telegram</span>
</button>
<button
className="logout-button"
className="sidebar-footer-btn logout"
onClick={handleLogout}
disabled={isLoggingOut}
>
<FontAwesomeIcon icon={faSignOutAlt} />
<span>
{isLoggingOut ? "Déconnexion..." : "Déconnexion"}
</span>
<span>{isLoggingOut ? "Déconnexion…" : "Déconnexion"}</span>
</button>
</div>
</aside>