298 lines
13 KiB
TypeScript
298 lines
13 KiB
TypeScript
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">Milieu‑Nantais</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;
|