Compare commits
4 Commits
fix-maps/2
...
fix-maps/2
| Author | SHA1 | Date | |
|---|---|---|---|
| 67070bb2f1 | |||
| fb19ec60b2 | |||
| e8f5c5b174 | |||
| 74a4d88277 |
@@ -1,8 +1,4 @@
|
|||||||
import { BackButton } from "@/components";
|
|
||||||
import MapsView from "@/screens/Maps/MapsView";
|
|
||||||
import MapsView2 from "@/screens/Maps/MapsView2";
|
import MapsView2 from "@/screens/Maps/MapsView2";
|
||||||
import { Stack } from "expo-router";
|
|
||||||
import { Platform, Text, View } from "react-native";
|
|
||||||
|
|
||||||
export interface LocationItem {
|
export interface LocationItem {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
@@ -21,8 +17,8 @@ export default function Maps() {
|
|||||||
headerLeft: () => <BackButton />,
|
headerLeft: () => <BackButton />,
|
||||||
}}
|
}}
|
||||||
/> */}
|
/> */}
|
||||||
{Platform.OS === "ios" ? <MapsView /> : <MapsView2 />}
|
{/* {Platform.OS === "ios" ? <MapsView /> : <MapsView2 />} */}
|
||||||
{/* <MapsView2 /> */}
|
<MapsView2 />
|
||||||
{/* <View style={{ flex: 1, backgroundColor: "gray" }}><Text style={{ color: "white" }}>Map disabled</Text></View> */}
|
{/* <View style={{ flex: 1, backgroundColor: "gray" }}><Text style={{ color: "white" }}>Map disabled</Text></View> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
101
docs/PODS.back
Normal file
101
docs/PODS.back
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
NOTE:
|
||||||
|
|
||||||
|
Untuk Development Selanjutnya:
|
||||||
|
Sekarang Anda bisa menjalankan:
|
||||||
|
|
||||||
|
1 # Untuk run iOS dev client
|
||||||
|
2 bun run ios
|
||||||
|
3
|
||||||
|
4 # Atau dengan Expo
|
||||||
|
5 bunx expo run:ios
|
||||||
|
|
||||||
|
Jika di masa depan terjadi error serupa, Anda bisa gunakan command ini:
|
||||||
|
|
||||||
|
1 cd ios
|
||||||
|
2 rm -rf Pods Podfile.lock
|
||||||
|
3 pod install
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
use_modular_headers!
|
||||||
|
|
||||||
|
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||||
|
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||||
|
|
||||||
|
require 'json'
|
||||||
|
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||||
|
|
||||||
|
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||||
|
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||||
|
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
|
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||||
|
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||||
|
|
||||||
|
prepare_react_native_project!
|
||||||
|
|
||||||
|
target 'HIPMIBadungConnect' do
|
||||||
|
use_expo_modules!
|
||||||
|
|
||||||
|
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
||||||
|
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||||
|
else
|
||||||
|
config_command = [
|
||||||
|
'npx',
|
||||||
|
'expo-modules-autolinking',
|
||||||
|
'react-native-config',
|
||||||
|
'--json',
|
||||||
|
'--platform',
|
||||||
|
'ios'
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
config = use_native_modules!(config_command)
|
||||||
|
|
||||||
|
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||||
|
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||||
|
|
||||||
|
|
||||||
|
use_react_native!(
|
||||||
|
:path => config[:reactNativePath],
|
||||||
|
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||||
|
# An absolute path to your application root.
|
||||||
|
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||||
|
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||||
|
)
|
||||||
|
|
||||||
|
pod 'Firebase'
|
||||||
|
pod 'Firebase/Messaging'
|
||||||
|
|
||||||
|
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
|
||||||
|
post_install do |installer|
|
||||||
|
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
|
||||||
|
$MLRN.post_install(installer)
|
||||||
|
# @generated end @maplibre/maplibre-react-native:post-install
|
||||||
|
|
||||||
|
# Fix all script phases with incorrect paths
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_phases.each do |phase|
|
||||||
|
next unless phase.respond_to?(:shell_script)
|
||||||
|
|
||||||
|
# Fix duplicated path issue
|
||||||
|
if phase.shell_script.include?('with-environment.sh')
|
||||||
|
# Remove any existing path and use proper relative path
|
||||||
|
phase.shell_script = phase.shell_script.gsub(
|
||||||
|
%r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},
|
||||||
|
'${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Standard React Native post install
|
||||||
|
react_native_post_install(
|
||||||
|
installer,
|
||||||
|
config[:reactNativePath],
|
||||||
|
:mac_catalyst_enabled => false,
|
||||||
|
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
||||||
|
)
|
||||||
|
end
|
||||||
|
# @generated end post_installer
|
||||||
|
|
||||||
|
end
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"originHash" : "e70d3525c8e2819a8b34f22909815dab5c700c25a06c32388f3930f7b3627768",
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "maplibre-gl-native-distribution",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "c68c970ff3ece56cfc3b36849db70167fa208beb",
|
||||||
|
"version" : "6.17.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 3
|
||||||
|
}
|
||||||
969
ios/Podfile.lock
969
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
125
screens/Maps/DrawerMaps.tsx
Normal file
125
screens/Maps/DrawerMaps.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
DrawerCustom,
|
||||||
|
DummyLandscapeImage,
|
||||||
|
Spacing,
|
||||||
|
StackCustom,
|
||||||
|
TextCustom,
|
||||||
|
Grid,
|
||||||
|
ButtonCustom,
|
||||||
|
} from "@/components";
|
||||||
|
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||||
|
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||||
|
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
|
||||||
|
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
|
||||||
|
interface TypeDrawerMaps {
|
||||||
|
openDrawer: boolean;
|
||||||
|
setOpenDrawer: (value: boolean) => void;
|
||||||
|
selected: {
|
||||||
|
id: string;
|
||||||
|
bidangBisnis: string;
|
||||||
|
nomorTelepon: string;
|
||||||
|
alamatBisnis: string;
|
||||||
|
namePin: string;
|
||||||
|
imageId: string;
|
||||||
|
portofolioId: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DrawerMaps({
|
||||||
|
openDrawer,
|
||||||
|
setOpenDrawer,
|
||||||
|
selected,
|
||||||
|
}: TypeDrawerMaps) {
|
||||||
|
return (
|
||||||
|
<DrawerCustom
|
||||||
|
isVisible={openDrawer}
|
||||||
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
|
height={"auto"}
|
||||||
|
>
|
||||||
|
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
||||||
|
<Spacing />
|
||||||
|
<StackCustom gap={"xs"}>
|
||||||
|
<GridTwoView
|
||||||
|
spanLeft={2}
|
||||||
|
spanRight={10}
|
||||||
|
leftItem={
|
||||||
|
<FontAwesome
|
||||||
|
name="building-o"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GridTwoView
|
||||||
|
spanLeft={2}
|
||||||
|
spanRight={10}
|
||||||
|
leftItem={
|
||||||
|
<Ionicons
|
||||||
|
name="list-outline"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<GridTwoView
|
||||||
|
spanLeft={2}
|
||||||
|
spanRight={10}
|
||||||
|
leftItem={
|
||||||
|
<Ionicons
|
||||||
|
name="call-outline"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightItem={<TextCustom>{selected.nomorTelepon}</TextCustom>}
|
||||||
|
/>
|
||||||
|
<GridTwoView
|
||||||
|
spanLeft={2}
|
||||||
|
spanRight={10}
|
||||||
|
leftItem={
|
||||||
|
<Ionicons
|
||||||
|
name="location-outline"
|
||||||
|
size={ICON_SIZE_SMALL}
|
||||||
|
color="white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6} style={{ paddingRight: 10 }}>
|
||||||
|
<ButtonCustom
|
||||||
|
onPress={() => {
|
||||||
|
setOpenDrawer(false);
|
||||||
|
router.push(`/portofolio/${selected.portofolioId}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</ButtonCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6} style={{ paddingLeft: 10 }}>
|
||||||
|
<ButtonCustom
|
||||||
|
onPress={() => {
|
||||||
|
openInDeviceMaps({
|
||||||
|
latitude: selected.latitude,
|
||||||
|
longitude: selected.longitude,
|
||||||
|
title: selected.namePin,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Buka Maps
|
||||||
|
</ButtonCustom>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</StackCustom>
|
||||||
|
</DrawerCustom>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +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 {
|
import { useFocusEffect } from "expo-router";
|
||||||
Camera,
|
|
||||||
MapView,
|
|
||||||
PointAnnotation,
|
|
||||||
} from "@maplibre/maplibre-react-native";
|
|
||||||
import { router, useFocusEffect } from "expo-router";
|
|
||||||
import {
|
|
||||||
DrawerCustom,
|
|
||||||
DummyLandscapeImage,
|
|
||||||
Spacing,
|
|
||||||
StackCustom,
|
|
||||||
TextCustom,
|
|
||||||
Grid,
|
|
||||||
ButtonCustom,
|
|
||||||
} from "@/components";
|
|
||||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
|
||||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
|
||||||
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
|
|
||||||
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -52,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);
|
||||||
@@ -88,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) {
|
||||||
@@ -98,162 +71,45 @@ export default function MapsView2() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const markers: MapMarker[] = list?.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
coordinate: [item.longitude, item.latitude] as [number, number],
|
||||||
|
imageId: item.Portofolio.logoId,
|
||||||
|
onSelected: () => {
|
||||||
|
setOpenDrawer(true);
|
||||||
|
setSelected({
|
||||||
|
id: item.id,
|
||||||
|
bidangBisnis: item.Portofolio.MasterBidangBisnis.name,
|
||||||
|
nomorTelepon: item.Portofolio.tlpn,
|
||||||
|
alamatBisnis: item.Portofolio.alamatKantor,
|
||||||
|
namePin: item.namePin,
|
||||||
|
imageId: item.imageId,
|
||||||
|
portofolioId: item.Portofolio.id,
|
||||||
|
latitude: item.latitude,
|
||||||
|
longitude: item.longitude,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={styles.container}>
|
<ViewWrapper>
|
||||||
<MapView style={styles.map} mapStyle={MAP_STYLE}>
|
<MapsV2Custom markers={markers} />
|
||||||
<Camera
|
</ViewWrapper>
|
||||||
zoomLevel={12}
|
|
||||||
centerCoordinate={[defaultRegion.longitude, defaultRegion.latitude]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{list?.map((item: TypeMaps) => {
|
<DrawerMaps
|
||||||
const imageUrl = API_IMAGE.GET({ fileId: item.Portofolio.logoId });
|
openDrawer={openDrawer}
|
||||||
|
setOpenDrawer={setOpenDrawer}
|
||||||
return (
|
selected={selected}
|
||||||
<PointAnnotation
|
/>
|
||||||
key={item.id}
|
|
||||||
id={item.id}
|
|
||||||
coordinate={[item.longitude, item.latitude] as [number, number]}
|
|
||||||
onSelected={() => {
|
|
||||||
setOpenDrawer(true);
|
|
||||||
setSelected({
|
|
||||||
id: item?.id,
|
|
||||||
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
|
|
||||||
nomorTelepon: item?.Portofolio?.tlpn,
|
|
||||||
alamatBisnis: item?.Portofolio?.alamatKantor,
|
|
||||||
namePin: item?.namePin,
|
|
||||||
imageId: item?.imageId,
|
|
||||||
portofolioId: item?.Portofolio?.id,
|
|
||||||
latitude: item?.latitude,
|
|
||||||
longitude: item?.longitude,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={styles.markerContainer}>
|
|
||||||
<Image
|
|
||||||
source={{ uri: imageUrl }}
|
|
||||||
style={styles.markerImage}
|
|
||||||
resizeMode="cover"
|
|
||||||
onError={(e: any) =>
|
|
||||||
console.log("Image error:", e.nativeEvent.error)
|
|
||||||
} // Tangkap error image
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</PointAnnotation>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</MapView>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<DrawerCustom
|
|
||||||
isVisible={openDrawer}
|
|
||||||
closeDrawer={() => setOpenDrawer(false)}
|
|
||||||
height={"auto"}
|
|
||||||
>
|
|
||||||
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
|
||||||
<Spacing />
|
|
||||||
<StackCustom gap={"xs"}>
|
|
||||||
<GridTwoView
|
|
||||||
spanLeft={2}
|
|
||||||
spanRight={10}
|
|
||||||
leftItem={
|
|
||||||
<FontAwesome
|
|
||||||
name="building-o"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GridTwoView
|
|
||||||
spanLeft={2}
|
|
||||||
spanRight={10}
|
|
||||||
leftItem={
|
|
||||||
<Ionicons
|
|
||||||
name="list-outline"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<GridTwoView
|
|
||||||
spanLeft={2}
|
|
||||||
spanRight={10}
|
|
||||||
leftItem={
|
|
||||||
<Ionicons
|
|
||||||
name="call-outline"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightItem={<TextCustom>{selected.nomorTelepon}</TextCustom>}
|
|
||||||
/>
|
|
||||||
<GridTwoView
|
|
||||||
spanLeft={2}
|
|
||||||
spanRight={10}
|
|
||||||
leftItem={
|
|
||||||
<Ionicons
|
|
||||||
name="location-outline"
|
|
||||||
size={ICON_SIZE_SMALL}
|
|
||||||
color="white"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.Col span={6} style={{ paddingRight: 10 }}>
|
|
||||||
<ButtonCustom
|
|
||||||
onPress={() => {
|
|
||||||
setOpenDrawer(false);
|
|
||||||
router.push(`/portofolio/${selected.portofolioId}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Detail
|
|
||||||
</ButtonCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6} style={{ paddingLeft: 10 }}>
|
|
||||||
<ButtonCustom
|
|
||||||
onPress={() => {
|
|
||||||
openInDeviceMaps({
|
|
||||||
latitude: selected.latitude,
|
|
||||||
longitude: selected.longitude,
|
|
||||||
title: selected.namePin,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Buka Maps
|
|
||||||
</ButtonCustom>
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
|
||||||
</StackCustom>
|
|
||||||
</DrawerCustom>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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