Compare commits

...

20 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
57c9215771 Feat validasi mobile otp
modified:   context/AuthContext.tsx
        modified:   screens/Authentication/VerificationView.tsx
        modified:   service/api-config.ts

### No Issue
2026-03-11 15:04:32 +08:00
4efdbd3c7b feat: update admin features, user confirmation, and native configs
- Admin: Update layout, notification bell, and event detail screen
- User: Improve event confirmation flow
- Config: Update AndroidManifest, Info.plist, entitlements, and app.config.js

### No Issue
2026-03-11 11:29:20 +08:00
ad32eb6fe6 feat: implement deep linking & universal links for event confirmation
- Add QR code toggle for HTTPS/Custom Scheme links
- Add Universal Links (iOS) and App Links (Android) support
- Create deep link route handler with platform detection
- Add .well-known files for domain verification
- Fix Content-Type headers for apple-app-site-association
- Add environment configuration for staging & production
- Add comprehensive testing documentation

Testing:
- QR scan → Safari → App switch working 
- Platform detection working 
- Auto redirect to custom scheme working 
- Web fallback JSON response working 

### No Issue
2026-03-09 16:39:01 +08:00
103 changed files with 3717 additions and 2254 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-staging-hipmi.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

@@ -1,6 +1,17 @@
// app.config.js
require("dotenv").config();
// const isDev = process.env.NODE_ENV === "development";
// const isStaging = process.env.NEXT_PUBLIC_ENV === "staging";
// const isProd = process.env.NEXT_PUBLIC_ENV === "production";
// Domain berdasarkan environment
// const domain = isDev
// ? "localhost:3000"
// : isStaging
// ? "cld-dkr-hipmi-stg.wibudev.com"
// : "hipmi.muku.id"; // Production domain
export default {
name: "HIPMI Badung Connect",
slug: "hipmi-mobile",
@@ -14,25 +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-staging-hipmi.wibudev.com"],
buildNumber: "3",
associatedDomains: [
"applinks:hipmi.muku.id",
],
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: [
{
@@ -41,7 +54,7 @@ export default {
data: [
{
scheme: "https",
host: "cld-dkr-staging-hipmi.wibudev.com",
host: "hipmi.muku.id",
pathPrefix: "/",
},
],
@@ -57,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,7 +9,8 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import AppHeader from "@/components/_ShareComponent/AppHeader";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import {
apiEventConfirmationAction,
@@ -60,7 +61,7 @@ export default function UserEventConfirmation() {
useFocusEffect(
useCallback(() => {
checkTokenAndDataParticipants() || console.log("Token is null");
}, [token, id, user?.id])
}, [token, id, user?.id]),
);
const checkTokenAndDataParticipants = async () => {
@@ -113,7 +114,7 @@ export default function UserEventConfirmation() {
confirmationStart,
confirmationEnd,
null,
"[]"
"[]",
);
// --- [4] Status waktu event (untuk pesan UI) ---
@@ -218,9 +219,14 @@ export default function UserEventConfirmation() {
if (isWithinConfirmationWindow) {
if (konfirmasi === false) {
return (
<TamplateBox data={data}>
<TamplateText text="Konfirmasi Kehadiran" />
</TamplateBox>
// <TamplateBox data={data}>
// <TamplateText text="Konfirmasi Kehadiran" />
// </TamplateBox>
<UserParticipan_And_DuringEvent
id={data.id}
userId={user?.id as string}
data={data}
/>
);
}
return (
@@ -260,18 +266,20 @@ export default function UserEventConfirmation() {
<>
<Stack.Screen
options={{
title: "Konfirmasi Event",
// headerLeft: () => (
// <Ionicons
// name="arrow-back"
// size={20}
// color={MainColor.yellow}
// onPress={() =>
// router.navigate("/(application)/(user)/event/create")
// }
// />
// ),
}}
header: () => (
<AppHeader
title="Konfirmasi Event"
left={
<Ionicons
name="arrow-back"
size={20}
color={MainColor.yellow}
onPress={() => router.navigate("/")}
/>
}
/>
),
}}
/>
<ViewWrapper>{handlerReturn()}</ViewWrapper>
</>
@@ -497,7 +505,6 @@ const UserNotParticipan_And_DuringEvent = ({
);
};
// 🟡 ZONA ACARA BERLANGSUN
// User sudah terdaftar & Event sedang berlangsung & user harus konfirmasi
const UserParticipan_And_DuringEvent = ({

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";
@@ -17,6 +18,7 @@ import {
ICON_SIZE_XLARGE,
} from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
import {
adminListMenu,
@@ -34,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
@@ -51,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

@@ -22,7 +22,7 @@ type AuthContextType = {
isAdmin: boolean;
isUserActive: boolean;
loginWithNomor: (nomor: string) => Promise<boolean>;
validateOtp: (nomor: string) => Promise<any>;
validateOtp: (nomor: string, code: string) => Promise<any>;
logout: () => Promise<void>;
registerUser: (userData: {
username: string;
@@ -97,10 +97,10 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
};
// --- 2. Validasi OTP & cek user ---
const validateOtp = async (nomor: string) => {
const validateOtp = async (nomor: string, code: string) => {
try {
setIsLoading(true);
const response = await apiValidationCode({ nomor: nomor });
const response = await apiValidationCode({ nomor: nomor, code: code });
const { token } = response;
console.log("[RESPONSE VALIDASI OTP]", JSON.stringify(response, null, 2));

253
docs/QR_CODE_TESTING.md Normal file
View File

@@ -0,0 +1,253 @@
# QR Code Testing Guide - HIPMI Mobile
## 📋 Overview
Dokumentasi ini menjelaskan cara testing QR Code untuk Universal Links (iOS) dan App Links (Android) pada fitur Event Confirmation.
## 🔧 Update Terbaru
File `screens/Admin/Event/EventDetailQRCode.tsx` telah diupdate dengan fitur:
- **Toggle Button**: Switch antara HTTPS link dan Custom Scheme link
- **HTTPS Link**: Untuk testing Universal Links/App Links dengan domain staging
- **Custom Scheme**: Untuk testing langsung tanpa domain verification
## 🎯 Cara Testing QR Code
### Opsi 1: HTTPS Link (Recommended untuk Production)
**Gunakan tombol "HTTPS"** di component QR Code.
**Link yang di-generate:**
```
https://cld-dkr-staging-hipmi.wibudev.com/event/{id}/confirmation?userId={userId}
```
**Cara kerja:**
1. User scan QR code dengan kamera
2. Safari/Chrome terbuka dengan URL HTTPS
3. iOS/Android mendeteksi domain terverifikasi
4. App terbuka otomatis dan menuju halaman confirmation
**Prerequisites:**
- ✅ File `apple-app-site-association` harus accessible di Next.js server
- ✅ File `assetlinks.json` harus accessible di Next.js server
- ✅ Domain harus terverifikasi di app.config.js
- ✅ App harus di-build ulang setelah perubahan domain
**Testing Steps:**
```bash
# 1. Pastikan .well-known files accessible
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json
# 2. Rebuild app
bunx expo prebuild --clean
# 3. Run di physical device (bukan simulator)
bun run android # untuk Android
bun run ios # untuk iOS
```
### Opsi 2: Custom Scheme Link (Untuk Development/Testing Cepat)
**Gunakan tombol "Custom Scheme"** di component QR Code.
**Link yang di-generate:**
```
hipmimobile://event/{id}/confirmation?userId={userId}
```
**Cara kerja:**
1. User scan QR code dengan kamera
2. iOS: Pilih "Open in HIPMI Badung Connect"
3. Android: Langsung buka app
4. App terbuka dan menuju halaman confirmation
**Keuntungan:**
- ✅ Tidak butuh domain verification
- ✅ Bisa testing langsung tanpa rebuild
- ✅ Cocok untuk development
**Kekurangan:**
- ❌ Tidak bisa dibuka dari web browser
- ❌ Tidak support universal linking dari website lain
## 📱 Testing Checklist
### iOS (Universal Links)
- [ ] File `apple-app-site-association` valid dan accessible
- [ ] Domain terdaftar di `app.config.js``ios.associatedDomains`
- [ ] Bundle ID match dengan konfigurasi
- [ ] Team ID benar di apple-app-site-association
- [ ] Test dengan **physical device** (simulator tidak support)
- [ ] Test dengan **Safari** (bukan Chrome)
- [ ] Long press link → ada opsi "Open"
**Debug iOS:**
```bash
# Cek apple-app-site-association
curl -I https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
# Harus return:
# Content-Type: application/json
# HTTP/2 200
```
### Android (App Links)
- [ ] File `assetlinks.json` valid dan accessible
- [ ] SHA256 fingerprint benar
- [ ] Package name match
- [ ] Intent filters terdaftar di app.config.js
- [ ] Test dengan **physical device**
- [ ] Test dengan **Chrome**
**Debug Android:**
```bash
# Dapatkan SHA256 fingerprint
cd android
./gradlew signingReport
# Cek assetlinks.json
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json
```
## 🐛 Troubleshooting
### Problem: QR Scan Terbuka di Safari, Tidak Balik ke App
**Penyebab:**
- Domain belum terverifikasi untuk Universal Links/App Links
- File `.well-known` tidak accessible atau invalid
- App belum di-rebuild setelah perubahan domain
**Solusi:**
1. Pastikan file `.well-known` accessible:
```bash
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
```
2. Rebuild app:
```bash
bunx expo prebuild --clean
bun run android # atau bun run ios
```
3. Gunakan **Custom Scheme** untuk testing cepat
### Problem: Link Tidak Membuka App Sama Sekali
**Cek:**
1. App sudah terinstall di device
2. Link format benar (hipmimobile:// atau https://)
3. Route handler sudah ada di app folder
**Test manual:**
```bash
# iOS Simulator
xcrun simctl openurl booted "hipmimobile://event/123/confirmation?userId=456"
# Android Emulator
adb shell am start -W -a android.intent.action.VIEW \
-d "hipmimobile://event/123/confirmation?userId=456" \
com.bip.hipmimobileapp
```
### Problem: "Cannot GET /event/..." di Next.js
**Penyebab:**
Route `/event/[id]/confirmation` tidak ada di Next.js server
**Solusi:**
Pastikan Next.js project punya file:
```
public/.well-known/apple-app-site-association
public/.well-known/assetlinks.json
```
Dan API route untuk handle:
```
pages/api/event/[id]/confirmation.ts
```
## 📄 File Configuration
### app.config.js - iOS
```javascript
ios: {
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
}
```
### app.config.js - Android
```javascript
android: {
intentFilters: [
{
action: "VIEW",
autoVerify: true,
data: [
{
scheme: "https",
host: "cld-dkr-staging-hipmi.wibudev.com",
pathPrefix: "/",
},
],
category: ["BROWSABLE", "DEFAULT"],
},
],
}
```
### apple-app-site-association (Next.js)
```json
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.com.anonymous.hipmi-mobile",
"paths": ["/event/*/confirmation"]
}
]
}
}
```
### assetlinks.json (Next.js)
```json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.bip.hipmimobileapp",
"sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
}
}
]
```
## 🎓 Best Practices
1. **Development**: Gunakan Custom Scheme untuk testing cepat
2. **Staging**: Gunakan HTTPS link dengan domain staging
3. **Production**: Gunakan HTTPS link dengan domain production
4. **Testing**: Selalu test di physical device, bukan simulator
5. **Debugging**: Enable logging di confirmation page untuk track deep link
## 🔗 Related Files
- `screens/Admin/Event/EventDetailQRCode.tsx` - QR Code generator
- `app/(application)/(user)/event/[id]/confirmation.tsx` - Confirmation page
- `app.config.js` - App configuration
- `service/api-config.ts` - API configuration (DEEP_LINK_URL)
## 📞 Support
Jika masih ada masalah:
1. Cek logs di console
2. Test manual dengan adb/xcrun
3. Verify .well-known files dengan curl
4. Pastikan app rebuild setelah perubahan config

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-staging-hipmi.wibudev.com</string>
<string>applinks:hipmi.muku.id</string>
</array>
</dict>
</plist>

View File

@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>3</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

@@ -4,10 +4,16 @@ import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useEffect } from "react";
import { Text, View } from "react-native";
export default function AdminNotificationBell() {
const { unreadCount } = useNotificationStore();
const { unreadCount, syncUnreadCount } = useNotificationStore();
useEffect(() => {
console.log("Syncing unread count");
syncUnreadCount();
}, [syncUnreadCount]);
return (
<View style={{ position: "relative" }}>

View File

@@ -1,26 +1,96 @@
import { BaseBox, LoaderCustom, Spacing, StackCustom, TextCustom } from "@/components";
import {
BaseBox,
ButtonCustom,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { BASE_URL } from "@/service/api-config";
import { useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { StyleSheet } from "react-native";
import QRCode from "react-native-qrcode-svg";
interface EventDetailQRCodeProps {
qrValue: string;
userId: string;
isLoading: boolean;
}
export function EventDetailQRCode({ qrValue, isLoading }: EventDetailQRCodeProps) {
export function EventDetailQRCode({
userId,
isLoading,
}: EventDetailQRCodeProps) {
const { id } = useLocalSearchParams();
const [useHttpsLink, setUseHttpsLink] = useState(true);
// HTTPS link untuk Universal Links (iOS) dan App Links (Android)
// Ini akan membuka file .well-known di Next.js server Anda
const httpsLink = `https://cld-dkr-hipmi-stg.wibudev.com/event/${id}/confirmation?userId=${userId}`;
// Custom scheme link untuk fallback atau testing tanpa universal links
const deepLinkURL = `${BASE_URL}/event/${id}/confirmation?userId=${userId}`;
// Toggle antara HTTPS link dan custom scheme
// const qrValue = useHttpsLink ? httpsLink : deepLinkURL;
const qrValue = deepLinkURL;
return (
<BaseBox>
<StackCustom style={{ alignItems: "center" }}>
<TextCustom bold>QR Code Event</TextCustom>
{isLoading ? (
<LoaderCustom />
) : (
<QRCode
value={qrValue}
size={200}
/>
)}
{isLoading ? <LoaderCustom /> : <QRCode value={qrValue} size={200} />}
</StackCustom>
<Spacing />
<TextCustom color="gray" align="center" size="small">
{qrValue}
</TextCustom>
<Spacing />
{/* <StackCustom direction="row" gap="sm">
<ButtonCustom
onPress={() => setUseHttpsLink(true)}
backgroundColor={useHttpsLink ? MainColor.yellow : "transparent"}
textColor={useHttpsLink ? MainColor.black : MainColor.yellow}
style={[
stylesButton.smallButton,
useHttpsLink && stylesButton.border,
]}
>
HTTPS
</ButtonCustom>
<ButtonCustom
onPress={() => setUseHttpsLink(false)}
backgroundColor={!useHttpsLink ? MainColor.yellow : "transparent"}
textColor={!useHttpsLink ? MainColor.black : MainColor.yellow}
style={[
stylesButton.smallButton,
!useHttpsLink && stylesButton.border,
]}
>
Custom Scheme
</ButtonCustom>
</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> */}
</BaseBox>
);
}
const stylesButton = StyleSheet.create({
smallButton: {
paddingHorizontal: 12,
paddingVertical: 6,
minWidth: 140,
},
border: {
borderWidth: 1,
borderColor: MainColor.yellow,
},
});

View File

@@ -1,22 +1,22 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ActionIcon, AlertDefaultSystem } from "@/components";
import { ActionIcon, AlertDefaultSystem, Spacing } from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import ReportBox from "@/components/Box/ReportBox";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import ReportBox from "@/components/Box/ReportBox";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
import { DEEP_LINK_URL } from "@/service/api-config";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useState } from "react";
import Toast from "react-native-toast-message";
import { BoxEventDetail } from "./BoxEventDetail";
import { EventDetailDrawer } from "./EventDetailDrawer";
import { EventDetailQRCode } from "./EventDetailQRCode";
import { View } from "react-native";
export function Admin_ScreenEventDetail() {
const { user } = useAuth();
@@ -25,11 +25,6 @@ export function Admin_ScreenEventDetail() {
const [data, setData] = useState<any | null>(null);
const [loadData, setLoadData] = useState(false);
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
const isDevLink =
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
useFocusEffect(
useCallback(() => {
onLoadData();
@@ -133,14 +128,19 @@ export function Admin_ScreenEventDetail() {
);
}
return null;
return <Spacing height={100} />;
}, [status, id]);
return (
<>
<NewWrapper
hideFooter
headerComponent={headerComponent}
footerComponent={footerComponent}
// footerComponent={
// <View style={{ paddingInline: 8 }}>
// {footerComponent}
// </View>
// }
>
<BoxEventDetail data={data} status={status as string} />
@@ -149,8 +149,11 @@ export function Admin_ScreenEventDetail() {
)}
{(status === "publish" || status === "history") && (
<EventDetailQRCode qrValue={isDevLink} isLoading={loadData} />
<EventDetailQRCode userId={user?.id || ""} isLoading={loadData} />
)}
{footerComponent}
<Spacing />
</NewWrapper>
<EventDetailDrawer

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

@@ -21,10 +21,10 @@ export default function VerificationView() {
const [loading, setLoading] = useState<boolean>(false);
const [recodeOtp, setRecodeOtp] = useState<boolean>(false);
// 🔑 DETEKSI MODE REVIEW (HANYA UNTUK NOMOR DEMO & PRODUCTION)
// 🔑 DETEKSI MODE REVIEW (HANYA UNTUK NOMOR DEMO & DEVELOPMENT BUILD)
// Menggunakan Constants.expoConfig untuk mendeteksi development build
const isReviewMode =
typeof window !== "undefined" && // pastikan di browser/production
process.env.NODE_ENV === "production" &&
process.env.NODE_ENV === "development" &&
nomor === "6282340374412";
// --- Context ---
@@ -37,10 +37,6 @@ export default function VerificationView() {
// Hanya jalankan logika OTP normal jika BUKAN review mode
onLoadCheckCodeOtp();
}
console.log("[NODE_ENV]:", process.env.NODE_ENV);
console.log("[isReviewMode]:", isReviewMode);
console.log("[nomor]:", nomor);
}, [recodeOtp, isReviewMode]);
async function onLoadCheckCodeOtp() {
@@ -85,29 +81,30 @@ export default function VerificationView() {
const handleVerification = async () => {
if (isReviewMode) {
// ✅ VERIFIKASI OTOMATIS UNTUK APPLE REVIEW
if (inputOtp === "1234") {
try {
await validateOtp(nomor as string);
return;
} catch (error) {
console.log("Error verification", error);
Toast.show({ type: "error", text1: "Gagal verifikasi" });
}
} else {
// ✅ VERIFIKASI OTOMATIS UNTUK APPLE REVIEW (Development Only)
if (inputOtp !== "1234") {
Toast.show({ type: "error", text1: "Kode OTP tidak sesuai" });
return;
}
try {
await validateOtp(nomor as string, inputOtp);
return;
} catch (error) {
console.log("Error verification", error);
Toast.show({ type: "error", text1: "Gagal verifikasi" });
}
return;
}
// 🔁 VERIFIKASI NORMAL (untuk pengguna sungguhan)
try {
await validateOtp(nomor as string);
return
} catch (error) {
await validateOtp(nomor as string, inputOtp);
return;
} catch (error: any) {
console.log("Error verification", error);
Toast.show({ type: "error", text1: "Gagal verifikasi" });
Toast.show({
type: "error",
text1: error.response?.data?.message || "Gagal verifikasi",
});
}
};

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

@@ -45,9 +45,16 @@ export async function apiCheckCodeOtp({ kodeId }: { kodeId: string }) {
return response.data;
}
export async function apiValidationCode({ nomor }: { nomor: string }) {
export async function apiValidationCode({
nomor,
code,
}: {
nomor: string;
code: string;
}) {
const response = await apiConfig.post(`/auth/mobile-validasi`, {
nomor: nomor,
code: code,
});
return response.data;
}

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
```

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