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: () => ( +
+

Dang tai ban do...

+
+ ), + }, +); + 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: () => ( +
+

Dang tai ban do...

+
+ ), + }, +); + 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, + ]} + /> + + + + + + ); +}