diff --git a/packages/map-sdk/README.md b/packages/map-sdk/README.md
index f979f7c..76688a5 100644
--- a/packages/map-sdk/README.md
+++ b/packages/map-sdk/README.md
@@ -18,7 +18,7 @@ import React from 'react';
import { MapView, Marker, useLocationTracking, Polyline } from '@lynkedup/map-sdk';
export default function App() {
- const { isTracking, path, startTracking, stopTracking, clear } = useLocationTracking();
+ const { isTracking, path, startTracking, stopTracking, clear } = useLocationTracking(/* opts?: TrackingOptions, geolocation?: GeolocationAPI */);
return (
@@ -27,6 +27,8 @@ export default function App() {
);
}
+
+> Tip: `useLocationTracking()` accepts an optional `TrackingOptions` object and an optional `GeolocationAPI` (handy for testing with a mocked geolocation provider). Ensure you request platform location permissions before starting tracking.
```
Tracking notes:
@@ -103,11 +105,14 @@ export default function PropertiesScreen() {
Props summary:
-- `apiUrl?: string` — POST endpoint used for property searches (defaults to the demo host).
-- `initialRegion?: Region` — starting region.
-- `radius?: number` — search radius in meters (default: 500).
-- `showSearch?: boolean` — render search bar (default: true).
-- `onSelectProperty?: (item) => void` — callback for property selection.
+| Prop | Type | Default | Description |
+| ---- | ---- | ------- | ----------- |
+| `apiUrl` | `string` | demo host | POST endpoint used for property searches. Should accept `{ location, radius }` and return `{ success, properties, location }`.
+| `initialRegion` | `Region` | `DEFAULT_REGION` | Starting region for the map.
+| `radius` | `number` | `500` | Search radius in meters.
+| `showSearch` | `boolean` | `true` | Show or hide the built-in search bar.
+| `onSelectProperty` | `(item: PropertyItem) => void` | — | Callback invoked when a property is selected.
+| `mapProps` | `Partial` | — | Props forwarded to the underlying `react-native-maps` view (e.g., `mapType`, `showsMyLocationButton`).
---
diff --git a/packages/map-sdk/src/components/PriceMarker.tsx b/packages/map-sdk/src/components/PriceMarker.tsx
index a9b3e29..5e62682 100644
--- a/packages/map-sdk/src/components/PriceMarker.tsx
+++ b/packages/map-sdk/src/components/PriceMarker.tsx
@@ -2,41 +2,53 @@ import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Marker } from 'react-native-maps';
+/**
+ * Minimal property item shape used by the SDK components.
+ */
export type PropertyItem = {
id: string | number;
location?: { lat: number; lng: number };
price?: number;
price_display?: string;
+ address?: string;
+ bedrooms?: number;
+ bathrooms?: number;
+ polygon?: number[][]; // [[lat, lng], ...]
};
+/**
+ * Simple price formatter that returns human-readable price strings (K/M abbreviations).
+ */
export const formatPrice = (value?: number | string) => {
- if (!value) return '';
+ if (value === null || value === undefined || value === '') return '';
const num = typeof value === 'string' ? Number(value) : value;
- if (!num) return String(value);
+ if (!num && num !== 0) return String(value);
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
if (num >= 1000) return `${Math.round(num / 1000)}K`;
return num.toString();
};
-const PriceMarker: React.FC<{
+export type PriceMarkerProps = {
item: PropertyItem;
selected?: boolean;
onPress?: (item: PropertyItem) => void;
-}> = ({ item, selected, onPress }) => {
- if (!item?.location) return null;
+};
- return (
- {
- e.stopPropagation();
- onPress && onPress(item);
- }}
+/**
+ * A small marker component that displays a price label and forwards press events.
+ */
+const PriceMarker: React.FC = ({ item, selected, onPress }) => {
+ coordinate = {{ latitude: item.location.lat, longitude: item.location.lng }
+}
+onPress = {(e) => {
+ e.stopPropagation();
+ onPress && onPress(item);
+}}
>
-
- {item.price_display ?? formatPrice(item.price)}
-
-
+
+ {item.price_display ?? formatPrice(item.price)}
+
+
);
};
diff --git a/packages/map-sdk/src/components/PropertyMap.tsx b/packages/map-sdk/src/components/PropertyMap.tsx
index 807d772..9f8f3b4 100644
--- a/packages/map-sdk/src/components/PropertyMap.tsx
+++ b/packages/map-sdk/src/components/PropertyMap.tsx
@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react';
import {
View,
StyleSheet,
- Platform,
Text,
FlatList,
Image,
@@ -13,21 +12,32 @@ import {
TouchableOpacity,
} from 'react-native';
import ClusteredMapView from 'react-native-map-clustering';
-import { Polygon } from 'react-native-maps';
-import Geolocation from '@react-native-community/geolocation';
+import RNMapView, { Region, Polygon, LatLng } from 'react-native-maps';
+import type { MapViewProps as RNMapViewProps } from 'react-native-maps';
import PriceMarker, { formatPrice, PropertyItem } from './PriceMarker';
const { width } = Dimensions.get('window');
+/**
+ * Props for the `PropertyMap` high-level component.
+ */
export type PropertyMapProps = {
+ /** API endpoint used for POST property searches. Should accept `{ location, radius }` and return `{ success, properties, location }` */
apiUrl?: string;
- initialRegion?: any;
+ /** Initial map region */
+ initialRegion?: Region;
+ /** Search radius in meters (default: 500) */
radius?: number;
+ /** Show/hide the search bar */
showSearch?: boolean;
+ /** Callback when a property/item is selected */
onSelectProperty?: (item: PropertyItem) => void;
+ /** Optional props forwarded to the underlying map view */
+ mapProps?: Partial;
};
-export const DEFAULT_REGION = {
+/** Default region used when none is provided */
+export const DEFAULT_REGION: Region = {
latitude: 25.48131,
longitude: 81.854249,
latitudeDelta: 0.02,
@@ -35,20 +45,24 @@ export const DEFAULT_REGION = {
};
export const getPolygonCoords = (polygon?: number[][]) => {
- if (!polygon) return [];
+ if (!polygon) return [] as LatLng[];
return polygon.map(([lat, lng]) => ({ latitude: lat, longitude: lng }));
};
+/**
+ * `PropertyMap` — a ready-to-use map screen for property searches. Includes clustering, polygon rendering and a bottom property card.
+ */
const PropertyMap: React.FC = ({
apiUrl = 'http://64.227.108.180:5000/property-search',
initialRegion = DEFAULT_REGION,
radius = 500,
showSearch = true,
onSelectProperty,
+ mapProps = {},
}) => {
- const mapRef = useRef(null);
+ const mapRef = useRef(null);
- const [region, setRegion] = useState(initialRegion);
+ const [region, setRegion] = useState(initialRegion);
const [properties, setProperties] = useState([]);
const [selectedItem, setSelectedItem] = useState(null);
const [loading, setLoading] = useState(false);
@@ -70,12 +84,16 @@ const PropertyMap: React.FC = ({
const requestLocation = async () => {
try {
- Geolocation.getCurrentPosition((pos) => {
- const { latitude, longitude } = pos.coords;
- const newRegion = { latitude, longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 };
- setRegion(newRegion);
- mapRef.current?.animateToRegion?.(newRegion, 1000);
- });
+ // prefer bundled community geolocation if available
+ const geo = (global.navigator && (global.navigator as any).geolocation) || null;
+ if (geo?.getCurrentPosition) {
+ geo.getCurrentPosition((pos: any) => {
+ const { latitude, longitude } = pos.coords;
+ const newRegion = { latitude, longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 } as Region;
+ setRegion(newRegion);
+ mapRef.current?.animateToRegion?.(newRegion, 1000);
+ });
+ }
} catch (err) {
// ignore
}
@@ -96,13 +114,13 @@ const PropertyMap: React.FC = ({
const json = await response.json();
if (json?.success && json?.location?.coordinates?.lat && json?.location?.coordinates?.lng) {
- const newRegion = {
+ const newRegion: Region = {
latitude: json.location.coordinates.lat,
longitude: json.location.coordinates.lng,
latitudeDelta: 0.02,
longitudeDelta: 0.02,
};
- setProperties(json.properties || []);
+ setProperties((json.properties as PropertyItem[]) || []);
setRegion(newRegion);
mapRef.current?.animateToRegion?.(newRegion, 1000);
}
@@ -149,7 +167,7 @@ const PropertyMap: React.FC = ({
ref={mapRef}
style={StyleSheet.absoluteFillObject}
region={region}
- onRegionChangeComplete={(r: any) => {
+ onRegionChangeComplete={(r: Region) => {
if (!r?.longitudeDelta) return;
setRegion(r);
}}
@@ -157,53 +175,54 @@ const PropertyMap: React.FC = ({
animationEnabled
clusterPressMaxZoom={16}
spiralEnabled={false}
- >
- {properties.map((item) =>
- (item as any).polygon ? (
-
- ) : null
+ {...mapProps}
+ key={`poly-${item.id}`}
+ coordinates={getPolygonCoords((item as any).polygon)}
+ strokeColor={selectedItem?.id === item.id ? '#F4C430' : '#8B0000'}
+ fillColor="rgba(139, 0, 0, 0.15)"
+ strokeWidth={2}
+ />
+ ) : null
)}
- {properties.map((item) => (
-
- ))}
-
+ {properties.map((item) => (
+
+ ))}
+
- {loading && (
-
-
-
- )}
+ {
+ loading && (
+
+
+
+ )
+ }
- {selectedItem && (
-
- String(item.id)}
- showsHorizontalScrollIndicator={false}
- renderItem={({ item }) => (
-
-
-
- {item.price_display ?? formatPrice(item.price)}
- {(item as any).bedrooms} Beds • {(item as any).bathrooms} Baths
-
- {(item as any).address}
-
-
+ {
+ selectedItem && (
+
+ String(item.id)}
+ showsHorizontalScrollIndicator={false}
+ renderItem={({ item }) => (
+
+
+
+ {item.price_display ?? formatPrice(item.price)}
+ {(item as any).bedrooms} Beds • {(item as any).bathrooms} Baths
+
+ {(item as any).address}
+
- )}
- />
-
- )}
-
+
+ )}
+ />
+
+ )
+ }
+
);
};
diff --git a/packages/map-sdk/src/hooks/useLocationTracking.ts b/packages/map-sdk/src/hooks/useLocationTracking.ts
index 02226f4..de4816c 100644
--- a/packages/map-sdk/src/hooks/useLocationTracking.ts
+++ b/packages/map-sdk/src/hooks/useLocationTracking.ts
@@ -1,22 +1,49 @@
import { useRef, useState, useCallback } from 'react';
import type { LatLng } from 'react-native-maps';
+/**
+ * Options for location tracking.
+ * - distanceFilter: minimum change in meters to receive updates.
+ * - interval / fastestInterval: Android/JS timers (ms).
+ * - accuracy: 'high' enables high accuracy via GPS.
+ */
export type TrackingOptions = {
- distanceFilter?: number; // meters
- interval?: number; // ms
- fastestInterval?: number; // ms
+ distanceFilter?: number;
+ interval?: number;
+ fastestInterval?: number;
accuracy?: 'high' | 'balanced' | 'low';
};
-export function useLocationTracking(opts: TrackingOptions = {}) {
+/**
+ * Minimal Geolocation API interface (compatible with browser and @react-native-community/geolocation).
+ */
+export type GeolocationAPI = {
+ watchPosition: (
+ success: (pos: { coords: { latitude: number; longitude: number; accuracy?: number } }) => void,
+ error?: (err: any) => void,
+ options?: { enableHighAccuracy?: boolean; distanceFilter?: number; interval?: number }
+ ) => number | string;
+ clearWatch: (id: number | string) => void;
+};
+
+/**
+ * Hook to track user's location path.
+ *
+ * @param opts TrackingOptions - config for watchPosition
+ * @param geolocation optional custom geolocation implementation (useful for testing)
+ * @returns object with tracking state and control functions
+ */
+export function useLocationTracking(opts: TrackingOptions = {}, geolocation?: GeolocationAPI) {
const [isTracking, setIsTracking] = useState(false);
const [path, setPath] = useState([]);
const [current, setCurrent] = useState(null);
- const watchIdRef = useRef(null);
+ const watchIdRef = useRef(null);
- const onPosition = useCallback((pos: any) => {
- const lat = pos.coords?.latitude;
- const lng = pos.coords?.longitude;
+ const geo = geolocation ?? (global.navigator && (global.navigator as any).geolocation) as GeolocationAPI | undefined;
+
+ const onPosition = useCallback((pos: { coords: { latitude: number; longitude: number } }) => {
+ const lat = pos?.coords?.latitude;
+ const lng = pos?.coords?.longitude;
if (typeof lat === 'number' && typeof lng === 'number') {
const point = { latitude: lat, longitude: lng } as LatLng;
setPath((p) => [...p, point]);
@@ -26,33 +53,33 @@ export function useLocationTracking(opts: TrackingOptions = {}) {
const startTracking = useCallback(() => {
if (isTracking) return;
- if (!global.navigator?.geolocation?.watchPosition) {
+ if (!geo || !geo.watchPosition) {
console.warn('[useLocationTracking] Geolocation.watchPosition not available.');
return;
}
- const id = global.navigator.geolocation.watchPosition(
+ const id = geo.watchPosition(
onPosition,
(err: any) => console.warn('geolocation error', err),
{
enableHighAccuracy: opts.accuracy === 'high',
distanceFilter: opts.distanceFilter ?? 0,
- interval: opts.interval
+ interval: opts.interval,
}
);
- watchIdRef.current = id as unknown as number;
+ watchIdRef.current = id;
setIsTracking(true);
- }, [isTracking, onPosition, opts.accuracy, opts.distanceFilter, opts.interval]);
+ }, [isTracking, onPosition, geo, opts.accuracy, opts.distanceFilter, opts.interval]);
const stopTracking = useCallback(() => {
const id = watchIdRef.current;
- if (id != null && global.navigator?.geolocation?.clearWatch) {
- global.navigator.geolocation.clearWatch(id);
+ if (id != null && geo && geo.clearWatch) {
+ geo.clearWatch(id);
}
watchIdRef.current = null;
setIsTracking(false);
- }, []);
+ }, [geo]);
const clear = useCallback(() => {
setPath([]);
diff --git a/packages/map-sdk/src/index.ts b/packages/map-sdk/src/index.ts
index d32f29c..17ccf07 100644
--- a/packages/map-sdk/src/index.ts
+++ b/packages/map-sdk/src/index.ts
@@ -3,9 +3,11 @@ export type { MapHandle } from './MapView';
export { Marker, Polyline, PROVIDER_GOOGLE } from 'react-native-maps';
export { CameraPresets } from './cameraPresets';
export { useLocationTracking } from './hooks/useLocationTracking';
+export type { TrackingOptions, GeolocationAPI } from './hooks/useLocationTracking';
// High-level components
export { default as PropertyMap } from './components/PropertyMap';
+export type { PropertyMapProps } from './components/PropertyMap';
export { default as PriceMarker } from './components/PriceMarker';
export { formatPrice } from './components/PriceMarker';
-export type { PropertyItem } from './components/PriceMarker';
+export type { PropertyItem, PriceMarkerProps } from './components/PriceMarker';