Maps Platform Update
- components/Map/MapSelectedPlatform.tsx
- components/Map/MapSelectedV2.tsx

Maps Screens
- screens/Maps/ScreenMapsCreate.tsx
- screens/Maps/ScreenMapsEdit.tsx

Home
- screens/Home/bottomFeatureSection.tsx

Config & Native
- app.config.js
- android/app/build.gradle
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
This commit is contained in:
2026-03-02 16:34:24 +08:00
parent 4c63485a5b
commit 9c94ec0454
9 changed files with 273 additions and 204 deletions

View File

@@ -100,8 +100,8 @@ packagingOptions {
applicationId 'com.bip.hipmimobileapp'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 4
versionName "1.0.1"
versionCode 1
versionName "1.0.2"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}

View File

@@ -4,7 +4,7 @@ require("dotenv").config();
export default {
name: "HIPMI Badung Connect",
slug: "hipmi-mobile",
version: "1.0.1",
version: "1.0.2",
orientation: "portrait",
icon: "./assets/images/icon.png",
scheme: "hipmimobile",
@@ -21,7 +21,7 @@ export default {
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "21",
buildNumber: "1",
},
android: {
@@ -32,7 +32,7 @@ export default {
},
edgeToEdgeEnabled: true,
package: "com.bip.hipmimobileapp",
versionCode: 4,
versionCode: 1,
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
intentFilters: [
{

View File

@@ -1,7 +1,6 @@
import { Platform } from "react-native";
import MapSelected from "./MapSelected";
import { MapSelectedV2 } from "./MapSelectedV2";
import { Region } from "./MapSelectedV2";
import MapSelectedV2 from "./MapSelectedV2";
import { LatLng } from "react-native-maps";
/**
@@ -58,18 +57,18 @@ export function MapSelectedPlatform({
showsMyLocationButton = true,
}: MapSelectedPlatformProps) {
// iOS: Gunakan react-native-maps
if (Platform.OS === "ios") {
return (
<MapSelected
initialRegion={initialRegion}
selectedLocation={(selectedLocation as LatLng) || { latitude: 0, longitude: 0 }}
setSelectedLocation={(location: LatLng) => {
onLocationSelect(location);
}}
height={height}
/>
);
}
// if (Platform.OS === "ios") {
// return (
// <MapSelected
// initialRegion={initialRegion}
// selectedLocation={(selectedLocation as LatLng) || { latitude: 0, longitude: 0 }}
// setSelectedLocation={(location: LatLng) => {
// onLocationSelect(location);
// }}
// height={height}
// />
// );
// }
// Android: Gunakan MapLibre
// Konversi dari LatLng ke [longitude, latitude] jika perlu
@@ -81,7 +80,6 @@ export function MapSelectedPlatform({
return (
<MapSelectedV2
initialRegion={initialRegion as Region}
selectedLocation={androidLocation}
onLocationSelect={(location: [number, number]) => {
// Konversi dari [longitude, latitude] ke LatLng untuk konsistensi
@@ -92,8 +90,8 @@ export function MapSelectedPlatform({
onLocationSelect(latLng);
}}
height={height}
showUserLocation={showUserLocation}
showsMyLocationButton={showsMyLocationButton}
// showUserLocation={showUserLocation}
// showsMyLocationButton={showsMyLocationButton}
/>
);
}

View File

@@ -1,125 +1,119 @@
import React, { useCallback, useMemo, useRef } from "react";
import {
StyleSheet,
View,
ViewStyle,
StyleProp,
} from "react-native";
import { MainColor } from "@/constants/color-palet";
import React, { useCallback, useRef, useEffect, useState } from "react";
import { StyleSheet, View, ActivityIndicator } from "react-native";
import {
MapView,
Camera,
PointAnnotation,
} from "@maplibre/maplibre-react-native";
import * as Location from "expo-location";
const DEFAULT_MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
const DEBOUNCE_MS = 800;
export interface Region {
latitude: number;
longitude: number;
latitudeDelta: number;
longitudeDelta: number;
}
export interface MapSelectedV2Props {
initialRegion?: Region;
interface Props {
selectedLocation?: [number, number];
onLocationSelect?: (location: [number, number]) => void;
height?: number;
style?: StyleProp<ViewStyle>;
mapViewStyle?: StyleProp<ViewStyle>;
showUserLocation?: boolean;
showsMyLocationButton?: boolean;
mapStyle?: string;
zoomLevel?: number;
}
// ✅ Marker simple tanpa Animated — hapus pulse animation
function SelectedLocationMarker({
color = MainColor.darkblue,
}: {
size?: number;
color?: string;
}) {
return (
<View style={styles.markerContainer}>
<View style={[styles.markerRing, { borderColor: color }]} />
<View style={[styles.markerDot, { backgroundColor: color }]} />
</View>
);
}
export function MapSelectedV2({
initialRegion,
selectedLocation,
onLocationSelect,
height = 400,
style = styles.container,
mapViewStyle = styles.map,
mapStyle,
zoomLevel = 12,
}: MapSelectedV2Props) {
const defaultRegion = useMemo(
() => ({
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
}),
[],
}: Props) {
const lastTapRef = useRef<number>(0);
const cameraRef = useRef<any>(null);
const [userLocation, setUserLocation] = useState<[number, number] | null>(
null,
);
const [isLoadingLocation, setIsLoadingLocation] = useState(true);
const region = initialRegion || defaultRegion;
// ✅ Ambil lokasi user saat pertama mount
useEffect(() => {
(async () => {
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== "granted") {
console.log("Permission lokasi ditolak");
setIsLoadingLocation(false);
return;
}
// ✅ Simpan initial center — TIDAK berubah saat user tap
const initialCenter = useRef<[number, number]>([
region.longitude,
region.latitude,
]);
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
const coords: [number, number] = [
location.coords.longitude,
location.coords.latitude,
];
setUserLocation(coords);
// ✅ Fly ke posisi user jika belum ada selectedLocation
if (!selectedLocation && cameraRef.current) {
cameraRef.current.flyTo(coords, 1000);
}
} catch (error) {
console.log("Gagal ambil lokasi:", error);
} finally {
setIsLoadingLocation(false);
}
})();
}, [isLoadingLocation]);
const handleMapPress = useCallback(
(event: any) => {
const coordinate = event?.geometry?.coordinates || event?.coordinates;
if (coordinate && Array.isArray(coordinate) && coordinate.length === 2) {
console.log("[MapSelectedV2] coordinate", coordinate);
onLocationSelect?.([coordinate[0], coordinate[1]]);
}
const now = Date.now();
if (now - lastTapRef.current < DEBOUNCE_MS) return;
lastTapRef.current = now;
const coords = event?.geometry?.coordinates;
if (!coords || coords.length < 2) return;
onLocationSelect?.([coords[0], coords[1]]);
},
[onLocationSelect],
);
// Center awal kamera:
// 1. Jika ada selectedLocation → pakai itu
// 2. Jika ada userLocation → pakai itu
// 3. Fallback → Bali
const initialCenter: [number, number] = selectedLocation ??
userLocation ?? [115.1756897, -8.737109];
return (
<View style={[style, { height }]} collapsable={false}>
<View style={{ height, width: "100%" }}>
{/* Loading indicator saat fetch lokasi */}
{isLoadingLocation && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="small" color="#0a1f44" />
</View>
)}
<MapView
style={mapViewStyle}
mapStyle={mapStyle || DEFAULT_MAP_STYLE}
style={StyleSheet.absoluteFillObject}
mapStyle={MAP_STYLE}
onPress={handleMapPress}
logoEnabled={false}
compassEnabled={true}
compassViewPosition={2}
compassViewMargins={{ x: 10, y: 10 }}
scrollEnabled={true}
zoomEnabled={true}
rotateEnabled={true}
pitchEnabled={false}
>
{/* ✅ Camera hanya set sekali di awal, tidak reactive ke selectedLocation */}
<Camera
ref={cameraRef}
defaultSettings={{
centerCoordinate: initialCenter.current,
zoomLevel: zoomLevel,
centerCoordinate: initialCenter,
zoomLevel: 14,
}}
/>
{/* ✅ Hanya render PointAnnotation jika ada selectedLocation */}
{/* ✅ Key statis — tidak pernah unmount/remount */}
{selectedLocation && (
<PointAnnotation
id="selected-location"
key="selected-location"
coordinate={selectedLocation}
>
<SelectedLocationMarker />
<View style={styles.dot} />
</PointAnnotation>
)}
</MapView>
@@ -128,37 +122,29 @@ export function MapSelectedV2({
}
const styles = StyleSheet.create({
container: {
width: "100%",
backgroundColor: "#f5f5f5",
overflow: "hidden",
borderRadius: 8,
dot: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: "#0a1f44",
borderWidth: 2,
borderColor: "#fff",
},
map: {
flex: 1,
},
markerContainer: {
width: 40,
height: 40,
alignItems: "center",
justifyContent: "center",
},
// ✅ Ring statis pengganti pulse animation
markerRing: {
loadingOverlay: {
position: "absolute",
width: 36,
height: 36,
borderRadius: 18,
borderWidth: 2,
opacity: 0.4,
},
markerDot: {
width: 16,
height: 16,
borderRadius: 8,
borderWidth: 2,
borderColor: "#FFFFFF",
top: 10,
alignSelf: "center",
zIndex: 10,
backgroundColor: "#fff",
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 6,
elevation: 4,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 4,
},
});
export default MapSelectedV2;
export default MapSelectedV2;

View File

@@ -165,6 +165,12 @@
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */,
51F61F14096F4B7FBD9344A7 /* Remove signature files (Xcode workaround) */,
60F3AE3AC4B24C2FA7FC8F10 /* Remove signature files (Xcode workaround) */,
E188CB171C1B4A4DA64BC5C4 /* Remove signature files (Xcode workaround) */,
90714A8C562E4676B84E6E07 /* Remove signature files (Xcode workaround) */,
DB93AB500BC2459E8BAE3F74 /* Remove signature files (Xcode workaround) */,
EEC6AC8AF9C04E91AA81C190 /* Remove signature files (Xcode workaround) */,
D2BED766D85C4781B154BD69 /* Remove signature files (Xcode workaround) */,
E01278D305D540D5B29ED50A /* Remove signature files (Xcode workaround) */,
);
buildRules = (
);
@@ -671,6 +677,108 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
E188CB171C1B4A4DA64BC5C4 /* 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\";
";
};
90714A8C562E4676B84E6E07 /* 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\";
";
};
DB93AB500BC2459E8BAE3F74 /* 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\";
";
};
EEC6AC8AF9C04E91AA81C190 /* 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\";
";
};
D2BED766D85C4781B154BD69 /* 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\";
";
};
E01278D305D540D5B29ED50A /* 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 */
/* Begin PBXSourcesBuildPhase section */

View File

@@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0.1</string>
<string>1.0.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>21</string>
<string>1</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@@ -9,7 +9,7 @@ import { apiJobGetAll } from "@/service/api-client/api-job";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
export default function Home_BottomFeatureSection() {
const [listData, setListData] = useState<any>([]);
const [listData, setListData] = useState<any[] | null>(null);
const onLoadData = async () => {
try {
@@ -17,7 +17,7 @@ export default function Home_BottomFeatureSection() {
category: "beranda",
});
// console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
const result = response.data
.sort(
(a: any, b: any) =>
@@ -36,7 +36,7 @@ export default function Home_BottomFeatureSection() {
}, [])
);
if (!listData || listData.length === 0) {
if (listData === null) {
return <CustomSkeleton height={200}/>
}
@@ -54,7 +54,7 @@ export default function Home_BottomFeatureSection() {
<View style={stylesHome.vacancyList}>
{/* Vacancy Item 1 */}
{listData.map((item: any, index: number) => (
{listData?.map((item: any, index: number) => (
<View style={stylesHome.vacancyItem} key={index}>
<View style={stylesHome.vacancyDetails}>
<TextCustom bold color="yellow" truncate size="large">

View File

@@ -20,7 +20,6 @@ import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { LatLng } from "react-native-maps";
import Toast from "react-native-toast-message";
import MapSelected from "@/components/Map/MapSelected";
/**
* Screen untuk create maps
@@ -150,14 +149,9 @@ export function Maps_ScreenMapsCreate() {
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
<BaseBox>
{/* <MapSelected
selectedLocation={selectedLocation as any}
setSelectedLocation={setSelectedLocation}
/> */}
<MapSelectedPlatform
selectedLocation={selectedLocation}
onLocationSelect={(location) => {
// Set location (auto handle LatLng format)
setSelectedLocation(location as LatLng);
}}
height={300}

View File

@@ -9,6 +9,9 @@ import {
TextInputCustom,
ViewWrapper,
} from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MapSelectedPlatform } from "@/components/Map/MapSelectedPlatform";
import MapSelectedV2 from "@/components/Map/MapSelectedV2";
import API_IMAGE from "@/constants/api-storage";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiMapsGetOne, apiMapsUpdate } from "@/service/api-client/api-maps";
@@ -16,8 +19,7 @@ import { uploadFileService } from "@/service/upload-service";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { StyleSheet, View } from "react-native";
import MapView, { LatLng, Marker } from "react-native-maps";
import { LatLng } from "react-native-maps";
import Toast from "react-native-toast-message";
const defaultRegion = {
@@ -43,7 +45,7 @@ export function Maps_ScreenMapsEdit() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -64,10 +66,14 @@ export function Maps_ScreenMapsEdit() {
}
};
const handleMapPress = (event: any) => {
const { latitude, longitude } = event.nativeEvent.coordinate;
const location = { latitude, longitude };
setSelectedLocation(location);
const handleLocationSelect = (location: LatLng | [number, number]) => {
if (Array.isArray(location)) {
// Android format: [longitude, latitude]
setSelectedLocation({ latitude: location[1], longitude: location[0] });
} else {
// iOS format: LatLng
setSelectedLocation(location);
}
};
const handleSubmit = async () => {
@@ -149,51 +155,41 @@ export function Maps_ScreenMapsEdit() {
</BoxButtonOnFooter>
);
const initialRegion =
data?.latitude && data?.longitude
? {
latitude: Number(data?.latitude),
longitude: Number(data?.longitude),
latitudeDelta: 0.1,
longitudeDelta: 0.1,
}
: defaultRegion;
return (
<ViewWrapper footerComponent={buttonFooter}>
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
<View style={[styles.container, { height: 400 }]}>
<MapView
style={styles.map}
initialRegion={
data?.latitude && data?.longitude
? {
latitude: data?.latitude,
longitude: data?.longitude,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
}
: defaultRegion
}
onPress={handleMapPress}
showsUserLocation={true}
showsMyLocationButton={true}
loadingEnabled={true}
loadingIndicatorColor="#666"
loadingBackgroundColor="#f0f0f0"
>
{selectedLocation ? (
<Marker
coordinate={selectedLocation}
title="Lokasi Dipilih"
description={`Lat: ${selectedLocation.latitude.toFixed(
6
)}, Lng: ${selectedLocation.longitude.toFixed(6)}`}
pinColor="red"
/>
) : (
<Marker
coordinate={defaultRegion}
title="Lokasi Dipilih"
description={`Lat: ${defaultRegion.latitude.toFixed(
6
)}, Lng: ${defaultRegion.longitude.toFixed(6)}`}
pinColor="red"
/>
)}
</MapView>
</View>
{/* <MapSelectedPlatform
initialRegion={initialRegion}
selectedLocation={selectedLocation}
onLocationSelect={handleLocationSelect}
height={400}
/> */}
{!data || !data.latitude || !data.longitude ? (
<CustomSkeleton height={200} />
) : (
<MapSelectedV2
selectedLocation={[data.longitude, data.latitude]}
onLocationSelect={(location: [number, number]) => {
setData({
...data,
longitude: location[0],
latitude: location[1],
});
}}
/>
)}
<TextInputCustom
required
@@ -230,16 +226,3 @@ export function Maps_ScreenMapsEdit() {
</ViewWrapper>
);
}
const styles = StyleSheet.create({
container: {
width: "100%",
backgroundColor: "#f5f5f5",
overflow: "hidden",
borderRadius: 8,
marginBottom: 20,
},
map: {
flex: 1,
},
});