Compare commits

...

33 Commits

Author SHA1 Message Date
48196cd46b Fix Load data pada halaman yang membutuhkan infinite load
Job – User App
- app/(application)/(user)/job/(tabs)/index.tsx
- app/(application)/(user)/job/(tabs)/status.tsx
- app/(application)/(user)/job/(tabs)/archive.tsx
- app/(application)/(user)/job/create.tsx

Job – Screens
- screens/Job/ScreenBeranda.tsx
- screens/Job/ScreenBeranda2.tsx
- screens/Job/MainViewStatus.tsx
- screens/Job/MainViewStatus2.tsx
- screens/Job/ScreenArchive.tsx
- screens/Job/ScreenArchive2.tsx

API – Job (Client)
- service/api-client/api-job.ts

Notification
- screens/Notification/ScreenNotification.tsx

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

### No Issue
2026-02-02 17:09:58 +08:00
ec79a1fbcd Fix semua tampilan yang memiliki fungsi infitine load
UI – User Notifications
- app/(application)/(user)/notifications/index.tsx
- service/api-notifications.ts
- screens/Notification/

UI – Portofolio (User)
- app/(application)/(user)/portofolio/[id]/create.tsx
- app/(application)/(user)/portofolio/[id]/edit.tsx
- app/(application)/(user)/portofolio/[id]/list.tsx
- screens/Portofolio/BoxPortofolioView.tsx
- screens/Portofolio/ViewListPortofolio.tsx
- screens/Profile/PortofolioSection.tsx
- service/api-client/api-portofolio.ts

Forum & User Search
- screens/Forum/DetailForum2.tsx
- screens/Forum/ViewBeranda3.tsx
- screens/UserSeach/MainView_V2.tsx

Constants & Docs
- constants/constans-value.ts
- docs/prompt-for-qwen-code.md

### No Issue
2026-01-30 17:18:47 +08:00
ed16f1b204 Fix forum detail
Forum (User)
- app/(application)/(user)/forum/[id]/index.tsx
- screens/Forum/ViewForumku2.tsx
- service/api-client/api-forum.ts

Forum – New / Refactor
- screens/Forum/DetailForum.tsx
- screens/Forum/DetailForum2.tsx

Documentation
- docs/

Removed
- hipmi-note.md

### No Issue'
2026-01-29 17:36:17 +08:00
d693550a1f Fix Alur Login & Load data forum , user search
Admin – User Access
- app/(application)/admin/user-access/[id]/index.tsx

Authentication
- context/AuthContext.tsx
- screens/Authentication/EULASection.tsx
- screens/Authentication/LoginView.tsx

Forum
- screens/Forum/ViewBeranda3.tsx

Profile & UI Components
- components/Image/AvatarComp.tsx
- screens/Profile/AvatarAndBackground.tsx

### No Issue
2026-01-29 15:08:00 +08:00
b3bfbc0f7e Fix Infinite Load Data
Forum & User Search – User
- app/(application)/(user)/forum/index.tsx
- app/(application)/(user)/user-search/index.tsx

Global & Core
- app/+not-found.tsx
- screens/RootLayout/AppRoot.tsx
- constants/constans-value.ts

Component
- components/Image/AvatarComp.tsx

API Client
- service/api-client/api-user.ts

Untracked Files
- QWEN.md
- helpers/
- hooks/use-pagination.tsx
- screens/Forum/ViewBeranda3.tsx
- screens/UserSeach/

### No Issue
2026-01-29 11:36:24 +08:00
71e45d06cc Donation – User
- app/(application)/(user)/donation/(tabs)/index.tsx
- app/(application)/(user)/donation/(tabs)/my-donation.tsx
- app/(application)/(user)/donation/[id]/(transaction-flow)/index.tsx

Donation – Admin
- app/(application)/admin/donation/[id]/disbursement-of-funds.tsx

Image Preview
- app/(application)/(image)/preview-image/[id]/index.tsx

### No Issue
2026-01-27 17:42:14 +08:00
07e64c335e Donation – App & Admin
- app/(application)/(user)/donation/(tabs)/status.tsx
- app/(application)/(user)/donation/create-story.tsx
- app/(application)/admin/donation/[id]/[status]/transaction-detail.tsx
- app/(application)/admin/donation/[id]/reject-input.tsx
- screens/Admin/Donation/funDonationUpdateStatus.ts
- service/api-admin/api-admin-donation.ts

Config
- app.config.js
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-01-23 17:13:06 +08:00
1aebc9b4e8 Logika EULA
Dipindah ke halaman login dan cek dengan modal

Fix:
Authentication & EULA
- context/AuthContext.tsx
- screens/Authentication/EULAView.tsx
- screens/Authentication/LoginView.tsx

Add:
- components/Modal/ModalReactNative.tsx
- screens/Authentication/EULASection.tsx

### No Issue
2026-01-23 14:45:44 +08:00
5665dc88ba Notification investasi done
### No Isssue
2026-01-22 17:54:33 +08:00
da82a02a45 Investment UI (Admin & User)
- app/(application)/(user)/investment/[id]/(transaction-flow)/invoice.tsx
- app/(application)/admin/investment/[id]/[status]/index.tsx
- app/(application)/admin/investment/[id]/[status]/transaction-detail.tsx
- app/(application)/admin/investment/[status]/status.tsx

API Service (Admin)
- service/api-admin/api-admin-investment.ts

### No issue
2026-01-21 15:39:08 +08:00
14c0f0e499 User – Investment
app/(application)/(user)/investment/(tabs)/_layout.tsx

app/(application)/(user)/investment/(tabs)/portofolio.tsx

app/(application)/(user)/investment/create.tsx

Admin – Investment

app/(application)/admin/investment/[id]/reject-input.tsx

Screens / UI

screens/Invesment/BoxBerandaSection.tsx

API

service/api-admin/api-admin-investment.ts

Utils

utils/pickFile.ts

### No Issue
2026-01-20 17:41:02 +08:00
0262423c50 Fix notification to report comment
Fix:
User – Forum (Reporting & Preview)

app/(application)/(user)/forum/[id]/other-report-commentar.tsx

app/(application)/(user)/forum/[id]/other-report-posting.tsx

app/(application)/(user)/forum/[id]/preview-report-posting.tsx

app/(application)/(user)/forum/[id]/report-commentar.tsx

app/(application)/(user)/forum/[id]/report-posting.tsx

Admin – Forum Moderation

app/(application)/admin/forum/[id]/list-report-comment.tsx

app/(application)/admin/forum/report-posting.tsx

Layout

app/(application)/(user)/_layout.tsx

API Client & Admin

service/api-admin/api-admin-forum.ts

service/api-client/api-forum.ts

service/api-client/api-master.ts

Utils

utils/badWordsIndonesia.ts

### No Issue
2026-01-19 17:46:54 +08:00
c2682246d6 Fix notifikasi forum
FixL
 modified:   app/(application)/(user)/_layout.tsx
        modified:   app/(application)/(user)/forum/[id]/index.tsx
        modified:   app/(application)/(user)/forum/create.tsx
        modified:   app/(application)/admin/forum/[id]/list-report-posting.tsx
        modified:   screens/Home/tabsList.ts
        modified:   service/api-admin/api-admin-forum.ts
        modified:   service/api-client/api-forum.ts

Add:
 app/(application)/(user)/forum/[id]/preview-report-posting.tsx
        types/type-forum.ts

### No Issue
2026-01-17 15:59:00 +08:00
465e01015e Penambahan notifikasi untuk fitur voting
Fix:
- app/(application)/(user)/event/[id]/publish.tsx
- app/(application)/(user)/voting/(tabs)/_layout.tsx
- app/(application)/(user)/voting/(tabs)/status.tsx
- app/(application)/(user)/voting/[id]/index.tsx
- app/(application)/(user)/voting/create.tsx
- app/(application)/admin/voting/[id]/[status]/index.tsx
- app/(application)/admin/voting/[id]/[status]/reject-input.tsx
- screens/Admin/Event/funUpdateStatus.ts
- screens/Admin/Voting/funUpdateStatus.ts
- service/api-admin/api-admin-voting.ts
- types/type-collect-other.ts

### No Issue
2026-01-15 17:39:39 +08:00
3b15871ad4 Notifikasi join event
Fix:
modified:   app/(application)/(user)/event/[id]/history.tsx
        modified:   app/(application)/(user)/event/[id]/publish.tsx
        modified:   app/(application)/(user)/event/create.tsx
        modified:   app/(application)/(user)/notifications/index.tsx

### No Issue
2026-01-15 13:54:14 +08:00
9123e73606 Fix bug pada beberapa halaman
Fix:
modified:   app.config.js
        modified:   app/(application)/(user)/donation/[id]/[status]/detail.tsx
        modified:   app/(application)/(user)/investment/(tabs)/my-holding.tsx
        modified:   app/(application)/(user)/investment/[id]/[status]/detail.tsx
        modified:   app/(application)/(user)/investment/[id]/index.tsx
        modified:   app/(application)/admin/event/[id]/[status]/index.tsx
        modified:   components/DateInput/DateTimePickerCustom.tsx
        modified:   ios/HIPMIBadungConnect/Info.plist
        modified:   screens/Invesment/BoxProgressSection.tsx

### No Issue
2026-01-14 17:41:20 +08:00
6e2046467f Penerapan notifikasi di event
Add:
components/Button/BackButtonFromNotification.tsx
types/type-collect-other.ts

Fix:
- android/app/build.gradle
- app/(application)/(user)/_layout.tsx
- app/(application)/(user)/event/(tabs)/_layout.tsx
- app/(application)/(user)/event/(tabs)/status.tsx
- app/(application)/(user)/event/create.tsx
- app/(application)/(user)/job/(tabs)/_layout.tsx
- app/(application)/(user)/notifications/index.tsx
- app/(application)/admin/event/[id]/[status]/index.tsx
- app/(application)/admin/event/[id]/reject-input.tsx
- app/(application)/admin/notification/index.tsx
- components/Notification/NotificationInitializer.tsx
- hipmi-note.md
- hooks/use-notification-store.tsx
- screens/Admin/Event/funUpdateStatus.ts
- service/api-notifications.ts
- utils/formatChatTime.ts

### No Issue
2026-01-13 17:41:30 +08:00
ca33dd83bb Perbaikan untuk apple store dan push notifikasi ke preview
Fix:
- app.config.js
- app/(application)/(user)/_layout.tsx
- app/(application)/admin/notification/index.tsx
- eas.json
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-01-12 17:40:15 +08:00
ea3fbdc541 Penambahan metode login dengan menerapkan EULA di awal
Add:
app/eula.tsx
screens/Authentication/EULAView.tsx

Fix:
- context/AuthContext.tsx
- screens/Authentication/RegisterView.tsx
- screens/RootLayout/AppRoot.tsx
- service/api-config.ts

### No Issue
2026-01-09 17:44:13 +08:00
33cd47aaed Fix mobile notification:
> - Bug penerima pesan 2 kali

Fix:
modified:   hooks/use-foreground-notifications.ts

### No Issue
2026-01-09 14:46:21 +08:00
57ac1eb45e Notifikasi admin to user
Fix:
- android/app/src/main/AndroidManifest.xml
- app/(application)/(user)/job/(tabs)/_layout.tsx
- app/(application)/(user)/job/[id]/index.tsx
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/profile/[id]/index.tsx
- app/(application)/admin/job/[id]/[status]/index.tsx
- app/(application)/admin/notification/index.tsx
- app/+not-found.tsx
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- screens/Admin/Job/funUpdateStatus.ts
- screens/Home/bottomFeatureSection.tsx

### No Issue
2026-01-08 17:48:53 +08:00
145ad73616 Fix job notifikasi
###  No Issue
2026-01-08 10:12:53 +08:00
7c85e35c61 Note:
- Fitur notifikasi ke admin dari user baru
- Notifikasi ke user bahwa akunnya telah terverifikasi

Fix:
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/waiting-room.tsx
- app/(application)/admin/super-admin/[id]/index.tsx
- app/(application)/admin/user-access/[id]/index.tsx
- context/AuthContext.tsx
- screens/Home/tabsList.ts
- service/api-admin/api-admin-user-access.ts
- service/api-device-token.ts
- service/api-notifications.ts
- types/type-notification-category.ts

Add:
- lib/routeApp.ts

### No Issue
2026-01-06 12:27:30 +08:00
d098b8ca16 Setup notifikasi untuk android
Fix:
- modified:   service/api-notifications.ts
- modified:   types/type-notification-category.ts

### No Issue
2026-01-05 12:27:23 +08:00
73a473cdc7 Tambah fitur notifikasi untuk android
Add:
- android/app/src/main/res/xml/

Fix:
- android/app/src/main/AndroidManifest.xml
- android/build.gradle
- app/(application)/(user)/test-notifications.tsx
- bun.lock
- package.json

### No Issue
2026-01-02 17:43:23 +08:00
3f85f330d2 Background notifikasi berhasil dibuat
Add:
-components/Notification/BackgroundNotificationHandler.tsx

### No Issue
2025-12-24 17:43:53 +08:00
7743a2467c Fitur notifikasi dan foreground
Add:
- types/type-notification-category.ts

Fix:
- app/(application)/(user)/notifications/index.tsx
- app/(application)/(user)/test-notifications.tsx
- app/(application)/admin/notification/index.tsx
- components/Notification/NotificationInitializer.tsx
- hooks/use-notification-store.tsx
- service/api-notifications.ts
- utils/formatChatTime.ts

### No Issue
2025-12-24 15:29:58 +08:00
54611ef812 Fix notifikasi system
Add:
- screens/Admin/AdminNotificationBell.tsx

Fix:
- app.config.js
- app/(application)/(user)/home.tsx
- app/(application)/(user)/test-notifications.tsx
- app/(application)/admin/_layout.tsx
- app/_layout.tsx
- components/Notification/NotificationInitializer.tsx
- context/AuthContext.tsx
- hooks/use-notification-store.tsx
- ios/HIPMIBadungConnect/Info.plist
- screens/Home/HeaderBell.tsx
- service/api-device-token.ts
- service/api-notifications.ts

### No Issue
2025-12-23 17:45:30 +08:00
1503707eed Notifikasi terhubung ke DB
Add:
- components/Notification/
- hooks/use-notification-store.tsx.back

Fix:
- app.config.js
- app/(application)/(user)/_layout.tsx
- app/(application)/(user)/home.tsx
- app/(application)/(user)/test-notifications.tsx
- app/(application)/_layout.tsx
- app/_layout.tsx
- components/_Icon/IconComponent.tsx
- components/_Icon/IconPlus.tsx
- components/_ShareComponent/NotificationInitializer.tsx
- context/AuthContext.tsx
- hooks/use-notification-store.tsx
- ios/HIPMIBadungConnect/Info.plist
- screens/Home/HeaderBell.tsx
- service/api-device-token.ts
- service/api-notifications.ts

### No Issue
2025-12-19 17:54:49 +08:00
a01a9bd93f Filter console dan clean code
Add:
- service/api-device-token.ts

Fix:
- app/(application)/(user)/home.tsx
- app/(application)/(user)/test-notifications.tsx
- app/_layout.tsx
- components/_ShareComponent/NotificationInitializer.tsx
- context/AuthContext.tsx
- hooks/use-foreground-notifications.ts
- screens/Home/HeaderBell.tsx
- service/api-notifications.ts

### No Issue
2025-12-17 17:46:28 +08:00
05c1cac10f Penerapan ke database
Fix:
- android/app/src/main/AndroidManifest.xml
- app/(application)/(user)/home.tsx
- components/_ShareComponent/NotificationInitializer.tsx
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist
- service/api-notifications.ts

### No Issue
2025-12-16 17:47:50 +08:00
d27c01ed56 Percobaan notifikasi
Add:
- app/(application)/(user)/test-notifications.tsx
- components/_ShareComponent/NotificationInitializer.tsx
- screens/Home/HeaderBell.tsx
- service/api-notifications.ts

Fix:
- app/(application)/(user)/home.tsx
- app/_layout.tsx
- screens/Authentication/LoginView.tsx

### No Issue
2025-12-15 17:46:05 +08:00
34680a4c38 test 2025-12-13 17:31:48 +08:00
160 changed files with 6373 additions and 1577 deletions

0
QWEN.md Normal file
View File

View File

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

View File

@@ -15,8 +15,8 @@
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false">
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:enableOnBackInvokedCallback="false" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules">
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color" tools:replace="android:resource"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="SECURESTORE"/>
</full-backup-content>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="SECURESTORE"/>
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="SECURESTORE"/>
</device-transfer>
</data-extraction-rules>

View File

