fix(web): add proper Vietnamese diacritics to all dashboard and listing pages

Vietnamese text throughout the frontend was missing accent marks (diacritics),
using plain ASCII instead of proper Unicode characters. Fixed all user-visible
text across dashboard, analytics, listings, search, and chart components.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-08 13:21:37 +07:00
parent 47c34f129e
commit 36c1e3b39a
7 changed files with 158 additions and 158 deletions

View File

@@ -15,12 +15,12 @@ import {
const DistrictBarChart = dynamic(
() => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart),
{ ssr: false, loading: () => <div className="flex h-64 items-center justify-center text-muted-foreground">Dang tai bieu do...</div> },
{ ssr: false, loading: () => <div className="flex h-64 items-center justify-center text-muted-foreground">Đang ti biu đ...</div> },
);
const PriceTrendChart = dynamic(
() => import('@/components/charts/price-trend-chart').then((mod) => mod.PriceTrendChart),
{ ssr: false, loading: () => <div className="flex h-64 items-center justify-center text-muted-foreground">Dang tai bieu do...</div> },
{ ssr: false, loading: () => <div className="flex h-64 items-center justify-center text-muted-foreground">Đang ti biu đ...</div> },
);
const CITIES = ['Ho Chi Minh', 'Ha Noi', 'Da Nang'];
@@ -29,14 +29,14 @@ const TREND_PERIODS = ['2025-Q1', '2025-Q2', '2025-Q3', '2025-Q4', '2026-Q1'];
function formatPrice(priceStr: string): string {
const num = Number(priceStr);
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} ty`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`;
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} t`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triu`;
return num.toLocaleString('vi-VN');
}
function formatPriceM2(price: number): string {
if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m2`;
return `${price.toLocaleString('vi-VN')} d/m2`;
if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`;
return `${price.toLocaleString('vi-VN')} đ/m²`;
}
function YoYBadge({ value }: { value: number | null }) {
@@ -91,7 +91,7 @@ export default function AnalyticsPage() {
setTrendDistrict(firstDistrict);
}
})
.catch(() => setError('Khong the tai du lieu phan tich'))
.catch(() => setError('Không th ti d liu phân tích'))
.finally(() => setLoading(false));
}, [city, period]);
@@ -131,16 +131,16 @@ export default function AnalyticsPage() {
const trendChartData = priceTrend.map((p) => ({
period: p.period,
'Gia/m2': Math.round(p.avgPriceM2 / 1_000_000),
'Tin dang': p.totalListings,
'Tin đăng': p.totalListings,
}));
return (
<div className="space-y-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-3xl font-bold">Phan tich thi truong</h1>
<h1 className="text-3xl font-bold">Phân tích th trường</h1>
<p className="mt-2 text-muted-foreground">
Bao cao thi truong bat dong san - {period}
Báo cáo th trường bt đng sn - {period}
</p>
</div>
<div className="flex gap-2">
@@ -163,7 +163,7 @@ export default function AnalyticsPage() {
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardDescription>Tong tin dang</CardDescription>
<CardDescription>Tng tin đăng</CardDescription>
<CardTitle className="text-2xl">
{loading ? '...' : totalListings.toLocaleString('vi-VN')}
</CardTitle>
@@ -171,7 +171,7 @@ export default function AnalyticsPage() {
</Card>
<Card>
<CardHeader className="pb-2">
<CardDescription>Gia TB/m2</CardDescription>
<CardDescription>Giá TB/m²</CardDescription>
<CardTitle className="text-2xl">
{loading ? '...' : formatPriceM2(avgPriceM2)}
</CardTitle>
@@ -179,15 +179,15 @@ export default function AnalyticsPage() {
</Card>
<Card>
<CardHeader className="pb-2">
<CardDescription>Ngay trung binh de ban</CardDescription>
<CardDescription>Ngày trung bình đ bán</CardDescription>
<CardTitle className="text-2xl">
{loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngay`}
{loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngày`}
</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-2">
<CardDescription>So quan/huyen</CardDescription>
<CardDescription>S qun/huyn</CardDescription>
<CardTitle className="text-2xl">
{loading ? '...' : new Set(marketReport.map((d) => d.district)).size}
</CardTitle>
@@ -198,9 +198,9 @@ export default function AnalyticsPage() {
{/* Tabs */}
<Tabs value={tab} onValueChange={setTab}>
<TabsList>
<TabsTrigger value="overview">Tong quan</TabsTrigger>
<TabsTrigger value="trends">Xu huong gia</TabsTrigger>
<TabsTrigger value="districts">Chi tiet quan</TabsTrigger>
<TabsTrigger value="overview">Tng quan</TabsTrigger>
<TabsTrigger value="trends">Xu hướng giá</TabsTrigger>
<TabsTrigger value="districts">Chi tiết qun</TabsTrigger>
</TabsList>
{/* Overview Tab */}
@@ -209,17 +209,17 @@ export default function AnalyticsPage() {
{/* Bar Chart - Price by District */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Gia trung binh theo quan</CardTitle>
<CardDescription>Trieu VND/m2 tai {city}</CardDescription>
<CardTitle className="text-lg">Giá trung bình theo qun</CardTitle>
<CardDescription>Triu VND/m² ti {city}</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Dang tai...
Đang ti...
</div>
) : barChartData.length === 0 ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Chua co du lieu
Chưa có d liu
</div>
) : (
<DistrictBarChart data={barChartData} height={300} dataKey="price" />
@@ -230,17 +230,17 @@ export default function AnalyticsPage() {
{/* Heatmap - Card Grid */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Ban do gia theo quan</CardTitle>
<CardDescription>So sanh gia trung binh/m2 tai {city}</CardDescription>
<CardTitle className="text-lg">Bn đ giá theo qun</CardTitle>
<CardDescription>So sánh giá trung bình/m² ti {city}</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Dang tai...
Đang ti...
</div>
) : heatmap.length === 0 ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Chua co du lieu
Chưa có d liu
</div>
) : (
<div className="grid gap-2 sm:grid-cols-2">
@@ -267,7 +267,7 @@ export default function AnalyticsPage() {
{formatPriceM2(point.avgPriceM2)}
</div>
<div className="text-xs text-muted-foreground">
{point.totalListings} tin dang
{point.totalListings} tin đăng
</div>
</div>
);
@@ -299,20 +299,20 @@ export default function AnalyticsPage() {
<Card>
<CardHeader>
<CardTitle className="text-lg">
Xu huong gia - {trendDistrict || 'Chon quan'}
Xu hướng giá - {trendDistrict || 'Chn qun'}
</CardTitle>
<CardDescription>
Bien dong gia trung binh/m2 qua cac quy (Can ho)
Biến đng giá trung bình/m² qua các quý (Căn h)
</CardDescription>
</CardHeader>
<CardContent>
{trendLoading ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Dang tai...
Đang ti...
</div>
) : trendChartData.length === 0 ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Chua co du lieu xu huong
Chưa có d liu xu hướng
</div>
) : (
<PriceTrendChart data={trendChartData} height={350} />
@@ -328,31 +328,31 @@ export default function AnalyticsPage() {
{/* Stats Table */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Thong ke chi tiet theo quan</CardTitle>
<CardTitle className="text-lg">Thng kê chi tiết theo qun</CardTitle>
<CardDescription>
Du lieu thi truong bat dong san tai {city} - {period}
D liu th trường bt đng sn ti {city} - {period}
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex h-48 items-center justify-center text-muted-foreground">
Dang tai...
Đang ti...
</div>
) : districtStats.length === 0 ? (
<div className="flex h-48 items-center justify-center text-muted-foreground">
Chua co du lieu
Chưa có d liu
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left">
<th className="pb-2 pr-4 font-medium">Quan</th>
<th className="pb-2 pr-4 font-medium">Loai BDS</th>
<th className="pb-2 pr-4 font-medium text-right">Gia trung vi</th>
<th className="pb-2 pr-4 font-medium text-right">Gia/m2</th>
<th className="pb-2 pr-4 font-medium text-right">Tin dang</th>
<th className="pb-2 pr-4 font-medium text-right">Ngay ban</th>
<th className="pb-2 pr-4 font-medium">Qun</th>
<th className="pb-2 pr-4 font-medium">Loi BĐS</th>
<th className="pb-2 pr-4 font-medium text-right">Giá trung v</th>
<th className="pb-2 pr-4 font-medium text-right">Giá/m²</th>
<th className="pb-2 pr-4 font-medium text-right">Tin đăng</th>
<th className="pb-2 pr-4 font-medium text-right">Ngày bán</th>
<th className="pb-2 font-medium text-right">YoY</th>
</tr>
</thead>
@@ -391,17 +391,17 @@ export default function AnalyticsPage() {
{/* Market Report Cards */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Bao cao thi truong</CardTitle>
<CardDescription>Tong hop chi so thi truong theo tung quan</CardDescription>
<CardTitle className="text-lg">Báo cáo th trường</CardTitle>
<CardDescription>Tng hp ch s th trường theo tng qun</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex h-48 items-center justify-center text-muted-foreground">
Dang tai...
Đang ti...
</div>
) : marketReport.length === 0 ? (
<div className="flex h-48 items-center justify-center text-muted-foreground">
Chua co du lieu
Chưa có d liu
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
@@ -411,25 +411,25 @@ export default function AnalyticsPage() {
<h3 className="font-semibold">{district.district}</h3>
<div className="mt-2 space-y-1 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Gia trung vi</span>
<span className="text-muted-foreground">Giá trung v</span>
<span className="font-medium">
{formatPrice(district.medianPrice)}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Gia/m2</span>
<span className="text-muted-foreground">Giá/m²</span>
<span>{formatPriceM2(district.avgPriceM2)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Tin dang</span>
<span className="text-muted-foreground">Tin đăng</span>
<span>{district.totalListings}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Ton kho</span>
<span className="text-muted-foreground">Tn kho</span>
<span>{district.inventoryLevel}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Thay doi YoY</span>
<span className="text-muted-foreground">Thay đi YoY</span>
<YoYBadge value={district.yoyChange} />
</div>
</div>

View File

@@ -16,7 +16,7 @@ import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/lis
const DistrictBarChart = dynamic(
() => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart),
{ ssr: false, loading: () => <div className="flex h-64 items-center justify-center text-muted-foreground">Dang tai bieu do...</div> },
{ ssr: false, loading: () => <div className="flex h-64 items-center justify-center text-muted-foreground">Đang ti biu đ...</div> },
);
const CITY = 'Ho Chi Minh';
@@ -24,14 +24,14 @@ const PERIOD = '2026-Q1';
function formatPrice(priceStr: string): string {
const num = Number(priceStr);
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} ty`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`;
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} t`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triu`;
return num.toLocaleString('vi-VN');
}
function formatPriceM2(price: number): string {
if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m2`;
return `${price.toLocaleString('vi-VN')} d/m2`;
if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`;
return `${price.toLocaleString('vi-VN')} đ/m²`;
}
interface StatCardProps {
@@ -124,35 +124,35 @@ export default function DashboardPage() {
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Bang dieu khien</h1>
<h1 className="text-3xl font-bold">Bng điều khin</h1>
<p className="mt-2 text-muted-foreground">
Tong quan thi truong va tin dang cua ban
Tng quan th trường và tin đăng ca bn
</p>
</div>
<Link href="/listings/new">
<Button>Dang tin moi</Button>
<Button>Đăng tin mi</Button>
</Link>
</div>
{/* Stats overview */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<StatCard
title="Tin dang cua toi"
title="Tin đăng ca tôi"
value={loading ? '...' : myListingsCount.toLocaleString('vi-VN')}
description="Tong so tin da dang"
description="Tng s tin đã đăng"
/>
<StatCard
title="Luot xem"
title="Lượt xem"
value={loading ? '...' : totalViews.toLocaleString('vi-VN')}
description="Tren tat ca tin dang"
description="Trên tt c tin đăng"
/>
<StatCard
title="Lien he"
title="Liên h"
value={loading ? '...' : totalInquiries.toLocaleString('vi-VN')}
description="Yeu cau tu khach hang"
description="Yêu cu t khách hàng"
/>
<StatCard
title="Gia TB thi truong"
title="Giá TB th trường"
value={loading ? '...' : formatPriceM2(avgPriceM2)}
trend={avgYoy}
description="YoY"
@@ -164,24 +164,24 @@ export default function DashboardPage() {
{/* Price chart */}
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle className="text-lg">Gia trung binh theo quan</CardTitle>
<CardDescription>{CITY} - {PERIOD} (trieu VND/m2)</CardDescription>
<CardTitle className="text-lg">Giá trung bình theo qun</CardTitle>
<CardDescription>{CITY} - {PERIOD} (triu VND/m²)</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Dang tai...
Đang ti...
</div>
) : chartData.length === 0 ? (
<div className="flex h-64 items-center justify-center text-muted-foreground">
Chua co du lieu
Chưa có d liu
</div>
) : (
<DistrictBarChart
data={chartData}
height={280}
dataKey="Gia/m2"
tooltipFormatter={(value) => [`${value} tr/m2`, 'Gia']}
tooltipFormatter={(value) => [`${value} tr/m²`, 'Giá']}
/>
)}
</CardContent>
@@ -190,30 +190,30 @@ export default function DashboardPage() {
{/* Market summary */}
<Card>
<CardHeader>
<CardTitle className="text-lg">Thi truong {CITY}</CardTitle>
<CardDescription>Chi so chinh - {PERIOD}</CardDescription>
<CardTitle className="text-lg">Th trường {CITY}</CardTitle>
<CardDescription>Ch s chính - {PERIOD}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Tong tin dang</span>
<span className="text-sm text-muted-foreground">Tng tin đăng</span>
<span className="font-semibold">
{loading ? '...' : totalListings.toLocaleString('vi-VN')}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Gia TB/m2</span>
<span className="text-sm text-muted-foreground">Giá TB/m²</span>
<span className="font-semibold">
{loading ? '...' : formatPriceM2(avgPriceM2)}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Ngay TB de ban</span>
<span className="text-sm text-muted-foreground">Ngày TB đ bán</span>
<span className="font-semibold">
{loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngay`}
{loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngày`}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">So quan</span>
<span className="text-sm text-muted-foreground">S qun</span>
<span className="font-semibold">
{loading ? '...' : new Set(marketReport.map((d) => d.district)).size}
</span>
@@ -221,7 +221,7 @@ export default function DashboardPage() {
<div className="pt-2">
<Link href="/analytics">
<Button variant="outline" size="sm" className="w-full">
Xem phan tich chi tiet
Xem phân tích chi tiết
</Button>
</Link>
</div>
@@ -233,26 +233,26 @@ export default function DashboardPage() {
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle className="text-lg">Tin dang gan day</CardTitle>
<CardDescription>Danh sach tin dang moi nhat cua ban</CardDescription>
<CardTitle className="text-lg">Tin đăng gn đây</CardTitle>
<CardDescription>Danh sách tin đăng mi nht ca bn</CardDescription>
</div>
<Link href="/listings">
<Button variant="outline" size="sm">
Xem tat ca
Xem tt c
</Button>
</Link>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex h-32 items-center justify-center text-muted-foreground">
Dang tai...
Đang ti...
</div>
) : !listings || listings.data.length === 0 ? (
<div className="flex h-32 flex-col items-center justify-center text-muted-foreground">
<p>Chua co tin dang nao</p>
<p>Chưa có tin đăng nào</p>
<Link href="/listings/new" className="mt-2">
<Button variant="outline" size="sm">
Dang tin dau tien
Đăng tin đu tiên
</Button>
</Link>
</div>
@@ -292,8 +292,8 @@ export default function DashboardPage() {
<ListingStatusBadge status={listing.status} />
</div>
<div className="hidden sm:flex sm:gap-3 sm:text-sm sm:text-muted-foreground">
<span>{listing.viewCount} luot xem</span>
<span>{listing.inquiryCount} lien he</span>
<span>{listing.viewCount} lượt xem</span>
<span>{listing.inquiryCount} liên h</span>
</div>
</Link>
))}

View File

@@ -16,8 +16,8 @@ import {
import { PROPERTY_TYPES, TRANSACTION_TYPES, LISTING_STATUSES } from '@/lib/validations/listings';
function formatPrice(priceVND: string): string {
const num = Number(priceVND);
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} ty`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`;
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} t`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triu`;
return num.toLocaleString('vi-VN');
}
@@ -77,13 +77,13 @@ export default function ListingsPage() {
{/* Header */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold">Quan ly tin dang</h1>
<h1 className="text-2xl font-bold">Qun lý tin đăng</h1>
<p className="text-sm text-muted-foreground">
Quan ly, theo doi va cap nhat cac tin dang cua ban
Qun lý, theo dõi và cp nht các tin đăng ca bn
</p>
</div>
<Link href="/listings/new">
<Button>Dang tin moi</Button>
<Button>Đăng tin mi</Button>
</Link>
</div>
@@ -91,13 +91,13 @@ export default function ListingsPage() {
<div className="grid gap-3 sm:grid-cols-4">
<Card>
<CardHeader className="pb-2">
<CardDescription>Tong tin dang</CardDescription>
<CardDescription>Tng tin đăng</CardDescription>
<CardTitle className="text-xl">{loading ? '...' : stats.total}</CardTitle>
</CardHeader>
</Card>
<Card>
<CardHeader className="pb-2">
<CardDescription>Dang hoat dong</CardDescription>
<CardDescription>Đang hot đng</CardDescription>
<CardTitle className="text-xl text-green-600">
{loading ? '...' : stats.active}
</CardTitle>
@@ -105,7 +105,7 @@ export default function ListingsPage() {
</Card>
<Card>
<CardHeader className="pb-2">
<CardDescription>Cho duyet</CardDescription>
<CardDescription>Ch duyt</CardDescription>
<CardTitle className="text-xl text-yellow-600">
{loading ? '...' : stats.pending}
</CardTitle>
@@ -113,7 +113,7 @@ export default function ListingsPage() {
</Card>
<Card>
<CardHeader className="pb-2">
<CardDescription>Tong luot xem</CardDescription>
<CardDescription>Tng lượt xem</CardDescription>
<CardTitle className="text-xl">{loading ? '...' : stats.views}</CardTitle>
</CardHeader>
</Card>
@@ -128,7 +128,7 @@ export default function ListingsPage() {
}
className="w-40"
>
<option value="">Tat ca giao dich</option>
<option value="">Tt c giao dch</option>
{TRANSACTION_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
@@ -142,7 +142,7 @@ export default function ListingsPage() {
}
className="w-44"
>
<option value="">Tat ca loai BDS</option>
<option value="">Tt c loi BĐS</option>
{PROPERTY_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
@@ -154,7 +154,7 @@ export default function ListingsPage() {
onChange={(e) => setFilters((f) => ({ ...f, status: e.target.value, page: 1 }))}
className="w-40"
>
<option value="">Tat ca trang thai</option>
<option value="">Tt c trng thái</option>
{Object.entries(LISTING_STATUSES).map(([key, { label }]) => (
<option key={key} value={key}>
{label}
@@ -168,14 +168,14 @@ export default function ListingsPage() {
size="sm"
onClick={() => setViewMode('grid')}
>
Luoi
Lưới
</Button>
<Button
variant={viewMode === 'table' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('table')}
>
Bang
Bng
</Button>
</div>
</div>
@@ -187,10 +187,10 @@ export default function ListingsPage() {
</div>
) : !result || result.data.length === 0 ? (
<div className="flex min-h-[300px] flex-col items-center justify-center text-muted-foreground">
<p>Chua co tin dang nao</p>
<p>Chưa có tin đăng nào</p>
<Link href="/listings/new" className="mt-2">
<Button variant="outline" size="sm">
Dang tin dau tien
Đăng tin đu tiên
</Button>
</Link>
</div>
@@ -228,7 +228,7 @@ export default function ListingsPage() {
</p>
<div className="mt-3 flex flex-wrap gap-1.5">
<Badge variant="secondary" className="text-xs">
{listing.property.areaM2} m2
{listing.property.areaM2} m²
</Badge>
{listing.property.bedrooms != null && (
<Badge variant="secondary" className="text-xs">
@@ -242,9 +242,9 @@ export default function ListingsPage() {
)}
</div>
<div className="mt-3 flex gap-3 text-xs text-muted-foreground">
<span>{listing.viewCount} luot xem</span>
<span>{listing.inquiryCount} lien he</span>
<span>{listing.saveCount} da luu</span>
<span>{listing.viewCount} lượt xem</span>
<span>{listing.inquiryCount} liên h</span>
<span>{listing.saveCount} đã lưu</span>
</div>
</CardContent>
</Card>
@@ -259,14 +259,14 @@ export default function ListingsPage() {
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left">
<th className="p-3 font-medium">Tin dang</th>
<th className="p-3 font-medium">Loai</th>
<th className="p-3 font-medium text-right">Gia</th>
<th className="p-3 font-medium text-right">Dien tich</th>
<th className="p-3 font-medium text-center">Trang thai</th>
<th className="p-3 font-medium text-right">Luot xem</th>
<th className="p-3 font-medium text-right">Lien he</th>
<th className="p-3 font-medium text-right">Ngay dang</th>
<th className="p-3 font-medium">Tin đăng</th>
<th className="p-3 font-medium">Loi</th>
<th className="p-3 font-medium text-right">Giá</th>
<th className="p-3 font-medium text-right">Din tích</th>
<th className="p-3 font-medium text-center">Trng thái</th>
<th className="p-3 font-medium text-right">Lượt xem</th>
<th className="p-3 font-medium text-right">Liên h</th>
<th className="p-3 font-medium text-right">Ngày đăng</th>
</tr>
</thead>
<tbody>
@@ -311,7 +311,7 @@ export default function ListingsPage() {
<td className="p-3 text-right font-medium text-primary">
{formatPrice(listing.priceVND)}
</td>
<td className="p-3 text-right">{listing.property.areaM2} m2</td>
<td className="p-3 text-right">{listing.property.areaM2} m²</td>
<td className="p-3 text-center">
<ListingStatusBadge status={listing.status} />
</td>
@@ -338,7 +338,7 @@ export default function ListingsPage() {
disabled={filters.page <= 1}
onClick={() => setFilters((f) => ({ ...f, page: f.page - 1 }))}
>
Truoc
Trước
</Button>
<span className="text-sm text-muted-foreground">
Trang {result.page} / {result.totalPages}
@@ -349,7 +349,7 @@ export default function ListingsPage() {
disabled={filters.page >= result.totalPages}
onClick={() => setFilters((f) => ({ ...f, page: f.page + 1 }))}
>
Tiep
Tiếp
</Button>
</div>
)}

View File

@@ -17,7 +17,7 @@ const ListingMap = dynamic(
ssr: false,
loading: () => (
<div className="flex h-[300px] items-center justify-center rounded-lg bg-muted">
<p className="text-sm text-muted-foreground">Dang tai ban do...</p>
<p className="text-sm text-muted-foreground">Đang ti bn đ...</p>
</div>
),
},
@@ -45,7 +45,7 @@ export default function PublicListingDetailPage() {
listingsApi
.getById(id)
.then(setListing)
.catch((err) => setError(err instanceof Error ? err.message : 'Khong tai duoc tin dang'))
.catch((err) => setError(err instanceof Error ? err.message : 'Không ti được tin đăng'))
.finally(() => setLoading(false));
}, [id]);
@@ -74,9 +74,9 @@ export default function PublicListingDetailPage() {
<svg className="h-12 w-12 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<p className="text-destructive">{error || 'Khong tim thay tin dang'}</p>
<p className="text-destructive">{error || 'Không tìm thy tin đăng'}</p>
<Link href="/search">
<Button variant="outline">Quay lai tim kiem</Button>
<Button variant="outline">Quay li tìm kiếm</Button>
</Link>
</div>
);
@@ -90,9 +90,9 @@ export default function PublicListingDetailPage() {
<div className="mx-auto max-w-6xl px-4 py-6">
{/* Breadcrumb */}
<nav className="mb-4 flex items-center gap-1.5 text-sm text-muted-foreground">
<Link href="/" className="hover:text-foreground">Trang chu</Link>
<Link href="/" className="hover:text-foreground">Trang ch</Link>
<span>/</span>
<Link href="/search" className="hover:text-foreground">Tim kiem</Link>
<Link href="/search" className="hover:text-foreground">Tìm kiếm</Link>
<span>/</span>
<span className="truncate text-foreground">{property.title}</span>
</nav>
@@ -121,12 +121,12 @@ export default function PublicListingDetailPage() {
<p className="text-2xl font-bold text-primary md:text-3xl">{formatPrice(listing.priceVND)} VND</p>
{listing.pricePerM2 != null && (
<p className="text-sm text-muted-foreground">
~{listing.pricePerM2.toLocaleString('vi-VN')} VND/m2
~{listing.pricePerM2.toLocaleString('vi-VN')} VND/m²
</p>
)}
{listing.rentPriceMonthly && (
<p className="text-sm text-muted-foreground">
Thue: {formatPrice(listing.rentPriceMonthly)}/thang
Thuê: {formatPrice(listing.rentPriceMonthly)}/tháng
</p>
)}
</div>
@@ -137,18 +137,18 @@ export default function PublicListingDetailPage() {
{/* Quick specs bar */}
<div className="my-6 flex flex-wrap gap-4 rounded-lg border bg-card p-4">
<QuickStat icon="area" label="Dien tich" value={`${property.areaM2} m\u00B2`} />
<QuickStat icon="area" label="Din tích" value={`${property.areaM2} m\u00B2`} />
{property.bedrooms != null && (
<QuickStat icon="bed" label="Phong ngu" value={`${property.bedrooms}`} />
<QuickStat icon="bed" label="Phòng ng" value={`${property.bedrooms}`} />
)}
{property.bathrooms != null && (
<QuickStat icon="bath" label="Phong tam" value={`${property.bathrooms}`} />
<QuickStat icon="bath" label="Phòng tm" value={`${property.bathrooms}`} />
)}
{property.floors != null && (
<QuickStat icon="floors" label="So tang" value={`${property.floors}`} />
<QuickStat icon="floors" label="S tng" value={`${property.floors}`} />
)}
{property.direction && (
<QuickStat icon="compass" label="Huong" value={getLabel(DIRECTIONS, property.direction) || ''} />
<QuickStat icon="compass" label="Hướng" value={getLabel(DIRECTIONS, property.direction) || ''} />
)}
</div>
@@ -158,7 +158,7 @@ export default function PublicListingDetailPage() {
{/* Description */}
<Card>
<CardHeader>
<CardTitle>Mo ta</CardTitle>
<CardTitle>Mô t</CardTitle>
</CardHeader>
<CardContent>
<p className="whitespace-pre-wrap text-sm leading-relaxed">{property.description}</p>
@@ -168,19 +168,19 @@ export default function PublicListingDetailPage() {
{/* Details */}
<Card>
<CardHeader>
<CardTitle>Thong tin chi tiet</CardTitle>
<CardTitle>Thông tin chi tiết</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<InfoItem label="Loai BDS" value={propertyTypeLabel || '---'} />
<InfoItem label="Dien tich" value={`${property.areaM2} m\u00B2`} />
<InfoItem label="Phong ngu" value={property.bedrooms != null ? `${property.bedrooms}` : '---'} />
<InfoItem label="Phong tam" value={property.bathrooms != null ? `${property.bathrooms}` : '---'} />
<InfoItem label="So tang" value={property.floors != null ? `${property.floors}` : '---'} />
<InfoItem label="Huong" value={getLabel(DIRECTIONS, property.direction) || '---'} />
<InfoItem label="Nam xay" value={property.yearBuilt ? `${property.yearBuilt}` : '---'} />
<InfoItem label="Phap ly" value={property.legalStatus || '---'} />
<InfoItem label="Du an" value={property.projectName || '---'} />
<InfoItem label="Loi BĐS" value={propertyTypeLabel || '---'} />
<InfoItem label="Din tích" value={`${property.areaM2} m\u00B2`} />
<InfoItem label="Phòng ng" value={property.bedrooms != null ? `${property.bedrooms}` : '---'} />
<InfoItem label="Phòng tm" value={property.bathrooms != null ? `${property.bathrooms}` : '---'} />
<InfoItem label="S tng" value={property.floors != null ? `${property.floors}` : '---'} />
<InfoItem label="Hướng" value={getLabel(DIRECTIONS, property.direction) || '---'} />
<InfoItem label="Năm xây" value={property.yearBuilt ? `${property.yearBuilt}` : '---'} />
<InfoItem label="Pháp lý" value={property.legalStatus || '---'} />
<InfoItem label="Dự án" value={property.projectName || '---'} />
</div>
</CardContent>
</Card>
@@ -189,7 +189,7 @@ export default function PublicListingDetailPage() {
{property.amenities && property.amenities.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Tien ich</CardTitle>
<CardTitle>Tin ích</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
@@ -206,7 +206,7 @@ export default function PublicListingDetailPage() {
{/* Map */}
<Card>
<CardHeader>
<CardTitle>Vi tri tren ban do</CardTitle>
<CardTitle>V trí trên bn đ</CardTitle>
</CardHeader>
<CardContent>
<ListingMap
@@ -222,7 +222,7 @@ export default function PublicListingDetailPage() {
{/* Contact card */}
<Card className="sticky top-20">
<CardHeader>
<CardTitle>Lien he</CardTitle>
<CardTitle>Liên h</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-3">
@@ -242,22 +242,22 @@ export default function PublicListingDetailPage() {
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
Goi ngay
Gi ngay
</Button>
</a>
<Button variant="outline" className="w-full gap-2">
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
Nhan tin
Nhn tin
</Button>
{agent && (
<div className="border-t pt-3">
<p className="text-xs text-muted-foreground">Moi gioi</p>
<p className="text-xs text-muted-foreground">Môi gii</p>
{agent.agency && <p className="text-sm font-medium">{agent.agency}</p>}
{listing.commissionPct != null && (
<p className="text-xs text-muted-foreground">Hoa hong: {listing.commissionPct}%</p>
<p className="text-xs text-muted-foreground">Hoa hng: {listing.commissionPct}%</p>
)}
</div>
)}
@@ -270,20 +270,20 @@ export default function PublicListingDetailPage() {
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<p className="text-lg font-bold">{listing.viewCount}</p>
<p className="text-xs text-muted-foreground">Luot xem</p>
<p className="text-xs text-muted-foreground">Lượt xem</p>
</div>
<div>
<p className="text-lg font-bold">{listing.saveCount}</p>
<p className="text-xs text-muted-foreground">Luot luu</p>
<p className="text-xs text-muted-foreground">Lượt lưu</p>
</div>
<div>
<p className="text-lg font-bold">{listing.inquiryCount}</p>
<p className="text-xs text-muted-foreground">Lien he</p>
<p className="text-xs text-muted-foreground">Liên h</p>
</div>
</div>
{listing.publishedAt && (
<p className="mt-3 border-t pt-3 text-center text-xs text-muted-foreground">
Dang ngay {new Date(listing.publishedAt).toLocaleDateString('vi-VN')}
Đăng ngày {new Date(listing.publishedAt).toLocaleDateString('vi-VN')}
</p>
)}
</CardContent>

View File

@@ -14,7 +14,7 @@ const ListingMap = dynamic(
ssr: false,
loading: () => (
<div className="flex h-[calc(100vh-220px)] items-center justify-center rounded-lg bg-muted">
<p className="text-sm text-muted-foreground">Dang tai ban do...</p>
<p className="text-sm text-muted-foreground">Đang ti bn đ...</p>
</div>
),
},

View File

@@ -27,8 +27,8 @@ export function DistrictBarChart({
tooltipFormatter,
}: DistrictBarChartProps) {
const defaultFormatter: TooltipFormatter = (value, name) => [
name === dataKey ? `${value} tr/m2` : String(value),
name === dataKey ? 'Gia' : 'Tin dang',
name === dataKey ? `${value} tr/m²` : String(value),
name === dataKey ? 'Giá' : 'Tin đăng',
];
return (

View File

@@ -12,7 +12,7 @@ import {
} from 'recharts';
interface PriceTrendChartProps {
data: { period: string; 'Gia/m2': number; 'Tin dang': number }[];
data: { period: string; 'Gia/m2': number; 'Tin đăng': number }[];
height?: number;
}
@@ -37,7 +37,7 @@ export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) {
fontSize: '0.875rem',
}}
formatter={(value, name) => [
name === 'Gia/m2' ? `${value} tr/m2` : value,
name === 'Gia/m2' ? `${value} tr/m²` : value,
name,
]}
/>
@@ -54,7 +54,7 @@ export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) {
<Line
yAxisId="right"
type="monotone"
dataKey="Tin dang"
dataKey="Tin đăng"
stroke="hsl(var(--muted-foreground))"
strokeWidth={1}
strokeDasharray="5 5"