Compare commits

...

9 Commits

Author SHA1 Message Date
a5026cc285 add: Admin Event detail screen dan komponen pendukung
Deskripsi:

Menambahkan halaman detail event pada admin panel dengan status parameter

Menambahkan beberapa komponen UI untuk menampilkan detail event, drawer informasi, dan QR Code

Update konfigurasi aplikasi dan iOS project

Perbaikan pada halaman verifikasi authentication

Update dokumentasi prompt untuk Qwen

File yang diubah:

Modified

app.config.js

app/(application)/admin/event/[id]/[status]/index.tsx

docs/prompt-for-qwen-code.md

ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

ios/HIPMIBadungConnect/Info.plist

screens/Authentication/VerificationView.tsx

New Admin Event Components

screens/Admin/Event/BoxEventDetail.tsx

screens/Admin/Event/EventDetailDrawer.tsx

screens/Admin/Event/EventDetailQRCode.tsx

screens/Admin/Event/ScreenEventDetail.tsx

### No Issue
2026-03-06 16:39:55 +08:00
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
67070bb2f1 Fix 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-26 18:04:45 +08:00
fb19ec60b2 Fix 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-26 17:53:23 +08:00
32 changed files with 2651 additions and 942 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
- **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
<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
- `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)

View File

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

View File

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

View File

@@ -1,66 +1,93 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
import HeaderBell from "@/screens/Home/HeaderBell";
import { stylesHome } from "@/screens/Home/homeViewStyle";
import Home_ImageSection from "@/screens/Home/imageSection";
import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList";
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
import { RefreshControl, View } from "react-native";
export default function Application() {
const { token, user, userData } = useAuth();
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore();
const [listData, setListData] = useState<any[] | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataJob();
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();
}, [user?.id, token]),
);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log(
"[Profile ID]>>",
JSON.stringify(response?.data?.Profile?.id, null, 2),
);
setData(response.data);
try {
const response = await apiUser(user?.id as string);
setData(response.data);
} catch (error: any) {
console.log("[ERROR onLoadData]", error?.message);
console.log("[ERROR Response]", error?.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 response = await apiVersion();
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
try {
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(() => {
setRefreshing(true);
onLoadData();
onLoadDataJob();
checkVersion();
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) {
console.log("User is not active");
console.warn("User is not active");
return (
<BasicWrapper>
<Redirect href={`/waiting-room`} />
@@ -69,7 +96,7 @@ export default function Application() {
}
if (data && data?.Profile === null) {
console.log("Profile is null");
console.warn("Profile is null");
return (
<BasicWrapper>
<Redirect href={`/profile/create`} />
@@ -91,17 +118,25 @@ export default function Application() {
<Stack.Screen
options={{
title: `HIPMI`,
headerLeft: () => (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
),
headerRight: () => <HeaderBell />,
headerLeft: () =>
data ? (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
headerRight: () =>
data ? (
<HeaderBell />
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
}}
/>
<ViewWrapper
@@ -114,25 +149,51 @@ export default function Application() {
/>
}
footerComponent={
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
/>
data && data ? (
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
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>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
<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>
</ViewWrapper>
</>

View File

@@ -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<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>
);
return <Maps_ScreenMapsEdit />;
}
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 {
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<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 (
<ViewWrapper footerComponent={buttonFooter}>
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
<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>
<>
<Maps_ScreenMapsCreate />
</>
);
}

View File

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

View File

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

View File

@@ -1,254 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
AlertDefaultSystem,
BadgeCustom,
BaseBox,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconDot, IconList } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import ReportBox from "@/components/Box/ReportBox";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
import { DEEP_LINK_URL } from "@/service/api-config";
import { colorBadgeStatus } from "@/utils/colorBadge";
import { dateTimeView } from "@/utils/dateTimeView";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import React, { useCallback } from "react";
import QRCode from "react-native-qrcode-svg";
import Toast from "react-native-toast-message";
import { Admin_ScreenEventDetail } from "@/screens/Admin/Event/ScreenEventDetail";
export default function AdminEventDetail() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = React.useState(false);
const [data, setData] = React.useState<any | null>(null);
const [loadData, setLoadData] = React.useState(false);
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
const isDevLink =
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminEventById({
id: id as string,
});
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
const listData = [
{
label: "Pembuat Event",
value: (data && data?.Author?.username) || "-",
},
{
label: "Judul Event",
value: (data && data?.title) || "-",
},
{
label: "Status",
value:
(data && (
<BadgeCustom color={colorBadgeStatus({ status: status as string })}>
{_.startCase(status as string)}
</BadgeCustom>
)) ||
"-",
},
{
label: "Lokasi",
value: (data && data?.lokasi) || "-",
},
{
label: "Tipe Acara",
value: (data && data?.EventMaster_TipeAcara?.name) || "-",
},
{
label: "Mulai Event",
value:
(data && data?.tanggal && dateTimeView({ date: data?.tanggal })) || "-",
},
{
label: "Event Berakhir",
value:
(data &&
data?.tanggalSelesai &&
dateTimeView({ date: data?.tanggalSelesai })) ||
"-",
},
{
label: "Deskripsi",
value: (data && data?.deskripsi) || "-",
},
];
const rightComponent = (
<ActionIcon
icon={<IconDot size={ICON_SIZE_BUTTON} />}
onPress={() => {
setOpenDrawer(true);
}}
/>
);
const handlerSubmit = async () => {
try {
const response = await funUpdateStatusEvent({
id: id as string,
changeStatus: "publish",
data: { catatan: "", senderId: user?.id as string },
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal mempublikasikan event",
});
return;
}
Toast.show({
type: "success",
text1: "Event berhasil dipublikasikan",
});
router.back();
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<>
<ViewWrapper
headerComponent={
<AdminBackButtonAntTitle
title={`Detail Data`}
rightComponent={
(status === "publish" || status === "history") && rightComponent
}
/>
}
>
<BaseBox>
<StackCustom>
{listData.map((item, i) => (
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}
/>
))}
</StackCustom>
<Spacing />
</BaseBox>
{data &&
data?.catatan &&
(status === "reject" || status === "review") && (
<ReportBox text={data?.catatan} />
)}
{(status === "publish" || status === "history") && (
<BaseBox>
<StackCustom style={{ alignItems: "center" }}>
<TextCustom bold>QR Code Event</TextCustom>
{loadData ? (
<LoaderCustom />
) : (
<QRCode
value={isDevLink}
size={200}
// logo={require("@/assets/images/logo-hipmi.png")}
// logoSize={70}
// logoBackgroundColor="transparent"
// logoBorderRadius={50}
// color="black"
/>
)}
{/* <TextCustom align="center">{isDevLink}</TextCustom> */}
</StackCustom>
</BaseBox>
)}
{status === "review" && (
<AdminButtonReview
onPublish={() => {
AlertDefaultSystem({
title: "Publish",
message: "Apakah anda yakin ingin mempublikasikan data ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => handlerSubmit(),
});
}}
onReject={() => {
router.push(`/admin/event/${id}/reject-input?status=${status}`);
}}
/>
)}
{status === "reject" && (
<AdminButtonReject
title="Tambah Catatan"
onReject={() => {
router.push(`/admin/event/${id}/reject-input?status=${status}`);
}}
/>
)}
<Spacing />
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Daftar Peserta",
icon: <IconList />,
path: `/admin/event/${id}/list-of-participants`,
},
]}
onPressItem={(item) => {
setOpenDrawer(false);
router.push(item.path as any);
}}
/>
</DrawerCustom>
</>
);
return <Admin_ScreenEventDetail />;
}

View File

@@ -9,32 +9,14 @@ import {
ViewWrapper,
} from "@/components";
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 { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { router, useFocusEffect } from "expo-router";
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() {
const [list, setList] = useState<any[] | null>(null);
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 (
<>
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
{/* <MapCustom height={"100%"} /> */}
<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>
<MapsV2Custom markers={markers} />
</ViewWrapper>
<DrawerCustom
@@ -147,7 +85,9 @@ export default function AdminMaps() {
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<DummyLandscapeImage height={200} imageId={selected.imageId} />
{selected.imageId && (
<DummyLandscapeImage height={200} imageId={selected.imageId} />
)}
<Spacing />
<StackCustom gap={"xs"}>
<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

@@ -0,0 +1,542 @@
import { ReactNode, useCallback, useMemo, useState, useEffect } from "react";
import {
Image,
StyleSheet,
View,
ViewStyle,
StyleProp,
Animated,
Easing,
} from "react-native";
import API_IMAGE from "@/constants/api-storage";
import { MainColor } from "@/constants/color-palet";
import {
Camera,
MapView,
PointAnnotation,
} from "@maplibre/maplibre-react-native";
// Style peta default
const DEFAULT_MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
// Region default (Bali, Indonesia)
const DEFAULT_REGION = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
// Zoom level default
const DEFAULT_ZOOM_LEVEL = 12;
// Ukuran marker default
const DEFAULT_MARKER_SIZE = 30;
/**
* Interface data marker untuk MapsV2Custom
*/
export interface MapMarker {
id: string;
coordinate: [number, number]; // [longitude, latitude]
imageId?: string;
imageUrl?: string;
onSelected?: () => void;
[key: string]: any; // Izinkan properti custom tambahan
}
/**
* Interface region untuk positioning kamera
*/
export interface Region {
latitude: number;
longitude: number;
latitudeDelta: number;
longitudeDelta: number;
}
/**
* Props untuk komponen MapsV2Custom
*/
export interface MapsV2CustomProps {
/** URL style peta custom (default: liberty style) */
mapStyle?: string;
/** Override style container */
style?: StyleProp<ViewStyle>;
/** Override style MapView */
mapViewStyle?: StyleProp<ViewStyle>;
/** Region awal kamera */
initialRegion?: Region;
/** Zoom level awal (default: 12) */
zoomLevel?: number;
/**
* Data marker - mendukung single marker atau array of markers
* @example
* // Single marker
* markers={{ id: "1", coordinate: [115.175, -8.737], imageId: "abc" }}
*
* @example
* // Multiple markers
* markers={[
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
* ]}
*/
markers?: MapMarker | MapMarker[];
/** Custom renderer marker */
renderMarker?: (marker: MapMarker) => ReactNode;
/** Callback ketika marker ditekan */
onMarkerPress?: (marker: MapMarker) => void;
/** Gunakan style marker image default (default: true jika markers disediakan) */
showDefaultMarkers?: boolean;
/** Ukuran untuk marker default (default: 30) */
markerSize?: number;
/** Warna border untuk marker default */
markerBorderColor?: string;
/** Children tambahan untuk MapView (custom overlays, dll.) */
children?: ReactNode;
/** Handler untuk tekan pada peta */
onMapPress?: (coordinates: [number, number]) => void;
/** Test identifier */
testID?: string;
/** Props tambahan untuk Camera */
cameraProps?: Partial<Omit<React.ComponentProps<typeof Camera>, "centerCoordinate" | "zoomLevel">>;
/** Props tambahan untuk MapView */
mapViewProps?: Partial<React.ComponentProps<typeof MapView>>;
/** Props tambahan untuk PointAnnotation */
annotationProps?: Partial<{
id: string;
title?: string;
snippet?: string;
selected?: boolean;
draggable?: boolean;
coordinate: number[];
anchor?: { x: number; y: number };
onSelected?: (payload: any) => void;
onDeselected?: (payload: any) => void;
onDragStart?: (payload: any) => void;
onDragEnd?: (payload: any) => void;
onDrag?: (payload: any) => void;
style?: StyleProp<ViewStyle>;
}>;
}
/**
* Normalisasi markers ke array - mendukung single marker atau array
*/
function normalizeMarkers(markers: MapMarker | MapMarker[] | undefined): MapMarker[] {
if (!markers) return [];
return Array.isArray(markers) ? markers : [markers];
}
/**
* Validasi marker memiliki props yang required (hanya development mode)
*/
function validateMarker(marker: MapMarker, index: number): boolean {
if (!marker.id) {
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'id' yang required`);
return false;
}
if (!marker.coordinate || !Array.isArray(marker.coordinate) || marker.coordinate.length !== 2) {
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'coordinate' yang required. Format yang diharapkan: [longitude, latitude]`);
return false;
}
return true;
}
/**
* Komponen skeleton untuk loading state dengan shimmer animation
*/
function SkeletonMarker({
size = DEFAULT_MARKER_SIZE,
borderColor = MainColor.darkblue,
loadingColor = "#C5C5C5",
}: {
size?: number;
borderColor?: string;
loadingColor?: string;
}) {
const shimmerAnim = useMemo(() => new Animated.Value(0), []);
useEffect(() => {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(shimmerAnim, {
toValue: 1,
duration: 800,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(shimmerAnim, {
toValue: 0,
duration: 800,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
])
);
animation.start();
return () => animation.stop();
}, [shimmerAnim]);
const shimmerOpacity = shimmerAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 0.7],
});
return (
<View
style={[
styles.markerContainer,
{
width: size,
height: size,
borderRadius: size / 2,
borderColor,
backgroundColor: loadingColor,
},
]}
>
<Animated.View
style={[
styles.skeletonShimmer,
{
opacity: shimmerOpacity,
backgroundColor: "#FFFFFF",
},
]}
/>
</View>
);
}
/**
* Komponen fallback untuk error state
*/
function FallbackMarker({
size = DEFAULT_MARKER_SIZE,
borderColor = MainColor.darkblue,
iconColor = MainColor.darkblue,
}: {
size?: number;
borderColor?: string;
iconColor?: string;
}) {
return (
<View
style={[
styles.markerContainer,
{
width: size,
height: size,
borderRadius: size / 2,
borderColor,
backgroundColor: "#F5F5F5",
justifyContent: "center",
alignItems: "center",
},
]}
>
<View style={[styles.fallbackIcon, { borderColor: iconColor }]}>
<View style={[styles.fallbackIconInner, { backgroundColor: iconColor }]} />
</View>
</View>
);
}
/**
* Props untuk DefaultMarker component
*/
export interface DefaultMarkerProps {
/** ID file image dari API */
imageId?: string;
/** URL image langsung */
imageUrl?: string;
/** Ukuran marker (default: 30) */
size?: number;
/** Warna border marker (default: darkblue) */
borderColor?: string;
/** Warna skeleton loading (default: gray) */
loadingColor?: string;
/** Warna icon fallback (default: darkblue) */
fallbackIconColor?: string;
}
/**
* Komponen marker default dengan image, border, shadows, loading skeleton, dan error fallback
*/
export function DefaultMarker({
imageId,
imageUrl,
size = DEFAULT_MARKER_SIZE,
borderColor = MainColor.darkblue,
loadingColor = MainColor.white_gray,
fallbackIconColor = MainColor.darkblue,
}: DefaultMarkerProps) {
const [hasError, setHasError] = useState(false);
const uri = imageId ? API_IMAGE.GET({ fileId: imageId }) : imageUrl;
// Debug log untuk development
if (__DEV__ && uri) {
console.log("[DefaultMarker] Image URI:", uri);
}
const handleError = useCallback((error: any) => {
console.log("[DefaultMarker] Image error:", error?.nativeEvent?.error || error);
setHasError(true);
}, []);
const handleLoad = useCallback(() => {
console.log("[DefaultMarker] Image loaded successfully");
}, []);
// Jika tidak ada URI atau error, tampilkan fallback
if (!uri || hasError) {
return (
<FallbackMarker
size={size}
borderColor={borderColor}
iconColor={fallbackIconColor}
/>
);
}
// Render image dengan placeholder (defaultSource) untuk loading state
return (
<View
style={[
styles.markerContainer,
{
width: size,
height: size,
borderRadius: size / 2,
borderColor,
backgroundColor: loadingColor, // Background color sebagai placeholder
},
]}
>
<Image
source={{ uri }}
style={[styles.markerImage, { width: size, height: size }]}
resizeMode="cover"
onLoad={handleLoad}
onError={handleError}
// Placeholder untuk Android saat loading
defaultSource={require("@/assets/images/icon.png")}
/>
</View>
);
}
/**
* Komponen Map yang reusable dan customizable menggunakan Mapbox/MapLibre
*
* Mendukung single marker, multiple markers, atau empty state.
*
* @example
* // Single marker
* <MapsV2Custom
* markers={{
* id: "1",
* coordinate: [115.1756897, -8.737109],
* imageId: "file-123"
* }}
* />
*
* @example
* // Multiple markers
* <MapsV2Custom
* markers={[
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
* ]}
* />
*
* @example
* // Peta dengan custom style dan custom markers
* <MapsV2Custom
* mapStyle="https://your-custom-style.com"
* markers={markers}
* markerSize={40}
* markerBorderColor={MainColor.primary}
* />
*
* @example
* // Dengan custom marker renderer
* <MapsV2Custom
* markers={data}
* renderMarker={(marker) => <CustomMarker {...marker} />}
* />
*
* @example
* // Peta kosong (tanpa markers)
* <MapsV2Custom
* initialRegion={{ latitude: -6.2, longitude: 106.8, latitudeDelta: 0.1, longitudeDelta: 0.1 }}
* />
*/
export function MapsV2Custom({
mapStyle = DEFAULT_MAP_STYLE,
style = styles.container,
mapViewStyle = styles.map,
initialRegion = DEFAULT_REGION,
zoomLevel = DEFAULT_ZOOM_LEVEL,
markers,
renderMarker,
onMarkerPress,
showDefaultMarkers = true,
markerSize = DEFAULT_MARKER_SIZE,
markerBorderColor = MainColor.darkblue,
children,
onMapPress,
testID,
cameraProps,
mapViewProps,
annotationProps,
}: MapsV2CustomProps) {
// Normalisasi markers ke array (mendukung single atau multiple)
const normalizedMarkers = useMemo(
() => {
const arr = normalizeMarkers(markers);
// Filter marker yang invalid
return arr.filter((marker) => {
if (!marker.id) {
console.warn("[MapsV2Custom] Marker tanpa id akan diabaikan");
return false;
}
if (!marker.coordinate || marker.coordinate.length !== 2) {
console.warn("[MapsV2Custom] Marker tanpa coordinate valid akan diabaikan");
return false;
}
return true;
});
},
[markers]
);
// Validasi markers dalam development mode
useMemo(() => {
if (__DEV__) {
normalizedMarkers.forEach((marker, index) => {
validateMarker(marker, index);
});
}
}, [normalizedMarkers]);
const handleMarkerSelected = useCallback(
(marker: MapMarker) => {
if (marker.onSelected) {
marker.onSelected();
}
if (onMarkerPress) {
onMarkerPress(marker);
}
},
[onMarkerPress]
);
const renderMarkerComponent = useCallback(
(marker: MapMarker): ReactNode => {
if (renderMarker) {
return renderMarker(marker);
}
if (showDefaultMarkers) {
return (
<DefaultMarker
imageId={marker.imageId}
imageUrl={marker.imageUrl}
size={markerSize}
borderColor={markerBorderColor}
/>
);
}
return null;
},
[renderMarker, showDefaultMarkers, markerSize, markerBorderColor]
);
return (
<View style={style} testID={testID}>
<MapView style={mapViewStyle} mapStyle={mapStyle} {...mapViewProps}>
<Camera
zoomLevel={zoomLevel}
centerCoordinate={[initialRegion.longitude, initialRegion.latitude]}
{...cameraProps}
/>
{normalizedMarkers.map((marker) => (
<PointAnnotation
key={marker.id}
id={marker.id}
coordinate={marker.coordinate}
onSelected={() => handleMarkerSelected(marker)}
{...annotationProps}
>
{renderMarkerComponent(marker) as any}
</PointAnnotation>
))}
{children}
</MapView>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
markerContainer: {
overflow: "hidden",
borderWidth: 1,
elevation: 4,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
},
markerImage: {
width: "100%",
height: "100%",
},
skeletonShimmer: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 999,
},
fallbackIcon: {
width: "60%",
height: "60%",
borderRadius: 999,
borderWidth: 2,
justifyContent: "center",
alignItems: "center",
},
fallbackIconInner: {
width: "40%",
height: "40%",
borderRadius: 999,
},
});

View File

@@ -0,0 +1,272 @@
import React, { useState, useCallback, useRef } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
} from "react-native";
import {
MapView,
Camera,
PointAnnotation,
MarkerView,
} from "@maplibre/maplibre-react-native";
import * as Location from "expo-location";
import { useFocusEffect, useRouter } from "expo-router";
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
type Coordinate = {
latitude: number;
longitude: number;
};
export default function SelectLocationMap() {
const router = useRouter();
const annotationRef = useRef<any>(null);
const [selectedCoord, setSelectedCoord] = useState<Coordinate | null>(null);
const [address, setAddress] = useState<string>("");
const [isLoadingAddress, setIsLoadingAddress] = useState(false);
const [cameraCenter, setCameraCenter] = useState<[number, number]>([
106.8272, -6.1751,
]);
const reverseGeocode = async (coord: Coordinate): Promise<string> => {
try {
const { status } = await Location.getForegroundPermissionsAsync();
if (status !== "granted") {
await Location.requestForegroundPermissionsAsync();
}
const results = await Location.reverseGeocodeAsync({
latitude: coord.latitude,
longitude: coord.longitude,
});
if (!results || results.length === 0) return "Alamat tidak ditemukan";
const loc = results[0];
const parts = [
loc.street,
loc.district,
loc.subregion,
loc.city,
loc.region,
loc.country,
].filter(Boolean);
return parts.length > 0 ? parts.join(", ") : "Alamat tidak ditemukan";
} catch (error: any) {
console.log("reverseGeocode error:", error?.message || error);
return "Gagal mengambil alamat";
}
};
const handleMapPress = useCallback(async (event: any) => {
try {
const coordinates = event?.geometry?.coordinates;
if (!coordinates) return;
const [longitude, latitude] = coordinates;
if (!longitude || !latitude) return;
const coord: Coordinate = { latitude, longitude };
// ✅ Update state koordinat, BUKAN ganti key
setSelectedCoord(coord);
setCameraCenter([longitude, latitude]);
setAddress("");
setIsLoadingAddress(true);
const resolvedAddress = await reverseGeocode(coord);
setAddress(resolvedAddress);
setIsLoadingAddress(false);
// ✅ Refresh annotation tanpa unmount
annotationRef.current?.refresh?.();
} catch (error: any) {
console.log("handleMapPress error:", error?.message || error);
setIsLoadingAddress(false);
}
}, []);
const handleConfirm = () => {
if (!selectedCoord) return;
router.navigate({
pathname: "/maps/create",
params: {
latitude: String(selectedCoord.latitude),
longitude: String(selectedCoord.longitude),
address,
},
});
};
// Sembunyikan marker sebelum halaman unmount
useFocusEffect(
useCallback(() => {
return () => {
// Cleanup saat leave — sembunyikan marker dulu sebelum unmount
setSelectedCoord(null);
};
}, []),
);
return (
<View style={styles.container}>
<MapView style={styles.map} mapStyle={MAP_STYLE} onPress={handleMapPress}>
<Camera
zoomLevel={14}
centerCoordinate={cameraCenter}
animationMode="flyTo"
animationDuration={300}
/>
{/* ✅ Key statis — tidak pernah berubah, tidak unmount/remount */}
{selectedCoord && (
<MarkerView
id="selected-marker"
coordinate={[selectedCoord.longitude, selectedCoord.latitude]}
anchor={{ x: 0.5, y: 1 }} // Anchor bawah tengah
>
<View style={styles.pin}>
<View style={styles.pinDot} />
</View>
</MarkerView>
)}
</MapView>
{/* Bottom Sheet */}
<View style={styles.bottomSheet}>
{!selectedCoord ? (
<Text style={styles.hintText}>
Tap pada peta untuk memilih lokasi
</Text>
) : (
<>
<View style={styles.coordRow}>
<View style={styles.coordItem}>
<Text style={styles.coordLabel}>Latitude</Text>
<Text style={styles.coordValue}>
{selectedCoord.latitude.toFixed(6)}
</Text>
</View>
<View style={styles.dividerVertical} />
<View style={styles.coordItem}>
<Text style={styles.coordLabel}>Longitude</Text>
<Text style={styles.coordValue}>
{selectedCoord.longitude.toFixed(6)}
</Text>
</View>
</View>
<View style={styles.addressContainer}>
<Text style={styles.coordLabel}>Alamat</Text>
{isLoadingAddress ? (
<ActivityIndicator size="small" color="#0a1f44" />
) : (
<Text style={styles.addressText} numberOfLines={2}>
{address || "-"}
</Text>
)}
</View>
<TouchableOpacity
style={[
styles.confirmButton,
isLoadingAddress && styles.confirmButtonDisabled,
]}
onPress={handleConfirm}
disabled={isLoadingAddress}
>
<Text style={styles.confirmButtonText}>Konfirmasi Lokasi</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
pin: {
width: 28,
height: 28,
borderRadius: 100,
backgroundColor: "#0a1f44",
justifyContent: "center",
alignItems: "center",
borderWidth: 2,
borderColor: "#fff",
},
pinDot: {
width: 8,
height: 8,
borderRadius: 100,
backgroundColor: "#fff",
},
bottomSheet: {
backgroundColor: "#fff",
paddingHorizontal: 20,
paddingVertical: 20,
paddingBottom: 32,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
elevation: 10,
shadowColor: "#000",
shadowOffset: { width: 0, height: -3 },
shadowOpacity: 0.1,
shadowRadius: 6,
minHeight: 140,
justifyContent: "center",
},
hintText: {
textAlign: "center",
color: "#888",
fontSize: 14,
},
coordRow: {
flexDirection: "row",
marginBottom: 12,
},
coordItem: { flex: 1 },
dividerVertical: {
width: 1,
backgroundColor: "#e0e0e0",
marginHorizontal: 12,
},
coordLabel: {
fontSize: 11,
color: "#888",
marginBottom: 2,
},
coordValue: {
fontSize: 14,
fontWeight: "600",
color: "#0a1f44",
},
addressContainer: { marginBottom: 16 },
addressText: {
fontSize: 13,
color: "#333",
lineHeight: 18,
},
confirmButton: {
backgroundColor: "#0a1f44",
borderRadius: 12,
paddingVertical: 14,
alignItems: "center",
},
confirmButtonDisabled: {
backgroundColor: "#aaa",
},
confirmButtonText: {
color: "#fff",
fontWeight: "700",
fontSize: 15,
},
});

View File

@@ -77,8 +77,12 @@ export default function NotificationInitializer() {
});
console.log("✅ Device token berhasil didaftarkan ke backend");
} catch (error) {
console.error("❌ Gagal mendaftarkan device token:", error);
} catch (error: any) {
// 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,
height: 100,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 4,
borderRadius: 1,
}}
/>
</View>

View File

@@ -55,10 +55,10 @@ Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
<!-- START Prompt Admin Refactoring -->
<!-- Pindah kode ke Screen Component -->
File source: app/(application)/admin/forum/[id]/list-comment.tsx
Folder tujuan: screens/Admin/Forum
Nama file utama: ScreenForumListComment.tsx
Nama function utama: Admin_ScreenForumListComment
File source: app/(application)/admin/event/[id]/[status]/index.tsx
Folder tujuan: screens/Admin/Event
Nama file utama: ScreenEventDetail.tsx
Nama function utama: Admin_ScreenEventDetail
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Buat 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"
@@ -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
<!-- Create Box -->
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"
<!-- END Create Box -->

View File

@@ -155,6 +155,34 @@
B6436A881D9B484CB6D18085 /* Remove signature files (Xcode workaround) */,
2BAE9DA9D4244A23B39651C7 /* Remove signature files (Xcode workaround) */,
81D5244C04C44C06AD9AE152 /* Remove signature files (Xcode workaround) */,
806E7861EC0045A8A1F95D2D /* Remove signature files (Xcode workaround) */,
4B22761083094691A59ABFF6 /* Remove signature files (Xcode workaround) */,
B68271891C364FB0B1E59DC4 /* Remove signature files (Xcode workaround) */,
A38A95E05F7448A8922DE72A /* Remove signature files (Xcode workaround) */,
6692ADBCD8384D8EA2D9A355 /* Remove signature files (Xcode workaround) */,
5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */,
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */,
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */,
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) */,
FB0CB57BF4D74C1D87C2036C /* Remove signature files (Xcode workaround) */,
14B3DE54EE4049AEB1EADA6B /* Remove signature files (Xcode workaround) */,
B4CF5E09DBB44A4FB9CB91B9 /* Remove signature files (Xcode workaround) */,
);
buildRules = (
);
@@ -491,6 +519,482 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
806E7861EC0045A8A1F95D2D /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
4B22761083094691A59ABFF6 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
B68271891C364FB0B1E59DC4 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
A38A95E05F7448A8922DE72A /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
6692ADBCD8384D8EA2D9A355 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
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\";
";
};
FB0CB57BF4D74C1D87C2036C /* 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\";
";
};
14B3DE54EE4049AEB1EADA6B /* 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\";
";
};
B4CF5E09DBB44A4FB9CB91B9 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

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

View File

@@ -0,0 +1,85 @@
import { BadgeCustom, BaseBox, Spacing, StackCustom, TextCustom } from "@/components";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { colorBadgeStatus } from "@/utils/colorBadge";
import { dateTimeView } from "@/utils/dateTimeView";
import _ from "lodash";
interface EventDetailData {
Author?: {
username?: string;
};
title?: string;
lokasi?: string;
EventMaster_TipeAcara?: {
name?: string;
};
tanggal?: string;
tanggalSelesai?: string;
deskripsi?: string;
catatan?: string;
}
interface BoxEventDetailProps {
data: EventDetailData | null;
status: string;
}
export function BoxEventDetail({ data, status }: BoxEventDetailProps) {
const listData = [
{
label: "Pembuat Event",
value: data?.Author?.username || "-",
},
{
label: "Judul Event",
value: data?.title || "-",
},
{
label: "Status",
value: data ? (
<BadgeCustom color={colorBadgeStatus({ status })}>
{_.startCase(status)}
</BadgeCustom>
) : (
"-"
),
},
{
label: "Lokasi",
value: data?.lokasi || "-",
},
{
label: "Tipe Acara",
value: data?.EventMaster_TipeAcara?.name || "-",
},
{
label: "Mulai Event",
value: data?.tanggal ? dateTimeView({ date: data.tanggal }) : "-",
},
{
label: "Event Berakhir",
value: data?.tanggalSelesai
? dateTimeView({ date: data.tanggalSelesai })
: "-",
},
{
label: "Deskripsi",
value: data?.deskripsi || "-",
},
];
return (
<BaseBox>
<StackCustom>
{listData.map((item, i) => (
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}
/>
))}
</StackCustom>
<Spacing />
</BaseBox>
);
}

View File

@@ -0,0 +1,37 @@
import { DrawerCustom, MenuDrawerDynamicGrid } from "@/components";
import { IconList } from "@/components/_Icon/IconComponent";
import { router } from "expo-router";
interface EventDetailDrawerProps {
isVisible: boolean;
onClose: () => void;
eventId: string;
}
export function EventDetailDrawer({
isVisible,
onClose,
eventId,
}: EventDetailDrawerProps) {
return (
<DrawerCustom
isVisible={isVisible}
closeDrawer={onClose}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Daftar Peserta",
icon: <IconList />,
path: `/admin/event/${eventId}/list-of-participants`,
},
]}
onPressItem={(item) => {
onClose();
router.push(item.path as any);
}}
/>
</DrawerCustom>
);
}

View File

@@ -0,0 +1,26 @@
import { BaseBox, LoaderCustom, Spacing, StackCustom, TextCustom } from "@/components";
import QRCode from "react-native-qrcode-svg";
interface EventDetailQRCodeProps {
qrValue: string;
isLoading: boolean;
}
export function EventDetailQRCode({ qrValue, isLoading }: EventDetailQRCodeProps) {
return (
<BaseBox>
<StackCustom style={{ alignItems: "center" }}>
<TextCustom bold>QR Code Event</TextCustom>
{isLoading ? (
<LoaderCustom />
) : (
<QRCode
value={qrValue}
size={200}
/>
)}
</StackCustom>
<Spacing />
</BaseBox>
);
}

View File

@@ -0,0 +1,163 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ActionIcon, AlertDefaultSystem } from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import ReportBox from "@/components/Box/ReportBox";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
import { DEEP_LINK_URL } from "@/service/api-config";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import Toast from "react-native-toast-message";
import { BoxEventDetail } from "./BoxEventDetail";
import { EventDetailDrawer } from "./EventDetailDrawer";
import { EventDetailQRCode } from "./EventDetailQRCode";
export function Admin_ScreenEventDetail() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any | null>(null);
const [loadData, setLoadData] = useState(false);
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
const isDevLink =
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id]),
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminEventById({
id: id as string,
});
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
const rightComponent = (
<ActionIcon
icon={<IconDot size={ICON_SIZE_BUTTON} />}
onPress={() => {
setOpenDrawer(true);
}}
/>
);
const handlerSubmit = async () => {
try {
const response = await funUpdateStatusEvent({
id: id as string,
changeStatus: "publish",
data: { catatan: "", senderId: user?.id as string },
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal mempublikasikan event",
});
return;
}
Toast.show({
type: "success",
text1: "Event berhasil dipublikasikan",
});
router.back();
} catch (error) {
console.log("[ERROR]", error);
}
};
const headerComponent = useMemo(
() => (
<AdminBackButtonAntTitle
title={`Detail Data`}
rightComponent={
status === "publish" || status === "history"
? rightComponent
: undefined
}
/>
),
[status],
);
const footerComponent = useMemo(() => {
if (status === "review") {
return (
<AdminButtonReview
onPublish={() => {
AlertDefaultSystem({
title: "Publish",
message: "Apakah anda yakin ingin mempublikasikan data ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => handlerSubmit(),
});
}}
onReject={() => {
router.push(`/admin/event/${id}/reject-input?status=${status}`);
}}
/>
);
}
if (status === "reject") {
return (
<AdminButtonReject
title="Tambah Catatan"
onReject={() => {
router.push(`/admin/event/${id}/reject-input?status=${status}`);
}}
/>
);
}
return null;
}, [status, id]);
return (
<>
<NewWrapper
headerComponent={headerComponent}
footerComponent={footerComponent}
>
<BoxEventDetail data={data} status={status as string} />
{data?.catatan && (status === "reject" || status === "review") && (
<ReportBox text={data.catatan} />
)}
{(status === "publish" || status === "history") && (
<EventDetailQRCode qrValue={isDevLink} isLoading={loadData} />
)}
</NewWrapper>
<EventDetailDrawer
isVisible={openDrawer}
onClose={() => setOpenDrawer(false)}
eventId={id as string}
/>
</>
);
}

View File

@@ -5,7 +5,6 @@ import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiCheckCodeOtp } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { registerForPushNotificationsAsync } from "@/utils/notifications";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
@@ -17,8 +16,6 @@ import Toast from "react-native-toast-message";
export default function VerificationView() {
const { nomor } = useLocalSearchParams<{ nomor: string }>();
console.log("[NOMOR]", nomor);
const [inputOtp, setInputOtp] = useState<string>("");
const [userNumber, setUserNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);

View File

@@ -1,45 +1,15 @@
import { ClickableCustom, TextCustom } from "@/components";
import Spacing from "@/components/_ShareComponent/Spacing";
import React, { useCallback, useState } from "react";
import { router } from "expo-router";
import { View } from "react-native";
import Icon from "react-native-vector-icons/FontAwesome";
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 (
<>
<ClickableCustom onPress={() => router.push("/job")}>
@@ -54,7 +24,7 @@ export default function Home_BottomFeatureSection() {
<View style={stylesHome.vacancyList}>
{/* Vacancy Item 1 */}
{listData.map((item: any, index: number) => (
{listData?.map((item: any, index: number) => (
<View style={stylesHome.vacancyItem} key={index}>
<View style={stylesHome.vacancyDetails}>
<TextCustom bold color="yellow" truncate size="large">

View File

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

View File

@@ -1,19 +1,13 @@
import { useCallback, useState } from "react";
import { Image, StyleSheet, View } from "react-native";
// Cek versi >= 10.x gunakan ini
import API_IMAGE from "@/constants/api-storage";
import { MainColor } from "@/constants/color-palet";
import { apiMapsGetAll } from "@/service/api-client/api-maps";
import {
Camera,
MapView,
PointAnnotation,
} from "@maplibre/maplibre-react-native";
import { useFocusEffect } from "expo-router";
import DrawerMaps from "./DrawerMaps";
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
import { ViewWrapper } from "@/components";
import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
import DrawerMaps from "./DrawerMaps";
import { StyleSheet } from "react-native";
interface TypeMaps {
id: string;
@@ -40,14 +34,6 @@ interface TypeMaps {
};
}
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
height: 300,
};
export default function MapsView2() {
const [list, setList] = useState<TypeMaps[] | null>(null);
const [loadList, setLoadList] = useState(false);
@@ -76,7 +62,6 @@ export default function MapsView2() {
const response = await apiMapsGetAll();
if (response.success) {
// console.log("[RESPONSE]", JSON.stringify(response.data, null, 2));
setList(response.data);
}
} catch (error) {
@@ -86,53 +71,31 @@ export default function MapsView2() {
}
};
const markers: MapMarker[] = list?.map((item) => ({
id: item.id,
coordinate: [item.longitude, item.latitude] as [number, number],
imageId: item.Portofolio.logoId,
onSelected: () => {
setOpenDrawer(true);
setSelected({
id: item.id,
bidangBisnis: item.Portofolio.MasterBidangBisnis.name,
nomorTelepon: item.Portofolio.tlpn,
alamatBisnis: item.Portofolio.alamatKantor,
namePin: item.namePin,
imageId: item.imageId,
portofolioId: item.Portofolio.id,
latitude: item.latitude,
longitude: item.longitude,
});
},
})) || [];
return (
<>
<View style={styles.container}>
<MapView style={styles.map} mapStyle={MAP_STYLE}>
<Camera
zoomLevel={12}
centerCoordinate={[defaultRegion.longitude, defaultRegion.latitude]}
/>
{list?.map((item: TypeMaps) => {
const imageUrl = API_IMAGE.GET({ fileId: item.Portofolio.logoId });
return (
<PointAnnotation
key={item.id}
id={item.id}
coordinate={[item.longitude, item.latitude] as [number, number]}
onSelected={() => {
setOpenDrawer(true);
setSelected({
id: item?.id,
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
nomorTelepon: item?.Portofolio?.tlpn,
alamatBisnis: item?.Portofolio?.alamatKantor,
namePin: item?.namePin,
imageId: item?.imageId,
portofolioId: item?.Portofolio?.id,
latitude: item?.latitude,
longitude: item?.longitude,
});
}}
>
<View style={styles.markerContainer}>
<Image
source={{ uri: imageUrl }}
style={styles.markerImage}
resizeMode="cover"
onError={(e: any) =>
console.log("Image error:", e.nativeEvent.error)
} // Tangkap error image
/>
</View>
</PointAnnotation>
);
})}
</MapView>
</View>
<ViewWrapper>
<MapsV2Custom markers={markers} />
</ViewWrapper>
<DrawerMaps
openDrawer={openDrawer}
@@ -144,23 +107,9 @@ export default function MapsView2() {
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
markerContainer: {
width: 30,
height: 30,
borderRadius: 100,
overflow: "hidden", // Wajib agar borderRadius terapply pada Image
borderWidth: 1,
borderColor: MainColor.darkblue, // Opsional, biar lebih cantik
elevation: 4, // Shadow Android
shadowColor: "#000", // Shadow iOS
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 3,
map: {
flex: 1,
width: "50%",
maxHeight: "50%",
},
markerImage: {
width: "100%",
height: "100%",
},
});
});

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

@@ -1,19 +1,42 @@
import {
BaseBox,
MapCustom,
StackCustom,
TextCustom
} from "@/components";
import { StyleSheet, View } from "react-native";
import { BaseBox, StackCustom, TextCustom } from "@/components";
import { MapsV2Custom } from "@/components/Map/MapsV2Custom";
export default function Portofolio_BusinessLocation({
data,
imageId,
setOpenDrawerLocation,
}: {
data: any;
data: {
id: string;
imageId: string;
latitude: number;
longitude: number;
namePin: string;
pinId: string;
} | null;
imageId?: string;
setOpenDrawerLocation: (value: boolean) => void;
}) {
console.log("data", data);
// Buat marker hanya jika data lengkap
const markers =
data?.latitude && data?.longitude
? [
{
id: data.id || "location-marker",
coordinate: [data.longitude, data.latitude] as [number, number],
imageId,
onSelected: () => {
setOpenDrawerLocation(true);
},
},
]
: [];
return (
<>
<BaseBox style={{ height: !data ? 200 : "auto" }}>
@@ -30,18 +53,33 @@ export default function Portofolio_BusinessLocation({
Lokasi bisnis belum ditambahkan
</TextCustom>
) : (
<MapCustom
latitude={data?.latitude}
longitude={data?.longitude}
namePin={data?.namePin}
imageId={imageId}
onPress={() => {
setOpenDrawerLocation(true);
}}
/>
<View style={styles.mapContainer}>
<MapsV2Custom
markers={markers}
zoomLevel={15}
showDefaultMarkers={true}
markerSize={35}
initialRegion={{
latitude: data?.latitude,
longitude: data?.longitude,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
}}
/>
</View>
)}
</StackCustom>
</BaseBox>
</>
);
}
const styles = StyleSheet.create({
mapContainer: {
width: "100%",
height: 250,
borderRadius: 8,
overflow: "hidden",
marginTop: 8,
},
});

View File

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

View File

@@ -15,13 +15,14 @@ export async function apiDeviceRegisterToken({
data: DeviceTokenData;
}) {
try {
const response = await apiConfig.post(`/mobile/auth/device-tokens`, {
data: data,
});
const response = await apiConfig.post(`/mobile/auth/device-tokens`, data);
return response.data;
} catch (error) {
} catch (error: any) {
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;
}
}