Compare commits

..

6 Commits

Author SHA1 Message Date
836ef709d2 Fix bug home
### No Issue
2026-03-05 16:41:29 +08:00
3bbee15c3a Perbaikan Bug & Error Handling: │
│                                                                                                            │
  │    1. Device Token Registration Error (HTTP 500)                                                           │
  │       - File: service/api-device-token.ts                                                                  │
  │       - Fix: Hapus nested data wrapper pada payload                                                        │
  │       - Improvement: Tambahkan error logging detail                                                        │
  │                                                                                                            │
  │    2. Uncaught Promise Errors                                                                              │
  │       - File: components/Notification/NotificationInitializer.tsx                                          │
  │       - Fix: Better error handling untuk device token registration                                         │
  │       - File: app/(application)/(user)/home.tsx                                                            │
  │       - Fix: Add .catch() untuk userData() dan error handling apiUser()                                    │
  │       - File: app/(application)/(user)/profile/[id]/index.tsx                                              │
  │       - Fix: Add error handling untuk apiProfile(), apiUser(), userData()                                  │
  │                                                                                                            │
  │    3. UI Improvements                                                                                      │
  │       - File: app/(application)/(user)/home.tsx                                                            │
  │       - Feature: 4 skeleton lingkaran untuk loading state grid features                                    │
  │                                                                                                            │
  │    4. Maps Migration                                                                                       │
  │       - File: app/(application)/admin/maps.tsx                                                             │
  │       - Change: Replace react-native-maps dengan MapsV2Custom (Maplibre)                                   │
  │       - Cleanup: Hapus unused imports dan interfaces                                                       │
  │                                                                                                            │
  │   Files Modified (7)                                                                                       │
  │    - app/(application)/(user)/home.tsx                                                                     │
  │    - app/(application)/(user)/profile/[id]/index.tsx                                                       │
  │    - app/(application)/admin/maps.tsx                                                                      │
  │    - components/Notification/NotificationInitializer.tsx                                                   │
  │    - service/api-device-token.ts                                                                           │
  │    - constants/constans-value.ts                                                                           │
  │    - screens/Home/bottomFeatureSection.tsx                                                                 │
  │    - screens/UserSeach/MainView_V2.tsx

### No Issue
2026-03-04 16:39:57 +08:00
ad7dbaf162 Fix Bug DB
User Pages
- app/(application)/(user)/home.tsx
- app/(application)/(user)/portofolio/[id]/index.tsx
- app/(application)/(user)/profile/[id]/index.tsx

Home
- screens/Home/bottomFeatureSection.tsx

Components
- components/Notification/NotificationInitializer.tsx
- components/_ShareComponent/SkeletonCustom.tsx

Service
- service/api-device-token.ts

Config & iOS
- app.config.js
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-03-03 16:44:45 +08:00
9c94ec0454 Fix Bug
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
2026-03-02 16:34:24 +08:00
4c63485a5b Clean Code Edit Maps
Maps Edit Feature
- app/(application)/(user)/maps/[id]/edit.tsx
- components/Map/MapSelectedV2.tsx

Docs
- docs/prompt-for-qwen-code.md

New Screen
- screens/Maps/ScreenMapsEdit.tsx

### No Issue
2026-03-02 10:31:29 +08:00
f5d09a2906 Fix create maps
iOS Project
- HIPMIBadungConnect.xcodeproj/project.pbxproj

Maps & Location Screens
- screens/Maps/MapsView2.tsx
- screens/Portofolio/BusinessLocationSection.tsx

New Map Components
- components/Map/MapsV2Custom.tsx
- components/Map/SelectLocationMap.tsx

### No Issue
2026-02-27 16:57:01 +08:00
22 changed files with 1235 additions and 584 deletions

33
QWEN.md
View File

