Files
goodgo-platform/apps/web/app/[locale]/(public)/layout.tsx
Ho Ngoc Hai b93c62372d
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 12s
Deploy / Build Web Image (push) Failing after 22s
Deploy / Build AI Services Image (push) Failing after 10s
E2E Tests / Playwright E2E (push) Failing after 21s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 4s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m32s
Deploy / Build API Image (push) Failing after 42s
Deploy / Deploy to Staging (push) Has been cancelled
Deploy / Smoke Test Staging (push) Has been cancelled
Deploy / Rollback Staging (push) Has been cancelled
Deploy / Smoke Test Production (push) Has been cancelled
Deploy / Rollback Production (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled
Security Scanning / Trivy Scan — Web Image (push) Has been cancelled
Security Scanning / Trivy Scan — AI Services Image (push) Has been cancelled
Security Scanning / Trivy Filesystem Scan (push) Has been cancelled
Security Scanning / Security Gate (push) Has been cancelled
Security Scanning / Trivy Scan — API Image (push) Has been cancelled
fix(web): tighten PublicLayout header on mobile
The header on <sm viewports was crowded for logged-in users: the
desktop Dashboard button, NotificationBell, and hamburger toggle all
rendered on the same row next to the LanguageSwitcher, which pushed
content to the edges on 375px screens and duplicated the Dashboard CTA
(the mobile hamburger menu already exposes it).

- Hide the Dashboard button in the header behind `hidden sm:inline-flex`
  — mobile users reach it through the hamburger menu's full-width CTA.
- Hide NotificationBell behind `hidden sm:block` for the same reason;
  the bell needs enough room for its popover which doesn't fit well on
  mobile widths.
- Switch the right-side container from `space-x-2` to `gap-1 sm:gap-2`
  so icon-only buttons don't touch on narrow screens.
- Clamp the `user.fullName` inline label with `max-w-[12rem] truncate`
  to stop extremely long names pushing the header out of shape on
  borderline-sm widths.
- Mark the hamburger button as `shrink-0` + `type="button"` +
  `aria-expanded`, and annotate the `min-w-0` on the right group so
  flex children can truncate correctly.

Verified at 375×812: header now shows logo | language | hamburger only;
tapping the hamburger opens the drawer which carries bell-adjacent
items and the Dashboard CTA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:19:19 +07:00

222 lines
9.1 KiB
TypeScript

'use client';
import { Menu, 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 { Button } from '@/components/ui/button';
import { LanguageSwitcher } from '@/components/ui/language-switcher';
import { Link } from '@/i18n/navigation';
import { useAuthStore } from '@/lib/auth-store';
import { cn } from '@/lib/utils';
export default function PublicLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { user } = useAuthStore();
const t = useTranslations();
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const navLinks = [
{
href: '/' as const,
label: t('nav.home'),
isActive: pathname === '/' || !!pathname.match(/^\/(vi|en)\/?$/),
},
{
href: '/search' as const,
label: t('nav.search'),
isActive: pathname.includes('/search'),
},
{
href: '/du-an' as const,
label: t('nav.projects'),
isActive: pathname.includes('/du-an'),
},
{
href: '/khu-cong-nghiep' as const,
label: t('nav.industrialParks'),
isActive: pathname.includes('/khu-cong-nghiep'),
},
{
href: '/chuyen-nhuong' as const,
label: t('nav.transfer'),
isActive: pathname.includes('/chuyen-nhuong'),
},
{
href: '/pricing' as const,
label: t('nav.pricing'),
isActive: pathname.includes('/pricing'),
},
];
return (
<div className="min-h-screen bg-background">
<header
role="banner"
className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
>
<div className="mx-auto flex h-14 max-w-7xl items-center px-4">
<Link href="/" className="mr-6 flex items-center space-x-2">
<span className="text-lg font-bold text-primary">{t('common.goodgo')}</span>
</Link>
{/* Desktop nav */}
<nav aria-label={t('nav.mainNav')} className="hidden items-center space-x-1 sm:flex">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className={cn(
'rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
link.isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground',
)}
>
{link.label}
</Link>
))}
</nav>
<div className="ml-auto flex min-w-0 items-center gap-1 sm:gap-2">
<LanguageSwitcher />
{user ? (
<>
{/* Bell only on sm+ to avoid overcrowding mobile header */}
<div className="hidden sm:block">
<NotificationBell />
</div>
<span className="hidden max-w-[12rem] truncate text-sm text-muted-foreground sm:inline">
{user.fullName}
</span>
{/* Dashboard button is desktop-only; mobile users reach it via the hamburger menu */}
<Link href="/dashboard" className="hidden sm:inline-flex">
<Button size="sm">{t('common.dashboard')}</Button>
</Link>
</>
) : (
<>
<Link href="/login" className="hidden sm:inline-flex">
<Button variant="ghost" size="sm">
{t('common.login')}
</Button>
</Link>
<Link href="/register" className="hidden sm:inline-flex">
<Button size="sm">{t('common.register')}</Button>
</Link>
</>
)}
{/* Mobile menu toggle */}
<button
type="button"
aria-label={mobileMenuOpen ? t('nav.closeMenu') : t('nav.openMenu')}
aria-expanded={mobileMenuOpen}
className="inline-flex shrink-0 items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<nav
aria-label={t('nav.mainNav')}
className="border-t px-4 pb-4 pt-2 sm:hidden"
>
<div className="space-y-1">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileMenuOpen(false)}
className={cn(
'block rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
link.isActive
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
)}
>
{link.label}
</Link>
))}
</div>
<div className="mt-3 space-y-2 border-t pt-3">
{user ? (
<>
<p className="px-3 text-sm text-muted-foreground">{user.fullName}</p>
<Link href="/dashboard" onClick={() => setMobileMenuOpen(false)}>
<Button size="sm" className="w-full">{t('common.dashboard')}</Button>
</Link>
</>
) : (
<div className="flex gap-2">
<Link href="/login" className="flex-1" onClick={() => setMobileMenuOpen(false)}>
<Button variant="ghost" size="sm" className="w-full">
{t('common.login')}
</Button>
</Link>
<Link href="/register" className="flex-1" onClick={() => setMobileMenuOpen(false)}>
<Button size="sm" className="w-full">{t('common.register')}</Button>
</Link>
</div>
)}
</div>
</nav>
)}
</header>
<main id="main-content" role="main">
{children}
</main>
<CompareFloatingBar />
<footer role="contentinfo" className="border-t bg-muted/40">
<div className="mx-auto max-w-7xl px-4 py-8">
<div className="grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
<div>
<h3 className="mb-3 text-sm font-semibold">{t('common.goodgo')}</h3>
<p className="text-sm text-muted-foreground">
{t('footer.description')}
</p>
</div>
<div>
<h3 className="mb-3 text-sm font-semibold">{t('footer.propertyTypes')}</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li><Link href="/search?propertyType=APARTMENT" className="hover:text-foreground">{t('propertyTypes.APARTMENT')}</Link></li>
<li><Link href="/search?propertyType=HOUSE" className="hover:text-foreground">{t('propertyTypes.HOUSE')}</Link></li>
<li><Link href="/search?propertyType=VILLA" className="hover:text-foreground">{t('propertyTypes.VILLA')}</Link></li>
<li><Link href="/search?propertyType=LAND" className="hover:text-foreground">{t('propertyTypes.LAND')}</Link></li>
</ul>
</div>
<div>
<h3 className="mb-3 text-sm font-semibold">{t('footer.areas')}</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li><Link href="/search?city=Hồ Chí Minh" className="hover:text-foreground">TP. Hồ Chí Minh</Link></li>
<li><Link href="/search?city=Hà Nội" className="hover:text-foreground"> Nội</Link></li>
<li><Link href="/search?city=Đà Nẵng" className="hover:text-foreground">Đà Nẵng</Link></li>
<li><Link href="/search?city=Nha Trang" className="hover:text-foreground">Nha Trang</Link></li>
</ul>
</div>
<div>
<h3 className="mb-3 text-sm font-semibold">{t('footer.support')}</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
<li><Link href="/pricing" className="hover:text-foreground">{t('nav.pricing')}</Link></li>
<li><Link href="/login" className="hover:text-foreground">{t('common.login')}</Link></li>
<li><Link href="/register" className="hover:text-foreground">{t('common.register')}</Link></li>
</ul>
</div>
</div>
<div className="mt-8 border-t pt-4 text-center text-sm text-muted-foreground">
{t('common.allRightsReserved')}
</div>
</div>
</footer>
</div>
);
}