chore: build
This commit is contained in:
@@ -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
|
||||||
@@ -338,5 +410,12 @@ func GetAdminStats(c *gin.Context) {
|
|||||||
"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,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 className="menu-label">{item.label}</span>
|
)}
|
||||||
<FontAwesomeIcon icon={faChevronRight} className="menu-chevron" />
|
</span>
|
||||||
|
<span className="bottom-tab-label">{item.label}</span>
|
||||||
</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
|
|
||||||
key={i}
|
|
||||||
className={`notif-item ${n.read ? "notif-read" : "notif-unread"}`}
|
|
||||||
>
|
|
||||||
<span className="notif-message">{n.message}</span>
|
<span className="notif-message">{n.message}</span>
|
||||||
<span className="notif-time">{formatNotifDate(n.created_at)}</span>
|
<span className="notif-time">{formatNotifDate(n.created_at)}</span>
|
||||||
</div>
|
</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 ───────────────────────── */}
|
|
||||||
<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>
|
</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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,67 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from "react-router-dom";
|
||||||
import Navbar from '../../components/Navbar';
|
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 {
|
import {
|
||||||
faUser, faMapMarkerAlt, faPhone, faCommentDots,
|
isUserAuthenticated,
|
||||||
faSave, faCheckCircle, faExclamationTriangle, faPaperPlane, faUnlink, faTimes, faLock, faShieldAlt,
|
extractUsernameFromToken,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
getMyProfile,
|
||||||
import './ProfilePage.css';
|
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_ADDRESS = "profile_default_address";
|
||||||
const STORAGE_PHONE = 'profile_default_phone';
|
const STORAGE_PHONE = "profile_default_phone";
|
||||||
const STORAGE_SIGNAL = 'profile_signal_pseudo';
|
const STORAGE_SIGNAL = "profile_signal_pseudo";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Données compte (backend)
|
// Données compte (backend)
|
||||||
const [nom, setNom] = useState('');
|
const [nom, setNom] = useState("");
|
||||||
const [prenom, setPrenom] = useState('');
|
const [prenom, setPrenom] = useState("");
|
||||||
const [telephone, setTelephone] = useState('');
|
const [telephone, setTelephone] = useState("");
|
||||||
const [loadingProfile, setLoadingProfile] = useState(true);
|
const [loadingProfile, setLoadingProfile] = useState(true);
|
||||||
|
|
||||||
// Données locales (localStorage)
|
// Données locales (localStorage)
|
||||||
const [defaultAddress, setDefaultAddress] = useState(() => localStorage.getItem(STORAGE_ADDRESS) ?? '');
|
const [defaultAddress, setDefaultAddress] = useState(
|
||||||
const [defaultPhone, setDefaultPhone] = useState(() => localStorage.getItem(STORAGE_PHONE) ?? '');
|
() => localStorage.getItem(STORAGE_ADDRESS) ?? "",
|
||||||
const [signalPseudo, setSignalPseudo] = useState(() => localStorage.getItem(STORAGE_SIGNAL) ?? '');
|
);
|
||||||
|
const [defaultPhone, setDefaultPhone] = useState(
|
||||||
|
() => localStorage.getItem(STORAGE_PHONE) ?? "",
|
||||||
|
);
|
||||||
|
const [signalPseudo, setSignalPseudo] = useState(
|
||||||
|
() => localStorage.getItem(STORAGE_SIGNAL) ?? "",
|
||||||
|
);
|
||||||
|
|
||||||
const [savingContact, setSavingContact] = useState(false);
|
const [savingContact, setSavingContact] = useState(false);
|
||||||
|
|
||||||
// Modals succès / erreur (pattern identique à l'app mobile)
|
// Modals succès / erreur (pattern identique à l'app mobile)
|
||||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
const [successTitle, setSuccessTitle] = useState('');
|
const [successTitle, setSuccessTitle] = useState("");
|
||||||
const [successMsg, setSuccessMsg] = useState('');
|
const [successMsg, setSuccessMsg] = useState("");
|
||||||
const [showErrorModal, setShowErrorModal] = useState(false);
|
const [showErrorModal, setShowErrorModal] = useState(false);
|
||||||
const [errorMsg, setErrorMsg] = useState('');
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
|
||||||
// Telegram
|
// Telegram
|
||||||
const [tgLinked, setTgLinked] = useState(false);
|
const [tgLinked, setTgLinked] = useState(false);
|
||||||
@@ -48,35 +75,45 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
// Modals confirmation
|
// Modals confirmation
|
||||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||||
const [showConfirmAddressModal, setShowConfirmAddressModal] = useState(false);
|
const [showConfirmAddressModal, setShowConfirmAddressModal] =
|
||||||
const [showConfirmContactModal, setShowConfirmContactModal] = useState(false);
|
useState(false);
|
||||||
|
const [showConfirmContactModal, setShowConfirmContactModal] =
|
||||||
|
useState(false);
|
||||||
const [showUnlinkModal, setShowUnlinkModal] = useState(false);
|
const [showUnlinkModal, setShowUnlinkModal] = useState(false);
|
||||||
const [showUnlinkSuccessModal, setShowUnlinkSuccessModal] = useState(false);
|
const [showUnlinkSuccessModal, setShowUnlinkSuccessModal] = useState(false);
|
||||||
|
|
||||||
const username = extractUsernameFromToken() ?? '';
|
const username = extractUsernameFromToken() ?? "";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isUserAuthenticated()) {
|
if (!isUserAuthenticated()) {
|
||||||
navigate('/login/client', { replace: true });
|
navigate("/login/client", { replace: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Statut Telegram
|
// Statut Telegram
|
||||||
getTelegramStatus().then((s) => { setTgLinked(s.linked); setTgEnabled(s.enabled); });
|
getTelegramStatus().then((s) => {
|
||||||
|
setTgLinked(s.linked);
|
||||||
|
setTgEnabled(s.enabled);
|
||||||
|
});
|
||||||
|
|
||||||
// Statut 2FA
|
// Statut 2FA
|
||||||
Promise.all([get2FAStatus(), getPublicSettings()]).then(([status, pub]) => {
|
Promise.all([get2FAStatus(), getPublicSettings()]).then(
|
||||||
|
([status, pub]) => {
|
||||||
setTwoFAEnabled(status.two_fa_enabled);
|
setTwoFAEnabled(status.two_fa_enabled);
|
||||||
setTwoFAAdminEnabled(pub.two_fa_enabled);
|
setTwoFAAdminEnabled(pub.two_fa_enabled);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Charger depuis backend
|
// Charger depuis backend
|
||||||
getMyProfile().then((res) => {
|
getMyProfile().then((res) => {
|
||||||
if (res.success && res.client) {
|
if (res.success && res.client) {
|
||||||
setNom(res.client.nom ?? '');
|
setNom(res.client.nom ?? "");
|
||||||
setPrenom(res.client.prenom ?? '');
|
setPrenom(res.client.prenom ?? "");
|
||||||
setTelephone(res.client.telephone ?? '');
|
setTelephone(res.client.telephone ?? "");
|
||||||
// Initialiser le téléphone par défaut si pas encore défini
|
// 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);
|
setDefaultPhone(res.client.telephone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,16 +122,22 @@ export default function ProfilePage() {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const showSuccess = (title: string, msg: string) => {
|
const showSuccess = (title: string, msg: string) => {
|
||||||
setSuccessTitle(title); setSuccessMsg(msg); setShowSuccessModal(true);
|
setSuccessTitle(title);
|
||||||
|
setSuccessMsg(msg);
|
||||||
|
setShowSuccessModal(true);
|
||||||
};
|
};
|
||||||
const showError = (msg: string) => {
|
const showError = (msg: string) => {
|
||||||
setErrorMsg(msg); setShowErrorModal(true);
|
setErrorMsg(msg);
|
||||||
|
setShowErrorModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveAddress = () => {
|
const saveAddress = () => {
|
||||||
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
|
localStorage.setItem(STORAGE_ADDRESS, defaultAddress.trim());
|
||||||
setShowConfirmAddressModal(false);
|
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 = () => {
|
const saveLocal = () => {
|
||||||
@@ -102,7 +145,10 @@ export default function ProfilePage() {
|
|||||||
localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
|
localStorage.setItem(STORAGE_PHONE, defaultPhone.trim());
|
||||||
localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
|
localStorage.setItem(STORAGE_SIGNAL, signalPseudo.trim());
|
||||||
setShowSaveModal(false);
|
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 () => {
|
const handleLinkTelegram = async () => {
|
||||||
@@ -110,10 +156,10 @@ export default function ProfilePage() {
|
|||||||
const res = await generateTelegramLinkToken();
|
const res = await generateTelegramLinkToken();
|
||||||
setTgLoading(false);
|
setTgLoading(false);
|
||||||
if (res.error || !res.link_url) {
|
if (res.error || !res.link_url) {
|
||||||
showError(res.error || 'Service Telegram non disponible');
|
showError(res.error || "Service Telegram non disponible");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.open(res.link_url, '_blank');
|
window.open(res.link_url, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUnlinkTelegram = () => {
|
const handleUnlinkTelegram = () => {
|
||||||
@@ -135,21 +181,35 @@ export default function ProfilePage() {
|
|||||||
setTwoFALoading(false);
|
setTwoFALoading(false);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setTwoFAEnabled(newVal);
|
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 {
|
} else {
|
||||||
showError(res.error || 'Erreur lors de la modification');
|
showError(res.error || "Erreur lors de la modification");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveContact = async () => {
|
const saveContact = async () => {
|
||||||
setShowConfirmContactModal(false);
|
setShowConfirmContactModal(false);
|
||||||
setSavingContact(true);
|
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);
|
setSavingContact(false);
|
||||||
if (res.success) {
|
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 {
|
} 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 />
|
<Navbar />
|
||||||
<div className="profile-container">
|
<div className="profile-container">
|
||||||
<div className="profile-loading"><div className="spinner" /></div>
|
<div className="profile-loading">
|
||||||
|
<div className="spinner" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -174,14 +236,17 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="profile-title">Mon Profil</h1>
|
<h1 className="profile-title">Mon Profil</h1>
|
||||||
<p className="profile-username">@{username}</p>
|
<p className="profile-username">{username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section compte */}
|
{/* Section compte */}
|
||||||
<div className="profile-card">
|
<div className="profile-card">
|
||||||
<h2 className="profile-card-title">
|
<h2 className="profile-card-title">
|
||||||
<FontAwesomeIcon icon={faUser} className="profile-card-icon" />
|
<FontAwesomeIcon
|
||||||
|
icon={faUser}
|
||||||
|
className="profile-card-icon"
|
||||||
|
/>
|
||||||
Mon compte
|
Mon compte
|
||||||
</h2>
|
</h2>
|
||||||
<div className="profile-fields">
|
<div className="profile-fields">
|
||||||
@@ -215,23 +280,38 @@ export default function ProfilePage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="profile-btn" onClick={() => setShowConfirmContactModal(true)} disabled={savingContact}>
|
<button
|
||||||
|
className="profile-btn"
|
||||||
|
onClick={() => setShowConfirmContactModal(true)}
|
||||||
|
disabled={savingContact}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faSave} />
|
<FontAwesomeIcon icon={faSave} />
|
||||||
{savingContact ? ' Enregistrement...' : ' Enregistrer le compte'}
|
{savingContact
|
||||||
|
? " Enregistrement..."
|
||||||
|
: " Enregistrer le compte"}
|
||||||
</button>
|
</button>
|
||||||
<button className="profile-btn profile-btn--secondary" onClick={() => navigate('/user/change-password')} style={{ marginTop: '0.6rem' }}>
|
<button
|
||||||
<FontAwesomeIcon icon={faLock} /> Changer le mot de passe
|
className="profile-btn profile-btn--secondary"
|
||||||
|
onClick={() => navigate("/user/change-password")}
|
||||||
|
style={{ marginTop: "0.6rem" }}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLock} /> Changer le mot de
|
||||||
|
passe
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Section adresse par défaut */}
|
{/* Section adresse par défaut */}
|
||||||
<div className="profile-card">
|
<div className="profile-card">
|
||||||
<h2 className="profile-card-title">
|
<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
|
Adresse par défaut
|
||||||
</h2>
|
</h2>
|
||||||
<p className="profile-hint">
|
<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>
|
</p>
|
||||||
<div className="profile-group">
|
<div className="profile-group">
|
||||||
<label>Adresse</label>
|
<label>Adresse</label>
|
||||||
@@ -242,7 +322,11 @@ export default function ProfilePage() {
|
|||||||
placeholder="Numéro, rue, ville, code postal"
|
placeholder="Numéro, rue, ville, code postal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<FontAwesomeIcon icon={faSave} /> Enregistrer l'adresse
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -250,11 +334,15 @@ export default function ProfilePage() {
|
|||||||
{/* Section contact commande */}
|
{/* Section contact commande */}
|
||||||
<div className="profile-card">
|
<div className="profile-card">
|
||||||
<h2 className="profile-card-title">
|
<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
|
Contact livraison
|
||||||
</h2>
|
</h2>
|
||||||
<p className="profile-hint">
|
<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>
|
</p>
|
||||||
<div className="profile-group">
|
<div className="profile-group">
|
||||||
<label>Téléphone par défaut</label>
|
<label>Téléphone par défaut</label>
|
||||||
@@ -265,9 +353,15 @@ export default function ProfilePage() {
|
|||||||
placeholder="+33 6 12 34 56 78"
|
placeholder="+33 6 12 34 56 78"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="profile-group" style={{ marginTop: '1rem' }}>
|
<div
|
||||||
|
className="profile-group"
|
||||||
|
style={{ marginTop: "1rem" }}
|
||||||
|
>
|
||||||
<label>
|
<label>
|
||||||
<FontAwesomeIcon icon={faCommentDots} style={{ marginRight: '0.4rem' }} />
|
<FontAwesomeIcon
|
||||||
|
icon={faCommentDots}
|
||||||
|
style={{ marginRight: "0.4rem" }}
|
||||||
|
/>
|
||||||
Pseudo Signal (optionnel)
|
Pseudo Signal (optionnel)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -277,33 +371,52 @@ export default function ProfilePage() {
|
|||||||
placeholder="@votre.pseudo.signal"
|
placeholder="@votre.pseudo.signal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button className="profile-btn profile-btn--secondary" onClick={() => setShowSaveModal(true)}>
|
<button
|
||||||
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos par défaut
|
className="profile-btn profile-btn--secondary"
|
||||||
|
onClick={() => setShowSaveModal(true)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSave} /> Enregistrer les infos
|
||||||
|
par défaut
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* Section Telegram */}
|
{/* Section Telegram */}
|
||||||
{tgEnabled && (
|
{tgEnabled && (
|
||||||
<div className="profile-card">
|
<div className="profile-card">
|
||||||
<h2 className="profile-card-title">
|
<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
|
Notifications Telegram
|
||||||
</h2>
|
</h2>
|
||||||
<p className="profile-hint">
|
<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>
|
</p>
|
||||||
{tgLinked ? (
|
{tgLinked ? (
|
||||||
<div className="profile-telegram-linked">
|
<div className="profile-telegram-linked">
|
||||||
<span className="profile-telegram-status">
|
<span className="profile-telegram-status">
|
||||||
<FontAwesomeIcon icon={faCheckCircle} /> Compte Telegram lié
|
<FontAwesomeIcon icon={faCheckCircle} />{" "}
|
||||||
|
Compte Telegram lié
|
||||||
</span>
|
</span>
|
||||||
<button className="profile-btn profile-btn--danger" onClick={handleUnlinkTelegram}>
|
<button
|
||||||
<FontAwesomeIcon icon={faUnlink} /> Délier Telegram
|
className="profile-btn profile-btn--danger"
|
||||||
|
onClick={handleUnlinkTelegram}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faUnlink} /> Délier
|
||||||
|
Telegram
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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} />
|
<FontAwesomeIcon icon={faPaperPlane} />
|
||||||
{tgLoading ? ' Génération du lien...' : ' Lier mon compte Telegram'}
|
{tgLoading
|
||||||
|
? " Génération du lien..."
|
||||||
|
: " Lier mon compte Telegram"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -313,18 +426,26 @@ export default function ProfilePage() {
|
|||||||
{twoFAAdminEnabled && tgLinked && (
|
{twoFAAdminEnabled && tgLinked && (
|
||||||
<div className="profile-card">
|
<div className="profile-card">
|
||||||
<h2 className="profile-card-title">
|
<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)
|
Double authentification (2FA)
|
||||||
</h2>
|
</h2>
|
||||||
<p className="profile-hint">
|
<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>
|
</p>
|
||||||
<div className="profile-2fa-row">
|
<div className="profile-2fa-row">
|
||||||
<div className="profile-2fa-label">
|
<div className="profile-2fa-label">
|
||||||
<span>{twoFAEnabled ? 'Activée' : 'Désactivée'}</span>
|
<span>
|
||||||
|
{twoFAEnabled ? "Activée" : "Désactivée"}
|
||||||
|
</span>
|
||||||
{twoFAEnabled && (
|
{twoFAEnabled && (
|
||||||
<span className="profile-2fa-badge">
|
<span className="profile-2fa-badge">
|
||||||
<FontAwesomeIcon icon={faCheckCircle} /> Protection active
|
<FontAwesomeIcon icon={faCheckCircle} />{" "}
|
||||||
|
Protection active
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -334,7 +455,7 @@ export default function ProfilePage() {
|
|||||||
<button
|
<button
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked={twoFAEnabled}
|
aria-checked={twoFAEnabled}
|
||||||
className={`profile-toggle ${twoFAEnabled ? 'profile-toggle--on' : ''}`}
|
className={`profile-toggle ${twoFAEnabled ? "profile-toggle--on" : ""}`}
|
||||||
onClick={handleToggle2FA}
|
onClick={handleToggle2FA}
|
||||||
aria-label="Activer ou désactiver la double authentification"
|
aria-label="Activer ou désactiver la double authentification"
|
||||||
>
|
>
|
||||||
@@ -347,24 +468,44 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showSaveModal && (
|
{showSaveModal && (
|
||||||
<div className="profile-modal-overlay" onClick={() => setShowSaveModal(false)}>
|
<div
|
||||||
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
|
className="profile-modal-overlay"
|
||||||
<button className="profile-modal-close" onClick={() => setShowSaveModal(false)}>
|
onClick={() => setShowSaveModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="profile-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-modal-close"
|
||||||
|
onClick={() => setShowSaveModal(false)}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-modal-icon">
|
<div className="profile-modal-icon">
|
||||||
<FontAwesomeIcon icon={faSave} />
|
<FontAwesomeIcon icon={faSave} />
|
||||||
</div>
|
</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">
|
<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>
|
</p>
|
||||||
<div className="profile-modal-actions">
|
<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
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={saveLocal}>
|
<button
|
||||||
<FontAwesomeIcon icon={faCheckCircle} /> Confirmer
|
className="profile-modal-btn profile-modal-btn--confirm"
|
||||||
|
onClick={saveLocal}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} />{" "}
|
||||||
|
Confirmer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -372,24 +513,45 @@ export default function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showConfirmAddressModal && (
|
{showConfirmAddressModal && (
|
||||||
<div className="profile-modal-overlay" onClick={() => setShowConfirmAddressModal(false)}>
|
<div
|
||||||
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
|
className="profile-modal-overlay"
|
||||||
<button className="profile-modal-close" onClick={() => setShowConfirmAddressModal(false)}>
|
onClick={() => setShowConfirmAddressModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="profile-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-modal-close"
|
||||||
|
onClick={() => setShowConfirmAddressModal(false)}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-modal-icon">
|
<div className="profile-modal-icon">
|
||||||
<FontAwesomeIcon icon={faMapMarkerAlt} />
|
<FontAwesomeIcon icon={faMapMarkerAlt} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="profile-modal-title">Enregistrer l'adresse ?</h3>
|
<h3 className="profile-modal-title">
|
||||||
|
Enregistrer l'adresse ?
|
||||||
|
</h3>
|
||||||
<p className="profile-modal-body">
|
<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>
|
</p>
|
||||||
<div className="profile-modal-actions">
|
<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
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={saveAddress}>
|
<button
|
||||||
<FontAwesomeIcon icon={faCheckCircle} /> Confirmer
|
className="profile-modal-btn profile-modal-btn--confirm"
|
||||||
|
onClick={saveAddress}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} />{" "}
|
||||||
|
Confirmer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -397,24 +559,48 @@ export default function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showConfirmContactModal && (
|
{showConfirmContactModal && (
|
||||||
<div className="profile-modal-overlay" onClick={() => setShowConfirmContactModal(false)}>
|
<div
|
||||||
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
|
className="profile-modal-overlay"
|
||||||
<button className="profile-modal-close" onClick={() => setShowConfirmContactModal(false)}>
|
onClick={() => setShowConfirmContactModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="profile-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-modal-close"
|
||||||
|
onClick={() => setShowConfirmContactModal(false)}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-modal-icon">
|
<div className="profile-modal-icon">
|
||||||
<FontAwesomeIcon icon={faUser} />
|
<FontAwesomeIcon icon={faUser} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="profile-modal-title">Enregistrer le compte ?</h3>
|
<h3 className="profile-modal-title">
|
||||||
|
Enregistrer le compte ?
|
||||||
|
</h3>
|
||||||
<p className="profile-modal-body">
|
<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>
|
</p>
|
||||||
<div className="profile-modal-actions">
|
<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
|
Annuler
|
||||||
</button>
|
</button>
|
||||||
<button className="profile-modal-btn profile-modal-btn--confirm" onClick={saveContact} disabled={savingContact}>
|
<button
|
||||||
<FontAwesomeIcon icon={faCheckCircle} /> {savingContact ? 'Enregistrement...' : 'Confirmer'}
|
className="profile-modal-btn profile-modal-btn--confirm"
|
||||||
|
onClick={saveContact}
|
||||||
|
disabled={savingContact}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCheckCircle} />{" "}
|
||||||
|
{savingContact
|
||||||
|
? "Enregistrement..."
|
||||||
|
: "Confirmer"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -422,23 +608,41 @@ export default function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showUnlinkModal && (
|
{showUnlinkModal && (
|
||||||
<div className="profile-modal-overlay" onClick={() => setShowUnlinkModal(false)}>
|
<div
|
||||||
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
|
className="profile-modal-overlay"
|
||||||
<button className="profile-modal-close" onClick={() => setShowUnlinkModal(false)}>
|
onClick={() => setShowUnlinkModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="profile-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-modal-close"
|
||||||
|
onClick={() => setShowUnlinkModal(false)}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-modal-icon profile-modal-icon--danger">
|
<div className="profile-modal-icon profile-modal-icon--danger">
|
||||||
<FontAwesomeIcon icon={faUnlink} />
|
<FontAwesomeIcon icon={faUnlink} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="profile-modal-title">Délier Telegram ?</h3>
|
<h3 className="profile-modal-title">
|
||||||
|
Délier Telegram ?
|
||||||
|
</h3>
|
||||||
<p className="profile-modal-body">
|
<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>
|
</p>
|
||||||
<div className="profile-modal-actions">
|
<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
|
Annuler
|
||||||
</button>
|
</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
|
<FontAwesomeIcon icon={faUnlink} /> Délier
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -448,9 +652,18 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
{/* Modal succès */}
|
{/* Modal succès */}
|
||||||
{showSuccessModal && (
|
{showSuccessModal && (
|
||||||
<div className="profile-modal-overlay" onClick={() => setShowSuccessModal(false)}>
|
<div
|
||||||
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
|
className="profile-modal-overlay"
|
||||||
<button className="profile-modal-close" onClick={() => setShowSuccessModal(false)}>
|
onClick={() => setShowSuccessModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="profile-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-modal-close"
|
||||||
|
onClick={() => setShowSuccessModal(false)}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-modal-icon profile-modal-icon--success">
|
<div className="profile-modal-icon profile-modal-icon--success">
|
||||||
@@ -459,7 +672,10 @@ export default function ProfilePage() {
|
|||||||
<h3 className="profile-modal-title">{successTitle}</h3>
|
<h3 className="profile-modal-title">{successTitle}</h3>
|
||||||
<p className="profile-modal-body">{successMsg}</p>
|
<p className="profile-modal-body">{successMsg}</p>
|
||||||
<div className="profile-modal-actions">
|
<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
|
Fermer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -469,18 +685,32 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
{/* Modal erreur */}
|
{/* Modal erreur */}
|
||||||
{showErrorModal && (
|
{showErrorModal && (
|
||||||
<div className="profile-modal-overlay" onClick={() => setShowErrorModal(false)}>
|
<div
|
||||||
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
|
className="profile-modal-overlay"
|
||||||
<button className="profile-modal-close" onClick={() => setShowErrorModal(false)}>
|
onClick={() => setShowErrorModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="profile-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-modal-close"
|
||||||
|
onClick={() => setShowErrorModal(false)}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-modal-icon profile-modal-icon--danger">
|
<div className="profile-modal-icon profile-modal-icon--danger">
|
||||||
<FontAwesomeIcon icon={faExclamationTriangle} />
|
<FontAwesomeIcon icon={faExclamationTriangle} />
|
||||||
</div>
|
</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>
|
<p className="profile-modal-body">{errorMsg}</p>
|
||||||
<div className="profile-modal-actions">
|
<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
|
Fermer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -489,9 +719,18 @@ export default function ProfilePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{showUnlinkSuccessModal && (
|
{showUnlinkSuccessModal && (
|
||||||
<div className="profile-modal-overlay" onClick={() => setShowUnlinkSuccessModal(false)}>
|
<div
|
||||||
<div className="profile-modal" onClick={(e) => e.stopPropagation()}>
|
className="profile-modal-overlay"
|
||||||
<button className="profile-modal-close" onClick={() => setShowUnlinkSuccessModal(false)}>
|
onClick={() => setShowUnlinkSuccessModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="profile-modal"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="profile-modal-close"
|
||||||
|
onClick={() => setShowUnlinkSuccessModal(false)}
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faTimes} />
|
<FontAwesomeIcon icon={faTimes} />
|
||||||
</button>
|
</button>
|
||||||
<div className="profile-modal-icon profile-modal-icon--success">
|
<div className="profile-modal-icon profile-modal-icon--success">
|
||||||
@@ -499,10 +738,14 @@ export default function ProfilePage() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="profile-modal-title">Compte délié</h3>
|
<h3 className="profile-modal-title">Compte délié</h3>
|
||||||
<p className="profile-modal-body">
|
<p className="profile-modal-body">
|
||||||
Votre compte Telegram a été délié avec succès. Vous ne recevrez plus de notifications via Telegram.
|
Votre compte Telegram a été délié avec succès. Vous
|
||||||
|
ne recevrez plus de notifications via Telegram.
|
||||||
</p>
|
</p>
|
||||||
<div className="profile-modal-actions">
|
<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
|
<FontAwesomeIcon icon={faCheckCircle} /> Fermer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user