@@ -21,7 +21,7 @@ export default {
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "15",
buildNumber: "20",
},
android: {
@@ -32,7 +32,7 @@ export default {
},
edgeToEdgeEnabled: true,
package: "com.bip.hipmimobileapp",
versionCode: 3,
versionCode: 4,
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
intentFilters: [
{

View File

@@ -1,8 +1,10 @@
import { CenterCustom, TextCustom, ViewWrapper } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import { MainColor } from "@/constants/color-palet";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import React, { useState } from "react";
import { View } from "react-native";
export default function PreviewImage() {
const { id } = useLocalSearchParams();
@@ -11,18 +13,48 @@ export default function PreviewImage() {
return (
<ViewWrapper>
{id ? (
<View
style={{
width: "100%",
height: "100%",
position: "relative",
}}
>
{/* Main Image */}
<Image
onLoad={() => {
setIsLoading(false);
}}
source={
isLoading
? require("@/assets/images/loading.gif")
: API_STRORAGE.GET({ fileId: id as string })
}
source={API_STRORAGE.GET({ fileId: id as string })}
contentFit="contain"
style={{ width: "100%", height: "100%" }}
// placeholder={require("@/assets/images/loading.gif")}
/>
{/* Custom Loader Overlay */}
{isLoading && (
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
backgroundColor: MainColor.darkblue,
zIndex: 1,
opacity: 0.5,
}}
>
<Image
source={require("@/assets/images/loading.gif")}
contentFit="contain"
style={{ width: 60, height: 60 }}
/>
</View>
)}
</View>
) : (
<CenterCustom>
<TextCustom>File not found</TextCustom>

View File

@@ -1,4 +1,6 @@
import { BackButton } from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import LeftButtonCustom from "@/components/Button/BackButton";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
@@ -51,24 +53,35 @@ export default function UserLayout() {
/>
{/* ========== Notification Section ========= */}
<Stack.Screen
{/* DIPINDAH DI FILE NOTIFICATION USER */}
{/* <Stack.Screen
name="notifications/index"
options={{
title: "Notifikasi",
headerLeft: () => <BackButton />,
// headerRight: () => (
// <IconPlus
// color={MainColor.yellow}
// onPress={() => router.push("/test-notifications")}
// />
// ),
}}
/>
/> */}
{/* ========== Event Section ========= */}
<Stack.Screen
name="event/(tabs)"
options={{
title: "Event",
headerLeft: () => (
<LeftButtonCustom path="/(application)/(user)/home" />
),
// NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx
// headerLeft: () => (
// <LeftButtonCustom path="/(application)/(user)/home" />
// ),
}}
/>
<Stack.Screen
name="event/create"
options={{
@@ -511,7 +524,8 @@ export default function UserLayout() {
name="job/(tabs)"
options={{
title: "Job Vacancy",
headerLeft: () => <BackButton path="/home" />,
// headerLeft: () => <BackButton path="/home" />,
// NOTE: headerLeft di pindahkan ke Tabs Layout
}}
/>
<Stack.Screen
@@ -602,6 +616,20 @@ export default function UserLayout() {
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="forum/[id]/preview-report-posting"
options={{
title: "Laporan Postingan",
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="forum/[id]/preview-report-comment"
options={{
title: "Laporan Komentar",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== Maps Section ========= */}
<Stack.Screen

View File

@@ -26,7 +26,6 @@ export default function DonationBeranda() {
const response = await apiDonationGetAll({
category: "beranda"
});
console.log("[RES GET ALL]", JSON.stringify(response.data, null, 2));
setList(response.data);
} catch (error) {

View File

@@ -16,6 +16,7 @@ import { Href, router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
export default function DonationMyDonation() {
const { user } = useAuth();
@@ -25,20 +26,25 @@ export default function DonationMyDonation() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
}, [user?.id]),
);
const onLoadData = async () => {
if (!user?.id) {
Toast.show({
type: "error",
text1: "Load data gagal, user tidak ditemukan",
});
return;
}
try {
setLoadList(true);
const response = await apiDonationGetAll({
category: "my-donation",
authorId: user?.id,
});
console.log(
"[RES GET MY DONATION]",
JSON.stringify(response.data, null, 2)
);
setList(response.data);
} catch (error) {

View File

@@ -9,14 +9,16 @@ import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
import { useFocusEffect } from "expo-router";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function DonationStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
status || "publish",
);
const [listData, setListData] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
@@ -24,7 +26,7 @@ export default function DonationStatus() {
useFocusEffect(
useCallback(() => {
onLoadList();
}, [activeCategory])
}, [activeCategory]),
);
const onLoadList = async () => {

View File

@@ -10,21 +10,32 @@ import {
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key";
import { useAuth } from "@/hooks/use-auth";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { Ionicons } from "@expo/vector-icons";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function InvestmentInputDonation() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [nominal, setNominal] = useState<number>(0);
const handlerSubmit = async () => {
if (!user?.id) {
Toast.show({
type: "error",
text1: "User tidak ditemukan",
});
return;
}
try {
await AsyncStorage.setItem(
LOCAL_STORAGE_KEY.transactionDonation,
JSON.stringify({ nominal: nominal.toString() })
JSON.stringify({ nominal: nominal.toString() }),
);
router.replace(`/donation/${id}/select-bank`);
} catch (error) {

View File

@@ -16,6 +16,7 @@ import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDeta
import Donation_ComponentStoryFunrising from "@/screens/Donation/ComponentStoryFunrising";
import Donation_ProgressSection from "@/screens/Donation/ProgressSection";
import { apiDonationGetOne } from "@/service/api-client/api-donation";
import { countDownAndCondition } from "@/utils/countDownAndCondition";
import { FontAwesome6 } from "@expo/vector-icons";
import {
router,
@@ -24,7 +25,7 @@ import {
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
export default function DonasiDetailStatus() {
const { id, status } = useLocalSearchParams();
@@ -58,6 +59,27 @@ export default function DonasiDetailStatus() {
setOpenDrawer(false);
};
const [value, setValue] = useState({
sisa: 0,
reminder: false,
});
useEffect(() => {
updateCountDown();
}, [data]);
const updateCountDown = () => {
const countDown = countDownAndCondition({
duration: data?.DonasiMaster_Durasi?.name,
publishTime: data?.publishTime,
});
setValue({
sisa: countDown.durationDay,
reminder: countDown.reminder,
});
};
return (
<>
<Stack.Screen
@@ -74,10 +96,15 @@ export default function DonasiDetailStatus() {
/>
<ViewWrapper>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
bottomSection={
status === "publish" && (
<Donation_ProgressSection id={id as string} />
<Donation_ProgressSection
id={id as string}
progres={Number(data?.progres) || 0}
/>
)
}
/>

View File

@@ -103,7 +103,7 @@ export default function DonationCreateStory() {
type: "success",
text1: "Donasi berhasil disimpan",
});
router.replace("/donation/status");
router.replace("/donation/status?status=review");
} catch (error) {
console.log("[ERROR]", error);
} finally {

View File

@@ -4,10 +4,34 @@ import {
IconHome,
IconStatus,
} from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles";
import { Tabs } from "expo-router";
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
import { useLayoutEffect } from "react";
export default function EventTabsLayout() {
const navigation = useNavigation();
const { from, category } = useLocalSearchParams<{
from?: string;
category?: string;
}>();
console.log("from", from);
console.log("category", category);
// Atur header secara dinamis
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => (
<BackButtonFromNotification
from={from as string}
category={category as string}
/>
),
});
}, [from, router, navigation]);
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen

View File

@@ -11,15 +11,17 @@ import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useFocusEffect } from "expo-router";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function EventStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
status || "publish"
);
const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false);

View File

@@ -56,7 +56,7 @@ export default function EventDetailHistory() {
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={250}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })}

View File

@@ -1,14 +1,15 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
ButtonCustom,
DotButton,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
ViewWrapper,
} from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import LeftButtonCustom from "@/components/Button/BackButton";
import { useAuth } from "@/hooks/use-auth";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
@@ -18,23 +19,26 @@ import {
apiEventGetOne,
apiEventJoin,
} from "@/service/api-client/api-event";
import dayjs from "dayjs";
import {
Redirect,
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function EventDetailPublish() {
const { id } = useLocalSearchParams();
const now = new Date().toISOString();
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(false);
const [isLoadingJoin, setIsLoadingJoin] = useState(false);
const [data, setData] = useState();
const [data, setData] = useState<any>();
const [isParticipant, setIsParticipant] = useState<boolean | null>(null);
useFocusEffect(
@@ -55,8 +59,6 @@ export default function EventDetailPublish() {
userId: user?.id as string,
});
console.log("[RES CHECK PARTICIPANTS]", responseCheckParticipants);
if (
responseCheckParticipants.success &&
responseCheckParticipants.data
@@ -108,7 +110,24 @@ export default function EventDetailPublish() {
}
};
const footerButton = () => {
const isEventFinished =
id && data?.tanggalSelesai && dayjs(data.tanggalSelesai).isBefore(now);
useEffect(() => {
if (isEventFinished) {
router.replace(`/(application)/(user)/event/${id}/history`);
}
}, [isEventFinished, id]);
if (isEventFinished) {
return (
<ViewWrapper>
<CustomSkeleton />
</ViewWrapper>
);
}
const FooterButton = () => {
return (
<>
<ButtonCustom
@@ -138,17 +157,17 @@ export default function EventDetailPublish() {
<Stack.Screen
options={{
title: `Event Publish`,
headerLeft: () => <LeftButtonCustom />,
headerLeft: () => <BackButton onPress={() => router.back()} />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}}
/>
<ViewWrapper>
{isLoadingData ? (
<LoaderCustom />
<CustomSkeleton height={400} />
) : (
<Event_BoxDetailPublishSection
data={data}
footerButton={footerButton()}
footerButton={FooterButton()}
/>
)}
</ViewWrapper>

View File

@@ -14,7 +14,7 @@ import { apiEventCreate } from "@/service/api-client/api-event";
import { apiMasterEventType } from "@/service/api-client/api-master";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import { router } from "expo-router";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
interface EventCreateProps {
@@ -78,23 +78,6 @@ export default function EventCreate() {
return;
}
// if (selectedDate) {
// console.log("Tanggal yang dipilih:", selectedDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal belum dipilih");
// }
// if (selectedEndDate) {
// console.log("Tanggal yang dipilih:", selectedEndDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal berakhir belum dipilih");
// }
try {
setIsLoading(true);
@@ -110,7 +93,7 @@ export default function EventCreate() {
const response = await apiEventCreate(newData);
console.log("Response", JSON.stringify(response, null, 2));
router.replace("/event/status");
router.replace("/event/status?status=review");
} catch (error) {
console.log(error);
} finally {

View File

@@ -1,271 +1,11 @@
import {
ButtonCustom,
DrawerCustom,
LoaderCustom,
Spacing,
TextAreaCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { useAuth } from "@/hooks/use-auth";
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar";
import {
apiForumCreateComment,
apiForumGetComment,
apiForumGetOne,
apiForumUpdateStatus,
} from "@/service/api-client/api-forum";
import { isBadContent } from "@/utils/badWordsIndonesia";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { Alert } from "react-native";
interface CommentProps {
id: string;
isActive: boolean;
komentar: string;
createdAt: Date;
authorId: string;
Author: {
id: string;
username: string;
Profile: {
id: string;
imageId: string;
};
};
}
import DetailForum from "@/screens/Forum/DetailForum";
import DetailForum2 from "@/screens/Forum/DetailForum2";
export default function ForumDetail() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any | null>(null);
const [listComment, setListComment] = useState<CommentProps[] | null>(null);
const [isLoadingComment, setLoadingComment] = useState(false);
// Status
const [status, setStatus] = useState("");
const [text, setText] = useState("");
const [authorId, setAuthorId] = useState("");
const [dataId, setDataId] = useState("");
// Comentar
const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false);
const [commentId, setCommentId] = useState("");
const [commentAuthorId, setCommentAuthorId] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
const onLoadData = async (id: string) => {
try {
const response = await apiForumGetOne({ id });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
useEffect(() => {
onLoadListComment(id as string);
}, [id]);
const onLoadListComment = async (id: string) => {
try {
const response = await apiForumGetComment({
id: id as string,
});
setListComment(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
// Update Status
const handlerUpdateStatus = async (value: any) => {
try {
const response = await apiForumUpdateStatus({
id: id as string,
data: value,
});
if (response.success) {
setStatus(response.data);
setData({
...data,
ForumMaster_StatusPosting: {
status: response.data,
},
});
}
} catch (error) {
console.log("[ERROR]", error);
}
};
// Create Commentar
const handlerCreateCommentar = async () => {
if (isBadContent(text)) {
AlertWarning({});
return;
}
const newData = {
comment: text,
authorId: user?.id,
};
try {
setLoadingComment(true);
const response = await apiForumCreateComment({
id: id as string,
data: newData,
});
if (response.success) {
setText("");
const newComment = {
id: response.data.id,
isActive: response.data.isActive,
komentar: response.data.komentar,
createdAt: response.data.createdAt,
authorId: response.data.authorId,
Author: response.data.Author,
};
setListComment((prev) => [newComment, ...(prev || [])]);
setData({
...data,
count: data.count + 1,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingComment(false);
}
};
return (
<>
<ViewWrapper>
{!data && !listComment ? (
<LoaderCustom />
) : (
<>
{/* Box Posting */}
<Forum_BoxDetailSection
data={data}
onSetData={() => {
setOpenDrawer(true);
setStatus(data.ForumMaster_StatusPosting?.status);
setAuthorId(data.Author?.id);
setDataId(data.id);
}}
/>
{/* Area Commentar */}
{data?.ForumMaster_StatusPosting?.status === "Open" && (
<>
<TextAreaCustom
placeholder="Ketik diskusi anda..."
maxLength={1000}
showCount
value={text}
onChangeText={setText}
style={{
marginBottom: 0,
}}
/>
<ButtonCustom
isLoading={isLoadingComment}
style={{
alignSelf: "flex-end",
}}
onPress={() => {
handlerCreateCommentar();
}}
>
Balas
</ButtonCustom>
{/* <DetailForum />; */}
<DetailForum2 />
</>
)}
<Spacing height={40} />
{/* List Commentar */}
{_.isEmpty(listComment) ? (
<TextCustom align="center" color="gray" size={"small"}>
Tidak ada komentar
</TextCustom>
) : (
<TextCustom color="gray">Komentar :</TextCustom>
)}
<Spacing height={5} />
{listComment?.map((item: any, index: number) => (
<Forum_CommentarBoxSection
key={index}
data={item}
onSetData={(value) => {
setCommentId(value.setCommentId);
setOpenDrawerCommentar(value.setOpenDrawer);
setCommentAuthorId(value.setCommentAuthorId);
}}
/>
))}
</>
)}
</ViewWrapper>
{/* Posting Drawer */}
<DrawerCustom
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={dataId}
authorUsername={data?.Author?.username as string}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
authorId={authorId}
handlerUpdateStatus={(value: any) => {
handlerUpdateStatus(value);
}}
/>
</DrawerCustom>
{/* Commentar Drawer */}
<DrawerCustom
height={"auto"}
isVisible={openDrawerCommentar}
closeDrawer={() => setOpenDrawerCommentar(false)}
>
<Forum_MenuDrawerCommentar
id={commentId as string}
commentId={commentId}
commentAuthorId={commentAuthorId}
setIsDrawerOpen={() => {
setOpenDrawerCommentar(false);
}}
listComment={listComment}
setListComment={setListComment}
countComment={data?.count}
setCountComment={(val: any) => {
setData((prev: any) => ({
...prev,
count: val,
}));
}}
/>
</DrawerCustom>
</>
);
)
}

View File

@@ -6,7 +6,7 @@ import {
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreateReportCommentar } from "@/service/api-client/api-master";
import { apiForumCreateReportCommentar } from "@/service/api-client/api-forum";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";

View File

@@ -6,7 +6,7 @@ import {
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreateReportPosting } from "@/service/api-client/api-master";
import { apiForumCreateReportPosting } from "@/service/api-client/api-forum";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";

View File

@@ -0,0 +1,91 @@
import {
BaseBox,
NewWrapper,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { apiForumGetReportComment } from "@/service/api-client/api-forum";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function ForumPreviewReportComment() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [listData, setListData] = useState<any | null>(null);
const [loading, setLoading] = useState<boolean>(false);
// Status
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
const onLoadData = async (id: string) => {
try {
setLoading(true);
const response = await apiForumGetReportComment({ id });
setData(response.data);
setListData(response?.data?.Forum_ReportKomentar);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return (
<>
<NewWrapper>
<StackCustom>
<TextCustom color="red" bold>
Komentar anda telah melanggar aturan forum ! Admin mengambil
tindakan untuk menghapus komentar anda!
</TextCustom>
{loading ? (
<CustomSkeleton height={100} />
) : (
<BaseBox>
<TextCustom>"{data?.komentar ? data?.komentar : "-"}"</TextCustom>
</BaseBox>
)}
</StackCustom>
<Spacing height={10} />
<TextCustom bold>Beberapa laporan yang telah diterima</TextCustom>
<Spacing height={10} />
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText />
) : (
listData?.map((e: any, index: number) => (
<BaseBox key={index}>
{e?.deskripsi ? (
<StackCustom gap={"sm"}>
<TextCustom bold>Laporan Lainnya</TextCustom>
<TextCustom>{e?.deskripsi}</TextCustom>
</StackCustom>
) : (
<StackCustom gap={"sm"}>
<TextCustom bold>
{e?.ForumMaster_KategoriReport?.title}
</TextCustom>
<TextCustom>
{e?.ForumMaster_KategoriReport?.deskripsi}
</TextCustom>
</StackCustom>
)}
</BaseBox>
))
)}
</NewWrapper>
</>
);
}

View File

@@ -0,0 +1,91 @@
import {
BaseBox,
NewWrapper,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { apiForumGetReportPosting } from "@/service/api-client/api-forum";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function ForumPreviewReportPosting() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [listData, setListData] = useState<any | null>(null);
const [loading, setLoading] = useState<boolean>(false);
// Status
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
const onLoadData = async (id: string) => {
try {
setLoading(true);
const response = await apiForumGetReportPosting({ id });
setData(response.data);
setListData(response?.data?.Forum_ReportPosting);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return (
<>
<NewWrapper>
<StackCustom>
<TextCustom color="red" bold>
Postingan anda telah melanggar aturan forum ! Admin mengambil
tindakan untuk menghapus komentar anda!
</TextCustom>
{loading ? (
<CustomSkeleton height={100} />
) : (
<BaseBox>
<TextCustom>"{data?.diskusi ? data?.diskusi : "-"}"</TextCustom>
</BaseBox>
)}
</StackCustom>
<Spacing height={10} />
<TextCustom bold>Beberapa laporan yang telah diterima</TextCustom>
<Spacing height={10} />
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText />
) : (
listData?.map((e: any) => (
<BaseBox key={e?.id}>
{e?.deskripsi ? (
<StackCustom gap={"sm"}>
<TextCustom bold>Laporan Lainnya</TextCustom>
<TextCustom>{e?.deskripsi}</TextCustom>
</StackCustom>
) : (
<StackCustom gap={"sm"}>
<TextCustom bold>
{e?.ForumMaster_KategoriReport?.title}
</TextCustom>
<TextCustom>
{e?.ForumMaster_KategoriReport?.deskripsi}
</TextCustom>
</StackCustom>
)}
</BaseBox>
))
)}
</NewWrapper>
</>
);
}

View File

@@ -8,7 +8,8 @@ import {
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
import { apiForumCreateReportCommentar, apiMasterForumReportList } from "@/service/api-client/api-master";
import { apiForumCreateReportCommentar } from "@/service/api-client/api-forum";
import { apiMasterForumReportList } from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useState, useEffect } from "react";
import Toast from "react-native-toast-message";

View File

@@ -9,8 +9,8 @@ import {
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
import { apiForumCreateReportPosting } from "@/service/api-client/api-forum";
import {
apiForumCreateReportPosting,
apiMasterForumReportList,
} from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";

View File

@@ -2,15 +2,14 @@ import {
BoxButtonOnFooter,
ButtonCustom,
TextAreaCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreate } from "@/service/api-client/api-forum";
import { isBadContent } from "@/utils/badWordsIndonesia";
import { censorText, isBadContent } from "@/utils/badWordsIndonesia";
import { router } from "expo-router";
import { useState } from "react";
import { Alert } from "react-native";
import Toast from "react-native-toast-message";
export default function ForumCreate() {
@@ -19,16 +18,22 @@ export default function ForumCreate() {
const [isLoading, setIsLoading] = useState(false);
const handlerSubmit = async () => {
if (isBadContent(text)) {
AlertWarning({})
if (text.trim() === "") {
AlertWarning({
title: "Lengkapi Data",
description: "Postingan tidak boleh kosong",
});
return;
}
// Bisa di sensor atau return dan tidak bisa di post
const cencorContent = censorText(text)
const newData = {
diskusi: text,
diskusi: cencorContent,
authorId: user?.id,
};
try {
setIsLoading(true);
const response = await apiForumCreate({ data: newData });
@@ -50,6 +55,7 @@ export default function ForumCreate() {
const buttonFooter = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!text.trim() || isLoading}
isLoading={isLoading}
onPress={() => {
handlerSubmit();

View File

@@ -1,12 +1,14 @@
/* eslint-disable react-hooks/exhaustive-deps */
import Forum_ViewBeranda from "@/screens/Forum/ViewBeranda";
import Forum_ViewBeranda2 from "@/screens/Forum/ViewBeranda2";
import Forum_ViewBeranda3 from "@/screens/Forum/ViewBeranda3";
export default function Forum() {
return (
<>
{/* <Forum_ViewBeranda /> */}
<Forum_ViewBeranda2 />
{/* <Forum_ViewBeranda2 /> */}
<Forum_ViewBeranda3 />
</>
);
}

View File

@@ -3,7 +3,9 @@
import { StackCustom, ViewWrapper } from "@/components";
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 Home_ImageSection from "@/screens/Home/imageSection";
import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList";
@@ -12,21 +14,21 @@ import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
export default function Application() {
const { token, user, userData } = useAuth();
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
console.log("[User] >>", JSON.stringify(user?.id, null, 2));
const { syncUnreadCount } = useNotificationStore();
// ‼️ Untuk cek apakah: 1. user ada, 2. user punya profile, 3. accept temrs of forum nya ada atau tidak
useFocusEffect(
useCallback(() => {
onLoadData();
checkVersion();
userData(token as string);
syncUnreadCount();
}, [user?.id, token])
);
@@ -52,10 +54,10 @@ export default function Application() {
setRefreshing(false);
}, []);
if (user && user?.termsOfServiceAccepted === false) {
console.log("User is not accept term service");
return <Redirect href={`/terms-agreement`} />;
}
// 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");
@@ -82,17 +84,7 @@ export default function Application() {
}}
/>
),
headerRight: () => (
<Ionicons
disabled={true}
name="notifications"
size={20}
color={MainColor.placeholder}
onPress={() => {
router.push("/notifications");
}}
/>
),
headerRight: () => <HeaderBell />,
}}
/>
<ViewWrapper
@@ -109,6 +101,10 @@ export default function Application() {
}
>
<StackCustom>
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
Test Notif
</ButtonCustom> */}
<Home_ImageSection />
<Home_FeatureSection />

View File

@@ -1,9 +1,33 @@
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { TabsStyles } from "@/styles/tabs-styles";
import { Feather, FontAwesome6, Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
import { useLayoutEffect } from "react";
export default function InvestmentTabsLayout() {
// const navigation = useNavigation();
// const { from, category } = useLocalSearchParams<{
// from?: string;
// category?: string;
// }>();
// console.log("from", from);
// console.log("category", category);
// // Atur header secara dinamis
// useLayoutEffect(() => {
// navigation.setOptions({
// headerLeft: () => (
// <BackButtonFromNotification
// from={from as string}
// category={category as string}
// />
// ),
// });
// }, [from, router, navigation]);
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen

View File

@@ -11,9 +11,7 @@ import {
} from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { useAuth } from "@/hooks/use-auth";
import {
apiInvestmentGetAll
} from "@/service/api-client/api-investment";
import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
@@ -63,37 +61,20 @@ export default function InvestmentMyHolding() {
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
}
>
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"xs"}>
<StackCustom>
<TextCustom truncate={2}>{item?.title}</TextCustom>
<Spacing height={5} />
<TextCustom size="small">
<TextCustom>
Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
<TextCustom size="small">
{item?.lembarTerbeli} Lembar
</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col
span={5}
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
<ProgressCustom
value={item?.progress}
label={`${item?.progress}%`}
label={`${item.progress}%`}
value={Number(item.progress)}
size="lg"
animated
color="primary"
/>
</Grid.Col>
</Grid>
</StackCustom>
</BaseBox>
))
)}

View File

@@ -9,14 +9,16 @@ import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Investment_StatusBox from "@/screens/Invesment/StatusBox";
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
import { useFocusEffect } from "expo-router";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function InvestmentPortofolio() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
status || "publish"
);
const [listData, setListData] = useState<any[]>([]);

View File

@@ -115,7 +115,11 @@ export default function InvestmentAddNews() {
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
<ButtonCustom isLoading={isLoading} onPress={handlerSubmit}>
<ButtonCustom
disabled={!data.title || !data.deskripsi || isLoading}
isLoading={isLoading}
onPress={handlerSubmit}
>
Simpan
</ButtonCustom>
</StackCustom>

View File

@@ -27,7 +27,6 @@ import Toast from "react-native-toast-message";
export default function InvestmentInvoice() {
const { id } = useLocalSearchParams();
console.log("[ID]", id);
const [data, setData] = useState<any>({});
const [image, setImage] = useState<IFileData>({
name: "",
@@ -49,7 +48,6 @@ export default function InvestmentInvoice() {
category: "invoice",
});
console.log("[RES INVOICE]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
@@ -64,8 +62,6 @@ export default function InvestmentInvoice() {
imageUri: image?.uri,
});
console.log("[RESPONSE UPLOAD IMAGE]", responseUploadImage);
if (!responseUploadImage?.data?.id) {
Toast.show({
type: "error",
@@ -83,10 +79,6 @@ export default function InvestmentInvoice() {
});
if (response.success) {
console.log(
"[RESPONSE UPDATE]",
JSON.stringify(response.data, null, 2)
);
Toast.show({
type: "success",
text1: "Berhasil mengunggah bukti transfer",
@@ -210,7 +202,6 @@ export default function InvestmentInvoice() {
pickFile({
allowedType: "image",
setImageUri(file: any) {
console.log("[IMAGE]", file);
setImage(file);
},
});
@@ -224,7 +215,7 @@ export default function InvestmentInvoice() {
<ButtonCustom
isLoading={isLoading}
disabled={!image}
disabled={!image || isLoading}
onPress={() => {
handlerSubmitUpdate();
}}

View File

@@ -15,6 +15,7 @@ import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvesta
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
import { apiInvestmentGetOne } from "@/service/api-client/api-investment";
import { countDownAndCondition } from "@/utils/countDownAndCondition";
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import {
router,
@@ -23,7 +24,7 @@ import {
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
export default function InvestmentDetailStatus() {
const { user } = useAuth();
@@ -63,6 +64,29 @@ export default function InvestmentDetailStatus() {
setOpenDrawerPublish(false);
};
const [value, setValue] = useState({
sisa: 0,
reminder: false,
});
useEffect(() => {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({
duration: data?.MasterPencarianInvestor.name,
publishTime: data?.countDown,
});
setValue({
sisa: countDown.durationDay,
reminder: countDown.reminder,
});
};
const bottomSection = (
<Invesment_ComponentBoxOnBottomDetail
id={data?.id}
@@ -72,7 +96,11 @@ export default function InvestmentDetailStatus() {
);
const buttonSection = (
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} />
<Investment_ButtonInvestasiSection
id={id as string}
isMine={user?.id === data?.author?.id}
reminder={value.reminder}
/>
);
return (

View File

@@ -18,10 +18,7 @@ import {
apiInvestmentUpdateData,
} from "@/service/api-client/api-investment";
import { apiMasterInvestment } from "@/service/api-client/api-master";
import {
deleteFileService,
uploadFileService,
} from "@/service/upload-service";
import { deleteFileService, uploadFileService } from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -70,7 +67,7 @@ export default function InvestmentEdit() {
useCallback(() => {
onLoadMaster();
onLoadData();
}, [id])
}, [id]),
);
const onLoadMaster = async () => {
@@ -178,7 +175,7 @@ export default function InvestmentEdit() {
const responseUpdate = await apiInvestmentUpdateData({
id: id as string,
data: newData,
category: "data"
category: "data",
});
if (responseUpdate.success) {
@@ -256,6 +253,7 @@ export default function InvestmentEdit() {
/>
<TextInputCustom
disabled
required
placeholder="0"
label="Total Lembar"

View File

@@ -86,8 +86,6 @@ export default function InvestmentDetail() {
});
};
const bottomSection = (
<Invesment_ComponentBoxOnBottomDetail
id={id as string}
@@ -97,7 +95,11 @@ export default function InvestmentDetail() {
);
const buttonSection = (
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} reminder={value.reminder} />
<Investment_ButtonInvestasiSection
id={id as string}
isMine={user?.id === data?.author?.id}
reminder={value.reminder}
/>
);
return (

View File

@@ -54,7 +54,7 @@ export default function InvestmentCreate() {
useFocusEffect(
useCallback(() => {
onLoadMaster();
}, [])
}, []),
);
const onLoadMaster = async () => {
@@ -167,7 +167,7 @@ export default function InvestmentCreate() {
text1: "Berhasil",
text2: response.message,
});
router.replace("/investment/portofolio");
router.replace("/investment/portofolio?status=review");
} else {
Toast.show({
type: "error",
@@ -224,7 +224,6 @@ export default function InvestmentCreate() {
onPress={() => {
pickFile({
setPdfUri: ({ uri, name, size }) => {
setPdf({ uri, name, size });
},
allowedType: "pdf",
@@ -265,6 +264,7 @@ export default function InvestmentCreate() {
<StackCustom gap={0}>
<TextInputCustom
disabled
required
placeholder="0"
label="Total Lembar"
@@ -357,7 +357,11 @@ export default function InvestmentCreate() {
)}
<Spacing />
<ButtonCustom isLoading={isLoading} onPress={() => handleSubmit()}>
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</StackCustom>

View File

@@ -1,10 +1,36 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BackButton } from "@/components";
import { IconHome, IconStatus } from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import {
router,
Tabs,
useLocalSearchParams,
useNavigation
} from "expo-router";
import { useLayoutEffect } from "react";
export default function JobTabsLayout() {
const navigation = useNavigation();
const { from, category } = useLocalSearchParams<{
from?: string;
category?: string;
}>();
// Atur header secara dinamis
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => (
<BackButtonFromNotification from={from as string} category={category as string} />
),
});
}, [from, router, navigation]);
return (
<>
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
@@ -30,5 +56,6 @@ export default function JobTabsLayout() {
}}
/>
</Tabs>
</>
);
}

View File

@@ -1,57 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2";
export default function JobArchive() {
const { user } = useAuth();
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({
category: "archive",
authorId: user?.id,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper hideFooter>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
) : (
listData.map((item, index) => (
<BaseBox
key={index}
paddingTop={20}
paddingBottom={20}
href={`/job/${item.id}/archive`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title || "-"}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
<>
<Job_ScreenArchive2 />
</>
);
}

View File

@@ -1,83 +1,10 @@
import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
FloatingButton,
LoaderCustom,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda";
import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2";
export default function JobBeranda() {
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData(search);
}, [search])
);
const onLoadData = async (search: string) => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({ search, category: "beranda" });
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handleSearch = (search: string) => {
setSearch(search);
onLoadData(search);
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/job/create")} />
}
headerComponent={
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
}
>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada lowongan</TextCustom>
) : (
listData.map((item, index) => (
<BoxWithHeaderSection
key={index}
onPress={() => router.push(`/job/${item.id}`)}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
name={item?.Author?.username}
/>
<TextCustom truncate={2} align="center" bold size="large">
{item?.title || "-"}
</TextCustom>
</StackCustom>
<Spacing />
</BoxWithHeaderSection>
))
)}
<Spacing />
</ViewWrapper>
<>
<Job_ScreenBeranda2 />
</>
);
}

View File

@@ -1,84 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiJobGetByStatus } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Job_MainViewStatus from "@/screens/Job/MainViewStatus";
import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2";
export default function JobStatus() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
);
const [listData, setListData] = useState<any[]>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id, activeCategory])
);
const onLoadData = async () => {
try {
setIsLoadList(true);
const response = await apiJobGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
<>
{/* <Job_MainViewStatus /> */}
<Job_MainViewStatus2 />
</>
);
}

View File

@@ -74,7 +74,7 @@ export default function JobDetailStatus() {
<StackCustom gap={"xs"}>
{data &&
data?.catatan &&
(status === "draft" || status === "rejected") && (
(status === "draft" || status === "reject") && (
<ReportBox text={data?.catatan} />
)}

View File

@@ -25,6 +25,8 @@ export default function JobDetail() {
setIsLoading(true);
const response = await apiJobGetOne({ id: id as string });
console.log("DATA", JSON.stringify(response.data, null,2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);

View File

@@ -1,13 +1,14 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
NewWrapper,
Spacing,
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper
TextInputCustom
} from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
@@ -19,7 +20,7 @@ import { useState } from "react";
import Toast from "react-native-toast-message";
export default function JobCreate() {
const nextUrl = "/(application)/(user)/job/(tabs)/status";
const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review";
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState<string | null>(null);
@@ -99,16 +100,17 @@ export default function JobCreate() {
const buttonSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
Simpan
</ButtonCustom>
<Spacing />
</BoxButtonOnFooter>
</>
);
};
return (
<ViewWrapper>
<NewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
@@ -160,9 +162,7 @@ export default function JobCreate() {
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
{buttonSubmit()}
</StackCustom>
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,111 +1,9 @@
import {
BaseBox,
Grid,
ScrollableCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useState } from "react";
import { View } from "react-native";
import ScreenNotification from "@/screens/Notification/ScreenNotification";
const categories = [
{ value: "all", label: "Semua" },
{ value: "event", label: "Event" },
{ value: "job", label: "Job" },
{ value: "voting", label: "Voting" },
{ value: "donasi", label: "Donasi" },
{ value: "investasi", label: "Investasi" },
{ value: "forum", label: "Forum" },
{ value: "collaboration", label: "Collaboration" },
];
const selectedCategory = (value: string) => {
const category = categories.find((c) => c.value === value);
return category?.label;
};
const BoxNotification = ({
index,
activeCategory,
}: {
index: number;
activeCategory: string | null;
}) => {
export default function Notification() {
return (
<>
<BaseBox
onPress={() =>
console.log(
"Notification >",
selectedCategory(activeCategory as string)
)
}
>
<StackCustom>
<TextCustom bold>
# {selectedCategory(activeCategory as string)}
</TextCustom>
<View
style={{
borderBottomColor: MainColor.white_gray,
borderBottomWidth: 1,
}}
/>
<TextCustom truncate={2}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint odio
unde quidem voluptate quam culpa sequi molestias ipsa corrupti id,
soluta, nostrum adipisci similique, et illo asperiores deleniti eum
labore.
</TextCustom>
<Grid>
<Grid.Col span={6}>
<TextCustom size="small" color="gray">
{index + 1} Agustus 2025
</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom size="small" color="gray">
Belum lihat
</TextCustom>
</Grid.Col>
</Grid>
</StackCustom>
</BaseBox>
<ScreenNotification />
</>
);
};
export default function Notifications() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
return (
<ViewWrapper
headerComponent={
<ScrollableCustom
data={categories.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
>
{Array.from({ length: 20 }).map((e, i) => (
<View key={i}>
<BoxNotification index={i} activeCategory={activeCategory as any} />
</View>
))}
</ViewWrapper>
);
}

View File

@@ -7,6 +7,7 @@ import {
CenterCustom,
Grid,
InformationBox,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
@@ -120,7 +121,7 @@ export default function PortofolioCreate() {
};
return (
<ViewWrapper
<NewWrapper
footerComponent={
<Portofolio_ButtonCreate
id={id as string}
@@ -357,8 +358,8 @@ export default function PortofolioCreate() {
setDataMedsos({ ...dataMedsos, youtube: value })
}
/>
<Spacing />
{/* <Spacing /> */}
</StackCustom>
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -4,14 +4,15 @@ import {
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
@@ -238,7 +239,7 @@ export default function PortofolioEdit() {
return !dataArray.some(
(item: any) =>
!item.MasterSubBidangBisnis.id ||
item.MasterSubBidangBisnis.id.trim() === ""
item.MasterSubBidangBisnis.id.trim() === "",
);
}
@@ -319,16 +320,16 @@ export default function PortofolioEdit() {
if (!bidangBisnis || !subBidangBisnis) {
return (
<>
<ViewWrapper>
<ActivityIndicator size="large" color={MainColor.yellow} />
</ViewWrapper>
<NewWrapper>
<ListSkeletonComponent height={80} />
</NewWrapper>
</>
);
}
return (
<>
<ViewWrapper footerComponent={buttonUpdate}>
<NewWrapper footerComponent={buttonUpdate}>
<StackCustom gap={"xs"}>
<TextInputCustom
required
@@ -471,7 +472,7 @@ export default function PortofolioEdit() {
/>
<Spacing />
</StackCustom>
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -1,28 +1,9 @@
import { TextCustom, ViewWrapper } from "@/components";
import Portofolio_BoxView from "@/screens/Portofolio/BoxPortofolioView";
import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import ViewListPortofolio from "@/screens/Portofolio/ViewListPortofolio";
export default function ListPortofolio() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any[]>([]);
useFocusEffect(
useCallback(() => {
onLoadPortofolio(id as string);
}, [id])
);
const onLoadPortofolio = async (id: string) => {
const response = await apiGetPortofolio({ id: id });
setData(response.data);
};
return (
<ViewWrapper>
{data ? data?.map((item: any, index: number) => (
<Portofolio_BoxView key={index} data={item} />
)) : <TextCustom>Tidak ada portofolio</TextCustom>}
</ViewWrapper>
<>
<ViewListPortofolio />
</>
);
}

View File

@@ -139,7 +139,9 @@ const ButtonnDot = ({
isUserCheck: boolean;
logout: () => Promise<void>;
}) => {
const isId = id === undefined || id === null;
console.log("[ID] >>", id);
const isId = id === undefined || id === "undefined";
if (isId) {
return (

View File

@@ -36,7 +36,7 @@ export default function ProfileLayout() {
<Stack.Screen
name="[id]/blocked-list"
options={{ title: "Blocked List", headerLeft: () => <BackButton /> }}
options={{ title: "Daftar Blokir", headerLeft: () => <BackButton /> }}
/>
<Stack.Screen

View File

@@ -0,0 +1,75 @@
import {
ButtonCustom,
NewWrapper,
StackCustom,
TextInputCustom,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiNotificationsSend } from "@/service/api-notifications";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function TestNotification() {
const { user } = useAuth();
const [data, setData] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
console.log("[Data Dikirim]", data);
setLoading(true);
const response = await apiNotificationsSend({
data: {
title: "Test Notification !!",
body: data,
userLoginId: user?.id || "",
appId: "hipmi",
status: "publish",
kategoriApp: "JOB",
type: "announcement",
deepLink: "/job/cmhjz8u3h0005cfaxezyeilrr",
},
});
if (response.success) {
console.log("[RES SEND NOTIF]", JSON.stringify(response, null, 2));
Toast.show({
type: "success",
text1: "Notifikasi berhasil dikirim",
});
} else {
Toast.show({
type: "error",
text1: "Gagal mengirim notifikasi",
});
}
} catch (error) {
console.log("[ERROR SEND NOTIF]", error);
Toast.show({
type: "error",
text1: "Gagal mengirim notifikasi",
});
} finally {
setLoading(false);
}
};
return (
<>
<NewWrapper>
<StackCustom>
<TextInputCustom
required
label="Pesan"
placeholder="Masukkan pesan"
value={data}
onChangeText={(text) => setData(text)}
/>
<ButtonCustom onPress={handleSubmit} disabled={loading}>
Kirim
</ButtonCustom>
</StackCustom>
</NewWrapper>
</>
);
}

View File

@@ -1,115 +1,11 @@
import {
AvatarComp,
ClickableCustom,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiAllUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import UserSearchMainView from "@/screens/UserSeach/MainView";
import UserSearchMainView_V2 from "@/screens/UserSeach/MainView_V2";
export default function UserSearch() {
const [data, setData] = useState<any[]>([]);
const [search, setSearch] = useState<string>("");
const [isLoadList, setIsLoadList] = useState(false);
useEffect(() => {
onLoadData(search);
}, [search]);
const onLoadData = async (search: string) => {
try {
setIsLoadList(true);
const response = await apiAllUser({ search: search });
console.log("[DATA USER] >", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("Error fetching data", error);
} finally {
setIsLoadList(false);
}
};
const handleSearch = (search: string) => {
setSearch(search);
onLoadData(search);
};
return (
<>
<ViewWrapper
headerComponent={
<TextInputCustom
value={search}
onChangeText={handleSearch}
iconLeft={
<Ionicons
name="search"
size={ICON_SIZE_SMALL}
color={MainColor.placeholder}
/>
}
placeholder="Cari Pengguna"
borderRadius={50}
containerStyle={{ marginBottom: 0 }}
/>
}
>
<StackCustom>
{isLoadList ? (
<LoaderCustom />
) : !_.isEmpty(data) ? (
data?.map((e, index) => {
return (
<ClickableCustom
key={index}
onPress={() => {
console.log("Ke Profile");
router.push(`/profile/${e?.Profile?.id}`);
}}
>
<Grid>
<Grid.Col span={2}>
<AvatarComp fileId={e?.Profile?.imageId} size="base" />
</Grid.Col>
<Grid.Col span={9}>
<StackCustom gap={"sm"}>
<TextCustom size="large">{e?.username}</TextCustom>
<TextCustom size="small">+{e?.nomor}</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
}}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</ClickableCustom>
);
})
) : (
<TextCustom align="center">Tidak ditemukan</TextCustom>
)}
</StackCustom>
<Spacing height={50} />
</ViewWrapper>
{/* <UserSearchMainView /> */}
<UserSearchMainView_V2 />
</>
);
}

View File

@@ -4,10 +4,34 @@ import {
IconHome,
IconStatus,
} from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles";
import { Tabs } from "expo-router";
import { Tabs, useLocalSearchParams, useNavigation, router } from "expo-router";
import { useLayoutEffect } from "react";
export default function VotingTabsLayout() {
const navigation = useNavigation();
const { from, category } = useLocalSearchParams<{
from?: string;
category?: string;
}>();
console.log("from", from);
console.log("category", category);
// Atur header secara dinamis
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => (
<BackButtonFromNotification
from={from as string}
category={category as string}
/>
),
});
}, [from, router, navigation]);
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen

View File

@@ -12,15 +12,17 @@ import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
"publish"
status || "publish"
);
const [listData, setListData] = useState([]);
@@ -86,8 +88,14 @@ export default function VotingStatus() {
style={{ width: "70%", alignSelf: "center" }}
variant="light"
>
{item?.awalVote && dateTimeView({date: item?.awalVote, withoutTime: true})} -{" "}
{item?.akhirVote && dateTimeView({date: item?.akhirVote, withoutTime: true})}
{item?.awalVote &&
dateTimeView({
date: item?.awalVote,
withoutTime: true,
})}{" "}
-{" "}
{item?.akhirVote &&
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
</BadgeCustom>
</StackCustom>
</BaseBox>

View File

@@ -13,6 +13,7 @@ import {
} from "@/components";
import { IconArchive, IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection";
@@ -22,13 +23,14 @@ import {
apiVotingUpdateData,
} from "@/service/api-client/api-voting";
import { today } from "@/utils/dateTimeView";
import dayjs from "dayjs";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function VotingDetail() {
@@ -119,6 +121,23 @@ export default function VotingDetail() {
setOpenDrawerPublish(false);
};
const now = new Date().toISOString();
const isEventFinished = id && data?.akhirVote && dayjs(data.akhirVote).isBefore(now);
useEffect(() => {
if (isEventFinished) {
router.replace(`/(application)/(user)/voting/${id}/history`);
}
}, [isEventFinished, id]);
if (isEventFinished) {
return (
<ViewWrapper>
<CustomSkeleton />
</ViewWrapper>
);
}
return (
<>
<Stack.Screen

View File

@@ -79,7 +79,7 @@ export default function VotingCreate() {
type: "success",
text1: "Data berhasil disimpan",
});
router.replace("/(application)/(user)/voting/(tabs)/status");
router.replace("/(application)/(user)/voting/(tabs)/status?status=review");
} else {
Toast.show({
type: "error",

View File

@@ -1,12 +1,10 @@
import {
AlertDefaultSystem,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
NewWrapper,
StackCustom,
ViewWrapper,
StackCustom
} from "@/components";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";

View File

@@ -1,16 +1,29 @@
import { BackButton } from "@/components";
import BackgroundNotificationHandler from "@/components/Notification/BackgroundNotificationHandler";
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
import { NotificationProvider } from "@/hooks/use-notification-store";
import { HeaderStyles } from "@/styles/header-styles";
import { Stack } from "expo-router";
export default function ApplicationLayout() {
return (
<>
<NotificationProvider>
<NotificationInitializer />
<BackgroundNotificationHandler />
<ApplicationStack />
</NotificationProvider>
</>
);
}
function ApplicationStack() {
return (
<>
<Stack screenOptions={HeaderStyles}>
<Stack.Screen name="(user)" options={{ headerShown: false }} />
<Stack.Screen name="admin" options={{ headerShown: false }} />
{/* Take Picture */}
<Stack.Screen
name="(image)/take-picture/[id]/index"

View File

@@ -15,6 +15,7 @@ import {
ICON_SIZE_XLARGE,
} from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
import {
adminListMenu,
superAdminListMenu,
@@ -192,11 +193,12 @@ export default function AdminLayout() {
label: "Notifikasi",
value: "notification",
icon: (
<Ionicons
name="notifications"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
// <Ionicons
// name="notifications"
// size={ICON_SIZE_SMALL}
// color={MainColor.white}
// />
<AdminNotificationBell/>
),
path: "/admin/notification",
},

View File

@@ -1,4 +1,5 @@
import {
AlertDefaultSystem,
BadgeCustom,
BaseBox,
BoxButtonOnFooter,
@@ -9,6 +10,7 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminDonationInvoiceDetailById,
apiAdminDonationInvoiceUpdateById,
@@ -22,6 +24,7 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminDonasiTransactionDetail() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
console.log("[STATUS]", id, status);
@@ -33,7 +36,7 @@ export default function AdminDonasiTransactionDetail() {
onLoadData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -57,6 +60,7 @@ export default function AdminDonasiTransactionDetail() {
const newData = {
donationId: data?.donasiId,
nominal: data?.nominal,
senderId: user?.id,
};
const response = await apiAdminDonationInvoiceUpdateById({
@@ -97,7 +101,15 @@ export default function AdminDonasiTransactionDetail() {
<ButtonCustom
isLoading={isLoading}
onPress={() => {
AlertDefaultSystem({
title: "Konfirmasi transaksi",
message: "Apakah anda yakin ingin menyetujui transaksi ini?",
textLeft: "Tidak",
textRight: "Ya",
onPressRight: () => {
handlerSubmit();
},
});
}}
>
Terima donasi
@@ -140,7 +152,7 @@ export default function AdminDonasiTransactionDetail() {
})}
>
{_.startCase(
(data?.DonasiMaster_StatusInvoice?.name as any) || "-"
(data?.DonasiMaster_StatusInvoice?.name as any) || "-",
)}
</BadgeCustom>
)) ||
@@ -157,7 +169,7 @@ export default function AdminDonasiTransactionDetail() {
<ButtonCustom
onPress={() =>
router.push(
`/(application)/(image)/preview-image/${data?.imageId}`
`/(application)/(image)/preview-image/${data?.imageId}`,
)
}
>

View File

@@ -14,7 +14,11 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiAdminDonationDetailById, apiAdminDonationDisbursementOfFundsCreated } from "@/service/api-admin/api-admin-donation";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminDonationDetailById,
apiAdminDonationDisbursementOfFundsCreated,
} from "@/service/api-admin/api-admin-donation";
import { uploadFileService } from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile from "@/utils/pickFile";
@@ -25,7 +29,7 @@ import Toast from "react-native-toast-message";
export default function AdminDonationDisbursementOfFunds() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [data, setData] = React.useState<any | null>(null);
const [isLoading, setIsLoading] = React.useState(false);
@@ -40,7 +44,7 @@ export default function AdminDonationDisbursementOfFunds() {
useFocusEffect(
React.useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -94,6 +98,7 @@ export default function AdminDonationDisbursementOfFunds() {
const newData = {
...value,
authorId: user?.id,
imageId: imageId,
};

View File

@@ -7,15 +7,15 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import { useAuth } from "@/hooks/use-auth";
import { funUpdateStatusDonation } from "@/screens/Admin/Donation/funDonationUpdateStatus";
import {
apiAdminDonationDetailById
} from "@/service/api-admin/api-admin-donation";
import { apiAdminDonationDetailById } from "@/service/api-admin/api-admin-donation";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import React from "react";
import Toast from "react-native-toast-message";
export default function AdminDonationRejectInput() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [data, setData] = React.useState<any | null>(null);
@@ -24,7 +24,7 @@ export default function AdminDonationRejectInput() {
useFocusEffect(
React.useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -48,11 +48,23 @@ export default function AdminDonationRejectInput() {
changeStatus: "publish" | "review" | "reject";
}) => {
try {
if (!user?.id) {
Toast.show({
type: "error",
text1: "User tidak ditemukan",
});
return;
}
setIsLoading(true);
const response = await funUpdateStatusDonation({
id: id as string,
changeStatus,
data: data,
data: {
senderId: user?.id as string,
catatan: data,
},
});
if (!response.success) {
@@ -61,7 +73,7 @@ export default function AdminDonationRejectInput() {
text1: "Report gagal",
});
return
return;
}
Toast.show({

View File

@@ -41,8 +41,8 @@ export default function AdminEventDetail() {
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;
const isDevLink =
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
useFocusEffect(
useCallback(() => {
@@ -126,6 +126,7 @@ export default function AdminEventDetail() {
const response = await funUpdateStatusEvent({
id: id as string,
changeStatus: "publish",
data: { catatan: "", senderId: user?.id as string },
});
if (!response.success) {

View File

@@ -7,6 +7,7 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import { useAuth } from "@/hooks/use-auth";
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -14,9 +15,13 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminEventRejectInput() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any>("");
const [data, setData] = useState<any>({
catatan: "",
senderId: "",
});
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
@@ -45,10 +50,16 @@ export default function AdminEventRejectInput() {
}) => {
try {
setIsLoading(true);
const newData = {
catatan: data,
senderId: user?.id as string,
};
const response = await funUpdateStatusEvent({
id: id as string,
changeStatus,
data: data,
data: newData,
});
if (!response.success) {

View File

@@ -15,12 +15,11 @@ import { IconDot, IconView } from "@/components/_Icon/IconComponent";
import { IconTrash } from "@/components/_Icon/IconTrash";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminForumCommentById,
apiAdminForumDeactivateComment,
@@ -35,6 +34,7 @@ import Toast from "react-native-toast-message";
export default function AdminForumReportComment() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [data, setData] = useState<any | null>(null);
const [listReport, setListReport] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
@@ -113,7 +113,11 @@ export default function AdminForumReportComment() {
<StackCustom gap={"sm"}>
<GridSpan_NewComponent
text1={<TextCustom bold align="center">Aksi</TextCustom>}
text1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
text2={<TextCustom bold>Pelapor</TextCustom>}
text3={<TextCustom bold>Kategori Report</TextCustom>}
/>
@@ -131,7 +135,9 @@ export default function AdminForumReportComment() {
text1={
<CenterCustom>
<ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
@@ -188,15 +194,18 @@ export default function AdminForumReportComment() {
onPressRight: async () => {
const deleteComment = await apiAdminForumDeactivateComment({
id: id as string,
data: {
senderId: user?.id as string,
},
});
if (!deleteComment.success) {
Toast.show({
type: "error",
text1: "Komentar gagal dihapus",
});
return;
}
// if (!deleteComment.success) {
// Toast.show({
// type: "error",
// text1: "Komentar gagal dihapus",
// });
// return;
// }
setOpenDrawer(false);
Toast.show({

View File

@@ -16,12 +16,11 @@ import { IconDot, IconView } from "@/components/_Icon/IconComponent";
import { IconTrash } from "@/components/_Icon/IconTrash";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminForumDeactivatePosting,
apiAdminForumListReportPostingById,
@@ -35,6 +34,7 @@ import { Divider } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function AdminForumReportPosting() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawerPage, setOpenDrawerPage] = useState(false);
const [openDrawerAction, setOpenDrawerAction] = useState(false);
@@ -215,6 +215,9 @@ export default function AdminForumReportPosting() {
onPressRight: async () => {
const response = await apiAdminForumDeactivatePosting({
id: id as string,
data: {
senderId: user?.id as string,
},
});
if (!response.success) {

View File

@@ -73,7 +73,7 @@ export default function AdminForumReportPosting() {
<GridSpan_NewComponent
text1={
<TextCustom bold truncate>
Username
Pelapor
</TextCustom>
}
text2={

View File

@@ -20,6 +20,7 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
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 CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
@@ -28,6 +29,7 @@ import {
apiAdminInvestmentDetailById,
} from "@/service/api-admin/api-admin-investment";
import { colorBadgeStatus } from "@/utils/colorBadge";
import { countDownAndCondition } from "@/utils/countDownAndCondition";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
@@ -40,91 +42,41 @@ export default function AdminInvestmentDetail() {
const [data, setData] = React.useState<any | null>(null);
const [isLoading, setLoading] = React.useState(false);
const [remind, setRemind] = React.useState({
sisa: 0,
reminder: false,
});
useFocusEffect(
React.useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
try {
const response = await apiAdminInvestmentDetailById({ id: id as string });
// console.log("[GETONE INVEST]", JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
const duration = response?.data?.MasterPencarianInvestor?.name;
const publishTime = response?.data?.countDown;
const countDown = countDownAndCondition({
duration: duration,
publishTime: publishTime
});
setRemind({
sisa: countDown.durationDay,
reminder: countDown.reminder,
});
}
} catch (error) {
console.log(error);
console.log("Error", error);
}
};
const listData = [
{
label: "Username",
value: (data && data?.author?.username) || "-",
},
{
label: "Judul",
value: (data && data?.title) || "-",
},
{
label: "Status",
value:
data && data?.MasterStatusInvestasi?.name ? (
<BadgeCustom
color={colorBadgeStatus({
status: data?.MasterStatusInvestasi?.name as string,
})}
>
{_.startCase(data?.MasterStatusInvestasi?.name as string)}
</BadgeCustom>
) : (
"-"
),
},
{
label: "Dana Dibutuhkan",
value: `Rp. ${
(data && data?.targetDana && formatCurrencyDisplay(data?.targetDana)) ||
"-"
}`,
},
{
label: "Harga Perlembar",
value: `Rp. ${
(data &&
data?.hargaLembar &&
formatCurrencyDisplay(data?.hargaLembar)) ||
"-"
}`,
},
{
label: "Total Lembar",
value:
(data &&
data?.totalLembar &&
formatCurrencyDisplay(data?.totalLembar)) ||
"-",
},
{
label: "ROI",
value: `${(data && data?.roi && data?.roi) || 0} %`,
},
{
label: "Pembagian Deviden",
value: (data && data?.MasterPembagianDeviden?.name) + " bulan" || "-",
},
{
label: "Jadwal Pembagian",
value: (data && data?.MasterPeriodeDeviden?.name) || "-",
},
{
label: "Pencarian Investor",
value: (data && data?.MasterPencarianInvestor?.name) + " hari" || "-",
},
];
const handlerSubmitPublish = async () => {
try {
setLoading(true);
@@ -134,7 +86,6 @@ export default function AdminInvestmentDetail() {
data: data,
});
// console.log("[GET ON INVEST]", JSON.stringify(response, null, 2));
if (!response.success) {
Toast.show({
type: "error",
@@ -164,6 +115,16 @@ export default function AdminInvestmentDetail() {
/>
);
if (!data) {
return (
<>
<ViewWrapper>
<CustomSkeleton height={200} />
</ViewWrapper>
</>
);
}
return (
<>
<ViewWrapper
@@ -177,8 +138,8 @@ export default function AdminInvestmentDetail() {
{status === "publish" && (
<BaseBox>
<ProgressCustom
label={data && `${data.progress}%` || "0%"}
value={data && data.progress || 0}
label={(data && `${data.progress}%`) || "0%"}
value={(data && data.progress) || 0}
size="lg"
/>
<Spacing />
@@ -187,7 +148,8 @@ export default function AdminInvestmentDetail() {
label={<TextCustom bold>Sisa Saham</TextCustom>}
value={
<TextCustom>
{data && formatCurrencyDisplay(data && data?.sisaLembar)} lembar
{data && formatCurrencyDisplay(data && data?.sisaLembar)}{" "}
lembar
</TextCustom>
}
/>
@@ -206,13 +168,15 @@ export default function AdminInvestmentDetail() {
<BaseBox>
<StackCustom>
<DummyLandscapeImage imageId={data?.imageId} />
{listData.map((item, i) => (
{listData({ data: data, reminder: remind.reminder })?.map(
(item, i) => (
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}
/>
))}
),
)}
</StackCustom>
</BaseBox>
@@ -230,7 +194,7 @@ export default function AdminInvestmentDetail() {
}
onPress={() => {
router.push(
`/(application)/(file)/${data?.prospektusFileId}`
`/(application)/(file)/${data?.prospektusFileId}`,
);
}}
>
@@ -259,7 +223,7 @@ export default function AdminInvestmentDetail() {
}
onPress={() => {
router.push(
`/(application)/(file)/${item?.fileId}`
`/(application)/(file)/${item?.fileId}`,
);
}}
>
@@ -299,8 +263,8 @@ export default function AdminInvestmentDetail() {
onReject={() => {
router.push(
`/admin/investment/${id}/reject-input?status=${_.lowerCase(
data?.MasterStatusInvestasi?.name
)}`
data?.MasterStatusInvestasi?.name,
)}`,
);
}}
/>
@@ -343,3 +307,67 @@ export default function AdminInvestmentDetail() {
</>
);
}
const listData = ({ data, reminder }: { data: any; reminder: boolean }) => [
{
label: "Username",
value: (data && data?.author?.username) || "-",
},
{
label: "Judul",
value: (data && data?.title) || "-",
},
{
label: "Status",
value:
data && data?.MasterStatusInvestasi?.name ? (
<BadgeCustom
color={colorBadgeStatus({
status: reminder ? "periode berakhir" : "publish",
})}
>
{reminder
? "Periode Berakhir"
: _.startCase(data?.MasterStatusInvestasi?.name as string)}
</BadgeCustom>
) : (
"-"
),
},
{
label: "Dana Dibutuhkan",
value: `Rp. ${
(data && data?.targetDana && formatCurrencyDisplay(data?.targetDana)) ||
"-"
}`,
},
{
label: "Harga Perlembar",
value: `Rp. ${
(data && data?.hargaLembar && formatCurrencyDisplay(data?.hargaLembar)) ||
"-"
}`,
},
{
label: "Total Lembar",
value:
(data && data?.totalLembar && formatCurrencyDisplay(data?.totalLembar)) ||
"-",
},
{
label: "ROI",
value: `${(data && data?.roi && data?.roi) || 0} %`,
},
{
label: "Pembagian Deviden",
value: (data && data?.MasterPembagianDeviden?.name) + " bulan" || "-",
},
{
label: "Jadwal Pembagian",
value: (data && data?.MasterPeriodeDeviden?.name) || "-",
},
{
label: "Pencarian Investor",
value: (data && data?.MasterPencarianInvestor?.name) + " hari" || "-",
},
];

View File

@@ -13,6 +13,7 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminInvestmentGetOneInvoiceById,
apiAdminInvestmentUpdateInvoice,
@@ -25,6 +26,7 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminInvestmentTransactionDetail() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [isLoading, setLoading] = useState<boolean>(false);
@@ -32,7 +34,7 @@ export default function AdminInvestmentTransactionDetail() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -40,7 +42,6 @@ export default function AdminInvestmentTransactionDetail() {
const response = await apiAdminInvestmentGetOneInvoiceById({
id: id as string,
});
// console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
}
@@ -92,7 +93,7 @@ export default function AdminInvestmentTransactionDetail() {
<ButtonCustom
onPress={() =>
router.push(
`/(application)/(image)/preview-image/${data?.imageId}`
`/(application)/(image)/preview-image/${data?.imageId}`,
)
}
>
@@ -109,6 +110,13 @@ export default function AdminInvestmentTransactionDetail() {
}: {
category: "accept" | "deny";
}) => {
if (!user?.id) {
Toast.show({
type: "error",
text1: "Gagal update status transaksi",
});
return;
}
try {
setLoading(true);
const response = await apiAdminInvestmentUpdateInvoice({
@@ -117,11 +125,10 @@ export default function AdminInvestmentTransactionDetail() {
data: {
investasiId: data?.investasiId,
lembarTerbeli: data?.lembarTerbeli,
senderId: user?.id as any,
},
});
// console.log("[RESPONSE SUBMIT]", JSON.stringify(response, null, 2));
if (!response.success) {
Toast.show({
type: "error",
@@ -153,6 +160,7 @@ export default function AdminInvestmentTransactionDetail() {
styleRight={{ paddingLeft: 10 }}
leftIcon={
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
backgroundColor={MainColor.red}
textColor="white"
@@ -175,6 +183,7 @@ export default function AdminInvestmentTransactionDetail() {
}
rightIcon={
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => {
AlertDefaultSystem({
@@ -198,8 +207,8 @@ export default function AdminInvestmentTransactionDetail() {
} else if (data?.StatusInvoice?.name === "Gagal") {
return (
<>
<ButtonCustom textColor="red" onPress={() => router.back()}>
Gagal
<ButtonCustom disabled onPress={() => router.back()}>
Transaksi telah gagal
</ButtonCustom>
</>
);

View File

@@ -7,16 +7,21 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import { apiAdminInvestasiUpdateByStatus, apiAdminInvestmentDetailById } from "@/service/api-admin/api-admin-investment";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminInvestasiUpdateByStatus,
apiAdminInvestmentDetailById,
} from "@/service/api-admin/api-admin-investment";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminInvestmentRejectInput() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
console.log("[STATUS]", status);
const [value, setValue] = useState<any | null>(null);
const [isLoading , setLoading] = useState(false)
const [isLoading, setLoading] = useState(false);
useFocusEffect(
useCallback(() => {
@@ -45,12 +50,23 @@ export default function AdminInvestmentRejectInput() {
return;
}
if (!user?.id) {
Toast.show({
type: "error",
text1: "User tidak ditemukan",
});
return;
}
try {
setLoading(true)
setLoading(true);
const response = await apiAdminInvestasiUpdateByStatus({
id: id as string,
status: "reject",
data: value,
data: {
catatan: value,
senderId: user?.id as string,
},
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
@@ -76,7 +92,7 @@ export default function AdminInvestmentRejectInput() {
} catch (error) {
console.error(["ERROR"], error);
} finally {
setLoading(false)
setLoading(false);
}
};

View File

@@ -22,8 +22,6 @@ import { Divider } from "react-native-paper";
export default function AdminInvestmentStatus() {
const { status } = useLocalSearchParams();
console.log("[STATUS]", status);
const [listData, setListData] = React.useState<any[] | null>(null);
const [loadData, setLoadingData] = React.useState(false);
const [search, setSearch] = React.useState("");
@@ -41,7 +39,7 @@ export default function AdminInvestmentStatus() {
category: status as "publish" | "review" | "reject",
search,
});
console.log("[LIST DATA]", JSON.stringify(response, null, 2));
if (response.success) {
setListData(response.data);
}

View File

@@ -15,6 +15,7 @@ import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import funUpdateStatusJob from "@/screens/Admin/Job/funUpdateStatus";
import { apiAdminJobGetById } from "@/service/api-admin/api-admin-job";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -23,8 +24,10 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminJobDetailStatus() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
@@ -92,6 +95,9 @@ export default function AdminJobDetailStatus() {
const response = await funUpdateStatusJob({
id: id as string,
changeStatus,
data: {
senderId: user?.id as string,
},
});
if (!response.success) {
@@ -142,12 +148,15 @@ export default function AdminJobDetailStatus() {
</StackCustom>
</BaseBox>
{data && data?.catatan && (status === "reject" || status === "review") && (
{data &&
data?.catatan &&
(status === "reject" || status === "review") && (
<ReportBox text={data?.catatan} />
)}
{status === "review" && (
<AdminButtonReview
isLoading={isLoading}
onPublish={() => {
AlertDefaultSystem({
title: "Publish",
@@ -156,6 +165,7 @@ export default function AdminJobDetailStatus() {
textRight: "Ya",
onPressRight: () => {
handleUpdate({ changeStatus: "publish" });
setIsLoading(true);
},
});
}}

View File

@@ -15,7 +15,10 @@ import Toast from "react-native-toast-message";
export default function AdminJobRejectInput() {
const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [data, setData] = useState({
catatan: "",
senderId: ""
});
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
@@ -102,8 +105,8 @@ export default function AdminJobRejectInput() {
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
>
<TextAreaCustom
value={data}
onChangeText={setData}
value={data?.catatan}
onChangeText={(text) => setData({ ...data, catatan: text })}
placeholder="Masukan alasan"
required
showCount

View File

@@ -1,20 +1,213 @@
import { BackButton, TextCustom, ViewWrapper } from "@/components";
import { Stack } from "expo-router";
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find(
(c) => c.value === value
);
return category?.label;
};
const BoxNotification = ({
data,
activeCategory,
}: {
data: any;
activeCategory: string | null;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
console.log(
"Notification >",
selectedCategory(activeCategory as string)
);
router.push(data.deepLink);
markAsRead(data.id);
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function AdminNotification() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
useFocusEffect(
useCallback(() => {
fecthData();
}, [activeCategory])
);
const fecthData = async () => {
try {
setLoading(true);
const response = await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
});
if (response.success) {
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("Error Notification", error);
} finally {
setLoading(false);
}
};
const onRefresh = () => {
setRefreshing(true);
fecthData();
setRefreshing(false);
};
return (
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => <></>,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<ViewWrapper>
<TextCustom>Notification</TextCustom>
</ViewWrapper>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
path: "",
},
]}
onPressItem={(item: any) => {
console.log("Item", item.value);
if (item.value === "read-all") {
AlertDefaultSystem({
title: "Tandai Semua Dibaca",
message:
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => {
markAsReadAll(user?.id as any);
const data = _.cloneDeep(listData);
data.forEach((e) => {
e.isRead = true;
});
setListData(data);
onRefresh();
setOpenDrawer(false);
},
});
}
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -48,6 +48,7 @@ export default function SuperAdminDetail() {
const response = await apiAdminUserAccessUpdateStatus({
id: id as string,
role: data?.masterUserRoleId === "2" ? "user" : "admin",
category: "role"
});
if (!response.success) {

View File

@@ -9,6 +9,7 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { useAuth } from "@/hooks/use-auth";
import {
apiAdminUserAccessGetById,
apiAdminUserAccessUpdateStatus,
@@ -18,6 +19,7 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminUserAccessDetail() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [loadData, setLoadData] = useState(false);
@@ -33,6 +35,7 @@ export default function AdminUserAccessDetail() {
try {
setLoadData(true);
const response = await apiAdminUserAccessGetById({ id: id as string });
console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
@@ -48,6 +51,7 @@ export default function AdminUserAccessDetail() {
const response = await apiAdminUserAccessUpdateStatus({
id: id as string,
active: !data?.active,
category: "access",
});
if (!response.success) {
@@ -61,6 +65,7 @@ export default function AdminUserAccessDetail() {
type: "success",
text1: "Update aktifasi berhasil ",
});
router.back();
} catch (error) {
console.log("[ERROR UPDATE STATUS]", error);

View File

@@ -16,6 +16,7 @@ import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus";
import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting";
import { colorBadgeStatus } from "@/utils/colorBadge";
@@ -29,6 +30,7 @@ import { List } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function AdminVotingDetail() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false);
@@ -139,6 +141,10 @@ export default function AdminVotingDetail() {
const response = await funUpdateStatusVoting({
id: id as string,
changeStatus,
data: {
senderId: user?.id as string,
catatan: "",
},
});
if (!response.success) {

View File

@@ -7,6 +7,7 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import { useAuth } from "@/hooks/use-auth";
import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus";
import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -14,6 +15,7 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function AdminVotingRejectInput() {
const { user } = useAuth()
const { id, status } = useLocalSearchParams();
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -48,7 +50,10 @@ export default function AdminVotingRejectInput() {
const response = await funUpdateStatusVoting({
id: id as string,
changeStatus,
data: data,
data: {
catatan: data,
senderId: user?.id as string,
},
});
if (!response.success) {

View File

@@ -1,16 +1,26 @@
import { StackCustom, TextCustom, ViewWrapper } from "@/components";
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
import { Stack } from "expo-router";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen
options={{ headerShown: true, title: "", headerLeft: () => <BackButton /> }}
/>
<ViewWrapper>
<StackCustom align="center" gap={0} style={{justifyContent: "center", alignItems: "center", flex: 1}}>
<StackCustom
align="center"
gap={0}
style={{ justifyContent: "center", alignItems: "center", flex: 1 }}
>
<TextCustom size="large" bold style={{ fontSize: 100 }}>
404
</TextCustom>
<TextCustom size="large" bold>
Sorry, File Not Found
Sorry, Page Not Found
</TextCustom>
</StackCustom>
</ViewWrapper>
</>
);
}

View File

@@ -1,32 +1,10 @@
import { AuthProvider } from "@/context/AuthContext";
import AppRoot from "@/screens/RootLayout/AppRoot";
import { useEffect } from "react";
import "react-native-gesture-handler";
import { SafeAreaProvider } from "react-native-safe-area-context";
import Toast from "react-native-toast-message";
import messaging from "@react-native-firebase/messaging";
export default function RootLayout() {
useEffect(() => {
const testFCM = async () => {
if (!messaging().isSupported()) {
console.warn("Firebase Messaging not supported (e.g. Expo Go)");
return;
}
const authStatus = await messaging().requestPermission();
if (authStatus !== messaging.AuthorizationStatus.AUTHORIZED) {
console.warn("Permission not granted");
return;
}
const token = await messaging().getToken();
console.log("✅ FCM Token:", token);
};
testFCM();
}, []);
return (
<>
<SafeAreaProvider>

9
app/eula.tsx Normal file
View File

@@ -0,0 +1,9 @@
import EULAView from "@/screens/Authentication/EULAView";
export default function EULA() {
return (
<>
<EULAView />
</>
);
}

263
bun.lock
View File

@@ -68,6 +68,7 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@react-native-community/cli": "^20.0.2",
"@react-native/metro-config": "^0.83.1",
"@types/react": "~19.1.10",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
@@ -707,9 +708,9 @@
"@react-native/assets-registry": ["@react-native/assets-registry@0.81.4", "", {}, "sha512-AMcDadefBIjD10BRqkWw+W/VdvXEomR6aEZ0fhQRAv7igrBzb4PTn4vHKYg+sUK0e3wa74kcMy2DLc/HtnGcMA=="],
"@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.79.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.79.5" } }, "sha512-Rt/imdfqXihD/sn0xnV4flxxb1aLLjPtMF1QleQjEhJsTUPpH4TFlfOpoCvsrXoDl4OIcB1k4FVM24Ez92zf5w=="],
"@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.83.1" } }, "sha512-VPj8O3pG1ESjZho9WVKxqiuryrotAECPHGF5mx46zLUYNTWR5u9OMUXYk7LeLy+JLWdGEZ2Gn3KoXeFZbuqE+g=="],
"@react-native/babel-preset": ["@react-native/babel-preset@0.79.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.79.5", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-GDUYIWslMLbdJHEgKNfrOzXk8EDKxKzbwmBXUugoiSlr6TyepVZsj3GZDLEFarOcTwH1EXXHJsixihk8DCRQDA=="],
"@react-native/babel-preset": ["@react-native/babel-preset@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.83.1", "babel-plugin-syntax-hermes-parser": "0.32.0", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-xI+tbsD4fXcI6PVU4sauRCh0a5fuLQC849SINmU2J5wP8kzKu4Ye0YkGjUW3mfGrjaZcjkWmF6s33jpyd3gdTw=="],
"@react-native/codegen": ["@react-native/codegen@0.81.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.29.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-LWTGUTzFu+qOQnvkzBP52B90Ym3stZT8IFCzzUrppz8Iwglg83FCtDZAR4yLHI29VY/x/+pkcWAMCl3739XHdw=="],
@@ -721,7 +722,11 @@
"@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.81.4", "", {}, "sha512-T7fPcQvDDCSusZFVSg6H1oVDKb/NnVYLnsqkcHsAF2C2KGXyo3J7slH/tJAwNfj/7EOA2OgcWxfC1frgn9TQvw=="],
"@react-native/js-polyfills": ["@react-native/js-polyfills@0.81.4", "", {}, "sha512-sr42FaypKXJHMVHhgSbu2f/ZJfrLzgaoQ+HdpRvKEiEh2mhFf6XzZwecyLBvWqf2pMPZa+CpPfNPiejXjKEy8w=="],
"@react-native/js-polyfills": ["@react-native/js-polyfills@0.83.1", "", {}, "sha512-qgPpdWn/c5laA+3WoJ6Fak8uOm7CG50nBsLlPsF8kbT7rUHIVB9WaP6+GPsoKV/H15koW7jKuLRoNVT7c3Ht3w=="],
"@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@react-native/babel-preset": "0.83.1", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-fqt6DHWX1GBGDKa5WJOjDtPPy2M9lkYVLn59fBeFQ0GXhBRzNbUh8JzWWI/Q2CLDZ2tgKCcwaiXJ1OHWVd2BCQ=="],
"@react-native/metro-config": ["@react-native/metro-config@0.83.1", "", { "dependencies": { "@react-native/js-polyfills": "0.83.1", "@react-native/metro-babel-transformer": "0.83.1", "metro-config": "^0.83.3", "metro-runtime": "^0.83.3" } }, "sha512-1rjYZf62fCm6QAinHmRAKnJxIypX0VF/zBPd0qWvWABMZugrS0eACuIbk9Wk0StBod4yL8KnwEJyg77ak8xYzQ=="],
"@react-native/normalize-colors": ["@react-native/normalize-colors@0.81.4", "", {}, "sha512-9nRRHO1H+tcFqjb9gAM105Urtgcanbta2tuqCVY0NATHeFPDEAB7gPyiLxCHKMi1NbhP6TH0kxgSWXKZl1cyRg=="],
@@ -1585,9 +1590,9 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
"hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"hermes-parser": ["hermes-parser@0.32.0", "", { "dependencies": { "hermes-estree": "0.32.0" } }, "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw=="],
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
@@ -1917,17 +1922,17 @@
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"metro": ["metro@0.83.1", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.29.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.1", "metro-cache": "0.83.1", "metro-cache-key": "0.83.1", "metro-config": "0.83.1", "metro-core": "0.83.1", "metro-file-map": "0.83.1", "metro-resolver": "0.83.1", "metro-runtime": "0.83.1", "metro-source-map": "0.83.1", "metro-symbolicate": "0.83.1", "metro-transform-plugins": "0.83.1", "metro-transform-worker": "0.83.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA=="],
"metro": ["metro@0.83.3", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q=="],
"metro-babel-transformer": ["metro-babel-transformer@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.29.1", "nullthrows": "^1.1.1" } }, "sha512-r3xAD3964E8dwDBaZNSO2aIIvWXjIK80uO2xo0/pi3WI8XWT9h5SCjtGWtMtE5PRWw+t20TN0q1WMRsjvhC1rQ=="],
"metro-cache": ["metro-cache@0.83.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.1" } }, "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ=="],
"metro-cache": ["metro-cache@0.83.3", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.3" } }, "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q=="],
"metro-cache-key": ["metro-cache-key@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-ZUs+GD5CNeDLxx5UUWmfg26IL+Dnbryd+TLqTlZnDEgehkIa11kUSvgF92OFfJhONeXzV4rZDRGNXoo6JT+8Gg=="],
"metro-config": ["metro-config@0.83.1", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.1", "metro-cache": "0.83.1", "metro-core": "0.83.1", "metro-runtime": "0.83.1" } }, "sha512-HJhpZx3wyOkux/jeF1o7akFJzZFdbn6Zf7UQqWrvp7gqFqNulQ8Mju09raBgPmmSxKDl4LbbNeigkX0/nKY1QA=="],
"metro-config": ["metro-config@0.83.3", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.3", "metro-cache": "0.83.3", "metro-core": "0.83.3", "metro-runtime": "0.83.3", "yaml": "^2.6.1" } }, "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA=="],
"metro-core": ["metro-core@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.1" } }, "sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q=="],
"metro-core": ["metro-core@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.3" } }, "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw=="],
"metro-file-map": ["metro-file-map@0.83.1", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-Yu429lnexKl44PttKw3nhqgmpBR+6UQ/tRaYcxPeEShtcza9DWakCn7cjqDTQZtWR2A8xSNv139izJMyQ4CG+w=="],
@@ -1935,7 +1940,7 @@
"metro-resolver": ["metro-resolver@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g=="],
"metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="],
"metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="],
"metro-source-map": ["metro-source-map@0.83.1", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.1", "nullthrows": "^1.1.1", "ob1": "0.83.1", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-De7Vbeo96fFZ2cqmI0fWwVJbtHIwPZv++LYlWSwzTiCzxBDJORncN0LcT48Vi2UlQLzXJg+/CuTAcy7NBVh69A=="],
@@ -2717,10 +2722,22 @@
"@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
"@expo/metro/metro": ["metro@0.83.1", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.29.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.1", "metro-cache": "0.83.1", "metro-cache-key": "0.83.1", "metro-config": "0.83.1", "metro-core": "0.83.1", "metro-file-map": "0.83.1", "metro-resolver": "0.83.1", "metro-runtime": "0.83.1", "metro-source-map": "0.83.1", "metro-symbolicate": "0.83.1", "metro-transform-plugins": "0.83.1", "metro-transform-worker": "0.83.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA=="],
"@expo/metro/metro-cache": ["metro-cache@0.83.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.1" } }, "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ=="],
"@expo/metro/metro-config": ["metro-config@0.83.1", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.1", "metro-cache": "0.83.1", "metro-core": "0.83.1", "metro-runtime": "0.83.1" } }, "sha512-HJhpZx3wyOkux/jeF1o7akFJzZFdbn6Zf7UQqWrvp7gqFqNulQ8Mju09raBgPmmSxKDl4LbbNeigkX0/nKY1QA=="],
"@expo/metro/metro-core": ["metro-core@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.1" } }, "sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q=="],
"@expo/metro/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="],
"@expo/metro-config/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@expo/metro-config/@expo/json-file": ["@expo/json-file@10.0.7", "", { "dependencies": { "@babel/code-frame": "~7.10.4", "json5": "^2.2.3" } }, "sha512-z2OTC0XNO6riZu98EjdNHC05l51ySeTto6GP7oSQrCvQgG9ARBwD1YvMQaVZ9wU7p/4LzSf1O7tckL3B45fPpw=="],
"@expo/metro-config/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"@expo/metro-config/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@expo/npm-proofread/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
@@ -2785,28 +2802,24 @@
"@react-native-community/cli-tools/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.79.5", "", { "dependencies": { "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ=="],
"@react-native/babel-plugin-codegen/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
"@react-native/babel-preset/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA=="],
"@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.83.1", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", "glob": "^7.1.1", "hermes-parser": "0.32.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" } }, "sha512-FpRxenonwH+c2a5X5DZMKUD7sCudHxB3eSQPgV9R+uxd28QWslyAWrpnJM/Az96AEksHnymDzEmzq2HLX5nb+g=="],
"@react-native/babel-preset/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.27.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ=="],
"@react-native/babel-preset/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA=="],
"@react-native/babel-preset/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.27.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA=="],
"@react-native/babel-preset/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.27.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.27.3", "@babel/plugin-transform-parameters": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q=="],
"@react-native/babel-preset/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg=="],
"@react-native/babel-preset/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.27.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q=="],
"@react-native/babel-preset/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.25.1", "", { "dependencies": { "hermes-parser": "0.25.1" } }, "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ=="],
"@react-native/babel-preset/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.32.0", "", { "dependencies": { "hermes-parser": "0.32.0" } }, "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg=="],
"@react-native/codegen/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@react-native/codegen/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"@react-native/community-cli-plugin/metro": ["metro@0.83.1", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.29.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.1", "metro-cache": "0.83.1", "metro-cache-key": "0.83.1", "metro-config": "0.83.1", "metro-core": "0.83.1", "metro-file-map": "0.83.1", "metro-resolver": "0.83.1", "metro-runtime": "0.83.1", "metro-source-map": "0.83.1", "metro-symbolicate": "0.83.1", "metro-transform-plugins": "0.83.1", "metro-transform-worker": "0.83.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA=="],
"@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.1", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.1", "metro-cache": "0.83.1", "metro-core": "0.83.1", "metro-runtime": "0.83.1" } }, "sha512-HJhpZx3wyOkux/jeF1o7akFJzZFdbn6Zf7UQqWrvp7gqFqNulQ8Mju09raBgPmmSxKDl4LbbNeigkX0/nKY1QA=="],
"@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.1" } }, "sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q=="],
"@react-native/community-cli-plugin/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
@@ -2849,10 +2862,14 @@
"babel-plugin-react-compiler/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"babel-preset-expo/@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.27.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-transform-destructuring": "^7.27.3", "@babel/plugin-transform-parameters": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q=="],
"babel-preset-expo/@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg=="],
"babel-preset-expo/@react-native/babel-preset": ["@react-native/babel-preset@0.79.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/plugin-proposal-export-default-from": "^7.24.7", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-default-from": "^7.24.7", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-optional-chaining": "^7.8.3", "@babel/plugin-transform-arrow-functions": "^7.24.7", "@babel/plugin-transform-async-generator-functions": "^7.25.4", "@babel/plugin-transform-async-to-generator": "^7.24.7", "@babel/plugin-transform-block-scoping": "^7.25.0", "@babel/plugin-transform-class-properties": "^7.25.4", "@babel/plugin-transform-classes": "^7.25.4", "@babel/plugin-transform-computed-properties": "^7.24.7", "@babel/plugin-transform-destructuring": "^7.24.8", "@babel/plugin-transform-flow-strip-types": "^7.25.2", "@babel/plugin-transform-for-of": "^7.24.7", "@babel/plugin-transform-function-name": "^7.25.1", "@babel/plugin-transform-literals": "^7.25.2", "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", "@babel/plugin-transform-modules-commonjs": "^7.24.8", "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", "@babel/plugin-transform-numeric-separator": "^7.24.7", "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/plugin-transform-optional-catch-binding": "^7.24.7", "@babel/plugin-transform-optional-chaining": "^7.24.8", "@babel/plugin-transform-parameters": "^7.24.7", "@babel/plugin-transform-private-methods": "^7.24.7", "@babel/plugin-transform-private-property-in-object": "^7.24.7", "@babel/plugin-transform-react-display-name": "^7.24.7", "@babel/plugin-transform-react-jsx": "^7.25.2", "@babel/plugin-transform-react-jsx-self": "^7.24.7", "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@babel/plugin-transform-regenerator": "^7.24.7", "@babel/plugin-transform-runtime": "^7.24.7", "@babel/plugin-transform-shorthand-properties": "^7.24.7", "@babel/plugin-transform-spread": "^7.24.7", "@babel/plugin-transform-sticky-regex": "^7.24.7", "@babel/plugin-transform-typescript": "^7.25.2", "@babel/plugin-transform-unicode-regex": "^7.24.7", "@babel/template": "^7.25.0", "@react-native/babel-plugin-codegen": "0.79.5", "babel-plugin-syntax-hermes-parser": "0.25.1", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-GDUYIWslMLbdJHEgKNfrOzXk8EDKxKzbwmBXUugoiSlr6TyepVZsj3GZDLEFarOcTwH1EXXHJsixihk8DCRQDA=="],
"babel-preset-expo/babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.25.1", "", { "dependencies": { "hermes-parser": "0.25.1" } }, "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ=="],
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
@@ -3039,9 +3056,27 @@
"metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
"metro/metro-babel-transformer": ["metro-babel-transformer@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.32.0", "nullthrows": "^1.1.1" } }, "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g=="],
"metro/metro-cache-key": ["metro-cache-key@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw=="],
"metro/metro-file-map": ["metro-file-map@0.83.3", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA=="],
"metro/metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="],
"metro/metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="],
"metro/metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="],
"metro/metro-transform-plugins": ["metro-transform-plugins@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A=="],
"metro/metro-transform-worker": ["metro-transform-worker@0.83.3", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "metro": "0.83.3", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-minify-terser": "0.83.3", "metro-source-map": "0.83.3", "metro-transform-plugins": "0.83.3", "nullthrows": "^1.1.1" } }, "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA=="],
"metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"metro-config/cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="],
"metro-babel-transformer/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"metro-core/metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="],
"metro-source-map/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
@@ -3057,6 +3092,10 @@
"metro-transform-worker/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"metro-transform-worker/metro": ["metro@0.83.1", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.29.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.1", "metro-cache": "0.83.1", "metro-cache-key": "0.83.1", "metro-config": "0.83.1", "metro-core": "0.83.1", "metro-file-map": "0.83.1", "metro-resolver": "0.83.1", "metro-runtime": "0.83.1", "metro-source-map": "0.83.1", "metro-symbolicate": "0.83.1", "metro-transform-plugins": "0.83.1", "metro-transform-worker": "0.83.1", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-UGKepmTxoGD4HkQV8YWvpvwef7fUujNtTgG4Ygf7m/M0qjvb9VuDmAsEU+UdriRX7F61pnVK/opz89hjKlYTXA=="],
"metro-transform-worker/metro-cache": ["metro-cache@0.83.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.1" } }, "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
@@ -3085,10 +3124,14 @@
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"react-native/@react-native/js-polyfills": ["@react-native/js-polyfills@0.81.4", "", {}, "sha512-sr42FaypKXJHMVHhgSbu2f/ZJfrLzgaoQ+HdpRvKEiEh2mhFf6XzZwecyLBvWqf2pMPZa+CpPfNPiejXjKEy8w=="],
"react-native/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"react-native/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="],
"react-native/semver": ["semver@7.7.2", "", { "bin": "bin/semver.js" }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"react-native-paper/color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
@@ -3265,8 +3308,26 @@
"@expo/metro-config/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
"@expo/metro-config/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"@expo/metro-config/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"@expo/metro/metro/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@expo/metro/metro/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@expo/metro/metro/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
"@expo/metro/metro/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@expo/metro/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
"@expo/metro/metro/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"@expo/metro/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"@expo/metro/metro-config/cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="],
"@expo/package-manager/@expo/json-file/@babel/code-frame": ["@babel/code-frame@7.10.4", "", { "dependencies": { "@babel/highlight": "^7.10.4" } }, "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg=="],
"@expo/package-manager/ora/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="],
@@ -3295,16 +3356,44 @@
"@jest/reporters/string-length/char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="],
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"@react-native/babel-preset/@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"@react-native/codegen/@babel/parser/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"@react-native/community-cli-plugin/metro/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"@react-native/community-cli-plugin/metro/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"@react-native/community-cli-plugin/metro/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
"@react-native/community-cli-plugin/metro/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@react-native/community-cli-plugin/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
"@react-native/community-cli-plugin/metro/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.83.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.1" } }, "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ=="],
"@react-native/community-cli-plugin/metro/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="],
"@react-native/community-cli-plugin/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"@react-native/community-cli-plugin/metro-config/cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="],
"@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.1", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.1" } }, "sha512-7N/Ad1PHa1YMWDNiyynTPq34Op2qIE68NWryGEQ4TSE3Zy6a8GpsYnEEZE4Qi6aHgsE+yZHKkRczeBgxhnFIxQ=="],
"@react-native/community-cli-plugin/metro-config/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="],
"@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
"@testing-library/react-native/pretty-format/@jest/schemas": ["@jest/schemas@30.0.5", "", { "dependencies": { "@sinclair/typebox": "^0.34.0" } }, "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA=="],
@@ -3321,8 +3410,22 @@
"ansi-fragments/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
"babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"babel-preset-expo/@babel/plugin-transform-object-rest-spread/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.27.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA=="],
"babel-preset-expo/@react-native/babel-preset/@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA=="],
"babel-preset-expo/@react-native/babel-preset/@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.27.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ=="],
"babel-preset-expo/@react-native/babel-preset/@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-compilation-targets": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA=="],
"babel-preset-expo/@react-native/babel-preset/@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.27.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA=="],
"babel-preset-expo/@react-native/babel-preset/@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.27.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q=="],
"babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.79.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.79.5" } }, "sha512-Rt/imdfqXihD/sn0xnV4flxxb1aLLjPtMF1QleQjEhJsTUPpH4TFlfOpoCvsrXoDl4OIcB1k4FVM24Ez92zf5w=="],
"babel-preset-expo/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"better-opn/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
@@ -3405,11 +3508,7 @@
"logkitty/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"metro-config/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="],
"metro-config/cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"metro-config/cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="],
"metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"metro-source-map/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
@@ -3431,10 +3530,30 @@
"metro-transform-worker/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"metro-transform-worker/metro/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
"metro-transform-worker/metro/ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="],
"metro-transform-worker/metro/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"metro-transform-worker/metro/metro-config": ["metro-config@0.83.1", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.1", "metro-cache": "0.83.1", "metro-core": "0.83.1", "metro-runtime": "0.83.1" } }, "sha512-HJhpZx3wyOkux/jeF1o7akFJzZFdbn6Zf7UQqWrvp7gqFqNulQ8Mju09raBgPmmSxKDl4LbbNeigkX0/nKY1QA=="],
"metro-transform-worker/metro/metro-core": ["metro-core@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.1" } }, "sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q=="],
"metro-transform-worker/metro/metro-runtime": ["metro-runtime@0.83.1", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-3Ag8ZS4IwafL/JUKlaeM6/CbkooY+WcVeqdNlBG0m4S0Qz0om3rdFdy1y6fYBpl6AwXJwWeMuXrvZdMuByTcRA=="],
"metro-transform-worker/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
"metro-transform-worker/metro-cache/metro-core": ["metro-core@0.83.1", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.1" } }, "sha512-uVL1eAJcMFd2o2Q7dsbpg8COaxjZBBGaXqO2OHnivpCdfanraVL8dPmY6It9ZeqWLOihUKZ2yHW4b6soVCzH/Q=="],
"metro/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"metro/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"metro/metro-source-map/ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="],
"metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ=="],
"node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
@@ -3529,6 +3648,18 @@
"@expo/cli/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
"@expo/metro/metro-config/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="],
"@expo/metro/metro-config/cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"@expo/metro/metro-config/cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="],
"@expo/metro/metro/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@expo/metro/metro/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@expo/metro/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"@expo/package-manager/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
"@expo/package-manager/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
@@ -3543,12 +3674,30 @@
"@jest/reporters/istanbul-lib-instrument/@babel/parser/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"@react-native/babel-plugin-codegen/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@react-native/babel-plugin-codegen/@react-native/codegen/@babel/parser/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"@react-native/community-cli-plugin/metro-config/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="],
"@react-native/community-cli-plugin/metro-config/cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"@react-native/community-cli-plugin/metro-config/cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="],
"@react-native/community-cli-plugin/metro/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@react-native/community-cli-plugin/metro/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@react-native/community-cli-plugin/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"@testing-library/react-native/pretty-format/@jest/schemas/@sinclair/typebox": ["@sinclair/typebox@0.34.40", "", {}, "sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw=="],
"babel-preset-expo/@react-native/babel-preset/@babel/plugin-transform-classes/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
"babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen": ["@react-native/codegen@0.79.5", "", { "dependencies": { "glob": "^7.1.1", "hermes-parser": "0.25.1", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "yargs": "^17.6.2" }, "peerDependencies": { "@babel/core": "*" } }, "sha512-FO5U1R525A1IFpJjy+KVznEinAgcs3u7IbnbRJUG9IH/MBXi2lEU2LtN+JarJ81MCfW4V2p0pg6t/3RGHFRrlQ=="],
"babel-preset-expo/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"expo-constants/@expo/config/@expo/config-plugins/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
@@ -3575,14 +3724,14 @@
"logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
"metro-config/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="],
"metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"metro-source-map/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"metro-source-map/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"metro-transform-worker/metro/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"metro-transform-worker/metro/metro-config/cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -3613,6 +3762,10 @@
"@expo/cli/ora/cli-cursor/restore-cursor/onetime": ["onetime@2.0.1", "", { "dependencies": { "mimic-fn": "^1.0.0" } }, "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ=="],
"@expo/metro/metro-config/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="],
"@expo/metro/metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"@expo/package-manager/ora/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"@expo/package-manager/ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
@@ -3621,12 +3774,28 @@
"@istanbuljs/load-nyc-config/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"@react-native/community-cli-plugin/metro-config/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="],
"@react-native/community-cli-plugin/metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"expo/babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@babel/traverse": ["@babel/traverse@7.28.3", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", "@babel/types": "^7.28.2", "debug": "^4.3.1" } }, "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ=="],
"expo/babel-preset-expo/@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"expo/babel-preset-expo/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"logkitty/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"metro-transform-worker/metro/metro-config/cosmiconfig/import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="],
"metro-transform-worker/metro/metro-config/cosmiconfig/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": "bin/js-yaml.js" }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"metro-transform-worker/metro/metro-config/cosmiconfig/parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="],
"pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
@@ -3639,14 +3808,22 @@
"@expo/package-manager/ora/cli-cursor/restore-cursor/onetime/mimic-fn": ["mimic-fn@1.2.0", "", {}, "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="],
"babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@react-native/codegen/hermes-parser/hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
"expo/babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@babel/traverse/@babel/generator": ["@babel/generator@7.28.3", "", { "dependencies": { "@babel/parser": "^7.28.3", "@babel/types": "^7.28.2", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw=="],
"expo/babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@babel/traverse/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="],
"expo/babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@babel/traverse/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="],
"expo/babel-preset-expo/@react-native/babel-preset/babel-plugin-syntax-hermes-parser/hermes-parser/hermes-estree": ["hermes-estree@0.29.1", "", {}, "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ=="],
"logkitty/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"metro-transform-worker/metro/metro-config/cosmiconfig/import-fresh/resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="],
"metro-transform-worker/metro/metro-config/cosmiconfig/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"expo/babel-preset-expo/@react-native/babel-preset/@react-native/babel-plugin-codegen/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],

View File

@@ -12,10 +12,12 @@ const LeftButtonCustom = ({
path,
icon = "arrow-back",
iconCustom,
onPress,
}: {
path?: Href;
icon?: React.ReactNode | any;
iconCustom?: React.ReactNode;
onPress?: () => void;
}) => {
return (
<>
@@ -26,7 +28,7 @@ const LeftButtonCustom = ({
name={icon}
size={20}
color={MainColor.yellow}
onPress={() => (path ? router.replace(path) : router.back())}
onPress={() => (onPress ? onPress() : path ? router.replace(path) : router.back())}
/>
)}
</>

View File

@@ -0,0 +1,29 @@
import { useRouter } from "expo-router";
import { BackButton } from "..";
export default function BackButtonFromNotification({
from,
category,
}: {
from: string;
category?: string;
}) {
const router = useRouter();
return (
<>
<BackButton
onPress={() => {
if (from === "notifications") {
router.replace(`/notifications?category=${category}`);
} else {
if (from) {
router.replace(`/${from}` as any);
} else {
router.navigate("/home");
}
}
}}
/>
</>
);
}

View File

@@ -24,8 +24,6 @@ const DateTimePickerCustom: React.FC<Props> = ({
disabled = false,
}) => {
console.log("Date Android Comp", value)
return (
<>
{Platform.OS === "ios" ? (

View File

@@ -30,7 +30,6 @@ export default function AvatarComp({
href = `/(application)/(image)/preview-image/${fileId}`,
}: AvatarCompProps) {
const dimension = sizeMap[size];
const avatarImage = () => {
return (
<Avatar.Image
@@ -52,8 +51,9 @@ export default function AvatarComp({
<TouchableOpacity
activeOpacity={0.9}
onPress={
href && fileId ? () => router.navigate(href as any) : onPress
href || fileId ? () => router.navigate(href as any) : onPress
}
disabled={!fileId}
>
{avatarImage()}
</TouchableOpacity>

View File

@@ -0,0 +1,33 @@
import { Modal, View } from "react-native";
export default function ModalReactNative({
children,
isVisible,
}: {
children: React.ReactNode;
isVisible: boolean;
}) {
return (
<Modal
animationType="slide"
backdropColor={"rgba(0, 0, 0, 0.5)"}
visible={isVisible}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.5)",
// margin: 10,
marginBlock: 30,
padding: 10,
borderRadius: 10,
paddingTop: 30
}}
>
{children}
</View>
</Modal>
);
}

View File

@@ -0,0 +1,140 @@
// src/components/BackgroundNotificationHandler.tsx
import { useNotificationStore } from "@/hooks/use-notification-store";
import AsyncStorage from "@react-native-async-storage/async-storage";
import {
FirebaseMessagingTypes,
getInitialNotification,
getMessaging,
onNotificationOpenedApp,
} from "@react-native-firebase/messaging";
import { router } from "expo-router";
import { useEffect, useRef } from "react";
const HANDLED_NOTIFICATIONS_KEY = "handled_notifications";
export default function BackgroundNotificationHandler() {
const { addNotification, markAsRead } = useNotificationStore();
const messaging = getMessaging();
const unsubscribeRef = useRef<(() => void) | null>(null); // 🔑 cegah duplikasi
useEffect(() => {
const init = async () => {
// 1. Handle (cold start)
const initialNotification = await getInitialNotification(messaging);
if (initialNotification) {
handleNotification(initialNotification);
return;
}
// 2. Handle background
if (unsubscribeRef.current) {
unsubscribeRef.current();
}
const unsubscribe = onNotificationOpenedApp(
messaging,
(remoteMessage) => {
handleNotification(remoteMessage);
}
);
unsubscribeRef.current = unsubscribe;
};
init();
// Cleanup saat komponen unmount
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}, [addNotification, messaging]);
const isNotificationHandled = async (
notificationId: string
): Promise<boolean> => {
const handled = await AsyncStorage.getItem(HANDLED_NOTIFICATIONS_KEY);
const ids = handled ? JSON.parse(handled) : [];
return ids.includes(notificationId);
};
const markNotificationAsHandled = async (notificationId: string) => {
const handled = await AsyncStorage.getItem(HANDLED_NOTIFICATIONS_KEY);
const ids = handled ? JSON.parse(handled) : [];
if (!ids.includes(notificationId)) {
ids.push(notificationId);
// Simpan maksimal 50 ID terakhir untuk hindari memori bocor
await AsyncStorage.setItem(
HANDLED_NOTIFICATIONS_KEY,
JSON.stringify(ids.slice(-50))
);
}
};
const handleNotification = async (
remoteMessage: FirebaseMessagingTypes.RemoteMessage
) => {
const { notification, data } = remoteMessage;
if (!notification?.title) return;
console.log(
"🚀 Notification received:",
JSON.stringify(remoteMessage, null, 2)
);
const notificationId = data?.id;
if (!notificationId || typeof notificationId !== "string") {
console.warn("Notification missing notificationId, skipping navigation");
return;
}
// ✅ Cek apakah sudah pernah ditangani
if (await isNotificationHandled(notificationId)) {
console.log("Notification already handled, skipping:", notificationId);
return;
}
// ✅ Tandai sebagai ditangani
await markNotificationAsHandled(notificationId);
// ✅ Normalisasi deepLink: pastikan string
let deepLink: string | undefined;
if (data?.deepLink) {
if (typeof data.deepLink === "string") {
deepLink = data.deepLink;
} else {
// Jika object (jarang), coba string-kan
deepLink = JSON.stringify(data.deepLink);
}
}
// Tambahkan ke UI state (agar muncul di daftar notifikasi & badge)
addNotification({
title: notification.title,
body: notification.body || "",
type: "announcement",
data: data as Record<string, string>, // aman karena di-normalisasi di useNotificationStore
});
markAsRead(data?.id as any);
// Navigasi
if (
data?.deepLink &&
typeof data.deepLink === "string" &&
data.deepLink.startsWith("/")
) {
setTimeout(() => {
try {
router.push(data.deepLink as any);
} catch (error) {
console.warn("Navigation failed:", error);
}
}, 100);
}
};
return null;
}

View File

@@ -0,0 +1,112 @@
// src/components/NotificationInitializer.tsx
import { useEffect } from "react";
import { useForegroundNotifications } from "@/hooks/use-foreground-notifications";
import { useNotificationStore } from "@/hooks/use-notification-store";
import type { FirebaseMessagingTypes } from "@react-native-firebase/messaging";
import { useAuth } from "@/hooks/use-auth";
import { Platform } from "react-native";
import * as Device from "expo-device";
import * as Application from "expo-application";
import { apiDeviceRegisterToken } from "@/service/api-device-token";
import messaging, {
isSupported,
requestPermission,
getToken,
AuthorizationStatus,
} from "@react-native-firebase/messaging";
// ✅ Modular imports (sesuai v22+)
export default function NotificationInitializer() {
// Setup handler notifikasi
const { user, logout } = useAuth(); // dari AuthContext
const { addNotification } = useNotificationStore();
// Ambil token FCM (opsional, hanya untuk log)
useEffect(() => {
if (!user) {
console.log("User not available, skipping token sync");
return;
}
const registerDeviceToken = async () => {
try {
// ✅ Dapatkan instance messaging
const messagingInstance = messaging();
// ✅ Gunakan instance sebagai argumen
const supported = await isSupported(messagingInstance);
if (!supported) {
console.log("‼️ FCM tidak didukung");
return;
}
const authStatus = await requestPermission(messagingInstance);
if (authStatus !== AuthorizationStatus.AUTHORIZED) {
console.warn("Izin telah ditolak");
return;
}
const fcmToken = await getToken(messagingInstance);
if (!fcmToken) {
console.warn("Tidak bisa mendapatkan FCM token");
// logout();
return;
}
console.log("✅ FCM Token:", fcmToken);
const platform = Platform.OS; // "ios" | "android"
const model = Device.modelName || "unknown";
const appVersion =
(Application.nativeApplicationVersion || "unknown") +
"-" +
(Application.nativeBuildVersion || "unknown");
const deviceId =
Device.osInternalBuildId || Device.modelName || "unknown";
// Kirim ke backend
await apiDeviceRegisterToken({
data: {
fcmToken,
platform,
deviceId,
model,
appVersion,
userId: user?.id || "",
},
});
console.log("✅ Device token berhasil didaftarkan ke backend");
} catch (error) {
console.error("❌ Gagal mendaftarkan device token:", error);
}
};
registerDeviceToken();
}, [user?.id]);
const handleForegroundNotification = (
message: FirebaseMessagingTypes.RemoteMessage
) => {
const title = message.notification?.title || "Notifikasi";
const body = message.notification?.body || "";
const rawData = message.data || {};
const safeData: Record<string, string> = {};
for (const key in rawData) {
safeData[key] =
typeof rawData[key] === "string"
? rawData[key]
: JSON.stringify(rawData[key]);
}
console.log("📥 Menambahkan ke store:", { title, body, safeData });
addNotification({ title, body, data: safeData, type: "announcement" });
console.log("✅ Notifikasi ditambahkan ke state");
};
useForegroundNotifications(handleForegroundNotification);
return null; // komponen ini tidak merender apa-apa
}

View File

@@ -98,13 +98,14 @@ export const IconView = ({
);
};
export const IconDot = ({ size, color }: { size?: number; color?: string }) => {
export const IconDot = ({ size, color, onPress }: { size?: number; color?: string , onPress?: () => void}) => {
return (
<>
<Ionicons
name="ellipsis-vertical"
size={size || ICON_SIZE_MEDIUM}
color={color || MainColor.darkblue}
onPress={onPress}
/>
</>
);

View File

@@ -4,12 +4,21 @@ import { Octicons } from "@expo/vector-icons";
export { IconPlus };
function IconPlus({ color, size }: { color?: string; size?: number }) {
function IconPlus({
color,
size,
onPress,
}: {
color?: string;
size?: number;
onPress?: () => void;
}) {
return (
<Octicons
name="plus-circle"
size={size || ICON_SIZE_MEDIUM}
color={color || MainColor.white}
onPress={onPress}
/>
);
}

View File

@@ -19,11 +19,12 @@ export {
PADDING_SMALL,
PADDING_MEDIUM,
PADDING_LARGE,
PAGINATION_DEFAULT_TAKE
};
// OS Height
const OS_ANDROID_HEIGHT = 115
const OS_IOS_HEIGHT = 70
const OS_IOS_HEIGHT = 90
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
// Text Size
@@ -51,3 +52,5 @@ const PADDING_SMALL = 12
const PADDING_MEDIUM = 16
const PADDING_LARGE = 20
// Pagination
const PAGINATION_DEFAULT_TAKE = 10;

View File

@@ -2,10 +2,13 @@ import {
apiConfig,
apiLogin,
apiRegister,
apiUpdatedTermCondition,
apiValidationCode,
} from "@/service/api-config";
import { apiDeviceTokenDeleted } from "@/service/api-device-token";
import { IUser } from "@/types/User";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as Device from "expo-device";
import { router } from "expo-router";
import { createContext, useEffect, useState } from "react";
import Toast from "react-native-toast-message";
@@ -18,7 +21,7 @@ type AuthContextType = {
isAuthenticated: boolean;
isAdmin: boolean;
isUserActive: boolean;
loginWithNomor: (nomor: string) => Promise<void>;
loginWithNomor: (nomor: string) => Promise<boolean>;
validateOtp: (nomor: string) => Promise<any>;
logout: () => Promise<void>;
registerUser: (userData: {
@@ -27,11 +30,15 @@ type AuthContextType = {
termsOfServiceAccepted: boolean;
}) => Promise<void>;
userData: (token: string) => Promise<any>;
acceptedTerms: (
nomor: string,
onSetModalVisible: (visible: boolean) => void,
) => Promise<any>;
};
// --- Create Context ---
export const AuthContext = createContext<AuthContextType | undefined>(
undefined
undefined,
);
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
@@ -72,30 +79,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const loginWithNomor = async (nomor: string) => {
setIsLoading(true);
try {
console.log("[Masuk provider]", nomor);
const response = await apiLogin({ nomor: nomor });
console.log("[RESPONSE AUTH]", JSON.stringify(response));
if (response.success) {
console.log("[Keluar provider]", nomor);
Toast.show({
type: "success",
text1: "Sukses",
text2: "Kode OTP berhasil dikirim",
});
console.log("[RESPONSE AUTH]", JSON.stringify(response, null, 2));
if (response.success && response.isAcceptTerms) {
await AsyncStorage.setItem("kode_otp", response.kodeId);
router.push(`/verification?nomor=${nomor}`);
return;
return true;
} else {
router.push(`/register?nomor=${nomor}`);
Toast.show({
type: "info",
text1: "Info",
text2: "Silahkan mendaftar",
});
return;
return false;
}
} catch (error: any) {
throw new Error(error.response?.data?.message || "Gagal kirim OTP");
@@ -104,18 +96,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
}
};
// const loginWithNomor = async (nomor: string) => {
// setIsLoading(true);
// try {
// const response = await apiLogin({ nomor: nomor });
// await AsyncStorage.setItem("kode_otp", response.kodeId);
// } catch (error: any) {
// throw new Error(error.response?.data?.message || "Gagal kirim OTP");
// } finally {
// setIsLoading(false);
// }
// };
// --- 2. Validasi OTP & cek user ---
const validateOtp = async (nomor: string) => {
try {
@@ -139,13 +119,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await AsyncStorage.setItem("userData", JSON.stringify(dataUser));
if (response.active) {
if (response.roleId === "1") {
// if (response.roleId === "1") {
// router.replace("/(application)/(user)/home");
// return;
// } else {
// router.replace("/(application)/admin/dashboard");
// return;
// }
router.replace("/(application)/(user)/home");
return;
} else {
router.replace("/(application)/admin/dashboard");
return;
}
} else {
router.replace("/(application)/(user)/waiting-room");
return;
@@ -161,7 +143,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
} catch (error: any) {
console.log("Error validasi otp >>", (error as Error).message || error);
throw new Error(
error.response?.data?.message || "OTP salah atau user tidak ditemukan"
error.response?.data?.message || "OTP salah atau user tidak ditemukan",
);
} finally {
setIsLoading(false);
@@ -190,7 +172,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
} catch (error: any) {
console.log(
"[LOAD USER DATA]",
error.response?.data?.message + "user" || "Gagal mengambil data user"
error.response?.data?.message + "user" || "Gagal mengambil data user",
);
} finally {
setIsLoading(false);
@@ -206,7 +188,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(true);
try {
const response = await apiRegister({ data: userData });
console.log("[REGISTER FETCH]", JSON.stringify(response, null, 2));
if (!response.success) {
Toast.show({
@@ -236,42 +217,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(false);
}
};
// const registerUser = async (userData: {
// username: string;
// nomor: string;
// termsOfServiceAccepted: boolean;
// }) => {
// setIsLoading(true);
// try {
// const response = await apiRegister({ data: userData });
// console.log("response", response);
// const { token } = response;
// if (!response.success) {
// Toast.show({
// type: "info",
// text1: "Info",
// text2: response.message,
// });
// return;
// }
// setToken(token);
// await AsyncStorage.setItem("authToken", token);
// Toast.show({
// type: "success",
// text1: "Sukses",
// text2: "Anda berhasil terdaftar",
// });
// router.replace("/(application)/(user)/waiting-room");
// return;
// } catch (error: any) {
// console.log("Error register", error);
// } finally {
// setIsLoading(false);
// }
// };
// --- 5. Logout ---
@@ -280,9 +225,13 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(true);
setToken(null);
setUser(null);
const deviceId =
Device.osInternalBuildId || Device.modelName || "unknown";
await AsyncStorage.removeItem("authToken");
await AsyncStorage.removeItem("userData");
setIsLoading(false);
await apiDeviceTokenDeleted({ userId: user?.id as any, deviceId });
Toast.show({
type: "success",
@@ -297,6 +246,28 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
}
};
// --- 6. Accept Terms ---
const acceptedTerms = async (
nomor: string,
onSetModalVisible: (visible: boolean) => void,
) => {
try {
setIsLoading(true);
const response = await apiUpdatedTermCondition({ nomor: nomor });
if (response.success) {
return `/verification?nomor=${nomor}`;
} else {
return `/register?nomor=${nomor}`;
}
} catch (error) {
console.log("Error accept terms", error);
} finally {
setIsLoading(false);
onSetModalVisible(false);
}
};
return (
<>
<AuthContext.Provider
@@ -312,6 +283,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
logout,
registerUser,
userData,
acceptedTerms,
}}
>
{children}

View File

@@ -13,3 +13,13 @@ Exp: open ios/HIPMIBADUNG.xcworkspace
perubahan versi : npm version patch
ios: bunx expo prebuild --platform ios
android: bunx expo prebuild --platform android
### Android
adb devices : cek device yang terhubung
Note: izinkan perangkat dulu agar statusnya tidak unauthorized
adb install android/app/build/outputs/apk/debug/app-debug.apk : install apk ke device / emulator
Note:
Gunakan flag -s (serial) di perintah adb untuk menentukan target
adb -s <0G52319V261040B2 ini adalah id nya> install android/app/build/outputs/apk/debug/app-debug.apk

View File

@@ -0,0 +1,22 @@
<!-- Start Penerapan Pagination -->
File utama: screens/Job/ScreenArchive2.tsx
Fun fecth: apiJobGetByStatus
File fetch: service/api-client/api-job.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Terapkan pagination pada file "File utama"
Analisa juga file "File utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Fun fecth" , pada file "File fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- End Penerapan Pagination -->
<!-- Start Penerapan NewWrapper -->
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti

View File

@@ -11,7 +11,7 @@
"preview": {
"distribution": "internal",
"android": {
"buildType": "app-bundle"
"buildType": "apk"
},
"ios": {
"simulator": false

517
helpers/PaginationGuide.md Normal file
View File

@@ -0,0 +1,517 @@
# 📱 Reusable Pagination untuk React Native + Expo
Komponen pagination yang terintegrasi dengan **NewWrapper** untuk infinite scroll, pull-to-refresh, skeleton loading, dan empty state.
---
## 📦 File Structure
```
/hooks/
└── usePagination.tsx # Custom hook untuk logika pagination
/helpers/
└── paginationHelpers.tsx # Helper functions untuk komponen UI
/components/
└── NewWrapper.tsx # Komponen wrapper utama (existing)
```
---
## 🚀 Cara Penggunaan
### **Step 1: Import Hook dan Helpers**
```tsx
import { usePagination } from "@/hooks/usePagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
```
### **Step 2: Setup Pagination Hook**
```tsx
const pagination = usePagination({
// ✅ Fungsi untuk fetch data (harus return { data: T[] })
fetchFunction: async (page, searchQuery) => {
return await apiForumGetAll({
category: "beranda",
search: searchQuery || "",
userLoginId: user.id,
page: String(page),
});
},
// ✅ Page size (harus sama dengan API)
pageSize: 5,
// ✅ Query pencarian
searchQuery: search,
// ✅ Dependencies (reload saat berubah)
dependencies: [user?.id, category],
// ⚙️ Optional callbacks
onDataFetched: (data) => console.log("Loaded:", data.length),
onError: (error) => console.error("Error:", error),
});
```
### **Step 3: Generate Komponen Pagination**
```tsx
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Tidak ada data",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: 5,
skeletonHeight: 200,
});
```
### **Step 4: Gunakan dengan NewWrapper**
```tsx
<NewWrapper
// Props dari pagination hook
listData={pagination.listData}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
// Komponen dari helpers
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
// Render item
renderItem={({ item }) => <YourComponent data={item} />}
// Props lain dari NewWrapper
headerComponent={<SearchInput />}
floatingButton={<FloatingButton />}
/>
```
---
## 📖 Contoh Implementasi Lengkap
### **Contoh 1: Forum Page (Basic)**
```tsx
import { usePagination } from "@/hooks/usePagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
export default function ForumPage() {
const { user } = useAuth();
const [search, setSearch] = useState("");
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
if (!user?.id) return { data: [] };
return await apiForumGetAll({
category: "beranda",
search: searchQuery || "",
userLoginId: user.id,
page: String(page),
});
},
pageSize: 5,
searchQuery: search,
dependencies: [user?.id],
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Tidak ada diskusi",
emptySearchMessage: "Tidak ada hasil pencarian",
});
return (
<NewWrapper
headerComponent={
<SearchInput
placeholder="Cari diskusi..."
onChangeText={_.debounce(setSearch, 500)}
/>
}
listData={pagination.listData}
renderItem={({ item }) => <ForumItem data={item} />}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}
```
### **Contoh 2: Product Page (Dengan Filter)**
```tsx
export default function ProductPage() {
const [search, setSearch] = useState("");
const [category, setCategory] = useState("all");
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
return await apiProductGetAll({
page: String(page),
search: searchQuery || "",
category: category !== "all" ? category : undefined,
});
},
pageSize: 10,
searchQuery: search,
dependencies: [category], // Reload saat category berubah
});
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Belum ada produk",
skeletonCount: 8,
skeletonHeight: 100,
});
return (
<NewWrapper
headerComponent={
<View>
<SearchInput onChangeText={setSearch} />
<CategoryFilter value={category} onChange={setCategory} />
</View>
}
listData={pagination.listData}
renderItem={({ item }) => <ProductCard product={item} />}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}
```
---
## ⚙️ API Reference
### **usePagination Hook**
#### Props
| Prop | Type | Required | Default | Deskripsi |
|------|------|----------|---------|-----------|
| `fetchFunction` | `(page, search?) => Promise<{data: T[]}>` | ✅ | - | Fungsi fetch data dari API |
| `pageSize` | `number` | ❌ | `5` | Jumlah data per halaman |
| `searchQuery` | `string` | ❌ | `""` | Query pencarian |
| `dependencies` | `any[]` | ❌ | `[]` | Dependencies untuk trigger reload |
| `onDataFetched` | `(data: T[]) => void` | ❌ | - | Callback saat data berhasil di-fetch |
| `onError` | `(error: any) => void` | ❌ | - | Callback saat terjadi error |
#### Return Value
```tsx
{
listData: T[]; // Array data untuk NewWrapper
loading: boolean; // Loading state
refreshing: boolean; // Refreshing state
hasMore: boolean; // Apakah masih ada data
page: number; // Current page
onRefresh: () => void; // Function untuk refresh
loadMore: () => void; // Function untuk load more
reset: () => void; // Function untuk reset state
setListData: (data) => void; // Function untuk set data manual
}
```
---
### **createPaginationComponents Helper**
#### Props
| Prop | Type | Required | Default | Deskripsi |
|------|------|----------|---------|-----------|
| `loading` | `boolean` | ✅ | - | Loading state |
| `refreshing` | `boolean` | ✅ | - | Refreshing state |
| `listData` | `any[]` | ✅ | - | List data |
| `searchQuery` | `string` | ❌ | `""` | Query pencarian |
| `emptyMessage` | `string` | ❌ | `"Tidak ada data"` | Pesan empty state |
| `emptySearchMessage` | `string` | ❌ | `"Tidak ada hasil pencarian"` | Pesan empty saat search |
| `skeletonCount` | `number` | ❌ | `5` | Jumlah skeleton items |
| `skeletonHeight` | `number` | ❌ | `200` | Tinggi skeleton items |
| `loadingFooterText` | `string` | ❌ | - | Text loading footer |
#### Return Value
```tsx
{
ListEmptyComponent: React.ReactElement; // Component untuk empty state
ListFooterComponent: React.ReactElement; // Component untuk loading footer
}
```
---
### **Helper Functions Lain**
#### `createSkeletonList(options)`
Generate skeleton list untuk loading state.
```tsx
const SkeletonComponent = createSkeletonList({
count: 5,
height: 200
});
```
#### `createEmptyState(options)`
Generate empty state component.
```tsx
const EmptyComponent = createEmptyState({
message: "Tidak ada data",
searchMessage: "Tidak ada hasil pencarian",
searchQuery: search
});
```
#### `createLoadingFooter(options)`
Generate loading footer component.
```tsx
const FooterComponent = createLoadingFooter({
show: loading && listData.length > 0,
text: "Memuat data..."
});
```
---
## 🎨 Custom Components
### **Custom Empty State**
```tsx
import { createSkeletonList } from "@/helpers/paginationHelpers";
const CustomEmpty = (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>🔍</Text>
<TextCustom>Data tidak ditemukan</TextCustom>
</View>
);
const ListEmptyComponent =
pagination.loading && pagination.listData.length === 0
? createSkeletonList({ count: 5, height: 200 })
: CustomEmpty;
<NewWrapper
ListEmptyComponent={ListEmptyComponent}
// ...
/>
```
### **Custom Loading Footer**
```tsx
import { createLoadingFooter } from "@/helpers/paginationHelpers";
const CustomFooter = createLoadingFooter({
show: pagination.loading && !pagination.refreshing && pagination.listData.length > 0,
customComponent: (
<View style={{ padding: 20, alignItems: "center" }}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={{ marginTop: 8 }}>Loading more...</Text>
</View>
)
});
<NewWrapper
ListFooterComponent={CustomFooter}
// ...
/>
```
---
## ✨ Fitur-Fitur
**Infinite Scroll** - Auto load saat scroll ke bawah
**Pull to Refresh** - Swipe down untuk refresh
**Skeleton Loading** - Smooth loading animation
**Empty State** - Tampilan saat data kosong
**Search Integration** - Support search dengan debounce
**Multi Dependencies** - Reload berdasarkan filter apapun
**Error Handling** - Built-in error handling
**TypeScript** - Full type safety
**Fully Customizable** - Custom components untuk semua state
---
## 🎯 Best Practices
### 1. **Gunakan Debounce untuk Search**
```tsx
<SearchInput
onChangeText={_.debounce((text) => setSearch(text), 500)}
/>
```
### 2. **Sesuaikan Page Size dengan API**
```tsx
const pagination = usePagination({
pageSize: 5, // Harus sama dengan takeData di API
});
```
### 3. **Tambahkan Dependencies yang Relevan**
```tsx
const pagination = usePagination({
dependencies: [userId, category, sortBy], // Reload saat berubah
});
```
### 4. **Handle Error dengan Baik**
```tsx
const pagination = usePagination({
onError: (error) => {
console.error("Error:", error);
Alert.alert("Error", "Gagal memuat data");
},
});
```
### 5. **Pastikan API Return Format yang Benar**
```tsx
// ❌ SALAH
fetchFunction: async () => [data1, data2];
// ✅ BENAR
fetchFunction: async () => ({ data: [data1, data2] });
```
---
## 🔧 Troubleshooting
### **Data tidak muncul?**
- Pastikan `fetchFunction` return `{ data: T[] }`
- Cek apakah API return format yang benar
- Pastikan `pageSize` sesuai dengan API
### **Infinite scroll tidak jalan?**
- Pastikan API return data sesuai `pageSize`
- Cek `hasMore` state
- Pastikan `onEndReachedThreshold` tidak terlalu kecil (default 0.5)
### **Skeleton terus muncul?**
- Cek `loading` state
- Pastikan `fetchFunction` resolve dengan benar
- Cek error di console
### **Refresh tidak bekerja?**
- Pastikan `RefreshControl` menggunakan `pagination.refreshing` dan `pagination.onRefresh`
- Cek apakah API dipanggil saat pull-to-refresh
---
## 📝 Migration Guide
### **Dari Code Lama ke Code Baru**
#### **BEFORE:**
```tsx
const [listData, setListData] = useState([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const fetchData = async (pageNumber, clear) => {
// ... 30+ lines of code
};
useEffect(() => {
setPage(1);
setListData([]);
setHasMore(true);
fetchData(1, true);
}, [search, user?.id]);
const onRefresh = useCallback(() => {
fetchData(1, true);
}, [search, user?.id]);
const loadMore = useCallback(() => {
if (hasMore && !loading && !refreshing) {
fetchData(page + 1, false);
}
}, [hasMore, loading, refreshing, page, search, user?.id]);
// ... skeleton, empty, footer components
```
#### **AFTER:**
```tsx
const pagination = usePagination({
fetchFunction: async (page, search) => await apiGetData({ page, search }),
pageSize: 5,
searchQuery: search,
dependencies: [user?.id]
});
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
});
```
**Result:** 50+ lines → 15 lines! 🎉
---
## 👨‍💻 Author
Created by Full-Stack Developer
React Native + Expo Specialist
---
## 📄 License
MIT License - Feel free to use in your projects!

View File

@@ -0,0 +1,280 @@
import { View } from "react-native";
import { LoaderCustom, TextCustom, StackCustom } from "@/components";
import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom";
import _ from "lodash";
/**
* Pagination Helpers
*
* Helper functions untuk membuat komponen-komponen pagination
* yang sering digunakan (Skeleton, Empty State, Loading Footer)
*/
interface SkeletonListOptions {
/**
* Jumlah skeleton items
* @default 5
*/
count?: number;
/**
* Tinggi setiap skeleton item
* @default 200
*/
height?: number;
}
/**
* Generate Skeleton List Component untuk loading state
*
* @example
* ```tsx
* <NewWrapper
* listData={listData}
* ListEmptyComponent={
* loading && _.isEmpty(listData)
* ? createSkeletonList({ count: 5, height: 200 })
* : createEmptyState({ message: "Tidak ada data" })
* }
* />
* ```
*/
export const createSkeletonList = (options: SkeletonListOptions = {}) => {
const { count = 5, height = 200 } = options;
return (
<View style={{ flex: 1 }}>
<StackCustom>
{Array.from({ length: count }).map((_, i) => (
<SkeletonCustom height={height} key={i} />
))}
</StackCustom>
</View>
);
};
interface EmptyStateOptions {
/**
* Pesan untuk empty state
* @default "Tidak ada data"
*/
message?: string;
/**
* Pesan untuk empty state saat search
*/
searchMessage?: string;
/**
* Query pencarian (untuk menentukan pesan mana yang ditampilkan)
*/
searchQuery?: string;
/**
* Custom component untuk empty state
*/
customComponent?: React.ReactElement;
}
/**
* Generate Empty State Component
*
* @example
* ```tsx
* ListEmptyComponent={
* createEmptyState({
* message: "Tidak ada diskusi",
* searchMessage: "Tidak ada hasil pencarian",
* searchQuery: search
* })
* }
* ```
*/
export const createEmptyState = (options: EmptyStateOptions = {}) => {
const {
message = "Tidak ada data",
searchMessage = "Tidak ada hasil pencarian",
searchQuery = "",
customComponent,
} = options;
if (customComponent) return customComponent;
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
}}
>
<TextCustom align="center" color="gray">
{searchQuery ? searchMessage : message}
</TextCustom>
</View>
);
};
interface LoadingFooterOptions {
/**
* Tampilkan loading footer
*/
show: boolean;
/**
* Custom text untuk loading footer
*/
text?: string;
/**
* Custom component untuk loading footer
*/
customComponent?: React.ReactElement;
}
/**
* Generate Loading Footer Component
*
* @example
* ```tsx
* ListFooterComponent={
* createLoadingFooter({
* show: loading && !refreshing && listData.length > 0,
* text: "Memuat data..."
* })
* }
* ```
*/
export const createLoadingFooter = (options: LoadingFooterOptions) => {
const { show, text, customComponent } = options;
if (!show) return null;
if (customComponent) return customComponent;
return (
<View style={{ paddingVertical: 16, alignItems: "center" }}>
{text ? (
<TextCustom color="gray">
{text}
</TextCustom>
) : (
<LoaderCustom />
)}
</View>
);
};
interface PaginationComponentsOptions {
/**
* Loading state
*/
loading: boolean;
/**
* Refreshing state
*/
refreshing: boolean;
/**
* List data
*/
listData: any[];
/**
* Query pencarian
*/
searchQuery?: string;
/**
* Pesan empty state
*/
emptyMessage?: string;
/**
* Pesan empty state saat search
*/
emptySearchMessage?: string;
/**
* Jumlah skeleton items
*/
skeletonCount?: number;
/**
* Tinggi skeleton items
*/
skeletonHeight?: number;
/**
* Text loading footer
*/
loadingFooterText?: string;
/**
* Loading pertama
*/
isInitialLoad?: boolean;
}
/**
* Generate semua komponen pagination sekaligus
*
* @example
* ```tsx
* const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
* loading,
* refreshing,
* listData,
* searchQuery: search,
* emptyMessage: "Tidak ada diskusi",
* emptySearchMessage: "Tidak ada hasil pencarian",
* skeletonCount: 5,
* skeletonHeight: 200
* });
*
* <NewWrapper
* listData={listData}
* ListEmptyComponent={ListEmptyComponent}
* ListFooterComponent={ListFooterComponent}
* />
* ```
*/
export const createPaginationComponents = (
options: PaginationComponentsOptions
) => {
const {
loading,
refreshing,
listData,
searchQuery = "",
emptyMessage = "Tidak ada data",
emptySearchMessage = "Tidak ada hasil pencarian",
skeletonCount = 5,
skeletonHeight = 200,
loadingFooterText,
isInitialLoad,
} = options;
// Empty Compotnent: Skeleton saat loading pertama, Empty State saat data kosong
const ListEmptyComponent =
loading && _.isEmpty(listData)
? createSkeletonList({ count: skeletonCount, height: skeletonHeight })
: createEmptyState({
message: emptyMessage,
searchMessage: emptySearchMessage,
searchQuery,
});
// Footer Component: Loading indicator saat load more
const ListFooterComponent = createLoadingFooter({
show: loading && !refreshing && listData.length > 0,
text: loadingFooterText,
});
return {
ListEmptyComponent,
ListFooterComponent,
};
};

View File

@@ -0,0 +1,37 @@
import { useEffect } from "react";
import {
getMessaging,
onMessage,
FirebaseMessagingTypes,
} from "@react-native-firebase/messaging";
import { useAuth } from "./use-auth";
// Gunakan tipe resmi dari library
type RemoteMessage = FirebaseMessagingTypes.RemoteMessage;
export function useForegroundNotifications(
onMessageReceived: (message: RemoteMessage) => void
) {
const { user } = useAuth();
useEffect(() => {
const messaging = getMessaging();
const unsubscribe = onMessage(messaging, (remoteMessage) => {
const data = remoteMessage.data;
// console.log("DATA NOTIFIKASI DARI SERVER", data)
if (data?.recipientId && data?.recipientId !== user?.id) {
console.log("📵 Notification untuk user lain", data);
return;
}
console.log(
"🔔 Notifikasi diterima saat app aktif:",
JSON.stringify(data, null, 2)
);
onMessageReceived(remoteMessage);
});
return unsubscribe;
}, [user?.id, onMessageReceived]);
}

Some files were not shown because too many files have changed in this diff Show More