@@ -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 - **Architecture**: File-based routing with Expo Router
- **State Management**: Context API (AuthContext) - **State Management**: Context API (AuthContext)
- **UI Components**: React Native Paper, custom components - **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 - **Push Notifications**: React Native Firebase Messaging
- **Build System**: Metro bundler - **Build System**: Metro bundler
- **Package Manager**: Bun - **Package Manager**: Bun
@@ -381,8 +381,8 @@ apiConfig.interceptors.request.use(async (config) => {
- Push Notifications (FCM) - Push Notifications (FCM)
- Configured for both iOS and Android - Configured for both iOS and Android
### Mapbox ### Maplibre
- Map integration via `@rnmapbox/maps` - Map integration via `@maplibre/maplibre-react-native`
- Location permissions configured - Location permissions configured
### Deep Linking ### Deep Linking
@@ -475,10 +475,34 @@ rm -rf node_modules bun.lock
bun install 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
<PointAnnotation
coordinate={annotationCoordinate} // Always rendered
...
>
<View style={{ opacity: selectedLocation ? 1 : 0 }}>
<SelectedLocationMarker />
</View>
</PointAnnotation>
// ❌ BAD: Conditional rendering causes crash
{selectedLocation && (
<PointAnnotation coordinate={selectedLocation} ... />
)}
```
## Documentation Files ## Documentation Files
- `docs/CHANGE_LOG.md` - Change log for recent updates - `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/hipmi-note.md` - Build and deployment notes
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns - `docs/prompt-for-qwen-code.md` - Development prompts and patterns
@@ -488,3 +512,4 @@ bun install
- [React Native Documentation](https://reactnative.dev/) - [React Native Documentation](https://reactnative.dev/)
- [Expo Router Documentation](https://docs.expo.dev/router/introduction/) - [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
- [TypeScript Documentation](https://www.typescriptlang.org/docs/) - [TypeScript Documentation](https://www.typescriptlang.org/docs/)
- [Maplibre React Native](https://github.com/maplibre/maplibre-react-native)

View File

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

View File

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

View File

@@ -1,66 +1,93 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components"; import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store"; import { useNotificationStore } from "@/hooks/use-notification-store";
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection"; import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
import HeaderBell from "@/screens/Home/HeaderBell"; import HeaderBell from "@/screens/Home/HeaderBell";
import { stylesHome } from "@/screens/Home/homeViewStyle";
import Home_ImageSection from "@/screens/Home/imageSection"; import Home_ImageSection from "@/screens/Home/imageSection";
import TabSection from "@/screens/Home/tabSection"; import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList"; import { tabsHome } from "@/screens/Home/tabsList";
import Home_FeatureSection from "@/screens/Home/topFeatureSection"; import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { apiUser } from "@/service/api-client/api-user"; import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config"; import { apiVersion } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack, useFocusEffect } from "expo-router"; import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { RefreshControl } from "react-native"; import { RefreshControl, View } from "react-native";
export default function Application() { export default function Application() {
const { token, user, userData } = useAuth(); const { token, user, userData } = useAuth();
const [data, setData] = useState<any>(); const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore(); const { syncUnreadCount } = useNotificationStore();
const [listData, setListData] = useState<any[] | null>(null);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
onLoadDataJob();
checkVersion(); checkVersion();
userData(token as string); userData(token as string).catch((error) => {
console.log("[ERROR userData]", error?.message);
console.log("[ERROR userData Response]", error?.response?.data);
});
syncUnreadCount(); syncUnreadCount();
}, [user?.id, token]), }, [user?.id, token]),
); );
async function onLoadData() { async function onLoadData() {
const response = await apiUser(user?.id as string); try {
console.log( const response = await apiUser(user?.id as string);
"[Profile ID]>>", setData(response.data);
JSON.stringify(response?.data?.Profile?.id, null, 2), } catch (error: any) {
); console.log("[ERROR onLoadData]", error?.message);
console.log("[ERROR Response]", error?.response?.data);
setData(response.data); // Set data tetap agar UI tidak stuck di loading
setData(null);
}
} }
const onLoadDataJob = async () => {
try {
const response = await apiJobGetAll({
category: "beranda",
});
const result = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
)
.slice(0, 2);
setListData(result);
} catch (error) {
console.log("[ERROR]", error);
}
};
const checkVersion = async () => { const checkVersion = async () => {
const response = await apiVersion(); try {
console.log("[Version] >>", JSON.stringify(response.data, null, 2)); const response = await apiVersion();
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
} catch (error: any) {
console.log("[ERROR checkVersion]", error?.message);
}
}; };
const onRefresh = useCallback(() => { const onRefresh = useCallback(() => {
setRefreshing(true); setRefreshing(true);
onLoadData(); onLoadData();
onLoadDataJob();
checkVersion(); checkVersion();
setRefreshing(false); setRefreshing(false);
}, []); }, []);
// if (user && user?.termsOfServiceAccepted === false) {
// console.log("User is not accept term service");
// return <Redirect href={`/terms-agreement`} />;
// }
if (data && data?.active === false) { if (data && data?.active === false) {
console.log("User is not active"); console.warn("User is not active");
return ( return (
<BasicWrapper> <BasicWrapper>
<Redirect href={`/waiting-room`} /> <Redirect href={`/waiting-room`} />
@@ -69,7 +96,7 @@ export default function Application() {
} }
if (data && data?.Profile === null) { if (data && data?.Profile === null) {
console.log("Profile is null"); console.warn("Profile is null");
return ( return (
<BasicWrapper> <BasicWrapper>
<Redirect href={`/profile/create`} /> <Redirect href={`/profile/create`} />
@@ -91,17 +118,25 @@ export default function Application() {
<Stack.Screen <Stack.Screen
options={{ options={{
title: `HIPMI`, title: `HIPMI`,
headerLeft: () => ( headerLeft: () =>
<Ionicons data ? (
name="search" <Ionicons
size={20} name="search"
color={MainColor.yellow} size={20}
onPress={() => { color={MainColor.yellow}
router.push("/user-search"); onPress={() => {
}} router.push("/user-search");
/> }}
), />
headerRight: () => <HeaderBell />, ) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
headerRight: () =>
data ? (
<HeaderBell />
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
}} }}
/> />
<ViewWrapper <ViewWrapper
@@ -114,25 +149,51 @@ export default function Application() {
/> />
} }
footerComponent={ footerComponent={
<TabSection data && data ? (
<TabSection
tabs={tabsHome({ tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt, acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id, profileId: data?.Profile?.id,
})} })}
/> />
) : (
<View style={GStyles.tabBar}>
<View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
{Array.from({ length: 4 }).map((e, index) => (
<CustomSkeleton
key={index}
height={40}
width={40}
radius={100}
/>
))}
</View>
</View>
)
} }
> >
<StackCustom> <StackCustom>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
<Home_ImageSection /> <Home_ImageSection />
<Home_FeatureSection /> {data && data ? (
<Home_FeatureSection />
) : (
<View style={stylesHome.gridContainer}>
{Array.from({ length: 4 }).map((item, index) => (
<CustomSkeleton
key={index}
style={stylesHome.gridItem}
radius={50}
/>
))}
</View>
)}
<Home_BottomFeatureSection /> {data ? (
<Home_BottomFeatureSection listData={listData} />
) : (
<CustomSkeleton height={200} />
)}
</StackCustom> </StackCustom>
</ViewWrapper> </ViewWrapper>
</> </>

View File

@@ -1,244 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */ import { Maps_ScreenMapsEdit } from "@/screens/Maps/ScreenMapsEdit";
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";
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
export default function MapsEdit() { export default function MapsEdit() {
const { id } = useLocalSearchParams(); return <Maps_ScreenMapsEdit />;
const [data, setData] = useState<any | null>({
id: "",
namePin: "",
latitude: "",
longitude: "",
imageId: "",
});
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
const [image, setImage] = useState<IFileData | null>(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 = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data.namePin}
onPress={handleSubmit}
isLoading={isLoading}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
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>
<TextInputCustom
required
label="Nama Pin"
placeholder="Masukkan nama pin maps"
value={data?.namePin}
onChangeText={(value) => setData({ ...data, namePin: value })}
/>
<Spacing />
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
<LandscapeFrameUploaded
image={
image
? image?.uri
: API_IMAGE.GET({ fileId: data?.imageId as string })
}
/>
<ButtonCenteredOnly
icon="upload"
onPress={() => {
pickFile({
allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}}
>
Upload
</ButtonCenteredOnly>
<Spacing height={50} />
</ViewWrapper>
);
} }
const styles = StyleSheet.create({
container: {
width: "100%",
backgroundColor: "#f5f5f5",
overflow: "hidden",
borderRadius: 8,
marginBottom: 20,
},
map: {
flex: 1,
},
});

View File

@@ -1,143 +1,9 @@
import { import Maps_ScreenMapsCreate from "@/screens/Maps/ScreenMapsCreate";
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";
export default function MapsCreate() { export default function MapsCreate() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
const [name, setName] = useState<string>("");
const [image, setImage] = useState<IFileData | null>(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 = (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
disabled={!selectedLocation || name === ""}
onPress={handleSubmit}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
);
return ( return (
<ViewWrapper footerComponent={buttonFooter}> <>
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." /> <Maps_ScreenMapsCreate />
</>
<BaseBox>
<MapSelected
selectedLocation={selectedLocation as any}
setSelectedLocation={setSelectedLocation}
/>
</BaseBox>
<TextInputCustom
required
label="Nama Pin"
placeholder="Masukkan nama pin maps"
value={name}
onChangeText={setName}
/>
<Spacing height={50} />
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
<LandscapeFrameUploaded image={image?.uri} />
<ButtonCenteredOnly
icon="upload"
onPress={() => {
pickFile({
allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}}
>
Upload
</ButtonCenteredOnly>
<Spacing height={50} />
</ViewWrapper>
); );
} }

View File

@@ -10,6 +10,7 @@ import {
} from "@/components"; } from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView"; import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
@@ -64,6 +65,8 @@ export default function Portofolio() {
setProfileId(response?.data?.Profile?.id); setProfileId(response?.data?.Profile?.id);
}; };
return ( return (
<> <>
{/* Header */} {/* Header */}
@@ -87,7 +90,10 @@ export default function Portofolio() {
/> />
<ViewWrapper> <ViewWrapper>
{!data || !profileId ? ( {!data || !profileId ? (
<LoaderCustom /> <StackCustom>
<CustomSkeleton height={400} />
<CustomSkeleton height={300} />
</StackCustom>
) : ( ) : (
<StackCustom> <StackCustom>
<Portofolio_Data <Portofolio_Data

View File

@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom } from "@/components"; import { NewWrapper, StackCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import DrawerCustom from "@/components/Drawer/DrawerCustom"; import DrawerCustom from "@/components/Drawer/DrawerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -16,8 +16,8 @@ import { GStyles } from "@/styles/global-styles";
import { IProfile } from "@/types/Type-Profile"; import { IProfile } from "@/types/Type-Profile";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import React, { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { TouchableOpacity } from "react-native"; import { RefreshControl, TouchableOpacity } from "react-native";
export default function Profile() { export default function Profile() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
@@ -25,6 +25,7 @@ export default function Profile() {
const [data, setData] = useState<IProfile>(); const [data, setData] = useState<IProfile>();
const [dataToken, setDataToken] = useState<IProfile>(); const [dataToken, setDataToken] = useState<IProfile>();
const [listPortofolio, setListPortofolio] = useState<any[]>(); const [listPortofolio, setListPortofolio] = useState<any[]>();
const [refreshing, setRefreshing] = useState(false);
const { token, logout, isAdmin, user, userData } = useAuth(); const { token, logout, isAdmin, user, userData } = useAuth();
@@ -43,7 +44,7 @@ export default function Profile() {
onLoadUserByToken(); onLoadUserByToken();
isUserCheck(); isUserCheck();
userData(token as string); userData(token as string);
}, [id, token]) }, [id, token]),
); );
const isUserCheck = () => { const isUserCheck = () => {
@@ -54,13 +55,21 @@ export default function Profile() {
}; };
const onLoadData = async (id: string) => { const onLoadData = async (id: string) => {
const response = await apiProfile({ id: id }); try {
setData(response.data); const response = await apiProfile({ id: id });
setData(response.data);
} catch (error) {
console.log("[ERROR onLoadData]", error);
}
}; };
const onLoadUserByToken = async () => { const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string); try {
setDataToken(response?.data?.Profile); const response = await apiUser(user?.id as string);
setDataToken(response?.data?.Profile);
} catch (error) {
console.log("[ERROR onLoadUserByToken]", error);
}
}; };
const onLoadPortofolio = async (id: string) => { const onLoadPortofolio = async (id: string) => {
@@ -69,15 +78,25 @@ export default function Profile() {
const lastTwoByDate = response.data const lastTwoByDate = response.data
.sort( .sort(
(a: any, b: any) => (a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
) // urut desc ) // urut desc
.slice(0, 2); .slice(0, 2);
setListPortofolio(lastTwoByDate); setListPortofolio(lastTwoByDate);
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR onLoadPortofolio]", error);
} }
}; };
const onRefresh = useCallback(() => {
setRefreshing(true);
onLoadData(id as string);
onLoadPortofolio(id as string);
onLoadUserByToken();
isUserCheck();
userData(token as string);
setRefreshing(false);
}, [id, token]);
return ( return (
<> <>
<Stack.Screen <Stack.Screen
@@ -97,9 +116,21 @@ export default function Profile() {
}} }}
/> />
{/* Main View */} {/* Main View */}
<ViewWrapper> <NewWrapper
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
>
{!data || !dataToken ? ( {!data || !dataToken ? (
<LoaderCustom /> <StackCustom>
<CustomSkeleton height={400} />
<CustomSkeleton height={200} />
</StackCustom>
) : ( ) : (
<> <>
<ProfileSection data={data as any} /> <ProfileSection data={data as any} />
@@ -110,7 +141,7 @@ export default function Profile() {
/> />
</> </>
)} )}
</ViewWrapper> </NewWrapper>
{/* Drawer Komponen Eksternal */} {/* Drawer Komponen Eksternal */}
<DrawerCustom <DrawerCustom

View File

@@ -9,32 +9,14 @@ import {
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView"; import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import API_IMAGE from "@/constants/api-storage"; import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiMapsGetAll } from "@/service/api-client/api-maps"; import { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps"; import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons"; import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { router, useFocusEffect } from "expo-router"; import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { View } from "react-native";
import MapView, { Marker } from "react-native-maps";
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
height: 300,
};
export interface LocationItem {
id: string | number;
latitude: number;
longitude: number;
name: string;
imageId?: string;
}
export default function AdminMaps() { export default function AdminMaps() {
const [list, setList] = useState<any[] | null>(null); const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false); const [loadList, setLoadList] = useState(false);
@@ -72,74 +54,30 @@ export default function AdminMaps() {
} }
}; };
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 (
<> <>
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}> <ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
{/* <MapCustom height={"100%"} /> */} <MapsV2Custom markers={markers} />
<View style={{ flex: 1 }}>
{loadList ? (
<MapView
initialRegion={defaultRegion}
style={{
width: "100%",
height: "100%",
}}
/>
) : (
<MapView
initialRegion={defaultRegion}
style={{
width: "100%",
height: "100%",
}}
>
{list?.map((item: any, index: number) => {
return (
<Marker
key={item?.id}
coordinate={{
latitude: item?.latitude,
longitude: item?.longitude,
}}
title={item?.namePin}
onPress={() => {
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,
});
}}
// Gunakan gambar kustom jika tersedia
>
<View>
<Image
source={{
uri: API_IMAGE.GET({
fileId: item?.Portofolio?.logoId,
}),
}}
style={{
width: 30,
height: 30,
borderRadius: 100,
borderWidth: 1,
}}
/>
</View>
</Marker>
);
})}
</MapView>
)}
</View>
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom
@@ -147,7 +85,9 @@ export default function AdminMaps() {
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
height={"auto"} height={"auto"}
> >
<DummyLandscapeImage height={200} imageId={selected.imageId} /> {selected.imageId && (
<DummyLandscapeImage height={200} imageId={selected.imageId} />
)}
<Spacing /> <Spacing />
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<GridTwoView <GridTwoView

View File

@@ -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
* <MapSelectedPlatform
* selectedLocation={selectedLocation}
* onLocationSelect={setSelectedLocation}
* height={300}
* />
* ```
*/
export function MapSelectedPlatform({
initialRegion,
selectedLocation,
onLocationSelect,
height = 400,
showUserLocation = true,
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}
// />
// );
// }
// 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 (
<MapSelectedV2
selectedLocation={androidLocation}
onLocationSelect={(location: [number, number]) => {
// 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;

View File

@@ -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<number>(0);
const cameraRef = useRef<any>(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 (
<View style={{ height, width: "100%" }}>
{/* Loading indicator saat fetch lokasi */}
{isLoadingLocation && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="small" color="#0a1f44" />
</View>
)}
<MapView
style={StyleSheet.absoluteFillObject}
mapStyle={MAP_STYLE}
onPress={handleMapPress}
logoEnabled={false}
>
<Camera
ref={cameraRef}
defaultSettings={{
centerCoordinate: initialCenter,
zoomLevel: 14,
}}
/>
{selectedLocation && (
<PointAnnotation
id="selected-location"
key="selected-location"
coordinate={selectedLocation}
>
<View style={styles.dot} />
</PointAnnotation>
)}
</MapView>
</View>
);
}
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;

View File

@@ -77,8 +77,12 @@ export default function NotificationInitializer() {
}); });
console.log("✅ Device token berhasil didaftarkan ke backend"); console.log("✅ Device token berhasil didaftarkan ke backend");
} catch (error) { } catch (error: any) {
console.error("❌ Gagal mendaftarkan device token:", error); // Log error detail tapi jangan crash aplikasi
console.error("❌ Gagal mendaftarkan device token:", error?.message);
console.error("Response status:", error?.response?.status);
console.error("Response data:", error?.response?.data);
// Skip logout - biarkan user tetap bisa pakai app meski notif gagal
} }
}; };

