From d5915b86554f77294b72c38b0c9b285b5e507e46 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 19 Apr 2026 09:24:15 +0700 Subject: [PATCH] feat(web): richer auth block in PublicLayout mobile menu + role-aware dashboard link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mobile menu's logged-in footer previously showed just the user's full name and a generic "Bảng điều khiển" button that always pointed at /dashboard, even for ADMIN users whose real console lives at /admin. The desktop header had the same ADMIN mis-route. Mobile menu footer — now a compact account card: - Avatar (avatarUrl) or initials fallback in a primary-tinted circle (getInitials handles single-word and multi-word names). - Full name (truncated). - Secondary line: email if present, otherwise phone. - Role badge via ROLE_LABELS (Quản trị viên / Đại lý / Người bán / Người mua) — skipped when the role string isn't in the map. - Primary CTA: routes to /admin for ADMIN, /dashboard otherwise. Button label flips to "Admin" vs "Bảng điều khiển" accordingly. - Secondary CTA: /dashboard/profile with UserIcon. - Tertiary: destructive-styled Đăng xuất button that calls the auth-store logout() action then router.push('/'). Desktop header: Dashboard button on the right now uses the same dashboardHref + label computation so ADMIN users land on /admin. i18n: added common.profile in both vi.json and en.json. Verified at 375×812 (mobile preset): card + 3 buttons render within viewport, initials bubble shows "HH" in red, role badge reads "Quản trị viên". Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/web/app/[locale]/(public)/layout.tsx | 105 ++++++++++++++++++++-- apps/web/messages/en.json | 1 + apps/web/messages/vi.json | 1 + 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/apps/web/app/[locale]/(public)/layout.tsx b/apps/web/app/[locale]/(public)/layout.tsx index 9841034..a457714 100644 --- a/apps/web/app/[locale]/(public)/layout.tsx +++ b/apps/web/app/[locale]/(public)/layout.tsx @@ -1,23 +1,50 @@ 'use client'; -import { Menu, X } from 'lucide-react'; +import { LogOut, Menu, User as UserIcon, X } from 'lucide-react'; import { usePathname } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { useState } from 'react'; import { CompareFloatingBar } from '@/components/comparison/compare-floating-bar'; import { NotificationBell } from '@/components/notifications/notification-bell'; +import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { LanguageSwitcher } from '@/components/ui/language-switcher'; -import { Link } from '@/i18n/navigation'; +import { Link, useRouter } from '@/i18n/navigation'; import { useAuthStore } from '@/lib/auth-store'; import { cn } from '@/lib/utils'; +/** Map backend role strings to Vietnamese labels shown in the nav. */ +const ROLE_LABELS: Record = { + ADMIN: 'Quản trị viên', + AGENT: 'Đại lý', + SELLER: 'Người bán', + BUYER: 'Người mua', +}; + +/** First+last initials fallback for when avatarUrl is null. */ +function getInitials(fullName: string): string { + const parts = fullName.trim().split(/\s+/).filter(Boolean); + if (parts.length === 0) return '?'; + if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase(); + return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase(); +} + export default function PublicLayout({ children }: { children: React.ReactNode }) { const pathname = usePathname(); - const { user } = useAuthStore(); + const router = useRouter(); + const { user, logout } = useAuthStore(); const t = useTranslations(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + /** Route the Dashboard CTA to the correct surface based on role. */ + const dashboardHref = user?.role === 'ADMIN' ? '/admin' : '/dashboard'; + + const handleLogout = async () => { + setMobileMenuOpen(false); + await logout(); + router.push('/'); + }; + const navLinks = [ { href: '/' as const, @@ -92,8 +119,10 @@ export default function PublicLayout({ children }: { children: React.ReactNode } {user.fullName} {/* Dashboard button is desktop-only; mobile users reach it via the hamburger menu */} - - + + ) : ( @@ -147,10 +176,70 @@ export default function PublicLayout({ children }: { children: React.ReactNode }
{user ? ( <> -

{user.fullName}

- setMobileMenuOpen(false)}> - + {/* User card: avatar (or initials) + name + email/phone + role */} +
+ {user.avatarUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( + + )} +
+

{user.fullName}

+

+ {user.email ?? user.phone} +

+
+ {ROLE_LABELS[user.role] ? ( + + {ROLE_LABELS[user.role]} + + ) : null} +
+ + setMobileMenuOpen(false)} + className="block" + > + + + setMobileMenuOpen(false)} + className="block" + > + + + + ) : (
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 2eba8f7..596a593 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -17,6 +17,7 @@ "logout": "Logout", "admin": "Admin", "dashboard": "Dashboard", + "profile": "Profile", "errorCode": "Error code: {code}", "retriedCount": "Retried {count} times", "allRightsReserved": "© 2026 GoodGo. All rights reserved.", diff --git a/apps/web/messages/vi.json b/apps/web/messages/vi.json index 6ab09fd..8d4e5cb 100644 --- a/apps/web/messages/vi.json +++ b/apps/web/messages/vi.json @@ -17,6 +17,7 @@ "logout": "Đăng xuất", "admin": "Admin", "dashboard": "Bảng điều khiển", + "profile": "Hồ sơ", "errorCode": "Mã lỗi: {code}", "retriedCount": "Đã thử lại {count} lần", "allRightsReserved": "© 2026 GoodGo. Tất cả quyền được bảo lưu.",