Compare commits
2 Commits
app-header
...
tabs/25-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e58f8c7b4 | |||
| b6cd308b0b |
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
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -111,7 +112,6 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
return `${String(item.id)}-${index}`;
|
||||
})
|
||||
}
|
||||
|
||||
refreshControl={refreshControl} // ✅ dari BaseProps
|
||||
onEndReached={listProps.onEndReached}
|
||||
onEndReachedThreshold={0.5}
|
||||
@@ -156,15 +156,27 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
<View style={GStyles.stickyHeader}>{headerComponent}</View>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
<View style={{ flex: 0 }} collapsable={false}>
|
||||
<ScrollView
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
refreshControl={refreshControl} // ✅ sekarang valid
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
{renderContainer(staticProps.children)}
|
||||
</TouchableWithoutFeedback>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* <ScrollView
|
||||
contentContainerStyle={{ flexGrow: 0 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
refreshControl={refreshControl} // ✅ sekarang valid
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
{renderContainer(staticProps.children)}
|
||||
</TouchableWithoutFeedback>
|
||||
</ScrollView>
|
||||
</ScrollView> */}
|
||||
|
||||
{footerComponent ? (
|
||||
<SafeAreaView
|
||||
|
||||
@@ -24,7 +24,7 @@ export {
|
||||
|
||||
// OS Height
|
||||
const OS_ANDROID_HEIGHT = 115
|
||||
const OS_IOS_HEIGHT = 90
|
||||
const OS_IOS_HEIGHT = 80
|
||||
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
|
||||
|
||||
// Text Size
|
||||
|
||||
@@ -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,7 @@
|
||||
D5CA1D54CFF74AB4B8B5B583 /* Remove signature files (Xcode workaround) */,
|
||||
97C01196E2194AF5A13C7773 /* Remove signature files (Xcode workaround) */,
|
||||
EB19F4C53C8B434CBAD50897 /* Remove signature files (Xcode workaround) */,
|
||||
95ABFC1FE48F4F2ABAF407D8 /* Remove signature files (Xcode workaround) */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -1247,6 +1248,23 @@
|
||||
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\";
|
||||
";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NewWrapper } from "@/components";
|
||||
import { NewWrapper, ViewWrapper } from "@/components";
|
||||
import ButtonCustom from "@/components/Button/ButtonCustom";
|
||||
import ModalReactNative from "@/components/Modal/ModalReactNative";
|
||||
import Spacing from "@/components/_ShareComponent/Spacing";
|
||||
@@ -128,7 +128,7 @@ export default function LoginView() {
|
||||
}
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
<ViewWrapper
|
||||
withBackground
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
@@ -205,6 +205,6 @@ export default function LoginView() {
|
||||
setLoadingTerm={setLoadingTerm}
|
||||
/>
|
||||
</ModalReactNative>
|
||||
</NewWrapper>
|
||||
</ViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -94,7 +94,7 @@ export const stylesHome = StyleSheet.create({
|
||||
jobVacancyHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 16,
|
||||
marginBottom: 20,
|
||||
},
|
||||
jobVacancyTitle: {
|
||||
fontSize: 18,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -33,7 +33,7 @@ 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>("");
|
||||
@@ -72,7 +72,7 @@ export function Admin_ScreenPortofolioCreate() {
|
||||
useCallback(() => {
|
||||
onLoadMaster();
|
||||
onLoadMasterSubBidangBisnis();
|
||||
}, [])
|
||||
}, []),
|
||||
);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
@@ -97,7 +97,7 @@ 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[]);
|
||||
};
|
||||
@@ -168,8 +168,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 +187,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,
|
||||
|
||||
@@ -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,7 +110,7 @@ export default function UserSearchMainView_V2() {
|
||||
});
|
||||
return response;
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
pageSize: PAGE_SIZE,
|
||||
searchQuery: search,
|
||||
});
|
||||
|
||||
@@ -49,119 +118,42 @@ export default function UserSearchMainView_V2() {
|
||||
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>
|
||||
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,
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ export const TabsStyles: BottomTabNavigationOptions = {
|
||||
tabBarStyle: Platform.select({
|
||||
ios: {
|
||||
borderTopWidth: 0,
|
||||
paddingTop: 5,
|
||||
paddingTop: 12,
|
||||
height: OS_IOS_HEIGHT,
|
||||
},
|
||||
android: {
|
||||
|
||||
Reference in New Issue
Block a user