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>
This commit is contained in:
Ho Ngoc Hai
2026-04-11 01:39:59 +07:00
parent a59bf8eda2
commit 759052a71f
12 changed files with 234 additions and 30 deletions

View File

@@ -1,6 +1,6 @@
export default function AuthLayout({ children }: { children: React.ReactNode }) { export default function AuthLayout({ children }: { children: React.ReactNode }) {
return ( return (
<main id="main-content" role="main" className="flex min-h-screen items-center justify-center bg-muted/40 px-4 py-12"> <main id="main-content" role="main" className="flex min-h-screen items-center justify-center bg-muted/40 px-4 py-6 md:py-12">
<div className="w-full max-w-md">{children}</div> <div className="w-full max-w-md">{children}</div>
</main> </main>
); );

View File

@@ -79,7 +79,7 @@ export default function KycPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-3xl font-bold">Xác minh danh tính (KYC)</h1> <h1 className="text-2xl font-bold sm:text-3xl">Xác minh danh tính (KYC)</h1>
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
Xác minh danh tính đ sử dụng đy đ tính năng của GoodGo Xác minh danh tính đ sử dụng đy đ tính năng của GoodGo
</p> </p>
@@ -131,7 +131,7 @@ export default function KycPage() {
</div> </div>
<span className="ml-2 hidden text-sm sm:inline">{s.title}</span> <span className="ml-2 hidden text-sm sm:inline">{s.title}</span>
{i < KYC_STEPS.length - 1 && ( {i < KYC_STEPS.length - 1 && (
<div className="mx-3 h-px w-8 bg-border sm:w-16" /> <div className="mx-1 h-px w-4 bg-border sm:mx-3 sm:w-16" />
)} )}
</div> </div>
))} ))}

View File

