Compare commits

...

11 Commits

Author SHA1 Message Date
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
32 changed files with 1492 additions and 331 deletions

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

@@ -0,0 +1,369 @@
# Fix Phone Number Input - iOS 16+ Compatibility
## 📋 Ringkasan Task
Memperbaiki masalah phone number input pada `screens/Authentication/LoginView.tsx` yang tidak berfungsi dengan baik pada iOS versi 16 ke atas.
---
## 🎯 Tujuan
1. Fix keyboard overlay issues pada iOS 16+
2. Perbaiki layout measurement dan safe area
3. Pastikan input phone number responsive di semua device
4. Maintain UX yang konsisten dengan Android
---
## 📁 File yang Terlibat
### File Utama
- **Target**: `screens/Authentication/LoginView.tsx`
### File Pendukung
- `components/TextInput/TextInputCustom.tsx` - Alternatif custom input
- `styles/global-styles.ts` - Styling adjustments
- `package.json` - Update dependencies (jika perlu)
### Dependencies Terkait
- `react-native-international-phone-number`: ^0.9.3
- `react-native-keyboard-controller`: ^1.18.6
---
## 🔍 Analisis Masalah
### Issues pada iOS 16+
#### 1. **Keyboard Overlay Problem**
**Symptom**: Input field tertutup keyboard saat aktif
**Cause**:
- Safe area insets tidak terhitung dengan benar
- Keyboard animation tidak sinkron dengan layout
- `react-native-keyboard-controller` tidak terintegrasi dengan baik
#### 2. **Layout Measurement Issues**
**Symptom**: Input field berubah ukuran secara tidak terduga
**Cause**:
- Dynamic Type settings mempengaruhi layout
- Font scaling pada iOS 16+ berbeda
- Container tidak memiliki fixed height
#### 3. **Focus Behavior**
**Symptom**: Input tidak auto-scroll saat di-focus
**Cause**:
- ScrollView/KeyboardAvoidingView configuration salah
- Keyboard dismissing behavior tidak konsisten
#### 4. **Visual Glitches**
**Symptom**: Country flag dropdown tidak muncul atau terpotong
**Cause**:
- Z-index issues pada iOS
- Modal/Popover rendering problems
---
## 🎨 Solusi yang Direkomendasikan
### Option 1: **KeyboardAvoidingView Enhancement** (RECOMMENDED)
**Effort**: Medium
**Impact**: High
```typescript
import { KeyboardAvoidingView, Platform } from "react-native";
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 90 : 50}
style={{ flex: 1 }}
>
<ViewWrapper>
{/* Content */}
</ViewWrapper>
</KeyboardAvoidingView>
```
**Keuntungan**:
- Native solution dari React Native
- Tidak perlu tambahan library
- Stabil untuk iOS 16+
**Kekurangan**:
- Perlu tuning offset untuk setiap device
- Tidak se-smooth keyboard controller
### Option 2: **React Native Keyboard Controller** (BETTER UX)
**Effort**: Medium-High
**Impact**: High
```typescript
import { KeyboardProvider } from "react-native-keyboard-controller";
// Wrap di root app (sudah ada di _layout.tsx)
<KeyboardProvider>
<App />
</KeyboardProvider>
// Di LoginView
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
<KeyboardAwareScrollView
bottomOffset={20}
keyboardOffset={10}
>
{/* Content */}
</KeyboardAwareScrollView>
```
**Keuntungan**:
- Smooth keyboard animations
- Better control over keyboard behavior
- Cross-platform consistency
**Kekurangan**:
- Perlu verify konfigurasi yang sudah ada
- Mungkin perlu update library
### Option 3: **Custom Phone Input** (FALLBACK)
**Effort**: High
**Impact**: Medium
Membuat custom phone input dengan:
- TextInputCustom component
- Country code picker modal
- Validation logic
**Keuntungan**:
- Full control atas behavior
- Tidak depend on third-party issues
**Kekurangan**:
- Development time lebih lama
- Perlu testing ekstensif
- Maintain code sendiri
---
## 📝 Breakdown Task
### Task 1: Research & Setup ✅
- [x] Identifikasi masalah pada iOS 16+
- [x] Cek dokumentasi library
- [x] Review existing implementation
- [ ] Test di iOS simulator (iOS 16+)
- [ ] Test di device fisik (jika ada)
### Task 2: Implement KeyboardAvoidingView Fix
- [ ] Wrap ViewWrapper dengan KeyboardAvoidingView
- [ ] Set behavior berdasarkan Platform
- [ ] Adjust keyboardVerticalOffset
- [ ] Test di berbagai ukuran layar
- [ ] Test landscape mode (jika applicable)
### Task 3: Adjust Layout & Styling
- [ ] Fix container height/width
- [ ] Adjust safe area insets
- [ ] Test dengan Dynamic Type settings
- [ ] Ensure consistent padding/margin
### Task 4: Test Focus Behavior
- [ ] Auto-scroll saat focus
- [ ] Keyboard dismiss saat tap outside
- [ ] Next/previous field navigation (jika ada)
- [ ] Input validation on blur
### Task 5: Country Picker Fix
- [ ] Verify dropdown z-index
- [ ] Test modal presentation
- [ ] Ensure flag icons visible
- [ ] Test search functionality
### Task 6: Testing & QA
- [ ] Test iOS 16, 17, 18
- [ ] Test Android (regression)
- [ ] Test dengan berbagai device sizes
- [ ] Test accessibility (VoiceOver)
- [ ] Performance test (no lag)
### Task 7: Documentation
- [ ] Update code comments
- [ ] Document iOS-specific workarounds
- [ ] Add troubleshooting notes
---
## 💻 Implementation Guidelines
### Recommended Implementation (Option 1 + 2 Hybrid)
```typescript
import { KeyboardAvoidingView, Platform } from "react-native";
import { KeyboardProvider } from "react-native-keyboard-controller";
// Di LoginView.tsx
export default function LoginView() {
return (
<ViewWrapper
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, { paddingBottom: 40 }]}>
{/* Title Section */}
<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} />
{/* Phone Input - Wrap dengan View untuk stability */}
<View style={{ marginBottom: 20 }}>
<PhoneInput
value={inputValue}
onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
placeholder="Masukkan nomor"
// Add iOS-specific props
textInputProps={{
keyboardType: "phone-pad",
autoComplete: "tel",
importantForAutofill: "yes",
}}
/>
</View>
<Spacing />
{/* Login Button */}
<ButtonCustom
onPress={handleLogin}
disabled={loadingTerm}
isLoading={loading || loadingTerm}
>
Login
</ButtonCustom>
<Spacing height={50} />
{/* Terms Text */}
<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>
{/* Version Info */}
<Text
style={{
position: "absolute",
bottom: 35,
right: 50,
fontSize: 10,
fontWeight: "thin",
fontStyle: "italic",
color: MainColor.white_gray,
}}
>
{version} | powered by muku.id
</Text>
</View>
</KeyboardAvoidingView>
</ViewWrapper>
);
}
```
### Styling Adjustments
```typescript
// styles/global-styles.ts
export const GStyles = StyleSheet.create({
authContainer: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 24,
// Add padding bottom untuk keyboard space
paddingBottom: Platform.OS === "ios" ? 40 : 20,
},
// ... existing styles
});
```
---
## ✅ Acceptance Criteria
### Functional
- [ ] Input field tidak tertutup keyboard saat focus
- [ ] Country picker dropdown berfungsi dengan baik
- [ ] Auto-scroll bekerja smooth saat focus
- [ ] Keyboard dismiss saat tap outside
- [ ] Input validation berjalan normal
### Visual
- [ ] Layout tidak berubah saat keyboard muncul
- [ ] No visual glitches atau flickering
- [ ] Country flag icons visible
- [ ] Consistent spacing dan padding
### Compatibility
- [ ] iOS 16, 17, 18 - Tested ✅
- [ ] Android - No regression ✅
- [ ] iPad - Responsive ✅
- [ ] Landscape mode - Usable ✅
### Performance
- [ ] No lag saat typing
- [ ] Smooth keyboard animations
- [ ] No memory leaks
- [ ] Fast input response
---
## 🔗 References
- [React Native KeyboardAvoidingView Docs](https://reactnative.dev/docs/keyboardavoidingview)
- [react-native-keyboard-controller](https://github.com/kirillzyusko/react-native-keyboard-controller)
- [react-native-international-phone-number Issues](https://github.com/bluesky01/react-native-international-phone-number/issues)
- [iOS 16+ Keyboard Changes](https://developer.apple.com/documentation/uikit/uikit)
---
## 📊 Estimated Effort
- **Complexity**: Medium
- **Time Estimate**: 2-4 jam
- **Risk Level**: Medium (perlu testing ekstensif di device)
---
## 📝 Notes
- **Priority**: High (login adalah critical path)
- **Testing**: Wajib test di device fisik jika memungkinkan
- **Fallback**: Jika Option 1 & 2 gagal, siap untuk implement Option 3 (custom input)
- **Monitoring**: Add analytics untuk track input completion rate
---
**Created**: 2026-03-25
**Status**: Pending
**Priority**: High
**Related Issue**: iOS 16+ keyboard compatibility

View File

@@ -101,7 +101,7 @@ packagingOptions {
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1
versionName "1.0.2" versionName "1.0.3"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
} }

View File

@@ -15,7 +15,7 @@ require("dotenv").config();
export default { export default {
name: "HIPMI Badung Connect", name: "HIPMI Badung Connect",
slug: "hipmi-mobile", slug: "hipmi-mobile",
version: "1.0.2", version: "1.0.3",
orientation: "portrait", orientation: "portrait",
icon: "./assets/images/icon.png", icon: "./assets/images/icon.png",
scheme: "hipmimobile", scheme: "hipmimobile",
@@ -34,7 +34,7 @@ export default {
associatedDomains: [ associatedDomains: [
"applinks:cld-dkr-hipmi-stg.wibudev.com", "applinks:cld-dkr-hipmi-stg.wibudev.com",
], ],
buildNumber: "5", buildNumber: "7",
}, },
android: { android: {

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components"; import { BasicWrapper, NewWrapper, StackCustom, ViewWrapper } from "@/components";
import AppHeader from "@/components/_ShareComponent/AppHeader"; import AppHeader from "@/components/_ShareComponent/AppHeader";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom"; import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -148,7 +148,7 @@ export default function Application() {
}} }}
/> />
<ViewWrapper <NewWrapper
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={refreshing} refreshing={refreshing}
@@ -166,18 +166,19 @@ export default function Application() {
})} })}
/> />
) : ( ) : (
<View style={GStyles.tabBar}> null
<View style={[GStyles.tabContainer, { paddingTop: 10 }]}> // <View style={GStyles.tabBar}>
{Array.from({ length: 4 }).map((e, index) => ( // <View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
<CustomSkeleton // {Array.from({ length: 4 }).map((e, index) => (
key={index} // <CustomSkeleton
height={40} // key={index}
width={40} // height={40}
radius={100} // width={40}
/> // radius={100}
))} // />
</View> // ))}
</View> // </View>
// </View>
) )
} }
> >
@@ -201,10 +202,10 @@ export default function Application() {
{data ? ( {data ? (
<Home_BottomFeatureSection listData={listData} /> <Home_BottomFeatureSection listData={listData} />
) : ( ) : (
<CustomSkeleton height={200} /> <CustomSkeleton height={150} />
)} )}
</StackCustom> </StackCustom>
</ViewWrapper> </NewWrapper>
</> </>
); );
} }

