From 1668c800feb9b7d43137d7465d2a623ca35e9f24 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 22 Apr 2026 15:49:38 +0700 Subject: [PATCH] fix(web): resolve all 22 TypeScript typecheck errors in apps/web (TEC-3208) - Fix TS4111: use bracket notation for index signature access in metadata.spec.ts, neighborhood-poi-map.tsx, and neighborhood-poi-map.spec.tsx - Fix TS2740: add missing property fields (usableAreaM2, floor, totalFloors, nearbyPOIs, etc.) to test mock objects in 5 spec files - Fix TS2339: add missing estimate() and create() methods to transferApi - Fix TS4114: add override modifier to render() in page.tsx error boundary - Fix TS2532: add optional chaining for possibly undefined features in neighborhood-poi-map.tsx Co-Authored-By: Paperclip --- .../listings/[id]/__tests__/metadata.spec.ts | 16 ++++++++-------- apps/web/app/[locale]/(public)/page.tsx | 2 +- .../__tests__/comparison-table.spec.tsx | 14 ++++++++++++++ .../__tests__/neighborhood-poi-map.spec.tsx | 2 +- .../neighborhood/neighborhood-poi-map.tsx | 8 ++++---- .../search/__tests__/property-card.spec.tsx | 14 ++++++++++++++ .../search/__tests__/search-results.spec.tsx | 14 ++++++++++++++ .../components/seo/__tests__/json-ld.spec.tsx | 14 ++++++++++++++ apps/web/lib/__tests__/comparison-store.spec.ts | 14 ++++++++++++++ apps/web/lib/chuyen-nhuong-api.ts | 13 +++++++++++++ 10 files changed, 97 insertions(+), 14 deletions(-) diff --git a/apps/web/app/[locale]/(public)/listings/[id]/__tests__/metadata.spec.ts b/apps/web/app/[locale]/(public)/listings/[id]/__tests__/metadata.spec.ts index 413d4fe..983203c 100644 --- a/apps/web/app/[locale]/(public)/listings/[id]/__tests__/metadata.spec.ts +++ b/apps/web/app/[locale]/(public)/listings/[id]/__tests__/metadata.spec.ts @@ -119,17 +119,17 @@ describe('listing page generateMetadata', () => { expect(meta.alternates?.languages?.en).toMatch(/\/en\/listings\/listing-1$/); const og = meta.openGraph as Record; - expect(og.type).toBe('article'); - expect(og.locale).toBe('vi_VN'); - expect(og.siteName).toBe('GoodGo'); - const ogImages = og.images as Array<{ url: string; width: number; height: number }>; + expect(og['type']).toBe('article'); + expect(og['locale']).toBe('vi_VN'); + expect(og['siteName']).toBe('GoodGo'); + const ogImages = og['images'] as Array<{ url: string; width: number; height: number }>; expect(ogImages[0]?.url).toBe('https://cdn.example.com/img1.jpg'); expect(ogImages[0]?.width).toBe(1200); expect(ogImages[0]?.height).toBe(630); const twitter = meta.twitter as Record; - expect(twitter.card).toBe('summary_large_image'); - expect((twitter.images as string[])[0]).toBe('https://cdn.example.com/img1.jpg'); + expect(twitter['card']).toBe('summary_large_image'); + expect((twitter['images'] as string[])[0]).toBe('https://cdn.example.com/img1.jpg'); expect(meta.other?.['og:price:currency']).toBe('VND'); expect(meta.other?.['og:price:amount']).toBe('3500000000'); @@ -146,8 +146,8 @@ describe('listing page generateMetadata', () => { }); const og = meta.openGraph as Record; - expect(og.locale).toBe('en_US'); - const ogImages = og.images as Array<{ url: string }>; + expect(og['locale']).toBe('en_US'); + const ogImages = og['images'] as Array<{ url: string }>; expect(ogImages[0]?.url).toBe('/og-image.png'); }); }); diff --git a/apps/web/app/[locale]/(public)/page.tsx b/apps/web/app/[locale]/(public)/page.tsx index d9a5b7d..9f28985 100644 --- a/apps/web/app/[locale]/(public)/page.tsx +++ b/apps/web/app/[locale]/(public)/page.tsx @@ -71,7 +71,7 @@ class SectionErrorBoundary extends React.Component< return { hasError: true }; } - render() { + override render() { if (this.state.hasError) { return (
diff --git a/apps/web/components/comparison/__tests__/comparison-table.spec.tsx b/apps/web/components/comparison/__tests__/comparison-table.spec.tsx index d5e326b..460570c 100644 --- a/apps/web/components/comparison/__tests__/comparison-table.spec.tsx +++ b/apps/web/components/comparison/__tests__/comparison-table.spec.tsx @@ -102,6 +102,20 @@ function makeListing(id: string, overrides: Partial = {}): Listin latitude: null, longitude: null, media: [{ id: 'm1', type: 'image', url: 'https://example.com/img.jpg', order: 0, caption: null }], + usableAreaM2: null, + floor: null, + totalFloors: null, + nearbyPOIs: null, + metroDistanceM: null, + furnishing: null, + propertyCondition: null, + balconyDirection: null, + maintenanceFeeVND: null, + parkingSlots: null, + viewType: [], + petFriendly: null, + suitableFor: [], + whyThisLocation: null, }, seller: { id: 's1', fullName: 'Seller', phone: '0901234567' }, agent: null, diff --git a/apps/web/components/neighborhood/__tests__/neighborhood-poi-map.spec.tsx b/apps/web/components/neighborhood/__tests__/neighborhood-poi-map.spec.tsx index 208d1b2..a8c1cd8 100644 --- a/apps/web/components/neighborhood/__tests__/neighborhood-poi-map.spec.tsx +++ b/apps/web/components/neighborhood/__tests__/neighborhood-poi-map.spec.tsx @@ -228,7 +228,7 @@ describe('NeighborhoodPOIMap', () => { expect(capturedData).not.toBeNull(); expect(capturedData!.features).toHaveLength(2); - expect(capturedData!.features.map((f) => f.properties?.category)).not.toContain('school'); + expect(capturedData!.features.map((f) => f.properties?.['category'])).not.toContain('school'); }); // ── Loading state ───────────────────────────────────────────────────────────── diff --git a/apps/web/components/neighborhood/neighborhood-poi-map.tsx b/apps/web/components/neighborhood/neighborhood-poi-map.tsx index eb56133..a93f1f8 100644 --- a/apps/web/components/neighborhood/neighborhood-poi-map.tsx +++ b/apps/web/components/neighborhood/neighborhood-poi-map.tsx @@ -264,12 +264,12 @@ export function NeighborhoodPOIMap({ map.on('click', LAYER_CLUSTERS, (e) => { const features = map.queryRenderedFeatures(e.point, { layers: [LAYER_CLUSTERS] }); if (!features.length) return; - const clusterId = features[0].properties?.cluster_id as number; + const clusterId = features[0]?.properties?.['cluster_id'] as number; (map.getSource(SOURCE_ID) as mapboxgl.GeoJSONSource).getClusterExpansionZoom( clusterId, (err, expansionZoom) => { if (err || expansionZoom == null) return; - const coords = (features[0].geometry as GeoJSON.Point).coordinates as [number, number]; + const coords = (features[0]?.geometry as GeoJSON.Point).coordinates as [number, number]; map.easeTo({ center: coords, zoom: expansionZoom }); }, ); @@ -279,8 +279,8 @@ export function NeighborhoodPOIMap({ map.on('click', LAYER_UNCLUSTERED, (e) => { const features = map.queryRenderedFeatures(e.point, { layers: [LAYER_UNCLUSTERED] }); if (!features.length) return; - const { name, categoryLabel, distance } = features[0].properties ?? {}; - const coords = (features[0].geometry as GeoJSON.Point).coordinates.slice() as [ + const { name, categoryLabel, distance } = features[0]?.properties ?? {}; + const coords = (features[0]?.geometry as GeoJSON.Point).coordinates.slice() as [ number, number, ]; diff --git a/apps/web/components/search/__tests__/property-card.spec.tsx b/apps/web/components/search/__tests__/property-card.spec.tsx index b8c1598..c0aef5b 100644 --- a/apps/web/components/search/__tests__/property-card.spec.tsx +++ b/apps/web/components/search/__tests__/property-card.spec.tsx @@ -70,6 +70,20 @@ function makeListing(overrides: Partial = {}): ListingDetail { { id: 'media-1', type: 'image', url: 'https://example.com/img1.jpg', order: 0, caption: null }, { id: 'media-2', type: 'image', url: 'https://example.com/img2.jpg', order: 1, caption: null }, ], + usableAreaM2: null, + floor: null, + totalFloors: null, + nearbyPOIs: null, + metroDistanceM: null, + furnishing: null, + propertyCondition: null, + balconyDirection: null, + maintenanceFeeVND: null, + parkingSlots: null, + viewType: [], + petFriendly: null, + suitableFor: [], + whyThisLocation: null, }, seller: { id: 'seller-1', diff --git a/apps/web/components/search/__tests__/search-results.spec.tsx b/apps/web/components/search/__tests__/search-results.spec.tsx index a3000e6..859c76e 100644 --- a/apps/web/components/search/__tests__/search-results.spec.tsx +++ b/apps/web/components/search/__tests__/search-results.spec.tsx @@ -49,6 +49,20 @@ function makeListing(id: string): ListingDetail { latitude: null, longitude: null, media: [], + usableAreaM2: null, + floor: null, + totalFloors: null, + nearbyPOIs: null, + metroDistanceM: null, + furnishing: null, + propertyCondition: null, + balconyDirection: null, + maintenanceFeeVND: null, + parkingSlots: null, + viewType: [], + petFriendly: null, + suitableFor: [], + whyThisLocation: null, }, seller: { id: 's1', fullName: 'Seller', phone: '0901234567' }, agent: null, diff --git a/apps/web/components/seo/__tests__/json-ld.spec.tsx b/apps/web/components/seo/__tests__/json-ld.spec.tsx index 8e9365c..ed0d2e0 100644 --- a/apps/web/components/seo/__tests__/json-ld.spec.tsx +++ b/apps/web/components/seo/__tests__/json-ld.spec.tsx @@ -126,6 +126,20 @@ describe('generateListingJsonLd', () => { latitude: 10.73, longitude: 106.72, media: [{ id: 'media-1', type: 'image', url: 'https://example.com/img1.jpg', order: 0, caption: null }], + usableAreaM2: null, + floor: null, + totalFloors: null, + nearbyPOIs: null, + metroDistanceM: null, + furnishing: null, + propertyCondition: null, + balconyDirection: null, + maintenanceFeeVND: null, + parkingSlots: null, + viewType: [], + petFriendly: null, + suitableFor: [], + whyThisLocation: null, }, seller: { id: 'seller-1', fullName: 'Seller', phone: '0912345678' }, agent: null, diff --git a/apps/web/lib/__tests__/comparison-store.spec.ts b/apps/web/lib/__tests__/comparison-store.spec.ts index 4322607..03585fb 100644 --- a/apps/web/lib/__tests__/comparison-store.spec.ts +++ b/apps/web/lib/__tests__/comparison-store.spec.ts @@ -262,6 +262,20 @@ function makeListing( latitude: null, longitude: null, media: [], + usableAreaM2: null, + floor: null, + totalFloors: null, + nearbyPOIs: null, + metroDistanceM: null, + furnishing: null, + propertyCondition: null, + balconyDirection: null, + maintenanceFeeVND: null, + parkingSlots: null, + viewType: [], + petFriendly: null, + suitableFor: [], + whyThisLocation: null, }, seller: { id: 'seller-1', diff --git a/apps/web/lib/chuyen-nhuong-api.ts b/apps/web/lib/chuyen-nhuong-api.ts index da2dc12..03009e4 100644 --- a/apps/web/lib/chuyen-nhuong-api.ts +++ b/apps/web/lib/chuyen-nhuong-api.ts @@ -8,6 +8,7 @@ import { type LucideIcon, } from 'lucide-react'; import { apiClient } from './api-client'; +import type { AiEstimateResult } from './transfer-wizard-store'; // ─── Types ────────────────────────────────────────────── @@ -185,4 +186,16 @@ export const transferApi = { getStats: () => apiClient.get('/transfer/stats'), + + estimate: (payload: { category: TransferCategory; condition: TransferCondition; originalPriceVND: number; purchaseYear: number }[]) => + apiClient.post( + '/transfer/estimate', + payload, + ), + + create: (payload: Record) => + apiClient.post<{ listingId: string; status: string }>( + '/transfer/listings', + payload, + ), };