View File

@@ -49,7 +49,7 @@ const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
right: 0, right: 0,
height: 100, height: 100,
backgroundColor: MainColor.soft_darkblue, backgroundColor: MainColor.soft_darkblue,
borderRadius: 4, borderRadius: 1,
}} }}
/> />
</View> </View>

View File

@@ -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 Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
<!-- Create Box --> <!-- Create Box -->
File Utama: screens/Admin/Investment/ScreenInvestmentStatus.tsx File Utama: app/(application)/(user)/maps/[id]/edit.tsx
Folder tujuan: screens/Admin/Investment Folder tujuan: screens/Maps
Reffrensi: screens/Admin/Donation/BoxDonationStatus.tsx Nama file utama: ScreenMapsEdit.tsx
Buatkan box component baru pada file "File Utama" di bagian renderItem agar lebih rapi buat file baru dengan nama BoxInvestmentStatus.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"
<!-- END Create Box --> <!-- END Create Box -->

View File

@@ -163,6 +163,23 @@
5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */, 5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */,
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */, 0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */,
CCCF75FD0B87410193A6B7DB /* 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) */,
72EDC26CA2144B90BEFE947F /* Remove signature files (Xcode workaround) */,
0A09E19272A94BEBAAF5A27A /* Remove signature files (Xcode workaround) */,
9B007D2599C64C7F8F525B86 /* Remove signature files (Xcode workaround) */,
1393AE9C86924FA8B1F8D11E /* Remove signature files (Xcode workaround) */,
E1F9AE3DCABE4A088A05E180 /* Remove signature files (Xcode workaround) */,
211F6E22A1B24524B67693F8 /* Remove signature files (Xcode workaround) */,
469F2CAA8928481CA86EB0F4 /* Remove signature files (Xcode workaround) */,
0F9297956F4F4FC9881920F8 /* Remove signature files (Xcode workaround) */,
058D2457CFA64FD9AC31C74F /* Remove signature files (Xcode workaround) */,
); );
buildRules = ( buildRules = (
); );
@@ -635,6 +652,295 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; 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\";
";
};
72EDC26CA2144B90BEFE947F /* 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\";
";
};
0A09E19272A94BEBAAF5A27A /* 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\";
";
};
9B007D2599C64C7F8F525B86 /* 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\";
";
};
1393AE9C86924FA8B1F8D11E /* 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\";
";
};
E1F9AE3DCABE4A088A05E180 /* 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\";
";
};
211F6E22A1B24524B67693F8 /* 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\";
";
};
469F2CAA8928481CA86EB0F4 /* 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\";
";
};
0F9297956F4F4FC9881920F8 /* 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\";
";
};
058D2457CFA64FD9AC31C74F /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */

