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 <noreply@paperclip.ing>
This commit is contained in:
@@ -119,17 +119,17 @@ describe('listing page generateMetadata', () => {
|
|||||||
expect(meta.alternates?.languages?.en).toMatch(/\/en\/listings\/listing-1$/);
|
expect(meta.alternates?.languages?.en).toMatch(/\/en\/listings\/listing-1$/);
|
||||||
|
|
||||||
const og = meta.openGraph as Record<string, unknown>;
|
const og = meta.openGraph as Record<string, unknown>;
|
||||||
expect(og.type).toBe('article');
|
expect(og['type']).toBe('article');
|
||||||
expect(og.locale).toBe('vi_VN');
|
expect(og['locale']).toBe('vi_VN');
|
||||||
expect(og.siteName).toBe('GoodGo');
|
expect(og['siteName']).toBe('GoodGo');
|
||||||
const ogImages = og.images as Array<{ url: string; width: number; height: number }>;
|
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]?.url).toBe('https://cdn.example.com/img1.jpg');
|
||||||
expect(ogImages[0]?.width).toBe(1200);
|
expect(ogImages[0]?.width).toBe(1200);
|
||||||
expect(ogImages[0]?.height).toBe(630);
|
expect(ogImages[0]?.height).toBe(630);
|
||||||
|
|
||||||
const twitter = meta.twitter as Record<string, unknown>;
|
const twitter = meta.twitter as Record<string, unknown>;
|
||||||
expect(twitter.card).toBe('summary_large_image');
|
expect(twitter['card']).toBe('summary_large_image');
|
||||||
expect((twitter.images as string[])[0]).toBe('https://cdn.example.com/img1.jpg');
|
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:currency']).toBe('VND');
|
||||||
expect(meta.other?.['og:price:amount']).toBe('3500000000');
|
expect(meta.other?.['og:price:amount']).toBe('3500000000');
|
||||||
@@ -146,8 +146,8 @@ describe('listing page generateMetadata', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const og = meta.openGraph as Record<string, unknown>;
|
const og = meta.openGraph as Record<string, unknown>;
|
||||||
expect(og.locale).toBe('en_US');
|
expect(og['locale']).toBe('en_US');
|
||||||
const ogImages = og.images as Array<{ url: string }>;
|
const ogImages = og['images'] as Array<{ url: string }>;
|
||||||
expect(ogImages[0]?.url).toBe('/og-image.png');
|
expect(ogImages[0]?.url).toBe('/og-image.png');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class SectionErrorBoundary extends React.Component<
|
|||||||
return { hasError: true };
|
return { hasError: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
override render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2 rounded-md border border-border bg-background-surface p-4 text-sm text-foreground-muted">
|
<div className="flex items-center gap-2 rounded-md border border-border bg-background-surface p-4 text-sm text-foreground-muted">
|
||||||
|
|||||||
@@ -102,6 +102,20 @@ function makeListing(id: string, overrides: Partial<ListingDetail> = {}): Listin
|
|||||||
latitude: null,
|
latitude: null,
|
||||||
longitude: null,
|
longitude: null,
|
||||||
media: [{ id: 'm1', type: 'image', url: 'https://example.com/img.jpg', order: 0, caption: 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' },
|
seller: { id: 's1', fullName: 'Seller', phone: '0901234567' },
|
||||||
agent: null,
|
agent: null,
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ describe('NeighborhoodPOIMap', () => {
|
|||||||
|
|
||||||
expect(capturedData).not.toBeNull();
|
expect(capturedData).not.toBeNull();
|
||||||
expect(capturedData!.features).toHaveLength(2);
|
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 ─────────────────────────────────────────────────────────────
|
// ── Loading state ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -264,12 +264,12 @@ export function NeighborhoodPOIMap({
|
|||||||
map.on('click', LAYER_CLUSTERS, (e) => {
|
map.on('click', LAYER_CLUSTERS, (e) => {
|
||||||
const features = map.queryRenderedFeatures(e.point, { layers: [LAYER_CLUSTERS] });
|
const features = map.queryRenderedFeatures(e.point, { layers: [LAYER_CLUSTERS] });
|
||||||
if (!features.length) return;
|
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(
|
(map.getSource(SOURCE_ID) as mapboxgl.GeoJSONSource).getClusterExpansionZoom(
|
||||||
clusterId,
|
clusterId,
|
||||||
(err, expansionZoom) => {
|
(err, expansionZoom) => {
|
||||||
if (err || expansionZoom == null) return;
|
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 });
|
map.easeTo({ center: coords, zoom: expansionZoom });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -279,8 +279,8 @@ export function NeighborhoodPOIMap({
|
|||||||
map.on('click', LAYER_UNCLUSTERED, (e) => {
|
map.on('click', LAYER_UNCLUSTERED, (e) => {
|
||||||
const features = map.queryRenderedFeatures(e.point, { layers: [LAYER_UNCLUSTERED] });
|
const features = map.queryRenderedFeatures(e.point, { layers: [LAYER_UNCLUSTERED] });
|
||||||
if (!features.length) return;
|
if (!features.length) return;
|
||||||
const { name, categoryLabel, distance } = features[0].properties ?? {};
|
const { name, categoryLabel, distance } = features[0]?.properties ?? {};
|
||||||
const coords = (features[0].geometry as GeoJSON.Point).coordinates.slice() as [
|
const coords = (features[0]?.geometry as GeoJSON.Point).coordinates.slice() as [
|
||||||
number,
|
number,
|
||||||
number,
|
number,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ function makeListing(overrides: Partial<ListingDetail> = {}): ListingDetail {
|
|||||||
{ id: 'media-1', type: 'image', url: 'https://example.com/img1.jpg', order: 0, caption: null },
|
{ 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 },
|
{ 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: {
|
seller: {
|
||||||
id: 'seller-1',
|
id: 'seller-1',
|
||||||
|
|||||||
@@ -49,6 +49,20 @@ function makeListing(id: string): ListingDetail {
|
|||||||
latitude: null,
|
latitude: null,
|
||||||
longitude: null,
|
longitude: null,
|
||||||
media: [],
|
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' },
|
seller: { id: 's1', fullName: 'Seller', phone: '0901234567' },
|
||||||
agent: null,
|
agent: null,
|
||||||
|
|||||||
@@ -126,6 +126,20 @@ describe('generateListingJsonLd', () => {
|
|||||||
latitude: 10.73,
|
latitude: 10.73,
|
||||||
longitude: 106.72,
|
longitude: 106.72,
|
||||||
media: [{ id: 'media-1', type: 'image', url: 'https://example.com/img1.jpg', order: 0, caption: null }],
|
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' },
|
seller: { id: 'seller-1', fullName: 'Seller', phone: '0912345678' },
|
||||||
agent: null,
|
agent: null,
|
||||||
|
|||||||
@@ -262,6 +262,20 @@ function makeListing(
|
|||||||
latitude: null,
|
latitude: null,
|
||||||
longitude: null,
|
longitude: null,
|
||||||
media: [],
|
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: {
|
seller: {
|
||||||
id: 'seller-1',
|
id: 'seller-1',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { apiClient } from './api-client';
|
import { apiClient } from './api-client';
|
||||||
|
import type { AiEstimateResult } from './transfer-wizard-store';
|
||||||
|
|
||||||
// ─── Types ──────────────────────────────────────────────
|
// ─── Types ──────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -185,4 +186,16 @@ export const transferApi = {
|
|||||||
|
|
||||||
getStats: () =>
|
getStats: () =>
|
||||||
apiClient.get<TransferStats>('/transfer/stats'),
|
apiClient.get<TransferStats>('/transfer/stats'),
|
||||||
|
|
||||||
|
estimate: (payload: { category: TransferCategory; condition: TransferCondition; originalPriceVND: number; purchaseYear: number }[]) =>
|
||||||
|
apiClient.post<AiEstimateResult>(
|
||||||
|
'/transfer/estimate',
|
||||||
|
payload,
|
||||||
|
),
|
||||||
|
|
||||||
|
create: (payload: Record<string, unknown>) =>
|
||||||
|
apiClient.post<{ listingId: string; status: string }>(
|
||||||
|
'/transfer/listings',
|
||||||
|
payload,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user