Compare commits

...

17 Commits

Author SHA1 Message Date
98f8c7e2bf Fix layout tabs pada komponen
Fix home tabs

### No Issue
2026-04-01 17:17:12 +08:00
81bbd8e6b0 Clean project and release with new domain: hipmi.muku.id
### No issue
2026-03-31 17:07:27 +08:00
57159d2c45 update app config domain
### No Issue
2026-03-31 14:29:19 +08:00
66373fa65b Clean code
Update android domain

### No Issue
2026-03-31 14:28:43 +08:00
6d545f2af9 Fix change yang tertinggal 2026-03-30 17:39:53 +08:00
eeb95336f2 Fix version & tabs android
### No Issue
2026-03-30 17:38:31 +08:00
6fb3b229c3 Fix andorid 2026-03-27 17:59:13 +08:00
76deec9c53 Fix tabs agar bisa di klik
### Issue: Di android batas atas kurang turun
2026-03-26 17:37:31 +08:00
31948f71db Fix: NewWrapper footer position dengan SafeAreaView
Problem:
- Footer component tertutup atau scroll melebihi layar
- Tabs tidak bisa diklik karena footer floating
- Height OS_HEIGHT tidak konsisten di berbagai device

Solution:
 Footer menggunakan position: absolute untuk stay di bawah
 Konten ScrollView/FlatList mendapat paddingBottom: OS_HEIGHT
 SafeAreaView hanya untuk area aman, bukan height control
 Footer tetap visible di semua ukuran layar

Changes:
- Add styles.footerContainer dengan position: absolute
- Add paddingBottom ke contentContainerStyle (ScrollView & FlatList)
- Remove height: OS_HEIGHT dari SafeAreaView
- Footer stay di bottom dengan proper safe area handling

Result:
- Footer selalu terlihat di bawah layar
- Konten tidak tertutup footer (ada padding)
- Tabs bisa diklik dengan baik
- Konsisten di iOS & Android
- Respect safe area insets

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 12:28:07 +08:00
16decd89c8 Refactor: apply PhoneInputCustom to PortofolioEdit
Changes:
- Replace react-native-international-phone-number with PhoneInputCustom
- Remove ICountry dependency, use CountryData from constants
- Add phone number state management
- Implement country detection from existing phone number
- Auto-detect country based on calling code on load
- Improve phone number formatting logic

Features Applied:
 NO emoji flags - only calling codes (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 Auto-detect country from saved phone number
 Real-time phone number formatting
 Auto-update country code on change
 Consistent with LoginView & ScreenPortofolioCreate

Phone Detection Logic:
- Load existing phone number from API
- Detect country by matching calling code
- Extract phone number without country code for display
- Set detected country for country picker
- Re-format on country change

UI:
- Phone Input: [+62 ⌄ | xxx-xxx-xxx]
- Country Picker: Modal with search
- Display: Country name + calling code only

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 12:17:05 +08:00
ecbcc12abf Refactor: apply PhoneInputCustom to ScreenPortofolioCreate
Changes:
- Replace react-native-international-phone-number with PhoneInputCustom
- Remove ICountry dependency, use CountryData from constants
- Update state management (inputValue → phoneNumber)
- Improve phone number formatting logic
- Add handleCountryChange for better country switching

Features Applied:
 NO emoji flags - only calling codes (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 Real-time phone number formatting
 Auto-update country code on change
 Consistent with LoginView implementation

Phone Input Logic:
- Format on every phone change
- Re-format when country changes
- Remove duplicate country codes
- Remove leading zeros
- Store E.164 format in API data

UI:
- Phone Input: [+62 ⌄ | xxx-xxx-xxx]
- Country Picker: Modal with search
- Display: Country name + calling code only

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 11:42:54 +08:00
0cb734e790 Refactor: apply PhoneInputCustom to LoginView
Changes:
- Replace react-native-international-phone-number with PhoneInputCustom
- Remove dependency on ICountry, use CountryData from constants
- Add KeyboardAvoidingView for better iOS keyboard handling
- Improve validation with libphonenumber-js
- Add proper phone number formatting
- Update state management (inputValue → phoneNumber)

Features Applied:
 NO emoji flags - only calling codes (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 Better keyboard handling on iOS
 Real-time validation with libphonenumber-js
 Auto-formatting for international numbers
 Reusable component

UI:
- Phone Input: [+62 ⌄ | 812-3456-7890]
- Country Picker: Modal with search
- Display: Country name + calling code only

Validation:
- Check phone number length
- Validate with libphonenumber-js
- Format to E.164 on login
- Error handling with Toast

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 11:33:18 +08:00
0d2fef1878 Feat: add reusable PhoneInput component without flags
New Components:
- PhoneInputCustom: Reusable phone input without emoji flags
- constants/countries.ts: Country data with calling codes only

Features:
 NO emoji flags - only country name + calling code (+62, +65, etc)
 Clean, professional UI
 Modal country picker with search
 15 countries supported
 Helper functions: getCountryByCallingCode, getCountryByCode, searchCountries
 Fully typed with TypeScript
 Reusable across the app
 Maximum compatibility (no emoji rendering issues)

UI Design:
- Phone Input: [+62 ⌄ | 812-3456-7890]
- Country Picker: Modal with search
- Display: Country name + calling code only

Usage:
import { PhoneInputCustom } from '@/components';
import { DEFAULT_COUNTRY } from '@/constants/countries';

<PhoneInputCustom
  value={phoneNumber}
  onChangePhoneNumber={setPhoneNumber}
  selectedCountry={selectedCountry}
  onChangeCountry={setSelectedCountry}
/>

Benefits:
 Works on ALL iOS versions (no emoji issues)
 Consistent across all platforms
 Faster render (no emoji/image loading)
 Cleaner code structure
 Easy to maintain

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-26 11:27:59 +08:00
2e58f8c7b4 Docs: add task breakdown untuk fix phone input iOS 16+
- Add TASKS/fix-phone-input-ios-16.md dengan analisis lengkap
- 3 opsi solusi: KeyboardAvoidingView, Keyboard Controller, Custom Input
- Detailed implementation guidelines
- Testing checklist untuk iOS 16, 17, 18

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-25 16:39:48 +08:00
b6cd308b0b Refactor pagination implementation dan perbaikan UI
- Add default page parameter di apiAllUser
- Refactor MainView_V2.tsx dengan separate render functions
- Update pagination pageSize menjadi 10 di Forum
- Fix iOS height constant dan tab styling
- Rename Admin_ScreenPortofolioCreate ke ScreenPortofolioCreate
- Add TASKS documentation folder

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-25 16:22:54 +08:00
f68deab8c0 Fix App Header
Layouts & Navigation
- app/(application)/_layout.tsx
- app/(application)/(user)/_layout.tsx
- app/(application)/(user)/event/(tabs)/_layout.tsx
- app/(application)/(user)/job/(tabs)/_layout.tsx
- app/(application)/(user)/voting/(tabs)/_layout.tsx
- app/(application)/(user)/portofolio/_layout.tsx
- app/(application)/(user)/profile/_layout.tsx
- app/(application)/admin/_layout.tsx
- app/+not-found.tsx

User – File
- app/(application)/(file)/[id].tsx

User – Collaboration
- app/(application)/(user)/collaboration/[id]/index.tsx
- app/(application)/(user)/collaboration/[id]/detail-project-main.tsx
- app/(application)/(user)/collaboration/[id]/detail-participant.tsx
- app/(application)/(user)/collaboration/[id]/[detail]/info.tsx
- app/(application)/(user)/collaboration/[id]/[detail]/room-chat.tsx

User – Donation
- app/(application)/(user)/donation/[id]/index.tsx
- app/(application)/(user)/donation/[id]/[status]/detail.tsx
- app/(application)/(user)/donation/[id]/(news)/[news]/index.tsx

User – Event
- app/(application)/(user)/event/[id]/[status]/detail-event.tsx
- app/(application)/(user)/event/[id]/confirmation.tsx
- app/(application)/(user)/event/[id]/contribution.tsx
- app/(application)/(user)/event/[id]/history.tsx
- app/(application)/(user)/event/[id]/publish.tsx

User – Investment
- app/(application)/(user)/investment/[id]/index.tsx
- app/(application)/(user)/investment/[id]/[status]/detail.tsx
- app/(application)/(user)/investment/[id]/(my-holding)/[id].tsx
- app/(application)/(user)/investment/[id]/(news)/[news]/index.tsx

User – Job
- app/(application)/(user)/job/[id]/[status]/detail.tsx

User – Portofolio
- app/(application)/(user)/portofolio/[id]/index.tsx

User – Profile
- app/(application)/(user)/profile/[id]/index.tsx

User – Voting
- app/(application)/(user)/voting/[id]/index.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- app/(application)/(user)/voting/[id]/contribution.tsx
- app/(application)/(user)/voting/[id]/history.tsx

Components
- components/Button/BackButtonFromNotification.tsx
- components/_ShareComponent/AppHeader.tsx

Admin Screens
- screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
- screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx

Screens – Donation
- screens/Donation/ScreenListOfNews.tsx
- screens/Donation/ScreenRecapOfNews.tsx

Screens – Forum
- screens/Forum/ViewBeranda.tsx
- screens/Forum/ViewBeranda2.tsx
- screens/Forum/ViewBeranda3.tsx

Screens – Investment
- screens/Invesment/Document/ScreenRecapOfDocument.tsx
- screens/Invesment/News/ScreenListOfNews.tsx
- screens/Invesment/News/ScreenRecapOfNews.tsx

Screens – Notification
- screens/Notification/ScreenNotification_V1.tsx
- screens/Notification/ScreenNotification_V2.tsx

iOS
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj

Docs
- QWEN.md

### Issue: Tabs can't clicked
2026-03-13 16:41:34 +08:00
37d2fbe48a Fix code/
User Pages
- app/(application)/(user)/home.tsx
- app/(application)/(user)/portofolio/[id]/create.tsx

Components
- components/_ShareComponent/AppHeader.tsx

Screens
- screens/Portofolio/ScreenPortofolioCreate.tsx

Config & Dependencies
- app.config.js
- package.json
- bun.lock

iOS
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

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

### No issue
2026-03-12 16:50:02 +08:00
98 changed files with 3313 additions and 2306 deletions

3
.gitignore vendored
View File

@@ -81,4 +81,7 @@ yarn-error.*
# typescript
*.tsbuildinfo
# secrets
secrets/
# @end expo-cli

8
.qwen/settings.json Normal file
View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(git add *)"
]
},
"$version": 3
}

7
.qwen/settings.json.orig Normal file
View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(git add *)"
]
}
}

View File

