Files
goodgo-platform/apps/web/app/[locale]/(dashboard)/layout.tsx
Ho Ngoc Hai 759052a71f fix(web): update dashboard pages, layouts, and listing forms
Update 12 page/layout files across auth, dashboard, listings, and search
routes to improve type safety, fix component imports, and align with
latest API changes.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 01:39:59 +07:00

174 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { LogOut, Menu, X } from 'lucide-react';
import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useTheme } from '@/components/providers/theme-provider';
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 DashboardLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const { user, logout } = useAuthStore();
const { theme, toggleTheme } = useTheme();
const t = useTranslations();
const [sidebarOpen, setSidebarOpen] = useState(false);
const navItems = [
{ href: '/dashboard' as const, label: t('dashboard.title'), icon: '🏠' },
{ href: '/listings' as const, label: t('dashboard.listings'), icon: '📋' },
{ href: '/listings/new' as const, label: t('dashboard.createListing'), icon: '' },
{ href: '/analytics' as const, label: t('dashboard.analytics'), icon: '📊' },
{ href: '/dashboard/saved-searches' as const, label: t('dashboard.savedSearches'), icon: '🔖' },
{ href: '/dashboard/valuation' as const, label: t('dashboard.aiValuation'), icon: '🤖' },
{ href: '/dashboard/profile' as const, label: t('dashboard.profile'), icon: '👤' },
{ href: '/dashboard/subscription' as const, label: t('dashboard.subscription'), icon: '💎' },
{ href: '/dashboard/payments' as const, label: t('dashboard.payments'), icon: '💳' },
];
return (
<div className="min-h-screen bg-background">
{/* Mobile overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 z-40 bg-black/50 md:hidden"
onClick={() => setSidebarOpen(false)}
aria-hidden="true"
/>
)}
{/* Mobile sidebar */}
<aside
role="navigation"
aria-label={t('nav.dashboardNav')}
className={cn(
'fixed inset-y-0 left-0 z-50 w-64 border-r bg-card transition-transform md:hidden',
sidebarOpen ? 'translate-x-0' : '-translate-x-full',
)}
>
<div className="flex h-14 items-center border-b px-4">
<Link href="/" className="flex items-center space-x-2">
<span className="text-lg font-bold text-primary">{t('common.goodgo')}</span>
</Link>
<button
aria-label={t('nav.closeMenu')}
className="ml-auto"
onClick={() => setSidebarOpen(false)}
>
<X className="h-5 w-5" />
</button>
</div>
<nav className="flex flex-col gap-1 p-3">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
onClick={() => setSidebarOpen(false)}
className={cn(
'flex items-center gap-3 rounded-md px-3 py-2.5 text-sm font-medium transition-colors',
pathname === item.href || (item.href !== '/dashboard' && pathname.startsWith(item.href))
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
)}
>
<span aria-hidden="true">{item.icon}</span>
{item.label}
</Link>
))}
</nav>
<div className="mt-auto border-t p-3">
{user && (
<p className="mb-2 truncate px-3 text-xs text-muted-foreground">{user.fullName}</p>
)}
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2"
onClick={() => logout()}
>
<LogOut className="h-4 w-4" aria-hidden="true" />
{t('common.logout')}
</Button>
</div>
</aside>
<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">
{/* Mobile hamburger */}
<button
aria-label={t('nav.openMenu')}
className="mr-3 inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground md:hidden"
onClick={() => setSidebarOpen(true)}
>
<Menu className="h-5 w-5" />
</button>
<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.dashboardNav')} className="hidden items-center space-x-1 md:flex">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
aria-label={item.label}
className={cn(
'rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground',
pathname === item.href || (item.href !== '/dashboard' && pathname.startsWith(item.href))
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground',
)}
>
<span className="mr-1.5" aria-hidden="true">{item.icon}</span>
<span className="hidden lg:inline">{item.label}</span>
</Link>
))}
</nav>
<div className="ml-auto flex items-center space-x-2">
{user && (
<span className="hidden text-sm text-muted-foreground sm:inline">
{user.fullName}
</span>
)}
<LanguageSwitcher />
<Button
variant="ghost"
size="sm"
onClick={toggleTheme}
aria-label={theme === 'light' ? t('dashboard.darkMode') : t('dashboard.lightMode')}
className="h-9 w-9 p-0"
>
{theme === 'light' ? (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
) : (
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
)}
</Button>
<Button variant="ghost" size="sm" className="hidden md:inline-flex" onClick={() => logout()}>
{t('common.logout')}
</Button>
</div>
</div>
</header>
<main id="main-content" role="main" className="mx-auto max-w-7xl px-4 py-6">{children}</main>
</div>
);
}