View File

@@ -1,5 +1,5 @@
import { Admin_ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate"; import { ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate";
export default function PortofolioCreate() { export default function PortofolioCreate() {
return <Admin_ScreenPortofolioCreate />; return <ScreenPortofolioCreate />;
} }

View File

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

View File

@@ -41,6 +41,7 @@
"expo-symbols": "~1.0.7", "expo-symbols": "~1.0.7",
"expo-system-ui": "~6.0.7", "expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.9", "expo-web-browser": "~15.0.9",
"libphonenumber-js": "^1.12.40",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moti": "^0.30.0", "moti": "^0.30.0",
"react": "19.1.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=="], "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=="], "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=="], "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

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

@@ -30,7 +30,7 @@ export default function AppHeader({
? isIOS26Plus ? isIOS26Plus
? insets.top - 10 ? insets.top - 10
: insets.top : insets.top
: 10; : 40;
const paddingBottom = Platform.OS === "ios" ? 8 : 13; const paddingBottom = Platform.OS === "ios" ? 8 : 13;

View File

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

View File

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

View File

@@ -23,8 +23,8 @@ export {
}; };
// OS Height // OS Height
const OS_ANDROID_HEIGHT = 115 const OS_ANDROID_HEIGHT = 70
const OS_IOS_HEIGHT = 90 const OS_IOS_HEIGHT = 80
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
// Text Size // 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

@@ -120,4 +120,9 @@ Buatkan file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah na
<!-- END Create Box --> <!-- 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 --> <!-- END Use Prompt Now -->

View File

@@ -197,6 +197,22 @@
D5CA1D54CFF74AB4B8B5B583 /* Remove signature files (Xcode workaround) */, D5CA1D54CFF74AB4B8B5B583 /* Remove signature files (Xcode workaround) */,
97C01196E2194AF5A13C7773 /* Remove signature files (Xcode workaround) */, 97C01196E2194AF5A13C7773 /* Remove signature files (Xcode workaround) */,
EB19F4C53C8B434CBAD50897 /* Remove signature files (Xcode workaround) */, EB19F4C53C8B434CBAD50897 /* Remove signature files (Xcode workaround) */,
95ABFC1FE48F4F2ABAF407D8 /* Remove signature files (Xcode workaround) */,
A8B22025FD1A438684F239A7 /* Remove signature files (Xcode workaround) */,
1041145746B64EBB994D41F1 /* Remove signature files (Xcode workaround) */,
A6E85C04CEFE4EB1BEC42A8F /* Remove signature files (Xcode workaround) */,
90185735FF1241C998928089 /* Remove signature files (Xcode workaround) */,
7FC9CE1604F2440EBA9E61D6 /* Remove signature files (Xcode workaround) */,
3D825348A9EE47AEB9FEB9FC /* Remove signature files (Xcode workaround) */,
F18AFF842FD14072824B885B /* Remove signature files (Xcode workaround) */,
CEB288FB251945499C04360F /* Remove signature files (Xcode workaround) */,
EE74CCF78BB449818616D056 /* Remove signature files (Xcode workaround) */,
F1E1D92E19934D0BA39896EB /* Remove signature files (Xcode workaround) */,
A6A8CCDC2FE54E96B87BA30E /* Remove signature files (Xcode workaround) */,
13FFB811A4C84DC3A1CECF5E /* Remove signature files (Xcode workaround) */,
706EFB5693F44588A1644A80 /* Remove signature files (Xcode workaround) */,
04F90533DBC3454B881643D4 /* Remove signature files (Xcode workaround) */,
45B8C246D763424E956A3E6F /* Remove signature files (Xcode workaround) */,
); );
buildRules = ( buildRules = (
); );
@@ -1247,6 +1263,278 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
"; ";
}; };
95ABFC1FE48F4F2ABAF407D8 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
A8B22025FD1A438684F239A7 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
1041145746B64EBB994D41F1 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
A6E85C04CEFE4EB1BEC42A8F /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
90185735FF1241C998928089 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
7FC9CE1604F2440EBA9E61D6 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
3D825348A9EE47AEB9FEB9FC /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
F18AFF842FD14072824B885B /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
CEB288FB251945499C04360F /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
EE74CCF78BB449818616D056 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
F1E1D92E19934D0BA39896EB /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
A6A8CCDC2FE54E96B87BA30E /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
13FFB811A4C84DC3A1CECF5E /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
706EFB5693F44588A1644A80 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
04F90533DBC3454B881643D4 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
45B8C246D763424E956A3E6F /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
import { NewWrapper } from "@/components"; import { NewWrapper, PhoneInputCustom, ViewWrapper } from "@/components";
import ButtonCustom from "@/components/Button/ButtonCustom"; import ButtonCustom from "@/components/Button/ButtonCustom";
import ModalReactNative from "@/components/Modal/ModalReactNative"; import ModalReactNative from "@/components/Modal/ModalReactNative";
import Spacing from "@/components/_ShareComponent/Spacing"; import Spacing from "@/components/_ShareComponent/Spacing";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { DEFAULT_COUNTRY, type CountryData } from "@/constants/countries";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiVersion, BASE_URL } from "@/service/api-config"; import { apiVersion, BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
@@ -10,16 +11,23 @@ import { openBrowser } from "@/utils/openBrower";
import versionBadge from "@/utils/viersionBadge"; import versionBadge from "@/utils/viersionBadge";
import { Redirect } from "expo-router"; import { Redirect } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { RefreshControl, Text, View } from "react-native"; import {
import PhoneInput, { ICountry } from "react-native-international-phone-number"; KeyboardAvoidingView,
Platform,
RefreshControl,
Text,
View,
} from "react-native";
import { parsePhoneNumber } from "libphonenumber-js";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
import EULASection from "./EULASection"; import EULASection from "./EULASection";
export default function LoginView() { export default function LoginView() {
const url = BASE_URL; const url = BASE_URL;
const [version, setVersion] = useState<string>(""); const [version, setVersion] = useState<string>("");
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null); const [selectedCountry, setSelectedCountry] =
const [inputValue, setInputValue] = useState<string>(""); useState<CountryData>(DEFAULT_COUNTRY);
const [phoneNumber, setPhoneNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false); const [refreshing, setRefreshing] = useState<boolean>(false);
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
@@ -43,41 +51,43 @@ export default function LoginView() {
async function handleRefresh() { async function handleRefresh() {
setRefreshing(true); setRefreshing(true);
await onLoadVersion(); await onLoadVersion();
setInputValue(""); setPhoneNumber("");
setSelectedCountry(DEFAULT_COUNTRY);
setLoading(false); setLoading(false);
setRefreshing(false); setRefreshing(false);
} }
function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber);
}
function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country);
}
async function validateData() { async function validateData() {
if (inputValue.length === 0) { if (phoneNumber.length === 0) {
return Toast.show({ return Toast.show({
type: "error", type: "error",
text1: "Masukan nomor anda", text1: "Masukan nomor anda",
}); });
} }
if (selectedCountry === null) { if (phoneNumber.length < 9) {
return Toast.show({
type: "error",
text1: "Pilih negara",
});
}
if (inputValue.length < 9) {
return Toast.show({ return Toast.show({
type: "error", type: "error",
text1: "Nomor tidak valid", 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; return true;
} }
@@ -85,8 +95,17 @@ export default function LoginView() {
const isValid = await validateData(); const isValid = await validateData();
if (!isValid) return; if (!isValid) return;
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || ""; // Format phone number with country code
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, ""); 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; const realNumber = callingCode + fixNumber;
@@ -128,75 +147,84 @@ export default function LoginView() {
} }
return ( return (
<NewWrapper <KeyboardAvoidingView
withBackground behavior={Platform.OS === "ios" ? "padding" : "height"}
refreshControl={ keyboardVerticalOffset={Platform.OS === "ios" ? 100 : 50}
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} /> style={{ flex: 1 }}
}
> >
<View style={GStyles.authContainer}> <ViewWrapper
<View> withBackground
<View style={GStyles.authContainerTitle}> refreshControl={
<Text style={GStyles.authSubTitle}>WELCOME TO</Text> <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
<Spacing height={5} /> }
<Text style={GStyles.authTitle}>HIPMI BADUNG APPS</Text> >
<Spacing height={5} /> <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> </View>
<Spacing height={50} />
<Text <Spacing height={20} />
style={{
position: "absolute", <PhoneInputCustom
bottom: 35, value={phoneNumber}
right: 50, onChangePhoneNumber={setPhoneNumber}
fontSize: 10, selectedCountry={selectedCountry}
fontWeight: "thin", onChangeCountry={setSelectedCountry}
fontStyle: "italic", placeholder="Masukkan nomor"
color: MainColor.white_gray, />
}}
<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> </Text>
</View> </View>
</ViewWrapper>
<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>
<ModalReactNative isVisible={modalVisible}> <ModalReactNative isVisible={modalVisible}>
<EULASection <EULASection
@@ -205,6 +233,6 @@ export default function LoginView() {
setLoadingTerm={setLoadingTerm} setLoadingTerm={setLoadingTerm}
/> />
</ModalReactNative> </ModalReactNative>
</NewWrapper> </KeyboardAvoidingView>
); );
} }

View File

@@ -18,7 +18,7 @@ import _ from "lodash";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { RefreshControl, TouchableOpacity, View } from "react-native"; import { RefreshControl, TouchableOpacity, View } from "react-native";
const PAGE_SIZE = 5; const PAGE_SIZE = 10;
export default function Forum_ViewBeranda3() { export default function Forum_ViewBeranda3() {
const { user } = useAuth(); const { user } = useAuth();

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import {
CenterCustom, CenterCustom,
InformationBox, InformationBox,
NewWrapper, NewWrapper,
PhoneInputCustom,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
@@ -15,6 +16,7 @@ import {
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import DUMMY_IMAGE from "@/constants/dummy-image-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 Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
import { import {
apiMasterBidangBisnis, apiMasterBidangBisnis,
@@ -30,13 +32,12 @@ import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Text, View } from "react-native"; import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { Avatar } from "react-native-paper"; import { Avatar } from "react-native-paper";
export function Admin_ScreenPortofolioCreate() { export function ScreenPortofolioCreate() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null); const [selectedCountry, setSelectedCountry] = useState<CountryData>(DEFAULT_COUNTRY);
const [inputValue, setInputValue] = useState<string>(""); const [phoneNumber, setPhoneNumber] = useState<string>("");
const [data, setData] = useState({ const [data, setData] = useState({
namaBisnis: "", namaBisnis: "",
masterBidangBisnisId: "", masterBidangBisnisId: "",
@@ -72,7 +73,7 @@ export function Admin_ScreenPortofolioCreate() {
useCallback(() => { useCallback(() => {
onLoadMaster(); onLoadMaster();
onLoadMasterSubBidangBisnis(); onLoadMasterSubBidangBisnis();
}, []) }, []),
); );
const onLoadMaster = async () => { const onLoadMaster = async () => {
@@ -97,21 +98,47 @@ export function Admin_ScreenPortofolioCreate() {
const handlerSelectedSubBidang = ({ id }: { id: string }) => { const handlerSelectedSubBidang = ({ id }: { id: string }) => {
const selectedList = subBidangBisnis?.filter( const selectedList = subBidangBisnis?.filter(
(item) => (item?.masterBidangBisnisId as any) === id (item) => (item?.masterBidangBisnisId as any) === id,
); );
setSelectedSubBidang(selectedList as any[]); setSelectedSubBidang(selectedList as any[]);
}; };
const handleInputValue = (phoneNumber: string) => { const handlePhoneChange = (phone: string) => {
setInputValue(phoneNumber); setPhoneNumber(phone);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, ""); // 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; const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber }); setData({ ...data, tlpn: realNumber });
}; };
const handleSelectedCountry = (country: ICountry) => { const handleCountryChange = (country: CountryData) => {
setSelectedCountry(country); 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 ( return (
@@ -168,8 +195,7 @@ export function Admin_ScreenPortofolioCreate() {
.filter((option: any) => { .filter((option: any) => {
const selectedValues = listSubBidangSelected.map((s) => s.id); const selectedValues = listSubBidangSelected.map((s) => s.id);
return ( return (
option.id === item.id || option.id === item.id || !selectedValues.includes(option.id)
!selectedValues.includes(option.id)
); );
}) })
.map((e: any) => ({ .map((e: any) => ({
@@ -188,7 +214,9 @@ export function Admin_ScreenPortofolioCreate() {
<CenterCustom> <CenterCustom>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}> <View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon <ActionIcon
disabled={selectedSubBidang.length === listSubBidangSelected.length} disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => { onPress={() => {
setListSubBidangSelected([ setListSubBidangSelected([
...listSubBidangSelected, ...listSubBidangSelected,
@@ -233,12 +261,11 @@ export function Admin_ScreenPortofolioCreate() {
<Text style={{ color: "red" }}> *</Text> <Text style={{ color: "red" }}> *</Text>
</View> </View>
<Spacing height={5} /> <Spacing height={5} />
<PhoneInput <PhoneInputCustom
value={inputValue} value={phoneNumber}
onChangePhoneNumber={handleInputValue} onChangePhoneNumber={handlePhoneChange}
selectedCountry={selectedCountry} selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry} onChangeCountry={handleCountryChange}
defaultCountry="ID"
placeholder="xxx-xxx-xxx" placeholder="xxx-xxx-xxx"
/> />
</View> </View>

View File

@@ -12,6 +12,7 @@ import {
ICON_SIZE_SMALL, ICON_SIZE_SMALL,
PAGINATION_DEFAULT_TAKE, PAGINATION_DEFAULT_TAKE,
} from "@/constants/constans-value"; } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination"; import { usePagination } from "@/hooks/use-pagination";
import { apiAllUser } from "@/service/api-client/api-user"; import { apiAllUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
@@ -19,21 +20,89 @@ import { router, useFocusEffect } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useRef, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { RefreshControl, View } from "react-native"; 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() { export default function UserSearchMainView_V2() {
const isInitialMount = useRef(true); const isInitialMount = useRef(true);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const { const pagination = usePagination({
listData,
loading,
refreshing,
hasMore,
onRefresh,
loadMore,
isInitialLoad,
} = usePagination({
fetchFunction: async (page, searchQuery) => { fetchFunction: async (page, searchQuery) => {
const response = await apiAllUser({ const response = await apiAllUser({
page: String(page), page: String(page),
@@ -41,127 +110,50 @@ export default function UserSearchMainView_V2() {
}); });
return response; return response;
}, },
pageSize: PAGINATION_DEFAULT_TAKE, pageSize: PAGE_SIZE,
searchQuery: search, searchQuery: search,
}); });
// 🔁 Refresh otomatis saat kembali ke halaman ini // 🔁 Refresh otomatis saat kembali ke halaman ini
useFocusEffect( // useFocusEffect(
useCallback(() => { // useCallback(() => {
if (isInitialMount.current) { // if (isInitialMount.current) {
// Skip saat pertama kali mount // isInitialMount.current = false;
isInitialMount.current = false; // return;
return; // }
} // pagination.onRefresh();
// Hanya refresh saat kembali dari screen lain // }, [pagination.onRefresh]),
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>
);
const { ListEmptyComponent, ListFooterComponent } = const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({ createPaginationComponents({
loading, loading: pagination.loading,
refreshing, refreshing: pagination.refreshing,
listData, listData: pagination.listData,
searchQuery: search, searchQuery: search,
emptyMessage: "Tidak ada pengguna ditemukan", emptyMessage: "Tidak ada pengguna ditemukan",
emptySearchMessage: "Tidak ada hasil pencarian", emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: PAGINATION_DEFAULT_TAKE, skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100, skeletonHeight: 100,
loadingFooterText: "Memuat lebih banyak pengguna...", loadingFooterText: "Memuat lebih banyak pengguna...",
isInitialLoad, isInitialLoad: pagination.isInitialLoad,
}); });
return ( return (
<> <NewWrapper
<NewWrapper headerComponent={renderHeader(search, setSearch)}
headerComponent={renderHeader()} listData={pagination.listData}
listData={listData} renderItem={renderItem}
renderItem={renderItem} onEndReached={pagination.loadMore}
onEndReached={loadMore} refreshControl={
refreshControl={ <RefreshControl
<RefreshControl progressBackgroundColor={MainColor.yellow}
progressBackgroundColor={MainColor.yellow} refreshing={pagination.refreshing}
refreshing={refreshing} onRefresh={pagination.onRefresh}
onRefresh={onRefresh} />
/> }
} ListFooterComponent={ListFooterComponent}
ListFooterComponent={ListFooterComponent} ListEmptyComponent={ListEmptyComponent}
ListEmptyComponent={ListEmptyComponent} />
/>
</>
); );
} }

View File

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

View File

@@ -159,7 +159,7 @@ export const GStyles = StyleSheet.create({
transform: [{ scale: 1.05 }], transform: [{ scale: 1.05 }],
}, },
iconContainer: { iconContainer: {
padding: 8, padding: 5,
borderRadius: 20, borderRadius: 20,
// marginBottom: 4, // marginBottom: 4,
}, },
@@ -207,7 +207,7 @@ export const GStyles = StyleSheet.create({
elevation: 8, // untuk Android elevation: 8, // untuk Android
}, },
bottomBarContainer: { bottomBarContainer: {
paddingHorizontal: 15, paddingHorizontal: 25,
paddingVertical: 10, paddingVertical: 10,
}, },
// =============== BOTTOM BAR =============== // // =============== BOTTOM BAR =============== //

View File

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