diff --git a/components/_ShareComponent/AndroidWrapper.tsx b/components/_ShareComponent/AndroidWrapper.tsx new file mode 100644 index 0000000..f927996 --- /dev/null +++ b/components/_ShareComponent/AndroidWrapper.tsx @@ -0,0 +1,237 @@ +// @/components/AndroidWrapper.tsx +// Android Wrapper - Based on NewWrapper_V2 (with keyboard handling for Android) +import { MainColor } from "@/constants/color-palet"; +import { OS_HEIGHT } from "@/constants/constans-value"; +import { GStyles } from "@/styles/global-styles"; +import { + ImageBackground, + Keyboard, + KeyboardAvoidingView, + Platform, + ScrollView, + FlatList, + TouchableWithoutFeedback, + View, + StyleProp, + ViewStyle, +} from "react-native"; +import { + NativeSafeAreaViewProps, + SafeAreaView, +} from "react-native-safe-area-context"; +import type { ScrollViewProps, FlatListProps } from "react-native"; +import { useKeyboardForm } from "@/hooks/useKeyboardForm"; + +// --- Base Props --- +interface BaseProps { + withBackground?: boolean; + headerComponent?: React.ReactNode; + footerComponent?: React.ReactNode; + floatingButton?: React.ReactNode; + hideFooter?: boolean; + edgesFooter?: NativeSafeAreaViewProps["edges"]; + style?: StyleProp; + refreshControl?: ScrollViewProps["refreshControl"]; + /** + * Enable keyboard handling with auto-scroll (Android only) + * @default false + */ + enableKeyboardHandling?: boolean; + /** + * Scroll offset when keyboard appears (Android only) + * @default 100 + */ + keyboardScrollOffset?: number; + /** + * Extra padding bottom for content to avoid navigation bar (Android only) + * @default 80 + */ + contentPaddingBottom?: number; + /** + * Padding untuk content container (Android only) + * Set to 0 untuk tidak ada padding, atau custom value sesuai kebutuhan + * @default 16 + */ + contentPadding?: number; +} + +interface StaticModeProps extends BaseProps { + children: React.ReactNode; + listData?: never; + renderItem?: never; +} + +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"]; +} + +type AndroidWrapperProps = StaticModeProps | ListModeProps; + +export function AndroidWrapper(props: AndroidWrapperProps) { + const { + withBackground = false, + headerComponent, + footerComponent, + floatingButton, + hideFooter = false, + edgesFooter = [], + style, + refreshControl, + enableKeyboardHandling = false, + keyboardScrollOffset = 100, + contentPaddingBottom = 80, + contentPadding = 16, + } = props; + + const assetBackground = require("../../assets/images/main-background.png"); + + // Use keyboard hook if enabled + const keyboardForm = enableKeyboardHandling + ? useKeyboardForm(keyboardScrollOffset) + : null; + + const renderContainer = (content: React.ReactNode) => { + if (withBackground) { + return ( + + + {content} + + + ); + } + return {content}; + }; + + // 🔹 Mode Dinamis (FlatList) + if ("listData" in props) { + const listProps = props as ListModeProps; + + return ( + + {headerComponent && ( + {headerComponent} + )} + `${String(item.id)}-${index}`) + } + refreshControl={refreshControl} + onEndReached={listProps.onEndReached} + onEndReachedThreshold={0.5} + ListHeaderComponent={listProps.ListHeaderComponent} + ListFooterComponent={listProps.ListFooterComponent} + ListEmptyComponent={listProps.ListEmptyComponent} + contentContainerStyle={{ + flexGrow: 1, + paddingBottom: + (footerComponent && !hideFooter ? OS_HEIGHT : 0) + + contentPaddingBottom, + padding: contentPadding, + }} + keyboardShouldPersistTaps="handled" + /> + + {/* Footer - Fixed di bawah dengan width 100% */} + {footerComponent && !hideFooter && ( + + {footerComponent} + + )} + + {!footerComponent && !hideFooter && ( + + )} + + {floatingButton && ( + {floatingButton} + )} + + ); + } + + // 🔹 Mode Statis (ScrollView) + const staticProps = props as StaticModeProps; + + return ( + + {headerComponent && ( + {headerComponent} + )} + + + + {renderContainer(staticProps.children)} + + + + {/* Footer - Fixed di bawah dengan width 100% */} + {footerComponent && !hideFooter && ( + + {footerComponent} + + )} + + {!footerComponent && !hideFooter && ( + + )} + + {floatingButton && ( + {floatingButton} + )} + + ); +} + +export default AndroidWrapper; diff --git a/components/_ShareComponent/IOSWrapper.tsx b/components/_ShareComponent/IOSWrapper.tsx new file mode 100644 index 0000000..30fe7e4 --- /dev/null +++ b/components/_ShareComponent/IOSWrapper.tsx @@ -0,0 +1,217 @@ +// @/components/iOSWrapper.tsx +// iOS Wrapper - Based on NewWrapper (stable version for iOS) +import { MainColor } from "@/constants/color-palet"; +import { OS_HEIGHT } from "@/constants/constans-value"; +import { GStyles } from "@/styles/global-styles"; +import { + ImageBackground, + Keyboard, + KeyboardAvoidingView, + Platform, + ScrollView, + FlatList, + TouchableWithoutFeedback, + View, + StyleProp, + ViewStyle, +} from "react-native"; +import { + NativeSafeAreaViewProps, + SafeAreaView, +} from "react-native-safe-area-context"; +import type { ScrollViewProps, FlatListProps } from "react-native"; + +// --- Base Props --- +interface BaseProps { + withBackground?: boolean; + headerComponent?: React.ReactNode; + footerComponent?: React.ReactNode; + floatingButton?: React.ReactNode; + hideFooter?: boolean; + edgesFooter?: NativeSafeAreaViewProps["edges"]; + style?: StyleProp; + refreshControl?: ScrollViewProps["refreshControl"]; +} + +interface StaticModeProps extends BaseProps { + children: React.ReactNode; + listData?: never; + renderItem?: never; +} + +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"]; +} + +type iOSWrapperProps = StaticModeProps | ListModeProps; + +const iOSWrapper = (props: iOSWrapperProps) => { + const { + withBackground = false, + headerComponent, + footerComponent, + floatingButton, + hideFooter = false, + edgesFooter = [], + style, + refreshControl, + } = props; + + const assetBackground = require("../../assets/images/main-background.png"); + + const renderContainer = (content: React.ReactNode) => { + if (withBackground) { + return ( + + + {content} + + + ); + } + return {content}; + }; + + // 🔹 Mode Dinamis (FlatList) + if ("listData" in props) { + const listProps = props as ListModeProps; + + return ( + + {headerComponent && ( + {headerComponent} + )} + + { + if (item.id == null) { + console.warn("Item tanpa 'id':", item); + return `fallback-${index}-${JSON.stringify(item)}`; + } + + return `${String(item.id)}-${index}`; + }) + } + refreshControl={refreshControl} + onEndReached={listProps.onEndReached} + onEndReachedThreshold={0.5} + ListHeaderComponent={listProps.ListHeaderComponent} + ListFooterComponent={listProps.ListFooterComponent} + ListEmptyComponent={listProps.ListEmptyComponent} + contentContainerStyle={{ + flexGrow: 1, + paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0 + }} + keyboardShouldPersistTaps="handled" + /> + + + {/* Footer - tetap di bawah dengan position absolute */} + {footerComponent && !hideFooter && ( + + + {footerComponent} + + + )} + + {!footerComponent && !hideFooter && ( + + )} + + {floatingButton && ( + {floatingButton} + )} + + ); + } + + // 🔹 Mode Statis (ScrollView) + const staticProps = props as StaticModeProps; + + return ( + + {headerComponent && ( + {headerComponent} + )} + + + + + {renderContainer(staticProps.children)} + + + + + {/* Footer - tetap di bawah dengan position absolute */} + {footerComponent && !hideFooter && ( + + + {footerComponent} + + + )} + + {!footerComponent && !hideFooter && ( + + )} + + {floatingButton && ( + {floatingButton} + )} + + ); +}; + +// Styles untuk footer dengan position absolute +const styles = { + footerContainer: { + position: "absolute" as const, + bottom: 0, + left: 0, + right: 0, + backgroundColor: MainColor.darkblue, + }, +}; + +export default iOSWrapper; diff --git a/components/_ShareComponent/OS_Wrapper.tsx b/components/_ShareComponent/OS_Wrapper.tsx new file mode 100644 index 0000000..024c00b --- /dev/null +++ b/components/_ShareComponent/OS_Wrapper.tsx @@ -0,0 +1,161 @@ +// @/components/OS_Wrapper.tsx +// OS-Specific Wrapper - Automatically routes to iOSWrapper or AndroidWrapper +// iOS: Uses NewWrapper (stable for iOS) +// Android: Uses NewWrapper_V2 (with keyboard handling) + +import { Platform } from "react-native"; +import type { ScrollViewProps, FlatListProps } from "react-native"; +import { + NativeSafeAreaViewProps, +} from "react-native-safe-area-context"; +import type { StyleProp, ViewStyle } from "react-native"; +import IOSWrapper from "./IOSWrapper"; +import AndroidWrapper from "./AndroidWrapper"; + +// ========== Base Props ========== +interface BaseProps { + withBackground?: boolean; + headerComponent?: React.ReactNode; + footerComponent?: React.ReactNode; + floatingButton?: React.ReactNode; + hideFooter?: boolean; + edgesFooter?: NativeSafeAreaViewProps["edges"]; + style?: StyleProp; + 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 (Android-specific keyboard handling) ========== +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 untuk content container (Android only) + * iOS ignores this prop + * @default 16 + */ + contentPadding?: 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 OS_WrapperProps = StaticModeProps | ListModeProps; +type PageWrapperProps = PageWrapperStaticProps | PageWrapperListProps; + +/** + * OS_Wrapper - Automatically selects iOSWrapper or AndroidWrapper based on platform + * + * @example Static Mode + * ```tsx + * + * + * + * ``` + * + * @example List Mode + * ```tsx + * } + * ListEmptyComponent={} + * /> + * ``` + */ +export function OS_Wrapper(props: OS_WrapperProps) { + // iOS uses IOSWrapper (based on NewWrapper) + if (Platform.OS === "ios") { + return ; + } + + // Android uses AndroidWrapper (based on NewWrapper_V2 with keyboard handling) + return ; +} + +/** + * PageWrapper - OS_Wrapper with keyboard handling support (Android only) + * Use this for forms with input fields + * + * @example + * ```tsx + * + * + * + * ``` + */ +export function PageWrapper(props: PageWrapperProps) { + // iOS: Keyboard handling props are ignored + if (Platform.OS === "ios") { + const { + enableKeyboardHandling: _, + keyboardScrollOffset: __1, + contentPaddingBottom: __2, + contentPadding: __3, + ...iosProps + } = props; + return ; + } + + // Android: Keyboard handling props are used + return ; +} + +// Re-export individual wrappers for direct usage if needed +export { default as IOSWrapper } from "./IOSWrapper"; +export { default as AndroidWrapper } from "./AndroidWrapper"; + +// Legacy export untuk backward compatibility +export { IOSWrapper as iOSWrapper }; + +export default OS_Wrapper; diff --git a/components/index.ts b/components/index.ts index 6b7f156..62e8df7 100644 --- a/components/index.ts +++ b/components/index.ts @@ -65,6 +65,8 @@ import NewWrapper from "./_ShareComponent/NewWrapper"; import BasicWrapper from "./_ShareComponent/BasicWrapper"; import { FormWrapper } from "./_ShareComponent/FormWrapper"; import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2"; +// OS-Specific Wrappers +import OS_Wrapper, { PageWrapper, IOSWrapper, AndroidWrapper } from "./_ShareComponent/OS_Wrapper"; // Progress import ProgressCustom from "./Progress/ProgressCustom"; @@ -132,6 +134,11 @@ export { BasicWrapper, FormWrapper, NewWrapper_V2, + // OS-Specific Wrappers + OS_Wrapper, + PageWrapper, + IOSWrapper, + AndroidWrapper, // Stack StackCustom, TabBarBackground, diff --git a/docs/OS-Wrapper-Quick-Reference.md b/docs/OS-Wrapper-Quick-Reference.md new file mode 100644 index 0000000..782e2ec --- /dev/null +++ b/docs/OS-Wrapper-Quick-Reference.md @@ -0,0 +1,150 @@ +# OS_Wrapper Quick Reference + +## 📦 Import +```tsx +import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/components"; +``` + +## 🎯 Usage Examples + +### 1. OS_Wrapper - List Mode (Most Common) +```tsx + } + ListEmptyComponent={} + ListFooterComponent={} + onEndReached={loadMore} + refreshControl={ + + } +/> +``` + +### 2. OS_Wrapper - Static Mode +```tsx +} + footerComponent={} + withBackground={true} +> + + +``` + +### 3. PageWrapper - Form dengan Keyboard Handling +```tsx + + + Submit + + + } +> + + + + + +``` + +### 4. Platform-Specific (Rare Cases) +```tsx +// iOS only + + + + +// Android only + + + +``` + +## 📋 Props Reference + +### Common Props (iOS + Android) +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `withBackground` | boolean | false | Show background image | +| `headerComponent` | ReactNode | - | Sticky header component | +| `footerComponent` | ReactNode | - | Fixed footer component | +| `floatingButton` | ReactNode | - | Floating button | +| `hideFooter` | boolean | false | Hide footer section | +| `edgesFooter` | Edge[] | [] | Safe area edges | +| `style` | ViewStyle | - | Custom container style | +| `refreshControl` | RefreshControl | - | Pull to refresh control | + +### Android-Only Props (Ignored on iOS) +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `enableKeyboardHandling` | boolean | false | Auto-scroll on input focus | +| `keyboardScrollOffset` | number | 100 | Scroll offset when keyboard appears | +| `contentPaddingBottom` | number | 80 | Extra bottom padding | +| `contentPadding` | number | 16 | Content padding (all sides) | + +## 🔄 Migration Pattern + +```diff +- import NewWrapper from "@/components/_ShareComponent/NewWrapper"; ++ import { OS_Wrapper } from "@/components"; + +- +``` + +```diff +- import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2"; ++ import { PageWrapper } from "@/components"; + +- ++ + + +``` + +## 💡 Tips + +1. **Pakai OS_Wrapper** untuk screen biasa (list, detail, dll) +2. **Pakai PageWrapper** untuk screen dengan form input (create, edit) +3. **Jangan mix** wrapper lama dan baru di screen yang sama +4. **Test di kedua platform** sebelum commit +5. **Keyboard handling** hanya bekerja di Android dengan PageWrapper + +## ⚠️ Common Mistakes + +### ❌ Wrong +```tsx +// Jangan import langsung dari file +import OS_Wrapper from "@/components/_ShareComponent/OS_Wrapper"; + +// Jangan mix wrapper + + {content} + +``` + +### ✅ Correct +```tsx +// Import dari @/components +import { OS_Wrapper } from "@/components"; + +// Pakai salah satu saja +{content} +``` + +--- + +Last updated: 2026-04-06 diff --git a/screens/Job/MainViewStatus.tsx b/screens/Job/MainViewStatus.tsx index 4be96f4..fbc51bf 100644 --- a/screens/Job/MainViewStatus.tsx +++ b/screens/Job/MainViewStatus.tsx @@ -2,9 +2,9 @@ import { BaseBox, LoaderCustom, + OS_Wrapper, ScrollableCustom, TextCustom, - ViewWrapper, } from "@/components"; import { useAuth } from "@/hooks/use-auth"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; @@ -64,7 +64,7 @@ export default function Job_MainViewStatus() { return ( <> - + {isLoadList ? ( ) : _.isEmpty(listData) ? ( @@ -85,7 +85,7 @@ export default function Job_MainViewStatus() { )) )} - + ); } diff --git a/screens/Job/MainViewStatus2.tsx b/screens/Job/MainViewStatus2.tsx index d4ee1c6..b315808 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, OS_Wrapper, 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} renderItem={renderJobItem} diff --git a/screens/Job/ScreenArchive.tsx b/screens/Job/ScreenArchive.tsx index f9d93fd..9ed8051 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, OS_Wrapper, 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 12d8978..be3bd26 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, OS_Wrapper, TextCustom, ViewWrapper } 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 ( - router.push("/job/create")} /> @@ -78,6 +78,6 @@ export default function Job_ScreenBeranda() { )) )} - + ); } diff --git a/screens/Job/ScreenBeranda2.tsx b/screens/Job/ScreenBeranda2.tsx index db68c1e..5a8dfc8 100644 --- a/screens/Job/ScreenBeranda2.tsx +++ b/screens/Job/ScreenBeranda2.tsx @@ -2,7 +2,7 @@ import { AvatarUsernameAndOtherComponent, BoxWithHeaderSection, FloatingButton, - NewWrapper_V2, + OS_Wrapper, SearchInput, Spacing, StackCustom, @@ -74,7 +74,7 @@ export default function Job_ScreenBeranda2() { ); return ( - diff --git a/screens/Job/ScreenJobCreate.tsx b/screens/Job/ScreenJobCreate.tsx index a0ef1e6..fbceea3 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 fc956e4..43135ef 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 ( - )} - + ); } diff --git a/tasks/TASK-005-OS-Wrapper-Implementation.md b/tasks/TASK-005-OS-Wrapper-Implementation.md new file mode 100644 index 0000000..71594ce --- /dev/null +++ b/tasks/TASK-005-OS-Wrapper-Implementation.md @@ -0,0 +1,255 @@ +# TASK-005: OS_Wrapper Implementation + +## 📋 Overview +Migrasi dari `NewWrapper` dan `NewWrapper_V2` ke `OS_Wrapper` yang otomatis menyesuaikan dengan platform (iOS/Android). + +## 🎯 Goals +- ✅ Mengganti penggunaan `NewWrapper` → `OS_Wrapper` di user screens +- ✅ Mengganti penggunaan `NewWrapper_V2` → `OS_Wrapper` atau `PageWrapper` di form screens +- ✅ Memastikan tabs dan UI konsisten di iOS dan Android +- ✅ Backward compatible (wrapper lama tetap ada) + +## 📦 Available Wrappers + +### 1. **OS_Wrapper** (Recommended) +Auto-detect platform dan routing ke wrapper yang sesuai: +- iOS → `IOSWrapper` (berbasis NewWrapper) +- Android → `AndroidWrapper` (berbasis NewWrapper_V2) + +### 2. **PageWrapper** (For Forms) +Sama seperti OS_Wrapper tapi dengan keyboard handling (Android only): +- `enableKeyboardHandling` - Auto scroll saat input focus +- `keyboardScrollOffset` - Offset scroll (default: 100) +- `contentPaddingBottom` - Extra padding bottom (default: 80) +- `contentPadding` - Content padding (default: 16) + +### 3. **IOSWrapper** / **AndroidWrapper** (Direct Usage) +Untuk kasus khusus yang butuh platform-specific behavior. + +## 📝 Migration Guide + +### Before (Old Way) +```tsx +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; + +// atau +import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2"; +``` + +### After (New Way) +```tsx +import { OS_Wrapper, PageWrapper } from "@/components"; + +// Static mode + + + + +// List mode + } + ListEmptyComponent={} +/> + +// Form dengan keyboard handling + + + +``` + +## 🚀 Implementation Phases + +### Phase 1: User Screens (Priority: HIGH) +Files yang perlu di-migrate: + +#### 1.1 Home/Beranda +- [ ] `screens/Beranda/ScreenBeranda.tsx` atau `ScreenBeranda2.tsx` + - Ganti `NewWrapper` → `OS_Wrapper` + - Test tabs behavior di iOS dan Android + +#### 1.2 Profile Screens +- [ ] `screens/Profile/ScreenProfile.tsx` +- [ ] `screens/Profile/ScreenProfileEdit.tsx` +- [ ] `screens/Profile/ScreenProfileCreate.tsx` + +#### 1.3 Forum/Discussion +- [ ] `screens/Forum/ScreenForum.tsx` +- [ ] `screens/Forum/ScreenForumDetail.tsx` +- [ ] `screens/Forum/ScreenForumCreate.tsx` → pakai `PageWrapper` (ada form) + +#### 1.4 Portfolio +- [ ] `screens/Portfolio/ScreenPortfolio.tsx` +- [ ] `screens/Portfolio/ScreenPortfolioCreate.tsx` → pakai `PageWrapper` +- [ ] `screens/Portfolio/ScreenPortfolioEdit.tsx` → pakai `PageWrapper` + +### Phase 2: Admin Screens (Priority: MEDIUM) +Files yang perlu di-migrate: + +#### 2.1 Event Management +- [ ] `screens/Admin/Event/ScreenEventList.tsx` +- [ ] `screens/Admin/Event/ScreenEventCreate.tsx` → pakai `PageWrapper` +- [ ] `screens/Admin/Event/ScreenEventEdit.tsx` → pakai `PageWrapper` + +#### 2.2 Voting Management +- [ ] `screens/Admin/Voting/ScreenVotingList.tsx` +- [ ] `screens/Admin/Voting/ScreenVotingCreate.tsx` → pakai `PageWrapper` +- [ ] `screens/Admin/Voting/ScreenVotingEdit.tsx` → pakai `PageWrapper` + +#### 2.3 Donation Management +- [ ] `screens/Admin/Donation/ScreenDonationList.tsx` +- [ ] `screens/Admin/Donation/ScreenDonationCreate.tsx` → pakai `PageWrapper` +- [ ] `screens/Admin/Donation/ScreenDonationEdit.tsx` → pakai `PageWrapper` + +#### 2.4 Job Management +- [ ] `screens/Job/ScreenJobList.tsx` (jika ada) +- [ ] `screens/Job/ScreenJobCreate.tsx` → sudah pakai `BoxButtonOnFooter`? +- [ ] `screens/Job/ScreenJobEdit.tsx` → sudah pakai `BoxButtonOnFooter`? + +### Phase 3: Other Screens (Priority: LOW) +- [ ] `screens/Investasi/` - Investment screens +- [ ] `screens/Kolaborasi/` - Collaboration screens +- [ ] Other user-facing screens + +## ✅ Testing Checklist + +Setiap screen yang sudah di-migrate, test: + +### iOS Testing +- [ ] UI tampil sesuai design +- [ ] Tabs berfungsi dengan baik +- [ ] ScrollView/FlatList scroll dengan smooth +- [ ] Keyboard tidak menutupi input (jika pakai PageWrapper) +- [ ] Footer muncul di posisi yang benar +- [ ] Pull to refresh berfungsi (jika ada) + +### Android Testing +- [ ] UI tampil sesuai design +- [ ] Tabs berfungsi dengan baik +- [ ] ScrollView/FlatList scroll dengan smooth +- [ ] Keyboard handling: auto scroll saat input focus (jika pakai PageWrapper) +- [ ] Footer muncul di posisi yang benar (tidak tertutup navigation bar) +- [ ] Pull to refresh berfungsi (jika ada) + +### Common Testing +- [ ] Background image muncul (jika `withBackground={true}`) +- [ ] Sticky header berfungsi (jika ada `headerComponent`) +- [ ] Footer fixed di bottom (jika ada `footerComponent`) +- [ ] Floating button muncul (jika ada `floatingButton`) +- [ ] Loading skeleton muncul saat pagination +- [ ] Empty state muncul saat data kosong + +## 📌 Notes + +### Kapan pakai OS_Wrapper vs PageWrapper? +- **OS_Wrapper**: Untuk screen yang hanya menampilkan data (list, detail, dll) +- **PageWrapper**: Untuk screen yang ada form input (create, edit, login, dll) + +### Props yang sering digunakan: + +#### Untuk List Screen: +```tsx + + } +/> +``` + +#### Untuk Static Screen: +```tsx +} + footerComponent={} +> + + +``` + +#### Untuk Form Screen: +```tsx + + Submit + + } +> + + +``` + +### Migration Pattern: + +```tsx +// OLD +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; + + + +// NEW +import { OS_Wrapper } from "@/components"; + + +``` + +## 🐛 Troubleshooting + +### Issue: Tabs tidak muncul di Android +**Solution**: Pastikan tidak ada custom padding yang overriding default behavior. Jika masih bermasalah, cek apakah `contentPadding` atau `contentPaddingBottom` terlalu besar. + +### Issue: Keyboard menutupi input di Android +**Solution**: Pastikan pakai `PageWrapper` dengan `enableKeyboardHandling={true}`. Adjust `keyboardScrollOffset` jika perlu. + +### Issue: Footer terlalu jauh dari bottom +**Solution**: Kurangi `contentPaddingBottom` (default: 80). Untuk list screen tanpa navigation bar overlay, bisa set ke 0. + +### Issue: White space di bottom saat keyboard close (Android) +**Solution**: Ini sudah di-fix di AndroidWrapper. Pastikan screen pakai OS_Wrapper/PageWrapper, bukan NewWrapper langsung. + +## 📊 Progress Tracking + +| Phase | Total Files | Migrated | Testing | Status | +|-------|-------------|----------|---------|--------| +| Phase 1 (User) | TBD | 0 | 0 | ⏳ Pending | +| Phase 2 (Admin) | TBD | 0 | 0 | ⏳ Pending | +| Phase 3 (Other) | TBD | 0 | 0 | ⏳ Pending | +| **Total** | **TBD** | **0** | **0** | **0%** | + +## 🔄 Rollback Plan + +Jika ada issue yang tidak bisa di-fix dalam 1 jam: +1. Revert perubahan di file yang bermasalah +2. Kembali ke NewWrapper/NewWrapper_V2 +3. Dokumentasikan issue di CHANGE_LOG.md +4. Investigasi lebih lanjut dan coba lagi + +--- + +**Co-authored-by**: Qwen-Coder +**Created**: 2026-04-06 +**Status**: Ready for Implementation