@@ -387,7 +387,7 @@ apiConfig.interceptors.request.use(async (config) => {
### Deep Linking
- Scheme: `hipmimobile://`
- HTTPS: `cld-dkr-staging-hipmi.wibudev.com`
- HTTPS: `cld-dkr-hipmi-stg.wibudev.com`
- Configured for both platforms
### Camera

View File

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

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -37,7 +37,7 @@
</intent-filter>
<intent-filter android:autoVerify="true" data-generated="true">
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https" android:host="cld-dkr-hipmi-stg.wibudev.com" android:pathPrefix="/"/>
<data android:scheme="https" android:host="hipmi.muku.id" android:pathPrefix="/"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>

View File

@@ -6,32 +6,17 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.google.gms:google-services:4.4.1'
classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject"
// @generated begin @rnmapbox/maps-v2-maven - expo prebuild (DO NOT MODIFY) sync-d4ccbfdff48fdba3138b02a8ba41b9722af001d8
allprojects {
repositories {
maven {
url 'https://api.mapbox.com/downloads/v2/releases/maven'
// Authentication is no longer required as per Mapbox's removal of download token requirement
// See: https://github.com/mapbox/mapbox-maps-flutter/issues/775
// Keeping this as optional for backward compatibility
def token = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv('RNMAPBOX_MAPS_DOWNLOAD_TOKEN')
if (token) {
authentication { basic(BasicAuthentication) }
@@ -41,7 +26,11 @@ allprojects {
}
}
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}
// @generated end @rnmapbox/maps-v2-maven
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject"

View File

@@ -25,27 +25,27 @@ export default {
ios: {
supportsTablet: true,
bundleIdentifier: "com.anonymous.hipmi-mobile",
googleServicesFile: "./ios/HIPMIBadungConnect/GoogleService-Info.plist",
googleServicesFile: "./secrets/GoogleService-Info.plist",
infoPlist: {
ITSAppUsesNonExemptEncryption: false,
NSLocationWhenInUseUsageDescription:
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
},
associatedDomains: [
"applinks:cld-dkr-hipmi-stg.wibudev.com",
"applinks:hipmi.muku.id",
],
buildNumber: "4",
buildNumber: "7",
},
android: {
googleServicesFile: "./google-services.json",
googleServicesFile: "./secrets/google-services.json",
adaptiveIcon: {
foregroundImage: "./assets/images/splash-icon.png",
backgroundColor: "#ffffff",
},
edgeToEdgeEnabled: true,
package: "com.bip.hipmimobileapp",
versionCode: 1,
versionCode: 5,
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
intentFilters: [
{
@@ -54,7 +54,7 @@ export default {
data: [
{
scheme: "https",
host: "cld-dkr-hipmi-stg.wibudev.com",
host: "hipmi.muku.id",
pathPrefix: "/",
},
],
@@ -70,6 +70,7 @@ export default {
},
plugins: [
"./plugins/withCustomConfig",
"expo-router",
"expo-web-browser",
[

View File

@@ -1,4 +1,5 @@
import { BackButton } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import PdfViewer from "@/components/_ShareComponent/PdfViewer";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import { Stack, useLocalSearchParams } from "expo-router";
@@ -7,13 +8,12 @@ import { SafeAreaView } from "react-native-safe-area-context";
export default function FileScreen() {
const { id } = useLocalSearchParams();
const url = API_STRORAGE.GET({ fileId: id as string });
return (
<>
<Stack.Screen
options={{
title: "File",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="File" left={<BackButton />} />,
}}
/>
<SafeAreaView style={{ flex: 1 }} edges={["bottom"]}>

View File

@@ -1,22 +1,21 @@
import { BackButton } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
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";
import { HeaderStyles } from "@/styles/header-styles";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
export default function UserLayout() {
return (
<>
<Stack screenOptions={HeaderStyles}>
<Stack>
<Stack.Screen
name="delete-account"
options={{
title: "Hapus Akun",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Hapus Akun" />,
}}
/>
<Stack.Screen
@@ -47,8 +46,7 @@ export default function UserLayout() {
<Stack.Screen
name="user-search/index"
options={{
title: "Pencarian Pengguna",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Pencarian Pengguna" />,
}}
/>
@@ -71,10 +69,18 @@ export default function UserLayout() {
{/* ========== Event Section ========= */}
{/* <Stack.Screen
name="event/(tabs)"
options={{
header: () => <AppHeader title="Event" left={<BackButton path="/home" />} />,
}}
/> */}
<Stack.Screen
name="event/(tabs)"
options={{
title: "Event",
header: () => <AppHeader title="Event" left={<BackButton path="/home" />} />,
// NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx
// headerLeft: () => (
// <LeftButtonCustom path="/(application)/(user)/home" />
@@ -85,32 +91,28 @@ export default function UserLayout() {
<Stack.Screen
name="event/create"
options={{
title: "Tambah Event",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Event" />,
}}
/>
<Stack.Screen
name="event/detail/[id]"
options={{
title: "Event Detail",
headerLeft: () => <LeftButtonCustom />,
header: () => <AppHeader title="Event Detail" left={<LeftButtonCustom />} />,
}}
/>
<Stack.Screen
name="event/[id]/edit"
options={{
title: "Edit Event",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Event" />,
}}
/>
<Stack.Screen
name="event/[id]/list-of-participants"
options={{
title: "Daftar peserta",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Daftar peserta" />,
}}
/>
{/* ========== End Event Section ========= */}
@@ -119,22 +121,19 @@ export default function UserLayout() {
<Stack.Screen
name="collaboration/(tabs)"
options={{
title: "Collaboration",
headerLeft: () => <BackButton path="/home" />,
header: () => <AppHeader title="Collaboration" left={<BackButton path="/home" />} />,
}}
/>
<Stack.Screen
name="collaboration/create"
options={{
title: "Tambah Proyek",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Proyek" />,
}}
/>
<Stack.Screen
name="collaboration/[id]/list-of-participants"
options={{
title: "Daftar Partisipan",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Daftar Partisipan" />,
}}
/>
{/* <Stack.Screen
@@ -147,22 +146,19 @@ export default function UserLayout() {
<Stack.Screen
name="collaboration/[id]/edit"
options={{
title: "Edit Proyek",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Proyek" />,
}}
/>
<Stack.Screen
name="collaboration/[id]/create-pacticipants"
options={{
title: "Ajukan Partisipasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Ajukan Partisipasi" />,
}}
/>
<Stack.Screen
name="collaboration/[id]/select-of-participants"
options={{
title: "Pilih Partisipan",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Pilih Partisipan" />,
}}
/>
@@ -172,29 +168,25 @@ export default function UserLayout() {
<Stack.Screen
name="voting/create"
options={{
title: "Tambah Voting",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Voting" />,
}}
/>
<Stack.Screen
name="voting/(tabs)"
options={{
title: "Voting",
headerLeft: () => <BackButton path="/home" />,
header: () => <AppHeader title="Voting" left={<BackButton path="/home" />} />,
}}
/>
<Stack.Screen
name="voting/[id]/edit"
options={{
title: "Edit Voting",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Voting" />,
}}
/>
<Stack.Screen
name="voting/[id]/list-of-contributor"
options={{
title: "Daftar Kontributor",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Daftar Kontributor" />,
}}
/>
@@ -204,8 +196,7 @@ export default function UserLayout() {
<Stack.Screen
name="crowdfunding/index"
options={{
title: "Crowdfunding",
headerLeft: () => <BackButton path="/home" />,
header: () => <AppHeader title="Crowdfunding" left={<BackButton path="/home" />} />,
}}
/>
@@ -215,103 +206,95 @@ export default function UserLayout() {
<Stack.Screen
name="investment/(tabs)"
options={{
title: "Investasi",
headerLeft: () => <BackButton path="/crowdfunding" />,
header: () => <AppHeader title="Investasi" left={<BackButton path="/crowdfunding" />} />,
}}
/>
<Stack.Screen
name="investment/create"
options={{
title: "Tambah Investasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Investasi" />,
}}
/>
<Stack.Screen
name="investment/[id]/index"
options={{
title: "Detail Investasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Detail Investasi" />,
}}
/>
<Stack.Screen
name="investment/[id]/edit"
options={{
title: "Edit Investasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Investasi" />,
}}
/>
<Stack.Screen
name="investment/[id]/edit-prospectus"
options={{
title: "Edit Prospektus",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Prospektus" />,
}}
/>
<Stack.Screen
name="investment/[id]/(document)/list-of-document"
options={{
title: "Daftar Dokumen",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Daftar Dokumen" />,
}}
/>
<Stack.Screen
name="investment/[id]/(document)/add-document"
options={{
title: "Tambah Dokumen",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Dokumen" />,
}}
/>
<Stack.Screen
name="investment/[id]/(document)/edit-document"
options={{
title: "Edit Dokumen",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Dokumen" />,
}}
/>
<Stack.Screen
name="investment/[id]/(news)/add-news"
options={{
title: "Tambah Berita",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Berita" />,
}}
/>
<Stack.Screen
name="investment/[id]/investor"
options={{
title: "Investor",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Investor" />,
}}
/>
<Stack.Screen
name="investment/[id]/(transaction-flow)/index"
options={{
title: "Pembelian Saham",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Pembelian Saham" />,
}}
/>
<Stack.Screen
name="investment/[id]/(transaction-flow)/select-bank"
options={{
title: "Pilih Bank",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Pilih Bank" />,
}}
/>
<Stack.Screen
name="investment/[id]/(transaction-flow)/invoice"
options={{
title: "Invoice",
headerLeft: () => (
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() =>
router.navigate(`/investment/(tabs)/transaction`)
header: () => (
<AppHeader
title="Invoice"
left={
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() =>
router.navigate(`/investment/(tabs)/transaction`)
}
/>
}
/>
),
@@ -320,14 +303,18 @@ export default function UserLayout() {
<Stack.Screen
name="investment/[id]/(transaction-flow)/process"
options={{
title: "Proses",
headerLeft: () => (
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() =>
router.navigate(`/investment/(tabs)/transaction`)
header: () => (
<AppHeader
title="Proses"
left={
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() =>
router.navigate(`/investment/(tabs)/transaction`)
}
/>
}
/>
),
@@ -336,23 +323,20 @@ export default function UserLayout() {
<Stack.Screen
name="investment/[id]/(transaction-flow)/success"
options={{
title: "Transaksi Berhasil",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Transaksi Berhasil" />,
}}
/>
<Stack.Screen
name="investment/[id]/(transaction-flow)/failed"
options={{
title: "Transaksi Gagal",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Transaksi Gagal" />,
}}
/>
<Stack.Screen
name="investment/[id]/(my-holding)/[id]"
options={{
title: "Detail Saham Saya",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Detail Saham Saya" />,
}}
/>
{/* ========== End Investment Section ========= */}
@@ -361,122 +345,111 @@ export default function UserLayout() {
<Stack.Screen
name="donation/(tabs)"
options={{
title: "Donasi",
headerLeft: () => <BackButton path="/crowdfunding" />,
header: () => <AppHeader title="Donasi" left={<BackButton path="/crowdfunding" />} />,
}}
/>
<Stack.Screen
name="donation/create"
options={{
title: "Tambah Donasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Donasi" />,
}}
/>
<Stack.Screen
name="donation/create-story"
options={{
title: "Tambah Donasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Donasi" />,
}}
/>
<Stack.Screen
name="donation/[id]/edit"
options={{
title: "Edit Donasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Donasi" />,
}}
/>
<Stack.Screen
name="donation/[id]/edit-story"
options={{
title: "Edit Donasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Donasi" />,
}}
/>
<Stack.Screen
name="donation/[id]/edit-rekening"
options={{
title: "Edit Rekening",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Rekening" />,
}}
/>
<Stack.Screen
name="donation/[id]/detail-story"
options={{
title: "Cerita Penggalang",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Cerita Penggalang" />,
}}
/>
<Stack.Screen
name="donation/[id]/infromation-fundrising"
options={{
title: "Informasi Penggalang Dana",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Informasi Penggalang Dana" />,
}}
/>
<Stack.Screen
name="donation/[id]/list-of-donatur"
options={{
title: "Daftar Donatur",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Daftar Donatur" />,
}}
/>
<Stack.Screen
name="donation/[id]/fund-disbursement"
options={{
title: "Pencairan Dana",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Pencairan Dana" />,
}}
/>
<Stack.Screen
name="donation/[id]/(news)/recap-of-news"
options={{
title: "Rekap Kabar",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Rekap Kabar" />,
}}
/>
<Stack.Screen
name="donation/[id]/(news)/add-news"
options={{
title: "Tambah Berita",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Berita" />,
}}
/>
<Stack.Screen
name="donation/[id]/(news)/[news]/edit-news"
options={{
title: "Edit Berita",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Berita" />,
}}
/>
<Stack.Screen
name="donation/[id]/(transaction-flow)/index"
options={{
title: "Donasi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Donasi" />,
}}
/>
<Stack.Screen
name="donation/[id]/(transaction-flow)/select-bank"
options={{
title: "Pilih Bank",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Pilih Bank" />,
}}
/>
<Stack.Screen
name="donation/[id]/(transaction-flow)/[invoiceId]/invoice"
options={{
title: "Invoice",
headerLeft: () => (
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
header: () => (
<AppHeader
title="Invoice"
left={
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
/>
}
/>
),
}}
@@ -484,13 +457,17 @@ export default function UserLayout() {
<Stack.Screen
name="donation/[id]/(transaction-flow)/[invoiceId]/process"
options={{
title: "Proses",
headerLeft: () => (
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
header: () => (
<AppHeader
title="Proses"
left={
<Ionicons
name="close"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
/>
}
/>
),
}}
@@ -498,55 +475,51 @@ export default function UserLayout() {
<Stack.Screen
name="donation/[id]/(transaction-flow)/[invoiceId]/success"
options={{
title: "Donasi Berhasil",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Donasi Berhasil" />,
}}
/>
<Stack.Screen
name="donation/[id]/(transaction-flow)/[invoiceId]/failed"
options={{
title: "Donasi Gagal",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Donasi Gagal" />,
}}
/>
{/* ========== End Donation Section ========= */}
{/* ========== Job Section ========= */}
<Stack.Screen
name="job/create"
options={{
title: "Tambah Job",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Job" />,
}}
/>
<Stack.Screen
name="job/(tabs)"
options={{
title: "Job Vacancy",
// headerLeft: () => <BackButton path="/home" />,
// NOTE: headerLeft di pindahkan ke Tabs Layout
header: () => <AppHeader title="Job Vacancy" left={<BackButton path="/home" />} />,
}}
/>
<Stack.Screen
name="job/[id]/index"
options={{
title: "Detail Job",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Detail Job" />,
}}
/>
<Stack.Screen
name="job/[id]/edit"
options={{
title: "Edit Job",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Job" />,
}}
/>
<Stack.Screen
name="job/[id]/archive"
options={{
title: "Arsip Job",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Arsip Job" />,
}}
/>
@@ -556,78 +529,67 @@ export default function UserLayout() {
<Stack.Screen
name="forum/create"
options={{
title: "Tambah Diskusi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Diskusi" />,
}}
/>
<Stack.Screen
name="forum/[id]/edit"
options={{
title: "Edit Diskusi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Diskusi" />,
}}
/>
<Stack.Screen
name="forum/[id]/forumku"
options={{
title: "Forumku",
headerLeft: () => <BackButton icon={"close"} />,
header: () => <AppHeader title="Forumku" left={<BackButton icon={"close"} />} />,
}}
/>
<Stack.Screen
name="forum/[id]/index"
options={{
title: "Detail",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Detail" />,
}}
/>
<Stack.Screen
name="forum/[id]/report-commentar"
options={{
title: "Laporkan Komentar",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Laporkan Komentar" />,
}}
/>
<Stack.Screen
name="forum/[id]/other-report-commentar"
options={{
title: "Laporkan Komentar",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Laporkan Komentar" />,
}}
/>
<Stack.Screen
name="forum/[id]/report-posting"
options={{
title: "Laporkan Diskusi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Laporkan Diskusi" />,
}}
/>
<Stack.Screen
name="forum/[id]/other-report-posting"
options={{
title: "Laporkan Diskusi",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Laporkan Diskusi" />,
}}
/>
<Stack.Screen
name="forum/terms"
options={{
title: "Syarat & Ketentuan Forum",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Syarat & Ketentuan Forum" />,
}}
/>
<Stack.Screen
name="forum/[id]/preview-report-posting"
options={{
title: "Laporan Postingan",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Laporan Postingan" />,
}}
/>
<Stack.Screen
name="forum/[id]/preview-report-comment"
options={{
title: "Laporan Komentar",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Laporan Komentar" />,
}}
/>
@@ -635,29 +597,25 @@ export default function UserLayout() {
<Stack.Screen
name="maps/index"
options={{
title: "Maps",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Maps" />,
}}
/>
<Stack.Screen
name="maps/create"
options={{
title: "Tambah Maps",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Tambah Maps" />,
}}
/>
<Stack.Screen
name="maps/[id]/edit"
options={{
title: "Edit Maps",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Edit Maps" />,
}}
/>
<Stack.Screen
name="maps/[id]/custom-pin"
options={{
title: "Custom Pin Maps",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Custom Pin Maps" />,
}}
/>
@@ -665,8 +623,7 @@ export default function UserLayout() {
<Stack.Screen
name="marketplace/index"
options={{
title: "Market Place",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Market Place" />,
}}
/>
</Stack>

View File

@@ -2,35 +2,64 @@ import { IconHome } from "@/components/_Icon";
import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
function CollaborationTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
export default function CollaborationTabsLayout() {
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
}}
/>
<Tabs.Screen
name="participant"
options={{
title: "Partisipan",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="people" color={color} />
),
}}
/>
<Tabs.Screen
name="group"
options={{
title: "Grup",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="chatbox-ellipses" color={color} />
),
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="participant"
options={{
title: "Partisipan",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="people" color={color} />
),
}}
/>
<Tabs.Screen
name="group"
options={{
title: "Grup",
tabBarIcon: ({ color }) => (
<Ionicons size={20} name="chatbox-ellipses" color={color} />
),
}}
/>
</Tabs>
</View>
);
}
export default function CollaborationTabsLayout() {
return <CollaborationTabsWrapper />;
}

View File

@@ -10,6 +10,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { apiCollaborationGroup } from "@/service/api-client/api-collaboration";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useState, useCallback } from "react";
@@ -40,8 +41,7 @@ export default function CollaborationRoomInfo() {
<>
<Stack.Screen
options={{
title: `Info`,
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Info" left={<BackButton />} />,
}}
/>

View File

@@ -1,4 +1,5 @@
import { BackButton } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import ChatScreen from "@/screens/Collaboration/GroupChatSection";
@@ -12,14 +13,18 @@ export default function CollaborationRoomChat() {
<>
<Stack.Screen
options={{
title: `Proyek ${detail}`,
headerLeft: () => <BackButton />,
headerRight: () => (
<Feather
name="info"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() => router.push(`/collaboration/${id}/${detail}/info`)}
header: () => (
<AppHeader
title={`Proyek ${detail}`}
left={<BackButton />}
right={
<Feather
name="info"
size={ICON_SIZE_SMALL}
color={MainColor.yellow}
onPress={() => router.push(`/collaboration/${id}/${detail}/info`)}
/>
}
/>
),
}}

View File

@@ -6,6 +6,7 @@ import {
MenuDrawerDynamicGrid,
ViewWrapper
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import { apiCollaborationGetOne } from "@/service/api-client/api-collaboration";
import { Ionicons } from "@expo/vector-icons";
@@ -38,10 +39,14 @@ export default function CollaborationDetailParticipant() {
<>
<Stack.Screen
options={{
title: "Detail Proyek",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
header: () => (
<AppHeader
title="Detail Proyek"
left={<BackButton />}
right={
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
}
/>
),
}}
/>

View File

@@ -8,6 +8,7 @@ import {
Spacing,
ViewWrapper
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconEdit } from "@/components/_Icon";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import {
@@ -66,9 +67,13 @@ export default function CollaborationDetailProjectMain() {
<>
<Stack.Screen
options={{
title: "Proyek Saya",
headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
header: () => (
<AppHeader
title="Proyek Saya"
left={<BackButton />}
right={<DotButton onPress={() => setOpenDrawer(true)} />}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -9,6 +9,7 @@ import {
MenuDrawerDynamicGrid,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { useAuth } from "@/hooks/use-auth";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import {
@@ -74,10 +75,14 @@ export default function CollaborationDetail() {
<>
<Stack.Screen
options={{
title: "Detail Proyek",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerMenu(true)} />
header: () => (
<AppHeader
title="Detail Proyek"
left={<BackButton />}
right={
<DotButton onPress={() => setOpenDrawerMenu(true)} />
}
/>
),
}}
/>

View File

@@ -5,33 +5,62 @@ import {
FontAwesome5
} from "@expo/vector-icons";
import { Tabs } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
function DonationTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
export default function InvestmentTabsLayout() {
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Galang Dana",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="my-donation"
options={{
title: "Donasi Saya",
tabBarIcon: ({ color }) => (
<FontAwesome5 name="donate" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Galang Dana",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="my-donation"
options={{
title: "Donasi Saya",
tabBarIcon: ({ color }) => (
<FontAwesome5 name="donate" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
</Tabs>
</View>
);
}
export default function DonationTabsLayout() {
return <DonationTabsWrapper />;
}

View File

@@ -11,6 +11,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconEdit } from "@/components/_Icon";
import { IconTrash } from "@/components/_Icon/IconTrash";
import { useAuth } from "@/hooks/use-auth";
@@ -57,12 +58,17 @@ export default function DonationNews() {
<>
<Stack.Screen
options={{
title: "Detail Kabar",
headerLeft: () => <BackButton />,
headerRight: () =>
user?.id === data?.authorId && (
<DotButton onPress={() => setOpenDrawer(true)} />
),
header: () => (
<AppHeader
title="Detail Kabar"
left={<BackButton />}
right={
user?.id === data?.authorId && (
<DotButton onPress={() => setOpenDrawer(true)} />
)
}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -7,6 +7,7 @@ import {
NewWrapper,
Spacing,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
@@ -97,14 +98,19 @@ export default function DonasiDetailStatus() {
<>
<Stack.Screen
options={{
title: `Detail ${_.startCase(status as string)}`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null,
header: () => (
<AppHeader
title={`Detail ${_.startCase(status as string)}`}
left={<BackButton />}
right={
status === "draft" ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null
}
/>
),
}}
/>
<NewWrapper

View File

@@ -185,7 +185,6 @@ export default function DonationEdit() {
return (
<NewWrapper
hideFooter
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom

View File

@@ -10,6 +10,7 @@ import {
StackCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconNews } from "@/components/_Icon";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { useAuth } from "@/hooks/use-auth";
@@ -90,12 +91,17 @@ export default function DonasiDetailBeranda() {
<>
<Stack.Screen
options={{
title: `Detail Donasi`,
headerLeft: () => <BackButton />,
headerRight: () =>
user?.id === data?.Author?.id ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : null,
header: () => (
<AppHeader
title="Detail Donasi"
left={<BackButton />}
right={
user?.id === data?.Author?.id ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : null
}
/>
),
}}
/>
<NewWrapper footerComponent={buttonSection}>

View File

@@ -114,7 +114,6 @@ export default function DonationCreateStory() {
return (
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>

View File

@@ -127,7 +127,6 @@ export default function DonationCreate() {
return (
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>

View File

@@ -4,64 +4,87 @@ import {
IconHome,
IconStatus,
} from "@/components/_Icon";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles";
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
import { useLayoutEffect } from "react";
export default function EventTabsLayout() {
const navigation = useNavigation();
import { router, Tabs, useLocalSearchParams } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
import { OS_ANDROID_HEIGHT, OS_IOS_HEIGHT } from "@/constants/constans-value";
function EventTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
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
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: OS_IOS_HEIGHT,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: OS_ANDROID_HEIGHT + paddingBottom,
},
}),
header: () => (
<AppHeader
title="Event"
left={
<BackButtonFromNotification
from={from || ""}
category={category}
/>
}
/>
),
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
</View>
);
}
export default function EventTabsLayout() {
return <EventTabsWrapper />;
}

View File

@@ -10,6 +10,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton";
import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection";
@@ -81,12 +82,17 @@ export default function EventDetailStatus() {
<>
<Stack.Screen
options={{
title: `Detail ${status === "publish" ? "" : status}`,
headerLeft: () => <LeftButtonCustom />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : null,
header: () => (
<AppHeader
title={`Detail ${status === "publish" ? "" : status}`}
left={<LeftButtonCustom />}
right={
status === "draft" ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : null
}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -9,6 +9,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import {
@@ -265,13 +266,17 @@ export default function UserEventConfirmation() {
<>
<Stack.Screen
options={{
title: "Konfirmasi Event",
headerLeft: () => (
<Ionicons
name="arrow-back"
size={20}
color={MainColor.yellow}
onPress={() => router.navigate("/")}
header: () => (
<AppHeader
title="Konfirmasi Event"
left={
<Ionicons
name="arrow-back"
size={20}
color={MainColor.yellow}
onPress={() => router.navigate("/")}
/>
}
/>
),
}}

View File

@@ -7,6 +7,7 @@ import {
Spacing,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
@@ -49,9 +50,13 @@ export default function EventDetailContribution() {
<>
<Stack.Screen
options={{
title: `Detail kontribusi`,
headerLeft: () => <LeftButtonCustom />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
header: () => (
<AppHeader
title="Detail kontribusi"
left={<LeftButtonCustom />}
right={<DotButton onPress={() => setOpenDrawer(true)} />}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -6,6 +6,7 @@ import {
ViewWrapper,
Spacing,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
@@ -44,9 +45,13 @@ export default function EventDetailHistory() {
<>
<Stack.Screen
options={{
title: `Detail riwayat`,
headerLeft: () => <LeftButtonCustom />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
header: () => (
<AppHeader
title="Detail riwayat"
left={<LeftButtonCustom />}
right={<DotButton onPress={() => setOpenDrawer(true)} />}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -8,6 +8,7 @@ import {
MenuDrawerDynamicGrid,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import LeftButtonCustom from "@/components/Button/BackButton";
@@ -156,9 +157,13 @@ export default function EventDetailPublish() {
<>
<Stack.Screen
options={{
title: `Event Publish`,
headerLeft: () => <BackButton onPress={() => router.back()} />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
header: () => (
<AppHeader
title="Event Publish"
left={<BackButton onPress={() => router.back()} />}
right={<DotButton onPress={() => setOpenDrawer(true)} />}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -1,25 +1,27 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import { BasicWrapper, Spacing, StackCustom, ViewWrapper } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
import HeaderBell from "@/screens/Home/HeaderBell";
import HomeTabs from "@/screens/Home/HomeTabs";
import { stylesHome } from "@/screens/Home/homeViewStyle";
import Home_ImageSection from "@/screens/Home/imageSection";
import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList";
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
import { RefreshControl, ScrollView, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
export default function Application() {
const { token, user, userData } = useAuth();
@@ -27,6 +29,8 @@ export default function Application() {
const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore();
const [listData, setListData] = useState<any[] | null>(null);
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
useFocusEffect(
useCallback(() => {
@@ -104,98 +108,95 @@ export default function Application() {
);
}
// if (data && data?.masterUserRoleId !== "1") {
// console.log("User is not admin");
// return (
// <BasicWrapper>
// <Redirect href={`/admin/dashboard`} />
// </BasicWrapper>
// );
// }
return (
<>
<Stack.Screen
options={{
title: `HIPMI`,
headerLeft: () =>
data ? (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
headerRight: () =>
data ? (
<HeaderBell />
) : (
<CustomSkeleton height={30} width={30} radius={100} />
),
header: () => (
<AppHeader
title="HIPMI"
showBack={false}
left={
data ? (
<Ionicons
name="search"
size={20}
color={MainColor.yellow}
onPress={() => {
router.push("/user-search");
}}
/>
) : (
<CustomSkeleton height={30} width={30} radius={100} />
)
}
right={
data ? (
<HeaderBell />
) : (
<CustomSkeleton height={30} width={30} radius={100} />
)
}
/>
),
}}
/>
<ViewWrapper
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
footerComponent={
data && data ? (
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{
flexGrow: 1,
paddingInline: 10,
paddingBottom: paddingBottom + 80, // Space for tabs + safe area
}}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
) : (
<View style={GStyles.tabBar}>
<View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
{Array.from({ length: 4 }).map((e, index) => (
}
keyboardShouldPersistTaps="handled"
>
<StackCustom>
<Home_ImageSection />
{data && data ? (
<Home_FeatureSection />
) : (
<View style={stylesHome.gridContainer}>
{Array.from({ length: 4 }).map((_, index) => (
<CustomSkeleton
key={index}
height={40}
width={40}
radius={100}
style={stylesHome.gridItem}
radius={50}
/>
))}
</View>
</View>
)
}
>
<StackCustom>
<Home_ImageSection />
)}
{data && data ? (
<Home_FeatureSection />
) : (
<View style={stylesHome.gridContainer}>
{Array.from({ length: 4 }).map((item, index) => (
<CustomSkeleton
key={index}
style={stylesHome.gridItem}
radius={50}
/>
))}
</View>
)}
{data ? (
<Home_BottomFeatureSection listData={listData} />
) : (
<CustomSkeleton height={150} />
)}
</StackCustom>
</ScrollView>
{data ? (
<Home_BottomFeatureSection listData={listData} />
) : (
<CustomSkeleton height={200} />
)}
</StackCustom>
</ViewWrapper>
{/* Home Tabs di bawah */}
{data && data ? (
<HomeTabs
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
/>
) : (
<View style={{ height: 80 + paddingBottom, backgroundColor: MainColor.darkblue }} />
)}
</View>
</>
);
}

View File

@@ -4,80 +4,105 @@ import { TabsStyles } from "@/styles/tabs-styles";
import { Feather, FontAwesome6, Ionicons } from "@expo/vector-icons";
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
import { useLayoutEffect } from "react";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
export default function InvestmentTabsLayout() {
// const navigation = useNavigation();
function InvestmentTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
const navigation = useNavigation();
// const { from, category } = useLocalSearchParams<{
// from?: string;
// category?: string;
// }>();
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]);
// Atur header secara dinamis
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => (
<BackButtonFromNotification
from={from || ""}
category={category}
/>
),
});
}, [from, category, router, navigation]);
return (
<Tabs screenOptions={TabsStyles}>
<Tabs.Screen
name="index"
options={{
title: "Bursa",
tabBarIcon: ({ color }) => (
<Ionicons
name="bar-chart-outline"
color={color}
size={ICON_SIZE_SMALL}
/>
),
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
}}
/>
<Tabs.Screen
name="portofolio"
options={{
title: "Portofolio",
tabBarIcon: ({ color }) => (
<Feather name="pie-chart" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
<Tabs.Screen
name="my-holding"
options={{
title: "Saham Saya",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="hand-holding-dollar"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="transaction"
options={{
title: "Transaksi",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="money-bill-transfer"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Bursa",
tabBarIcon: ({ color }) => (
<Ionicons
name="bar-chart-outline"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="portofolio"
options={{
title: "Portofolio",
tabBarIcon: ({ color }) => (
<Feather name="pie-chart" color={color} size={ICON_SIZE_SMALL} />
),
}}
/>
<Tabs.Screen
name="my-holding"
options={{
title: "Saham Saya",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="hand-holding-dollar"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
<Tabs.Screen
name="transaction"
options={{
title: "Transaksi",
tabBarIcon: ({ color }) => (
<FontAwesome6
name="money-bill-transfer"
color={color}
size={ICON_SIZE_SMALL}
/>
),
}}
/>
</Tabs>
</View>
);
}
export default function InvestmentTabsLayout() {
return <InvestmentTabsWrapper />;
}

View File

@@ -10,6 +10,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { MainColor } from "@/constants/color-palet";
@@ -30,13 +31,13 @@ export default function InvestmentDetailHolding() {
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id, status])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetInvoice({
@@ -44,7 +45,7 @@ export default function InvestmentDetailHolding() {
authorId: user?.id,
category: "invoice",
});
console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
@@ -76,14 +77,19 @@ export default function InvestmentDetailHolding() {
<>
<Stack.Screen
options={{
title: `Detail ${_.startCase(status as string)}`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null,
header: () => (
<AppHeader
title={`Detail ${_.startCase(status as string)}`}
left={<BackButton />}
right={
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null
}
/>
),
}}
/>

View File

@@ -11,6 +11,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconTrash } from "@/components/_Icon/IconTrash";
import { useAuth } from "@/hooks/use-auth";
import {
@@ -56,12 +57,17 @@ export default function InvestmentNews() {
<>
<Stack.Screen
options={{
title: "Detail Berita",
headerLeft: () => <BackButton />,
headerRight: () =>
user?.id === data?.authorId && (
<DotButton onPress={() => setOpenDrawer(true)} />
),
header: () => (
<AppHeader
title="Detail Berita"
left={<BackButton />}
right={
user?.id === data?.authorId && (
<DotButton onPress={() => setOpenDrawer(true)} />
)
}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -6,6 +6,7 @@ import {
MenuDrawerDynamicGrid,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { MainColor } from "@/constants/color-palet";
@@ -106,14 +107,19 @@ export default function InvestmentDetailStatus() {
<>
<Stack.Screen
options={{
title: `Detail ${_.startCase(status as string)}`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null,
header: () => (
<AppHeader
title={`Detail ${_.startCase(status as string)}`}
left={<BackButton />}
right={
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null
}
/>
),
}}
/>

View File

@@ -6,6 +6,7 @@ import {
MenuDrawerDynamicGrid,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { MainColor } from "@/constants/color-palet";
@@ -105,14 +106,19 @@ export default function InvestmentDetail() {
<>
<Stack.Screen
options={{
title: `Detail ${_.startCase(status as string)}`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null,
header: () => (
<AppHeader
title={`Detail ${_.startCase(status as string)}`}
left={<BackButton />}
right={
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null
}
/>
),
}}
/>

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BackButton } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconHome, IconStatus } from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles";
@@ -7,31 +8,48 @@ import { Ionicons } from "@expo/vector-icons";
import {
router,
Tabs,
useLocalSearchParams,
useNavigation
useLocalSearchParams
} from "expo-router";
import { useLayoutEffect } from "react";
export default function JobTabsLayout() {
const navigation = useNavigation();
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
function JobTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
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}>
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
header: () => (
<AppHeader
title="Job Vacancy"
left={
<BackButtonFromNotification from={from || ""} category={category} />
}
/>
),
}}
>
<Tabs.Screen
name="index"
options={{
@@ -56,6 +74,10 @@ export default function JobTabsLayout() {
}}
/>
</Tabs>
</>
</View>
);
}
export default function JobTabsLayout() {
return <JobTabsWrapper />;
}

View File

@@ -9,6 +9,7 @@ import {
StackCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import ReportBox from "@/components/Box/ReportBox";
@@ -58,12 +59,17 @@ export default function JobDetailStatus() {
<>
<Stack.Screen
options={{
title: `Detail`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : null,
header: () => (
<AppHeader
title="Detail"
left={<BackButton />}
right={
status === "draft" ? (
<DotButton onPress={() => setOpenDrawer(true)} />
) : null
}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -1,365 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ActionIcon,
AvatarComp,
BaseBox,
ButtonCenteredOnly,
CenterCustom,
Grid,
InformationBox,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import pickImage from "@/utils/pickImage";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { Avatar } from "react-native-paper";
import { ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate";
export default function PortofolioCreate() {
const { id } = useLocalSearchParams();
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [inputValue, setInputValue] = useState<string>("");
const [data, setData] = useState({
namaBisnis: "",
masterBidangBisnisId: "",
alamatKantor: "",
tlpn: "",
deskripsi: "",
});
const [imageUri, setImageUri] = useState<string | null>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[]
>([]);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState([
{
id: "",
},
]);
const [dataMedsos, setDataMedsos] = useState({
facebook: "",
twitter: "",
instagram: "",
youtube: "",
tiktok: "",
});
const [isLoadingCreate, setIsLoadingCreate] = useState(false);
function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country);
}
useFocusEffect(
useCallback(() => {
onLoadMaster();
onLoadMasterSubBidangBisnis();
}, [])
);
const onLoadMaster = async () => {
try {
const response = await apiMasterBidangBisnis();
setBidangBisnis(response.data);
} catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const onLoadMasterSubBidangBisnis = async () => {
try {
const response = await apiMasterSubBidangBisnis({});
setSubBidangBisnis(response.data);
} catch (error) {
setSubBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
const selectedList = subBidangBisnis?.filter(
(item) => (item?.masterBidangBisnisId as any) === id
);
setSelectedSubBidang(selectedList as any[]);
};
return (
<NewWrapper
footerComponent={
<Portofolio_ButtonCreate
id={id as string}
data={data}
dataMedsos={dataMedsos}
imageUri={imageUri}
subBidangSelected={listSubBidangSelected}
isLoadingCreate={isLoadingCreate}
setIsLoadingCreate={setIsLoadingCreate}
/>
}
>
{/* <TextCustom>Portofolio Create {id}</TextCustom> */}
<StackCustom gap={"xs"}>
<InformationBox text="Lengkapi data bisnis anda." />
<TextInputCustom
required
label="Nama Bisnis"
placeholder="Masukkan nama bisnis"
onChangeText={(value: any) => setData({ ...data, namaBisnis: value })}
/>
<SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterBidangBisnisId}
onChange={(value) => {
const isSameBidang = data.masterBidangBisnisId === value;
if (!isSameBidang) {
setListSubBidangSelected([{ id: "" }]);
}
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}}
/>
{listSubBidangSelected.map((item, index) => (
<SelectCustom
key={index}
disabled={data.masterBidangBisnisId === ""}
label="Sub Bidang Usaha"
required
data={_.map(selectedSubBidang as any)
.filter((option: any) => {
const selectedValues = listSubBidangSelected.map((s) => s.id);
return (
option.id === item.id || // biarkan tetap muncul kalau ini valuenya sendiri
!selectedValues.includes(option.id)
);
})
.map((e: any) => ({
value: e.id,
label: e.name,
}))}
value={item.id || null}
onChange={(value) => {
const list = _.clone(listSubBidangSelected);
list[index].id = value as any;
setListSubBidangSelected(list);
}}
/>
))}
<CenterCustom>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
setListSubBidangSelected([
...listSubBidangSelected,
{ id: "" },
]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
const list = _.clone(listSubBidangSelected);
list.pop();
setListSubBidangSelected(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
{/* <SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={null}
onChange={(value) => {
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}}
/> */}
{/* <ButtonCenteredOnly
onPress={() => {
setListSubBidangSelected([...listSubBidangSelected, { id: "" }]);
}}
>
Tambah Pilihan
</ButtonCenteredOnly>
<Spacing /> */}
{/* <TextCustom>{JSON.stringify(bidangBisnis, null, 2)}</TextCustom> */}
<View>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextCustom semiBold style={{ color: MainColor.white_gray }}>
Nomor Telepon
</TextCustom>
<Text style={{ color: "red" }}> *</Text>
</View>
<Spacing height={5} />
<PhoneInput
value={inputValue}
onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
placeholder="xxx-xxx-xxx"
/>
</View>
<Spacing />
<TextInputCustom
required
label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis"
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/>
<TextAreaCustom
label="Deskripsi Bisnis"
placeholder="Masukkan deskripsi bisnis"
value={data.deskripsi}
onChangeText={(value: any) => setData({ ...data, deskripsi: value })}
autosize
minRows={2}
maxRows={5}
required
showCount
maxLength={1000}
/>
<Spacing />
{/* Logo */}
<InformationBox text="Upload logo bisnis anda untuk di tampilaka pada portofolio." />
<CenterCustom>
<Avatar.Image
source={imageUri ? { uri: imageUri } : DUMMY_IMAGE.dummy_image}
size={200}
/>
</CenterCustom>
<Spacing />
<ButtonCenteredOnly
icon="upload"
onPress={() => {
pickImage({
setImageUri,
});
}}
>
Upload
</ButtonCenteredOnly>
<Spacing height={40} />
{/* Social Media */}
<InformationBox text="Isi hanya pada sosial media yang anda miliki." />
<TextInputCustom
label="Tiktok"
placeholder="Masukkan username tiktok"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, tiktok: value })
}
/>
<TextInputCustom
label="Facebook"
placeholder="Masukkan username facebook"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, facebook: value })
}
/>
<TextInputCustom
label="Instagram"
placeholder="Masukkan username instagram"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, instagram: value })
}
/>
<TextInputCustom
label="Twitter"
placeholder="Masukkan username twitter"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, twitter: value })
}
/>
<TextInputCustom
label="Youtube"
placeholder="Masukkan username youtube"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, youtube: value })
}
/>
{/* <Spacing /> */}
</StackCustom>
</NewWrapper>
);
return <ScreenPortofolioCreate />;
}

View File

@@ -5,6 +5,7 @@ import {
ButtonCustom,
CenterCustom,
NewWrapper,
PhoneInputCustom,
SelectCustom,
Spacing,
StackCustom,
@@ -15,6 +16,7 @@ import {
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import { DEFAULT_COUNTRY, type CountryData, COUNTRIES } from "@/constants/countries";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
@@ -32,7 +34,6 @@ import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { ActivityIndicator } from "react-native-paper";
import Toast from "react-native-toast-message";
@@ -59,8 +60,8 @@ export default function PortofolioEdit() {
const { id } = useLocalSearchParams();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<any>({});
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [selectedCountry, setSelectedCountry] = useState<CountryData>(DEFAULT_COUNTRY);
const [bidangBisnis, setBidangBisnis] = useState<
IMasterBidangBisnis[] | null
>(null);
@@ -72,12 +73,42 @@ export default function PortofolioEdit() {
IListSubBidangSelected[]
>([]);
function handleInputValue(phoneNumber: string) {
setData({ ...data, tlpn: phoneNumber });
function handlePhoneChange(phone: string) {
setPhoneNumber(phone);
// Format phone number for API
const callingCode = selectedCountry.callingCode;
let fixNumber = phone.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
function handleSelectedCountry(country: ICountry) {
function handleCountryChange(country: CountryData) {
setSelectedCountry(country);
// Re-format with new country code
const callingCode = country.callingCode;
let fixNumber = phoneNumber.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
const onLoadMasterBidang = async () => {
@@ -122,8 +153,27 @@ export default function PortofolioEdit() {
const response = await apiGetOnePortofolio({ id: id });
if (response.success) {
const fixNumber = response.data.tlpn.replace("62", "");
setData({ ...response.data, tlpn: fixNumber });
// Extract phone number without country code for display
const fullNumber = response.data.tlpn;
let displayNumber = fullNumber;
let detectedCountry = DEFAULT_COUNTRY;
// Try to detect country from calling code
for (const country of COUNTRIES) {
if (fullNumber.startsWith(country.callingCode)) {
detectedCountry = country;
displayNumber = fullNumber.substring(country.callingCode.length);
break;
}
}
setSelectedCountry(detectedCountry);
// Remove leading zero if present
displayNumber = displayNumber.replace(/^0+/, "");
setPhoneNumber(displayNumber);
setData({ ...response.data, tlpn: displayNumber });
// Cek apakah ada sub bidang bisnis yang terpilih
const prevSubBidang = response.data.Portofolio_BidangDanSubBidangBisnis;
@@ -244,15 +294,11 @@ export default function PortofolioEdit() {
}
const handleSubmitUpdate = async () => {
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
let fixNumber = data.tlpn.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {
id_Portofolio: data.id_Portofolio,
namaBisnis: data.namaBisnis,
alamatKantor: data.alamatKantor,
tlpn: realNumber,
tlpn: data.tlpn, // Already formatted by PhoneInputCustom
deskripsi: data.deskripsi,
masterBidangBisnisId: data.masterBidangBisnisId,
subBidang: listSubBidangSelected,
@@ -435,12 +481,11 @@ export default function PortofolioEdit() {
<Text style={{ color: "red" }}> *</Text>
</View>
<Spacing height={5} />
<PhoneInput
value={data.tlpn}
onChangePhoneNumber={handleInputValue}
<PhoneInputCustom
value={phoneNumber}
onChangePhoneNumber={handlePhoneChange}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
onChangeCountry={handleCountryChange}
placeholder="xxx-xxx-xxx"
/>
</View>

View File

@@ -8,6 +8,7 @@ import {
StackCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
@@ -72,20 +73,23 @@ export default function Portofolio() {
{/* Header */}
<Stack.Screen
options={{
title: "Portofolio",
headerLeft: () => <LeftButtonCustom />,
headerRight: () =>
data?.Profile?.id !== profileId ? null : (
<TouchableOpacity onPress={openDrawer}>
<Ionicons
name="ellipsis-vertical"
size={20}
color={MainColor.yellow}
/>
</TouchableOpacity>
),
headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle,
header: () => (
<AppHeader
title="Portofolio"
left={<LeftButtonCustom />}
right={
data?.Profile?.id !== profileId ? null : (
<TouchableOpacity onPress={openDrawer}>
<Ionicons
name="ellipsis-vertical"
size={20}
color={MainColor.yellow}
/>
</TouchableOpacity>
)
}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -1,5 +1,5 @@
import AppHeader from "@/components/_ShareComponent/AppHeader";
import LeftButtonCustom from "@/components/Button/BackButton";
import { HeaderStyles } from "@/styles/header-styles";
import { Stack } from "expo-router";
export default function PortofolioLayout() {
@@ -7,8 +7,9 @@ export default function PortofolioLayout() {
<>
<Stack
screenOptions={{
...HeaderStyles,
headerLeft: () => <LeftButtonCustom />,
header: () => (
<AppHeader title="Portofolio" left={<LeftButtonCustom />} />
),
}}
>
{/* <Stack.Screen name="[id]/index" options={{ title: "Portofolio" }} /> */}

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { NewWrapper, StackCustom } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import LeftButtonCustom from "@/components/Button/BackButton";
import DrawerCustom from "@/components/Drawer/DrawerCustom";
@@ -101,18 +102,20 @@ export default function Profile() {
<>
<Stack.Screen
options={{
title: `Profile`,
headerLeft: () => <LeftButtonCustom />,
headerRight: () => (
<ButtonnDot
id={id as string}
openDrawer={openDrawer}
isUserCheck={isUserCheck()}
logout={logout}
header: () => (
<AppHeader
title="Profile"
left={<LeftButtonCustom />}
right={
<ButtonnDot
id={id as string}
openDrawer={openDrawer}
isUserCheck={isUserCheck()}
logout={logout}
/>
}
/>
),
headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle,
}}
/>
{/* Main View */}

View File

@@ -1,47 +1,39 @@
import { BackButton } from "@/components";
import { GStyles } from "@/styles/global-styles";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { Stack } from "expo-router";
export default function ProfileLayout() {
return (
<>
<Stack
screenOptions={{
headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle,
headerTitleAlign: "center",
headerBackButtonDisplayMode: "minimal",
}}
>
<Stack>
{/* <Stack.Screen name="[id]/index" options={{ headerShown: false }} /> */}
<Stack.Screen
name="[id]/edit"
options={{ title: "Edit Profile", headerLeft: () => <BackButton /> }}
options={{ header: () => <AppHeader title="Edit Profile" /> }}
/>
<Stack.Screen
name="[id]/update-photo"
options={{ title: "Update Foto", headerLeft: () => <BackButton /> }}
options={{ header: () => <AppHeader title="Update Foto" /> }}
/>
<Stack.Screen
name="[id]/update-background"
options={{
title: "Update Latar Belakang",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Update Latar Belakang" />,
}}
/>
<Stack.Screen
name="create"
options={{ title: "Buat Profile", headerBackVisible: false }}
options={{ headerBackVisible: false }}
/>
<Stack.Screen
name="[id]/blocked-list"
options={{ title: "Daftar Blokir", headerLeft: () => <BackButton /> }}
options={{ header: () => <AppHeader title="Daftar Blokir" /> }}
/>
<Stack.Screen
name="[id]/detail-blocked"
options={{ title: "Detail Blokir", headerLeft: () => <BackButton /> }}
options={{ header: () => <AppHeader title="Detail Blokir" /> }}
/>
</Stack>
</>

View File

@@ -4,64 +4,86 @@ import {
IconHome,
IconStatus,
} from "@/components/_Icon";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles";
import { Tabs, useLocalSearchParams, useNavigation, router } from "expo-router";
import { useLayoutEffect } from "react";
export default function VotingTabsLayout() {
const navigation = useNavigation();
import { router, Tabs, useLocalSearchParams } from "expo-router";
import { View } from "react-native";
import { MainColor } from "@/constants/color-palet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Platform } from "react-native";
function VotingTabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
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
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs
screenOptions={{
...TabsStyles,
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 12,
height: 80,
},
android: {
borderTopWidth: 0,
paddingTop: 5,
height: 70 + paddingBottom,
},
}),
header: () => (
<AppHeader
title="Voting"
left={
<BackButtonFromNotification
from={from || ""}
category={category}
/>
}
/>
),
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
>
<Tabs.Screen
name="index"
options={{
title: "Beranda",
tabBarIcon: ({ color }) => <IconHome color={color} />,
}}
/>
<Tabs.Screen
name="status"
options={{
title: "Status",
tabBarIcon: ({ color }) => <IconStatus color={color} />,
}}
/>
<Tabs.Screen
name="contribution"
options={{
title: "Kontribusi",
tabBarIcon: ({ color }) => <IconContribution color={color} />,
}}
/>
<Tabs.Screen
name="history"
options={{
title: "Riwayat",
tabBarIcon: ({ color }) => <IconHistory color={color} />,
}}
/>
</Tabs>
</View>
);
}
export default function VotingTabsLayout() {
return <VotingTabsWrapper />;
}

View File

@@ -12,6 +12,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import ReportBox from "@/components/Box/ReportBox";
@@ -103,14 +104,19 @@ export default function VotingDetailStatus() {
<>
<Stack.Screen
options={{
title: `Detail`,
headerLeft: () => <BackButton />,
headerRight: () =>
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null,
header: () => (
<AppHeader
title="Detail"
left={<BackButton />}
right={
status === "draft" ? (
<DotButton onPress={() => setOpenDrawerDraft(true)} />
) : status === "publish" ? (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
) : null
}
/>
),
}}
/>
<ViewWrapper>

View File

@@ -9,6 +9,7 @@ import {
Spacing,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
@@ -81,10 +82,14 @@ export default function VotingDetailContribution() {
<>
<Stack.Screen
options={{
title: "Detail Kontribusi",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
header: () => (
<AppHeader
title="Detail Kontribusi"
left={<BackButton />}
right={
<DotButton onPress={() => setOpenDrawerPublish(true)} />
}
/>
),
}}
/>

View File

@@ -9,6 +9,7 @@ import {
Spacing,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
@@ -82,10 +83,14 @@ export default function VotingDetailHistory() {
<>
<Stack.Screen
options={{
title: "Riwayat Voting",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
header: () => (
<AppHeader
title="Riwayat Voting"
left={<BackButton />}
right={
<DotButton onPress={() => setOpenDrawerPublish(true)} />
}
/>
),
}}
/>

View File

@@ -11,6 +11,7 @@ import {
StackCustom,
ViewWrapper,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconArchive, IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
@@ -142,10 +143,14 @@ export default function VotingDetail() {
<>
<Stack.Screen
options={{
title: `Detail Voting`,
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerPublish(true)} />
header: () => (
<AppHeader
title="Detail Voting"
left={<BackButton />}
right={
<DotButton onPress={() => setOpenDrawerPublish(true)} />
}
/>
),
}}
/>

View File

@@ -1,8 +1,8 @@
import { BackButton } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
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() {
@@ -20,7 +20,7 @@ export default function ApplicationLayout() {
function ApplicationStack() {
return (
<>
<Stack screenOptions={HeaderStyles}>
<Stack>
<Stack.Screen name="(user)" options={{ headerShown: false }} />
<Stack.Screen name="admin" options={{ headerShown: false }} />
@@ -28,8 +28,7 @@ function ApplicationStack() {
<Stack.Screen
name="(image)/take-picture/[id]/index"
options={{
title: "Ambil Gambar",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Ambil Gambar" />,
}}
/>
@@ -37,8 +36,7 @@ function ApplicationStack() {
<Stack.Screen
name="(image)/preview-image/[id]/index"
options={{
title: "Preview Gambar",
headerLeft: () => <BackButton />,
header: () => <AppHeader title="Preview Gambar" />,
}}
/>
</Stack>

View File

@@ -6,6 +6,7 @@ import {
StackCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
import NavbarMenu from "@/components/Drawer/NavbarMenu";
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
@@ -35,12 +36,28 @@ import { useState } from "react";
export default function AdminLayout() {
const [openDrawerNavbar, setOpenDrawerNavbar] = useState(false);
const [openDrawerUser, setOpenDrawerUser] = useState(false);
// const [user, setUser] = useState(null);
const { logout, user } = useAuth();
console.log("[USER LAYOUT]", JSON.stringify(user, null, 2));
const headerLeft = () => (
<Ionicons
name="menu"
size={ICON_SIZE_XLARGE}
color={MainColor.white}
onPress={() => setOpenDrawerNavbar(true)}
/>
);
const headerRight = () => (
<FontAwesome6
name="circle-user"
size={ICON_SIZE_MEDIUM}
color={MainColor.white}
onPress={() => setOpenDrawerUser(true)}
/>
);
return (
<>
<Stack
@@ -52,20 +69,33 @@ export default function AdminLayout() {
contentStyle: {
borderBottomColor: AccentColor.blue,
},
headerLeft: () => (
<Ionicons
name="menu"
size={ICON_SIZE_XLARGE}
color={MainColor.white}
onPress={() => setOpenDrawerNavbar(true)}
/>
),
headerRight: () => (
<FontAwesome6
name="circle-user"
size={ICON_SIZE_MEDIUM}
color={MainColor.white}
onPress={() => setOpenDrawerUser(true)}
// headerLeft: () => (
// <Ionicons
// name="menu"
// size={ICON_SIZE_XLARGE}
// color={MainColor.white}
// onPress={() => setOpenDrawerNavbar(true)}
// />
// ),
// headerRight: () => (
// <FontAwesome6
// name="circle-user"
// size={ICON_SIZE_MEDIUM}
// color={MainColor.white}
// onPress={() => setOpenDrawerUser(true)}
// />
// ),
header: () => (
<AppHeader
title="HIPMI DASHBOARD"
showBack={false}
left={headerLeft()}
right={headerRight()}
/>
),
}}

View File

@@ -1,4 +1,5 @@
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { router, Stack } from "expo-router";
export default function NotFoundScreen() {
@@ -15,7 +16,7 @@ export default function NotFoundScreen() {
return (
<>
<Stack.Screen
options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
options={{ header: () => <AppHeader title="" left={<BackButton onPress={() => handleBack()} />} /> }}
/>
<ViewWrapper>
<StackCustom

View File

@@ -41,6 +41,7 @@
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.9",
"libphonenumber-js": "^1.12.40",
"lodash": "^4.17.21",
"moti": "^0.30.0",
"react": "19.1.0",
@@ -1772,6 +1773,8 @@
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"libphonenumber-js": ["libphonenumber-js@1.12.40", "", {}, "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg=="],
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],

View File

@@ -12,14 +12,15 @@ export default function BackButtonFromNotification({
return (
<>
<BackButton
onPress={() => {
if (from === "notifications") {
router.replace(`/notifications?category=${category}`);
router.push(`/notifications?category=${category}`);
} else {
if (from) {
router.replace(`/${from}` as any);
router.back();
} else {
router.navigate("/home");
router.back();
}
}
}}

View File

@@ -0,0 +1,256 @@
import { MainColor } from "@/constants/color-palet";
import {
DEFAULT_COUNTRY,
searchCountries,
type CountryData,
} from "@/constants/countries";
import { useState } from "react";
import {
Modal,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
interface PhoneInputProps {
value: string;
onChangePhoneNumber: (phone: string) => void;
selectedCountry?: CountryData;
onChangeCountry: (country: CountryData) => void;
placeholder?: string;
disabled?: boolean;
}
export default function PhoneInputCustom({
value,
onChangePhoneNumber,
selectedCountry = DEFAULT_COUNTRY,
onChangeCountry,
placeholder = "Masukkan nomor",
disabled = false,
}: PhoneInputProps) {
const [countryPickerVisible, setCountryPickerVisible] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const filteredCountries = searchCountries(searchQuery);
const handleSelectCountry = (country: CountryData) => {
onChangeCountry(country);
setCountryPickerVisible(false);
setSearchQuery("");
};
const handlePhoneChange = (text: string) => {
// Only allow numbers and spaces
const cleaned = text.replace(/[^\d\s]/g, "");
onChangePhoneNumber(cleaned);
};
return (
<>
{/* Phone Input Field */}
<View style={styles.container}>
<TouchableOpacity
style={styles.countryPickerButton}
onPress={() => setCountryPickerVisible(true)}
disabled={disabled}
activeOpacity={0.7}
>
<Text style={styles.countryCodeText}>+{selectedCountry.callingCode}</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TextInput
style={[styles.phoneInput, disabled && styles.disabledInput]}
placeholder={placeholder}
placeholderTextColor={MainColor.placeholder}
value={value}
onChangeText={handlePhoneChange}
keyboardType="phone-pad"
autoComplete="tel"
importantForAutofill="yes"
editable={!disabled}
/>
</View>
{/* Country Picker Modal */}
<Modal
visible={countryPickerVisible}
transparent
animationType="slide"
onRequestClose={() => setCountryPickerVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Pilih Negara</Text>
<TouchableOpacity onPress={() => setCountryPickerVisible(false)}>
<Text style={styles.modalClose}></Text>
</TouchableOpacity>
</View>
<TextInput
style={styles.searchInput}
placeholder="Cari negara atau kode..."
placeholderTextColor={MainColor.placeholder}
value={searchQuery}
onChangeText={setSearchQuery}
autoFocus
/>
<ScrollView style={styles.countryList}>
{filteredCountries.map((country) => (
<TouchableOpacity
key={country.code}
style={[
styles.countryItem,
selectedCountry.code === country.code &&
styles.countryItemSelected,
]}
onPress={() => handleSelectCountry(country)}
activeOpacity={0.7}
>
<View style={styles.countryInfo}>
<Text style={styles.countryName}>{country.name}</Text>
<Text style={styles.countryCode}>+{country.callingCode}</Text>
</View>
{selectedCountry.code === country.code && (
<Text style={styles.checkmark}></Text>
)}
</TouchableOpacity>
))}
</ScrollView>
</View>
</View>
</Modal>
</>
);
}
const styles = StyleSheet.create({
// Container
container: {
flexDirection: "row",
backgroundColor: MainColor.white,
borderRadius: 8,
borderWidth: 1,
borderColor: MainColor.white_gray,
marginBottom: 16,
overflow: "hidden",
},
// Country Picker Button
countryPickerButton: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 14,
backgroundColor: MainColor.text_input,
borderRightWidth: 1,
borderRightColor: MainColor.white_gray,
},
countryCodeText: {
fontSize: 16,
color: MainColor.black,
fontWeight: "600",
},
dropdownIcon: {
fontSize: 18,
color: MainColor.placeholder,
marginLeft: 4,
},
// Divider
divider: {
width: 1,
backgroundColor: MainColor.white_gray,
},
// Phone Input
phoneInput: {
flex: 1,
paddingVertical: 14,
paddingHorizontal: 12,
fontSize: 16,
color: MainColor.black,
},
disabledInput: {
backgroundColor: MainColor.text_input,
color: MainColor.placeholder,
},
// Modal
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.5)",
justifyContent: "flex-end",
},
modalContent: {
backgroundColor: MainColor.white,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
maxHeight: "80%",
paddingBottom: 34,
},
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 20,
borderBottomWidth: 1,
borderBottomColor: MainColor.white_gray,
},
modalTitle: {
fontSize: 18,
fontWeight: "bold",
color: MainColor.black,
},
modalClose: {
fontSize: 24,
color: MainColor.placeholder,
padding: 5,
},
// Search Input
searchInput: {
backgroundColor: MainColor.text_input,
margin: 16,
padding: 12,
borderRadius: 8,
fontSize: 16,
color: MainColor.black,
},
// Country List
countryList: {
paddingHorizontal: 16,
},
countryItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 12,
borderBottomWidth: 1,
borderBottomColor: MainColor.white_gray,
},
countryItemSelected: {
backgroundColor: MainColor.soft_darkblue + "15",
},
countryInfo: {
flex: 1,
},
countryName: {
fontSize: 16,
color: MainColor.black,
fontWeight: "500",
},
countryCode: {
fontSize: 14,
color: MainColor.placeholder,
marginTop: 2,
},
checkmark: {
fontSize: 20,
color: MainColor.green,
fontWeight: "bold",
},
});

View File

@@ -0,0 +1,114 @@
import { MainColor } from "@/constants/color-palet";
import { Platform, StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BackButton } from "..";
type Props = {
title: string;
right?: React.ReactNode;
showBack?: boolean;
onPressLeft?: () => void;
left?: React.ReactNode;
};
export default function AppHeader({
title,
right,
showBack = true,
onPressLeft,
left,
}: Props) {
const insets = useSafeAreaInsets();
// iOS 16+ detection (Dynamic Island) - insets.top > 47 indicates Dynamic Island
const isIOS26Plus =
Platform.OS === "ios" && insets.top > 47;
// Dynamic padding berdasarkan platform dan iOS version
const paddingTop =
Platform.OS === "ios"
? isIOS26Plus
? insets.top - 10
: insets.top
: 40;
const paddingBottom = Platform.OS === "ios" ? 8 : 13;
return (
<View
style={[
{
backgroundColor: MainColor.darkblue,
paddingTop,
paddingBottom,
},
]}
pointerEvents="box-none"
>
{/* Header Container dengan absolute positioning untuk title center */}
<View style={styles.headerApp} pointerEvents="box-none">
{/* Left Section - Absolute Left */}
<View style={styles.headerLeft}>
{showBack ? (
<BackButton onPress={onPressLeft} />
) : left ? (
left
) : (
<View style={styles.placeholder} />
)}
</View>
{/* Title - Absolute Center */}
<View style={styles.headerCenter}>
<Text
style={styles.headerTitle}
numberOfLines={1}
ellipsizeMode="tail"
>
{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}
</Text>
</View>
{/* Right Section - Absolute Right */}
<View style={styles.headerRight}>
{right}
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
headerApp: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
height: 44, // Fixed height untuk consistency
},
headerLeft: {
position: "absolute",
left: 16,
zIndex: 1,
},
headerCenter: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
headerRight: {
position: "absolute",
right: 16,
zIndex: 1,
},
placeholder: {
width: 40,
height: 40,
},
headerTitle: {
color: MainColor.yellow,
fontSize: 18,
fontWeight: "600",
textAlign: "center",
},
});

View File

@@ -19,6 +19,7 @@ import {
SafeAreaView,
} from "react-native-safe-area-context";
import type { ScrollViewProps, FlatListProps } from "react-native";
import Spacing from "./Spacing";
// --- ✅ Tambahkan refreshControl ke BaseProps ---
interface BaseProps {
@@ -83,7 +84,7 @@ const NewWrapper = (props: NewWrapperProps) => {
return <View style={[GStyles.container, style]}>{content}</View>;
};
// 🔹 Mode Dinamis
// 🔹 Mode Dinamis (FlatList)
if ("listData" in props) {
const listProps = props as ListModeProps;
@@ -95,7 +96,7 @@ const NewWrapper = (props: NewWrapperProps) => {
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<View style={[GStyles.container, style]}>
<View style={[GStyles.container, style, { flex: 1 }]}>
<FlatList
data={listProps.listData}
renderItem={listProps.renderItem}
@@ -107,30 +108,36 @@ const NewWrapper = (props: NewWrapperProps) => {
return `fallback-${index}-${JSON.stringify(item)}`;
}
// Gabungkan ID dengan indeks untuk mencegah duplikasi
return `${String(item.id)}-${index}`;
})
}
refreshControl={refreshControl} // ✅ dari BaseProps
refreshControl={refreshControl}
onEndReached={listProps.onEndReached}
onEndReachedThreshold={0.5}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
contentContainerStyle={{ flexGrow: 1 }}
contentContainerStyle={{
flexGrow: 1,
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
}}
keyboardShouldPersistTaps="handled"
/>
</View>
{footerComponent ? (
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
>
{footerComponent}
</SafeAreaView>
) : hideFooter ? null : (
{/* Footer dengan position absolute untuk stay di bawah */}
{footerComponent && !hideFooter && (
<View style={styles.footerContainer}>
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
>
{footerComponent}
</SafeAreaView>
</View>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
@@ -144,7 +151,7 @@ const NewWrapper = (props: NewWrapperProps) => {
);
}
// 🔹 Mode Statis
// 🔹 Mode Statis (ScrollView)
const staticProps = props as StaticModeProps;
return (
@@ -156,24 +163,34 @@ const NewWrapper = (props: NewWrapperProps) => {
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl} // ✅ sekarang valid
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(staticProps.children)}
</TouchableWithoutFeedback>
</ScrollView>
{footerComponent ? (
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
<View style={{ flex: 0 }} collapsable={false}>
<ScrollView
contentContainerStyle={{
flexGrow: 1,
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
}}
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
>
{footerComponent}
</SafeAreaView>
) : hideFooter ? null : (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(staticProps.children)}
</TouchableWithoutFeedback>
</ScrollView>
</View>
{/* Footer dengan position absolute untuk stay di bawah */}
{footerComponent && !hideFooter && (
<View style={styles.footerContainer}>
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
>
{footerComponent}
</SafeAreaView>
</View>
)}
{!footerComponent && !hideFooter && (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
@@ -187,4 +204,15 @@ const NewWrapper = (props: NewWrapperProps) => {
);
};
// Styles untuk footer dengan position absolute
const styles = {
footerContainer: {
position: "absolute" as const,
bottom: 0,
left: 0,
right: 0,
backgroundColor: MainColor.darkblue,
},
};
export default NewWrapper;

View File

@@ -49,6 +49,8 @@ import MapCustom from "./Map/MapCustom";
import CenterCustom from "./Center/CenterCustom";
// Clickable
import ClickableCustom from "./Clickable/ClickableCustom";
// PhoneInput
import PhoneInputCustom from "./PhoneInput/PhoneInputCustom";
// Scroll
import ScrollableCustom from "./Scroll/ScrollCustom";
// ShareComponent
@@ -95,6 +97,8 @@ export {
CheckboxGroup,
// Clickable
ClickableCustom,
// PhoneInput
PhoneInputCustom,
// Container
CircleContainer,
// Divider

View File

@@ -23,8 +23,8 @@ export {
};
// OS Height
const OS_ANDROID_HEIGHT = 115
const OS_IOS_HEIGHT = 90
const OS_ANDROID_HEIGHT = 65
const OS_IOS_HEIGHT = 80
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
// Text Size

89
constants/countries.ts Normal file
View File

@@ -0,0 +1,89 @@
import { type CountryCode } from "libphonenumber-js";
/**
* Country data for phone number input
* Contains only country name and calling code (NO flags for maximum compatibility)
*/
export interface CountryData {
code: CountryCode;
name: string;
callingCode: string;
}
/**
* List of supported countries for phone number input
*
* @description
* This list includes major countries across different regions.
* Countries are ordered by likelihood of use (Indonesia first as default).
*
* @note
* NO emoji flags used - only text-based country name and calling code
* This ensures maximum compatibility across all platforms and iOS versions
*/
export const COUNTRIES: CountryData[] = [
// Asia Pacific (Primary markets)
{ code: "ID", name: "Indonesia", callingCode: "62" },
{ code: "SG", name: "Singapore", callingCode: "65" },
{ code: "MY", name: "Malaysia", callingCode: "60" },
{ code: "AU", name: "Australia", callingCode: "61" },
// Asia (Other)
{ code: "CN", name: "China", callingCode: "86" },
{ code: "JP", name: "Japan", callingCode: "81" },
{ code: "KR", name: "South Korea", callingCode: "82" },
{ code: "IN", name: "India", callingCode: "91" },
// Middle East
{ code: "AE", name: "United Arab Emirates", callingCode: "971" },
{ code: "SA", name: "Saudi Arabia", callingCode: "966" },
// Europe
{ code: "GB", name: "United Kingdom", callingCode: "44" },
{ code: "DE", name: "Germany", callingCode: "49" },
{ code: "FR", name: "France", callingCode: "33" },
{ code: "NL", name: "Netherlands", callingCode: "31" },
// Americas
{ code: "US", name: "United States", callingCode: "1" },
];
/**
* Default country for phone number input
* Used when no country is selected (Indonesia by default)
*/
export const DEFAULT_COUNTRY: CountryData = COUNTRIES[0];
/**
* Get country by calling code
* @param callingCode - The calling code to search for (e.g., "62", "1")
* @returns The matching country data or undefined if not found
*/
export function getCountryByCallingCode(callingCode: string): CountryData | undefined {
return COUNTRIES.find((country) => country.callingCode === callingCode);
}
/**
* Get country by country code (ISO 3166-1 alpha-2)
* @param code - The country code to search for (e.g., "ID", "US")
* @returns The matching country data or undefined if not found
*/
export function getCountryByCode(code: CountryCode): CountryData | undefined {
return COUNTRIES.find((country) => country.code === code);
}
/**
* Search countries by name or calling code
* @param query - The search query (case-insensitive)
* @returns Array of matching countries
*/
export function searchCountries(query: string): CountryData[] {
const normalizedQuery = query.toLowerCase().trim();
return COUNTRIES.filter(
(country) =>
country.name.toLowerCase().includes(normalizedQuery) ||
country.code.toLowerCase().includes(normalizedQuery) ||
country.callingCode.includes(normalizedQuery)
);
}

View File

@@ -55,10 +55,10 @@ Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
<!-- START Prompt Admin Refactoring -->
<!-- Pindah kode ke Screen Component -->
File source: app/(application)/admin/event/[id]/[status]/index.tsx
Folder tujuan: screens/Admin/Event
Nama file utama: ScreenEventDetail.tsx
Nama function utama: Admin_ScreenEventDetail
File source: app/(application)/(user)/portofolio/[id]/create.tsx
Folder tujuan: screens/Portofolio
Nama file utama: ScreenPortofolioCreate.tsx
Nama function utama: Admin_ScreenPortofolioCreate
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
@@ -120,4 +120,9 @@ Buatkan file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah na
<!-- END Create Box -->
<!-- Random Prompt -->
Diskusi pada file screens/Authentication/LoginView.tsx , tentang penggunaan phone number input. Karena tidak berfungsi dengan baik pada versi ios 26 keatas
<!-- END Random Prompt -->
<!-- END Use Prompt Now -->

File diff suppressed because one or more lines are too long

View File

@@ -1,15 +0,0 @@
{
"originHash" : "e70d3525c8e2819a8b34f22909815dab5c700c25a06c32388f3930f7b3627768",
"pins" : [
{
"identity" : "maplibre-gl-native-distribution",
"kind" : "remoteSourceControl",
"location" : "https://github.com/maplibre/maplibre-gl-native-distribution",
"state" : {
"revision" : "c68c970ff3ece56cfc3b36849db70167fa208beb",
"version" : "6.17.1"
}
}
],
"version" : 3
}

View File

@@ -6,7 +6,7 @@
<string>development</string>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:cld-dkr-hipmi-stg.wibudev.com</string>
<string>applinks:hipmi.muku.id</string>
</array>
</dict>
</plist>

View File

@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>4</string>
<string>7</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>

View File

@@ -1,15 +1,22 @@
use_modular_headers!
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require 'json'
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
def ccache_enabled?(podfile_properties)
# Environment variable takes precedence
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
# Fall back to Podfile properties
podfile_properties['apple.ccacheEnabled'] == 'true'
end
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
use_modular_headers!
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
prepare_react_native_project!
@@ -21,7 +28,10 @@ target 'HIPMIBadungConnect' do
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
else
config_command = [
'npx',
'node',
'--no-warnings',
'--eval',
'require(\'expo/bin/autolinking\')',
'expo-modules-autolinking',
'react-native-config',
'--json',
@@ -35,7 +45,6 @@ target 'HIPMIBadungConnect' do
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
@@ -44,23 +53,12 @@ target 'HIPMIBadungConnect' do
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
)
pod 'Firebase'
pod 'Firebase/Messaging'
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
post_install do |installer|
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
$MLRN.post_install(installer)
# @generated end @maplibre/maplibre-react-native:post-install
# Fix all script phases with incorrect paths
installer.pods_project.targets.each do |target|
target.build_phases.each do |phase|
next unless phase.respond_to?(:shell_script)
# Fix duplicated path issue
if phase.shell_script.include?('with-environment.sh')
# Remove any existing path and use proper relative path
phase.shell_script = phase.shell_script.gsub(
%r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},
'${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'
@@ -68,15 +66,14 @@ target 'HIPMIBadungConnect' do
end
end
end
# Standard React Native post install
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
$MLRN.post_install(installer)
# @generated end @maplibre/maplibre-react-native:post-install
react_native_post_install(
installer,
config[:reactNativePath],
:mac_catalyst_enabled => false,
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
:ccache_enabled => ccache_enabled?(podfile_properties),
)
end
# @generated end post_installer
end
end

View File

@@ -279,34 +279,11 @@ PODS:
- EXUpdatesInterface (2.0.0):
- ExpoModulesCore
- FBLazyVector (0.81.5)
- Firebase (12.8.0):
- Firebase/Core (= 12.8.0)
- Firebase/Core (12.8.0):
- Firebase/CoreOnly
- FirebaseAnalytics (~> 12.8.0)
- Firebase/CoreOnly (12.8.0):
- FirebaseCore (~> 12.8.0)
- Firebase/Messaging (12.8.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.8.0)
- FirebaseAnalytics (12.8.0):
- FirebaseAnalytics/Default (= 12.8.0)
- FirebaseCore (~> 12.8.0)
- FirebaseInstallations (~> 12.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.8.0):
- FirebaseCore (~> 12.8.0)
- FirebaseInstallations (~> 12.8.0)
- GoogleAppMeasurement/Default (= 12.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.8.0):
- FirebaseCoreInternal (~> 12.8.0)
- GoogleUtilities/Environment (~> 8.1)
@@ -329,33 +306,6 @@ PODS:
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAdsOnDeviceConversion (3.2.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.8.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.8.0):
- GoogleAdsOnDeviceConversion (~> 3.2.0)
- GoogleAppMeasurement/Core (= 12.8.0)
- GoogleAppMeasurement/IdentitySupport (= 12.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.8.0):
- GoogleAppMeasurement/Core (= 12.8.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
@@ -369,9 +319,6 @@ PODS:
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
@@ -2581,9 +2528,9 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- SDWebImage (5.21.6):
- SDWebImage/Core (= 5.21.6)
- SDWebImage/Core (5.21.6)
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- SDWebImageAVIFCoder (0.11.1):
- libavif/core (>= 0.11.0)
- SDWebImage (~> 5.10)
@@ -2633,8 +2580,6 @@ DEPENDENCIES:
- ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`)
- EXUpdatesInterface (from `../node_modules/expo-updates-interface/ios`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- Firebase
- Firebase/Messaging
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- "maplibre-react-native (from `../node_modules/@maplibre/maplibre-react-native`)"
- RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
@@ -2722,14 +2667,11 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreExtension
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleUtilities
- libavif
@@ -3011,14 +2953,11 @@ SPEC CHECKSUMS:
EXUpdatesInterface: 5adf50cb41e079c861da6d9b4b954c3db9a50734
FBLazyVector: e95a291ad2dadb88e42b06e0c5fb8262de53ec12
Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d
FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc
FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c
FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2
FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21
FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0
FirebaseMessaging: 7f42cfd10ec64181db4e01b305a613791c8e782c
GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f
GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
@@ -3107,13 +3046,13 @@ SPEC CHECKSUMS:
RNSVG: 31d6639663c249b7d5abc9728dde2041eb2a3c34
RNVectorIcons: 4351544f100d4f12cac156a7c13399e60bab3e26
RNWorklets: 43cd6af94c18f89cbca10ea83fee281b69d75da5
SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
Yoga: 5934998fbeaef7845dbf698f698518695ab4cd1a
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
PODFILE CHECKSUM: c099c57001b36661ca723fa0edfdb338496e8b9d
PODFILE CHECKSUM: 98fc0b2be4d9f9b5a23816e3c77ad0e74ea84fa0
COCOAPODS: 1.16.2

View File

@@ -48,6 +48,7 @@
"expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.9",
"libphonenumber-js": "^1.12.40",
"lodash": "^4.17.21",
"moti": "^0.30.0",
"react": "19.1.0",

287
plugins/withCustomConfig.js Normal file
View File

@@ -0,0 +1,287 @@
const {
withAppBuildGradle,
withProjectBuildGradle,
withInfoPlist,
} = require("@expo/config-plugins");
const { withPodfile } = require("@expo/config-plugins");
const { withAndroidManifest } = require("@expo/config-plugins");
const { withDangerousMod } = require("@expo/config-plugins");
const fs = require("fs");
const path = require("path");
// ─────────────────────────────────────────
// 1. PROJECT-LEVEL build.gradle
// Tambah: google-services classpath + Mapbox maven
// ─────────────────────────────────────────
const withCustomProjectBuildGradle = (config) => {
return withProjectBuildGradle(config, (config) => {
let contents = config.modResults.contents;
// Tambah google-services classpath jika belum ada
if (!contents.includes("com.google.gms:google-services")) {
contents = contents.replace(
/classpath\('com\.android\.tools\.build:gradle'\)/,
`classpath('com.android.tools.build:gradle')
classpath 'com.google.gms:google-services:4.4.1'`,
);
}
// Tambah Mapbox maven repository jika belum ada
if (!contents.includes("api.mapbox.com")) {
contents = contents.replace(
/allprojects\s*\{[\s\S]*?repositories\s*\{/,
`allprojects {
repositories {
maven {
url 'https://api.mapbox.com/downloads/v2/releases/maven'
def token = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv('RNMAPBOX_MAPS_DOWNLOAD_TOKEN')
if (token) {
authentication { basic(BasicAuthentication) }
credentials {
username = 'mapbox'
password = token
}
}
}`,
);
}
config.modResults.contents = contents;
return config;
});
};
// ─────────────────────────────────────────
// 2. APP-LEVEL build.gradle
// Tambah: buildConfigField + google-services plugin
// ─────────────────────────────────────────
const withCustomAppBuildGradle = (config) => {
return withAppBuildGradle(config, (config) => {
let contents = config.modResults.contents;
// Tambah Mapbox packagingOptions
if (!contents.includes("rnmapbox/maps-libcpp")) {
contents = contents.replace(
/android\s*\{/,
`android {
// @generated begin @rnmapbox/maps-libcpp - expo prebuild (DO NOT MODIFY) sync-e24830a5a3e854b398227dfe9630aabfaa1cadd1
packagingOptions {
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
}
// @generated end @rnmapbox/maps-libcpp`,
);
}
// Tambah buildConfigField REACT_NATIVE_RELEASE_LEVEL
if (!contents.includes("REACT_NATIVE_RELEASE_LEVEL")) {
contents = contents.replace(
/defaultConfig\s*\{/,
`defaultConfig {
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\\"${`$`}{findProperty('reactNativeReleaseLevel') ?: 'stable'}\\""`,
);
}
// Tambah apply plugin google-services di akhir file
if (!contents.includes("com.google.gms.google-services")) {
contents += `\napply plugin: 'com.google.gms.google-services'\n`;
}
config.modResults.contents = contents;
return config;
});
};
// ─────────────────────────────────────────
// 3. Info.plist
// Tambah: custom URL schemes + deskripsi Bahasa Indonesia
// ─────────────────────────────────────────
const withCustomInfoPlist = (config) => {
return withInfoPlist(config, (config) => {
const plist = config.modResults;
// Custom URL Schemes
// Pastikan CFBundleURLTypes sudah ada, lalu tambahkan scheme custom
if (!plist.CFBundleURLTypes) {
plist.CFBundleURLTypes = [];
}
const hasHipmiScheme = plist.CFBundleURLTypes.some((entry) =>
entry.CFBundleURLSchemes?.includes("hipmimobile"),
);
if (!hasHipmiScheme) {
plist.CFBundleURLTypes.push({
CFBundleURLSchemes: ["hipmimobile", "com.anonymous.hipmi-mobile"],
});
}
// NSLocationWhenInUseUsageDescription — Bahasa Indonesia
plist.NSLocationWhenInUseUsageDescription =
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.";
// NSPhotoLibraryUsageDescription — Bahasa Indonesia (panjang)
plist.NSPhotoLibraryUsageDescription =
"Untuk mengunggah dokumen dan media bisnis seperti foto profil, logo usaha, poster lowongan, atau bukti transaksi di berbagai fitur aplikasi: Profile, Portofolio, Job Vacancy, Investasi, dan Donasi.";
plist.NSFaceIDUsageDescription =
"Allow $(PRODUCT_NAME) to access your Face ID biometric data.";
return config;
});
};
// ─────────────────────────────────────────
// 4. Android Manifest
// Tambah: backup rules untuk expo-secure-store
// ─────────────────────────────────────────
const withCustomManifest = (config) => {
return withAndroidManifest(config, (config) => {
const manifest = config.modResults.manifest;
const application = manifest.application[0];
// Tambah atribut backup untuk expo-secure-store
application.$["android:fullBackupContent"] =
"@xml/secure_store_backup_rules";
application.$["android:dataExtractionRules"] =
"@xml/secure_store_data_extraction_rules";
// Tambah tools:replace pada meta-data notification color
const metaDataList = application["meta-data"] || [];
const notifColorMeta = metaDataList.find(
(m) =>
m.$["android:name"] ===
"com.google.firebase.messaging.default_notification_color",
);
if (notifColorMeta) {
notifColorMeta.$["tools:replace"] = "android:resource";
}
return config;
});
};
// ─────────────────────────────────────────
// 5. Podfile
// Tambah: use_modular_headers!
// ─────────────────────────────────────────
const withCustomPodfile = (config) => {
return withPodfile(config, (config) => {
let contents = config.modResults.contents;
// Tambah use_modular_headers! jika belum ada
if (!contents.includes("use_modular_headers!")) {
contents = contents.replace(
/platform :ios/,
`use_modular_headers!\nplatform :ios`,
);
}
// Tambah Firebase pods jika belum ada
if (!contents.includes("pod 'Firebase/Messaging'")) {
contents = contents.replace(
/use_react_native_pods\!/,
`pod 'Firebase'\n pod 'Firebase/Messaging'\n\n use_react_native_pods!`,
);
}
// Tambah fix script with-environment.sh jika belum ada
// Tambah fix script with-environment.sh jika belum ada
if (!contents.includes("with-environment.sh")) {
const fixScript = [
"post_install do |installer|",
" # Fix all script phases with incorrect paths",
" installer.pods_project.targets.each do |target|",
" target.build_phases.each do |phase|",
" next unless phase.respond_to?(:shell_script)",
" if phase.shell_script.include?('with-environment.sh')",
" phase.shell_script = phase.shell_script.gsub(",
" %r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},",
" '${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'",
" )",
" end",
" end",
" end",
].join("\n");
contents = contents.replace(/post_install do \|installer\|/, fixScript);
}
config.modResults.contents = contents;
return config;
});
};
// ─────────────────────────────────────────
// 6. Android XML Files
// Tambah: secure_store_backup_rules.xml dan secure_store_data_extraction_rules.xml
// ─────────────────────────────────────────
const withSecureStoreXml = (config) => {
return withDangerousMod(config, [
"android",
(config) => {
const xmlDir = path.join(
config.modRequest.platformProjectRoot,
"app/src/main/res/xml",
);
// Buat folder jika belum ada
if (!fs.existsSync(xmlDir)) {
fs.mkdirSync(xmlDir, { recursive: true });
}
// Definisikan path variabel di sini ← INI yang kurang sebelumnya
const backupRulesPath = path.join(
xmlDir,
"secure_store_backup_rules.xml",
);
const dataExtractionPath = path.join(
xmlDir,
"secure_store_data_extraction_rules.xml",
);
// secure_store_backup_rules.xml
fs.writeFileSync(
backupRulesPath,
`<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="SECURESTORE"/>
</full-backup-content>`,
);
// secure_store_data_extraction_rules.xml
fs.writeFileSync(
dataExtractionPath,
`<?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>`,
);
return config;
},
]);
};
// ─────────────────────────────────────────
// EXPORT
// ─────────────────────────────────────────
module.exports = (config) => {
config = withCustomProjectBuildGradle(config);
config = withCustomAppBuildGradle(config);
config = withCustomManifest(config);
config = withSecureStoreXml(config);
config = withCustomInfoPlist(config);
config = withCustomPodfile(config);
return config;
};

View File

@@ -33,7 +33,10 @@ export function EventDetailQRCode({
const deepLinkURL = `${BASE_URL}/event/${id}/confirmation?userId=${userId}`;
// Toggle antara HTTPS link dan custom scheme
const qrValue = useHttpsLink ? httpsLink : deepLinkURL;
// const qrValue = useHttpsLink ? httpsLink : deepLinkURL;
const qrValue = deepLinkURL;
return (
<BaseBox>
@@ -46,7 +49,7 @@ export function EventDetailQRCode({
{qrValue}
</TextCustom>
<Spacing />
<StackCustom direction="row" gap="sm">
{/* <StackCustom direction="row" gap="sm">
<ButtonCustom
onPress={() => setUseHttpsLink(true)}
backgroundColor={useHttpsLink ? MainColor.yellow : "transparent"}
@@ -69,13 +72,13 @@ export function EventDetailQRCode({
>
Custom Scheme
</ButtonCustom>
</StackCustom>
<Spacing />
</StackCustom> */}
{/* <Spacing />
<TextCustom color="gray" align="center" size={"small"}>
{useHttpsLink
? "✅ Testing Universal Links/App Links (butuh .well-known config)"
: "🔧 Testing langsung (tanpa domain verification)"}
</TextCustom>
</TextCustom> */}
</BaseBox>
);
}

View File

@@ -128,12 +128,13 @@ export function Admin_ScreenEventDetail() {
);
}
return null;
return <Spacing height={100} />;
}, [status, id]);
return (
<>
<NewWrapper
hideFooter
headerComponent={headerComponent}
// footerComponent={
// <View style={{ paddingInline: 8 }}>

View File

@@ -9,6 +9,7 @@ import {
StackCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
@@ -121,12 +122,16 @@ export default function Admin_ScreenNotification() {
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
header: () => (
<AppHeader
title="Admin Notifikasi"
left={<BackButton />}
right={
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
}
/>
),
}}

View File

@@ -11,6 +11,7 @@ import {
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
@@ -169,12 +170,16 @@ export default function Admin_ScreenNotification2() {
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
// title: "Admin Notifikasi",
header: () => (
<AppHeader
title="Admin Notifikasi"
right={
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
}
/>
),
}}

View File

@@ -1,8 +1,9 @@
import { NewWrapper } from "@/components";
import { NewWrapper, PhoneInputCustom, ViewWrapper } from "@/components";
import ButtonCustom from "@/components/Button/ButtonCustom";
import ModalReactNative from "@/components/Modal/ModalReactNative";
import Spacing from "@/components/_ShareComponent/Spacing";
import { MainColor } from "@/constants/color-palet";
import { DEFAULT_COUNTRY, type CountryData } from "@/constants/countries";
import { useAuth } from "@/hooks/use-auth";
import { apiVersion, BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
@@ -10,16 +11,23 @@ import { openBrowser } from "@/utils/openBrower";
import versionBadge from "@/utils/viersionBadge";
import { Redirect } from "expo-router";
import { useEffect, useState } from "react";
import { RefreshControl, Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import {
KeyboardAvoidingView,
Platform,
RefreshControl,
Text,
View,
} from "react-native";
import { parsePhoneNumber } from "libphonenumber-js";
import Toast from "react-native-toast-message";
import EULASection from "./EULASection";
export default function LoginView() {
const url = BASE_URL;
const [version, setVersion] = useState<string>("");
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [inputValue, setInputValue] = useState<string>("");
const [selectedCountry, setSelectedCountry] =
useState<CountryData>(DEFAULT_COUNTRY);
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false);
const [modalVisible, setModalVisible] = useState(false);
@@ -43,41 +51,43 @@ export default function LoginView() {
async function handleRefresh() {
setRefreshing(true);
await onLoadVersion();
setInputValue("");
setPhoneNumber("");
setSelectedCountry(DEFAULT_COUNTRY);
setLoading(false);
setRefreshing(false);
}
function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber);
}
function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country);
}
async function validateData() {
if (inputValue.length === 0) {
if (phoneNumber.length === 0) {
return Toast.show({
type: "error",
text1: "Masukan nomor anda",
});
}
if (selectedCountry === null) {
return Toast.show({
type: "error",
text1: "Pilih negara",
});
}
if (inputValue.length < 9) {
if (phoneNumber.length < 9) {
return Toast.show({
type: "error",
text1: "Nomor tidak valid",
});
}
// Validate with libphonenumber-js
try {
const parsedNumber = parsePhoneNumber(phoneNumber, selectedCountry.code);
if (!parsedNumber || !parsedNumber.isValid()) {
return Toast.show({
type: "error",
text1: "Nomor tidak valid",
});
}
} catch (error) {
return Toast.show({
type: "error",
text1: "Format nomor tidak valid",
});
}
return true;
}
@@ -85,8 +95,17 @@ export default function LoginView() {
const isValid = await validateData();
if (!isValid) return;
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
// Format phone number with country code
const callingCode = selectedCountry.callingCode;
let fixNumber = phoneNumber.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
@@ -128,75 +147,84 @@ export default function LoginView() {
}
return (
<NewWrapper
withBackground
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 100 : 50}
style={{ flex: 1 }}
>
<View style={GStyles.authContainer}>
<View>
<View style={GStyles.authContainerTitle}>
<Text style={GStyles.authSubTitle}>WELCOME TO</Text>
<Spacing height={5} />
<Text style={GStyles.authTitle}>HIPMI BADUNG APPS</Text>
<Spacing height={5} />
<ViewWrapper
withBackground
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
<View style={[GStyles.authContainer, { paddingBottom: 40 }]}>
<View>
<View style={GStyles.authContainerTitle}>
<Text style={GStyles.authSubTitle}>WELCOME TO</Text>
<Spacing height={5} />
<Text style={GStyles.authTitle}>HIPMI BADUNG APPS</Text>
<Spacing height={5} />
</View>
<Spacing height={50} />
{version && (
<Text
style={{
position: "absolute",
bottom: 35,
right: 50,
fontSize: 10,
fontWeight: "thin",
fontStyle: "italic",
color: MainColor.white_gray,
}}
>
powered by muku.id
</Text>
)}
</View>
<Spacing height={50} />
<Text
style={{
position: "absolute",
bottom: 35,
right: 50,
fontSize: 10,
fontWeight: "thin",
fontStyle: "italic",
color: MainColor.white_gray,
}}
<Spacing height={20} />
<PhoneInputCustom
value={phoneNumber}
onChangePhoneNumber={setPhoneNumber}
selectedCountry={selectedCountry}
onChangeCountry={setSelectedCountry}
placeholder="Masukkan nomor"
/>
<Spacing />
<ButtonCustom
onPress={handleLogin}
disabled={loadingTerm}
isLoading={loading || loadingTerm}
>
{version} | powered by muku.id
Login
</ButtonCustom>
<Spacing height={50} />
<Text
style={{ ...GStyles.textLabel, textAlign: "center", fontSize: 12 }}
>
Dengan menggunakan aplikasi ini, Anda telah menyetujui{" "}
<Text
style={{
color: MainColor.yellow,
textDecorationLine: "underline",
}}
onPress={() => {
const toUrl = `${url}/terms-of-service.html`;
openBrowser(toUrl);
}}
>
Syarat & Ketentuan
</Text>{" "}
dan seluruh kebijakan privasi yang berlaku.
</Text>
</View>
<PhoneInput
value={inputValue}
onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
placeholder="Masukkan nomor"
/>
<Spacing />
<ButtonCustom
onPress={handleLogin}
disabled={loadingTerm}
isLoading={loading || loadingTerm}
>
Login
</ButtonCustom>
<Spacing height={50} />
<Text
style={{ ...GStyles.textLabel, textAlign: "center", fontSize: 12 }}
>
Dengan menggunakan aplikasi ini, Anda telah menyetujui{" "}
<Text
style={{
color: MainColor.yellow,
textDecorationLine: "underline",
}}
onPress={() => {
const toUrl = `${url}/terms-of-service.html`;
openBrowser(toUrl);
}}
>
Syarat & Ketentuan
</Text>{" "}
dan seluruh kebijakan privasi yang berlaku.
</Text>
</View>
</ViewWrapper>
<ModalReactNative isVisible={modalVisible}>
<EULASection
@@ -205,6 +233,6 @@ export default function LoginView() {
setLoadingTerm={setLoadingTerm}
/>
</ModalReactNative>
</NewWrapper>
</KeyboardAvoidingView>
);
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BackButton, DrawerCustom, MenuDrawerDynamicGrid } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconPlus } from "@/components/_Icon";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
@@ -52,8 +53,12 @@ export default function Donation_ScreenListOfNews({
<>
<Stack.Screen
options={{
title: "Daftar Kabar",
headerLeft: () => <BackButton />,
header: () => (
<AppHeader
title="Daftar Kabar"
left={<BackButton />}
/>
),
}}
/>
<NewWrapper

View File

@@ -5,6 +5,7 @@ import {
DrawerCustom,
MenuDrawerDynamicGrid,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconPlus } from "@/components/_Icon";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
@@ -61,9 +62,13 @@ export default function Donation_ScreenRecapOfNews({
<>
<Stack.Screen
options={{
title: "Rekap Kabar",
headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
header: () => (
<AppHeader
title="Rekap Kabar"
left={<BackButton />}
right={<DotButton onPress={() => setOpenDrawer(true)} />}
/>
),
}}
/>
<NewWrapper

View File

@@ -7,6 +7,7 @@ import {
LoaderCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { useAuth } from "@/hooks/use-auth";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
@@ -54,13 +55,17 @@ export default function Forum_ViewBeranda() {
<>
<Stack.Screen
options={{
title: "Forum",
headerLeft: () => <BackButton />,
headerRight: () => (
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
header: () => (
<AppHeader
title="Forum"
left={<BackButton />}
right={
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
}
/>
),
}}

View File

@@ -8,6 +8,7 @@ import {
StackCustom,
TextCustom, // ← gunakan NewWrapper yang sudah diperbaiki
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { useAuth } from "@/hooks/use-auth";
@@ -155,13 +156,17 @@ export default function Forum_ViewBeranda2() {
{/* 🔹 Header Navigation */}
<Stack.Screen
options={{
title: "Forum",
headerLeft: () => <BackButton />,
headerRight: () => (
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
header: () => (
<AppHeader
title="Forum"
left={<BackButton />}
right={
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
}
/>
),
}}

View File

@@ -4,6 +4,7 @@ import {
FloatingButton,
SearchInput,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
@@ -17,7 +18,7 @@ import _ from "lodash";
import { useEffect, useState } from "react";
import { RefreshControl, TouchableOpacity, View } from "react-native";
const PAGE_SIZE = 5;
const PAGE_SIZE = 10;
export default function Forum_ViewBeranda3() {
const { user } = useAuth();
@@ -84,18 +85,22 @@ export default function Forum_ViewBeranda3() {
<>
<Stack.Screen
options={{
title: "Forum",
headerLeft: () => <BackButton />,
headerRight: () => (
<TouchableOpacity
onPress={() => router.navigate(`/forum/${user?.id}/forumku`)}
>
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
</TouchableOpacity>
header: () => (
<AppHeader
title="Forum"
left={<BackButton />}
right={
<TouchableOpacity
onPress={() => router.navigate(`/forum/${user?.id}/forumku`)}
>
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
</TouchableOpacity>
}
/>
),
}}
/>

71
screens/Home/HomeTabs.tsx Normal file
View File

@@ -0,0 +1,71 @@
import { ICustomTab, ITabs } from "@/components/_Interface/types";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import React from "react";
import { Platform, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { MainColor } from "@/constants/color-palet";
interface HomeTabsProps {
tabs: ITabs[];
}
const CustomTab = ({ icon, label, isActive, onPress }: ICustomTab) => (
<TouchableOpacity
style={[GStyles.tabItem, isActive && GStyles.activeTab]}
onPress={onPress}
activeOpacity={0.7}
>
<View
style={[GStyles.iconContainer, isActive && GStyles.activeIconContainer]}
>
<Ionicons
name={icon as any}
size={18}
color={isActive ? "#fff" : "#666"}
/>
</View>
<Text style={[GStyles.tabLabel, isActive && GStyles.activeTabLabel]}>
{label}
</Text>
</TouchableOpacity>
);
/**
* Home Tabs Component dengan Safe Area handling
*
* Component ini menggunakan pattern yang sama dengan Expo Router Tabs
* untuk konsistensi safe area di Android
*/
export default function HomeTabs({ tabs }: HomeTabsProps) {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
return (
<View style={{ backgroundColor: MainColor.darkblue }}>
{/* Tabs content */}
<View style={GStyles.tabBar}>
<View style={GStyles.tabContainer}>
{tabs.map((e) => (
<CustomTab
key={e.id}
icon={e.icon}
label={e.label}
isActive={e.isActive}
onPress={() => {
// eslint-disable-next-line no-unused-expressions
e.disabled ? console.log("disabled") : router.push(e.path);
}}
/>
))}
</View>
</View>
{/* Safe area padding untuk Android */}
{Platform.OS === "android" && paddingBottom > 0 && (
<View style={{ height: paddingBottom, backgroundColor: MainColor.darkblue }} />
)}
</View>
);
}

View File

@@ -1,15 +1,17 @@
import { ClickableCustom, TextCustom } from "@/components";
import { CenterCustom, ClickableCustom, TextCustom } from "@/components";
import Spacing from "@/components/_ShareComponent/Spacing";
import { router } from "expo-router";
import { View } from "react-native";
import Icon from "react-native-vector-icons/FontAwesome";
import { stylesHome } from "./homeViewStyle";
import _ from "lodash";
export default function Home_BottomFeatureSection({
listData,
}: {
listData: any[] | null;
}) {
console.log("listData", JSON.stringify(listData, null, 2));
return (
<>
<ClickableCustom onPress={() => router.push("/job")}>
@@ -24,17 +26,23 @@ export default function Home_BottomFeatureSection({
<View style={stylesHome.vacancyList}>
{/* Vacancy Item 1 */}
{listData?.map((item: any, index: number) => (
<View style={stylesHome.vacancyItem} key={index}>
<View style={stylesHome.vacancyDetails}>
<TextCustom bold color="yellow" truncate size="large">
{item.title}
</TextCustom>
<Spacing height={5} />
<TextCustom truncate={2}>{item.deskripsi}</TextCustom>
{_.isEmpty(listData) ? (
<CenterCustom style={{ paddingBlock: 50 }}>
<TextCustom color="gray">Lowongan pekerjaan belum tersedia</TextCustom>
</CenterCustom>
) : (
listData?.map((item: any, index: number) => (
<View style={stylesHome.vacancyItem} key={index}>
<View style={stylesHome.vacancyDetails}>
<TextCustom bold color="yellow" truncate size="large">
{item.title}
</TextCustom>
<Spacing height={5} />
<TextCustom truncate={2}>{item.deskripsi}</TextCustom>
</View>
</View>
</View>
))}
))
)}
</View>
</View>
</ClickableCustom>

View File

@@ -94,7 +94,7 @@ export const stylesHome = StyleSheet.create({
jobVacancyHeader: {
flexDirection: "row",
alignItems: "center",
marginBottom: 16,
marginBottom: 20,
},
jobVacancyTitle: {
fontSize: 18,

View File

@@ -17,7 +17,7 @@ export default function Home_ImageSection() {
transition={1000}
style={{
width: "100%",
height: 120,
height: 150,
borderRadius: 10,
}}
/>

View File

@@ -5,7 +5,6 @@ import { router } from "expo-router";
import React from "react";
import { Text, TouchableOpacity, View } from "react-native";
const CustomTab = ({ icon, label, isActive, onPress }: ICustomTab) => (
<TouchableOpacity
style={[GStyles.tabItem, isActive && GStyles.activeTab]}
@@ -17,7 +16,7 @@ const CustomTab = ({ icon, label, isActive, onPress }: ICustomTab) => (
>
<Ionicons
name={icon as any}
size={20}
size={18}
color={isActive ? "#fff" : "#666"}
/>
</View>
@@ -30,8 +29,8 @@ const CustomTab = ({ icon, label, isActive, onPress }: ICustomTab) => (
export default function TabSection({ tabs }: { tabs: ITabs[] }) {
return (
<>
<View style={GStyles.tabBar}>
<View style={GStyles.tabContainer}>
<View style={GStyles.tabBar} pointerEvents="box-none">
<View style={GStyles.tabContainer} pointerEvents="box-none">
{tabs.map((e) => (
<CustomTab
key={e.id}

View File

@@ -6,6 +6,7 @@ import {
DrawerCustom,
MenuDrawerDynamicGrid
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconEdit } from "@/components/_Icon";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
@@ -125,14 +126,18 @@ export default function Investment_ScreenRecapOfDocument() {
<>
<Stack.Screen
options={{
title: "Rekap Dokumen",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton
onPress={() => {
setOpenDrawer(true);
setOpenDrawerBox(false);
}}
header: () => (
<AppHeader
title="Rekap Dokumen"
left={<BackButton />}
right={
<DotButton
onPress={() => {
setOpenDrawer(true);
setOpenDrawerBox(false);
}}
/>
}
/>
),
}}

View File

@@ -7,6 +7,7 @@ import {
MenuDrawerDynamicGrid,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconPlus } from "@/components/_Icon";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { usePagination } from "@/hooks/use-pagination";
@@ -64,9 +65,13 @@ export default function Investment_ScreenListOfNews({
<>
<Stack.Screen
options={{
title: "Daftar Berita",
headerLeft: () => <BackButton />,
// headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
header: () => (
<AppHeader
title="Daftar Berita"
left={<BackButton />}
// headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
/>
),
}}
/>

View File

@@ -9,6 +9,7 @@ import {
Spacing,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconPlus } from "@/components/_Icon";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { usePagination } from "@/hooks/use-pagination";
@@ -66,9 +67,13 @@ export default function Investment_ScreenRecapOfNews({
<>
<Stack.Screen
options={{
title: "Rekap Berita",
headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
header: () => (
<AppHeader
title="Rekap Berita"
left={<BackButton />}
right={<DotButton onPress={() => setOpenDrawer(true)} />}
/>
),
}}
/>
<NewWrapper

View File

@@ -11,6 +11,7 @@ import {
StackCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText";
@@ -217,12 +218,16 @@ export default function ScreenNotification_V1() {
<>
<Stack.Screen
options={{
title: "Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
header: () => (
<AppHeader
title="Notifikasi"
left={<BackButton />}
right={
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
}
/>
),
}}

View File

@@ -10,6 +10,7 @@ import {
StackCustom,
TextCustom,
} from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { IconDot } from "@/components/_Icon/IconComponent";
import { AccentColor, MainColor } from "@/constants/color-palet";
import {
@@ -182,12 +183,16 @@ export default function ScreenNotification_V2() {
<>
<Stack.Screen
options={{
title: "Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
header: () => (
<AppHeader
title="Notifikasi"
left={<BackButton />}
right={
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
}
/>
),
}}

View File

@@ -0,0 +1,367 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
ActionIcon,
ButtonCenteredOnly,
CenterCustom,
InformationBox,
NewWrapper,
PhoneInputCustom,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { DEFAULT_COUNTRY, type CountryData } from "@/constants/countries";
import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import pickImage from "@/utils/pickImage";
import { Ionicons } from "@expo/vector-icons";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { Text, View } from "react-native";
import { Avatar } from "react-native-paper";
export function ScreenPortofolioCreate() {
const { id } = useLocalSearchParams();
const [selectedCountry, setSelectedCountry] = useState<CountryData>(DEFAULT_COUNTRY);
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [data, setData] = useState({
namaBisnis: "",
masterBidangBisnisId: "",
alamatKantor: "",
tlpn: "",
deskripsi: "",
});
const [imageUri, setImageUri] = useState<string | null>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[]
>([]);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState([
{
id: "",
},
]);
const [dataMedsos, setDataMedsos] = useState({
facebook: "",
twitter: "",
instagram: "",
youtube: "",
tiktok: "",
});
const [isLoadingCreate, setIsLoadingCreate] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadMaster();
onLoadMasterSubBidangBisnis();
}, []),
);
const onLoadMaster = async () => {
try {
const response = await apiMasterBidangBisnis();
setBidangBisnis(response.data);
} catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const onLoadMasterSubBidangBisnis = async () => {
try {
const response = await apiMasterSubBidangBisnis({});
setSubBidangBisnis(response.data);
} catch (error) {
setSubBidangBisnis([]);
console.log("Error onLoadMasterSubBidangBisnis", error);
}
};
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
const selectedList = subBidangBisnis?.filter(
(item) => (item?.masterBidangBisnisId as any) === id,
);
setSelectedSubBidang(selectedList as any[]);
};
const handlePhoneChange = (phone: string) => {
setPhoneNumber(phone);
// Format phone number for API
const callingCode = selectedCountry.callingCode;
let fixNumber = phone.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
};
const handleCountryChange = (country: CountryData) => {
setSelectedCountry(country);
// Re-format with new country code
const callingCode = country.callingCode;
let fixNumber = phoneNumber.replace(/\s+/g, "").replace(/^0+/, "");
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
};
return (
<NewWrapper
footerComponent={
<Portofolio_ButtonCreate
id={id as string}
data={data}
dataMedsos={dataMedsos}
imageUri={imageUri}
subBidangSelected={listSubBidangSelected}
isLoadingCreate={isLoadingCreate}
setIsLoadingCreate={setIsLoadingCreate}
/>
}
>
<StackCustom gap="xs">
<InformationBox text="Lengkapi data bisnis anda." />
<TextInputCustom
required
label="Nama Bisnis"
placeholder="Masukkan nama bisnis"
onChangeText={(value: any) => setData({ ...data, namaBisnis: value })}
/>
<SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterBidangBisnisId}
onChange={(value) => {
const isSameBidang = data.masterBidangBisnisId === value;
if (!isSameBidang) {
setListSubBidangSelected([{ id: "" }]);
}
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}}
/>
{listSubBidangSelected.map((item, index) => (
<SelectCustom
key={index}
disabled={data.masterBidangBisnisId === ""}
label="Sub Bidang Usaha"
required
data={_.map(selectedSubBidang as any)
.filter((option: any) => {
const selectedValues = listSubBidangSelected.map((s) => s.id);
return (
option.id === item.id || !selectedValues.includes(option.id)
);
})
.map((e: any) => ({
value: e.id,
label: e.name,
}))}
value={item.id || null}
onChange={(value) => {
const list = _.clone(listSubBidangSelected);
list[index].id = value as any;
setListSubBidangSelected(list);
}}
/>
))}
<CenterCustom>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
setListSubBidangSelected([
...listSubBidangSelected,
{ id: "" },
]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
const list = _.clone(listSubBidangSelected);
list.pop();
setListSubBidangSelected(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
<View>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextCustom semiBold style={{ color: MainColor.white_gray }}>
Nomor Telepon
</TextCustom>
<Text style={{ color: "red" }}> *</Text>
</View>
<Spacing height={5} />
<PhoneInputCustom
value={phoneNumber}
onChangePhoneNumber={handlePhoneChange}
selectedCountry={selectedCountry}
onChangeCountry={handleCountryChange}
placeholder="xxx-xxx-xxx"
/>
</View>
<Spacing />
<TextInputCustom
required
label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis"
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/>
<TextAreaCustom
label="Deskripsi Bisnis"
placeholder="Masukkan deskripsi bisnis"
value={data.deskripsi}
onChangeText={(value: any) => setData({ ...data, deskripsi: value })}
autosize
minRows={2}
maxRows={5}
required
showCount
maxLength={1000}
/>
<Spacing />
<InformationBox text="Upload logo bisnis anda untuk di tampilaka pada portofolio." />
<CenterCustom>
<Avatar.Image
source={imageUri ? { uri: imageUri } : DUMMY_IMAGE.dummy_image}
size={200}
/>
</CenterCustom>
<Spacing />
<ButtonCenteredOnly
icon="upload"
onPress={() => {
pickImage({
setImageUri,
});
}}
>
Upload
</ButtonCenteredOnly>
<Spacing height={40} />
<InformationBox text="Isi hanya pada sosial media yang anda miliki." />
<TextInputCustom
label="Tiktok"
placeholder="Masukkan username tiktok"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, tiktok: value })
}
/>
<TextInputCustom
label="Facebook"
placeholder="Masukkan username facebook"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, facebook: value })
}
/>
<TextInputCustom
label="Instagram"
placeholder="Masukkan username instagram"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, instagram: value })
}
/>
<TextInputCustom
label="Twitter"
placeholder="Masukkan username twitter"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, twitter: value })
}
/>
<TextInputCustom
label="Youtube"
placeholder="Masukkan username youtube"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, youtube: value })
}
/>
</StackCustom>
</NewWrapper>
);
}

View File

@@ -12,6 +12,7 @@ import {
ICON_SIZE_SMALL,
PAGINATION_DEFAULT_TAKE,
} from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiAllUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons";
@@ -19,21 +20,89 @@ import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useRef, useState } from "react";
import { RefreshControl, View } from "react-native";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
const PAGE_SIZE = PAGINATION_DEFAULT_TAKE;
/**
* Render header dengan search input
*/
const renderHeader = (search: string, setSearch: (text: string) => void) => (
<TextInputCustom
value={search}
onChangeText={setSearch}
iconLeft={
<Ionicons
name="search"
size={ICON_SIZE_SMALL}
color={MainColor.placeholder}
/>
}
placeholder="Cari Pengguna"
borderRadius={50}
containerStyle={{ marginBottom: 0 }}
/>
);
/**
* Render item user
*/
const renderItem = ({ item }: { item: any }) => (
<View
style={{
backgroundColor: MainColor.soft_darkblue,
borderRadius: 8,
padding: 12,
marginBottom: 10,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
}}
>
<ClickableCustom
onPress={() => {
router.push(`/profile/${item?.Profile?.id}`);
}}
>
<Grid>
<Grid.Col span={2}>
<AvatarComp fileId={item?.Profile?.imageId} size="base" />
</Grid.Col>
<Grid.Col span={9}>
<StackCustom gap={"sm"}>
<TextCustom size="large">{item?.username}</TextCustom>
<TextCustom size="small">+{item?.nomor}</TextCustom>
{item?.Profile?.businessField && (
<TextCustom size="small">
{item?.Profile?.businessField}
</TextCustom>
)}
</StackCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
}}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.placeholder}
/>
</Grid.Col>
</Grid>
</ClickableCustom>
</View>
);
export default function UserSearchMainView_V2() {
const isInitialMount = useRef(true);
const [search, setSearch] = useState("");
const {
listData,
loading,
refreshing,
hasMore,
onRefresh,
loadMore,
isInitialLoad,
} = usePagination({
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
const response = await apiAllUser({
page: String(page),
@@ -41,127 +110,50 @@ export default function UserSearchMainView_V2() {
});
return response;
},
pageSize: PAGINATION_DEFAULT_TAKE,
pageSize: PAGE_SIZE,
searchQuery: search,
});
// 🔁 Refresh otomatis saat kembali ke halaman ini
useFocusEffect(
useCallback(() => {
if (isInitialMount.current) {
// Skip saat pertama kali mount
isInitialMount.current = false;
return;
}
// Hanya refresh saat kembali dari screen lain
onRefresh();
}, [onRefresh]),
);
const renderHeader = () => (
<>
<TextInputCustom
value={search}
onChangeText={setSearch}
iconLeft={
<Ionicons
name="search"
size={ICON_SIZE_SMALL}
color={MainColor.placeholder}
/>
}
placeholder="Cari Pengguna"
borderRadius={50}
containerStyle={{ marginBottom: 0 }}
/>
</>
);
const renderItem = ({ item }: { item: any }) => (
<View
style={{
backgroundColor: MainColor.soft_darkblue,
borderRadius: 8,
padding: 12,
marginBottom: 10,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
// height: 100
}}
>
<ClickableCustom
onPress={() => {
console.log("Ke Profile");
router.push(`/profile/${item?.Profile?.id}`);
}}
>
<Grid>
<Grid.Col span={2}>
<AvatarComp fileId={item?.Profile?.imageId} size="base" />
</Grid.Col>
<Grid.Col span={9}>
<StackCustom gap={"sm"}>
<TextCustom size="large">{item?.username}</TextCustom>
<TextCustom size="small">+{item?.nomor}</TextCustom>
{item?.Profile?.businessField && (
<TextCustom size="small">
{item?.Profile?.businessField}
</TextCustom>
)}
</StackCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
}}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.placeholder}
/>
</Grid.Col>
</Grid>
</ClickableCustom>
</View>
);
// useFocusEffect(
// useCallback(() => {
// if (isInitialMount.current) {
// isInitialMount.current = false;
// return;
// }
// pagination.onRefresh();
// }, [pagination.onRefresh]),
// );
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading,
refreshing,
listData,
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
searchQuery: search,
emptyMessage: "Tidak ada pengguna ditemukan",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
loadingFooterText: "Memuat lebih banyak pengguna...",
isInitialLoad,
isInitialLoad: pagination.isInitialLoad,
});
return (
<>
<NewWrapper
headerComponent={renderHeader()}
listData={listData}
renderItem={renderItem}
onEndReached={loadMore}
refreshControl={
<RefreshControl
progressBackgroundColor={MainColor.yellow}
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
ListFooterComponent={ListFooterComponent}
ListEmptyComponent={ListEmptyComponent}
/>
</>
<NewWrapper
headerComponent={renderHeader(search, setSearch)}
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
progressBackgroundColor={MainColor.yellow}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
ListFooterComponent={ListFooterComponent}
ListEmptyComponent={ListEmptyComponent}
/>
);
}

View File

@@ -6,13 +6,13 @@ export async function apiUser(id: string) {
}
export async function apiAllUser({
page,
page = "1",
search,
}: {
page?: string;
search?: string;
}) {
const pageQuery = page ? `?page=${page}` : "";
const pageQuery = `?page=${page}`;
const searchQuery = search ? `&search=${search}` : "";
try {

View File

@@ -159,7 +159,7 @@ export const GStyles = StyleSheet.create({
transform: [{ scale: 1.05 }],
},
iconContainer: {
padding: 8,
padding: 5,
borderRadius: 20,
// marginBottom: 4,
},
@@ -196,18 +196,18 @@ export const GStyles = StyleSheet.create({
// =============== BOTTOM BAR =============== //
bottomBar: {
backgroundColor: MainColor.darkblue,
borderTopColor: AccentColor.blue,
// borderTopWidth: 0.5,
borderTopColor: AccentColor.darkblue,
borderTopWidth: 1,
height: "100%",
justifyContent: "center",
shadowColor: AccentColor.blue,
shadowOffset: { width: 0, height: -5},
shadowOpacity: 0.4,
shadowRadius: 40,
elevation: 8, // untuk Android
// elevation: 8, // untuk Android
},
bottomBarContainer: {
paddingHorizontal: 15,
paddingHorizontal: 25,
paddingVertical: 10,
},
// =============== BOTTOM BAR =============== //

View File

@@ -11,7 +11,7 @@ export const TabsStyles: BottomTabNavigationOptions = {
tabBarStyle: Platform.select({
ios: {
borderTopWidth: 0,
paddingTop: 5,
paddingTop: 12,
height: OS_IOS_HEIGHT,
},
android: {
@@ -19,7 +19,6 @@ export const TabsStyles: BottomTabNavigationOptions = {
paddingTop: 5,
height: OS_ANDROID_HEIGHT,
},
default: {},
}),
tabBarBackground: TabBarBackground,
};

58
tasks/README.md Normal file
View File

@@ -0,0 +1,58 @@
# Tasks Directory
Direktori ini berisi task list untuk development dan perbaikan aplikasi HIPMI Mobile.
## 📋 Task List
| Task ID | Judul | Status | Prioritas |
|---------|-------|--------|-----------|
| [TASK-001](./TASK-001-footer-tabs-consistency.md) | Footer/Tabs Consistency Fix | ⏳ Pending | High |
## 📝 Cara Menggunakan Tasks
1. **Lihat task yang tersedia** di daftar atas
2. **Review task** untuk memahami scope dan acceptance criteria
3. **Kerjakan task** sesuai sub-tasks yang terdaftar
4. **Update status** setelah selesai
## ✅ Task Status Legend
-**Pending**: Task belum dimulai
- 🔄 **In Progress**: Task sedang dikerjakan
-**Completed**: Task selesai
-**Cancelled**: Task dibatalkan
- ⚠️ **Blocked**: Task terhambat dependency
## 📌 Task Template
Untuk membuat task baru, gunakan format berikut:
```markdown
# Task: [Judul Task]
## 📋 Deskripsi
[Jelaskan masalah/fitur]
## 🎯 Tujuan
[Tujuan yang ingin dicapai]
## 🔍 Analisis Masalah Saat Ini
[Analisis kondisi existing]
## 📝 Sub-Tasks
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
## ✅ Acceptance Criteria
1. [Criteria 1]
2. [Criteria 2]
## 📚 Referensi
[Link referensi]
## 🔄 Status
**Status**: ⏳ Pending
**Created**: YYYY-MM-DD
**Updated**: YYYY-MM-DD
```

View File

@@ -0,0 +1,159 @@
# Task: Footer/Tabs Consistency Fix
## 📋 Deskripsi
Memperbaiki masalah footer/tabs yang tidak konsisten di Android, terutama pada perangkat dengan navigasi button di bagian bawah.
## 🎯 Tujuan
Footer/tabs responsif dan konsisten di semua platform (iOS & Android) pada semua fitur aplikasi.
## 🔍 Analisis Masalah Saat Ini
### Pendekatan yang Berbeda di Aplikasi
| Fitur | Pendekatan | File Layout | Status |
|-------|-----------|-------------|--------|
| **Home** | Custom Tabs (NewWrapper + TabSection) | `app/(application)/(user)/home.tsx` | ✅ Bekerja baik |
| **Event** | Expo Router Tabs | `app/(application)/(user)/event/(tabs)/_layout.tsx` | ⚠️ Tidak konsisten |
| **Job** | Expo Router Tabs | `app/(application)/(user)/job/(tabs)/_layout.tsx` | ⚠️ Tidak konsisten |
| **Voting** | Expo Router Tabs | `app/(application)/(user)/voting/(tabs)/_layout.tsx` | ⚠️ Tidak konsisten |
| **Donation** | Expo Router Tabs | `app/(application)/(user)/donation/(tabs)/_layout.tsx` | ⚠️ Tidak konsisten |
| **Investment** | Expo Router Tabs | `app/(application)/(user)/investment/(tabs)/_layout.tsx` | ⚠️ Tidak konsisten |
| **Collaboration** | Expo Router Tabs | `app/(application)/(user)/collaboration/(tabs)/_layout.tsx` | ⚠️ Tidak konsisten |
### Gejala Masalah
- ❌ Tabs tertutup navigasi button Android pada beberapa device
- ❌ Height tabs tidak konsisten antara iOS dan Android
- ❌ Padding/spacing tidak sesuai di perangkat tertentu
## 📝 Sub-Tasks
### Task 1.1: Investigasi Mendalam
- [ ] Test di berbagai device Android (dengan navigasi buttons dan gesture)
- [ ] Test di berbagai device iOS (dengan home button dan gesture)
- [ ] Catat device mana saja yang mengalami masalah
- [ ] Screenshot perbandingan tampilan yang benar dan salah
### Task 1.2: Perbaikan NewWrapper Component
**File**: `components/_ShareComponent/NewWrapper.tsx`
- [ ] Tambah prop `useSafeAreaForFooter` (optional, default: false)
- [ ] Import `useSafeAreaInsets` dari `react-native-safe-area-context`
- [ ] Hitung footer height berdasarkan platform + safe area insets
- [ ] Sesuaikan `paddingBottom` di FlatList dan ScrollView
- [ ] Tambah padding di footer container saat `useSafeAreaForFooter={true}`
- [ ] Test tanpa merusak existing functionality
### Task 1.3: Perbaikan TabSection Component
**File**: `screens/Home/tabSection.tsx`
- [ ] Tambah prop `useSafeArea` (optional, default: false)
- [ ] Bungkus dengan `SafeAreaView` saat `useSafeArea={true}`
- [ ] Sesuaikan padding untuk iOS (12) dan Android (5)
- [ ] Test tanpa merusak existing functionality
### Task 1.4: Update Home Screen
**File**: `app/(application)/(user)/home.tsx`
- [ ] Tambah prop `useSafeAreaForFooter` di `NewWrapper`
- [ ] Tambah prop `useSafeArea` di `TabSection`
- [ ] Test di iOS dan Android
### Task 1.5: Review Expo Router Tabs Configuration
**File**: `styles/tabs-styles.ts`
- [ ] Cek apakah `TabsStyles` sudah benar untuk iOS dan Android
- [ ] Verifikasi height tabs (iOS: 80, Android: 70)
- [ ] Cek safe area handling di `TabBarBackground`
- [ ] Test semua fitur yang menggunakan Expo Router Tabs
### Task 1.6: Testing & Validasi
- [ ] Test Home screen di iOS
- [ ] Test Home screen di Android
- [ ] Test Event tabs di iOS
- [ ] Test Event tabs di Android
- [ ] Test Job tabs di iOS
- [ ] Test Job tabs di Android
- [ ] Test Voting tabs di iOS
- [ ] Test Voting tabs di Android
- [ ] Test Donation tabs di iOS
- [ ] Test Donation tabs di Android
- [ ] Test Investment tabs di iOS
- [ ] Test Investment tabs di Android
- [ ] Test Collaboration tabs di iOS
- [ ] Test Collaboration tabs di Android
## ✅ Acceptance Criteria
1. **Home Screen**:
- Tabs tidak tertutup navigasi Android
- Tabs terlihat jelas di semua device
- Pull-to-refresh berfungsi normal
2. **Expo Router Tabs** (Event, Job, Voting, Donation, Investment, Collaboration):
- Tabs tidak tertutup navigasi Android
- Height konsisten di semua device Android
- Height konsisten di semua device iOS
3. **General**:
- Tidak ada regression di fitur existing
- TypeScript compile tanpa error
- Lint passing
## 📚 Referensi
- [React Native Safe Area Context](https://github.com/th3rdwave/react-native-safe-area-context)
- [Expo Router Tabs Documentation](https://docs.expo.dev/router/reference/tabs/)
- [Android Navigation Patterns](https://developer.android.com/guide/navigation)
## 📝 Notes
- **Prioritas**: Task 1.2, 1.3, 1.4 (untuk Home screen)
- **Low Priority**: Task 1.5 (jika Expo Router Tabs sudah OK)
- **Jangan**: Mengubah struktur `<Tabs>` tanpa konfirmasi
- **Penting**: Test di device fisik, bukan hanya simulator
## 🔄 Status
**Status**: ✅ Completed
**Created**: 2026-04-01
**Updated**: 2026-04-01
**Completed**: 2026-04-01
## 📝 Implementation Summary
### Changes Made
1. **NewWrapper Component** (`components/_ShareComponent/NewWrapper.tsx`)
- Added `useSafeAreaForFooter` prop
- Added `useSafeAreaInsets()` hook
- Dynamic footer height calculation based on platform + safe area insets
- Applied safe area padding to footer container
2. **TabSection Component** (`screens/Home/tabSection.tsx`)
- Added `useSafeArea` prop
- Wrapped with `SafeAreaView` when `useSafeArea={true}`
- Platform-specific padding (iOS: 12, Android: 5)
3. **Home Screen** (`app/(application)/(user)/home.tsx`)
- Enabled `useSafeAreaForFooter` on `NewWrapper`
- Enabled `useSafeArea` on `TabSection`
4. **Expo Router Tabs** (`styles/tabs-styles.ts`)
- Reviewed - no changes needed (already configured correctly)
### Test Results
- ✅ TypeScript compilation: No errors
- ✅ Linting: No new errors (only pre-existing warnings)
- ✅ Code changes: 3 files, +77 insertions, -23 deletions
### Next Steps for User Testing
Test on physical devices:
- [ ] Android with navigation buttons
- [ ] Android with gesture navigation
- [ ] iOS with home button
- [ ] iOS with gesture (notch devices)

View File

@@ -0,0 +1,134 @@
# Task: TASK-002 - Expo Router Tabs Safe Area Fix
## 📋 Deskripsi
Expo Router Tabs di beberapa fitur (Event, Job, Voting, Donation, Investment) tertutup oleh navigation buttons Android pada device tertentu.
## 🎯 Tujuan
Tabs di semua fitur menggunakan Expo Router harus responsif dan tidak tertutup navigation buttons Android.
## 🔍 Analisis Masalah
### Fitur yang Terkena Dampak
| Fitur | Layout File | Status |
|-------|-------------|--------|
| Event | `app/(application)/(user)/event/(tabs)/_layout.tsx` | ❌ Tidak responsif |
| Job | `app/(application)/(user)/job/(tabs)/_layout.tsx` | ❌ Tidak responsif |
| Voting | `app/(application)/(user)/voting/(tabs)/_layout.tsx` | ❌ Tidak responsif |
| Donation | `app/(application)/(user)/donation/(tabs)/_layout.tsx` | ❌ Tidak responsif |
| Investment | `app/(application)/(user)/investment/(tabs)/_layout.tsx` | ❌ Tidak responsif |
| Collaboration | `app/(application)/(user)/collaboration/(tabs)/_layout.tsx` | ❌ Tidak responsif |
### Root Cause
`TabsStyles` di `styles/tabs-styles.ts` tidak menghormati safe area insets Android dengan benar.
## 📝 Solusi
### Opsi 1: Custom Tab Bar Component (RECOMMENDED)
Buat custom `tabBar` component yang menggunakan `SafeAreaView` untuk wrapping tab bar.
```typescript
// styles/tabs-styles.ts
import { useSafeAreaInsets } from 'react-native-safe-area-context';
export function CustomTabBar(props: any) {
const insets = useSafeAreaInsets();
return (
<View style={{
paddingBottom: insets.bottom,
backgroundColor: MainColor.darkblue
}}>
<BottomTabBar {...props} />
</View>
);
}
```
### Opsi 2: Update tabBarStyle dengan insets
Tambahkan dynamic height berdasarkan safe area insets.
## ✅ Acceptance Criteria
1. Tabs tidak tertutup navigation buttons Android
2. Tabs height konsisten di semua device
3. Tidak ada regression di iOS
4. Semua 6 fitur ter-fix
## 🔄 Status
**Status**: ✅ COMPLETED
**Created**: 2026-04-01
**Updated**: 2026-04-01
**Completed**: 2026-04-01
## 📝 Implementation Summary
### Changes Made
**Tabs Layout Wrappers** - Updated 6 layout files dengan safe area handling:
-`app/(application)/(user)/event/(tabs)/_layout.tsx`
-`app/(application)/(user)/job/(tabs)/_layout.tsx`
-`app/(application)/(user)/voting/(tabs)/_layout.tsx`
-`app/(application)/(user)/donation/(tabs)/_layout.tsx`
-`app/(application)/(user)/investment/(tabs)/_layout.tsx`
-`app/(application)/(user)/collaboration/(tabs)/_layout.tsx`
### Implementation Pattern
Setiap layout file menggunakan wrapper component pattern:
```typescript
function TabsWrapper() {
const insets = useSafeAreaInsets();
const paddingBottom = Platform.OS === "android" ? insets.bottom : 0;
return (
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
<Tabs screenOptions={TabsStyles}>
{/* Tabs content */}
</Tabs>
{/* Safe area padding untuk Android */}
{Platform.OS === "android" && paddingBottom > 0 && (
<View style={{ height: paddingBottom, backgroundColor: MainColor.darkblue }} />
)}
</View>
);
}
```
### Files Changed
- ✅ 6x Tabs layout files (Updated with safe area wrapper)
### Test Results
- ✅ TypeScript compilation: No errors
- ✅ All 6 tabs layouts: Safe area implemented
- ✅ Platform-specific: Android only (iOS unaffected)
- ✅ NewWrapper: Unchanged (original version preserved)
### Features Fixed
| Feature | Layout File | Status |
|---------|-------------|--------|
| Event | `app/(application)/(user)/event/(tabs)/_layout.tsx` | ✅ Fixed |
| Job | `app/(application)/(user)/job/(tabs)/_layout.tsx` | ✅ Fixed |
| Voting | `app/(application)/(user)/voting/(tabs)/_layout.tsx` | ✅ Fixed |
| Donation | `app/(application)/(user)/donation/(tabs)/_layout.tsx` | ✅ Fixed |
| Investment | `app/(application)/(user)/investment/(tabs)/_layout.tsx` | ✅ Fixed |
| Collaboration | `app/(application)/(user)/collaboration/(tabs)/_layout.tsx` | ✅ Fixed |
### Next Steps for User Testing
Test all 6 features on physical Android devices with:
- [ ] Navigation buttons (back, home, recent)
- [ ] Gesture navigation
- [ ] Various screen sizes
Test on iOS to ensure no regression:
- [ ] Home button devices
- [ ] Gesture devices (notch)

View File

@@ -0,0 +1,110 @@
# Task: TASK-003 - Footer Terangkat Saat Keyboard Close
## 📋 Deskripsi
Bug: Setelah input ke text input dan menutup keyboard, bagian bawah layar berwarna putih seakan footer terangkat.
## 🎯 Tujuan
Footer tetap di posisi yang benar setelah keyboard ditutup, tidak ada warna putih di bawah.
## 🔍 Analisis Masalah
### Gejala
- ✅ Terjadi di emulator dan device
- ✅ Setelah input ke text input
- ✅ Saat keyboard menutup (close)
- ✅ Bagian bawah berwarna putih
- ✅ Footer seperti terangkat
### Root Cause (Diduga)
1. **KeyboardAvoidingView behavior**
- `behavior={Platform.OS === "ios" ? "padding" : "height"}`
- Android menggunakan `height` yang bisa menyebabkan layout shift
2. **Keyboard listener tidak clean up**
- Event listener mungkin masih aktif setelah keyboard close
3. **Layout tidak re-render setelah keyboard close**
- Component tidak detect keyboard state change
## 📝 Sub-Tasks
### Task 3.1: Investigasi
- [ ] Identifikasi screen mana yang mengalami bug ini
- [ ] Test di berbagai screen dengan text input
- [ ] Catat pola kejadian bug
### Task 3.2: Perbaikan NewWrapper - Keyboard Handling
- [ ] Tambah keyboard event listener
- [ ] Handle keyboard show/hide events
- [ ] Force re-render saat keyboard close
- [ ] Test tanpa merusak existing functionality
### Task 3.3: Perbaikan KeyboardAvoidingView
- [ ] Evaluasi behavior untuk Android
- [ ] Coba gunakan `KeyboardAwareScrollView` jika perlu
- [ ] Test smooth keyboard transition
### Task 3.4: Testing & Validasi
- [ ] Test di emulator Android
- [ ] Test di device Android
- [ ] Test di emulator iOS
- [ ] Test di device iOS
- [ ] Pastikan tidak ada regression
## ✅ Acceptance Criteria
1. **Footer tetap di posisi** setelah keyboard close
2. **Tidak ada warna putih** di bagian bawah
3. **Keyboard transition smooth** (no lag)
4. **Input tetap berfungsi** normal
5. **No regression** di fitur lain
## 📚 Referensi
- [React Native KeyboardAvoidingView](https://reactnative.dev/docs/keyboardavoidingview)
- [React Native Keyboard](https://reactnative.dev/docs/keyboard)
- [KeyboardAwareScrollView](https://github.com/APSL/react-native-keyboard-aware-scroll-view)
## 🔄 Status
**Status**: ❌ Reverted
**Created**: 2026-04-01
**Updated**: 2026-04-01
**Completed**: -
## 📝 Notes
Implementation sudah dilakukan tetapi di-revert karena berefek pada tampilan lain.
Perlu pendekatan yang berbeda untuk fix bug ini.
### Root Cause (Identified)
1. **Footer menggunakan `position: absolute`** - Footer melayang di atas konten, tidak ikut layout flow
2. **`KeyboardAvoidingView` behavior** - Layout shift saat keyboard show/hide
3. **View wrapper dengan `flex: 0`** - ScrollView tidak expand dengan benar
### Implementation Attempted
**File**: `components/_ShareComponent/NewWrapper.tsx`
#### Perubahan yang dicoba:
- Hapus `View` wrapper dengan `flex: 1` di FlatList mode
- Hapus `View` wrapper dengan `flex: 0` di ScrollView mode
- Footer menggunakan `SafeAreaView` (normal flow, bukan position absolute)
- Hapus `styles.footerContainer` dengan `position: absolute`
### Why Reverted
❌ Berdampak pada tampilan lain (footer terangkat/berantakan)
❌ Perlu pendekatan yang lebih hati-hati
❌ Perlu test lebih menyeluruh di semua screen
### Next Steps
1. **Analisis lebih detail** - Cek screen mana saja yang affected
2. **Pendekatan bertahap** - Fix per screen atau per type
3. **Test menyeluruh** - Pastikan tidak ada regression
4. **Alternative solution** - Mungkin perlu custom keyboard handling