fix-maps #57
542
components/Map/MapsV2Custom.tsx
Normal file
542
components/Map/MapsV2Custom.tsx
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
import { ReactNode, useCallback, useMemo, useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Image,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
ViewStyle,
|
||||||
|
StyleProp,
|
||||||
|
Animated,
|
||||||
|
Easing,
|
||||||
|
} from "react-native";
|
||||||
|
|
||||||
|
import API_IMAGE from "@/constants/api-storage";
|
||||||
|
import { MainColor } from "@/constants/color-palet";
|
||||||
|
import {
|
||||||
|
Camera,
|
||||||
|
MapView,
|
||||||
|
PointAnnotation,
|
||||||
|
} from "@maplibre/maplibre-react-native";
|
||||||
|
|
||||||
|
// Style peta default
|
||||||
|
const DEFAULT_MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||||
|
|
||||||
|
// Region default (Bali, Indonesia)
|
||||||
|
const DEFAULT_REGION = {
|
||||||
|
latitude: -8.737109,
|
||||||
|
longitude: 115.1756897,
|
||||||
|
latitudeDelta: 0.1,
|
||||||
|
longitudeDelta: 0.1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Zoom level default
|
||||||
|
const DEFAULT_ZOOM_LEVEL = 12;
|
||||||
|
|
||||||
|
// Ukuran marker default
|
||||||
|
const DEFAULT_MARKER_SIZE = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface data marker untuk MapsV2Custom
|
||||||
|
*/
|
||||||
|
export interface MapMarker {
|
||||||
|
id: string;
|
||||||
|
coordinate: [number, number]; // [longitude, latitude]
|
||||||
|
imageId?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
onSelected?: () => void;
|
||||||
|
[key: string]: any; // Izinkan properti custom tambahan
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface region untuk positioning kamera
|
||||||
|
*/
|
||||||
|
export interface Region {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
latitudeDelta: number;
|
||||||
|
longitudeDelta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props untuk komponen MapsV2Custom
|
||||||
|
*/
|
||||||
|
export interface MapsV2CustomProps {
|
||||||
|
/** URL style peta custom (default: liberty style) */
|
||||||
|
mapStyle?: string;
|
||||||
|
|
||||||
|
/** Override style container */
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
|
||||||
|
/** Override style MapView */
|
||||||
|
mapViewStyle?: StyleProp<ViewStyle>;
|
||||||
|
|
||||||
|
/** Region awal kamera */
|
||||||
|
initialRegion?: Region;
|
||||||
|
|
||||||
|
/** Zoom level awal (default: 12) */
|
||||||
|
zoomLevel?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data marker - mendukung single marker atau array of markers
|
||||||
|
* @example
|
||||||
|
* // Single marker
|
||||||
|
* markers={{ id: "1", coordinate: [115.175, -8.737], imageId: "abc" }}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Multiple markers
|
||||||
|
* markers={[
|
||||||
|
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
|
||||||
|
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
|
||||||
|
* ]}
|
||||||
|
*/
|
||||||
|
markers?: MapMarker | MapMarker[];
|
||||||
|
|
||||||
|
/** Custom renderer marker */
|
||||||
|
renderMarker?: (marker: MapMarker) => ReactNode;
|
||||||
|
|
||||||
|
/** Callback ketika marker ditekan */
|
||||||
|
onMarkerPress?: (marker: MapMarker) => void;
|
||||||
|
|
||||||
|
/** Gunakan style marker image default (default: true jika markers disediakan) */
|
||||||
|
showDefaultMarkers?: boolean;
|
||||||
|
|
||||||
|
/** Ukuran untuk marker default (default: 30) */
|
||||||
|
markerSize?: number;
|
||||||
|
|
||||||
|
/** Warna border untuk marker default */
|
||||||
|
markerBorderColor?: string;
|
||||||
|
|
||||||
|
/** Children tambahan untuk MapView (custom overlays, dll.) */
|
||||||
|
children?: ReactNode;
|
||||||
|
|
||||||
|
/** Handler untuk tekan pada peta */
|
||||||
|
onMapPress?: (coordinates: [number, number]) => void;
|
||||||
|
|
||||||
|
/** Test identifier */
|
||||||
|
testID?: string;
|
||||||
|
|
||||||
|
/** Props tambahan untuk Camera */
|
||||||
|
cameraProps?: Partial<Omit<React.ComponentProps<typeof Camera>, "centerCoordinate" | "zoomLevel">>;
|
||||||
|
|
||||||
|
/** Props tambahan untuk MapView */
|
||||||
|
mapViewProps?: Partial<React.ComponentProps<typeof MapView>>;
|
||||||
|
|
||||||
|
/** Props tambahan untuk PointAnnotation */
|
||||||
|
annotationProps?: Partial<{
|
||||||
|
id: string;
|
||||||
|
title?: string;
|
||||||
|
snippet?: string;
|
||||||
|
selected?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
coordinate: number[];
|
||||||
|
anchor?: { x: number; y: number };
|
||||||
|
onSelected?: (payload: any) => void;
|
||||||
|
onDeselected?: (payload: any) => void;
|
||||||
|
onDragStart?: (payload: any) => void;
|
||||||
|
onDragEnd?: (payload: any) => void;
|
||||||
|
onDrag?: (payload: any) => void;
|
||||||
|
style?: StyleProp<ViewStyle>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisasi markers ke array - mendukung single marker atau array
|
||||||
|
*/
|
||||||
|
function normalizeMarkers(markers: MapMarker | MapMarker[] | undefined): MapMarker[] {
|
||||||
|
if (!markers) return [];
|
||||||
|
return Array.isArray(markers) ? markers : [markers];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validasi marker memiliki props yang required (hanya development mode)
|
||||||
|
*/
|
||||||
|
function validateMarker(marker: MapMarker, index: number): boolean {
|
||||||
|
if (!marker.id) {
|
||||||
|
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'id' yang required`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!marker.coordinate || !Array.isArray(marker.coordinate) || marker.coordinate.length !== 2) {
|
||||||
|
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'coordinate' yang required. Format yang diharapkan: [longitude, latitude]`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen skeleton untuk loading state dengan shimmer animation
|
||||||
|
*/
|
||||||
|
function SkeletonMarker({
|
||||||
|
size = DEFAULT_MARKER_SIZE,
|
||||||
|
borderColor = MainColor.darkblue,
|
||||||
|
loadingColor = "#C5C5C5",
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
borderColor?: string;
|
||||||
|
loadingColor?: string;
|
||||||
|
}) {
|
||||||
|
const shimmerAnim = useMemo(() => new Animated.Value(0), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const animation = Animated.loop(
|
||||||
|
Animated.sequence([
|
||||||
|
Animated.timing(shimmerAnim, {
|
||||||
|
toValue: 1,
|
||||||
|
duration: 800,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
Animated.timing(shimmerAnim, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 800,
|
||||||
|
easing: Easing.inOut(Easing.ease),
|
||||||
|
useNativeDriver: true,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
animation.start();
|
||||||
|
|
||||||
|
return () => animation.stop();
|
||||||
|
}, [shimmerAnim]);
|
||||||
|
|
||||||
|
const shimmerOpacity = shimmerAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [0.3, 0.7],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.markerContainer,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size / 2,
|
||||||
|
borderColor,
|
||||||
|
backgroundColor: loadingColor,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.skeletonShimmer,
|
||||||
|
{
|
||||||
|
opacity: shimmerOpacity,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen fallback untuk error state
|
||||||
|
*/
|
||||||
|
function FallbackMarker({
|
||||||
|
size = DEFAULT_MARKER_SIZE,
|
||||||
|
borderColor = MainColor.darkblue,
|
||||||
|
iconColor = MainColor.darkblue,
|
||||||
|
}: {
|
||||||
|
size?: number;
|
||||||
|
borderColor?: string;
|
||||||
|
iconColor?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.markerContainer,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size / 2,
|
||||||
|
borderColor,
|
||||||
|
backgroundColor: "#F5F5F5",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[styles.fallbackIcon, { borderColor: iconColor }]}>
|
||||||
|
<View style={[styles.fallbackIconInner, { backgroundColor: iconColor }]} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props untuk DefaultMarker component
|
||||||
|
*/
|
||||||
|
export interface DefaultMarkerProps {
|
||||||
|
/** ID file image dari API */
|
||||||
|
imageId?: string;
|
||||||
|
/** URL image langsung */
|
||||||
|
imageUrl?: string;
|
||||||
|
/** Ukuran marker (default: 30) */
|
||||||
|
size?: number;
|
||||||
|
/** Warna border marker (default: darkblue) */
|
||||||
|
borderColor?: string;
|
||||||
|
/** Warna skeleton loading (default: gray) */
|
||||||
|
loadingColor?: string;
|
||||||
|
/** Warna icon fallback (default: darkblue) */
|
||||||
|
fallbackIconColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen marker default dengan image, border, shadows, loading skeleton, dan error fallback
|
||||||
|
*/
|
||||||
|
export function DefaultMarker({
|
||||||
|
imageId,
|
||||||
|
imageUrl,
|
||||||
|
size = DEFAULT_MARKER_SIZE,
|
||||||
|
borderColor = MainColor.darkblue,
|
||||||
|
loadingColor = MainColor.white_gray,
|
||||||
|
fallbackIconColor = MainColor.darkblue,
|
||||||
|
}: DefaultMarkerProps) {
|
||||||
|
const [hasError, setHasError] = useState(false);
|
||||||
|
const uri = imageId ? API_IMAGE.GET({ fileId: imageId }) : imageUrl;
|
||||||
|
|
||||||
|
// Debug log untuk development
|
||||||
|
if (__DEV__ && uri) {
|
||||||
|
console.log("[DefaultMarker] Image URI:", uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = useCallback((error: any) => {
|
||||||
|
console.log("[DefaultMarker] Image error:", error?.nativeEvent?.error || error);
|
||||||
|
setHasError(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLoad = useCallback(() => {
|
||||||
|
console.log("[DefaultMarker] Image loaded successfully");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Jika tidak ada URI atau error, tampilkan fallback
|
||||||
|
if (!uri || hasError) {
|
||||||
|
return (
|
||||||
|
<FallbackMarker
|
||||||
|
size={size}
|
||||||
|
borderColor={borderColor}
|
||||||
|
iconColor={fallbackIconColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render image dengan placeholder (defaultSource) untuk loading state
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.markerContainer,
|
||||||
|
{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
borderRadius: size / 2,
|
||||||
|
borderColor,
|
||||||
|
backgroundColor: loadingColor, // Background color sebagai placeholder
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri }}
|
||||||
|
style={[styles.markerImage, { width: size, height: size }]}
|
||||||
|
resizeMode="cover"
|
||||||
|
onLoad={handleLoad}
|
||||||
|
onError={handleError}
|
||||||
|
// Placeholder untuk Android saat loading
|
||||||
|
defaultSource={require("@/assets/images/icon.png")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Komponen Map yang reusable dan customizable menggunakan Mapbox/MapLibre
|
||||||
|
*
|
||||||
|
* Mendukung single marker, multiple markers, atau empty state.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Single marker
|
||||||
|
* <MapsV2Custom
|
||||||
|
* markers={{
|
||||||
|
* id: "1",
|
||||||
|
* coordinate: [115.1756897, -8.737109],
|
||||||
|
* imageId: "file-123"
|
||||||
|
* }}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Multiple markers
|
||||||
|
* <MapsV2Custom
|
||||||
|
* markers={[
|
||||||
|
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
|
||||||
|
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
|
||||||
|
* ]}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Peta dengan custom style dan custom markers
|
||||||
|
* <MapsV2Custom
|
||||||
|
* mapStyle="https://your-custom-style.com"
|
||||||
|
* markers={markers}
|
||||||
|
* markerSize={40}
|
||||||
|
* markerBorderColor={MainColor.primary}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Dengan custom marker renderer
|
||||||
|
* <MapsV2Custom
|
||||||
|
* markers={data}
|
||||||
|
* renderMarker={(marker) => <CustomMarker {...marker} />}
|
||||||
|
* />
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Peta kosong (tanpa markers)
|
||||||
|
* <MapsV2Custom
|
||||||
|
* initialRegion={{ latitude: -6.2, longitude: 106.8, latitudeDelta: 0.1, longitudeDelta: 0.1 }}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
export function MapsV2Custom({
|
||||||
|
mapStyle = DEFAULT_MAP_STYLE,
|
||||||
|
style = styles.container,
|
||||||
|
mapViewStyle = styles.map,
|
||||||
|
initialRegion = DEFAULT_REGION,
|
||||||
|
zoomLevel = DEFAULT_ZOOM_LEVEL,
|
||||||
|
markers,
|
||||||
|
renderMarker,
|
||||||
|
onMarkerPress,
|
||||||
|
showDefaultMarkers = true,
|
||||||
|
markerSize = DEFAULT_MARKER_SIZE,
|
||||||
|
markerBorderColor = MainColor.darkblue,
|
||||||
|
children,
|
||||||
|
onMapPress,
|
||||||
|
testID,
|
||||||
|
cameraProps,
|
||||||
|
mapViewProps,
|
||||||
|
annotationProps,
|
||||||
|
}: MapsV2CustomProps) {
|
||||||
|
// Normalisasi markers ke array (mendukung single atau multiple)
|
||||||
|
const normalizedMarkers = useMemo(
|
||||||
|
() => {
|
||||||
|
const arr = normalizeMarkers(markers);
|
||||||
|
// Filter marker yang invalid
|
||||||
|
return arr.filter((marker) => {
|
||||||
|
if (!marker.id) {
|
||||||
|
console.warn("[MapsV2Custom] Marker tanpa id akan diabaikan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!marker.coordinate || marker.coordinate.length !== 2) {
|
||||||
|
console.warn("[MapsV2Custom] Marker tanpa coordinate valid akan diabaikan");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[markers]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Validasi markers dalam development mode
|
||||||
|
useMemo(() => {
|
||||||
|
if (__DEV__) {
|
||||||
|
normalizedMarkers.forEach((marker, index) => {
|
||||||
|
validateMarker(marker, index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [normalizedMarkers]);
|
||||||
|
|
||||||
|
const handleMarkerSelected = useCallback(
|
||||||
|
(marker: MapMarker) => {
|
||||||
|
if (marker.onSelected) {
|
||||||
|
marker.onSelected();
|
||||||
|
}
|
||||||
|
if (onMarkerPress) {
|
||||||
|
onMarkerPress(marker);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onMarkerPress]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderMarkerComponent = useCallback(
|
||||||
|
(marker: MapMarker): ReactNode => {
|
||||||
|
if (renderMarker) {
|
||||||
|
return renderMarker(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDefaultMarkers) {
|
||||||
|
return (
|
||||||
|
<DefaultMarker
|
||||||
|
imageId={marker.imageId}
|
||||||
|
imageUrl={marker.imageUrl}
|
||||||
|
size={markerSize}
|
||||||
|
borderColor={markerBorderColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[renderMarker, showDefaultMarkers, markerSize, markerBorderColor]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={style} testID={testID}>
|
||||||
|
<MapView style={mapViewStyle} mapStyle={mapStyle} {...mapViewProps}>
|
||||||
|
<Camera
|
||||||
|
zoomLevel={zoomLevel}
|
||||||
|
centerCoordinate={[initialRegion.longitude, initialRegion.latitude]}
|
||||||
|
{...cameraProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{normalizedMarkers.map((marker) => (
|
||||||
|
<PointAnnotation
|
||||||
|
key={marker.id}
|
||||||
|
id={marker.id}
|
||||||
|
coordinate={marker.coordinate}
|
||||||
|
onSelected={() => handleMarkerSelected(marker)}
|
||||||
|
{...annotationProps}
|
||||||
|
>
|
||||||
|
{renderMarkerComponent(marker) as any}
|
||||||
|
</PointAnnotation>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</MapView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1 },
|
||||||
|
map: { flex: 1 },
|
||||||
|
markerContainer: {
|
||||||
|
overflow: "hidden",
|
||||||
|
borderWidth: 1,
|
||||||
|
elevation: 4,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 3,
|
||||||
|
},
|
||||||
|
markerImage: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
skeletonShimmer: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
fallbackIcon: {
|
||||||
|
width: "60%",
|
||||||
|
height: "60%",
|
||||||
|
borderRadius: 999,
|
||||||
|
borderWidth: 2,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
fallbackIconInner: {
|
||||||
|
width: "40%",
|
||||||
|
height: "40%",
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
|
});
|
||||||
272
components/Map/SelectLocationMap.tsx
Normal file
272
components/Map/SelectLocationMap.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import React, { useState, useCallback, useRef } from "react";
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from "react-native";
|
||||||
|
import {
|
||||||
|
MapView,
|
||||||
|
Camera,
|
||||||
|
PointAnnotation,
|
||||||
|
MarkerView,
|
||||||
|
} from "@maplibre/maplibre-react-native";
|
||||||
|
import * as Location from "expo-location";
|
||||||
|
import { useFocusEffect, useRouter } from "expo-router";
|
||||||
|
|
||||||
|
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||||
|
|
||||||
|
type Coordinate = {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SelectLocationMap() {
|
||||||
|
const router = useRouter();
|
||||||
|
const annotationRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const [selectedCoord, setSelectedCoord] = useState<Coordinate | null>(null);
|
||||||
|
const [address, setAddress] = useState<string>("");
|
||||||
|
const [isLoadingAddress, setIsLoadingAddress] = useState(false);
|
||||||
|
const [cameraCenter, setCameraCenter] = useState<[number, number]>([
|
||||||
|
106.8272, -6.1751,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const reverseGeocode = async (coord: Coordinate): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const { status } = await Location.getForegroundPermissionsAsync();
|
||||||
|
if (status !== "granted") {
|
||||||
|
await Location.requestForegroundPermissionsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Location.reverseGeocodeAsync({
|
||||||
|
latitude: coord.latitude,
|
||||||
|
longitude: coord.longitude,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!results || results.length === 0) return "Alamat tidak ditemukan";
|
||||||
|
|
||||||
|
const loc = results[0];
|
||||||
|
const parts = [
|
||||||
|
loc.street,
|
||||||
|
loc.district,
|
||||||
|
loc.subregion,
|
||||||
|
loc.city,
|
||||||
|
loc.region,
|
||||||
|
loc.country,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
return parts.length > 0 ? parts.join(", ") : "Alamat tidak ditemukan";
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("reverseGeocode error:", error?.message || error);
|
||||||
|
return "Gagal mengambil alamat";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMapPress = useCallback(async (event: any) => {
|
||||||
|
try {
|
||||||
|
const coordinates = event?.geometry?.coordinates;
|
||||||
|
if (!coordinates) return;
|
||||||
|
|
||||||
|
const [longitude, latitude] = coordinates;
|
||||||
|
if (!longitude || !latitude) return;
|
||||||
|
|
||||||
|
const coord: Coordinate = { latitude, longitude };
|
||||||
|
|
||||||
|
// ✅ Update state koordinat, BUKAN ganti key
|
||||||
|
setSelectedCoord(coord);
|
||||||
|
setCameraCenter([longitude, latitude]);
|
||||||
|
setAddress("");
|
||||||
|
setIsLoadingAddress(true);
|
||||||
|
|
||||||
|
const resolvedAddress = await reverseGeocode(coord);
|
||||||
|
setAddress(resolvedAddress);
|
||||||
|
setIsLoadingAddress(false);
|
||||||
|
|
||||||
|
// ✅ Refresh annotation tanpa unmount
|
||||||
|
annotationRef.current?.refresh?.();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log("handleMapPress error:", error?.message || error);
|
||||||
|
setIsLoadingAddress(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (!selectedCoord) return;
|
||||||
|
router.navigate({
|
||||||
|
pathname: "/maps/create",
|
||||||
|
params: {
|
||||||
|
latitude: String(selectedCoord.latitude),
|
||||||
|
longitude: String(selectedCoord.longitude),
|
||||||
|
address,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sembunyikan marker sebelum halaman unmount
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
return () => {
|
||||||
|
// Cleanup saat leave — sembunyikan marker dulu sebelum unmount
|
||||||
|
setSelectedCoord(null);
|
||||||
|
};
|
||||||
|
}, []),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<MapView style={styles.map} mapStyle={MAP_STYLE} onPress={handleMapPress}>
|
||||||
|
<Camera
|
||||||
|
zoomLevel={14}
|
||||||
|
centerCoordinate={cameraCenter}
|
||||||
|
animationMode="flyTo"
|
||||||
|
animationDuration={300}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ✅ Key statis — tidak pernah berubah, tidak unmount/remount */}
|
||||||
|
{selectedCoord && (
|
||||||
|
<MarkerView
|
||||||
|
id="selected-marker"
|
||||||
|
coordinate={[selectedCoord.longitude, selectedCoord.latitude]}
|
||||||
|
anchor={{ x: 0.5, y: 1 }} // Anchor bawah tengah
|
||||||
|
>
|
||||||
|
<View style={styles.pin}>
|
||||||
|
<View style={styles.pinDot} />
|
||||||
|
</View>
|
||||||
|
</MarkerView>
|
||||||
|
)}
|
||||||
|
</MapView>
|
||||||
|
|
||||||
|
{/* Bottom Sheet */}
|
||||||
|
<View style={styles.bottomSheet}>
|
||||||
|
{!selectedCoord ? (
|
||||||
|
<Text style={styles.hintText}>
|
||||||
|
Tap pada peta untuk memilih lokasi
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<View style={styles.coordRow}>
|
||||||
|
<View style={styles.coordItem}>
|
||||||
|
<Text style={styles.coordLabel}>Latitude</Text>
|
||||||
|
<Text style={styles.coordValue}>
|
||||||
|
{selectedCoord.latitude.toFixed(6)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.dividerVertical} />
|
||||||
|
<View style={styles.coordItem}>
|
||||||
|
<Text style={styles.coordLabel}>Longitude</Text>
|
||||||
|
<Text style={styles.coordValue}>
|
||||||
|
{selectedCoord.longitude.toFixed(6)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.addressContainer}>
|
||||||
|
<Text style={styles.coordLabel}>Alamat</Text>
|
||||||
|
{isLoadingAddress ? (
|
||||||
|
<ActivityIndicator size="small" color="#0a1f44" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.addressText} numberOfLines={2}>
|
||||||
|
{address || "-"}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.confirmButton,
|
||||||
|
isLoadingAddress && styles.confirmButtonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={handleConfirm}
|
||||||
|
disabled={isLoadingAddress}
|
||||||
|
>
|
||||||
|
<Text style={styles.confirmButtonText}>Konfirmasi Lokasi</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1 },
|
||||||
|
map: { flex: 1 },
|
||||||
|
pin: {
|
||||||
|
width: 28,
|
||||||
|
height: 28,
|
||||||
|
borderRadius: 100,
|
||||||
|
backgroundColor: "#0a1f44",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "#fff",
|
||||||
|
},
|
||||||
|
pinDot: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 100,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
},
|
||||||
|
bottomSheet: {
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 20,
|
||||||
|
paddingBottom: 32,
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
elevation: 10,
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: -3 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 6,
|
||||||
|
minHeight: 140,
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
hintText: {
|
||||||
|
textAlign: "center",
|
||||||
|
color: "#888",
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
coordRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
coordItem: { flex: 1 },
|
||||||
|
dividerVertical: {
|
||||||
|
width: 1,
|
||||||
|
backgroundColor: "#e0e0e0",
|
||||||
|
marginHorizontal: 12,
|
||||||
|
},
|
||||||
|
coordLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: "#888",
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
coordValue: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#0a1f44",
|
||||||
|
},
|
||||||
|
addressContainer: { marginBottom: 16 },
|
||||||
|
addressText: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: "#333",
|
||||||
|
lineHeight: 18,
|
||||||
|
},
|
||||||
|
confirmButton: {
|
||||||
|
backgroundColor: "#0a1f44",
|
||||||
|
borderRadius: 12,
|
||||||
|
paddingVertical: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
confirmButtonDisabled: {
|
||||||
|
backgroundColor: "#aaa",
|
||||||
|
},
|
||||||
|
confirmButtonText: {
|
||||||
|
color: "#fff",
|
||||||
|
fontWeight: "700",
|
||||||
|
fontSize: 15,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -155,6 +155,14 @@
|
|||||||
B6436A881D9B484CB6D18085 /* Remove signature files (Xcode workaround) */,
|
B6436A881D9B484CB6D18085 /* Remove signature files (Xcode workaround) */,
|
||||||
2BAE9DA9D4244A23B39651C7 /* Remove signature files (Xcode workaround) */,
|
2BAE9DA9D4244A23B39651C7 /* Remove signature files (Xcode workaround) */,
|
||||||
81D5244C04C44C06AD9AE152 /* Remove signature files (Xcode workaround) */,
|
81D5244C04C44C06AD9AE152 /* Remove signature files (Xcode workaround) */,
|
||||||
|
806E7861EC0045A8A1F95D2D /* Remove signature files (Xcode workaround) */,
|
||||||
|
4B22761083094691A59ABFF6 /* Remove signature files (Xcode workaround) */,
|
||||||
|
B68271891C364FB0B1E59DC4 /* Remove signature files (Xcode workaround) */,
|
||||||
|
A38A95E05F7448A8922DE72A /* Remove signature files (Xcode workaround) */,
|
||||||
|
6692ADBCD8384D8EA2D9A355 /* Remove signature files (Xcode workaround) */,
|
||||||
|
5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */,
|
||||||
|
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */,
|
||||||
|
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -491,6 +499,142 @@
|
|||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
";
|
";
|
||||||
};
|
};
|
||||||
|
806E7861EC0045A8A1F95D2D /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
4B22761083094691A59ABFF6 /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
B68271891C364FB0B1E59DC4 /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
A38A95E05F7448A8922DE72A /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
6692ADBCD8384D8EA2D9A355 /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
|
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
name = "Remove signature files (Xcode workaround)";
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "
|
||||||
|
echo \"Remove signature files (Xcode workaround)\";
|
||||||
|
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||||
|
";
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Image, StyleSheet, View } from "react-native";
|
|
||||||
|
|
||||||
// Cek versi >= 10.x gunakan ini
|
|
||||||
import API_IMAGE from "@/constants/api-storage";
|
|
||||||
import { MainColor } from "@/constants/color-palet";
|
|
||||||
import { apiMapsGetAll } from "@/service/api-client/api-maps";
|
import { apiMapsGetAll } from "@/service/api-client/api-maps";
|
||||||
import {
|
|
||||||
Camera,
|
|
||||||
MapView,
|
|
||||||
PointAnnotation,
|
|
||||||
} from "@maplibre/maplibre-react-native";
|
|
||||||
import { useFocusEffect } from "expo-router";
|
import { useFocusEffect } from "expo-router";
|
||||||
import DrawerMaps from "./DrawerMaps";
|
|
||||||
|
|
||||||
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
import { ViewWrapper } from "@/components";
|
||||||
|
import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
|
||||||
|
|
||||||
|
import DrawerMaps from "./DrawerMaps";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
interface TypeMaps {
|
interface TypeMaps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -40,14 +34,6 @@ interface TypeMaps {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultRegion = {
|
|
||||||
latitude: -8.737109,
|
|
||||||
longitude: 115.1756897,
|
|
||||||
latitudeDelta: 0.1,
|
|
||||||
longitudeDelta: 0.1,
|
|
||||||
height: 300,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function MapsView2() {
|
export default function MapsView2() {
|
||||||
const [list, setList] = useState<TypeMaps[] | null>(null);
|
const [list, setList] = useState<TypeMaps[] | null>(null);
|
||||||
const [loadList, setLoadList] = useState(false);
|
const [loadList, setLoadList] = useState(false);
|
||||||
@@ -76,7 +62,6 @@ export default function MapsView2() {
|
|||||||
const response = await apiMapsGetAll();
|
const response = await apiMapsGetAll();
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// console.log("[RESPONSE]", JSON.stringify(response.data, null, 2));
|
|
||||||
setList(response.data);
|
setList(response.data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,53 +71,31 @@ export default function MapsView2() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const markers: MapMarker[] = list?.map((item) => ({
|
||||||
<>
|
id: item.id,
|
||||||
<View style={styles.container}>
|
coordinate: [item.longitude, item.latitude] as [number, number],
|
||||||
<MapView style={styles.map} mapStyle={MAP_STYLE}>
|
imageId: item.Portofolio.logoId,
|
||||||
<Camera
|
onSelected: () => {
|
||||||
zoomLevel={12}
|
|
||||||
centerCoordinate={[defaultRegion.longitude, defaultRegion.latitude]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{list?.map((item: TypeMaps) => {
|
|
||||||
const imageUrl = API_IMAGE.GET({ fileId: item.Portofolio.logoId });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PointAnnotation
|
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
coordinate={[item.longitude, item.latitude] as [number, number]}
|
|
||||||
onSelected={() => {
|
|
||||||
setOpenDrawer(true);
|
setOpenDrawer(true);
|
||||||
setSelected({
|
setSelected({
|
||||||
id: item?.id,
|
id: item.id,
|
||||||
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
|
bidangBisnis: item.Portofolio.MasterBidangBisnis.name,
|
||||||
nomorTelepon: item?.Portofolio?.tlpn,
|
nomorTelepon: item.Portofolio.tlpn,
|
||||||
alamatBisnis: item?.Portofolio?.alamatKantor,
|
alamatBisnis: item.Portofolio.alamatKantor,
|
||||||
namePin: item?.namePin,
|
namePin: item.namePin,
|
||||||
imageId: item?.imageId,
|
imageId: item.imageId,
|
||||||
portofolioId: item?.Portofolio?.id,
|
portofolioId: item.Portofolio.id,
|
||||||
latitude: item?.latitude,
|
latitude: item.latitude,
|
||||||
longitude: item?.longitude,
|
longitude: item.longitude,
|
||||||
});
|
});
|
||||||
}}
|
},
|
||||||
>
|
})) || [];
|
||||||
<View style={styles.markerContainer}>
|
|
||||||
<Image
|
return (
|
||||||
source={{ uri: imageUrl }}
|
<>
|
||||||
style={styles.markerImage}
|
<ViewWrapper>
|
||||||
resizeMode="cover"
|
<MapsV2Custom markers={markers} />
|
||||||
onError={(e: any) =>
|
</ViewWrapper>
|
||||||
console.log("Image error:", e.nativeEvent.error)
|
|
||||||
} // Tangkap error image
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</PointAnnotation>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</MapView>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<DrawerMaps
|
<DrawerMaps
|
||||||
openDrawer={openDrawer}
|
openDrawer={openDrawer}
|
||||||
@@ -144,23 +107,9 @@ export default function MapsView2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1 },
|
map: {
|
||||||
map: { flex: 1 },
|
flex: 1,
|
||||||
markerContainer: {
|
width: "50%",
|
||||||
width: 30,
|
maxHeight: "50%",
|
||||||
height: 30,
|
|
||||||
borderRadius: 100,
|
|
||||||
overflow: "hidden", // Wajib agar borderRadius terapply pada Image
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: MainColor.darkblue, // Opsional, biar lebih cantik
|
|
||||||
elevation: 4, // Shadow Android
|
|
||||||
shadowColor: "#000", // Shadow iOS
|
|
||||||
shadowOffset: { width: 0, height: 2 },
|
|
||||||
shadowOpacity: 0.3,
|
|
||||||
shadowRadius: 3,
|
|
||||||
},
|
|
||||||
markerImage: {
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1,19 +1,42 @@
|
|||||||
import {
|
import { StyleSheet, View } from "react-native";
|
||||||
BaseBox,
|
|
||||||
MapCustom,
|
import { BaseBox, StackCustom, TextCustom } from "@/components";
|
||||||
StackCustom,
|
import { MapsV2Custom } from "@/components/Map/MapsV2Custom";
|
||||||
TextCustom
|
|
||||||
} from "@/components";
|
|
||||||
|
|
||||||
export default function Portofolio_BusinessLocation({
|
export default function Portofolio_BusinessLocation({
|
||||||
data,
|
data,
|
||||||
imageId,
|
imageId,
|
||||||
setOpenDrawerLocation,
|
setOpenDrawerLocation,
|
||||||
}: {
|
}: {
|
||||||
data: any;
|
data: {
|
||||||
|
id: string;
|
||||||
|
imageId: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
namePin: string;
|
||||||
|
pinId: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
imageId?: string;
|
imageId?: string;
|
||||||
setOpenDrawerLocation: (value: boolean) => void;
|
setOpenDrawerLocation: (value: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
|
console.log("data", data);
|
||||||
|
|
||||||
|
// Buat marker hanya jika data lengkap
|
||||||
|
const markers =
|
||||||
|
data?.latitude && data?.longitude
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: data.id || "location-marker",
|
||||||
|
coordinate: [data.longitude, data.latitude] as [number, number],
|
||||||
|
imageId,
|
||||||
|
onSelected: () => {
|
||||||
|
setOpenDrawerLocation(true);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BaseBox style={{ height: !data ? 200 : "auto" }}>
|
<BaseBox style={{ height: !data ? 200 : "auto" }}>
|
||||||
@@ -30,18 +53,33 @@ export default function Portofolio_BusinessLocation({
|
|||||||
Lokasi bisnis belum ditambahkan
|
Lokasi bisnis belum ditambahkan
|
||||||
</TextCustom>
|
</TextCustom>
|
||||||
) : (
|
) : (
|
||||||
<MapCustom
|
<View style={styles.mapContainer}>
|
||||||
latitude={data?.latitude}
|
<MapsV2Custom
|
||||||
longitude={data?.longitude}
|
markers={markers}
|
||||||
namePin={data?.namePin}
|
zoomLevel={15}
|
||||||
imageId={imageId}
|
showDefaultMarkers={true}
|
||||||
onPress={() => {
|
markerSize={35}
|
||||||
setOpenDrawerLocation(true);
|
initialRegion={{
|
||||||
|
latitude: data?.latitude,
|
||||||
|
longitude: data?.longitude,
|
||||||
|
latitudeDelta: 0.1,
|
||||||
|
longitudeDelta: 0.1,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</View>
|
||||||
)}
|
)}
|
||||||
</StackCustom>
|
</StackCustom>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
mapContainer: {
|
||||||
|
width: "100%",
|
||||||
|
height: 250,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: "hidden",
|
||||||
|
marginTop: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user