Files
projet_gestion_commande/frontend-prep/src/components/Navbar.tsx
T
2026-05-09 15:17:19 +02:00

298 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef, useCallback } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { useCart } from "../context/useCart";
import { useTheme } from "../context/ThemeContext";
import "./Navbar.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faHome,
faBox,
faShoppingCart,
faTruck,
faClockRotateLeft,
faSignOutAlt,
faBars,
faTimes,
faBell,
faGift,
faUserCircle,
faSun,
faMoon,
} 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, getPublicSettings } from "../api/api";
import type { ClientNotification } from "../api/api";
interface MenuItem {
id: string;
label: string;
icon: IconDefinition;
path: string;
}
function Navbar() {
const { theme, toggleTheme } = useTheme();
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 [referralEnabled, setReferralEnabled] = useState(true);
const [shopName, setShopName] = useState("Milieu-Nantais");
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(() => {
getPublicSettings().then((s) => {
setReferralEnabled(s.referral_enabled);
if (s.shop_name) setShopName(s.shop_name);
});
}, []);
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: "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" },
...(referralEnabled ? [{ id: "parrainage", label: "Parrainage", icon: faGift, path: "/user/parrainage" } as MenuItem] : []),
{ id: "profil", label: "Mon Profil", icon: faUserCircle, path: "/user/profil" },
];
const toggleMenu = () => setIsMenuOpen((v) => !v);
const closeMenu = () => setIsMenuOpen(false);
const handleNavigation = (path: string) => {
navigate(path);
closeMenu();
};
const handleLogout = async () => {
if (isLoggingOut) return;
setIsLoggingOut(true);
try {
const token = sessionStorage.getItem("admin_token");
if (token) {
try {
await fetch("/api/v2/admin/auth/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
} catch { /* noop */ }
}
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login/client", { replace: true });
} catch {
sessionStorage.removeItem("admin_token");
sessionStorage.removeItem("admin_username");
navigate("/login/client", { replace: true });
} finally {
setIsLoggingOut(false);
}
};
const handleTelegram = () => window.open("https://t.me/milieu_nantais", "_blank");
return (
<>
{/* ── Top Bar ─────────────────────────────────── */}
<header className="topbar">
<button
className={`topbar-toggle ${isMenuOpen ? "is-open" : ""}`}
onClick={toggleMenu}
aria-label="Menu"
>
<FontAwesomeIcon icon={isMenuOpen ? faTimes : faBars} />
</button>
<span className="topbar-brand">MilieuNantais</span>
<div className="topbar-actions">
{/* Theme toggle */}
<button
className="topbar-theme-btn"
onClick={toggleTheme}
aria-label={theme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"}
>
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} />
</button>
{/* 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>
{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" : ""}`}>
<div className="sidebar-header">
<div className="sidebar-brand">
<div className="sidebar-brand-icon">
<FontAwesomeIcon icon={faShoppingCart} />
</div>
<div>
<p className="sidebar-brand-name">{shopName}</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>
<nav className="sidebar-nav">
<ul className="menu-list">
{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>
<div className="sidebar-footer">
<button
className="sidebar-footer-btn telegram"
onClick={handleTelegram}
disabled={isLoggingOut}
>
<FontAwesomeIcon icon={faTelegram} />
<span>Telegram</span>
</button>
<button
className="sidebar-footer-btn logout"
onClick={handleLogout}
disabled={isLoggingOut}
>
<FontAwesomeIcon icon={faSignOutAlt} />
<span>{isLoggingOut ? "Déconnexion…" : "Déconnexion"}</span>
</button>
</div>
</aside>
</>
);
}
export default Navbar;