@naarni/design-system
Version:
Naarni React Native Design System for EV Fleet Apps
477 lines (476 loc) • 18.5 kB
JavaScript
import React, { useRef, useEffect, useState, useCallback, useImperativeHandle } from 'react';
import { View, Text, StyleSheet, Platform, ActivityIndicator, } from 'react-native';
import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
const MapComponent = React.forwardRef(({ vehicles = [], initialRegion, style, mapStyle = 'default', showVehicleInfo = true, autoFitToVehicles = false, onVehiclePress, onMapPress, onRegionChange, zoomEnabled = true, scrollEnabled = true, rotateEnabled = true, pitchEnabled = true, showsUserLocation = false, showsMyLocationButton = false, showsCompass = true, showsScale = true, loadingEnabled = true, loadingIndicatorColor = '#007AFF', loadingBackgroundColor = '#ffffff', customMapStyle, testID, }, ref) => {
const mapRef = useRef(null);
const [isLoading, setIsLoading] = useState(true);
const [currentRegion, setCurrentRegion] = useState(initialRegion);
const [mapError, setMapError] = useState(null);
// Debug logging (reduced for stability)
console.log('🚗 MapComponent: Platform:', Platform.OS, 'Vehicles:', vehicles.length);
// Default region if not provided
const defaultRegion = {
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
};
const finalRegion = initialRegion || defaultRegion;
// Map style configurations
const getMapStyle = useCallback(() => {
if (customMapStyle) {
return customMapStyle;
}
const styles = {
default: [],
dark: [
{
elementType: 'geometry',
stylers: [{ color: '#212121' }],
},
{
elementType: 'labels.text.fill',
stylers: [{ color: '#757575' }],
},
{
elementType: 'labels.text.stroke',
stylers: [{ color: '#212121' }],
},
{
featureType: 'administrative',
elementType: 'geometry',
stylers: [{ color: '#757575' }],
},
{
featureType: 'administrative.country',
elementType: 'labels.text.fill',
stylers: [{ color: '#9e9e9e' }],
},
{
featureType: 'administrative.land_parcel',
stylers: [{ visibility: 'off' }],
},
{
featureType: 'administrative.locality',
elementType: 'labels.text.fill',
stylers: [{ color: '#bdbdbd' }],
},
{
featureType: 'poi',
elementType: 'labels.text.fill',
stylers: [{ color: '#757575' }],
},
{
featureType: 'poi.park',
elementType: 'geometry',
stylers: [{ color: '#181818' }],
},
{
featureType: 'poi.park',
elementType: 'labels.text.fill',
stylers: [{ color: '#616161' }],
},
{
featureType: 'poi.park',
elementType: 'labels.text.stroke',
stylers: [{ color: '#1b1b1b' }],
},
{
featureType: 'road',
elementType: 'geometry.fill',
stylers: [{ color: '#2c2c2c' }],
},
{
featureType: 'road',
elementType: 'labels.text.fill',
stylers: [{ color: '#8a8a8a' }],
},
{
featureType: 'road.arterial',
elementType: 'geometry',
stylers: [{ color: '#373737' }],
},
{
featureType: 'road.highway',
elementType: 'geometry',
stylers: [{ color: '#3c3c3c' }],
},
{
featureType: 'road.highway.controlled_access',
elementType: 'geometry',
stylers: [{ color: '#4e4e4e' }],
},
{
featureType: 'road.local',
elementType: 'labels.text.fill',
stylers: [{ color: '#616161' }],
},
{
featureType: 'transit',
elementType: 'labels.text.fill',
stylers: [{ color: '#757575' }],
},
{
featureType: 'water',
elementType: 'geometry',
stylers: [{ color: '#000000' }],
},
{
featureType: 'water',
elementType: 'labels.text.fill',
stylers: [{ color: '#3d3d3d' }],
},
],
light: [
{
elementType: 'geometry',
stylers: [{ color: '#f5f5f5' }],
},
{
elementType: 'labels.text.fill',
stylers: [{ color: '#616161' }],
},
{
elementType: 'labels.text.stroke',
stylers: [{ color: '#f5f5f5' }],
},
{
featureType: 'administrative.land_parcel',
elementType: 'labels.text.fill',
stylers: [{ color: '#bdbdbd' }],
},
{
featureType: 'poi',
elementType: 'geometry',
stylers: [{ color: '#eeeeee' }],
},
{
featureType: 'poi',
elementType: 'labels.text.fill',
stylers: [{ color: '#757575' }],
},
{
featureType: 'poi.park',
elementType: 'geometry',
stylers: [{ color: '#e5e5e5' }],
},
{
featureType: 'poi.park',
elementType: 'labels.text.fill',
stylers: [{ color: '#9e9e9e' }],
},
{
featureType: 'road',
elementType: 'geometry',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'road.arterial',
elementType: 'labels.text.fill',
stylers: [{ color: '#757575' }],
},
{
featureType: 'road.highway',
elementType: 'geometry',
stylers: [{ color: '#dadada' }],
},
{
featureType: 'road.highway',
elementType: 'labels.text.fill',
stylers: [{ color: '#616161' }],
},
{
featureType: 'road.local',
elementType: 'labels.text.fill',
stylers: [{ color: '#9e9e9e' }],
},
{
featureType: 'transit.line',
elementType: 'geometry',
stylers: [{ color: '#e5e5e5' }],
},
{
featureType: 'transit.station',
elementType: 'geometry',
stylers: [{ color: '#eeeeee' }],
},
{
featureType: 'water',
elementType: 'geometry',
stylers: [{ color: '#c9c9c9' }],
},
{
featureType: 'water',
elementType: 'labels.text.fill',
stylers: [{ color: '#9e9e9e' }],
},
],
blackAndWhite: [
{
elementType: 'geometry',
stylers: [{ color: '#ffffff' }],
},
{
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
elementType: 'labels.text.stroke',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'administrative',
elementType: 'geometry',
stylers: [{ color: '#f5f5f5' }],
},
{
featureType: 'administrative.country',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
featureType: 'administrative.land_parcel',
stylers: [{ visibility: 'off' }],
},
{
featureType: 'administrative.locality',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
featureType: 'poi',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
featureType: 'poi.park',
elementType: 'geometry',
stylers: [{ color: '#f5f5f5' }],
},
{
featureType: 'poi.park',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
featureType: 'poi.park',
elementType: 'labels.text.stroke',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'road',
elementType: 'geometry.fill',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'road',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
featureType: 'road.arterial',
elementType: 'geometry',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'road.highway',
elementType: 'geometry',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'road.highway.controlled_access',
elementType: 'geometry',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'road.local',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
featureType: 'transit',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
{
featureType: 'water',
elementType: 'geometry',
stylers: [{ color: '#ffffff' }],
},
{
featureType: 'water',
elementType: 'labels.text.fill',
stylers: [{ color: '#000000' }],
},
],
};
return styles[mapStyle] || styles.default;
}, [mapStyle, customMapStyle]);
// Handle map loading
const handleMapReady = useCallback(() => {
console.log('✅ Map is ready!');
setIsLoading(false);
setMapError(null);
}, []);
// Add timeout to detect if map is stuck loading
useEffect(() => {
const timeout = setTimeout(() => {
if (isLoading) {
console.warn('⚠️ Map loading timeout');
setMapError('Map loading timeout - check API key configuration');
setIsLoading(false);
}
}, 10000); // 10 second timeout
return () => clearTimeout(timeout);
}, [isLoading]);
// Handle map error
const handleMapError = useCallback((error) => {
console.error('❌ Map Error:', error);
setMapError(error?.message || 'Unknown map error');
setIsLoading(false);
// Remove Alert to prevent crashes
}, []);
// Handle region change
const handleRegionChange = useCallback((region) => {
setCurrentRegion(region);
onRegionChange?.(region);
}, [onRegionChange]);
// Fit map to show all vehicles
const fitToVehicles = useCallback(() => {
if (!mapRef.current || vehicles.length === 0)
return;
const coordinates = vehicles.map(vehicle => ({
latitude: vehicle.latitude,
longitude: vehicle.longitude,
}));
mapRef.current.fitToCoordinates(coordinates, {
edgePadding: { top: 50, right: 50, bottom: 50, left: 50 },
animated: true,
});
}, [vehicles]);
// Fit map to show specific vehicle
const fitToVehicle = useCallback((vehicle) => {
if (!mapRef.current)
return;
mapRef.current.animateToRegion({
latitude: vehicle.latitude,
longitude: vehicle.longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
}, 1000);
}, []);
// Expose methods via ref
useImperativeHandle(ref, () => ({
fitToVehicles,
fitToVehicle,
animateToRegion: (region, duration) => {
mapRef.current?.animateToRegion(region, duration);
},
fitToCoordinates: (coordinates, options) => {
mapRef.current?.fitToCoordinates(coordinates, options);
},
}), [fitToVehicles, fitToVehicle]);
// Auto-fit to vehicles when vehicles change
useEffect(() => {
if (vehicles.length > 0 && autoFitToVehicles) {
setTimeout(fitToVehicles, 500);
}
}, [vehicles, fitToVehicles]);
return (<View style={styles.container}>
{isLoading && (<View style={[styles.loadingContainer, { backgroundColor: loadingBackgroundColor }]}>
<ActivityIndicator size="large" color={loadingIndicatorColor}/>
<Text style={styles.loadingText}>Loading map...</Text>
</View>)}
{mapError && (<View style={styles.errorContainer}>
<Text style={styles.errorText}>Map Error: {mapError}</Text>
</View>)}
{(() => {
try {
return (<MapView ref={mapRef} provider={PROVIDER_GOOGLE} style={[styles.map, style]} initialRegion={finalRegion} onMapReady={() => {
try {
handleMapReady();
}
catch (error) {
console.error('Map ready error:', error);
setIsLoading(false);
}
}} onPress={(event) => {
try {
onMapPress?.(event);
}
catch (error) {
console.error('Map press error:', error);
}
}} onRegionChangeComplete={(region) => {
try {
handleRegionChange(region);
}
catch (error) {
console.error('Region change error:', error);
}
}} zoomEnabled={zoomEnabled} scrollEnabled={scrollEnabled} rotateEnabled={rotateEnabled} pitchEnabled={pitchEnabled} showsUserLocation={showsUserLocation} showsMyLocationButton={showsMyLocationButton} showsCompass={showsCompass} showsScale={showsScale}
// loadingEnabled={loadingEnabled}
loadingIndicatorColor={loadingIndicatorColor} loadingBackgroundColor={loadingBackgroundColor} customMapStyle={getMapStyle()} testID={testID}>
{vehicles.map((vehicle, index) => (<Marker key={vehicle.id || index} coordinate={{
latitude: vehicle.latitude,
longitude: vehicle.longitude,
}} title={showVehicleInfo ? vehicle.name || `Vehicle ${index + 1}` : undefined} description={showVehicleInfo ? vehicle.description : undefined} onPress={() => {
try {
onVehiclePress?.(vehicle);
}
catch (error) {
console.error('Vehicle press error:', error);
}
}} pinColor={vehicle.color || '#007AFF'} rotation={vehicle.heading || 0}/>))}
</MapView>);
}
catch (error) {
console.error('MapView render error:', error);
return (<View style={styles.errorContainer}>
<Text style={styles.errorText}>Failed to render map</Text>
</View>);
}
})()}
</View>);
});
const styles = StyleSheet.create({
container: {
flex: 1,
},
map: {
flex: 1,
},
loadingContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: '#666',
},
errorContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(255, 0, 0, 0.1)',
zIndex: 2,
},
errorText: {
fontSize: 16,
color: 'red',
textAlign: 'center',
padding: 20,
},
});
export default MapComponent;