diff --git a/app/(application)/(user)/job/[id]/[status]/detail.tsx b/app/(application)/(user)/job/[id]/[status]/detail.tsx index 12bd4c4..9564cab 100644 --- a/app/(application)/(user)/job/[id]/[status]/detail.tsx +++ b/app/(application)/(user)/job/[id]/[status]/detail.tsx @@ -5,7 +5,7 @@ import { DrawerCustom, LoaderCustom, MenuDrawerDynamicGrid, - NewWrapper_V2, + PageWrapper, Spacing, StackCustom, } from "@/components"; @@ -72,7 +72,7 @@ export default function JobDetailStatus() { ), }} /> - + {isLoadData ? ( ) : ( @@ -96,7 +96,7 @@ export default function JobDetailStatus() { )} - + ) : ( - + <> @@ -83,17 +83,10 @@ export default function JobDetailArchive() { > Publish kembali - {/* */} - + )} ); diff --git a/app/(application)/(user)/job/[id]/index.tsx b/app/(application)/(user)/job/[id]/index.tsx index a4183a1..c00e257 100644 --- a/app/(application)/(user)/job/[id]/index.tsx +++ b/app/(application)/(user)/job/[id]/index.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { ButtonCustom, LoaderCustom, Spacing, StackCustom, ViewWrapper } from "@/components"; +import { ButtonCustom, LoaderCustom, PageWrapper, Spacing, StackCustom } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection"; @@ -88,7 +88,7 @@ export default function JobDetail() { }; return ( - + {isLoading ? ( ) : ( @@ -101,6 +101,6 @@ export default function JobDetail() { )} - + ); } diff --git a/components/_ShareComponent/PageWrapper.tsx b/components/_ShareComponent/PageWrapper.tsx new file mode 100644 index 0000000..f75f1aa --- /dev/null +++ b/components/_ShareComponent/PageWrapper.tsx @@ -0,0 +1,229 @@ +/** + * PageWrapper - Platform-specific wrapper component + * + * Routes to: + * - iOS: NewWrapper (stable, tested) + * - Android: NewWrapper_V2 (with keyboard handling fix) + * + * Props are automatically adjusted based on platform. + * + * @example + * + * {children} + * + */ + +import { Platform } from "react-native"; +import { NewWrapper_V2 } from "./NewWrapper_V2"; +import type { NativeSafeAreaViewProps } from "react-native-safe-area-context"; +import type { ScrollViewProps, FlatListProps } from "react-native"; +import NewWrapper from "./NewWrapper"; + +// ========== Base Props ========== +interface BaseProps { + withBackground?: boolean; + headerComponent?: React.ReactNode; + footerComponent?: React.ReactNode; + floatingButton?: React.ReactNode; + hideFooter?: boolean; + edgesFooter?: NativeSafeAreaViewProps["edges"]; + style?: any; + refreshControl?: ScrollViewProps["refreshControl"]; +} + +// ========== Static Mode Props ========== +interface StaticModeProps extends BaseProps { + children: React.ReactNode; + listData?: never; + renderItem?: never; +} + +// ========== List Mode Props ========== +interface ListModeProps extends BaseProps { + children?: never; + listData?: any[]; + renderItem?: FlatListProps["renderItem"]; + onEndReached?: () => void; + ListHeaderComponent?: React.ReactElement | null; + ListFooterComponent?: React.ReactElement | null; + ListEmptyComponent?: React.ReactElement | null; + keyExtractor?: FlatListProps["keyExtractor"]; +} + +// ========== PageWrapper Props ========== +interface PageWrapperBaseProps extends BaseProps { + /** + * Enable keyboard handling (Android only - NewWrapper_V2) + * iOS ignores this prop + * @default false + */ + enableKeyboardHandling?: boolean; + + /** + * Scroll offset when keyboard appears (Android only) + * iOS ignores this prop + * @default 100 + */ + keyboardScrollOffset?: number; + + /** + * Extra padding bottom for content (Android only) + * iOS ignores this prop + * @default 80 + */ + contentPaddingBottom?: number; + + /** + * Padding Top for content container (Android only) + * iOS ignores this prop + * @default 8 + */ + contentPaddingTop?: number; + + /** + * Padding Horizontal for content container (Android only) + * iOS ignores this prop + * @default 0 + */ + contentPaddingHorizontal?: number; +} + +interface PageWrapperStaticProps extends PageWrapperBaseProps { + children: React.ReactNode; + listData?: never; + renderItem?: never; +} + +interface PageWrapperListProps extends PageWrapperBaseProps { + children?: never; + listData?: any[]; + renderItem?: FlatListProps["renderItem"]; + onEndReached?: () => void; + ListHeaderComponent?: React.ReactElement | null; + ListFooterComponent?: React.ReactElement | null; + ListEmptyComponent?: React.ReactElement | null; + keyExtractor?: FlatListProps["keyExtractor"]; +} + +type PageWrapperProps = PageWrapperStaticProps | PageWrapperListProps; + +export function PageWrapper(props: PageWrapperProps) { + const { + withBackground, + headerComponent, + footerComponent, + floatingButton, + hideFooter, + edgesFooter, + style, + refreshControl, + enableKeyboardHandling, + keyboardScrollOffset, + contentPaddingBottom, + contentPaddingTop, + contentPaddingHorizontal, + ...restProps + } = props; + + // ========== Android: Use NewWrapper_V2 with keyboard handling ========== + if (Platform.OS === "android") { + if ("listData" in props) { + // List mode + const listProps = props as PageWrapperListProps; + return ( + + ); + } + + // Static mode + const staticProps = props as PageWrapperStaticProps; + return ( + + {staticProps.children} + + ); + } + + // ========== iOS: Use NewWrapper (stable) ========== + if ("listData" in props) { + // List mode + const listProps = props as PageWrapperListProps; + return ( + + ); + } + + // Static mode + const staticProps = props as PageWrapperStaticProps; + return ( + + {staticProps.children} + + ); +} + +export default PageWrapper; diff --git a/components/index.ts b/components/index.ts index 6b7f156..9d850ee 100644 --- a/components/index.ts +++ b/components/index.ts @@ -65,6 +65,7 @@ import NewWrapper from "./_ShareComponent/NewWrapper"; import BasicWrapper from "./_ShareComponent/BasicWrapper"; import { FormWrapper } from "./_ShareComponent/FormWrapper"; import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2"; +import { PageWrapper } from "./_ShareComponent/PageWrapper"; // Progress import ProgressCustom from "./Progress/ProgressCustom"; @@ -132,6 +133,7 @@ export { BasicWrapper, FormWrapper, NewWrapper_V2, + PageWrapper, // Stack StackCustom, TabBarBackground, diff --git a/docs/PAGEWRAPPER-USAGE.md b/docs/PAGEWRAPPER-USAGE.md new file mode 100644 index 0000000..dc75abc --- /dev/null +++ b/docs/PAGEWRAPPER-USAGE.md @@ -0,0 +1,304 @@ +# PageWrapper - Platform-Specific Wrapper + +## 📋 Overview + +`PageWrapper` adalah wrapper component yang secara otomatis memilih wrapper yang tepat berdasarkan platform: + +- **iOS**: Menggunakan `NewWrapper` (stable, tested) +- **Android**: Menggunakan `NewWrapper_V2` (dengan keyboard handling fix) + +## 🎯 Kapan Menggunakan PageWrapper? + +### ✅ **Gunakan PageWrapper untuk:** +- Screen baru yang kamu buat +- Migrasi screen existing dari `NewWrapper`/`ViewWrapper` +- Form screens dengan TextInput/TextArea +- List screens dengan pagination + +### ❌ **Jangan gunakan PageWrapper untuk:** +- Screen yang sudah menggunakan `NewWrapper_V2` langsung dan sudah tested di iOS +- Custom wrapper requirements + +--- + +## 📝 Usage + +### **Basic Usage (Static Content)** + +```typescript +import { PageWrapper } from "@/components"; + +export function MyScreen() { + return ( + } + > + + + + + + ); +} +``` + +### **With Keyboard Handling (Android Only)** + +```typescript +} +> + + true}> + + + true}> + + + + +``` + +### **List Mode (Pagination)** + +```typescript + + } +/> +``` + +--- + +## 🔧 Props + +### **Common Props (iOS & Android)** + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `footerComponent` | `ReactNode` | - | Fixed footer component | +| `headerComponent` | `ReactNode` | - | Header component (sticky) | +| `floatingButton` | `ReactNode` | - | Floating button overlay | +| `hideFooter` | `boolean` | `false` | Hide footer footer | +| `withBackground` | `boolean` | `false` | Use background image | +| `style` | `ViewStyle` | - | Custom container style | +| `refreshControl` | `RefreshControl` | - | Pull-to-refresh control | + +### **Android-Only Props** (Ignored di iOS) + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `enableKeyboardHandling` | `boolean` | `false` | Enable keyboard auto-scroll | +| `keyboardScrollOffset` | `number` | `100` | Scroll offset when keyboard appears | +| `contentPaddingBottom` | `number` | `80` | Bottom padding for content | +| `contentPaddingTop` | `number` | `8` | Top padding for content | +| `contentPaddingHorizontal` | `number` | `0` | Horizontal padding for content | + +--- + +## 📊 Platform Behavior + +| Feature | iOS (NewWrapper) | Android (NewWrapper_V2) | +|---------|------------------|------------------------| +| **Keyboard Handling** | ❌ No auto-scroll | ✅ Auto-scroll to input | +| **Footer Position** | ✅ Fixed bottom | ✅ Fixed bottom (absolute) | +| **Safe Area** | ✅ Handled | ✅ Handled | +| **Content Padding** | Default | Customizable | +| **List Mode** | ✅ Supported | ✅ Supported | + +--- + +## 🔄 Migration Guide + +### **From NewWrapper** + +```typescript +// BEFORE +import { NewWrapper } from "@/components"; + + + {children} + + +// AFTER +import { PageWrapper } from "@/components"; + + + {children} + +``` + +### **From NewWrapper_V2** + +```typescript +// BEFORE +import { NewWrapper_V2 } from "@/components"; + + + true}> + + + + +// AFTER +import { PageWrapper } from "@/components"; + + + true}> + + + +``` + +--- + +## ⚠️ Important Notes + +### **For Form Screens (Android)** + +Jika menggunakan `enableKeyboardHandling`, **WAJIB wrap semua input** dengan `View onStartShouldSetResponder`: + +```typescript + true}> + + +``` + +**Kenapa?** +- Mencegah keyboard handling conflict +- Memastikan tap outside dismiss keyboard +- Konsisten behavior di Android + +### **For iOS Users** + +Props berikut **diabaikan di iOS**: +- `enableKeyboardHandling` +- `keyboardScrollOffset` +- `contentPaddingBottom` +- `contentPaddingTop` +- `contentPaddingHorizontal` + +iOS menggunakan `NewWrapper` yang sudah stable tanpa keyboard handling. + +--- + +## 🎨 Examples + +### **Example 1: Simple Form** + +```typescript +import { PageWrapper, TextInputCustom, StackCustom } from "@/components"; + +export function SimpleForm() { + return ( + } + > + + true}> + + + true}> + + + + + ); +} +``` + +### **Example 2: List with Pagination** + +```typescript +import { PageWrapper } from "@/components"; + +export function UserList() { + const pagination = usePagination({ fetchFunction: fetchUsers }); + + return ( + } + onEndReached={pagination.loadMore} + ListEmptyComponent={} + ListFooterComponent={} + refreshControl={ + + } + /> + ); +} +``` + +### **Example 3: Detail Screen (No Footer)** + +```typescript +import { PageWrapper } from "@/components"; + +export function DetailScreen() { + return ( + + + Title + Description + + + ); +} +``` + +--- + +## 🚀 Future Plans + +### **Phase 1: Current** (Now) +- ✅ `PageWrapper` created +- ✅ iOS → `NewWrapper` (stable) +- ✅ Android → `NewWrapper_V2` (keyboard fix) + +### **Phase 2: iOS Migration** (1-2 months) +- [ ] Fix iOS bugs di `NewWrapper_V2` +- [ ] Test `NewWrapper_V2` di iOS devices +- [ ] Update `PageWrapper` untuk use V2 untuk iOS juga + +### **Phase 3: Unify** (3 months) +- [ ] Deprecate `NewWrapper` lama +- [ ] Rename `NewWrapper_V2` → `NewWrapper` +- [ ] Update `PageWrapper` untuk always use V2 + +--- + +## 📚 Related Files + +- `components/_ShareComponent/PageWrapper.tsx` - Main component +- `components/_ShareComponent/NewWrapper.tsx` - iOS wrapper +- `components/_ShareComponent/NewWrapper_V2.tsx` - Android wrapper +- `hooks/useKeyboardForm.ts` - Keyboard handling hook + +--- + +**Last Updated**: 2026-04-06 +**Created by**: AI Assistant +**Status**: ✅ Ready to use diff --git a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj index d2032ce..9a03aa5 100644 --- a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj +++ b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj @@ -156,6 +156,7 @@ 92A25C61F4E34FB6A36E415B /* Remove signature files (Xcode workaround) */, 6440E59133324659A2C60D0B /* Remove signature files (Xcode workaround) */, 35CC0495598542E6801662A3 /* Remove signature files (Xcode workaround) */, + 5ED53AFC8AD1445DA81C7BD4 /* Remove signature files (Xcode workaround) */, ); buildRules = ( ); @@ -501,6 +502,23 @@ rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; "; }; + 5ED53AFC8AD1445DA81C7BD4 /* 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 */ diff --git a/screens/Job/MainViewStatus2.tsx b/screens/Job/MainViewStatus2.tsx index b8951f2..1e9857c 100644 --- a/screens/Job/MainViewStatus2.tsx +++ b/screens/Job/MainViewStatus2.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { BaseBox, NewWrapper_V2, ScrollableCustom, TextCustom } from "@/components"; +import { BaseBox, PageWrapper, ScrollableCustom, TextCustom } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; import { createPaginationComponents } from "@/helpers/paginationHelpers"; @@ -86,7 +86,7 @@ export default function Job_MainViewStatus2() { ); return ( - {scrollComponent}} listData={pagination.listData} diff --git a/screens/Job/ScreenArchive.tsx b/screens/Job/ScreenArchive.tsx index f9d93fd..94bd677 100644 --- a/screens/Job/ScreenArchive.tsx +++ b/screens/Job/ScreenArchive.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components"; +import { BaseBox, LoaderCustom, PageWrapper, TextCustom } from "@/components"; import { useAuth } from "@/hooks/use-auth"; import { apiJobGetAll } from "@/service/api-client/api-job"; import { useFocusEffect } from "expo-router"; @@ -33,7 +33,7 @@ export default function Job_ScreenArchive() { }; return ( - + {isLoadData ? ( ) : _.isEmpty(listData) ? ( @@ -52,6 +52,6 @@ export default function Job_ScreenArchive() { )) )} - + ); } diff --git a/screens/Job/ScreenArchive2.tsx b/screens/Job/ScreenArchive2.tsx index 7bcbb30..3d2e98b 100644 --- a/screens/Job/ScreenArchive2.tsx +++ b/screens/Job/ScreenArchive2.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { BaseBox, NewWrapper_V2, TextCustom, ViewWrapper } from "@/components"; +import { BaseBox, PageWrapper, TextCustom } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { useAuth } from "@/hooks/use-auth"; @@ -55,7 +55,7 @@ export default function Job_ScreenArchive2() { ); return ( - + setSearch(text), 500)} diff --git a/screens/Job/ScreenJobCreate.tsx b/screens/Job/ScreenJobCreate.tsx index 44866eb..e2eb09b 100644 --- a/screens/Job/ScreenJobCreate.tsx +++ b/screens/Job/ScreenJobCreate.tsx @@ -4,7 +4,7 @@ import { ButtonCustom, InformationBox, LandscapeFrameUploaded, - NewWrapper_V2, + PageWrapper, Spacing, StackCustom, TextAreaCustom, @@ -118,7 +118,7 @@ export function Job_ScreenCreate() { }; return ( - - + ); } diff --git a/screens/Job/ScreenJobEdit.tsx b/screens/Job/ScreenJobEdit.tsx index 344c6bb..6a8fd14 100644 --- a/screens/Job/ScreenJobEdit.tsx +++ b/screens/Job/ScreenJobEdit.tsx @@ -8,7 +8,7 @@ import { InformationBox, LandscapeFrameUploaded, LoaderCustom, - NewWrapper_V2, + PageWrapper, Spacing, StackCustom, TextAreaCustom, @@ -134,7 +134,7 @@ export function Job_ScreenEdit() { }; return ( - )} - + ); }