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
+79
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 ─────────────────────────────────────────────────────────
var totalOrders int64
var totalRevenue float64
@@ -338,5 +410,12 @@ func GetAdminStats(c *gin.Context) {
"by_hour": byHour,
"top_products": topProducts,
"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"`
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);
}
[data-theme="light"] .sidebar-footer {
background: #ffffff;
border-top-color: var(--border);
}
/* ============================================================
Body offset
============================================================ */
:root {
--bottomnav-h: 66px;
--bottomnav-offset: 15px;
}
body {
padding-top: var(--topbar-h);
padding-bottom: calc(var(--bottomnav-h) + var(--bottomnav-offset) + 12px);
}
/* ============================================================
@@ -201,299 +203,134 @@ body {
/* ============================================================
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;
top: 0;
left: 0;
width: var(--sidebar-w);
height: 100dvh;
z-index: 1000;
bottom: var(--bottomnav-offset);
left: 50%;
transform: translateX(-50%);
width: calc(100% - 30px);
max-width: 640px;
z-index: 900;
display: flex;
flex-direction: column;
background: color-mix(in srgb, var(--primary) 28%, rgba(6, 6, 18, 0.88));
align-items: stretch;
background: color-mix(in srgb, var(--primary) 30%, rgba(6, 6, 18, 0.88));
backdrop-filter: blur(50px) saturate(180%);
-webkit-backdrop-filter: blur(50px) saturate(180%);
border-right: 1px solid rgba(255, 255, 255, 0.1);
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
border-radius: 22px;
padding: 6px;
gap: 2px;
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 {
background: color-mix(in srgb, var(--primary) 14%, rgba(245, 242, 255, 0.92));
border-right-color: rgba(0, 0, 0, 0.06);
[data-theme="light"] .bottom-nav {
background: color-mix(in srgb, var(--primary) 14%, rgba(245, 242, 255, 0.94));
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 {
transform: translateX(0);
box-shadow: 8px 0 60px rgba(0, 0, 0, 0.55);
}
/* ── 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);
/* ── Tabs ── */
.bottom-tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1rem;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.2);
}
[data-theme="light"] .sidebar-avatar {
background: rgba(255, 255, 255, 0.55);
color: var(--primary);
border-color: rgba(255, 255, 255, 0.7);
}
.sidebar-header-info {
flex: 1;
gap: 3px;
padding: 8px 4px;
background: transparent;
border: none;
border-radius: 16px;
color: rgba(255, 255, 255, 0.45);
cursor: pointer;
transition: all 0.18s ease;
-webkit-tap-highlight-color: transparent;
min-width: 0;
}
.sidebar-brand-name {
margin: 0;
font-size: 0.92rem;
[data-theme="light"] .bottom-tab {
color: rgba(0, 0, 0, 0.35);
}
.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;
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;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
[data-theme="light"] .sidebar-brand-name {
color: rgba(0, 0, 0, 0.85);
/* Hide labels on very narrow screens */
@media (max-width: 380px) {
.bottom-tab-label { display: none; }
.bottom-tab { padding: 10px 4px; }
}
.sidebar-brand-sub {
margin: 0;
font-size: 0.7rem;
color: rgba(255, 255, 255, 0.5);
margin-top: 1px;
/* Logout button — couleur rouge subtile */
.topbar-logout-btn {
color: rgba(239, 68, 68, 0.7);
}
[data-theme="light"] .sidebar-brand-sub {
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;
.topbar-logout-btn:hover {
color: var(--red) !important;
}
/* ============================================================
@@ -623,11 +460,10 @@ body {
}
/* ============================================================
Responsive — disable hover states on touch
Responsive — touch devices
============================================================ */
@media (hover: none) {
.menu-item:hover { background: transparent; }
.menu-item.active:hover { background: rgba(255, 255, 255, 0.14); }
.topbar-toggle:hover { background: transparent; border-color: transparent; }
.bottom-tab:hover { background: transparent; color: rgba(255, 255, 255, 0.45); }
.bottom-tab.active:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
.topbar-icon-btn:hover { background: transparent; border-color: transparent; }
}
+48 -135
View File
@@ -11,16 +11,13 @@ import {
faTruck,
faClockRotateLeft,
faSignOutAlt,
faBars,
faTimes,
faBell,
faGift,
faUserCircle,
faSun,
faMoon,
faChevronRight,
} 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";
@@ -50,7 +47,6 @@ function formatNotifDate(dateStr: string): 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);
@@ -72,9 +68,7 @@ function Navbar() {
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);
}
if (!seenKeysRef.current.has(key)) seenKeysRef.current.add(key);
}
} else {
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 () => {
if (isLoggingOut) return;
setIsLoggingOut(true);
@@ -138,10 +109,7 @@ function Navbar() {
try {
await fetch("/api/v2/admin/auth/logout", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
});
} 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 showBadge = item.id === "panier" && cartCount > 0;
return (
<button
key={item.id}
className={`menu-item ${isActive ? "active" : ""}`}
onClick={() => handleNavigation(item.path)}
disabled={isLoggingOut}
className={`bottom-tab ${isActive ? "active" : ""}`}
onClick={() => navigate(item.path)}
aria-label={item.label}
>
<span className="menu-icon">
<span className="bottom-tab-icon-wrap">
<FontAwesomeIcon icon={item.icon} />
{showBadge && (
<span className="bottom-tab-badge">
{cartCount > 99 ? "99+" : cartCount}
</span>
<span className="menu-label">{item.label}</span>
<FontAwesomeIcon icon={faChevronRight} className="menu-chevron" />
)}
</span>
<span className="bottom-tab-label">{item.label}</span>
</button>
);
};
return (
<>
{/* ── Top Bar ─────────────────────────────────── */}
{/* ── 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">{shopName}</span>
<div className="topbar-actions">
<button
className="topbar-theme-btn"
onClick={toggleTheme}
aria-label={theme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"}
>
<button className="topbar-theme-btn" onClick={toggleTheme}
aria-label={theme === "dark" ? "Mode clair" : "Mode sombre"}>
<FontAwesomeIcon icon={theme === "dark" ? faSun : faMoon} />
</button>
<button
className="topbar-icon-btn"
onClick={handleNotifBellClick}
aria-label="Notifications"
>
<button className="topbar-icon-btn" onClick={handleNotifBellClick} aria-label="Notifications">
<FontAwesomeIcon icon={faBell} />
{unreadCount > 0 && (
<span className="topbar-badge">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
<span className="topbar-badge">{unreadCount > 99 ? "99+" : unreadCount}</span>
)}
</button>
<button
className="topbar-icon-btn"
onClick={() => navigate("/user/panier")}
aria-label="Panier"
className="topbar-icon-btn topbar-logout-btn"
onClick={handleLogout}
disabled={isLoggingOut}
aria-label="Se déconnecter"
>
<FontAwesomeIcon icon={faShoppingCart} />
{cartCount > 0 && (
<span className="topbar-badge">{cartCount}</span>
)}
<FontAwesomeIcon icon={faSignOutAlt} />
</button>
</div>
</header>
{/* ── Notifications modal ─────────────────────── */}
{/* ── Notifications ── */}
{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-header">
<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} />
</button>
</div>
<div className="notif-modal-body">
{notifications.length === 0 ? (
<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>
</div>
) : (
notifications.map((n, i) => (
<div
key={i}
className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`}
>
) : notifications.map((n, i) => (
<div key={i} className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`}>
<span className="notif-message">{n.message}</span>
<span className="notif-time">{formatNotifDate(n.created_at)}</span>
</div>
))
)}
))}
</div>
</div>
</div>
)}
{/* ── Overlay ─────────────────────────────────── */}
{isMenuOpen && <div className="sidebar-overlay" onClick={closeMenu} />}
{/* ── Sidebar iOS-style ───────────────────────── */}
<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>
{/* ── Bottom nav ── */}
<nav className="bottom-nav">
{allTabs.map(renderTab)}
</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>
</>
);
}
+359 -116
View File
@@ -1,40 +1,67 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Navbar from '../../components/Navbar';
import { isUserAuthenticated, extractUsernameFromToken, getMyProfile, updateMyProfile, getTelegramStatus, generateTelegramLinkToken, unlinkTelegram, get2FAStatus, toggle2FA, getPublicSettings } from '../../api/api';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import Navbar from "../../components/Navbar";
import {
faUser, faMapMarkerAlt, faPhone, faCommentDots,
faSave, faCheckCircle, faExclamationTriangle, faPaperPlane, faUnlink, faTimes, faLock, faShieldAlt,
} from '@fortawesome/free-solid-svg-icons';
import './ProfilePage.css';
isUserAuthenticated,
extractUsernameFromToken,
getMyProfile,
updateMyProfile,
getTelegramStatus,
generateTelegramLinkToken,
unlinkTelegram,
get2FAStatus,
toggle2FA,
getPublicSettings,
} from "../../api/api";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faUser,
faMapMarkerAlt,
faPhone,
faCommentDots,
faSave,
faCheckCircle,
faExclamationTriangle,
faPaperPlane,
faUnlink,
faTimes,
faLock,
faShieldAlt,
} from "@fortawesome/free-solid-svg-icons";
import "./ProfilePage.css";
const STORAGE_ADDRESS = 'profile_default_address';
const STORAGE_PHONE = 'profile_default_phone';
const STORAGE_SIGNAL = 'profile_signal_pseudo';
const STORAGE_ADDRESS = "profile_default_address";
const STORAGE_PHONE = "profile_default_phone";
const STORAGE_SIGNAL = "profile_signal_pseudo";
export default function ProfilePage() {
const navigate = useNavigate();
// Données compte (backend)
const [nom, setNom] = useState('');
const [prenom, setPrenom] = useState('');
const [telephone, setTelephone] = useState('');
const [nom, setNom] = useState("");
const [prenom, setPrenom] = useState("");
const [telephone, setTelephone] = useState("");
const [loadingProfile, setLoadingProfile] = useState(true);
// Données locales (localStorage)
const [defaultAddress, setDefaultAddress] = useState(() => localStorage.getItem(STORAGE_ADDRESS) ?? '');
const [defaultPhone, setDefaultPhone] = useState(() => localStorage.getItem(STORAGE_PHONE) ?? '');
const [signalPseudo, setSignalPseudo] = useState(() => localStorage.getItem(STORAGE_SIGNAL) ?? '');
const [defaultAddress, setDefaultAddress] = useState(
() => localStorage.getItem(STORAGE_ADDRESS) ?? "",
);
const [defaultPhone, setDefaultPhone] = useState(
() => localStorage.getItem(STORAGE_PHONE) ?? "",
);
const [signalPseudo, setSignalPseudo] = useState(
() => localStorage.getItem(STORAGE_SIGNAL) ?? "",
);
const [savingContact, setSavingContact] = useState(false);
// Modals succès / erreur (pattern identique à l'app mobile)
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [successTitle, setSuccessTitle] = useState('');
const [successMsg, setSuccessMsg] = useState('');
const [successTitle, setSuccessTitle] = useState("");
const [successMsg, setSuccessMsg] = useState("");
const [showErrorModal, setShowErrorModal] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
const [errorMsg, setErrorMsg] = useState("");
// Telegram
const [tgLinked, setTgLinked] = useState(false);
@@ -48,35 +75,45 @@ export default function ProfilePage() {
// Modals confirmation
const [showSaveModal, setShowSaveModal] = useState(false);
const [showConfirmAddressModal, setShowConfirmAddressModal] = useState(false);
const [showConfirmContactModal, setShowConfirmContactModal] = useState(false);
const [showConfirmAddressModal, setShowConfirmAddressModal] =
useState(false);
const [showConfirmContactModal, setShowConfirmContactModal] =
useState(false);
const [showUnlinkModal, setShowUnlinkModal] = useState(false);
const [showUnlinkSuccessModal, setShowUnlinkSuccessModal] = useState(false);
const username = extractUsernameFromToken() ?? '';
const username = extractUsernameFromToken() ?? "";
useEffect(() => {
if (!isUserAuthenticated()) {
navigate('/login/client', { replace: true });
navigate("/login/client", { replace: true });
return;
}
// Statut Telegram
getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); });
getTelegramStatus().then((s) => {
setTgLinked(s.linked);
setTgEnabled(s.enabled);
});
// Statut 2FA
Promise.all([get2FAStatus(), getPublicSettings()]).then(([status, pub]) => {
Promise.all([get2FAStatus(), getPublicSettings()]).then(
([status, pub]) => {
setTwoFAEnabled(status.two_fa_enabled);
setTwoFAAdminEnabled(pub.two_fa_enabled);
});
},
);
// Charger depuis backend
getMyProfile().then((res) => {
if (res.success && res.client) {
setNom(res.client.nom ?? '');
setPrenom(res.client.prenom ?? '');
setTelephone(res.client.telephone ?? '');
setNom(res.client.nom ?? "");
setPrenom(res.client.prenom ?? "");
setTelephone(res.client.telephone ?? "");
// Initialiser le téléphone par défaut si pas encore défini
if (!localStorage.getItem(STORAGE_PHONE) && res.client.telephone) {
if (
!localStorage.getItem(STORAGE_PHONE) &&
res.client.telephone
) {
setDefaultPhone(res.client.telephone);
}
}
@@ -85,16 +122,22 @@ export default function ProfilePage() {
}, [navigate]);
const showSuccess = (title: string, msg: string) => {
setSuccessTitle(title); setSuccessMsg(msg); setShowSuccessModal(true);
setSuccessTitle(title);
setSuccessMsg(msg);
setShowSuccessModal(true);
};
const showError = (msg: string) => {
setErrorMsg(msg); setShowErrorModal(true);
setErrorMsg(msg);
setShowErrorModal(true);
};
const saveAddress = () => {
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
setShowConfirmAddressModal(false);
showSuccess('Adresse enregistrée', 'Votre adresse par défaut a été sauvegardée et sera pré-remplie à votre prochaine commande.');
showSuccess(
"Adresse enregistrée",
"Votre adresse par défaut a été sauvegardée et sera pré-remplie à votre prochaine commande.",
);
};
const saveLocal = () => {
@@ -102,7 +145,10 @@ export default function ProfilePage() {
localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
setShowSaveModal(false);
showSuccess('Infos enregistrées', 'Adresse, téléphone et pseudo Signal sauvegardés. Ils seront pré-remplis à votre prochaine commande.');
showSuccess(
"Infos enregistrées",
"Adresse, téléphone et pseudo Signal sauvegardés. Ils seront pré-remplis à votre prochaine commande.",
);
};
const handleLinkTelegram = async () => {
@@ -110,10 +156,10 @@ export default function ProfilePage() {
const res = await generateTelegramLinkToken();
setTgLoading(false);
if (res.error || !res.link_url) {
showError(res.error || 'Service Telegram non disponible');
showError(res.error || "Service Telegram non disponible");
return;
}
window.open(res.link_url, '_blank');
window.open(res.link_url, "_blank");
};
const handleUnlinkTelegram = () => {
@@ -135,21 +181,35 @@ export default function ProfilePage() {
setTwoFALoading(false);
if (res.success) {
setTwoFAEnabled(newVal);
showSuccess(newVal ? '2FA activée' : '2FA désactivée', newVal ? 'Un code vous sera envoyé sur Telegram à chaque connexion.' : 'La double authentification a été désactivée.');
showSuccess(
newVal ? "2FA activée" : "2FA désactivée",
newVal
? "Un code vous sera envoyé sur Telegram à chaque connexion."
: "La double authentification a été désactivée.",
);
} else {
showError(res.error || 'Erreur lors de la modification');
showError(res.error || "Erreur lors de la modification");
}
};
const saveContact = async () => {
setShowConfirmContactModal(false);
setSavingContact(true);
const res = await updateMyProfile({ nom: nom.trim(), prenom: prenom.trim(), telephone: telephone.trim() });
const res = await updateMyProfile({
nom: nom.trim(),
prenom: prenom.trim(),
telephone: telephone.trim(),
});
setSavingContact(false);
if (res.success) {
showSuccess('Profil mis à jour', 'Vos informations de compte ont été enregistrées avec succès.');
showSuccess(
"Profil mis à jour",
"Vos informations de compte ont été enregistrées avec succès.",
);
} else {
showError(res.message ?? 'Erreur lors de la mise à jour du profil.');
showError(
res.message ?? "Erreur lors de la mise à jour du profil.",
);
}
};
@@ -158,7 +218,9 @@ export default function ProfilePage() {
<>
<Navbar />
<div className="profile-container">
<div className="profile-loading"><div className="spinner" /></div>
<div className="profile-loading">
<div className="spinner" />
</div>
</div>
</>
);
@@ -174,14 +236,17 @@ export default function ProfilePage() {
</div>
<div>
<h1 className="profile-title">Mon Profil</h1>
<p className="profile-username">@{username}</p>
<p className="profile-username">{username}</p>
</div>
</div>
{/* Section compte */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faUser} className="profile-card-icon" />
<FontAwesomeIcon
icon={faUser}
className="profile-card-icon"
/>
Mon compte
</h2>
<div className="profile-fields">
@@ -215,23 +280,38 @@ export default function ProfilePage() {
/>
</div>
</div>
<button className="profile-btn" onClick={() => setShowConfirmContactModal(true)} disabled={savingContact}>
<button
className="profile-btn"
onClick={() => setShowConfirmContactModal(true)}
disabled={savingContact}
>
<FontAwesomeIcon icon={faSave} />
{savingContact ? ' Enregistrement...' : ' Enregistrer le compte'}
{savingContact
? " Enregistrement..."
: " Enregistrer le compte"}
</button>
<button className="profile-btn profile-btn--secondary" onClick={() => navigate('/user/change-password')} style={{ marginTop: '0.6rem' }}>
<FontAwesomeIcon icon={faLock} /> Changer le mot de passe
<button
className="profile-btn profile-btn--secondary"
onClick={() => navigate("/user/change-password")}
style={{ marginTop: "0.6rem" }}
>
<FontAwesomeIcon icon={faLock} /> Changer le mot de
passe
</button>
</div>
{/* Section adresse par défaut */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faMapMarkerAlt} className="profile-card-icon profile-card-icon--address" />
<FontAwesomeIcon
icon={faMapMarkerAlt}
className="profile-card-icon profile-card-icon--address"
/>
Adresse par défaut
</h2>
<p className="profile-hint">
Sera pré-remplie dans le formulaire de commande. Vous pourrez la modifier si vous n'êtes pas à cette adresse.
Sera pré-remplie dans le formulaire de commande. Vous
pourrez la modifier si vous n'êtes pas à cette adresse.
</p>
<div className="profile-group">
<label>Adresse</label>
@@ -242,7 +322,11 @@ export default function ProfilePage() {
placeholder="Numéro, rue, ville, code postal"
/>
</div>
<button className="profile-btn profile-btn--secondary" onClick={() => setShowConfirmAddressModal(true)} style={{ marginTop: '0.8rem' }}>
<button
className="profile-btn profile-btn--secondary"
onClick={() => setShowConfirmAddressModal(true)}
style={{ marginTop: "0.8rem" }}
>
<FontAwesomeIcon icon={faSave} /> Enregistrer l'adresse
</button>
</div>
@@ -250,11 +334,15 @@ export default function ProfilePage() {
{/* Section contact commande */}
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faPhone} className="profile-card-icon profile-card-icon--phone" />
<FontAwesomeIcon
icon={faPhone}
className="profile-card-icon profile-card-icon--phone"
/>
Contact livraison
</h2>
<p className="profile-hint">
Numéro utilisé par le livreur lors de la livraison. Peut être différent du numéro de votre compte.
Numéro utilisé par le livreur lors de la livraison. Peut
être différent du numéro de votre compte.
</p>
<div className="profile-group">
<label>Téléphone par défaut</label>
@@ -265,9 +353,15 @@ export default function ProfilePage() {
placeholder="+33 6 12 34 56 78"
/>
</div>
<div className="profile-group" style={{ marginTop: '1rem' }}>
<div
className="profile-group"
style={{ marginTop: "1rem" }}
>
<label>
<FontAwesomeIcon icon={faCommentDots} style={{ marginRight: '0.4rem' }} />
<FontAwesomeIcon
icon={faCommentDots}
style={{ marginRight: "0.4rem" }}
/>
Pseudo Signal (optionnel)
</label>
<input
@@ -277,33 +371,52 @@ export default function ProfilePage() {
placeholder="@votre.pseudo.signal"
/>
</div>
<button className="profile-btn profile-btn--secondary" onClick={() => setShowSaveModal(true)}>
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos par défaut
<button
className="profile-btn profile-btn--secondary"
onClick={() => setShowSaveModal(true)}
>
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos
par défaut
</button>
</div>
{/* Section Telegram */}
{tgEnabled && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faPaperPlane} className="profile-card-icon profile-card-icon--telegram" />
<FontAwesomeIcon
icon={faPaperPlane}
className="profile-card-icon profile-card-icon--telegram"
/>
Notifications Telegram
</h2>
<p className="profile-hint">
Recevez vos notifications sur Telegram, même quand le site est fermé.
Recevez vos notifications sur Telegram, même quand
le site est fermé.
</p>
{tgLinked ? (
<div className="profile-telegram-linked">
<span className="profile-telegram-status">
<FontAwesomeIcon icon={faCheckCircle} /> Compte Telegram lié
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Compte Telegram lié
</span>
<button className="profile-btn profile-btn--danger" onClick={handleUnlinkTelegram}>
<FontAwesomeIcon icon={faUnlink} /> Délier Telegram
<button
className="profile-btn profile-btn--danger"
onClick={handleUnlinkTelegram}
>
<FontAwesomeIcon icon={faUnlink} /> Délier
Telegram
</button>
</div>
) : (
<button className="profile-btn profile-btn--telegram" onClick={handleLinkTelegram} disabled={tgLoading}>
<button
className="profile-btn profile-btn--telegram"
onClick={handleLinkTelegram}
disabled={tgLoading}
>
<FontAwesomeIcon icon={faPaperPlane} />
{tgLoading ? ' Génération du lien...' : ' Lier mon compte Telegram'}
{tgLoading
? " Génération du lien..."
: " Lier mon compte Telegram"}
</button>
)}
</div>
@@ -313,18 +426,26 @@ export default function ProfilePage() {
{twoFAAdminEnabled && tgLinked && (
<div className="profile-card">
<h2 className="profile-card-title">
<FontAwesomeIcon icon={faShieldAlt} className="profile-card-icon" style={{ color: '#6366f1' }} />
<FontAwesomeIcon
icon={faShieldAlt}
className="profile-card-icon"
style={{ color: "#6366f1" }}
/>
Double authentification (2FA)
</h2>
<p className="profile-hint">
À chaque connexion, un code à 6 chiffres vous sera envoyé sur Telegram avant d'accéder à votre compte.
À chaque connexion, un code à 6 chiffres vous sera
envoyé sur Telegram avant d'accéder à votre compte.
</p>
<div className="profile-2fa-row">
<div className="profile-2fa-label">
<span>{twoFAEnabled ? 'Activée' : 'Désactivée'}</span>
<span>
{twoFAEnabled ? "Activée" : "Désactivée"}
</span>
{twoFAEnabled && (
<span className="profile-2fa-badge">
<FontAwesomeIcon icon={faCheckCircle} /> Protection active
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Protection active
</span>
)}
</div>
@@ -334,7 +455,7 @@ export default function ProfilePage() {
<button
role="switch"
aria-checked={twoFAEnabled}
className={`profile-toggle ${twoFAEnabled ? 'profile-toggle--on' : ''}`}
className={`profile-toggle ${twoFAEnabled ? "profile-toggle--on" : ""}`}
onClick={handleToggle2FA}
aria-label="Activer ou désactiver la double authentification"
>
@@ -347,24 +468,44 @@ export default function ProfilePage() {
</div>
{showSaveModal && (
<div className="profile-modal-overlay" onClick={() => setShowSaveModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowSaveModal(false)}>
<div
className="profile-modal-overlay"
onClick={() => setShowSaveModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowSaveModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faSave} />
</div>
<h3 className="profile-modal-title">Enregistrer les infos par défaut ?</h3>
<h3 className="profile-modal-title">
Enregistrer les infos par défaut ?
</h3>
<p className="profile-modal-body">
Adresse, téléphone de livraison et pseudo Signal seront sauvegardés localement et pré-remplis lors de vos prochaines commandes.
Adresse, téléphone de livraison et pseudo Signal
seront sauvegardés localement et pré-remplis lors de
vos prochaines commandes.
</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--cancel" onClick={() => setShowSaveModal(false)}>
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() => setShowSaveModal(false)}
>
Annuler
</button>
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={saveLocal}>
<FontAwesomeIcon icon={faCheckCircle} /> Confirmer
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={saveLocal}
>
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Confirmer
</button>
</div>
</div>
@@ -372,24 +513,45 @@ export default function ProfilePage() {
)}
{showConfirmAddressModal && (
<div className="profile-modal-overlay" onClick={() => setShowConfirmAddressModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowConfirmAddressModal(false)}>
<div
className="profile-modal-overlay"
onClick={() => setShowConfirmAddressModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowConfirmAddressModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faMapMarkerAlt} />
</div>
<h3 className="profile-modal-title">Enregistrer l'adresse ?</h3>
<h3 className="profile-modal-title">
Enregistrer l'adresse ?
</h3>
<p className="profile-modal-body">
Cette adresse sera sauvegardée localement et pré-remplie lors de vos prochaines commandes.
Cette adresse sera sauvegardée localement et
pré-remplie lors de vos prochaines commandes.
</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--cancel" onClick={() => setShowConfirmAddressModal(false)}>
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() =>
setShowConfirmAddressModal(false)
}
>
Annuler
</button>
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={saveAddress}>
<FontAwesomeIcon icon={faCheckCircle} /> Confirmer
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={saveAddress}
>
<FontAwesomeIcon icon={faCheckCircle} />{" "}
Confirmer
</button>
</div>
</div>
@@ -397,24 +559,48 @@ export default function ProfilePage() {
)}
{showConfirmContactModal && (
<div className="profile-modal-overlay" onClick={() => setShowConfirmContactModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowConfirmContactModal(false)}>
<div
className="profile-modal-overlay"
onClick={() => setShowConfirmContactModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowConfirmContactModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon">
<FontAwesomeIcon icon={faUser} />
</div>
<h3 className="profile-modal-title">Enregistrer le compte ?</h3>
<h3 className="profile-modal-title">
Enregistrer le compte ?
</h3>
<p className="profile-modal-body">
Vos informations (prénom, nom, téléphone) seront mises à jour sur votre compte.
Vos informations (prénom, nom, téléphone) seront
mises à jour sur votre compte.
</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--cancel" onClick={() => setShowConfirmContactModal(false)}>
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() =>
setShowConfirmContactModal(false)
}
>
Annuler
</button>
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={saveContact} disabled={savingContact}>
<FontAwesomeIcon icon={faCheckCircle} /> {savingContact ? 'Enregistrement...' : 'Confirmer'}
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={saveContact}
disabled={savingContact}
>
<FontAwesomeIcon icon={faCheckCircle} />{" "}
{savingContact
? "Enregistrement..."
: "Confirmer"}
</button>
</div>
</div>
@@ -422,23 +608,41 @@ export default function ProfilePage() {
)}
{showUnlinkModal && (
<div className="profile-modal-overlay" onClick={() => setShowUnlinkModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowUnlinkModal(false)}>
<div
className="profile-modal-overlay"
onClick={() => setShowUnlinkModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowUnlinkModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--danger">
<FontAwesomeIcon icon={faUnlink} />
</div>
<h3 className="profile-modal-title">Délier Telegram ?</h3>
<h3 className="profile-modal-title">
Délier Telegram ?
</h3>
<p className="profile-modal-body">
Vous ne recevrez plus de notifications Telegram. La double authentification sera également désactivée.
Vous ne recevrez plus de notifications Telegram. La
double authentification sera également désactivée.
</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--cancel" onClick={() => setShowUnlinkModal(false)}>
<button
className="profile-modal-btn profile-modal-btn--cancel"
onClick={() => setShowUnlinkModal(false)}
>
Annuler
</button>
<button className="profile-modal-btn profile-modal-btn--danger" onClick={confirmUnlinkTelegram}>
<button
className="profile-modal-btn profile-modal-btn--danger"
onClick={confirmUnlinkTelegram}
>
<FontAwesomeIcon icon={faUnlink} /> Délier
</button>
</div>
@@ -448,9 +652,18 @@ export default function ProfilePage() {
{/* Modal succès */}
{showSuccessModal && (
<div className="profile-modal-overlay" onClick={() => setShowSuccessModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowSuccessModal(false)}>
<div
className="profile-modal-overlay"
onClick={() => setShowSuccessModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowSuccessModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--success">
@@ -459,7 +672,10 @@ export default function ProfilePage() {
<h3 className="profile-modal-title">{successTitle}</h3>
<p className="profile-modal-body">{successMsg}</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--confirm profile-modal-btn--success" onClick={() => setShowSuccessModal(false)}>
<button
className="profile-modal-btn profile-modal-btn--confirm profile-modal-btn--success"
onClick={() => setShowSuccessModal(false)}
>
Fermer
</button>
</div>
@@ -469,18 +685,32 @@ export default function ProfilePage() {
{/* Modal erreur */}
{showErrorModal && (
<div className="profile-modal-overlay" onClick={() => setShowErrorModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowErrorModal(false)}>
<div
className="profile-modal-overlay"
onClick={() => setShowErrorModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowErrorModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--danger">
<FontAwesomeIcon icon={faExclamationTriangle} />
</div>
<h3 className="profile-modal-title">Une erreur est survenue</h3>
<h3 className="profile-modal-title">
Une erreur est survenue
</h3>
<p className="profile-modal-body">{errorMsg}</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--danger" onClick={() => setShowErrorModal(false)}>
<button
className="profile-modal-btn profile-modal-btn--danger"
onClick={() => setShowErrorModal(false)}
>
Fermer
</button>
</div>
@@ -489,9 +719,18 @@ export default function ProfilePage() {
)}
{showUnlinkSuccessModal && (
<div className="profile-modal-overlay" onClick={() => setShowUnlinkSuccessModal(false)}>
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
<button className="profile-modal-close" onClick={() => setShowUnlinkSuccessModal(false)}>
<div
className="profile-modal-overlay"
onClick={() => setShowUnlinkSuccessModal(false)}
>
<div
className="profile-modal"
onClick={(e) => e.stopPropagation()}
>
<button
className="profile-modal-close"
onClick={() => setShowUnlinkSuccessModal(false)}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="profile-modal-icon profile-modal-icon--success">
@@ -499,10 +738,14 @@ export default function ProfilePage() {
</div>
<h3 className="profile-modal-title">Compte délié</h3>
<p className="profile-modal-body">
Votre compte Telegram a é délié avec succès. Vous ne recevrez plus de notifications via Telegram.
Votre compte Telegram a é délié avec succès. Vous
ne recevrez plus de notifications via Telegram.
</p>
<div className="profile-modal-actions">
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={() => setShowUnlinkSuccessModal(false)}>
<button
className="profile-modal-btn profile-modal-btn--confirm"
onClick={() => setShowUnlinkSuccessModal(false)}
>
<FontAwesomeIcon icon={faCheckCircle} /> Fermer
</button>
</div>
File diff suppressed because it is too large Load Diff