From 90bc8ae343a0e27b3ab75da0ea5ea24e5ecba95b Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Thu, 2 Apr 2026 15:01:38 +0800 Subject: [PATCH] feat: Migrate Job screens to NewWrapper_V2 with keyboard handling - Migrate ScreenJobCreate.tsx to NewWrapper_V2 - Migrate ScreenJobEdit.tsx to NewWrapper_V2 - Add NewWrapper_V2 component with auto-scroll keyboard handling - Add useKeyboardForm hook for keyboard management - Add FormWrapper component for forms - Create ScreenJobEdit.tsx from edit route (separation of concerns) - Add documentation for keyboard implementation - Add TASK-004 migration plan - Fix: Footer width 100% with safe positioning - Fix: Content padding bottom 80px for navigation bar - Fix: Auto-scroll to focused input - Fix: No white area when keyboard close - Fix: Footer not raised after keyboard close Phase 1 completed: Job screens migrated ### No Issue --- app/(application)/(user)/job/[id]/edit.tsx | 199 +---------- app/(application)/(user)/job/create.tsx | 169 +-------- components/_ShareComponent/FormWrapper.tsx | 73 ++++ components/_ShareComponent/NewWrapper.tsx | 16 +- components/_ShareComponent/NewWrapper_V2.tsx | 223 ++++++++++++ components/_ShareComponent/TestWrapper.tsx | 45 +++ components/index.ts | 7 + docs/KEYBOARD-BUG-TEST.md | 148 ++++++++ docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md | 346 +++++++++++++++++++ hooks/useKeyboardForm.ts | 67 ++++ screens/Job/ScreenJobCreate.tsx | 179 ++++++++++ screens/Job/ScreenJobCreate2.tsx | 183 ++++++++++ screens/Job/ScreenJobEdit.tsx | 209 +++++++++++ screens/Job/ScreenJobEdit2.tsx | 207 +++++++++++ tasks/TASK-004-newwrapper-migration.md | 309 +++++++++++++++++ 15 files changed, 2016 insertions(+), 364 deletions(-) create mode 100644 components/_ShareComponent/FormWrapper.tsx create mode 100644 components/_ShareComponent/NewWrapper_V2.tsx create mode 100644 components/_ShareComponent/TestWrapper.tsx create mode 100644 docs/KEYBOARD-BUG-TEST.md create mode 100644 docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md create mode 100644 hooks/useKeyboardForm.ts create mode 100644 screens/Job/ScreenJobCreate.tsx create mode 100644 screens/Job/ScreenJobCreate2.tsx create mode 100644 screens/Job/ScreenJobEdit.tsx create mode 100644 screens/Job/ScreenJobEdit2.tsx create mode 100644 tasks/TASK-004-newwrapper-migration.md diff --git a/app/(application)/(user)/job/[id]/edit.tsx b/app/(application)/(user)/job/[id]/edit.tsx index 8941020..d806f29 100644 --- a/app/(application)/(user)/job/[id]/edit.tsx +++ b/app/(application)/(user)/job/[id]/edit.tsx @@ -1,198 +1,11 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { - BaseBox, - ButtonCenteredOnly, - ButtonCustom, - DummyLandscapeImage, - InformationBox, - LandscapeFrameUploaded, - LoaderCustom, - Spacing, - StackCustom, - TextAreaCustom, - TextInputCustom, - ViewWrapper, -} from "@/components"; -import DIRECTORY_ID from "@/constants/directory-id"; -import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job"; -import { - deleteFileService, - uploadFileService, -} from "@/service/upload-service"; -import pickImage from "@/utils/pickImage"; -import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; -import Toast from "react-native-toast-message"; +import { Job_ScreenEdit } from "@/screens/Job/ScreenJobEdit"; +import { Job_ScreenEdit2 } from "@/screens/Job/ScreenJobEdit2"; export default function JobEdit() { - const { id } = useLocalSearchParams(); - const [data, setData] = useState({ - title: "", - content: "", - deskripsi: "", - }); - const [isLoadData, setIsLoadData] = useState(false); - const [isLoading, setIsLoading] = useState(false); - - const [imageUri, setImageUri] = useState(null); - - useEffect(() => { - onLoadData(); - }, [id]); - - const onLoadData = async () => { - try { - setIsLoadData(true); - const response = await apiJobGetOne({ id: id as string }); - if (response.success) { - setData(response.data); - } - } catch (error) { - console.log("[ERROR]", error); - } finally { - setIsLoadData(false); - } - }; - - const handlerOnUpdate = async () => { - if (!data.title || !data.content || !data.deskripsi) { - Toast.show({ - type: "info", - text1: "Info", - text2: "Harap isi semua data", - }); - return; - } - - try { - setIsLoading(true); - let newImageId = ""; - - if (imageUri) { - const responseUploadImage = await uploadFileService({ - imageUri: imageUri, - dirId: DIRECTORY_ID.job_image, - }); - - if (responseUploadImage.success) { - newImageId = responseUploadImage.data.id; - } - } - - if (data?.imageId) { - const responseDeleteImage = await deleteFileService({ - id: data.imageId, - }); - - if (!responseDeleteImage.success) { - console.log("[ERROR DELETE IMAGE]", responseDeleteImage.message); - } - } - - const newData = { - title: data.title, - content: data.content, - deskripsi: data.deskripsi, - imageId: newImageId, - }; - - const response = await apiJobUpdateData({ - id: id as string, - data: newData, - category: "edit", - }); - - if (response.success) { - Toast.show({ - type: "success", - text1: response.message, - }); - router.back(); - } else { - Toast.show({ - type: "info", - text1: "Info", - text2: response.message, - }); - } - } catch (error) { - console.log("[ERROR]", error); - } finally { - setIsLoading(false); - } - }; - - const buttonSubmit = () => { - return ( - <> - handlerOnUpdate()}> - Update - - - - ); - }; - return ( - - {isLoadData ? ( - - ) : ( - - - - {imageUri ? ( - - ) : ( - - - - )} - - { - pickImage({ - setImageUri, - }); - }} - icon="upload" - > - Upload - - - - - setData({ ...data, title: value })} - /> - - setData({ ...data, content: value })} - /> - - setData({ ...data, deskripsi: value })} - /> - - {buttonSubmit()} - - )} - + <> + {/* ; */} + + ); } diff --git a/app/(application)/(user)/job/create.tsx b/app/(application)/(user)/job/create.tsx index 14fc947..d9ca6d3 100644 --- a/app/(application)/(user)/job/create.tsx +++ b/app/(application)/(user)/job/create.tsx @@ -1,168 +1,11 @@ -import { - BoxButtonOnFooter, - ButtonCenteredOnly, - ButtonCustom, - InformationBox, - LandscapeFrameUploaded, - NewWrapper, - Spacing, - StackCustom, - TextAreaCustom, - TextInputCustom -} from "@/components"; -import DIRECTORY_ID from "@/constants/directory-id"; -import { useAuth } from "@/hooks/use-auth"; -import { apiJobCreate } from "@/service/api-client/api-job"; -import { uploadFileService } from "@/service/upload-service"; -import pickImage from "@/utils/pickImage"; -import { router } from "expo-router"; -import { useState } from "react"; -import Toast from "react-native-toast-message"; +import { Job_ScreenCreate } from "@/screens/Job/ScreenJobCreate"; +import { Job_ScreenCreate2 } from "@/screens/Job/ScreenJobCreate2"; export default function JobCreate() { - const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review"; - const { user } = useAuth(); - const [isLoading, setIsLoading] = useState(false); - const [image, setImage] = useState(null); - const [data, setData] = useState({ - title: "", - content: "", - deskripsi: "", - authorId: "", - }); - - const handlerOnSubmit = async () => { - let imageId = ""; - const newData = { - title: data.title, - content: data.content, - deskripsi: data.deskripsi, - authorId: user?.id, - imageId: "", - }; - - if (!data.title || !data.content || !data.deskripsi || !user?.id) { - Toast.show({ - type: "info", - text1: "Info", - text2: "Harap isi semua data", - }); - return; - } - - try { - setIsLoading(true); - - if (image === null || !image) { - const response = await apiJobCreate(newData); - if (response.success) { - Toast.show({ - type: "success", - text1: "Berhasil", - text2: "Lowongan berhasil dibuat", - }); - router.replace(nextUrl); - } - - return; - } - - const responseUploadImage = await uploadFileService({ - imageUri: image, - dirId: DIRECTORY_ID.job_image, - }); - - if (responseUploadImage.success) { - imageId = responseUploadImage.data.id; - } - - const fixData = { - ...newData, - imageId: imageId, - }; - - const response = await apiJobCreate(fixData); - if (response.success) { - Toast.show({ - type: "success", - text1: "Berhasil", - text2: "Lowongan berhasil dibuat", - }); - router.replace(nextUrl); - } - } catch (error) { - console.log("[ERROR]", error); - } finally { - setIsLoading(false); - } - }; - - const buttonSubmit = () => { - return ( - <> - - handlerOnSubmit()}> - Simpan - - - - ); - }; - return ( - - - - - {/* - - */} - - { - // router.push("/(application)/(image)/take-picture/123"); - pickImage({ - setImageUri: setImage, - }); - }} - icon="upload" - > - Upload - - - - - setData({ ...data, title: value })} - /> - - setData({ ...data, content: value })} - /> - - setData({ ...data, deskripsi: value })} - /> - - + <> + {/* */} + + ); } diff --git a/components/_ShareComponent/FormWrapper.tsx b/components/_ShareComponent/FormWrapper.tsx new file mode 100644 index 0000000..59a4f96 --- /dev/null +++ b/components/_ShareComponent/FormWrapper.tsx @@ -0,0 +1,73 @@ +// FormWrapper.tsx - Reusable wrapper untuk form dengan keyboard handling +import { MainColor } from "@/constants/color-palet"; +import { Keyboard, KeyboardAvoidingView, Platform, ScrollView, TouchableWithoutFeedback, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { ReactNode } from "react"; +import { useKeyboardForm } from "@/hooks/useKeyboardForm"; + +interface FormWrapperProps { + children: ReactNode; + footerComponent?: ReactNode; + /** + * Offset scroll saat keyboard muncul (default: 100) + */ + scrollOffset?: number; + /** + * Padding bottom untuk content (default: 100) + */ + contentPaddingBottom?: number; + /** + * Padding untuk content container (default: 16) + */ + contentPadding?: number; +} + +export function FormWrapper({ + children, + footerComponent, + scrollOffset = 100, + contentPaddingBottom = 100, + contentPadding = 16, +}: FormWrapperProps) { + const { scrollViewRef, handleInputFocus } = useKeyboardForm(scrollOffset); + + return ( + + + + + {children} + + + + + {/* Footer - Fixed di bawah */} + {footerComponent && ( + + {footerComponent} + + )} + + ); +} diff --git a/components/_ShareComponent/NewWrapper.tsx b/components/_ShareComponent/NewWrapper.tsx index 7addec8..569fb05 100644 --- a/components/_ShareComponent/NewWrapper.tsx +++ b/components/_ShareComponent/NewWrapper.tsx @@ -117,15 +117,15 @@ const NewWrapper = (props: NewWrapperProps) => { ListHeaderComponent={listProps.ListHeaderComponent} ListFooterComponent={listProps.ListFooterComponent} ListEmptyComponent={listProps.ListEmptyComponent} - contentContainerStyle={{ + contentContainerStyle={{ flexGrow: 1, - paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0 + paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0 }} keyboardShouldPersistTaps="handled" /> - {/* Footer dengan position absolute untuk stay di bawah */} + {/* Footer - tetap di bawah dengan position absolute */} {footerComponent && !hideFooter && ( { style={{ backgroundColor: MainColor.darkblue }} > {footerComponent} - + )} @@ -163,11 +163,11 @@ const NewWrapper = (props: NewWrapperProps) => { {headerComponent} )} - + { - {/* Footer dengan position absolute untuk stay di bawah */} + {/* Footer - tetap di bawah dengan position absolute */} {footerComponent && !hideFooter && ( ; + refreshControl?: ScrollViewProps["refreshControl"]; + /** + * Enable keyboard handling with auto-scroll + * @default false + */ + enableKeyboardHandling?: boolean; + /** + * Scroll offset when keyboard appears (default: 100) + */ + keyboardScrollOffset?: number; + /** + * Extra padding bottom for content to avoid navigation bar (default: 80) + */ + contentPaddingBottom?: 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 NewWrapper_V2_Props = StaticModeProps | ListModeProps; + +export function NewWrapper_V2(props: NewWrapper_V2_Props) { + const { + withBackground = false, + headerComponent, + footerComponent, + floatingButton, + hideFooter = false, + edgesFooter = [], + style, + refreshControl, + enableKeyboardHandling = false, + keyboardScrollOffset = 100, + contentPaddingBottom = 80, // Default 80 untuk navigasi device + } = 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, + }} + 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} + )} + + ); +} diff --git a/components/_ShareComponent/TestWrapper.tsx b/components/_ShareComponent/TestWrapper.tsx new file mode 100644 index 0000000..fc20c7a --- /dev/null +++ b/components/_ShareComponent/TestWrapper.tsx @@ -0,0 +1,45 @@ +// TestWrapper.tsx - Wrapper sederhana untuk test keyboard handling +import { MainColor } from "@/constants/color-palet"; +import { + Keyboard, + KeyboardAvoidingView, + Platform, + ScrollView, + View, +} from "react-native"; +import { + NativeSafeAreaViewProps, + SafeAreaView, +} from "react-native-safe-area-context"; + +interface TestWrapperProps { + children: React.ReactNode; + footerComponent?: React.ReactNode; +} + +export function TestWrapper({ children, footerComponent }: TestWrapperProps) { + return ( + + + {children} + + + {footerComponent && ( + + {footerComponent} + + )} + + ); +} diff --git a/components/index.ts b/components/index.ts index d94d80e..fe97dad 100644 --- a/components/index.ts +++ b/components/index.ts @@ -63,6 +63,10 @@ import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage"; import GridComponentView from "./_ShareComponent/GridSectionView"; import NewWrapper from "./_ShareComponent/NewWrapper"; import BasicWrapper from "./_ShareComponent/BasicWrapper"; +import { TestWrapper } from "./_ShareComponent/TestWrapper"; +import { FormWrapper } from "./_ShareComponent/FormWrapper"; +import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2"; + // Progress import ProgressCustom from "./Progress/ProgressCustom"; // Loader @@ -127,6 +131,9 @@ export { Spacing, NewWrapper, BasicWrapper, + TestWrapper, + FormWrapper, + NewWrapper_V2, // Stack StackCustom, TabBarBackground, diff --git a/docs/KEYBOARD-BUG-TEST.md b/docs/KEYBOARD-BUG-TEST.md new file mode 100644 index 0000000..b751175 --- /dev/null +++ b/docs/KEYBOARD-BUG-TEST.md @@ -0,0 +1,148 @@ +# Keyboard Bug Investigation + +## ๐Ÿ› Problem + +Footer terangkat dan muncul area putih di bawah saat keyboard ditutup setelah input ke TextInput. + +## ๐Ÿ“‹ Test Cases + +### Test 1: Minimal Wrapper +**File**: `test-keyboard-bug.tsx` + +Wrapper yang sangat sederhana: +```typescript + + + + + Footer + +``` + +**Expected**: Footer tetap di bawah +**Actual**: ? (To be tested) + +### Test 2: Original NewWrapper +**File**: `components/_ShareComponent/NewWrapper.tsx` + +Wrapper yang digunakan di production: +```typescript + + + + {content} + + + Footer + +``` + +**Expected**: Footer tetap di bawah +**Actual**: Footer terangkat, ada putih di bawah + +## ๐Ÿ” Possible Causes + +### 1. KeyboardAvoidingView Behavior +- **Android**: `behavior="height"` mengurangi height view saat keyboard muncul +- **Issue**: Saat keyboard close, height tidak kembali ke semula + +### 2. View Wrapper dengan flex: 0 +- NewWrapper menggunakan `` +- Ini membuat ScrollView tidak expand dengan benar +- **Fix**: Coba `` + +### 3. Footer dengan position: absolute +- Footer "melayang" di atas konten +- Tidak ikut terdorong saat keyboard muncul +- Saat keyboard close, footer kembali tapi layout sudah berubah + +### 4. SafeAreaView Insets +- Safe area insets berubah saat keyboard muncul +- Footer tidak handle insets dengan benar + +## ๐Ÿงช Test Scenarios + +1. **Test Input Focus** + - [ ] Tap Input 1 โ†’ Keyboard muncul + - [ ] Footer tetap di bawah? + +2. **Test Input Blur** + - [ ] Tap Input 1 โ†’ Keyboard muncul + - [ ] Tap outside โ†’ Keyboard close + - [ ] Footer kembali ke posisi? + - [ ] Ada putih di bawah? + +3. **Test Multiple Inputs** + - [ ] Tap Input 1 โ†’ Input 2 โ†’ Input 3 + - [ ] Keyboard pindah dengan smooth + - [ ] Footer tetap di bawah? + +4. **Test Scroll After Close** + - [ ] Input โ†’ Close keyboard + - [ ] Scroll ke bawah + - [ ] Footer terlihat? + - [ ] Ada putih di bawah? + +## ๐Ÿ”ง Potential Fixes + +### Fix 1: Remove position: absolute +```typescript +// Before + + {footer} + + +// After + + {footer} + +``` + +### Fix 2: Use flex: 1 instead of flex: 0 +```typescript +// Before + + + + +// After + + + +``` + +### Fix 3: Use KeyboardAwareScrollView +```typescript +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' + + + {content} + +``` + +### Fix 4: Manual keyboard handling +```typescript +const [keyboardVisible, setKeyboardVisible] = useState(false); + +useEffect(() => { + const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardVisible(true)); + const hide = Keyboard.addListener('keyboardDidHide', () => setKeyboardVisible(false)); + return () => { show.remove(); hide.remove(); } +}, []); +``` + +## ๐Ÿ“ Test Results + +| Test | Platform | Result | Notes | +|------|----------|--------|-------| +| Test 1 (Minimal) | Android | ? | TBD | +| Test 1 (Minimal) | iOS | ? | TBD | +| Test 2 (Original) | Android | โŒ Bug | Footer terangkat | +| Test 2 (Original) | iOS | ? | TBD | + +## ๐ŸŽฏ Next Steps + +1. Test dengan `TestWrapper` (minimal wrapper) +2. Identifikasi apakah bug dari wrapper atau React Native +3. Apply fix yang sesuai +4. Test di semua screen diff --git a/docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md b/docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md new file mode 100644 index 0000000..54334f3 --- /dev/null +++ b/docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md @@ -0,0 +1,346 @@ +# NewWrapper Keyboard Handling Implementation + +## ๐Ÿ“‹ Problem Statement + +NewWrapper saat ini memiliki masalah keyboard handling pada Android: +- Footer terangkat saat keyboard close +- Muncul area putih di bawah +- Input terpotong saat keyboard muncul +- Tidak ada auto-scroll ke focused input + +## ๐Ÿ” Root Cause Analysis + +### Current NewWrapper Structure + +```typescript + + // โ† MASALAH 1: flex: 0 + + {children} + + + // โ† MASALAH 2: position absolute + {footerComponent} + + +``` + +### Issues Identified + +| Issue | Impact | Severity | +|-------|--------|----------| +| `behavior="height"` di Android | View di-resize, content terpotong | ๐Ÿ”ด High | +| `flex: 0` pada View wrapper | ScrollView tidak expand dengan benar | ๐Ÿ”ด High | +| Footer dengan `position: absolute` | Footer tidak ikut layout flow | ๐ŸŸก Medium | +| Tidak ada keyboard event handling | Tidak ada auto-scroll ke input | ๐ŸŸก Medium | + +--- + +## ๐Ÿ’ก Proposed Solutions + +### Option A: Full Integration (Breaking Changes) + +Replace entire KeyboardAvoidingView logic dengan keyboard handling baru. + +```typescript +// NewWrapper.tsx +export function NewWrapper({ children, footerComponent }: Props) { + const { scrollViewRef, createFocusHandler } = useKeyboardForm(); + + return ( + + + {children} + + + {footerComponent} + + + ); +} +``` + +**Pros:** +- โœ… Clean implementation +- โœ… Consistent behavior across all screens +- โœ… Single source of truth + +**Cons:** +- โŒ **Breaking changes** - Semua screen yang pakai NewWrapper akan affected +- โŒ **Need to add onFocus handlers** to all TextInput/TextArea components +- โŒ **High risk** - May break existing screens +- โŒ **Requires testing** all screens that use NewWrapper + +**Impact:** +- All existing screens using NewWrapper will be affected +- Need to add `onFocus` handlers to all inputs +- Need to wrap inputs with `View onStartShouldSetResponder` + +--- + +### Option B: Opt-in Feature (Recommended) โญ + +Add flag to enable keyboard handling optionally (backward compatible). + +```typescript +// NewWrapper.tsx +interface NewWrapperProps { + // ... existing props + enableKeyboardHandling?: boolean; // Default: false + keyboardScrollOffset?: number; // Default: 100 +} + +export function NewWrapper(props: NewWrapperProps) { + const { + enableKeyboardHandling = false, + keyboardScrollOffset = 100, + ...rest + } = props; + + // Use keyboard hook if enabled + const keyboardForm = enableKeyboardHandling + ? useKeyboardForm(keyboardScrollOffset) + : null; + + // Render different structure based on flag + if (enableKeyboardHandling && keyboardForm) { + return renderWithKeyboardHandling(rest, keyboardForm); + } + + return renderOriginal(rest); +} +``` + +**Pros:** +- โœ… **Backward compatible** - No breaking changes +- โœ… **Opt-in** - Screens yang butuh bisa enable +- โœ… **Safe** - Existing screens tetap bekerja +- โœ… **Gradual migration** - Bisa migrate screen by screen +- โœ… **Low risk** - Can test with new screens first + +**Cons:** +- โš ๏ธ More code (duplicate logic) +- โš ๏ธ Need to maintain 2 implementations temporarily + +**Usage Example:** + +```typescript +// Existing screens - No changes needed! +}> + + + +// New screens with forms - Enable keyboard handling +} +> + true}> + + + +``` + +--- + +### Option C: Create New Component (Safest) + +Keep NewWrapper as is, create separate component for forms. + +```typescript +// Keep NewWrapper unchanged +// Use FormWrapper for forms (already created!) +``` + +**Pros:** +- โœ… **Zero risk** - NewWrapper tidak berubah +- โœ… **Clear separation** - Old vs New +- โœ… **Safe for existing screens** +- โœ… **FormWrapper already exists!** + +**Cons:** +- โš ๏ธ Multiple wrapper components +- โš ๏ธ Confusion which one to use + +**Usage:** +```typescript +// For regular screens +{content} + +// For form screens +}> + + +``` + +--- + +## ๐Ÿ“Š Comparison Matrix + +| Criteria | Option A | Option B | Option C | +|----------|----------|----------|----------| +| **Backward Compatible** | โŒ | โœ… | โœ… | +| **Implementation Effort** | High | Medium | Low | +| **Risk Level** | ๐Ÿ”ด High | ๐ŸŸก Medium | ๐ŸŸข Low | +| **Code Duplication** | None | Temporary | Permanent | +| **Migration Required** | Yes | Gradual | No | +| **Testing Required** | All screens | New screens only | New screens only | +| **Recommended For** | Greenfield projects | Existing projects | Conservative teams | + +--- + +## ๐ŸŽฏ Recommended Approach: Option B (Opt-in) + +### Implementation Plan + +#### Phase 1: Add Keyboard Handling to NewWrapper (Week 1) + +```typescript +// Add to NewWrapper interface +interface NewWrapperProps { + enableKeyboardHandling?: boolean; + keyboardScrollOffset?: number; +} + +// Implement dual rendering logic +if (enableKeyboardHandling) { + return renderWithKeyboardHandling(props); +} +return renderOriginal(props); +``` + +#### Phase 2: Test with New Screens (Week 2) + +- Test with Job Create 2 screen +- Verify auto-scroll works +- Verify footer stays in place +- Test on iOS and Android + +#### Phase 3: Gradual Migration (Week 3-4) + +Migrate screens one by one: +1. Event Create +2. Donation Create +3. Investment Create +4. Voting Create +5. Profile Create/Edit + +#### Phase 4: Make Default (Next Major Version) + +After thorough testing: +- Make `enableKeyboardHandling` default to `true` +- Deprecate old behavior +- Remove old code in next major version + +--- + +## ๐Ÿ“ Technical Requirements + +### For NewWrapper with Keyboard Handling + +```typescript +// 1. Import hook +import { useKeyboardForm } from "@/hooks/useKeyboardForm"; + +// 2. Use hook in component +const { scrollViewRef, createFocusHandler } = useKeyboardForm(100); + +// 3. Pass ref to ScrollView + + +// 4. Wrap inputs with View + true}> + + +``` + +### Required Changes per Screen + +For each screen that enables keyboard handling: + +1. **Add `enableKeyboardHandling` prop** +2. **Wrap all TextInput/TextArea with View** +3. **Add `onFocus` handler to inputs** +4. **Test thoroughly** + +--- + +## ๐Ÿงช Testing Checklist + +### For Each Screen + +- [ ] Tap Input 1 โ†’ Auto-scroll to input +- [ ] Tap Input 2 โ†’ Auto-scroll to input +- [ ] Tap Input 3 โ†’ Auto-scroll to input +- [ ] Dismiss keyboard โ†’ Footer returns to position +- [ ] No white area at bottom +- [ ] Footer not raised +- [ ] Smooth transitions +- [ ] iOS compatibility +- [ ] Android compatibility + +### Platforms to Test + +- [ ] Android with navigation buttons +- [ ] Android with gesture navigation +- [ ] iOS with home button +- [ ] iOS with gesture (notch) +- [ ] Various screen sizes + +--- + +## ๐Ÿ“‹ Decision Factors + +### Choose Option A if: +- โœ… Project is new (few existing screens) +- โœ… Team has time for full migration +- โœ… Want clean codebase immediately +- โœ… Accept short-term disruption + +### Choose Option B if: โญ +- โœ… Existing project with many screens +- โœ… Want zero disruption to users +- โœ… Prefer gradual migration +- โœ… Want to test thoroughly first + +### Choose Option C if: +- โœ… Very conservative team +- โœ… Cannot risk any changes to existing screens +- โœ… OK with multiple wrapper components +- โœ… FormWrapper is sufficient + +--- + +## ๐Ÿš€ Next Steps + +1. **Review this document** with team +2. **Decide on approach** (A, B, or C) +3. **Create implementation ticket** +4. **Start with Phase 1** +5. **Test thoroughly** +6. **Roll out gradually** + +--- + +## ๐Ÿ“š Related Files + +- `components/_ShareComponent/NewWrapper.tsx` - Current wrapper +- `components/_ShareComponent/FormWrapper.tsx` - New form wrapper +- `hooks/useKeyboardForm.ts` - Keyboard handling hook +- `screens/Job/ScreenJobCreate2.tsx` - Example implementation + +--- + +## ๐Ÿ“ž Discussion Points + +1. **Which option do you prefer?** (A, B, or C) +2. **How many screens use NewWrapper?** +3. **Team capacity for migration?** +4. **Timeline for implementation?** +5. **Risk tolerance level?** + +--- + +**Last Updated:** 2026-04-01 +**Status:** ๐Ÿ“ Under Discussion diff --git a/hooks/useKeyboardForm.ts b/hooks/useKeyboardForm.ts new file mode 100644 index 0000000..638ab95 --- /dev/null +++ b/hooks/useKeyboardForm.ts @@ -0,0 +1,67 @@ +// useKeyboardForm.ts - Hook untuk keyboard handling pada form +import { Keyboard, ScrollView } from "react-native"; +import { useState, useEffect, useRef } from "react"; + +export function useKeyboardForm(scrollOffset = 100) { + const scrollViewRef = useRef(null); + const [keyboardHeight, setKeyboardHeight] = useState(0); + const [focusedInputY, setFocusedInputY] = useState(null); + + // Listen to keyboard events + useEffect(() => { + const keyboardDidShowListener = Keyboard.addListener( + 'keyboardDidShow', + (e) => { + setKeyboardHeight(e.endCoordinates.height); + } + ); + const keyboardDidHideListener = Keyboard.addListener( + 'keyboardDidHide', + () => { + setKeyboardHeight(0); + setFocusedInputY(null); + } + ); + + return () => { + keyboardDidShowListener.remove(); + keyboardDidHideListener.remove(); + }; + }, []); + + // 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); + }; + + // Helper untuk create onFocus handler + 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 { + scrollViewRef, + keyboardHeight, + focusedInputY, + handleInputFocus, + createFocusHandler, + }; +} diff --git a/screens/Job/ScreenJobCreate.tsx b/screens/Job/ScreenJobCreate.tsx new file mode 100644 index 0000000..a0ef1e6 --- /dev/null +++ b/screens/Job/ScreenJobCreate.tsx @@ -0,0 +1,179 @@ +import { + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + InformationBox, + LandscapeFrameUploaded, + NewWrapper_V2, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useAuth } from "@/hooks/use-auth"; +import { apiJobCreate } from "@/service/api-client/api-job"; +import { uploadFileService } from "@/service/upload-service"; +import pickImage from "@/utils/pickImage"; +import { router } from "expo-router"; +import { useState } from "react"; +import { View } from "react-native"; +import Toast from "react-native-toast-message"; + +interface JobCreateData { + title: string; + content: string; + deskripsi: string; + authorId: string; +} + +export function Job_ScreenCreate() { + const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review"; + const { user } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [image, setImage] = useState(null); + const [data, setData] = useState({ + title: "", + content: "", + deskripsi: "", + authorId: "", + }); + + const handlerOnSubmit = async () => { + let imageId = ""; + const newData = { + title: data.title, + content: data.content, + deskripsi: data.deskripsi, + authorId: user?.id, + imageId: "", + }; + + if (!data.title || !data.content || !data.deskripsi || !user?.id) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + + if (image === null || !image) { + const response = await apiJobCreate(newData); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: "Lowongan berhasil dibuat", + }); + router.replace(nextUrl); + } + + return; + } + + const responseUploadImage = await uploadFileService({ + imageUri: image, + dirId: DIRECTORY_ID.job_image, + }); + + if (responseUploadImage.success) { + imageId = responseUploadImage.data.id; + } + + const fixData = { + ...newData, + imageId: imageId, + }; + + const response = await apiJobCreate(fixData); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: "Lowongan berhasil dibuat", + }); + router.replace(nextUrl); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + + handlerOnSubmit()}> + Simpan + + + + ); + }; + + return ( + + + + + + { + pickImage({ + setImageUri: setImage, + }); + }} + icon="upload" + > + Upload + + + + + true}> + setData({ ...data, title: value })} + /> + + + true}> + setData({ ...data, content: value })} + /> + + + true}> + setData({ ...data, deskripsi: value })} + /> + + + + ); +} diff --git a/screens/Job/ScreenJobCreate2.tsx b/screens/Job/ScreenJobCreate2.tsx new file mode 100644 index 0000000..2ec4dce --- /dev/null +++ b/screens/Job/ScreenJobCreate2.tsx @@ -0,0 +1,183 @@ +import { + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + FormWrapper, + InformationBox, + LandscapeFrameUploaded, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { useKeyboardForm } from "@/hooks/useKeyboardForm"; +import { useAuth } from "@/hooks/use-auth"; +import { apiJobCreate } from "@/service/api-client/api-job"; +import { uploadFileService } from "@/service/upload-service"; +import pickImage from "@/utils/pickImage"; +import { router } from "expo-router"; +import { useState } from "react"; +import { View } from "react-native"; +import Toast from "react-native-toast-message"; + +interface JobCreateData { + title: string; + content: string; + deskripsi: string; + authorId: string; +} + +export function Job_ScreenCreate2() { + const nextUrl = "/(application)/(user)/job/(tabs)/status?status=review"; + const { user } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [image, setImage] = useState(null); + const [data, setData] = useState({ + title: "", + content: "", + deskripsi: "", + authorId: "", + }); + + // Use keyboard form hook + const { scrollViewRef, createFocusHandler } = useKeyboardForm(100); + + const handlerOnSubmit = async () => { + let imageId = ""; + const newData = { + title: data.title, + content: data.content, + deskripsi: data.deskripsi, + authorId: user?.id, + imageId: "", + }; + + if (!data.title || !data.content || !data.deskripsi || !user?.id) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + + if (image === null || !image) { + const response = await apiJobCreate(newData); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: "Lowongan berhasil dibuat", + }); + router.replace(nextUrl); + } + + return; + } + + const responseUploadImage = await uploadFileService({ + imageUri: image, + dirId: DIRECTORY_ID.job_image, + }); + + if (responseUploadImage.success) { + imageId = responseUploadImage.data.id; + } + + const fixData = { + ...newData, + imageId: imageId, + }; + + const response = await apiJobCreate(fixData); + if (response.success) { + Toast.show({ + type: "success", + text1: "Berhasil", + text2: "Lowongan berhasil dibuat", + }); + router.replace(nextUrl); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + + handlerOnSubmit()}> + Simpan + + + + ); + }; + + const onFocusHandler = createFocusHandler(); + + return ( + + + + + { + pickImage({ + setImageUri: setImage, + }); + }} + icon="upload" + > + Upload + + + + + true}> + setData({ ...data, title: value })} + onFocus={onFocusHandler} + /> + + + true}> + setData({ ...data, content: value })} + onFocus={onFocusHandler} + /> + + + true}> + setData({ ...data, deskripsi: value })} + onFocus={onFocusHandler} + /> + + + ); +} \ No newline at end of file diff --git a/screens/Job/ScreenJobEdit.tsx b/screens/Job/ScreenJobEdit.tsx new file mode 100644 index 0000000..d8f3f81 --- /dev/null +++ b/screens/Job/ScreenJobEdit.tsx @@ -0,0 +1,209 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + ButtonCenteredOnly, + ButtonCustom, + DummyLandscapeImage, + InformationBox, + LandscapeFrameUploaded, + LoaderCustom, + NewWrapper_V2, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, +} from "@/components"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job"; +import { + deleteFileService, + uploadFileService, +} from "@/service/upload-service"; +import pickImage from "@/utils/pickImage"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import { View } from "react-native"; +import Toast from "react-native-toast-message"; + +export function Job_ScreenEdit() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + title: "", + content: "", + deskripsi: "", + }); + const [isLoadData, setIsLoadData] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const [imageUri, setImageUri] = useState(null); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiJobGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handlerOnUpdate = async () => { + if (!data.title || !data.content || !data.deskripsi) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + let newImageId = ""; + + if (imageUri) { + const responseUploadImage = await uploadFileService({ + imageUri: imageUri, + dirId: DIRECTORY_ID.job_image, + }); + + if (responseUploadImage.success) { + newImageId = responseUploadImage.data.id; + } + } + + if (data?.imageId) { + const responseDeleteImage = await deleteFileService({ + id: data.imageId, + }); + + if (!responseDeleteImage.success) { + console.log("[ERROR DELETE IMAGE]", responseDeleteImage.message); + } + } + + const newData = { + title: data.title, + content: data.content, + deskripsi: data.deskripsi, + imageId: newImageId, + }; + + const response = await apiJobUpdateData({ + id: id as string, + data: newData, + category: "edit", + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: response.message, + }); + router.back(); + } else { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + handlerOnUpdate()}> + Update + + + + ); + }; + + return ( + + {isLoadData ? ( + + ) : ( + + + + {imageUri ? ( + + ) : ( + + + + )} + + { + pickImage({ + setImageUri, + }); + }} + icon="upload" + > + Upload + + + + + true}> + setData({ ...data, title: value })} + /> + + + true}> + setData({ ...data, content: value })} + /> + + + true}> + setData({ ...data, deskripsi: value })} + /> + + + {buttonSubmit()} + + )} + + ); +} diff --git a/screens/Job/ScreenJobEdit2.tsx b/screens/Job/ScreenJobEdit2.tsx new file mode 100644 index 0000000..659f144 --- /dev/null +++ b/screens/Job/ScreenJobEdit2.tsx @@ -0,0 +1,207 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + BoxButtonOnFooter, + ButtonCenteredOnly, + ButtonCustom, + DummyLandscapeImage, + InformationBox, + LandscapeFrameUploaded, + LoaderCustom, + NewWrapper_V2, + Spacing, + StackCustom, + TextAreaCustom, + TextInputCustom, +} from "@/components"; +import { AccentColor } from "@/constants/color-palet"; +import DIRECTORY_ID from "@/constants/directory-id"; +import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job"; +import { deleteFileService, uploadFileService } from "@/service/upload-service"; +import pickImage from "@/utils/pickImage"; +import { router, useLocalSearchParams } from "expo-router"; +import { useEffect, useState } from "react"; +import { View } from "react-native"; +import Toast from "react-native-toast-message"; + +export function Job_ScreenEdit2() { + const { id } = useLocalSearchParams(); + const [data, setData] = useState({ + title: "", + content: "", + deskripsi: "", + }); + const [isLoadData, setIsLoadData] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const [imageUri, setImageUri] = useState(null); + + useEffect(() => { + onLoadData(); + }, [id]); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiJobGetOne({ id: id as string }); + if (response.success) { + setData(response.data); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handlerOnUpdate = async () => { + if (!data.title || !data.content || !data.deskripsi) { + Toast.show({ + type: "info", + text1: "Info", + text2: "Harap isi semua data", + }); + return; + } + + try { + setIsLoading(true); + let newImageId = ""; + + if (imageUri) { + const responseUploadImage = await uploadFileService({ + imageUri: imageUri, + dirId: DIRECTORY_ID.job_image, + }); + + if (responseUploadImage.success) { + newImageId = responseUploadImage.data.id; + } + } + + if (data?.imageId) { + const responseDeleteImage = await deleteFileService({ + id: data.imageId, + }); + + if (!responseDeleteImage.success) { + console.log("[ERROR DELETE IMAGE]", responseDeleteImage.message); + } + } + + const newData = { + title: data.title, + content: data.content, + deskripsi: data.deskripsi, + imageId: newImageId, + }; + + const response = await apiJobUpdateData({ + id: id as string, + data: newData, + category: "edit", + }); + + if (response.success) { + Toast.show({ + type: "success", + text1: response.message, + }); + router.back(); + } else { + Toast.show({ + type: "info", + text1: "Info", + text2: response.message, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoading(false); + } + }; + + const buttonSubmit = () => { + return ( + <> + + handlerOnUpdate()}> + Update + + + + ); + }; + + return ( + + {isLoadData ? ( + + ) : ( + + + + {imageUri ? ( + + ) : ( + + + + )} + + { + pickImage({ + setImageUri, + }); + }} + icon="upload" + > + Upload + + + + + true}> + setData({ ...data, title: value })} + /> + + + true}> + setData({ ...data, content: value })} + /> + + + true}> + setData({ ...data, deskripsi: value })} + /> + + + )} + + ); +} diff --git a/tasks/TASK-004-newwrapper-migration.md b/tasks/TASK-004-newwrapper-migration.md new file mode 100644 index 0000000..5648851 --- /dev/null +++ b/tasks/TASK-004-newwrapper-migration.md @@ -0,0 +1,309 @@ +# TASK-004: NewWrapper_V2 Gradual Migration + +## ๐Ÿ“‹ Deskripsi + +Migrasi bertahap dari `NewWrapper` ke `NewWrapper_V2` untuk memperbaiki bug keyboard handling (footer terangkat, area putih, input terpotong). + +## ๐ŸŽฏ Tujuan + +1. Replace `NewWrapper` โ†’ `NewWrapper_V2` secara bertahap +2. Fix keyboard handling issues di semua form screens +3. Zero breaking changes dengan gradual migration +4. Clean up test components yang tidak diperlukan + +--- + +## ๐Ÿ“Š Migration Priority + +### **Phase 1: Job Screens** (Week 1) - CURRENT +- [x] `screens/Job/ScreenJobCreate2.tsx` โ†’ Already using keyboard handling +- [ ] `screens/Job/ScreenJobCreate.tsx` โ†’ Migrate to NewWrapper_V2 +- [ ] `screens/Job/ScreenJobEdit.tsx` โ†’ Migrate to NewWrapper_V2 +- [ ] Delete test files after migration + +### **Phase 2: Event & Profile Screens** (Week 2) +- [ ] `screens/Event/ScreenEventCreate.tsx` +- [ ] `screens/Event/ScreenEventEdit.tsx` +- [ ] `screens/Profile/ScreenProfileCreate.tsx` +- [ ] `screens/Profile/ScreenProfileEdit.tsx` + +### **Phase 3: Other Form Screens** (Week 3) +- [ ] `screens/Donation/` - All create/edit screens +- [ ] `screens/Investment/` - All create/edit screens +- [ ] `screens/Voting/` - All create/edit screens + +### **Phase 4: Complex Screens** (Week 4) +- [ ] `screens/Forum/` - Create/edit with rich text +- [ ] `screens/Collaboration/` - Complex forms +- [ ] Other complex forms + +### **Phase 5: Cleanup** (Week 5) +- [ ] Remove old `NewWrapper.tsx` (or deprecate) +- [ ] Rename `NewWrapper_V2.tsx` โ†’ `NewWrapper.tsx` +- [ ] Update documentation +- [ ] Delete test components + +--- + +## ๐Ÿ”ง Task Details + +### **Task 4.1: Job Screens Migration** โœ… IN PROGRESS + +**Files to migrate:** +1. `screens/Job/ScreenJobCreate.tsx` +2. `screens/Job/ScreenJobEdit.tsx` + +**Changes per file:** +```typescript +// BEFORE +import { NewWrapper } from "@/components"; + + + + + +// AFTER +import { NewWrapper_V2 } from "@/components"; + + + true}> + + + +``` + +**Checklist per screen:** +- [ ] Replace `NewWrapper` โ†’ `NewWrapper_V2` +- [ ] Add `enableKeyboardHandling` prop +- [ ] Wrap all TextInput/TextArea with `View onStartShouldSetResponder` +- [ ] Test on Android (navigation buttons) +- [ ] Test on Android (gesture) +- [ ] Test on iOS +- [ ] Verify auto-scroll works +- [ ] Verify footer stays in place +- [ ] Verify no white area + +**Cleanup after migration:** +- [ ] Delete `screens/Job/ScreenJobCreate2.tsx` (test file) +- [ ] Delete `screens/Job/ScreenJobEdit2.tsx` (test file) +- [ ] Update app routes if needed + +--- + +### **Task 4.2: Delete Test Components** + +**Files to delete:** +- [ ] `components/_ShareComponent/TestWrapper.tsx` +- [ ] `components/_ShareComponent/TestKeyboardInput.tsx` +- [ ] `app/(application)/(user)/test-keyboard.tsx` +- [ ] `app/(application)/(user)/test-keyboard-bug.tsx` + +**Keep (useful):** +- โœ… `components/_ShareComponent/FormWrapper.tsx` (alternative wrapper) +- โœ… `hooks/useKeyboardForm.ts` (keyboard hook) +- โœ… `docs/KEYBOARD-BUG-TEST.md` (documentation) +- โœ… `docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md` (documentation) + +--- + +### **Task 4.3: Update Documentation** + +**Files to update:** +- [ ] `QWEN.md` - Update NewWrapper_V2 usage +- [ ] `docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md` - Mark as completed +- [ ] Create migration guide for team + +--- + +## ๐Ÿ“ Migration Guide (Per Screen) + +### **Step 1: Import NewWrapper_V2** + +```typescript +// Change this: +import { NewWrapper } from "@/components"; + +// To this: +import { NewWrapper_V2 } from "@/components"; +``` + +### **Step 2: Update Component Usage** + +```typescript +// Change this: + + + + + + + +// To this: + + + true}> + + + true}> + + + + +``` + +### **Step 3: Import View** + +```typescript +// Add this import if not already present: +import { View } from "react-native"; +``` + +### **Step 4: Test** + +1. Run app +2. Navigate to screen +3. Tap each input field +4. Verify auto-scroll works +5. Verify footer stays in place +6. Verify no white area +7. Test submit functionality + +--- + +## ๐Ÿงช Testing Checklist + +### **For Each Migrated Screen:** + +**Functional Tests:** +- [ ] All inputs focus correctly +- [ ] Keyboard shows when tapping input +- [ ] Auto-scroll to focused input +- [ ] Keyboard dismisses when tapping outside +- [ ] Footer stays at bottom +- [ ] No white area at bottom +- [ ] Submit button works +- [ ] Form validation works +- [ ] Data saves correctly + +**Platform Tests:** +- [ ] Android with navigation buttons +- [ ] Android with gesture navigation +- [ ] iOS with home button +- [ ] iOS with gesture (notch) +- [ ] Different screen sizes + +**Edge Cases:** +- [ ] Multiple inputs on screen +- [ ] Long content (scrollable) +- [ ] Loading state +- [ ] Error state +- [ ] Keyboard transition smooth + +--- + +## ๐Ÿ“Š Progress Tracking + +| Phase | Screens | Status | Completed Date | +|-------|---------|--------|----------------| +| **Phase 1: Job** | 2 screens | ๐ŸŸก In Progress | - | +| **Phase 2: Event & Profile** | 4 screens | โณ Pending | - | +| **Phase 3: Forms** | 6-8 screens | โณ Pending | - | +| **Phase 4: Complex** | 4-6 screens | โณ Pending | - | +| **Phase 5: Cleanup** | Cleanup | โณ Pending | - | + +--- + +## ๐Ÿš€ Current Status + +**Status**: ๐ŸŸก IN PROGRESS +**Current Phase**: Phase 1 - Job Screens +**Started**: 2026-04-01 +**ETA**: 2026-04-07 (Phase 1 complete) + +--- + +## ๐Ÿ“ž Next Actions + +1. **Immediate** (Today): + - [ ] Migrate `ScreenJobCreate.tsx` + - [ ] Migrate `ScreenJobEdit.tsx` + - [ ] Test both screens + +2. **This Week**: + - [ ] Delete test files + - [ ] Document any issues + - [ ] Prepare Phase 2 + +3. **Next Week**: + - [ ] Start Phase 2 (Event & Profile) + - [ ] Review Phase 1 results + - [ ] Adjust migration guide if needed + +--- + +## ๐Ÿ“š Related Files + +**Components:** +- `components/_ShareComponent/NewWrapper.tsx` (Old) +- `components/_ShareComponent/NewWrapper_V2.tsx` (New) +- `hooks/useKeyboardForm.ts` (Keyboard hook) + +**Documentation:** +- `docs/NEWWRAPPER-KEYBOARD-IMPLEMENTATION.md` (Full analysis) +- `docs/KEYBOARD-BUG-TEST.md` (Bug investigation) +- `tasks/TASK-004-newwrapper-migration.md` (This file) + +**Screens to Migrate:** +- `screens/Job/ScreenJobCreate.tsx` +- `screens/Job/ScreenJobEdit.tsx` +- (More in subsequent phases) + +--- + +## โš ๏ธ Risk Mitigation + +**If issues found during migration:** + +1. **Stop migration** for that screen +2. **Revert changes** if critical bug +3. **Document issue** in detail +4. **Fix NewWrapper_V2** if needed +5. **Resume migration** after fix + +**Rollback plan:** +- Keep old `NewWrapper` until all screens migrated +- Easy to revert per screen +- No breaking changes to other screens + +--- + +## โœ… Success Criteria + +**Phase 1 Complete when:** +- [ ] Job Create migrated +- [ ] Job Edit migrated +- [ ] Both screens tested on iOS & Android +- [ ] No critical bugs +- [ ] Test files deleted +- [ ] Documentation updated + +**Overall Migration Complete when:** +- [ ] All form screens migrated +- [ ] All screens tested +- [ ] Old NewWrapper deprecated/removed +- [ ] Team trained on NewWrapper_V2 +- [ ] Documentation complete + +--- + +**Last Updated**: 2026-04-01 +**Created by**: AI Assistant +**Status**: ๐ŸŸก IN PROGRESS