@@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { formatPrice, formatPricePerM2 } from '@/lib/currency'; import { formatPrice, formatPricePerM2 } from '@/lib/currency';
import { useMarketReport, useHeatmap } from '@/lib/hooks/use-analytics'; import { useMarketReport, useHeatmap } from '@/lib/hooks/use-analytics';
import { useListingsSearch } from '@/lib/hooks/use-listings'; import { useListingsSearch } from '@/lib/hooks/use-listings';
import { staticBlurDataURL } from '@/lib/image-blur';
const DistrictBarChart = dynamic( const DistrictBarChart = dynamic(
() => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart),
@@ -244,6 +245,8 @@ export default function DashboardPage() {
fill fill
sizes="64px" sizes="64px"
className="object-cover" className="object-cover"
placeholder="blur"
blurDataURL={staticBlurDataURL()}
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center text-xs text-muted-foreground"> <div className="flex h-full items-center justify-center text-xs text-muted-foreground">

View File

@@ -74,7 +74,7 @@ export default function ProfilePage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-3xl font-bold">Hồ nhân</h1> <h1 className="text-2xl font-bold sm:text-3xl">Hồ nhân</h1>
<p className="mt-2 text-muted-foreground">Quản thông tin tài khoản của bạn</p> <p className="mt-2 text-muted-foreground">Quản thông tin tài khoản của bạn</p>
</div> </div>
@@ -99,7 +99,7 @@ export default function ProfilePage() {
<div className="grid gap-6 lg:grid-cols-3"> <div className="grid gap-6 lg:grid-cols-3">
{/* Profile info */} {/* Profile info */}
<Card className="lg:col-span-2"> <Card className="lg:col-span-2">
<CardHeader className="flex flex-row items-center justify-between"> <CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div> <div>
<CardTitle className="text-lg">Thông tin nhân</CardTitle> <CardTitle className="text-lg">Thông tin nhân</CardTitle>
<CardDescription>Thông tin bản trên hồ của bạn</CardDescription> <CardDescription>Thông tin bản trên hồ của bạn</CardDescription>

View File

@@ -91,7 +91,7 @@ export default function SubscriptionPage() {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<h1 className="text-3xl font-bold">Gói dịch vụ</h1> <h1 className="text-2xl font-bold sm:text-3xl">Gói dịch vụ</h1>
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
Quản gói đăng theo dõi hạn mức sử dụng Quản gói đăng theo dõi hạn mức sử dụng
</p> </p>
@@ -112,10 +112,10 @@ export default function SubscriptionPage() {
</div> </div>
) : ( ) : (
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList> <TabsList className="w-full justify-start overflow-x-auto">
<TabsTrigger value="plan">Gói hiện tại</TabsTrigger> <TabsTrigger value="plan" className="min-w-fit">Gói hiện tại</TabsTrigger>
<TabsTrigger value="plans">So sánh gói</TabsTrigger> <TabsTrigger value="plans" className="min-w-fit">So sánh gói</TabsTrigger>
<TabsTrigger value="history">Lịch sử thanh toán</TabsTrigger> <TabsTrigger value="history" className="min-w-fit">Lịch sử thanh toán</TabsTrigger>
</TabsList> </TabsList>
{/* Current plan tab */} {/* Current plan tab */}

View File

@@ -34,7 +34,7 @@ export default function ValuationPage() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
<h1 className="text-3xl font-bold">Dinh gia AI</h1> <h1 className="text-2xl font-bold sm:text-3xl">Dinh gia AI</h1>
<p className="mt-2 text-muted-foreground"> <p className="mt-2 text-muted-foreground">
Su dung AI de uoc tinh gia tri bat dong san dua tren du lieu thi truong Su dung AI de uoc tinh gia tri bat dong san dua tren du lieu thi truong
</p> </p>

View File

@@ -1,7 +1,9 @@
'use client'; 'use client';
import { LogOut, Menu, X } from 'lucide-react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl'; import { useTranslations } from 'next-intl';
import { useState } from 'react';
import { useTheme } from '@/components/providers/theme-provider'; import { useTheme } from '@/components/providers/theme-provider';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { LanguageSwitcher } from '@/components/ui/language-switcher'; import { LanguageSwitcher } from '@/components/ui/language-switcher';
@@ -14,12 +16,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
const { user, logout } = useAuthStore(); const { user, logout } = useAuthStore();
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
const t = useTranslations(); const t = useTranslations();
const [sidebarOpen, setSidebarOpen] = useState(false);
const navItems = [ const navItems = [
{ href: '/dashboard' as const, label: t('dashboard.title'), icon: '🏠' }, { href: '/dashboard' as const, label: t('dashboard.title'), icon: '🏠' },
{ href: '/listings' as const, label: t('dashboard.listings'), icon: '📋' }, { href: '/listings' as const, label: t('dashboard.listings'), icon: '📋' },
{ href: '/listings/new' as const, label: t('dashboard.createListing'), icon: '' }, { href: '/listings/new' as const, label: t('dashboard.createListing'), icon: '' },
{ href: '/analytics' as const, label: t('dashboard.analytics'), 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/valuation' as const, label: t('dashboard.aiValuation'), icon: '🤖' },
{ href: '/dashboard/profile' as const, label: t('dashboard.profile'), icon: '👤' }, { href: '/dashboard/profile' as const, label: t('dashboard.profile'), icon: '👤' },
{ href: '/dashboard/subscription' as const, label: t('dashboard.subscription'), icon: '💎' }, { href: '/dashboard/subscription' as const, label: t('dashboard.subscription'), icon: '💎' },
@@ -28,30 +32,106 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
return ( return (
<div className="min-h-screen bg-background"> <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 <header
role="banner" role="banner"
className="sticky top-0 z-50 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60" 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"> <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"> <Link href="/" className="mr-6 flex items-center space-x-2">
<span className="text-lg font-bold text-primary">{t('common.goodgo')}</span> <span className="text-lg font-bold text-primary">{t('common.goodgo')}</span>
</Link> </Link>
<nav aria-label={t('nav.dashboardNav')} className="flex items-center space-x-1"> {/* Desktop nav */}
<nav aria-label={t('nav.dashboardNav')} className="hidden items-center space-x-1 md:flex">
{navItems.map((item) => ( {navItems.map((item) => (
<Link <Link
key={item.href} key={item.href}
href={item.href} href={item.href}
aria-label={item.label} aria-label={item.label}
className={cn( className={cn(
'rounded-md px-2 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground sm:px-3', '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)) pathname === item.href || (item.href !== '/dashboard' && pathname.startsWith(item.href))
? 'bg-accent text-accent-foreground' ? 'bg-accent text-accent-foreground'
: 'text-muted-foreground', : 'text-muted-foreground',
)} )}
> >
<span className="sm:mr-1.5" aria-hidden="true">{item.icon}</span> <span className="mr-1.5" aria-hidden="true">{item.icon}</span>
<span className="hidden sm:inline">{item.label}</span> <span className="hidden lg:inline">{item.label}</span>
</Link> </Link>
))} ))}
</nav> </nav>
@@ -80,7 +160,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
</svg> </svg>
)} )}
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => logout()}> <Button variant="ghost" size="sm" className="hidden md:inline-flex" onClick={() => logout()}>
{t('common.logout')} {t('common.logout')}
</Button> </Button>
</div> </div>

View File

@@ -88,9 +88,9 @@ export default function EditListingPage() {
} }
return ( return (
<div className="mx-auto max-w-3xl space-y-6"> <div className="mx-auto max-w-3xl space-y-4 sm:space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between gap-3">
<h1 className="text-2xl font-bold">Chỉnh sửa tin đăng</h1> <h1 className="text-xl font-bold sm:text-2xl">Chỉnh sửa tin đăng</h1>
<Button variant="outline" onClick={() => router.push(`/listings/${id}`)}> <Button variant="outline" onClick={() => router.push(`/listings/${id}`)}>
Xem tin Xem tin
</Button> </Button>
@@ -102,7 +102,7 @@ export default function EditListingPage() {
</p> </p>
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-2 md:grid-cols-4">
<TabsTrigger value="basic"> bản</TabsTrigger> <TabsTrigger value="basic"> bản</TabsTrigger>
<TabsTrigger value="location">Vị trí</TabsTrigger> <TabsTrigger value="location">Vị trí</TabsTrigger>
<TabsTrigger value="details">Chi tiết</TabsTrigger> <TabsTrigger value="details">Chi tiết</TabsTrigger>

View File

@@ -130,10 +130,10 @@ export default function CreateListingPage() {
return ( return (
<div className="mx-auto max-w-3xl"> <div className="mx-auto max-w-3xl">
<h1 className="mb-6 text-2xl font-bold">Đăng tin mới</h1> <h1 className="mb-4 text-xl font-bold sm:mb-6 sm:text-2xl">Đăng tin mới</h1>
{/* Step indicators */} {/* Step indicators */}
<div className="mb-8 flex items-center justify-between"> <div className="mb-6 flex items-center justify-between sm:mb-8">
{STEPS.map((step, index) => ( {STEPS.map((step, index) => (
<div key={step.title} className="flex items-center"> <div key={step.title} className="flex items-center">
<button <button
@@ -161,7 +161,7 @@ export default function CreateListingPage() {
{index < STEPS.length - 1 && ( {index < STEPS.length - 1 && (
<div <div
className={cn( className={cn(
'mx-3 h-px w-8 sm:w-12', 'mx-1 h-px w-4 sm:mx-3 sm:w-8 md:w-12',
index < currentStep ? 'bg-primary' : 'bg-muted', index < currentStep ? 'bg-primary' : 'bg-muted',
)} )}
/> />

View File

@@ -10,6 +10,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Select } from '@/components/ui/select'; import { Select } from '@/components/ui/select';
import { formatPrice } from '@/lib/currency'; import { formatPrice } from '@/lib/currency';
import { useListingsSearch } from '@/lib/hooks/use-listings'; import { useListingsSearch } from '@/lib/hooks/use-listings';
import { shimmerBlurDataURL, staticBlurDataURL } from '@/lib/image-blur';
import type { ListingDetail as _ListingDetail } from '@/lib/listings-api'; import type { ListingDetail as _ListingDetail } from '@/lib/listings-api';
import { PROPERTY_TYPES, TRANSACTION_TYPES, LISTING_STATUSES } from '@/lib/validations/listings'; import { PROPERTY_TYPES, TRANSACTION_TYPES, LISTING_STATUSES } from '@/lib/validations/listings';
@@ -190,6 +191,8 @@ export default function ListingsPage() {
fill fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
className="object-cover" className="object-cover"
placeholder="blur"
blurDataURL={shimmerBlurDataURL()}
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center text-muted-foreground"> <div className="flex h-full items-center justify-center text-muted-foreground">
@@ -270,6 +273,8 @@ export default function ListingsPage() {
fill fill
sizes="56px" sizes="56px"
className="object-cover" className="object-cover"
placeholder="blur"
blurDataURL={staticBlurDataURL()}
/> />
) : ( ) : (
<div className="flex h-full items-center justify-center text-xs text-muted-foreground"> <div className="flex h-full items-center justify-center text-xs text-muted-foreground">

View File

@@ -6,6 +6,7 @@ import * as React from 'react';
import { FilterBar, type SearchFilters } from '@/components/search/filter-bar'; import { FilterBar, type SearchFilters } from '@/components/search/filter-bar';
import { SearchResults } from '@/components/search/search-results'; import { SearchResults } from '@/components/search/search-results';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useCreateSavedSearch } from '@/lib/hooks/use-saved-searches';
import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api'; import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api';
const ListingMap = dynamic( const ListingMap = dynamic(
@@ -58,6 +59,12 @@ function SearchContent() {
const [viewMode, setViewMode] = React.useState<ViewMode>('list'); const [viewMode, setViewMode] = React.useState<ViewMode>('list');
const [showMobileFilters, setShowMobileFilters] = React.useState(false); const [showMobileFilters, setShowMobileFilters] = React.useState(false);
const [selectedListingId, setSelectedListingId] = React.useState<string | undefined>(); const [selectedListingId, setSelectedListingId] = React.useState<string | undefined>();
const [showSaveDialog, setShowSaveDialog] = React.useState(false);
const [saveName, setSaveName] = React.useState('');
const [saveAlertEnabled, setSaveAlertEnabled] = React.useState(true);
const [saveSuccess, setSaveSuccess] = React.useState(false);
const createSavedSearch = useCreateSavedSearch();
const handleMarkerClick = (listing: ListingDetail) => { const handleMarkerClick = (listing: ListingDetail) => {
setSelectedListingId(listing.id); setSelectedListingId(listing.id);
@@ -120,14 +127,116 @@ function SearchContent() {
([key, value]) => value && key !== 'sort', ([key, value]) => value && key !== 'sort',
).length; ).length;
const handleSaveSearch = () => {
if (!saveName.trim()) return;
const filterData: Record<string, string> = {};
Object.entries(filters).forEach(([key, value]) => {
if (value && key !== 'sort') filterData[key] = value;
});
createSavedSearch.mutate(
{ name: saveName.trim(), filters: filterData, alertEnabled: saveAlertEnabled },
{
onSuccess: () => {
setSaveSuccess(true);
setSaveName('');
setTimeout(() => {
setShowSaveDialog(false);
setSaveSuccess(false);
}, 1500);
},
},
);
};
return ( return (
<div className="mx-auto max-w-7xl px-4 py-6"> <div className="mx-auto max-w-7xl px-4 py-6">
{/* Header */} {/* Header */}
<div className="mb-6"> <div className="mb-6 flex items-start justify-between">
<h1 className="text-2xl font-bold md:text-3xl">Tìm kiếm bất đng sản</h1> <div>
<p className="mt-1 text-muted-foreground"> <h1 className="text-2xl font-bold md:text-3xl">Tìm kiếm bất đng sản</h1>
Tìm bất đng sản phù hợp với nhu cầu của bạn <p className="mt-1 text-muted-foreground">
</p> Tìm bất đng sản phù hợp với nhu cầu của bạn
</p>
</div>
{activeFilterCount > 0 && (
<div className="relative">
<Button
variant="outline"
size="sm"
onClick={() => setShowSaveDialog(!showSaveDialog)}
>
<svg className="mr-1.5 h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
Lưu tìm kiếm
</Button>
{/* Save search dialog */}
{showSaveDialog && (
<div className="absolute right-0 top-full z-50 mt-2 w-80 rounded-lg border bg-card p-4 shadow-lg">
{saveSuccess ? (
<div className="flex items-center gap-2 text-green-600">
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-sm font-medium">Đã lưu tìm kiếm!</span>
</div>
) : (
<>
<h3 className="mb-3 font-medium" id="save-search-heading">Lưu bộ lọc tìm kiếm</h3>
<label htmlFor="save-search-name" className="sr-only">Tên tìm kiếm</label>
<input
id="save-search-name"
type="text"
value={saveName}
onChange={(e) => setSaveName(e.target.value)}
placeholder="Tên tìm kiếm (VD: Chung cư Q7 dưới 3 tỷ)"
className="mb-3 w-full rounded-md border bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-primary"
maxLength={100}
onKeyDown={(e) => e.key === 'Enter' && handleSaveSearch()}
aria-describedby="save-search-heading"
/>
<div className="mb-3 flex items-center gap-2 text-sm">
<input
id="save-alert-enabled"
type="checkbox"
checked={saveAlertEnabled}
onChange={(e) => setSaveAlertEnabled(e.target.checked)}
className="rounded"
/>
<label htmlFor="save-alert-enabled">
Nhận thông báo khi kết quả mới
</label>
</div>
<div className="flex items-center justify-end gap-2">
<Button
size="sm"
variant="ghost"
onClick={() => setShowSaveDialog(false)}
>
Hủy
</Button>
<Button
size="sm"
onClick={handleSaveSearch}
disabled={!saveName.trim() || createSavedSearch.isPending}
>
{createSavedSearch.isPending ? 'Đang lưu...' : 'Lưu'}
</Button>
</div>
{createSavedSearch.isError && (
<p className="mt-2 text-xs text-destructive">
Không thể lưu tìm kiếm. Vui lòng thử lại.
</p>
)}
</>
)}
</div>
)}
</div>
)}
</div> </div>
{/* View Mode Toggle + Mobile Filter Button */} {/* View Mode Toggle + Mobile Filter Button */}

View File

@@ -1,4 +1,5 @@
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import { Inter } from 'next/font/google';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import { getMessages, getTranslations } from 'next-intl/server'; import { getMessages, getTranslations } from 'next-intl/server';
@@ -11,6 +12,12 @@ import type { Locale } from '@/i18n/config';
import { routing } from '@/i18n/routing'; import { routing } from '@/i18n/routing';
import '../globals.css'; import '../globals.css';
const inter = Inter({
subsets: ['latin', 'vietnamese'],
display: 'swap',
variable: '--font-inter',
});
const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn'; const siteUrl = process.env['NEXT_PUBLIC_SITE_URL'] || 'https://goodgo.vn';
export const viewport: Viewport = { export const viewport: Viewport = {
@@ -99,8 +106,8 @@ export default async function LocaleLayout({
const t = await getTranslations({ locale, namespace: 'common' }); const t = await getTranslations({ locale, namespace: 'common' });
return ( return (
<html lang={locale} suppressHydrationWarning> <html lang={locale} suppressHydrationWarning className={inter.variable}>
<body> <body className={inter.className}>
<JsonLd data={generateWebsiteJsonLd(siteUrl)} /> <JsonLd data={generateWebsiteJsonLd(siteUrl)} />
<a <a
href="#main-content" href="#main-content"