diff --git a/apps/web/app/(dashboard)/analytics/page.tsx b/apps/web/app/(dashboard)/analytics/page.tsx
index d14f045..8e2436c 100644
--- a/apps/web/app/(dashboard)/analytics/page.tsx
+++ b/apps/web/app/(dashboard)/analytics/page.tsx
@@ -1,18 +1,7 @@
'use client';
+import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
-import {
- BarChart,
- Bar,
- LineChart,
- Line,
- XAxis,
- YAxis,
- CartesianGrid,
- Tooltip,
- ResponsiveContainer,
- Legend,
-} from 'recharts';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
@@ -24,6 +13,16 @@ import {
type PriceTrendPoint,
} from '@/lib/analytics-api';
+const DistrictBarChart = dynamic(
+ () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart),
+ { ssr: false, loading: () =>
Dang tai bieu do...
},
+);
+
+const PriceTrendChart = dynamic(
+ () => import('@/components/charts/price-trend-chart').then((mod) => mod.PriceTrendChart),
+ { ssr: false, loading: () => Dang tai bieu do...
},
+);
+
const CITIES = ['Ho Chi Minh', 'Ha Noi', 'Da Nang'];
const CURRENT_PERIOD = '2026-Q1';
const TREND_PERIODS = ['2025-Q1', '2025-Q2', '2025-Q3', '2025-Q4', '2026-Q1'];
@@ -223,36 +222,7 @@ export default function AnalyticsPage() {
Chua co du lieu
) : (
-
-
-
-
-
- [
- name === 'price' ? `${value} tr/m2` : value,
- name === 'price' ? 'Gia' : 'Tin dang',
- ]}
- />
-
-
-
+
)}
@@ -345,61 +315,7 @@ export default function AnalyticsPage() {
Chua co du lieu xu huong
) : (
-
-
-
-
-
-
- [
- name === 'Gia/m2' ? `${value} tr/m2` : value,
- name,
- ]}
- />
-
-
-
-
-
+
)}
diff --git a/apps/web/app/(dashboard)/dashboard/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx
index 4f731ec..ef80e3d 100644
--- a/apps/web/app/(dashboard)/dashboard/page.tsx
+++ b/apps/web/app/(dashboard)/dashboard/page.tsx
@@ -1,17 +1,9 @@
'use client';
+import dynamic from 'next/dynamic';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
-import {
- BarChart,
- Bar,
- XAxis,
- YAxis,
- CartesianGrid,
- Tooltip,
- ResponsiveContainer,
-} from 'recharts';
import { ListingStatusBadge } from '@/components/listings/listing-status-badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
@@ -22,6 +14,11 @@ import {
} from '@/lib/analytics-api';
import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api';
+const DistrictBarChart = dynamic(
+ () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart),
+ { ssr: false, loading: () => Dang tai bieu do...
},
+);
+
const CITY = 'Ho Chi Minh';
const PERIOD = '2026-Q1';
@@ -180,27 +177,12 @@ export default function DashboardPage() {
Chua co du lieu
) : (
-
-
-
-
-
- [`${value} tr/m2`, 'Gia']}
- />
-
-
-
+ [`${value} tr/m2`, 'Gia']}
+ />
)}
diff --git a/apps/web/app/(public)/listings/[id]/page.tsx b/apps/web/app/(public)/listings/[id]/page.tsx
index 1a8f609..d153d7a 100644
--- a/apps/web/app/(public)/listings/[id]/page.tsx
+++ b/apps/web/app/(public)/listings/[id]/page.tsx
@@ -1,16 +1,28 @@
'use client';
+import dynamic from 'next/dynamic';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import * as React from 'react';
import { ImageGallery } from '@/components/listings/image-gallery';
-import { ListingMap } from '@/components/map/listing-map';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings';
+const ListingMap = dynamic(
+ () => import('@/components/map/listing-map').then((mod) => mod.ListingMap),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+
function formatPrice(priceVND: string): string {
const num = Number(priceVND);
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tá»·`;
diff --git a/apps/web/app/(public)/search/page.tsx b/apps/web/app/(public)/search/page.tsx
index e3f6e35..2310b3c 100644
--- a/apps/web/app/(public)/search/page.tsx
+++ b/apps/web/app/(public)/search/page.tsx
@@ -1,13 +1,25 @@
'use client';
+import dynamic from 'next/dynamic';
import { useRouter, useSearchParams } from 'next/navigation';
import * as React from 'react';
-import { ListingMap } from '@/components/map/listing-map';
import { FilterBar, type SearchFilters } from '@/components/search/filter-bar';
import { SearchResults } from '@/components/search/search-results';
import { Button } from '@/components/ui/button';
import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/listings-api';
+const ListingMap = dynamic(
+ () => import('@/components/map/listing-map').then((mod) => mod.ListingMap),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+
type ViewMode = 'list' | 'map' | 'split';
const defaultFilters: SearchFilters = {
diff --git a/apps/web/components/charts/district-bar-chart.tsx b/apps/web/components/charts/district-bar-chart.tsx
new file mode 100644
index 0000000..833fc11
--- /dev/null
+++ b/apps/web/components/charts/district-bar-chart.tsx
@@ -0,0 +1,60 @@
+'use client';
+
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+} from 'recharts';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type TooltipFormatter = (value: any, name: any) => [string, string];
+
+interface DistrictBarChartProps {
+ data: { district: string; price?: number; 'Gia/m2'?: number; listings: number }[];
+ height?: number;
+ dataKey?: string;
+ tooltipFormatter?: TooltipFormatter;
+}
+
+export function DistrictBarChart({
+ data,
+ height = 300,
+ dataKey = 'price',
+ tooltipFormatter,
+}: DistrictBarChartProps) {
+ const defaultFormatter: TooltipFormatter = (value, name) => [
+ name === dataKey ? `${value} tr/m2` : String(value),
+ name === dataKey ? 'Gia' : 'Tin dang',
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/components/charts/price-trend-chart.tsx b/apps/web/components/charts/price-trend-chart.tsx
new file mode 100644
index 0000000..afe9ee7
--- /dev/null
+++ b/apps/web/components/charts/price-trend-chart.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+ Legend,
+} from 'recharts';
+
+interface PriceTrendChartProps {
+ data: { period: string; 'Gia/m2': number; 'Tin dang': number }[];
+ height?: number;
+}
+
+export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) {
+ return (
+
+
+
+
+
+
+ [
+ name === 'Gia/m2' ? `${value} tr/m2` : value,
+ name,
+ ]}
+ />
+
+
+
+
+
+ );
+}