From 8592fb436c2af2c65b12cf7290c9a79315f0bbaa Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 16 Apr 2026 17:05:26 +0700 Subject: [PATCH] feat(web): integrate neighborhood radar chart into listing detail page Add NeighborhoodRadarChart to listing detail view, fetching scores from the analytics API based on the listing's district and city. Displays a 6-axis radar chart (education, healthcare, transport, shopping, environment, safety) with overall score and color-coded badges. Co-Authored-By: Paperclip --- .../listings/listing-detail-client.tsx | 49 ++++++++++++++++++- apps/web/lib/listings-api.ts | 19 +++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/apps/web/components/listings/listing-detail-client.tsx b/apps/web/components/listings/listing-detail-client.tsx index b4ac517..23a30ed 100644 --- a/apps/web/components/listings/listing-detail-client.tsx +++ b/apps/web/components/listings/listing-detail-client.tsx @@ -12,9 +12,15 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { AiEstimateButton } from '@/components/valuation/ai-estimate-button'; import { formatPrice, formatPricePerM2 } from '@/lib/currency'; -import type { ListingDetail } from '@/lib/listings-api'; +import type { ListingDetail, NeighborhoodScoreResult } from '@/lib/listings-api'; +import { listingsApi } from '@/lib/listings-api'; import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings'; +const NeighborhoodRadarChart = dynamic( + () => import('@/components/neighborhood').then((m) => m.NeighborhoodRadarChart), + { ssr: false }, +); + const ListingMap = dynamic( () => import('@/components/map/listing-map').then((mod) => mod.ListingMap), { @@ -36,11 +42,31 @@ interface ListingDetailClientProps { listing: ListingDetail; } +function mapScoreToCategories(result: NeighborhoodScoreResult) { + return [ + { category: 'education', label: 'Giáo dục', score: result.educationScore }, + { category: 'healthcare', label: 'Y tế', score: result.healthcareScore }, + { category: 'transport', label: 'Giao thông', score: result.transportScore }, + { category: 'shopping', label: 'Mua sắm', score: result.shoppingScore }, + { category: 'environment', label: 'Môi trường', score: result.greeneryScore }, + { category: 'safety', label: 'An ninh', score: result.safetyScore }, + ]; +} + export function ListingDetailClient({ listing }: ListingDetailClientProps) { const { property, seller, agent } = listing; const transactionLabel = getLabel(TRANSACTION_TYPES, listing.transactionType); const propertyTypeLabel = getLabel(PROPERTY_TYPES, property.propertyType); const [inquiryOpen, setInquiryOpen] = React.useState(false); + const [neighborhoodScore, setNeighborhoodScore] = React.useState(null); + + React.useEffect(() => { + if (!property.district || !property.city) return; + listingsApi + .getNeighborhoodScore(property.district, property.city) + .then(setNeighborhoodScore) + .catch(() => {/* silently ignore — section simply won't render */}); + }, [property.district, property.city]); return (
@@ -174,6 +200,27 @@ export function ListingDetailClient({ listing }: ListingDetailClientProps) { /> + + {/* Neighborhood Score Radar Chart */} + {neighborhoodScore && ( + + + Đánh giá khu vực + + +
+ + {neighborhoodScore.totalScore.toFixed(1)} + + /10 điểm tổng +
+ +
+
+ )}
{/* Sidebar */} diff --git a/apps/web/lib/listings-api.ts b/apps/web/lib/listings-api.ts index bed5e90..47a0dfc 100644 --- a/apps/web/lib/listings-api.ts +++ b/apps/web/lib/listings-api.ts @@ -132,6 +132,20 @@ export interface SearchListingsParams { limit?: number; } +export interface NeighborhoodScoreResult { + district: string; + city: string; + educationScore: number; + healthcareScore: number; + transportScore: number; + shoppingScore: number; + greeneryScore: number; + safetyScore: number; + totalScore: number; + poiCounts: Record; + calculatedAt: string; +} + // ─── API Functions ─────────────────────────────────────── const API_BASE_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1'; @@ -188,4 +202,9 @@ export const listingsApi = { return res.json() as Promise<{ mediaId: string; url: string }>; }, + + getNeighborhoodScore: (district: string, city: string = 'Hồ Chí Minh') => + apiClient.get( + `/analytics/neighborhoods/${encodeURIComponent(district)}/score?city=${encodeURIComponent(city)}`, + ), };