View File

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

View File

@@ -1,45 +1,15 @@
import { ClickableCustom, TextCustom } from "@/components"; import { ClickableCustom, TextCustom } from "@/components";
import Spacing from "@/components/_ShareComponent/Spacing"; import Spacing from "@/components/_ShareComponent/Spacing";
import React, { useCallback, useState } from "react"; import { router } from "expo-router";
import { View } from "react-native"; import { View } from "react-native";
import Icon from "react-native-vector-icons/FontAwesome"; import Icon from "react-native-vector-icons/FontAwesome";
import { stylesHome } from "./homeViewStyle"; import { stylesHome } from "./homeViewStyle";
import { router, useFocusEffect } from "expo-router";
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 onLoadData = async () => {
try {
const response = await apiJobGetAll({
category: "beranda",
});
// console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
const result = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
.slice(0, 2);
setListData(result);
} catch (error) {
console.log("[ERROR]", error);
}
};
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
if (!listData || listData.length === 0) {
return <CustomSkeleton height={200}/>
}
export default function Home_BottomFeatureSection({
listData,
}: {
listData: any[] | null;
}) {
return ( return (
<> <>
<ClickableCustom onPress={() => router.push("/job")}> <ClickableCustom onPress={() => router.push("/job")}>
@@ -54,7 +24,7 @@ export default function Home_BottomFeatureSection() {
<View style={stylesHome.vacancyList}> <View style={stylesHome.vacancyList}>
{/* Vacancy Item 1 */} {/* Vacancy Item 1 */}
{listData.map((item: any, index: number) => ( {listData?.map((item: any, index: number) => (
<View style={stylesHome.vacancyItem} key={index}> <View style={stylesHome.vacancyItem} key={index}>
<View style={stylesHome.vacancyDetails}> <View style={stylesHome.vacancyDetails}>
<TextCustom bold color="yellow" truncate size="large"> <TextCustom bold color="yellow" truncate size="large">

View File

@@ -40,8 +40,12 @@ export default function DrawerMaps({
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
height={"auto"} height={"auto"}
> >
<DummyLandscapeImage height={200} imageId={selected.imageId} /> {selected.imageId && (
<Spacing /> <>
<DummyLandscapeImage height={200} imageId={selected.imageId} />
<Spacing />
</>
)}
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<GridTwoView <GridTwoView
spanLeft={2} spanLeft={2}

View File

@@ -0,0 +1,186 @@
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
BoxButtonOnFooter,
ButtonCustom,
InformationBox,
BaseBox,
Spacing,
TextInputCustom,
LandscapeFrameUploaded,
ButtonCenteredOnly,
} from "@/components";
import { MapSelectedPlatform } from "@/components/Map/MapSelectedPlatform";
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 { IFileData } from "@/utils/pickFile";
import pickFile 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";
/**
* Screen untuk create maps
*
* Fitur:
* - Pilih lokasi dari map
* - Input nama pin
* - Upload foto lokasi
* - Submit data maps
*/
export function Maps_ScreenMapsCreate() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
// State management
// Format: LatLng { latitude, longitude } untuk konsistensi
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
const [name, setName] = useState<string>("");
const [image, setImage] = useState<IFileData | null>(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 = (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
disabled={!selectedLocation || name === ""}
onPress={handleSubmit}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
);
/**
* Render screen dengan NewWrapper
*/
return (
<NewWrapper footerComponent={buttonFooter}>
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
<BaseBox>
<MapSelectedPlatform
selectedLocation={selectedLocation}
onLocationSelect={(location) => {
setSelectedLocation(location as LatLng);
}}
height={300}
showUserLocation={true}
showsMyLocationButton={true}
/>
</BaseBox>
<TextInputCustom
required
label="Nama Pin"
placeholder="Masukkan nama pin maps"
value={name}
onChangeText={setName}
/>
<Spacing height={50} />
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
<LandscapeFrameUploaded image={image?.uri} />
<ButtonCenteredOnly icon="upload" onPress={handleImageUpload}>
Upload
</ButtonCenteredOnly>
<Spacing height={50} />
</NewWrapper>
);
}
export default Maps_ScreenMapsCreate;

View File

@@ -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<any | null>({
id: "",
namePin: "",
latitude: "",
longitude: "",
imageId: "",
});
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
const [image, setImage] = useState<IFileData | null>(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 = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data.namePin}
onPress={handleSubmit}
isLoading={isLoading}
>
Update
</ButtonCustom>
</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." />
{/* <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
label="Nama Pin"
placeholder="Masukkan nama pin maps"
value={data?.namePin}
onChangeText={(value) => setData({ ...data, namePin: value })}
/>
<Spacing />
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
<LandscapeFrameUploaded
image={
image
? image?.uri
: API_IMAGE.GET({ fileId: data?.imageId as string })
}
/>
<ButtonCenteredOnly
icon="upload"
onPress={() => {
pickFile({
allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}}
>
Upload
</ButtonCenteredOnly>
<Spacing height={50} />
</ViewWrapper>
);
}

View File

@@ -139,8 +139,8 @@ export default function UserSearchMainView_V2() {
searchQuery: search, searchQuery: search,
emptyMessage: "Tidak ada pengguna ditemukan", emptyMessage: "Tidak ada pengguna ditemukan",
emptySearchMessage: "Tidak ada hasil pencarian", emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: 5, skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 150, skeletonHeight: 100,
loadingFooterText: "Memuat lebih banyak pengguna...", loadingFooterText: "Memuat lebih banyak pengguna...",
isInitialLoad, isInitialLoad,
}); });

View File

@@ -15,13 +15,14 @@ export async function apiDeviceRegisterToken({
data: DeviceTokenData; data: DeviceTokenData;
}) { }) {
try { try {
const response = await apiConfig.post(`/mobile/auth/device-tokens`, { const response = await apiConfig.post(`/mobile/auth/device-tokens`, data);
data: data,
});
return response.data; return response.data;
} catch (error) { } catch (error: any) {
console.error("Failed to register device token:", error); console.error("Failed to register device token:", error);
console.error("Response data:", error?.response?.data);
console.error("Response status:", error?.response?.status);
console.error("Request payload:", data);
throw error; throw error;
} }
} }