diff --git a/QWEN.md b/QWEN.md index de3f5de..8ee1eca 100644 --- a/QWEN.md +++ b/QWEN.md @@ -10,7 +10,7 @@ HIPMI Mobile is a cross-platform mobile application built with Expo and React Na - **Architecture**: File-based routing with Expo Router - **State Management**: Context API (AuthContext) - **UI Components**: React Native Paper, custom components -- **Maps Integration**: Mapbox Maps for React Native +- **Maps Integration**: Maplibre Maps for React Native (`@maplibre/maplibre-react-native` v10.4.2) - **Push Notifications**: React Native Firebase Messaging - **Build System**: Metro bundler - **Package Manager**: Bun @@ -381,8 +381,8 @@ apiConfig.interceptors.request.use(async (config) => { - Push Notifications (FCM) - Configured for both iOS and Android -### Mapbox -- Map integration via `@rnmapbox/maps` +### Maplibre +- Map integration via `@maplibre/maplibre-react-native` - Location permissions configured ### Deep Linking @@ -475,10 +475,34 @@ rm -rf node_modules bun.lock bun install ``` +### iOS Maplibre Crash Fix + +When using Maplibre MapView on iOS, prevent "Attempt to recycle a mounted view" crash: + +1. **Always render PointAnnotation** (not conditional) +2. **Use opacity for visibility** instead of conditional rendering +3. **Avoid key prop changes** that force remounting + +```typescript +// ✅ GOOD: Stable PointAnnotation + + + + + + +// ❌ BAD: Conditional rendering causes crash +{selectedLocation && ( + +)} +``` + ## Documentation Files - `docs/CHANGE_LOG.md` - Change log for recent updates -- `docs/COMMIT_NOTES.md` - Commit notes and guidelines - `docs/hipmi-note.md` - Build and deployment notes - `docs/prompt-for-qwen-code.md` - Development prompts and patterns @@ -488,3 +512,4 @@ bun install - [React Native Documentation](https://reactnative.dev/) - [Expo Router Documentation](https://docs.expo.dev/router/introduction/) - [TypeScript Documentation](https://www.typescriptlang.org/docs/) +- [Maplibre React Native](https://github.com/maplibre/maplibre-react-native) diff --git a/android/app/build.gradle b/android/app/build.gradle index b761b19..836f4c2 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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'}\"" } diff --git a/app.config.js b/app.config.js index 3a7b560..2403739 100644 --- a/app.config.js +++ b/app.config.js @@ -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: [ { diff --git a/app/(application)/(user)/maps/[id]/edit.tsx b/app/(application)/(user)/maps/[id]/edit.tsx index 75f8894..c500870 100644 --- a/app/(application)/(user)/maps/[id]/edit.tsx +++ b/app/(application)/(user)/maps/[id]/edit.tsx @@ -1,244 +1,5 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { - BoxButtonOnFooter, - ButtonCenteredOnly, - ButtonCustom, - InformationBox, - LandscapeFrameUploaded, - Spacing, - TextInputCustom, - ViewWrapper, -} from "@/components"; -import API_IMAGE from "@/constants/api-storage"; -import DIRECTORY_ID from "@/constants/directory-id"; -import { apiMapsGetOne, apiMapsUpdate } from "@/service/api-client/api-maps"; -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 Toast from "react-native-toast-message"; +import { Maps_ScreenMapsEdit } from "@/screens/Maps/ScreenMapsEdit"; -const defaultRegion = { - latitude: -8.737109, - longitude: 115.1756897, - latitudeDelta: 0.1, - longitudeDelta: 0.1, -}; export default function MapsEdit() { - const { id } = useLocalSearchParams(); - const [data, setData] = useState({ - id: "", - namePin: "", - latitude: "", - longitude: "", - imageId: "", - }); - const [selectedLocation, setSelectedLocation] = useState(null); - const [image, setImage] = useState(null); - const [isLoading, setLoading] = useState(false); - - useFocusEffect( - useCallback(() => { - onLoadData(); - }, [id]) - ); - - const onLoadData = async () => { - try { - const response = await apiMapsGetOne({ id: id as string }); - - if (response.success) { - setData({ - id: response.data.id, - namePin: response.data.namePin, - latitude: response.data.latitude, - longitude: response.data.longitude, - imageId: response.data.imageId, - }); - } - } catch (error) { - console.log("[ERROR]", error); - } - }; - - const handleMapPress = (event: any) => { - const { latitude, longitude } = event.nativeEvent.coordinate; - const location = { latitude, longitude }; - setSelectedLocation(location); - }; - - const handleSubmit = async () => { - let newData: any; - if (!data.namePin) { - Toast.show({ - type: "error", - text1: "Nama pin harus diisi", - }); - return; - } - - newData = { - namePin: data?.namePin, - latitude: selectedLocation?.latitude || data?.latitude, - longitude: selectedLocation?.longitude || data?.longitude, - }; - - try { - setLoading(true); - if (image) { - const responseUpload = await uploadFileService({ - dirId: DIRECTORY_ID.map_image, - imageUri: image?.uri, - }); - - if (!responseUpload?.data?.id) { - Toast.show({ - type: "error", - text1: "Gagal mengunggah gambar", - }); - return; - } - - const imageId = responseUpload?.data?.id; - - newData = { - namePin: data?.namePin, - latitude: selectedLocation?.latitude, - longitude: selectedLocation?.longitude, - newImageId: imageId, - }; - } - - const responseUpdate = await apiMapsUpdate({ - id: data?.id, - data: newData, - }); - - if (!responseUpdate.success) { - Toast.show({ - type: "error", - text1: "Gagal mengupdate map", - }); - return; - } - - Toast.show({ - type: "success", - text1: "Map berhasil diupdate", - }); - router.back(); - } catch (error) { - console.log("[ERROR]", error); - } finally { - setLoading(false); - } - }; - - const buttonFooter = ( - - - Update - - - ); - - return ( - - - - - - {selectedLocation ? ( - - ) : ( - - )} - - - - setData({ ...data, namePin: value })} - /> - - - - - - { - pickFile({ - allowedType: "image", - setImageUri(file) { - setImage(file); - }, - }); - }} - > - Upload - - - - ); + return ; } - -const styles = StyleSheet.create({ - container: { - width: "100%", - backgroundColor: "#f5f5f5", - overflow: "hidden", - borderRadius: 8, - marginBottom: 20, - }, - map: { - flex: 1, - }, -}); diff --git a/app/(application)/(user)/maps/create.tsx b/app/(application)/(user)/maps/create.tsx index 2c8416b..8052baa 100644 --- a/app/(application)/(user)/maps/create.tsx +++ b/app/(application)/(user)/maps/create.tsx @@ -1,143 +1,9 @@ -import { - BaseBox, - BoxButtonOnFooter, - ButtonCenteredOnly, - ButtonCustom, - InformationBox, - LandscapeFrameUploaded, - Spacing, - TextInputCustom, - ViewWrapper, -} from "@/components"; -import MapSelected from "@/components/Map/MapSelected"; -import DIRECTORY_ID from "@/constants/directory-id"; -import { useAuth } from "@/hooks/use-auth"; -import { apiMapsCreate } from "@/service/api-client/api-maps"; -import { uploadFileService } from "@/service/upload-service"; -import pickFile, { IFileData } from "@/utils/pickFile"; -import { router, useLocalSearchParams } from "expo-router"; -import { useState } from "react"; -import { LatLng } from "react-native-maps"; -import Toast from "react-native-toast-message"; +import Maps_ScreenMapsCreate from "@/screens/Maps/ScreenMapsCreate"; export default function MapsCreate() { - const { user } = useAuth(); - const { id } = useLocalSearchParams(); - const [selectedLocation, setSelectedLocation] = useState(null); - const [name, setName] = useState(""); - const [image, setImage] = useState(null); - const [isLoading, setLoading] = useState(false); - - const handleSubmit = async () => { - try { - setLoading(true); - let newData: any; - newData = { - authorId: user?.id, - portofolioId: id, - namePin: name, - latitude: selectedLocation?.latitude, - longitude: selectedLocation?.longitude, - }; - - if (image) { - const responseUpload = await uploadFileService({ - dirId: DIRECTORY_ID.map_image, - imageUri: image?.uri, - }); - - if (!responseUpload?.data?.id) { - Toast.show({ - type: "error", - text1: "Gagal mengunggah gambar", - }); - return; - } - - const imageId = responseUpload?.data?.id; - - newData = { - authorId: user?.id, - portofolioId: id, - namePin: name, - latitude: selectedLocation?.latitude, - longitude: selectedLocation?.longitude, - imageId: imageId, - }; - } - - const response = await apiMapsCreate({ - data: newData, - }); - - if (!response.success) { - Toast.show({ - type: "error", - text1: "Gagal menambahkan map", - }); - return; - } - - Toast.show({ - type: "success", - text1: "Map berhasil ditambahkan", - }); - router.back(); - } catch (error) { - console.log("[ERROR]", error); - } finally { - setLoading(false); - } - }; - - const buttonFooter = ( - - - Simpan - - - ); return ( - - - - - - - - - - - - - - { - pickFile({ - allowedType: "image", - setImageUri(file) { - setImage(file); - }, - }); - }} - > - Upload - - - + <> + + ); } diff --git a/components/Map/MapSelectedPlatform.tsx b/components/Map/MapSelectedPlatform.tsx new file mode 100644 index 0000000..1eae1df --- /dev/null +++ b/components/Map/MapSelectedPlatform.tsx @@ -0,0 +1,110 @@ +import { Platform } from "react-native"; +import MapSelected from "./MapSelected"; +import MapSelectedV2 from "./MapSelectedV2"; +import { LatLng } from "react-native-maps"; + +/** + * Props untuk komponen MapSelectedPlatform + * Mendukung kedua format koordinat (LatLng untuk iOS, [number, number] untuk Android) + */ +export interface MapSelectedPlatformProps { + /** Region awal kamera */ + initialRegion?: { + latitude?: number; + longitude?: number; + latitudeDelta?: number; + longitudeDelta?: number; + }; + + /** Lokasi yang dipilih (support kedua format) */ + selectedLocation: LatLng | [number, number] | null; + + /** Callback ketika lokasi dipilih */ + onLocationSelect: (location: LatLng | [number, number]) => void; + + /** Tinggi peta dalam pixels (default: 400) */ + height?: number; + + /** Tampilkan lokasi user (default: true) */ + showUserLocation?: boolean; + + /** Tampilkan tombol my location (default: true) */ + showsMyLocationButton?: boolean; +} + +/** + * Komponen Map yang otomatis memilih implementasi berdasarkan platform + * + * Platform Strategy: + * - **iOS**: Menggunakan react-native-maps (MapSelected) + * - **Android**: Menggunakan @maplibre/maplibre-react-native (MapSelectedV2) + * + * @example + * ```tsx + * + * ``` + */ +export function MapSelectedPlatform({ + initialRegion, + selectedLocation, + onLocationSelect, + height = 400, + showUserLocation = true, + showsMyLocationButton = true, +}: MapSelectedPlatformProps) { + // iOS: Gunakan react-native-maps + // if (Platform.OS === "ios") { + // return ( + // { + // onLocationSelect(location); + // }} + // height={height} + // /> + // ); + // } + + // Android: Gunakan MapLibre + // Konversi dari LatLng ke [longitude, latitude] jika perlu + const androidLocation: [number, number] | undefined = selectedLocation + ? isLatLng(selectedLocation) + ? [selectedLocation.longitude, selectedLocation.latitude] + : selectedLocation + : undefined; + + return ( + { + // Konversi dari [longitude, latitude] ke LatLng untuk konsistensi + const latLng: LatLng = { + latitude: location[1], + longitude: location[0], + }; + onLocationSelect(latLng); + }} + height={height} + // showUserLocation={showUserLocation} + // showsMyLocationButton={showsMyLocationButton} + /> + ); +} + +/** + * Type guard untuk mengecek apakah object adalah LatLng + */ +function isLatLng(location: any): location is LatLng { + return ( + location && + typeof location.latitude === "number" && + typeof location.longitude === "number" + ); +} + +export default MapSelectedPlatform; diff --git a/components/Map/MapSelectedV2.tsx b/components/Map/MapSelectedV2.tsx new file mode 100644 index 0000000..432ec1e --- /dev/null +++ b/components/Map/MapSelectedV2.tsx @@ -0,0 +1,150 @@ +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 MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty"; +const DEBOUNCE_MS = 800; + +interface Props { + selectedLocation?: [number, number]; + onLocationSelect?: (location: [number, number]) => void; + height?: number; +} + +export function MapSelectedV2({ + selectedLocation, + onLocationSelect, + height = 400, +}: Props) { + const lastTapRef = useRef(0); + const cameraRef = useRef(null); + + const [userLocation, setUserLocation] = useState<[number, number] | null>( + null, + ); + const [isLoadingLocation, setIsLoadingLocation] = useState(true); + + // ✅ 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; + } + + 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 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 ( + + {/* Loading indicator saat fetch lokasi */} + {isLoadingLocation && ( + + + + )} + + + + + {selectedLocation && ( + + + + )} + + + ); +} + +const styles = StyleSheet.create({ + dot: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: "#0a1f44", + borderWidth: 2, + borderColor: "#fff", + }, + loadingOverlay: { + position: "absolute", + 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; diff --git a/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md index 1f24be0..a7682cd 100644 --- a/docs/prompt-for-qwen-code.md +++ b/docs/prompt-for-qwen-code.md @@ -111,10 +111,12 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1" ( stri Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx -File Utama: screens/Admin/Investment/ScreenInvestmentStatus.tsx -Folder tujuan: screens/Admin/Investment -Reffrensi: screens/Admin/Donation/BoxDonationStatus.tsx -Buatkan box component baru pada file "File Utama" di bagian renderItem agar lebih rapi buat file baru dengan nama BoxInvestmentStatus.tsx +File Utama: app/(application)/(user)/maps/[id]/edit.tsx +Folder tujuan: screens/Maps +Nama file utama: ScreenMapsEdit.tsx +Nama function utama: Maps_ScreenMapsEdit + +Buatkan file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source" diff --git a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj index fa94b4c..18e7185 100644 --- a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj +++ b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj @@ -163,6 +163,14 @@ 5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */, 0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */, 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 = ( ); @@ -635,6 +643,142 @@ rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; "; }; + 51F61F14096F4B7FBD9344A7 /* 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\"; + "; + }; + 60F3AE3AC4B24C2FA7FC8F10 /* 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\"; + "; + }; + 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 */ diff --git a/ios/HIPMIBadungConnect/Info.plist b/ios/HIPMIBadungConnect/Info.plist index 9f0f327..a743c17 100644 --- a/ios/HIPMIBadungConnect/Info.plist +++ b/ios/HIPMIBadungConnect/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.0.1 + 1.0.2 CFBundleSignature ???? CFBundleURLTypes @@ -39,7 +39,7 @@ CFBundleVersion - 21 + 1 ITSAppUsesNonExemptEncryption LSMinimumSystemVersion diff --git a/screens/Home/bottomFeatureSection.tsx b/screens/Home/bottomFeatureSection.tsx index 9e56f64..23bde4b 100644 --- a/screens/Home/bottomFeatureSection.tsx +++ b/screens/Home/bottomFeatureSection.tsx @@ -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([]); + const [listData, setListData] = useState(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 } @@ -54,7 +54,7 @@ export default function Home_BottomFeatureSection() { {/* Vacancy Item 1 */} - {listData.map((item: any, index: number) => ( + {listData?.map((item: any, index: number) => ( diff --git a/screens/Maps/DrawerMaps.tsx b/screens/Maps/DrawerMaps.tsx index 31ddffa..c6249dc 100644 --- a/screens/Maps/DrawerMaps.tsx +++ b/screens/Maps/DrawerMaps.tsx @@ -40,8 +40,12 @@ export default function DrawerMaps({ closeDrawer={() => setOpenDrawer(false)} height={"auto"} > - - + {selected.imageId && ( + <> + + + + )} (null); + const [name, setName] = useState(""); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + /** + * Handle submit form + * Upload image (jika ada) dan submit data maps ke API + */ + const handleSubmit = async () => { + try { + setLoading(true); + + // Prepare data tanpa image dulu + let newData: any = { + authorId: user?.id, + portofolioId: id, + namePin: name, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + }; + + // Upload image jika ada + if (image) { + const responseUpload = await uploadFileService({ + dirId: DIRECTORY_ID.map_image, + imageUri: image?.uri, + }); + + if (!responseUpload?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUpload?.data?.id; + + newData = { + authorId: user?.id, + portofolioId: id, + namePin: name, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + imageId: imageId, + }; + } + + // Submit ke API + const response = await apiMapsCreate({ + data: newData, + }); + + if (!response.success) { + Toast.show({ + type: "error", + text1: "Gagal menambahkan map", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Map berhasil ditambahkan", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + /** + * Handle image upload + */ + const handleImageUpload = async () => { + try { + await pickFile({ + allowedType: "image", + setImageUri: (file) => { + setImage(file); + }, + }); + } catch (error) { + console.log("[MapsCreate] Image upload error:", error); + } + }; + + /** + * Footer component dengan button submit + */ + const buttonFooter = ( + + + Simpan + + + ); + + /** + * Render screen dengan NewWrapper + */ + return ( + + + + + { + setSelectedLocation(location as LatLng); + }} + height={300} + showUserLocation={true} + showsMyLocationButton={true} + /> + + + + + + + + + + + + Upload + + + + + ); +} + +export default Maps_ScreenMapsCreate; diff --git a/screens/Maps/ScreenMapsEdit.tsx b/screens/Maps/ScreenMapsEdit.tsx new file mode 100644 index 0000000..deee80f --- /dev/null +++ b/screens/Maps/ScreenMapsEdit.tsx @@ -0,0 +1,228 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + Spacing, + 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"; +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 { LatLng } from "react-native-maps"; +import Toast from "react-native-toast-message"; + +const defaultRegion = { + latitude: -8.737109, + longitude: 115.1756897, + latitudeDelta: 0.1, + longitudeDelta: 0.1, +}; + +export function Maps_ScreenMapsEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + id: "", + namePin: "", + latitude: "", + longitude: "", + imageId: "", + }); + const [selectedLocation, setSelectedLocation] = useState(null); + const [image, setImage] = useState(null); + const [isLoading, setLoading] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [id]), + ); + + const onLoadData = async () => { + try { + const response = await apiMapsGetOne({ id: id as string }); + + if (response.success) { + setData({ + id: response.data.id, + namePin: response.data.namePin, + latitude: response.data.latitude, + longitude: response.data.longitude, + imageId: response.data.imageId, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + 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 () => { + let newData: any; + if (!data.namePin) { + Toast.show({ + type: "error", + text1: "Nama pin harus diisi", + }); + return; + } + + newData = { + namePin: data?.namePin, + latitude: selectedLocation?.latitude || data?.latitude, + longitude: selectedLocation?.longitude || data?.longitude, + }; + + try { + setLoading(true); + if (image) { + const responseUpload = await uploadFileService({ + dirId: DIRECTORY_ID.map_image, + imageUri: image?.uri, + }); + + if (!responseUpload?.data?.id) { + Toast.show({ + type: "error", + text1: "Gagal mengunggah gambar", + }); + return; + } + + const imageId = responseUpload?.data?.id; + + newData = { + namePin: data?.namePin, + latitude: selectedLocation?.latitude, + longitude: selectedLocation?.longitude, + newImageId: imageId, + }; + } + + const responseUpdate = await apiMapsUpdate({ + id: data?.id, + data: newData, + }); + + if (!responseUpdate.success) { + Toast.show({ + type: "error", + text1: "Gagal mengupdate map", + }); + return; + } + + Toast.show({ + type: "success", + text1: "Map berhasil diupdate", + }); + router.back(); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoading(false); + } + }; + + const buttonFooter = ( + + + Update + + + ); + + const initialRegion = + data?.latitude && data?.longitude + ? { + latitude: Number(data?.latitude), + longitude: Number(data?.longitude), + latitudeDelta: 0.1, + longitudeDelta: 0.1, + } + : defaultRegion; + + return ( + + + + {/* */} + + {!data || !data.latitude || !data.longitude ? ( + + ) : ( + { + setData({ + ...data, + longitude: location[0], + latitude: location[1], + }); + }} + /> + )} + + setData({ ...data, namePin: value })} + /> + + + + + + { + pickFile({ + allowedType: "image", + setImageUri(file) { + setImage(file); + }, + }); + }} + > + Upload + + + + ); +}