feat(web): add industrial compare page, listing search, and Mapbox park map

- Add interactive Mapbox map to /khu-cong-nghiep landing page with park markers and popups
- Build compare page at /khu-cong-nghiep/so-sanh with recharts RadarChart and detailed comparison table
- Build listing search page at /khu-cong-nghiep/cho-thue with filters for property type, lease type, area, and price
- Add IndustrialListing types, API client functions, and React Query hooks

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 12:40:35 +07:00
parent 28cdd92846
commit 5810f0be56
9 changed files with 964 additions and 1 deletions

View File

@@ -0,0 +1,95 @@
'use client';
import { Calendar, Eye, MapPin, Ruler } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import {
type IndustrialListingItem,
LEASE_TYPE_LABELS,
PROPERTY_TYPE_LABELS,
} from '@/lib/khu-cong-nghiep-api';
interface ListingCardProps {
listing: IndustrialListingItem;
}
export function IndustrialListingCard({ listing }: ListingCardProps) {
const priceText = listing.priceUsdM2
? `$${listing.priceUsdM2}/${listing.pricingUnit ?? 'm²/tháng'}`
: listing.totalLeasePrice
? `$${listing.totalLeasePrice.toLocaleString()}`
: 'Liên hệ';
const leaseTermText =
listing.minLeaseYears && listing.maxLeaseYears
? `${listing.minLeaseYears}${listing.maxLeaseYears} năm`
: listing.minLeaseYears
? `Từ ${listing.minLeaseYears} năm`
: null;
return (
<Card className="group h-full transition-shadow hover:shadow-lg">
<CardContent className="p-5">
{/* Header badges */}
<div className="mb-3 flex flex-wrap items-center gap-2">
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
{PROPERTY_TYPE_LABELS[listing.propertyType]}
</Badge>
<Badge variant="outline">
{LEASE_TYPE_LABELS[listing.leaseType]}
</Badge>
</div>
{/* Title */}
<h3 className="mb-2 line-clamp-2 font-semibold text-foreground group-hover:text-primary">
{listing.title}
</h3>
{/* Park location */}
<div className="mb-3 flex items-center gap-1 text-sm text-muted-foreground">
<MapPin className="h-3.5 w-3.5 shrink-0" />
<a
href={`/khu-cong-nghiep/${listing.parkSlug}`}
className="line-clamp-1 hover:text-primary hover:underline"
>
{listing.parkName}
</a>
</div>
{/* Stats grid */}
<div className="mb-3 grid grid-cols-2 gap-3">
<div className="rounded-md bg-muted p-2">
<div className="flex items-center gap-1 text-xs text-muted-foreground">
<Ruler className="h-3 w-3" />
Diện tích
</div>
<div className="font-semibold">{listing.areaM2.toLocaleString()} m²</div>
</div>
<div className="rounded-md bg-muted p-2">
<div className="text-xs text-muted-foreground">Giá thuê</div>
<div className="font-semibold text-primary">{priceText}</div>
</div>
</div>
{/* Additional info */}
<div className="flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
{listing.ceilingHeightM && (
<span>Cao trần: {listing.ceilingHeightM}m</span>
)}
{leaseTermText && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{leaseTermText}
</span>
)}
{listing.viewCount > 0 && (
<span className="flex items-center gap-1">
<Eye className="h-3 w-3" />
{listing.viewCount}
</span>
)}
</div>
</CardContent>
</Card>
);
}