From 1a5ca78041c8e7723df061897850e3aa26d3aacb Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Tue, 7 Apr 2026 17:50:15 +0800 Subject: [PATCH] feat: Complete OS_Wrapper implementation with keyboard handling and PADDING_INLINE OS_Wrapper System: - Simplify API: Remove PageWrapper, merge keyboard props into OS_Wrapper - Add auto-scroll when keyboard appears (Android only) - Add tap-to-dismiss keyboard for both Static and List modes - Fix contentPaddingBottom default to 250px (prevent keyboard overlap) - Change default contentPadding to 0 (per-screen control) - Remove Platform.OS checks from IOSWrapper and AndroidWrapper Constants: - Add PADDING_INLINE constant (16px) for consistent inline padding - Add OS_PADDING_TOP constants for tab layouts Job Screens Migration (9 files): - Apply PADDING_INLINE to all Job screens: - ScreenBeranda, ScreenBeranda2 - ScreenArchive, ScreenArchive2 - MainViewStatus, MainViewStatus2 - ScreenJobCreate, ScreenJobEdit - Job detail screen Keyboard Handling: - Simplified useKeyboardForm hook - Auto-scroll by keyboard height when keyboard appears - Track scroll position for accurate scroll targets - TouchableWithoutFeedback wraps all content for tap-to-dismiss Documentation: - Update TASK-005 with Phase 1 completion status - Update Quick Reference with unified API examples Co-authored-by: Qwen-Coder Co-authored-by: Qwen-Coder --- .../(user)/job/(tabs)/_layout.tsx | 25 ++- .../(user)/job/[id]/[status]/detail.tsx | 7 +- components/_ShareComponent/AndroidWrapper.tsx | 88 +++++---- components/_ShareComponent/FormWrapper.tsx | 4 +- components/_ShareComponent/IOSWrapper.tsx | 9 +- components/_ShareComponent/OS_Wrapper.tsx | 103 +++++----- components/index.ts | 3 +- constants/constans-value.ts | 12 +- docs/OS-Wrapper-Quick-Reference.md | 28 ++- hooks/useKeyboardForm.ts | 64 +++--- .../project.pbxproj | 18 ++ screens/Job/MainViewStatus.tsx | 3 +- screens/Job/MainViewStatus2.tsx | 3 +- screens/Job/ScreenArchive.tsx | 3 +- screens/Job/ScreenArchive2.tsx | 5 +- screens/Job/ScreenBeranda.tsx | 2 + screens/Job/ScreenBeranda2.tsx | 4 +- screens/Job/ScreenJobCreate.tsx | 12 +- screens/Job/ScreenJobEdit.tsx | 8 +- tasks/TASK-005-OS-Wrapper-Implementation.md | 186 +++++++++++------- 20 files changed, 349 insertions(+), 238 deletions(-) diff --git a/app/(application)/(user)/job/(tabs)/_layout.tsx b/app/(application)/(user)/job/(tabs)/_layout.tsx index ef24fba..ff0ec2d 100644 --- a/app/(application)/(user)/job/(tabs)/_layout.tsx +++ b/app/(application)/(user)/job/(tabs)/_layout.tsx @@ -5,15 +5,17 @@ import { IconHome, IconStatus } from "@/components/_Icon"; import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification"; import { TabsStyles } from "@/styles/tabs-styles"; import { Ionicons } from "@expo/vector-icons"; -import { - router, - Tabs, - useLocalSearchParams -} from "expo-router"; +import { router, Tabs, useLocalSearchParams } from "expo-router"; import { View } from "react-native"; import { MainColor } from "@/constants/color-palet"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Platform } from "react-native"; +import { + OS_ANDROID_HEIGHT, + OS_ANDROID_PADDING_TOP, + OS_IOS_HEIGHT, + OS_IOS_PADDING_TOP, +} from "@/constants/constans-value"; function JobTabsWrapper() { const insets = useSafeAreaInsets(); @@ -31,20 +33,23 @@ function JobTabsWrapper() { tabBarStyle: Platform.select({ ios: { borderTopWidth: 0, - paddingTop: 12, - height: 80, + paddingTop: OS_IOS_PADDING_TOP, + height: OS_IOS_HEIGHT, }, android: { borderTopWidth: 0, - paddingTop: 5, - height: 70 + paddingBottom, + paddingTop: OS_ANDROID_PADDING_TOP, + height: OS_ANDROID_HEIGHT + paddingBottom, }, }), header: () => ( + } /> ), diff --git a/app/(application)/(user)/job/[id]/[status]/detail.tsx b/app/(application)/(user)/job/[id]/[status]/detail.tsx index 12bd4c4..456a886 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, + OS_Wrapper, Spacing, StackCustom, } from "@/components"; @@ -23,6 +23,7 @@ import { useLocalSearchParams, } from "expo-router"; import { useCallback, useState } from "react"; +import { PADDING_INLINE } from "@/constants/constans-value"; export default function JobDetailStatus() { const { id, status } = useLocalSearchParams(); @@ -72,7 +73,7 @@ export default function JobDetailStatus() { ), }} /> - + {isLoadData ? ( ) : ( @@ -96,7 +97,7 @@ export default function JobDetailStatus() { )} - + { @@ -119,40 +123,41 @@ export function AndroidWrapper(props: AndroidWrapperProps) { 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" - /> + + + {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) + + finalContentPaddingBottom, + padding: finalContentPadding, + }} + keyboardShouldPersistTaps="handled" + /> {/* Footer - Fixed di bawah dengan width 100% */} {footerComponent && !hideFooter && ( {footerComponent} @@ -170,6 +175,7 @@ export function AndroidWrapper(props: AndroidWrapperProps) { {floatingButton} )} + ); } @@ -178,7 +184,7 @@ export function AndroidWrapper(props: AndroidWrapperProps) { return ( {headerComponent && ( @@ -187,13 +193,15 @@ export function AndroidWrapper(props: AndroidWrapperProps) { { return ( {headerComponent && ( @@ -128,7 +127,7 @@ const iOSWrapper = (props: iOSWrapperProps) => { {footerComponent && !hideFooter && ( {footerComponent} @@ -155,7 +154,7 @@ const iOSWrapper = (props: iOSWrapperProps) => { return ( {headerComponent && ( @@ -181,7 +180,7 @@ const iOSWrapper = (props: iOSWrapperProps) => { {footerComponent && !hideFooter && ( {footerComponent} diff --git a/components/_ShareComponent/OS_Wrapper.tsx b/components/_ShareComponent/OS_Wrapper.tsx index 024c00b..87568a6 100644 --- a/components/_ShareComponent/OS_Wrapper.tsx +++ b/components/_ShareComponent/OS_Wrapper.tsx @@ -43,10 +43,10 @@ interface ListModeProps extends BaseProps { keyExtractor?: FlatListProps["keyExtractor"]; } -// ========== PageWrapper Props (Android-specific keyboard handling) ========== -interface PageWrapperBaseProps extends BaseProps { +// ========== Keyboard Handling Props (Android only) ========== +interface KeyboardHandlingProps { /** - * Enable keyboard handling (Android only - NewWrapper_V2) + * Enable keyboard handling with auto-scroll (Android only) * iOS ignores this prop * @default false */ @@ -74,81 +74,74 @@ interface PageWrapperBaseProps extends BaseProps { contentPadding?: number; } -interface PageWrapperStaticProps extends PageWrapperBaseProps { - children: React.ReactNode; - listData?: never; - renderItem?: never; -} +// ========== Final Props Types ========== +type OS_WrapperStaticProps = StaticModeProps & KeyboardHandlingProps; +type OS_WrapperListProps = ListModeProps & KeyboardHandlingProps; -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; +type OS_WrapperProps = OS_WrapperStaticProps | OS_WrapperListProps; /** * OS_Wrapper - Automatically selects iOSWrapper or AndroidWrapper based on platform * - * @example Static Mode + * Features: + * - Auto platform detection + * - Optional keyboard handling for Android forms + * - Unified API for all use cases + * + * @example Static Mode (Simple Content) * ```tsx * * * * ``` - * - * @example List Mode + * + * @example List Mode (with pagination) * ```tsx * } * ListEmptyComponent={} + * onEndReached={loadMore} * /> * ``` + * + * @example Form Mode (with keyboard handling - Android only) + * ```tsx + * } + * > + * + * + * ``` */ export function OS_Wrapper(props: OS_WrapperProps) { + const { + enableKeyboardHandling = false, + keyboardScrollOffset = 100, + contentPaddingBottom = 250, + contentPadding = 0, + ...wrapperProps + } = props; + // iOS uses IOSWrapper (based on NewWrapper) if (Platform.OS === "ios") { - return ; + // Keyboard handling props are ignored on 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 ; + // Android uses AndroidWrapper (with keyboard handling support) + return ( + + ); } // Re-export individual wrappers for direct usage if needed diff --git a/components/index.ts b/components/index.ts index 62e8df7..c39182a 100644 --- a/components/index.ts +++ b/components/index.ts @@ -66,7 +66,7 @@ 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"; +import OS_Wrapper, { IOSWrapper, AndroidWrapper } from "./_ShareComponent/OS_Wrapper"; // Progress import ProgressCustom from "./Progress/ProgressCustom"; @@ -136,7 +136,6 @@ export { NewWrapper_V2, // OS-Specific Wrappers OS_Wrapper, - PageWrapper, IOSWrapper, AndroidWrapper, // Stack diff --git a/constants/constans-value.ts b/constants/constans-value.ts index 87a3a24..d803496 100644 --- a/constants/constans-value.ts +++ b/constants/constans-value.ts @@ -4,6 +4,10 @@ export { OS_ANDROID_HEIGHT, OS_IOS_HEIGHT, OS_HEIGHT, + OS_ANDROID_PADDING_TOP, + OS_IOS_PADDING_TOP, + OS_PADDING_TOP, + PADDING_INLINE, TEXT_SIZE_SMALL, TEXT_SIZE_MEDIUM, TEXT_SIZE_LARGE, @@ -23,10 +27,15 @@ export { }; // OS Height -const OS_ANDROID_HEIGHT = 65 +const OS_ANDROID_HEIGHT = 60 const OS_IOS_HEIGHT = 80 const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT +// OS Padding Top +const OS_ANDROID_PADDING_TOP = 12 +const OS_IOS_PADDING_TOP = 12 +const OS_PADDING_TOP = Platform.OS === "ios" ? OS_IOS_PADDING_TOP : OS_ANDROID_PADDING_TOP + // Text Size const TEXT_SIZE_SMALL = 12; const TEXT_SIZE_MEDIUM = 14; @@ -47,6 +56,7 @@ const DRAWER_HEIGHT = 500; // tinggi drawer5 const RADIUS_BUTTON = 50 // Padding +const PADDING_INLINE = 16 const PADDING_EXTRA_SMALL = 10 const PADDING_SMALL = 12 const PADDING_MEDIUM = 16 diff --git a/docs/OS-Wrapper-Quick-Reference.md b/docs/OS-Wrapper-Quick-Reference.md index 782e2ec..d08d5b2 100644 --- a/docs/OS-Wrapper-Quick-Reference.md +++ b/docs/OS-Wrapper-Quick-Reference.md @@ -2,7 +2,7 @@ ## 📦 Import ```tsx -import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/components"; +import { OS_Wrapper, IOSWrapper, AndroidWrapper } from "@/components"; ``` ## 🎯 Usage Examples @@ -35,9 +35,9 @@ import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/component ``` -### 3. PageWrapper - Form dengan Keyboard Handling +### 3. OS_Wrapper - Form dengan Keyboard Handling ```tsx - - + ``` ### 4. Platform-Specific (Rare Cases) @@ -107,21 +107,21 @@ import { OS_Wrapper, PageWrapper, IOSWrapper, AndroidWrapper } from "@/component ```diff - import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2"; -+ import { PageWrapper } from "@/components"; ++ import { OS_Wrapper } from "@/components"; - -+ ++ ``` ## 💡 Tips -1. **Pakai OS_Wrapper** untuk screen biasa (list, detail, dll) -2. **Pakai PageWrapper** untuk screen dengan form input (create, edit) +1. **Pakai OS_Wrapper** untuk semua screen (list, detail, form) +2. **Tambahkan `enableKeyboardHandling`** untuk form dengan input fields 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 +5. **Keyboard handling** hanya bekerja di Android (iOS mengabaikan props ini) ## ⚠️ Common Mistakes @@ -134,6 +134,9 @@ import OS_Wrapper from "@/components/_ShareComponent/OS_Wrapper"; {content} + +// Jangan pakai PageWrapper (sudah tidak ada) +import { PageWrapper } from "@/components"; ``` ### ✅ Correct @@ -141,8 +144,13 @@ import OS_Wrapper from "@/components/_ShareComponent/OS_Wrapper"; // Import dari @/components import { OS_Wrapper } from "@/components"; -// Pakai salah satu saja +// Simple content {content} + +// Form with keyboard handling + + + ``` --- diff --git a/hooks/useKeyboardForm.ts b/hooks/useKeyboardForm.ts index 638ab95..115f988 100644 --- a/hooks/useKeyboardForm.ts +++ b/hooks/useKeyboardForm.ts @@ -1,25 +1,35 @@ // useKeyboardForm.ts - Hook untuk keyboard handling pada form -import { Keyboard, ScrollView } from "react-native"; +import { Keyboard, ScrollView, Dimensions } from "react-native"; import { useState, useEffect, useRef } from "react"; +import React from "react"; export function useKeyboardForm(scrollOffset = 100) { const scrollViewRef = useRef(null); const [keyboardHeight, setKeyboardHeight] = useState(0); - const [focusedInputY, setFocusedInputY] = useState(null); + const currentScrollY = useRef(0); // Listen to keyboard events useEffect(() => { const keyboardDidShowListener = Keyboard.addListener( 'keyboardDidShow', (e) => { - setKeyboardHeight(e.endCoordinates.height); + const kbHeight = e.endCoordinates.height; + setKeyboardHeight(kbHeight); + + // Simple: scroll by keyboard height saat keyboard muncul + if (scrollViewRef.current) { + const targetY = currentScrollY.current + kbHeight - scrollOffset; + scrollViewRef.current.scrollTo({ + y: Math.max(0, targetY), + animated: true, + }); + } } ); const keyboardDidHideListener = Keyboard.addListener( 'keyboardDidHide', () => { setKeyboardHeight(0); - setFocusedInputY(null); } ); @@ -27,41 +37,35 @@ export function useKeyboardForm(scrollOffset = 100) { keyboardDidShowListener.remove(); keyboardDidHideListener.remove(); }; - }, []); + }, [scrollOffset]); - // Scroll ke focused input - useEffect(() => { - if (focusedInputY !== null && keyboardHeight > 0 && scrollViewRef.current) { - setTimeout(() => { - scrollViewRef.current?.scrollTo({ - y: Math.max(0, focusedInputY - scrollOffset), - animated: true, - }); - }, 100); - } - }, [focusedInputY, keyboardHeight, scrollOffset]); - - // Handler untuk track focused input position - const handleInputFocus = (yPosition: number) => { - setFocusedInputY(yPosition); + // Track scroll position + const handleScroll = (event: any) => { + currentScrollY.current = event.nativeEvent.contentOffset.y; }; - // Helper untuk create onFocus handler + // Dummy handlers (for API compatibility) const createFocusHandler = () => { - return (e: any) => { - e.target?.measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => { - if (pageY !== null) { - handleInputFocus(pageY); - } - }); - }; + return () => {}; }; + const registerInputFocus = () => {}; + return { scrollViewRef, keyboardHeight, - focusedInputY, - handleInputFocus, + registerInputFocus, createFocusHandler, + handleScroll, }; } + +/** + * Dummy helper (no-op, for API compatibility) + */ +export function cloneChildrenWithFocusHandler( + children: React.ReactNode, + _focusHandler: (inputRef: any) => void +): React.ReactNode { + return children; +} diff --git a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj index a7da5f7..1f87e74 100644 --- a/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj +++ b/ios/HIPMIBadungConnect.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ 3F53CC1C3B278545F11A1CAE /* [CP-User] [RNFB] Core Configuration */, 46ED08049A384B869D77364E /* Remove signature files (Xcode workaround) */, 92A25C61F4E34FB6A36E415B /* Remove signature files (Xcode workaround) */, + B122FE573BBA4E8C86B8F1C3 /* Remove signature files (Xcode workaround) */, ); buildRules = ( ); @@ -465,6 +466,23 @@ rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\"; "; }; + B122FE573BBA4E8C86B8F1C3 /* 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/MainViewStatus.tsx b/screens/Job/MainViewStatus.tsx index fbc51bf..fd8df11 100644 --- a/screens/Job/MainViewStatus.tsx +++ b/screens/Job/MainViewStatus.tsx @@ -12,6 +12,7 @@ import { apiJobGetByStatus } from "@/service/api-client/api-job"; import { useFocusEffect, useLocalSearchParams } from "expo-router"; import _ from "lodash"; import { useCallback, useState } from "react"; +import { PADDING_INLINE } from "@/constants/constans-value"; export default function Job_MainViewStatus() { const { user } = useAuth(); @@ -64,7 +65,7 @@ export default function Job_MainViewStatus() { return ( <> - + {isLoadList ? ( ) : _.isEmpty(listData) ? ( diff --git a/screens/Job/MainViewStatus2.tsx b/screens/Job/MainViewStatus2.tsx index b315808..3a84a66 100644 --- a/screens/Job/MainViewStatus2.tsx +++ b/screens/Job/MainViewStatus2.tsx @@ -1,7 +1,7 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { BaseBox, OS_Wrapper, ScrollableCustom, TextCustom } from "@/components"; import { MainColor } from "@/constants/color-palet"; -import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; +import { PAGINATION_DEFAULT_TAKE, PADDING_INLINE } from "@/constants/constans-value"; import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { useAuth } from "@/hooks/use-auth"; import { usePagination } from "@/hooks/use-pagination"; @@ -102,6 +102,7 @@ export default function Job_MainViewStatus2() { ListEmptyComponent={ListEmptyComponent} ListFooterComponent={ListFooterComponent} hideFooter + contentPadding={PADDING_INLINE} /> ); } diff --git a/screens/Job/ScreenArchive.tsx b/screens/Job/ScreenArchive.tsx index 9ed8051..b2a8daa 100644 --- a/screens/Job/ScreenArchive.tsx +++ b/screens/Job/ScreenArchive.tsx @@ -5,6 +5,7 @@ import { apiJobGetAll } from "@/service/api-client/api-job"; import { useFocusEffect } from "expo-router"; import _ from "lodash"; import { useCallback, useState } from "react"; +import { PADDING_INLINE } from "@/constants/constans-value"; export default function Job_ScreenArchive() { const { user } = useAuth(); @@ -33,7 +34,7 @@ export default function Job_ScreenArchive() { }; return ( - + {isLoadData ? ( ) : _.isEmpty(listData) ? ( diff --git a/screens/Job/ScreenArchive2.tsx b/screens/Job/ScreenArchive2.tsx index be3bd26..fd5ac94 100644 --- a/screens/Job/ScreenArchive2.tsx +++ b/screens/Job/ScreenArchive2.tsx @@ -1,5 +1,5 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { BaseBox, OS_Wrapper, TextCustom, ViewWrapper } from "@/components"; +import { BaseBox, OS_Wrapper, TextCustom } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { useAuth } from "@/hooks/use-auth"; @@ -9,7 +9,7 @@ import { useFocusEffect } from "expo-router"; import _ from "lodash"; import { useState } from "react"; import { RefreshControl } from "react-native"; -import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; +import { PAGINATION_DEFAULT_TAKE, PADDING_INLINE } from "@/constants/constans-value"; export default function Job_ScreenArchive2() { const { user } = useAuth(); @@ -70,6 +70,7 @@ export default function Job_ScreenArchive2() { ListEmptyComponent={ListEmptyComponent} ListFooterComponent={ListFooterComponent} hideFooter + contentPadding={PADDING_INLINE} /> ); } diff --git a/screens/Job/ScreenBeranda.tsx b/screens/Job/ScreenBeranda.tsx index 52ec911..0af030a 100644 --- a/screens/Job/ScreenBeranda.tsx +++ b/screens/Job/ScreenBeranda.tsx @@ -13,6 +13,7 @@ import { apiJobGetAll } from "@/service/api-client/api-job"; import { router, useFocusEffect } from "expo-router"; import _ from "lodash"; import { useCallback, useState } from "react"; +import { PADDING_INLINE } from "@/constants/constans-value"; export default function Job_ScreenBeranda() { const [listData, setListData] = useState([]); @@ -45,6 +46,7 @@ export default function Job_ScreenBeranda() { return ( router.push("/job/create")} /> } diff --git a/screens/Job/ScreenBeranda2.tsx b/screens/Job/ScreenBeranda2.tsx index 5a8dfc8..f687489 100644 --- a/screens/Job/ScreenBeranda2.tsx +++ b/screens/Job/ScreenBeranda2.tsx @@ -7,7 +7,6 @@ import { Spacing, StackCustom, TextCustom, - ViewWrapper, } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { createPaginationComponents } from "@/helpers/paginationHelpers"; @@ -17,7 +16,7 @@ import { router, useFocusEffect } from "expo-router"; import _ from "lodash"; import { useState } from "react"; import { RefreshControl, View } from "react-native"; -import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; +import { PAGINATION_DEFAULT_TAKE, PADDING_INLINE } from "@/constants/constans-value"; const PAGE_SIZE = 10; @@ -76,6 +75,7 @@ export default function Job_ScreenBeranda2() { return ( @@ -173,7 +175,9 @@ export function Job_ScreenCreate() { onChangeText={(value) => setData({ ...data, deskripsi: value })} /> + + - + ); } diff --git a/screens/Job/ScreenJobEdit.tsx b/screens/Job/ScreenJobEdit.tsx index 43135ef..d8ef9d2 100644 --- a/screens/Job/ScreenJobEdit.tsx +++ b/screens/Job/ScreenJobEdit.tsx @@ -8,13 +8,14 @@ import { InformationBox, LandscapeFrameUploaded, LoaderCustom, - PageWrapper, + OS_Wrapper, Spacing, StackCustom, TextAreaCustom, TextInputCustom, } from "@/components"; import DIRECTORY_ID from "@/constants/directory-id"; +import { PADDING_INLINE } from "@/constants/constans-value"; import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job"; import { deleteFileService, uploadFileService } from "@/service/upload-service"; import pickImage from "@/utils/pickImage"; @@ -134,9 +135,10 @@ export function Job_ScreenEdit() { }; return ( - {isLoadData ? ( @@ -203,6 +205,6 @@ export function Job_ScreenEdit() { {buttonSubmit()} )} - + ); } diff --git a/tasks/TASK-005-OS-Wrapper-Implementation.md b/tasks/TASK-005-OS-Wrapper-Implementation.md index 71594ce..113be0e 100644 --- a/tasks/TASK-005-OS-Wrapper-Implementation.md +++ b/tasks/TASK-005-OS-Wrapper-Implementation.md @@ -5,25 +5,38 @@ Migrasi dari `NewWrapper` dan `NewWrapper_V2` ke `OS_Wrapper` yang otomatis meny ## 🎯 Goals - ✅ Mengganti penggunaan `NewWrapper` → `OS_Wrapper` di user screens -- ✅ Mengganti penggunaan `NewWrapper_V2` → `OS_Wrapper` atau `PageWrapper` di form screens +- ✅ Mengganti penggunaan `NewWrapper_V2` → `OS_Wrapper` di form screens (dengan keyboard handling props) - ✅ Memastikan tabs dan UI konsisten di iOS dan Android - ✅ Backward compatible (wrapper lama tetap ada) +- ✅ **SIMPLIFIED**: Satu komponen `OS_Wrapper` untuk semua use cases (tidak ada `PageWrapper` terpisah) ## 📦 Available Wrappers -### 1. **OS_Wrapper** (Recommended) +### 1. **OS_Wrapper** (Recommended - Unified API) Auto-detect platform dan routing ke wrapper yang sesuai: - iOS → `IOSWrapper` (berbasis NewWrapper) -- Android → `AndroidWrapper` (berbasis NewWrapper_V2) +- Android → `AndroidWrapper` (berbasis NewWrapper_V2 dengan keyboard handling) -### 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) +**Props:** +```tsx +// Base props (kedua platform) +withBackground?: boolean; +headerComponent?: React.ReactNode; +footerComponent?: React.ReactNode; +floatingButton?: React.ReactNode; +hideFooter?: boolean; +edgesFooter?: Edge[]; +style?: ViewStyle; +refreshControl?: RefreshControl; -### 3. **IOSWrapper** / **AndroidWrapper** (Direct Usage) +// Keyboard handling (Android only - iOS mengabaikan) +enableKeyboardHandling?: boolean; // Default: false +keyboardScrollOffset?: number; // Default: 100 +contentPaddingBottom?: number; // Default: 80 +contentPadding?: number; // Default: 16 +``` + +### 2. **IOSWrapper** / **AndroidWrapper** (Direct Usage) Untuk kasus khusus yang butuh platform-specific behavior. ## 📝 Migration Guide @@ -31,85 +44,103 @@ Untuk kasus khusus yang butuh platform-specific behavior. ### Before (Old Way) ```tsx import NewWrapper from "@/components/_ShareComponent/NewWrapper"; - // atau import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2"; ``` -### After (New Way) +### After (New Way - Unified API) ```tsx -import { OS_Wrapper, PageWrapper } from "@/components"; +import { OS_Wrapper } from "@/components"; -// Static mode +// Static mode (simple content) -// List mode +// List mode (with pagination) } ListEmptyComponent={} + onEndReached={loadMore} /> -// Form dengan keyboard handling -} > - + ``` -## 🚀 Implementation Phases +## 🚀 Implementation Status -### Phase 1: User Screens (Priority: HIGH) -Files yang perlu di-migrate: +### ✅ Phase 1: Job Screens - COMPLETED (2026-04-06) -#### 1.1 Home/Beranda -- [ ] `screens/Beranda/ScreenBeranda.tsx` atau `ScreenBeranda2.tsx` - - Ganti `NewWrapper` → `OS_Wrapper` - - Test tabs behavior di iOS dan Android +**Files migrated: 8** -#### 1.2 Profile Screens +#### Job List Screens (OS_Wrapper): +- ✅ `screens/Job/ScreenBeranda.tsx` - ViewWrapper → OS_Wrapper +- ✅ `screens/Job/ScreenBeranda2.tsx` - NewWrapper_V2 → OS_Wrapper +- ✅ `screens/Job/ScreenArchive.tsx` - ViewWrapper → OS_Wrapper +- ✅ `screens/Job/ScreenArchive2.tsx` - NewWrapper_V2 → OS_Wrapper +- ✅ `screens/Job/MainViewStatus.tsx` - ViewWrapper → OS_Wrapper +- ✅ `screens/Job/MainViewStatus2.tsx` - NewWrapper_V2 → OS_Wrapper + +#### Job Form Screens (OS_Wrapper with keyboard handling): +- ✅ `screens/Job/ScreenJobCreate.tsx` - NewWrapper_V2 → OS_Wrapper + enableKeyboardHandling +- ✅ `screens/Job/ScreenJobEdit.tsx` - NewWrapper_V2 → OS_Wrapper + enableKeyboardHandling + +**Testing Status:** +- ✅ TypeScript: No errors +- ✅ Build: Success +- ✅ iOS Testing: Complete ✅ +- ✅ Android Testing: Complete ✅ + +**Implementation Notes:** +- Semua form screens menggunakan `enableKeyboardHandling` untuk keyboard auto-scroll di Android +- Semua list screens menggunakan pagination dengan `onEndReached` +- Floating button dan sticky header berfungsi dengan baik +- Footer component tetap di posisi bawah + +### ⏳ Phase 2: Other User Screens (Priority: HIGH) + +#### Profile Screens: - [ ] `screens/Profile/ScreenProfile.tsx` -- [ ] `screens/Profile/ScreenProfileEdit.tsx` -- [ ] `screens/Profile/ScreenProfileCreate.tsx` +- [ ] `screens/Profile/ScreenProfileEdit.tsx` → pakai `enableKeyboardHandling` +- [ ] `screens/Profile/ScreenProfileCreate.tsx` → pakai `enableKeyboardHandling` -#### 1.3 Forum/Discussion +#### Forum/Discussion: - [ ] `screens/Forum/ScreenForum.tsx` - [ ] `screens/Forum/ScreenForumDetail.tsx` -- [ ] `screens/Forum/ScreenForumCreate.tsx` → pakai `PageWrapper` (ada form) +- [ ] `screens/Forum/ScreenForumCreate.tsx` → pakai `enableKeyboardHandling` -#### 1.4 Portfolio +#### Portfolio: - [ ] `screens/Portfolio/ScreenPortfolio.tsx` -- [ ] `screens/Portfolio/ScreenPortfolioCreate.tsx` → pakai `PageWrapper` -- [ ] `screens/Portfolio/ScreenPortfolioEdit.tsx` → pakai `PageWrapper` +- [ ] `screens/Portfolio/ScreenPortfolioCreate.tsx` → pakai `enableKeyboardHandling` +- [ ] `screens/Portfolio/ScreenPortfolioEdit.tsx` → pakai `enableKeyboardHandling` -### Phase 2: Admin Screens (Priority: MEDIUM) -Files yang perlu di-migrate: +### ⏳ Phase 3: Admin Screens (Priority: MEDIUM) -#### 2.1 Event Management +#### Event Management: - [ ] `screens/Admin/Event/ScreenEventList.tsx` -- [ ] `screens/Admin/Event/ScreenEventCreate.tsx` → pakai `PageWrapper` -- [ ] `screens/Admin/Event/ScreenEventEdit.tsx` → pakai `PageWrapper` +- [ ] `screens/Admin/Event/ScreenEventCreate.tsx` → pakai `enableKeyboardHandling` +- [ ] `screens/Admin/Event/ScreenEventEdit.tsx` → pakai `enableKeyboardHandling` -#### 2.2 Voting Management +#### Voting Management: - [ ] `screens/Admin/Voting/ScreenVotingList.tsx` -- [ ] `screens/Admin/Voting/ScreenVotingCreate.tsx` → pakai `PageWrapper` -- [ ] `screens/Admin/Voting/ScreenVotingEdit.tsx` → pakai `PageWrapper` +- [ ] `screens/Admin/Voting/ScreenVotingCreate.tsx` → pakai `enableKeyboardHandling` +- [ ] `screens/Admin/Voting/ScreenVotingEdit.tsx` → pakai `enableKeyboardHandling` -#### 2.3 Donation Management +#### Donation Management: - [ ] `screens/Admin/Donation/ScreenDonationList.tsx` -- [ ] `screens/Admin/Donation/ScreenDonationCreate.tsx` → pakai `PageWrapper` -- [ ] `screens/Admin/Donation/ScreenDonationEdit.tsx` → pakai `PageWrapper` +- [ ] `screens/Admin/Donation/ScreenDonationCreate.tsx` → pakai `enableKeyboardHandling` +- [ ] `screens/Admin/Donation/ScreenDonationEdit.tsx` → pakai `enableKeyboardHandling` -#### 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) +### ⏳ Phase 4: Other Screens (Priority: LOW) - [ ] `screens/Investasi/` - Investment screens - [ ] `screens/Kolaborasi/` - Collaboration screens - [ ] Other user-facing screens @@ -122,7 +153,7 @@ Setiap screen yang sudah di-migrate, test: - [ ] UI tampil sesuai design - [ ] Tabs berfungsi dengan baik - [ ] ScrollView/FlatList scroll dengan smooth -- [ ] Keyboard tidak menutupi input (jika pakai PageWrapper) +- [ ] Keyboard tidak menutupi input (jika pakai `enableKeyboardHandling`) - [ ] Footer muncul di posisi yang benar - [ ] Pull to refresh berfungsi (jika ada) @@ -130,7 +161,7 @@ Setiap screen yang sudah di-migrate, test: - [ ] UI tampil sesuai design - [ ] Tabs berfungsi dengan baik - [ ] ScrollView/FlatList scroll dengan smooth -- [ ] Keyboard handling: auto scroll saat input focus (jika pakai PageWrapper) +- [ ] Keyboard handling: auto scroll saat input focus (jika pakai `enableKeyboardHandling`) - [ ] Footer muncul di posisi yang benar (tidak tertutup navigation bar) - [ ] Pull to refresh berfungsi (jika ada) @@ -144,11 +175,7 @@ Setiap screen yang sudah di-migrate, test: ## 📌 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: +### Usage Pattern: #### Untuk List Screen: ```tsx @@ -177,9 +204,9 @@ Setiap screen yang sudah di-migrate, test: ``` -#### Untuk Form Screen: +#### Untuk Form Screen (dengan keyboard handling): ```tsx - - + ``` ### Migration Pattern: @@ -217,28 +244,51 @@ import { OS_Wrapper } from "@/components"; /> ``` +```tsx +// OLD (Form with keyboard handling) +import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2"; + + + + + +// NEW (Unified API) +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. +**Solution**: Pastikan pakai `OS_Wrapper` 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. +**Solution**: Ini sudah di-fix di AndroidWrapper. Pastikan screen pakai OS_Wrapper, 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%** | +| Phase 1 (Job) | 8 | 8 | ✅ Complete | ✅ Complete | +| Phase 2 (User) | TBD | 0 | 0 | ⏳ Pending | +| Phase 3 (Admin) | TBD | 0 | 0 | ⏳ Pending | +| Phase 4 (Other) | TBD | 0 | 0 | ⏳ Pending | +| **Total** | **8+** | **8** | **8** | **100% (Phase 1)** | ## 🔄 Rollback Plan @@ -252,4 +302,6 @@ Jika ada issue yang tidak bisa di-fix dalam 1 jam: **Co-authored-by**: Qwen-Coder **Created**: 2026-04-06 -**Status**: Ready for Implementation +**Last Updated**: 2026-04-06 +**Status**: Phase 1 (Job Screens) Complete ✅ +**Next**: Phase 2 - Other User Screens