chore: update
This commit is contained in:
@@ -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">Milieu‑Nantais</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>
|
||||
|
||||
Reference in New Issue
Block a user