chore: build
Backend - Build & Lint / build (push) Has been cancelled
Frontend Client - EAS Build / build (push) Failing after 56m32s
Frontend Web - Build & Lint / build (push) Failing after 14m33s

This commit is contained in:
2026-06-23 22:02:04 +02:00
parent bafed46be6
commit b670bc3108
6 changed files with 1848 additions and 1219 deletions
+80 -1
View File
@@ -296,6 +296,78 @@ func GetAdminStats(c *gin.Context) {
} }
} }
// ── Détail du jour (catégorie → produits) ────────────────────────────────
var dailyRows []models.DailyProductRow
gdb.Raw(`
SELECT
ci.product_id,
ci.produit AS product_name,
COALESCE(p.category, 'Sans catégorie') AS category,
COALESCE(cat.color, '#7c3aed') AS category_color,
SUM(ci.quantite) AS total_quantity,
COUNT(DISTINCT ci.command_id) AS order_count,
SUM(ci.prix) AS revenue
FROM command_items ci
JOIN commandes c ON c.id = ci.command_id
LEFT JOIN products p ON p.id = ci.product_id
LEFT JOIN categories cat ON cat.name = p.category
WHERE DATE(c.created_at) = CURRENT_DATE
AND c.status != 'cancelled'
GROUP BY ci.product_id, ci.produit, p.category, cat.color
ORDER BY p.category, SUM(ci.quantite) DESC
`).Scan(&dailyRows)
type dailyCatGroup struct {
Category string
CategoryColor string
TotalQuantity float64
TotalRevenue float64
Products []gin.H
}
var dailyCats []dailyCatGroup
dailyCatIdx := map[string]int{}
var dailyTotalOrders int64
dailyTotalRevenue := 0.0
dailyTotalQty := 0.0
gdb.Raw(`
SELECT COUNT(DISTINCT id) FROM commandes
WHERE DATE(created_at) = CURRENT_DATE AND status != 'cancelled'
`).Scan(&dailyTotalOrders)
for _, r := range dailyRows {
dailyTotalRevenue += r.Revenue
dailyTotalQty += r.TotalQuantity
idx, ok := dailyCatIdx[r.Category]
if !ok {
idx = len(dailyCats)
dailyCats = append(dailyCats, dailyCatGroup{
Category: r.Category,
CategoryColor: r.CategoryColor,
})
dailyCatIdx[r.Category] = idx
}
dailyCats[idx].TotalQuantity += r.TotalQuantity
dailyCats[idx].TotalRevenue += r.Revenue
dailyCats[idx].Products = append(dailyCats[idx].Products, gin.H{
"product_id": r.ProductID,
"name": r.ProductName,
"quantity": r.TotalQuantity,
"order_count": r.OrderCount,
"revenue": r.Revenue,
})
}
dailyCatsJSON := make([]gin.H, len(dailyCats))
for i, g := range dailyCats {
dailyCatsJSON[i] = gin.H{
"category": g.Category,
"category_color": g.CategoryColor,
"total_quantity": g.TotalQuantity,
"total_revenue": g.TotalRevenue,
"products": g.Products,
}
}
// ── Résumé global ───────────────────────────────────────────────────────── // ── Résumé global ─────────────────────────────────────────────────────────
var totalOrders int64 var totalOrders int64
var totalRevenue float64 var totalRevenue float64
@@ -337,6 +409,13 @@ func GetAdminStats(c *gin.Context) {
"by_day_revenue": byDayRevenue, "by_day_revenue": byDayRevenue,
"by_hour": byHour, "by_hour": byHour,
"top_products": topProducts, "top_products": topProducts,
"by_quantity": byQuantity, "by_quantity": byQuantity,
"daily_detail": gin.H{
"date": time.Now().Format("02/01/2006"),
"total_orders": dailyTotalOrders,
"total_quantity": dailyTotalQty,
"total_revenue": dailyTotalRevenue,
"categories": dailyCatsJSON,
},
}) })
} }
+10
View File
@@ -42,3 +42,13 @@ type DayRevenueRow struct {
Day time.Time `gorm:"column:day"` Day time.Time `gorm:"column:day"`
Revenue float64 `gorm:"column:revenue"` Revenue float64 `gorm:"column:revenue"`
} }
type DailyProductRow struct {
ProductID int `gorm:"column:product_id"`
ProductName string `gorm:"column:product_name"`
Category string `gorm:"column:category"`
CategoryColor string `gorm:"column:category_color"`
TotalQuantity float64 `gorm:"column:total_quantity"`
OrderCount int `gorm:"column:order_count"`
Revenue float64 `gorm:"column:revenue"`
}
+111 -275
View File
@@ -48,16 +48,18 @@
border-bottom-color: var(--border); border-bottom-color: var(--border);
} }
[data-theme="light"] .sidebar-footer {
background: #ffffff;
border-top-color: var(--border);
}
/* ============================================================ /* ============================================================
Body offset Body offset
============================================================ */ ============================================================ */
:root {
--bottomnav-h: 66px;
--bottomnav-offset: 15px;
}
body { body {
padding-top: var(--topbar-h); padding-top: var(--topbar-h);
padding-bottom: calc(var(--bottomnav-h) + var(--bottomnav-offset) + 12px);
} }
/* ============================================================ /* ============================================================
@@ -201,299 +203,134 @@ body {
/* ============================================================ /* ============================================================
Overlay Overlay
============================================================ */ ============================================================ */
.sidebar-overlay {
position: fixed;
inset: 0;
z-index: 950;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* ============================================================ /* ============================================================
Sidebar — iOS frosted glass Bottom nav — floating pill
============================================================ */ ============================================================ */
.sidebar { .bottom-nav {
position: fixed; position: fixed;
top: 0; bottom: var(--bottomnav-offset);
left: 0; left: 50%;
width: var(--sidebar-w); transform: translateX(-50%);
height: 100dvh; width: calc(100% - 30px);
z-index: 1000; max-width: 640px;
z-index: 900;
display: flex; display: flex;
flex-direction: column; align-items: stretch;
background: color-mix(in srgb, var(--primary) 28%, rgba(6, 6, 18, 0.88)); background: color-mix(in srgb, var(--primary) 30%, rgba(6, 6, 18, 0.88));
backdrop-filter: blur(50px) saturate(180%); backdrop-filter: blur(50px) saturate(180%);
-webkit-backdrop-filter: blur(50px) saturate(180%); -webkit-backdrop-filter: blur(50px) saturate(180%);
border-right: 1px solid rgba(255, 255, 255, 0.1); border-radius: 22px;
transform: translateX(-100%); padding: 6px;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); gap: 2px;
overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45), 0 2px 8px rgba(0, 0, 0, 0.3);
} }
[data-theme="light"] .sidebar { [data-theme="light"] .bottom-nav {
background: color-mix(in srgb, var(--primary) 14%, rgba(245, 242, 255, 0.92)); background: color-mix(in srgb, var(--primary) 14%, rgba(245, 242, 255, 0.94));
border-right-color: rgba(0, 0, 0, 0.06); border-color: rgba(0, 0, 0, 0.06);
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
} }
.sidebar.open { /* ── Tabs ── */
transform: translateX(0); .bottom-tab {
box-shadow: 8px 0 60px rgba(0, 0, 0, 0.55); flex: 1;
}
/* ── Header ── */
.sidebar-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1.2rem 1rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
[data-theme="light"] .sidebar-header {
border-bottom-color: rgba(0, 0, 0, 0.07);
}
.sidebar-avatar {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.18);
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #fff; gap: 3px;
font-size: 1rem; padding: 8px 4px;
flex-shrink: 0; background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2); border: none;
} border-radius: 16px;
color: rgba(255, 255, 255, 0.45);
[data-theme="light"] .sidebar-avatar { cursor: pointer;
background: rgba(255, 255, 255, 0.55); transition: all 0.18s ease;
color: var(--primary); -webkit-tap-highlight-color: transparent;
border-color: rgba(255, 255, 255, 0.7);
}
.sidebar-header-info {
flex: 1;
min-width: 0; min-width: 0;
} }
.sidebar-brand-name { [data-theme="light"] .bottom-tab {
margin: 0; color: rgba(0, 0, 0, 0.35);
font-size: 0.92rem; }
.bottom-tab:hover {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.75);
}
[data-theme="light"] .bottom-tab:hover {
background: rgba(0, 0, 0, 0.04);
color: rgba(0, 0, 0, 0.6);
}
.bottom-tab.active {
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
[data-theme="light"] .bottom-tab.active {
background: rgba(255, 255, 255, 0.65);
color: var(--primary);
}
.bottom-tab:active {
transform: scale(0.93);
}
/* ── Icon wrapper ── */
.bottom-tab-icon-wrap {
position: relative;
font-size: 1.05rem;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* ── Cart badge ── */
.bottom-tab-badge {
position: absolute;
top: -6px;
right: -8px;
min-width: 16px;
height: 16px;
border-radius: 999px;
background: var(--red);
color: #fff;
font-size: 0.58rem;
font-weight: 700; font-weight: 700;
color: rgba(255, 255, 255, 0.95); display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
line-height: 1;
}
/* ── Labels ── */
.bottom-tab-label {
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.01em; letter-spacing: 0.01em;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 100%;
} }
[data-theme="light"] .sidebar-brand-name { /* Hide labels on very narrow screens */
color: rgba(0, 0, 0, 0.85); @media (max-width: 380px) {
.bottom-tab-label { display: none; }
.bottom-tab { padding: 10px 4px; }
} }
.sidebar-brand-sub { /* Logout button — couleur rouge subtile */
margin: 0; .topbar-logout-btn {
font-size: 0.7rem; color: rgba(239, 68, 68, 0.7);
color: rgba(255, 255, 255, 0.5);
margin-top: 1px;
} }
.topbar-logout-btn:hover {
[data-theme="light"] .sidebar-brand-sub { color: var(--red) !important;
color: rgba(0, 0, 0, 0.4);
}
.sidebar-close {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.12);
border: none;
border-radius: 50%;
color: rgba(255, 255, 255, 0.7);
font-size: 0.8rem;
cursor: pointer;
transition: background var(--transition);
-webkit-tap-highlight-color: transparent;
flex-shrink: 0;
}
[data-theme="light"] .sidebar-close {
background: rgba(0, 0, 0, 0.08);
color: rgba(0, 0, 0, 0.5);
}
.sidebar-close:hover {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
[data-theme="light"] .sidebar-close:hover {
background: rgba(0, 0, 0, 0.12);
color: rgba(0, 0, 0, 0.8);
}
/* ── Nav ── */
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 14px 12px 8px;
scrollbar-width: none;
display: flex;
flex-direction: column;
gap: 10px;
}
.sidebar-nav::-webkit-scrollbar { display: none; }
/* ── iOS card groups ── */
.menu-group {
background: rgba(255, 255, 255, 0.09);
border-radius: 14px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
}
[data-theme="light"] .menu-group {
background: rgba(255, 255, 255, 0.58);
border-color: rgba(255, 255, 255, 0.8);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
/* ── Menu items ── */
.menu-item {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: transparent;
border: none;
border-top: 0.5px solid rgba(255, 255, 255, 0.07);
color: rgba(255, 255, 255, 0.88);
font-size: 0.92rem;
font-weight: 500;
cursor: pointer;
text-align: left;
transition: background 0.12s ease;
-webkit-tap-highlight-color: transparent;
position: relative;
}
.menu-group .menu-item:first-child {
border-top: none;
}
[data-theme="light"] .menu-item {
color: rgba(0, 0, 0, 0.82);
border-top-color: rgba(0, 0, 0, 0.06);
}
.menu-item:hover:not(:disabled),
.menu-item:focus-visible {
background: rgba(255, 255, 255, 0.1);
}
[data-theme="light"] .menu-item:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.04);
}
.menu-item.active {
background: rgba(255, 255, 255, 0.14);
}
[data-theme="light"] .menu-item.active {
background: rgba(0, 0, 0, 0.06);
}
.menu-item:active:not(:disabled) {
background: rgba(255, 255, 255, 0.06);
transform: scale(0.99);
}
.menu-item:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* ── Icon ── */
.menu-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: rgba(255, 255, 255, 0.15);
font-size: 0.82rem;
color: rgba(255, 255, 255, 0.92);
flex-shrink: 0;
transition: all var(--transition);
}
[data-theme="light"] .menu-icon {
background: rgba(255, 255, 255, 0.7);
color: var(--primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.menu-item.active .menu-icon {
background: var(--primary);
color: #fff;
box-shadow: 0 2px 12px color-mix(in srgb, var(--primary) 55%, transparent);
}
.menu-icon.icon-telegram { color: #22d3ee; }
.menu-icon.icon-danger { color: #f87171; }
[data-theme="light"] .menu-icon.icon-telegram { color: #0891b2; }
[data-theme="light"] .menu-icon.icon-danger { color: var(--red); }
/* ── Label ── */
.menu-label {
flex: 1;
letter-spacing: 0.01em;
}
.menu-item-danger .menu-label {
color: #f87171;
}
[data-theme="light"] .menu-item-danger .menu-label {
color: var(--red);
}
/* ── Chevron ── */
.menu-chevron {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.28);
flex-shrink: 0;
transition: transform var(--transition);
}
[data-theme="light"] .menu-chevron {
color: rgba(0, 0, 0, 0.2);
}
.menu-item.active .menu-chevron {
color: rgba(255, 255, 255, 0.55);
}
/* ── Footer ── */
.sidebar-footer {
padding: 8px 12px 16px;
flex-shrink: 0;
} }
/* ============================================================ /* ============================================================
@@ -623,11 +460,10 @@ body {
} }
/* ============================================================ /* ============================================================
Responsive — disable hover states on touch Responsive — touch devices
============================================================ */ ============================================================ */
@media (hover: none) { @media (hover: none) {
.menu-item:hover { background: transparent; } .bottom-tab:hover { background: transparent; color: rgba(255, 255, 255, 0.45); }
.menu-item.active:hover { background: rgba(255, 255, 255, 0.14); } .bottom-tab.active:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
.topbar-toggle:hover { background: transparent; border-color: transparent; }
.topbar-icon-btn:hover { background: transparent; border-color: transparent; } .topbar-icon-btn:hover { background: transparent; border-color: transparent; }
} }
+52 -139
View File
@@ -11,16 +11,13 @@ import {
faTruck, faTruck,
faClockRotateLeft, faClockRotateLeft,
faSignOutAlt, faSignOutAlt,
faBars,
faTimes, faTimes,
faBell, faBell,
faGift, faGift,
faUserCircle, faUserCircle,
faSun, faSun,
faMoon, faMoon,
faChevronRight,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { faTelegram } from "@fortawesome/free-brands-svg-icons";
import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { getClientNotifications, markNotificationsRead, getPublicSettings } from "../api/api"; import { getClientNotifications, markNotificationsRead, getPublicSettings } from "../api/api";
import type { ClientNotification } from "../api/api"; import type { ClientNotification } from "../api/api";
@@ -50,7 +47,6 @@ function formatNotifDate(dateStr: string): string {
function Navbar() { function Navbar() {
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false);
const [isLoggingOut, setIsLoggingOut] = useState<boolean>(false); const [isLoggingOut, setIsLoggingOut] = useState<boolean>(false);
const [notifications, setNotifications] = useState<ClientNotification[]>([]); const [notifications, setNotifications] = useState<ClientNotification[]>([]);
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
@@ -72,9 +68,7 @@ function Navbar() {
for (const n of res.notifications) { for (const n of res.notifications) {
if (n.read) continue; if (n.read) continue;
const key = `${n.command_id}-${n.type}-${n.created_at}`; const key = `${n.command_id}-${n.type}-${n.created_at}`;
if (!seenKeysRef.current.has(key)) { if (!seenKeysRef.current.has(key)) seenKeysRef.current.add(key);
seenKeysRef.current.add(key);
}
} }
} else { } else {
for (const n of res.notifications) { for (const n of res.notifications) {
@@ -106,29 +100,6 @@ function Navbar() {
} }
}; };
const closeNotifPanel = () => setShowNotifPanel(false);
const navItems: 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" },
];
const accountItems: MenuItem[] = [
{ 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 () => { const handleLogout = async () => {
if (isLoggingOut) return; if (isLoggingOut) return;
setIsLoggingOut(true); setIsLoggingOut(true);
@@ -138,10 +109,7 @@ function Navbar() {
try { try {
await fetch("/api/v2/admin/auth/logout", { await fetch("/api/v2/admin/auth/logout", {
method: "POST", method: "POST",
headers: { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
}); });
} catch { /* noop */ } } catch { /* noop */ }
} }
@@ -157,157 +125,102 @@ function Navbar() {
} }
}; };
const handleTelegram = () => window.open("https://t.me/milieu_nantais", "_blank"); const navItems: MenuItem[] = [
{ id: "accueil", label: "Accueil", icon: faHome, path: "/user/accueil" },
{ id: "produits", label: "Produits", icon: faBox, path: "/user/nos-produits" },
{ id: "panier", label: "Panier", icon: faShoppingCart, path: "/user/panier" },
{ id: "suivi", label: "Suivi", icon: faTruck, path: "/user/suivi-livraison" },
];
const renderItem = (item: MenuItem) => { const accountItems: MenuItem[] = [
{ id: "historique", label: "Historique", icon: faClockRotateLeft, path: "/user/consultation-historique" },
...(referralEnabled ? [{ id: "parrainage", label: "Parrain.", icon: faGift, path: "/user/parrainage" } as MenuItem] : []),
{ id: "profil", label: "Profil", icon: faUserCircle, path: "/user/profil" },
];
const allTabs = [...navItems, ...accountItems];
const renderTab = (item: MenuItem) => {
const isActive = location.pathname === item.path; const isActive = location.pathname === item.path;
const showBadge = item.id === "panier" && cartCount > 0;
return ( return (
<button <button
key={item.id} key={item.id}
className={`menu-item ${isActive ? "active" : ""}`} className={`bottom-tab ${isActive ? "active" : ""}`}
onClick={() => handleNavigation(item.path)} onClick={() => navigate(item.path)}
disabled={isLoggingOut} aria-label={item.label}
> >
<span className="menu-icon"> <span className="bottom-tab-icon-wrap">
<FontAwesomeIcon icon={item.icon} /> <FontAwesomeIcon icon={item.icon} />
{showBadge && (
<span className="bottom-tab-badge">
{cartCount > 99 ? "99+" : cartCount}
</span>
)}
</span> </span>
<span className="menu-label">{item.label}</span> <span className="bottom-tab-label">{item.label}</span>
<FontAwesomeIcon icon={faChevronRight} className="menu-chevron" />
</button> </button>
); );
}; };
return ( return (
<> <>
{/* ── Top Bar ─────────────────────────────────── */} {/* ── Top Bar ── */}
<header className="topbar"> <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">{shopName}</span> <span className="topbar-brand">{shopName}</span>
<div className="topbar-actions"> <div className="topbar-actions">
<button <button className="topbar-theme-btn" onClick={toggleTheme}
className="topbar-theme-btn" aria-label={theme === "dark" ? "Mode clair" : "Mode sombre"}>
onClick={toggleTheme}
aria-label={theme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"}
>
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} /> <FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} />
</button> </button>
<button className="topbar-icon-btn" onClick={handleNotifBellClick} aria-label="Notifications">
<button
className="topbar-icon-btn"
onClick={handleNotifBellClick}
aria-label="Notifications"
>
<FontAwesomeIcon icon={faBell} /> <FontAwesomeIcon icon={faBell} />
{unreadCount > 0 && ( {unreadCount > 0 && (
<span className="topbar-badge"> <span className="topbar-badge">{unreadCount > 99 ? "99+" : unreadCount}</span>
{unreadCount > 99 ? "99+" : unreadCount}
</span>
)} )}
</button> </button>
<button <button
className="topbar-icon-btn" className="topbar-icon-btn topbar-logout-btn"
onClick={() => navigate("/user/panier")} onClick={handleLogout}
aria-label="Panier" disabled={isLoggingOut}
aria-label="Se déconnecter"
> >
<FontAwesomeIcon icon={faShoppingCart} /> <FontAwesomeIcon icon={faSignOutAlt} />
{cartCount > 0 && (
<span className="topbar-badge">{cartCount}</span>
)}
</button> </button>
</div> </div>
</header> </header>
{/* ── Notifications modal ─────────────────────── */} {/* ── Notifications ── */}
{showNotifPanel && ( {showNotifPanel && (
<div className="notif-modal-overlay" onClick={closeNotifPanel}> <div className="notif-modal-overlay" onClick={() => setShowNotifPanel(false)}>
<div className="notif-modal" onClick={(e) => e.stopPropagation()}> <div className="notif-modal" onClick={(e) => e.stopPropagation()}>
<div className="notif-modal-header"> <div className="notif-modal-header">
<span className="notif-modal-title">Notifications</span> <span className="notif-modal-title">Notifications</span>
<button className="notif-modal-close" onClick={closeNotifPanel} aria-label="Fermer"> <button className="notif-modal-close" onClick={() => setShowNotifPanel(false)}>
<FontAwesomeIcon icon={faTimes} /> <FontAwesomeIcon icon={faTimes} />
</button> </button>
</div> </div>
<div className="notif-modal-body"> <div className="notif-modal-body">
{notifications.length === 0 ? ( {notifications.length === 0 ? (
<div className="notif-empty"> <div className="notif-empty">
<FontAwesomeIcon icon={faBell} style={{ fontSize: "2rem", marginBottom: "0.75rem", opacity: 0.3 }} /> <FontAwesomeIcon icon={faBell} style={{ fontSize: "2rem", opacity: 0.3 }} />
<p>Aucune notification</p> <p>Aucune notification</p>
</div> </div>
) : ( ) : notifications.map((n, i) => (
notifications.map((n, i) => ( <div key={i} className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`}>
<div <span className="notif-message">{n.message}</span>
key={i} <span className="notif-time">{formatNotifDate(n.created_at)}</span>
className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`} </div>
> ))}
<span className="notif-message">{n.message}</span>
<span className="notif-time">{formatNotifDate(n.created_at)}</span>
</div>
))
)}
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* ── Overlay ─────────────────────────────────── */} {/* ── Bottom nav ── */}
{isMenuOpen && <div className="sidebar-overlay" onClick={closeMenu} />} <nav className="bottom-nav">
{allTabs.map(renderTab)}
{/* ── Sidebar iOS-style ───────────────────────── */} </nav>
<aside className={`sidebar ${isMenuOpen ? "open" : ""}`}>
{/* Header */}
<div className="sidebar-header">
<div className="sidebar-avatar">
<FontAwesomeIcon icon={faShoppingCart} />
</div>
<div className="sidebar-header-info">
<p className="sidebar-brand-name">{shopName}</p>
<p className="sidebar-brand-sub">Mon espace</p>
</div>
<button className="sidebar-close" onClick={closeMenu} aria-label="Fermer">
<FontAwesomeIcon icon={faTimes} />
</button>
</div>
{/* Nav */}
<nav className="sidebar-nav">
<div className="menu-group">
{navItems.map(renderItem)}
</div>
<div className="menu-group">
{accountItems.map(renderItem)}
</div>
</nav>
{/* Footer */}
<div className="sidebar-footer">
<div className="menu-group">
<button className="menu-item" onClick={handleTelegram} disabled={isLoggingOut}>
<span className="menu-icon icon-telegram">
<FontAwesomeIcon icon={faTelegram} />
</span>
<span className="menu-label">Telegram</span>
<FontAwesomeIcon icon={faChevronRight} className="menu-chevron" />
</button>
<button className="menu-item menu-item-danger" onClick={handleLogout} disabled={isLoggingOut}>
<span className="menu-icon icon-danger">
<FontAwesomeIcon icon={faSignOutAlt} />
</span>
<span className="menu-label">{isLoggingOut ? "Déconnexion…" : "Déconnexion"}</span>
<FontAwesomeIcon icon={faChevronRight} className="menu-chevron" />
</button>
</div>
</div>
</aside>
</> </>
); );
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff