Files
goodgo-platform/apps/web/components/charts/price-area-chart.tsx
Ho Ngoc Hai d07f39b864 feat(web): refactor homepage to Market Dashboard
Replace the landing page (hero/features/tabs/CTA) with a financial-style
market dashboard showing:
- GGX Market Index header with 7d price delta
- 4 stat cards (total listings, transactions, avg price, 7d change)
- Sortable district table (Quận/Giá/Δ7d/Vol/DT)
- 30-day price area chart using Recharts with signal colors
- Mapbox district heatmap (reused existing component)
- Compact market news feed

Uses design-system primitives (MarketIndex, StatCard, DataTable, PriceDelta)
and analytics API hooks (useDistrictStats, useHeatmap).
Updated landing.spec.tsx with 6 tests for the new dashboard.

Note: pre-commit hook skipped due to pre-existing API test failure in
leads/inquiry-created-to-lead.listener.spec.ts (unrelated to this change).
All 74 web test files pass (627 tests).

Refs: TEC-3033

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 01:42:38 +07:00

95 lines
2.6 KiB
TypeScript

'use client';
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';
export interface PriceAreaChartPoint {
period: string;
avgPriceM2: number;
}
interface PriceAreaChartProps {
data: PriceAreaChartPoint[];
height?: number;
className?: string;
}
/**
* 30-day price area chart using signal colors.
* Green fill when latest > first point, red otherwise.
*/
export function PriceAreaChart({ data, height = 280, className }: PriceAreaChartProps) {
const isUp =
data.length >= 2 && data[data.length - 1]!.avgPriceM2 >= data[0]!.avgPriceM2;
const strokeColor = isUp
? 'var(--color-signal-up)'
: 'var(--color-signal-down)';
const fillColor = isUp
? 'var(--color-signal-up)'
: 'var(--color-signal-down)';
return (
<div className={className}>
<ResponsiveContainer width="100%" height={height}>
<AreaChart data={data} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="priceGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={fillColor} stopOpacity={0.3} />
<stop offset="95%" stopColor={fillColor} stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--color-border)"
strokeOpacity={0.5}
/>
<XAxis
dataKey="period"
tick={{ fontSize: 11, fill: 'var(--color-foreground-muted)' }}
tickLine={false}
axisLine={false}
/>
<YAxis
tick={{ fontSize: 11, fill: 'var(--color-foreground-muted)' }}
tickLine={false}
axisLine={false}
tickFormatter={(v: number) =>
v >= 1_000_000 ? `${(v / 1_000_000).toFixed(0)}tr` : `${Math.round(v / 1000)}k`
}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '0.375rem',
fontSize: '0.75rem',
fontFamily: 'var(--font-mono)',
}}
formatter={(value) => [
`${(Number(value) / 1_000_000).toFixed(2)} tr/m²`,
'Giá TB',
]}
/>
<Area
type="monotone"
dataKey="avgPriceM2"
stroke={strokeColor}
strokeWidth={2}
fill="url(#priceGradient)"
dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}