Compare commits
11 Commits
app-header
...
qc/30-mar-
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d545f2af9 | |||
| eeb95336f2 | |||
| 6fb3b229c3 | |||
| 76deec9c53 | |||
| 31948f71db | |||
| 16decd89c8 | |||
| ecbcc12abf | |||
| 0cb734e790 | |||
| 0d2fef1878 | |||
| 2e58f8c7b4 | |||
| b6cd308b0b |
8
.qwen/settings.json
Normal file
8
.qwen/settings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add *)"
|
||||
]
|
||||
},
|
||||
"$version": 3
|
||||
}
|
||||
7
.qwen/settings.json.orig
Normal file
7
.qwen/settings.json.orig
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
369
TASKS/fix-phone-input-ios-16.md
Normal file
369
TASKS/fix-phone-input-ios-16.md
Normal 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
|
||||
@@ -101,7 +101,7 @@ packagingOptions {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0.2"
|
||||
versionName "1.0.3"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ require("dotenv").config();
|
||||
export default {
|
||||
name: "HIPMI Badung Connect",
|
||||
slug: "hipmi-mobile",
|
||||
version: "1.0.2",
|
||||
version: "1.0.3",
|
||||
orientation: "portrait",
|
||||
icon: "./assets/images/icon.png",
|
||||
scheme: "hipmimobile",
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
associatedDomains: [
|
||||
"applinks:cld-dkr-hipmi-stg.wibudev.com",
|
||||
],
|
||||
buildNumber: "5",
|
||||
buildNumber: "7",
|
||||
},
|
||||
|
||||
android: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* 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 CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -148,7 +148,7 @@ export default function Application() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
@@ -166,18 +166,19 @@ export default function Application() {
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<View style={GStyles.tabBar}>
|
||||
<View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
|
||||
{Array.from({ length: 4 }).map((e, index) => (
|
||||
<CustomSkeleton
|
||||
key={index}
|
||||
height={40}
|
||||
width={40}
|
||||
radius={100}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
null
|
||||
// <View style={GStyles.tabBar}>
|
||||
// <View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
|
||||
// {Array.from({ length: 4 }).map((e, index) => (
|
||||
// <CustomSkeleton
|
||||
// key={index}
|
||||
// height={40}
|
||||
// width={40}
|
||||
// radius={100}
|
||||
// />
|
||||
// ))}
|
||||
// </View>
|
||||
// </View>
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -201,10 +202,10 @@ export default function Application() {
|
||||
{data ? (
|
||||
<Home_BottomFeatureSection listData={listData} />
|
||||
) : (
|
||||
<CustomSkeleton height={200} />
|
||||
<CustomSkeleton height={150} />
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Admin_ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate";
|
||||
import { ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate";
|
||||
|
||||
export default function PortofolioCreate() {
|
||||
return <Admin_ScreenPortofolioCreate />;
|
||||
return <ScreenPortofolioCreate />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
3
bun.lock
3
bun.lock
@@ -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=="],
|
||||
|
||||
256
components/PhoneInput/PhoneInputCustom.tsx
Normal file
256
components/PhoneInput/PhoneInputCustom.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
@@ -30,7 +30,7 @@ export default function AppHeader({
|
||||
? isIOS26Plus
|
||||
? insets.top - 10
|
||||
: insets.top
|
||||
: 10;
|
||||
: 40;
|
||||
|
||||
const paddingBottom = Platform.OS === "ios" ? 8 : 13;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,8 +23,8 @@ export {
|
||||
};
|
||||
|
||||
// OS Height
|
||||
const OS_ANDROID_HEIGHT = 115
|
||||
const OS_IOS_HEIGHT = 90
|
||||
const OS_ANDROID_HEIGHT = 70
|
||||
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
89
constants/countries.ts
Normal 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)
|
||||
);
|
||||
}
|
||||
@@ -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 -->
|
||||
|
||||
@@ -197,6 +197,22 @@
|
||||
D5CA1D54CFF74AB4B8B5B583 /* Remove signature files (Xcode workaround) */,
|
||||
97C01196E2194AF5A13C7773 /* 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 = (
|
||||
);
|
||||
@@ -1247,6 +1263,278 @@
|
||||
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 */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.2</string>
|
||||
<string>1.0.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -39,7 +39,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>5</string>
|
||||
<string>7</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -33,7 +33,10 @@ export function EventDetailQRCode({
|
||||
const deepLinkURL = `${BASE_URL}/event/${id}/confirmation?userId=${userId}`;
|
||||
|
||||
// Toggle antara HTTPS link dan custom scheme
|
||||
const qrValue = useHttpsLink ? httpsLink : deepLinkURL;
|
||||
// const qrValue = useHttpsLink ? httpsLink : deepLinkURL;
|
||||
const qrValue = deepLinkURL;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<BaseBox>
|
||||
@@ -46,7 +49,7 @@ export function EventDetailQRCode({
|
||||
{qrValue}
|
||||
</TextCustom>
|
||||
<Spacing />
|
||||
<StackCustom direction="row" gap="sm">
|
||||
{/* <StackCustom direction="row" gap="sm">
|
||||
<ButtonCustom
|
||||
onPress={() => setUseHttpsLink(true)}
|
||||
backgroundColor={useHttpsLink ? MainColor.yellow : "transparent"}
|
||||
@@ -69,13 +72,13 @@ export function EventDetailQRCode({
|
||||
>
|
||||
Custom Scheme
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</StackCustom> */}
|
||||
{/* <Spacing />
|
||||
<TextCustom color="gray" align="center" size={"small"}>
|
||||
{useHttpsLink
|
||||
? "✅ Testing Universal Links/App Links (butuh .well-known config)"
|
||||
: "🔧 Testing langsung (tanpa domain verification)"}
|
||||
</TextCustom>
|
||||
</TextCustom> */}
|
||||
</BaseBox>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,12 +128,13 @@ export function Admin_ScreenEventDetail() {
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return <Spacing height={100} />;
|
||||
}, [status, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
headerComponent={headerComponent}
|
||||
// footerComponent={
|
||||
// <View style={{ paddingInline: 8 }}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -94,7 +94,7 @@ export const stylesHome = StyleSheet.create({
|
||||
jobVacancyHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
jobVacancyTitle: {
|
||||
fontSize: 18,
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function Home_ImageSection() {
|
||||
transition={1000}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 120,
|
||||
height: 150,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CenterCustom,
|
||||
InformationBox,
|
||||
NewWrapper,
|
||||
PhoneInputCustom,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
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,
|
||||
@@ -30,13 +32,12 @@ import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import PhoneInput, { ICountry } from "react-native-international-phone-number";
|
||||
import { Avatar } from "react-native-paper";
|
||||
|
||||
export function Admin_ScreenPortofolioCreate() {
|
||||
export function ScreenPortofolioCreate() {
|
||||
const { id } = useLocalSearchParams();
|
||||
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 [data, setData] = useState({
|
||||
namaBisnis: "",
|
||||
masterBidangBisnisId: "",
|
||||
@@ -72,7 +73,7 @@ export function Admin_ScreenPortofolioCreate() {
|
||||
useCallback(() => {
|
||||
onLoadMaster();
|
||||
onLoadMasterSubBidangBisnis();
|
||||
}, [])
|
||||
}, []),
|
||||
);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
@@ -97,21 +98,47 @@ export function Admin_ScreenPortofolioCreate() {
|
||||
|
||||
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
|
||||
const selectedList = subBidangBisnis?.filter(
|
||||
(item) => (item?.masterBidangBisnisId as any) === id
|
||||
(item) => (item?.masterBidangBisnisId as any) === id,
|
||||
);
|
||||
setSelectedSubBidang(selectedList as any[]);
|
||||
};
|
||||
|
||||
const handleInputValue = (phoneNumber: string) => {
|
||||
setInputValue(phoneNumber);
|
||||
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
|
||||
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
|
||||
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 handleSelectedCountry = (country: ICountry) => {
|
||||
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 (
|
||||
@@ -168,8 +195,7 @@ export function Admin_ScreenPortofolioCreate() {
|
||||
.filter((option: any) => {
|
||||
const selectedValues = listSubBidangSelected.map((s) => s.id);
|
||||
return (
|
||||
option.id === item.id ||
|
||||
!selectedValues.includes(option.id)
|
||||
option.id === item.id || !selectedValues.includes(option.id)
|
||||
);
|
||||
})
|
||||
.map((e: any) => ({
|
||||
@@ -188,7 +214,9 @@ export function Admin_ScreenPortofolioCreate() {
|
||||
<CenterCustom>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
||||
<ActionIcon
|
||||
disabled={selectedSubBidang.length === listSubBidangSelected.length}
|
||||
disabled={
|
||||
selectedSubBidang.length === listSubBidangSelected.length
|
||||
}
|
||||
onPress={() => {
|
||||
setListSubBidangSelected([
|
||||
...listSubBidangSelected,
|
||||
@@ -233,12 +261,11 @@ export function Admin_ScreenPortofolioCreate() {
|
||||
<Text style={{ color: "red" }}> *</Text>
|
||||
</View>
|
||||
<Spacing height={5} />
|
||||
<PhoneInput
|
||||
value={inputValue}
|
||||
onChangePhoneNumber={handleInputValue}
|
||||
<PhoneInputCustom
|
||||
value={phoneNumber}
|
||||
onChangePhoneNumber={handlePhoneChange}
|
||||
selectedCountry={selectedCountry}
|
||||
onChangeSelectedCountry={handleSelectedCountry}
|
||||
defaultCountry="ID"
|
||||
onChangeCountry={handleCountryChange}
|
||||
placeholder="xxx-xxx-xxx"
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -159,7 +159,7 @@ export const GStyles = StyleSheet.create({
|
||||
transform: [{ scale: 1.05 }],
|
||||
},
|
||||
iconContainer: {
|
||||
padding: 8,
|
||||
padding: 5,
|
||||
borderRadius: 20,
|
||||
// marginBottom: 4,
|
||||
},
|
||||
@@ -207,7 +207,7 @@ export const GStyles = StyleSheet.create({
|
||||
elevation: 8, // untuk Android
|
||||
},
|
||||
bottomBarContainer: {
|
||||
paddingHorizontal: 15,
|
||||
paddingHorizontal: 25,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
// =============== BOTTOM BAR =============== //
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user