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:
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
industrialApi,
|
||||
type SearchIndustrialListingsParams,
|
||||
type SearchIndustrialParksParams,
|
||||
} from '@/lib/khu-cong-nghiep-api';
|
||||
|
||||
@@ -11,6 +12,7 @@ export const industrialKeys = {
|
||||
stats: () => ['industrial', 'stats'] as const,
|
||||
market: () => ['industrial', 'market'] as const,
|
||||
compare: (ids: string[]) => ['industrial', 'compare', ids] as const,
|
||||
listings: (params: SearchIndustrialListingsParams) => ['industrial', 'listings', params] as const,
|
||||
};
|
||||
|
||||
export function useIndustrialParksSearch(params: SearchIndustrialParksParams = {}) {
|
||||
@@ -51,3 +53,10 @@ export function useIndustrialCompare(ids: string[]) {
|
||||
enabled: ids.length >= 2,
|
||||
});
|
||||
}
|
||||
|
||||
export function useIndustrialListingsSearch(params: SearchIndustrialListingsParams = {}) {
|
||||
return useQuery({
|
||||
queryKey: industrialKeys.listings(params),
|
||||
queryFn: () => industrialApi.searchListings(params),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,6 +90,65 @@ export interface IndustrialMarketData {
|
||||
rentByProvince: { province: string; avgLandRent: number | null; avgRbfRent: number | null; parkCount: number }[];
|
||||
}
|
||||
|
||||
// ─── Industrial Listing Types ───────────────────────────
|
||||
|
||||
export type IndustrialPropertyType =
|
||||
| 'INDUSTRIAL_LAND'
|
||||
| 'READY_BUILT_FACTORY'
|
||||
| 'READY_BUILT_WAREHOUSE'
|
||||
| 'LOGISTICS_CENTER'
|
||||
| 'OFFICE_IN_PARK'
|
||||
| 'DATA_CENTER';
|
||||
|
||||
export type IndustrialLeaseType =
|
||||
| 'LAND_LEASE'
|
||||
| 'FACTORY_LEASE'
|
||||
| 'WAREHOUSE_LEASE'
|
||||
| 'SUBLEASE';
|
||||
|
||||
export type IndustrialListingStatus =
|
||||
| 'DRAFT'
|
||||
| 'ACTIVE'
|
||||
| 'RESERVED'
|
||||
| 'LEASED'
|
||||
| 'EXPIRED';
|
||||
|
||||
export interface IndustrialListingItem {
|
||||
id: string;
|
||||
parkId: string;
|
||||
parkName: string;
|
||||
parkSlug: string;
|
||||
propertyType: IndustrialPropertyType;
|
||||
leaseType: IndustrialLeaseType;
|
||||
status: IndustrialListingStatus;
|
||||
title: string;
|
||||
description: string | null;
|
||||
areaM2: number;
|
||||
ceilingHeightM: number | null;
|
||||
priceUsdM2: number | null;
|
||||
pricingUnit: string | null;
|
||||
totalLeasePrice: number | null;
|
||||
minLeaseYears: number | null;
|
||||
maxLeaseYears: number | null;
|
||||
availableFrom: string | null;
|
||||
media: { url: string; type: string; caption?: string }[] | null;
|
||||
viewCount: number;
|
||||
publishedAt: string | null;
|
||||
}
|
||||
|
||||
export interface SearchIndustrialListingsParams {
|
||||
parkId?: string;
|
||||
propertyType?: IndustrialPropertyType;
|
||||
leaseType?: IndustrialLeaseType;
|
||||
minAreaM2?: number;
|
||||
maxAreaM2?: number;
|
||||
minPriceUsdM2?: number;
|
||||
maxPriceUsdM2?: number;
|
||||
q?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
@@ -132,6 +191,22 @@ export const REGION_LABELS: Record<VietnamRegion, string> = {
|
||||
SOUTH: 'Miền Nam',
|
||||
};
|
||||
|
||||
export const PROPERTY_TYPE_LABELS: Record<IndustrialPropertyType, string> = {
|
||||
INDUSTRIAL_LAND: 'Đất công nghiệp',
|
||||
READY_BUILT_FACTORY: 'Nhà xưởng xây sẵn',
|
||||
READY_BUILT_WAREHOUSE: 'Kho bãi xây sẵn',
|
||||
LOGISTICS_CENTER: 'Trung tâm logistics',
|
||||
OFFICE_IN_PARK: 'Văn phòng trong KCN',
|
||||
DATA_CENTER: 'Trung tâm dữ liệu',
|
||||
};
|
||||
|
||||
export const LEASE_TYPE_LABELS: Record<IndustrialLeaseType, string> = {
|
||||
LAND_LEASE: 'Thuê đất',
|
||||
FACTORY_LEASE: 'Thuê nhà xưởng',
|
||||
WAREHOUSE_LEASE: 'Thuê kho bãi',
|
||||
SUBLEASE: 'Cho thuê lại',
|
||||
};
|
||||
|
||||
// ─── API Functions ──────────────────────────────────────
|
||||
|
||||
export const industrialApi = {
|
||||
@@ -157,4 +232,15 @@ export const industrialApi = {
|
||||
|
||||
getMarket: () =>
|
||||
apiClient.get<IndustrialMarketData>('/industrial/market'),
|
||||
|
||||
searchListings: (params: SearchIndustrialListingsParams = {}) => {
|
||||
const query = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== '') query.append(key, String(value));
|
||||
});
|
||||
const qs = query.toString();
|
||||
return apiClient.get<PaginatedResult<IndustrialListingItem>>(
|
||||
`/industrial/listings${qs ? `?${qs}` : ''}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user