feat(web): complete du-an project pages, neighborhood components, and public notification bell
- Add grid/map view toggle on /du-an listing page with Mapbox project markers - Enhance du-an detail with master plan viewer, neighborhood radar chart, POI map, and price history chart - Create neighborhood component suite: radar chart (Recharts), POI map (Mapbox), score badges - Add du-an API client, server-side fetching, and React Query hooks - Wire NotificationBell into public layout header for authenticated users - Fix missing PROJECT_STATUS_COLORS import in du-an detail client Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
103
apps/web/components/neighborhood/neighborhood-radar-chart.tsx
Normal file
103
apps/web/components/neighborhood/neighborhood-radar-chart.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Radar,
|
||||
RadarChart,
|
||||
PolarGrid,
|
||||
PolarAngleAxis,
|
||||
PolarRadiusAxis,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
} from 'recharts';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { NeighborhoodCategory } from './types';
|
||||
|
||||
interface NeighborhoodRadarChartProps {
|
||||
categories: NeighborhoodCategory[];
|
||||
height?: number;
|
||||
showBadges?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function getScoreVariant(score: number): 'success' | 'warning' | 'destructive' {
|
||||
if (score > 7) return 'success';
|
||||
if (score >= 5) return 'warning';
|
||||
return 'destructive';
|
||||
}
|
||||
|
||||
function getScoreLabel(score: number): string {
|
||||
if (score > 7) return 'Tốt';
|
||||
if (score >= 5) return 'TB';
|
||||
return 'Yếu';
|
||||
}
|
||||
|
||||
export function NeighborhoodRadarChart({
|
||||
categories,
|
||||
height = 300,
|
||||
showBadges = true,
|
||||
className,
|
||||
}: NeighborhoodRadarChartProps) {
|
||||
const chartData = categories.map((cat) => ({
|
||||
subject: cat.label,
|
||||
score: cat.score,
|
||||
fullMark: 10,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<RadarChart cx="50%" cy="50%" outerRadius="75%" data={chartData}>
|
||||
<PolarGrid
|
||||
stroke="hsl(var(--border))"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
<PolarAngleAxis
|
||||
dataKey="subject"
|
||||
tick={{
|
||||
fontSize: 12,
|
||||
fill: 'hsl(var(--muted-foreground))',
|
||||
}}
|
||||
/>
|
||||
<PolarRadiusAxis
|
||||
angle={90}
|
||||
domain={[0, 10]}
|
||||
tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
|
||||
tickCount={6}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '0.5rem',
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
formatter={(value) => [`${Number(value).toFixed(1)}/10`, 'Điểm']}
|
||||
/>
|
||||
<Radar
|
||||
name="Điểm"
|
||||
dataKey="score"
|
||||
stroke="hsl(var(--primary))"
|
||||
fill="hsl(var(--primary))"
|
||||
fillOpacity={0.2}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
{showBadges && (
|
||||
<div className="mt-3 flex flex-wrap items-center justify-center gap-2">
|
||||
{categories.map((cat) => (
|
||||
<Badge
|
||||
key={cat.category}
|
||||
variant={getScoreVariant(cat.score)}
|
||||
className="gap-1 text-xs"
|
||||
>
|
||||
{cat.label}: {cat.score.toFixed(1)}
|
||||
<span className="opacity-70">({getScoreLabel(cat.score)})</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user