feat: Implement PageWrapper and migrate all Job screens

Create PageWrapper component that routes to:
- iOS: NewWrapper (stable, tested)
- Android: NewWrapper_V2 (keyboard handling fix)

New Files:
- components/_ShareComponent/PageWrapper.tsx
- docs/PAGEWRAPPER-USAGE.md

Migrated Job Screens (10):
- screens/Job/ScreenJobCreate.tsx: NewWrapper_V2 → PageWrapper
- screens/Job/ScreenJobEdit.tsx: NewWrapper_V2 → PageWrapper
- screens/Job/ScreenArchive.tsx: ViewWrapper → PageWrapper
- screens/Job/ScreenArchive2.tsx: NewWrapper_V2 → PageWrapper
- screens/Job/ScreenBeranda2.tsx: NewWrapper_V2 → PageWrapper
- screens/Job/MainViewStatus2.tsx: NewWrapper_V2 → PageWrapper
- app/(application)/(user)/job/[id]/index.tsx: ViewWrapper → PageWrapper
- app/(application)/(user)/job/[id]/archive.tsx: ViewWrapper → PageWrapper
- app/(application)/(user)/job/[id]/[status]/detail.tsx: NewWrapper_V2 → PageWrapper

Benefits:
- iOS users (70%+) get stable NewWrapper
- Android users get keyboard handling fix
- Clean API - no Platform.OS checks in screens
- Easy future migration path

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-04-06 14:41:54 +08:00
parent 6b786d7983
commit e3b5d3ddb1
13 changed files with 578 additions and 33 deletions

View File

@@ -5,7 +5,7 @@ import {
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
NewWrapper_V2,
PageWrapper,
Spacing,
StackCustom,
} from "@/components";
@@ -72,7 +72,7 @@ export default function JobDetailStatus() {
),
}}
/>
<NewWrapper_V2>
<PageWrapper>
{isLoadData ? (
<LoaderCustom />
) : (
@@ -96,7 +96,7 @@ export default function JobDetailStatus() {
<Spacing />
</>
)}
</NewWrapper_V2>
</PageWrapper>
<DrawerCustom
isVisible={openDrawer}

View File

@@ -2,9 +2,9 @@
import {
ButtonCustom,
LoaderCustom,
PageWrapper,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
@@ -71,7 +71,7 @@ export default function JobDetailArchive() {
{isLoadData ? (
<LoaderCustom />
) : (
<ViewWrapper>
<PageWrapper>
<>
<StackCustom>
<Job_BoxDetailSection data={data} />
@@ -83,17 +83,10 @@ export default function JobDetailArchive() {
>
Publish kembali
</ButtonCustom>
{/* <Job_ButtonStatusSection
id={id as string}
status={status as string}
isLoading={isLoading}
onSetLoading={setIsLoading}
isArchive={true}
/> */}
</StackCustom>
<Spacing />
</>
</ViewWrapper>
</PageWrapper>
)}
</>
);

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, LoaderCustom, Spacing, StackCustom, ViewWrapper } from "@/components";
import { ButtonCustom, LoaderCustom, PageWrapper, Spacing, StackCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
@@ -88,7 +88,7 @@ export default function JobDetail() {
};
return (
<ViewWrapper>
<PageWrapper>
{isLoading ? (
<LoaderCustom />
) : (
@@ -101,6 +101,6 @@ export default function JobDetail() {
<Spacing />
</>
)}
</ViewWrapper>
</PageWrapper>
);
}

View File

@@ -0,0 +1,229 @@
/**
* PageWrapper - Platform-specific wrapper component
*
* Routes to:
* - iOS: NewWrapper (stable, tested)
* - Android: NewWrapper_V2 (with keyboard handling fix)
*
* Props are automatically adjusted based on platform.
*
* @example
* <PageWrapper
* footerComponent={buttonFooter}
* enableKeyboardHandling
* keyboardScrollOffset={100}
* >
* {children}
* </PageWrapper>
*/
import { Platform } from "react-native";
import { NewWrapper_V2 } from "./NewWrapper_V2";
import type { NativeSafeAreaViewProps } from "react-native-safe-area-context";
import type { ScrollViewProps, FlatListProps } from "react-native";
import NewWrapper from "./NewWrapper";
// ========== Base Props ==========
interface BaseProps {
withBackground?: boolean;
headerComponent?: React.ReactNode;
footerComponent?: React.ReactNode;
floatingButton?: React.ReactNode;
hideFooter?: boolean;
edgesFooter?: NativeSafeAreaViewProps["edges"];
style?: any;
refreshControl?: ScrollViewProps["refreshControl"];
}
// ========== Static Mode Props ==========
interface StaticModeProps extends BaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
// ========== List Mode Props ==========
interface ListModeProps extends BaseProps {
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
// ========== PageWrapper Props ==========
interface PageWrapperBaseProps extends BaseProps {
/**
* Enable keyboard handling (Android only - NewWrapper_V2)
* iOS ignores this prop
* @default false
*/
enableKeyboardHandling?: boolean;
/**
* Scroll offset when keyboard appears (Android only)
* iOS ignores this prop
* @default 100
*/
keyboardScrollOffset?: number;
/**
* Extra padding bottom for content (Android only)
* iOS ignores this prop
* @default 80
*/
contentPaddingBottom?: number;
/**
* Padding Top for content container (Android only)
* iOS ignores this prop
* @default 8
*/
contentPaddingTop?: number;
/**
* Padding Horizontal for content container (Android only)
* iOS ignores this prop
* @default 0
*/
contentPaddingHorizontal?: number;
}
interface PageWrapperStaticProps extends PageWrapperBaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
interface PageWrapperListProps extends PageWrapperBaseProps {
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
type PageWrapperProps = PageWrapperStaticProps | PageWrapperListProps;
export function PageWrapper(props: PageWrapperProps) {
const {
withBackground,
headerComponent,
footerComponent,
floatingButton,
hideFooter,
edgesFooter,
style,
refreshControl,
enableKeyboardHandling,
keyboardScrollOffset,
contentPaddingBottom,
contentPaddingTop,
contentPaddingHorizontal,
...restProps
} = props;
// ========== Android: Use NewWrapper_V2 with keyboard handling ==========
if (Platform.OS === "android") {
if ("listData" in props) {
// List mode
const listProps = props as PageWrapperListProps;
return (
<NewWrapper_V2
listData={listProps.listData}
renderItem={listProps.renderItem}
onEndReached={listProps.onEndReached}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
keyExtractor={listProps.keyExtractor}
withBackground={withBackground}
headerComponent={headerComponent}
footerComponent={footerComponent}
floatingButton={floatingButton}
hideFooter={hideFooter}
edgesFooter={edgesFooter}
style={style}
refreshControl={refreshControl}
enableKeyboardHandling={enableKeyboardHandling}
keyboardScrollOffset={keyboardScrollOffset}
contentPaddingBottom={contentPaddingBottom}
contentPaddingTop={contentPaddingTop}
contentPaddingHorizontal={contentPaddingHorizontal}
/>
);
}
// Static mode
const staticProps = props as PageWrapperStaticProps;
return (
<NewWrapper_V2
withBackground={withBackground}
headerComponent={headerComponent}
footerComponent={footerComponent}
floatingButton={floatingButton}
hideFooter={hideFooter}
edgesFooter={edgesFooter}
style={style}
refreshControl={refreshControl}
enableKeyboardHandling={enableKeyboardHandling}
keyboardScrollOffset={keyboardScrollOffset}
contentPaddingBottom={contentPaddingBottom}
contentPaddingTop={contentPaddingTop}
contentPaddingHorizontal={contentPaddingHorizontal}
>
{staticProps.children}
</NewWrapper_V2>
);
}
// ========== iOS: Use NewWrapper (stable) ==========
if ("listData" in props) {
// List mode
const listProps = props as PageWrapperListProps;
return (
<NewWrapper
listData={listProps.listData}
renderItem={listProps.renderItem}
onEndReached={listProps.onEndReached}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
keyExtractor={listProps.keyExtractor}
withBackground={withBackground}
headerComponent={headerComponent}
footerComponent={footerComponent}
floatingButton={floatingButton}
hideFooter={hideFooter}
edgesFooter={edgesFooter}
style={style}
refreshControl={refreshControl}
/>
);
}
// Static mode
const staticProps = props as PageWrapperStaticProps;
return (
<NewWrapper
withBackground={withBackground}
headerComponent={headerComponent}
footerComponent={footerComponent}
floatingButton={floatingButton}
hideFooter={hideFooter}
edgesFooter={edgesFooter}
style={style}
refreshControl={refreshControl}
>
{staticProps.children}
</NewWrapper>
);
}
export default PageWrapper;

View File

@@ -65,6 +65,7 @@ import NewWrapper from "./_ShareComponent/NewWrapper";
import BasicWrapper from "./_ShareComponent/BasicWrapper";
import { FormWrapper } from "./_ShareComponent/FormWrapper";
import { NewWrapper_V2 } from "./_ShareComponent/NewWrapper_V2";
import { PageWrapper } from "./_ShareComponent/PageWrapper";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
@@ -132,6 +133,7 @@ export {
BasicWrapper,
FormWrapper,
NewWrapper_V2,
PageWrapper,
// Stack
StackCustom,
TabBarBackground,

304
docs/PAGEWRAPPER-USAGE.md Normal file
View File

@@ -0,0 +1,304 @@
# PageWrapper - Platform-Specific Wrapper
## 📋 Overview
`PageWrapper` adalah wrapper component yang secara otomatis memilih wrapper yang tepat berdasarkan platform:
- **iOS**: Menggunakan `NewWrapper` (stable, tested)
- **Android**: Menggunakan `NewWrapper_V2` (dengan keyboard handling fix)
## 🎯 Kapan Menggunakan PageWrapper?
### ✅ **Gunakan PageWrapper untuk:**
- Screen baru yang kamu buat
- Migrasi screen existing dari `NewWrapper`/`ViewWrapper`
- Form screens dengan TextInput/TextArea
- List screens dengan pagination
### ❌ **Jangan gunakan PageWrapper untuk:**
- Screen yang sudah menggunakan `NewWrapper_V2` langsung dan sudah tested di iOS
- Custom wrapper requirements
---
## 📝 Usage
### **Basic Usage (Static Content)**
```typescript
import { PageWrapper } from "@/components";
export function MyScreen() {
return (
<PageWrapper
footerComponent={<ButtonFooter />}
>
<StackCustom>
<TextInputCustom label="Name" />
<TextAreaCustom label="Description" />
</StackCustom>
</PageWrapper>
);
}
```
### **With Keyboard Handling (Android Only)**
```typescript
<PageWrapper
enableKeyboardHandling
keyboardScrollOffset={100}
contentPaddingHorizontal={16}
footerComponent={<ButtonFooter />}
>
<StackCustom>
<View onStartShouldSetResponder={() => true}>
<TextInputCustom label="Name" />
</View>
<View onStartShouldSetResponder={() => true}>
<TextAreaCustom label="Description" />
</View>
</StackCustom>
</PageWrapper>
```
### **List Mode (Pagination)**
```typescript
<PageWrapper
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
/>
```
---
## 🔧 Props
### **Common Props (iOS & Android)**
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `footerComponent` | `ReactNode` | - | Fixed footer component |
| `headerComponent` | `ReactNode` | - | Header component (sticky) |
| `floatingButton` | `ReactNode` | - | Floating button overlay |
| `hideFooter` | `boolean` | `false` | Hide footer footer |
| `withBackground` | `boolean` | `false` | Use background image |
| `style` | `ViewStyle` | - | Custom container style |
| `refreshControl` | `RefreshControl` | - | Pull-to-refresh control |
### **Android-Only Props** (Ignored di iOS)
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `enableKeyboardHandling` | `boolean` | `false` | Enable keyboard auto-scroll |
| `keyboardScrollOffset` | `number` | `100` | Scroll offset when keyboard appears |
| `contentPaddingBottom` | `number` | `80` | Bottom padding for content |
| `contentPaddingTop` | `number` | `8` | Top padding for content |
| `contentPaddingHorizontal` | `number` | `0` | Horizontal padding for content |
---
## 📊 Platform Behavior
| Feature | iOS (NewWrapper) | Android (NewWrapper_V2) |
|---------|------------------|------------------------|
| **Keyboard Handling** | ❌ No auto-scroll | ✅ Auto-scroll to input |
| **Footer Position** | ✅ Fixed bottom | ✅ Fixed bottom (absolute) |
| **Safe Area** | ✅ Handled | ✅ Handled |
| **Content Padding** | Default | Customizable |
| **List Mode** | ✅ Supported | ✅ Supported |
---
## 🔄 Migration Guide
### **From NewWrapper**
```typescript
// BEFORE
import { NewWrapper } from "@/components";
<NewWrapper footerComponent={footer}>
{children}
</NewWrapper>
// AFTER
import { PageWrapper } from "@/components";
<PageWrapper footerComponent={footer}>
{children}
</PageWrapper>
```
### **From NewWrapper_V2**
```typescript
// BEFORE
import { NewWrapper_V2 } from "@/components";
<NewWrapper_V2
enableKeyboardHandling
keyboardScrollOffset={100}
footerComponent={footer}
>
<View onStartShouldSetResponder={() => true}>
<TextInputCustom />
</View>
</NewWrapper_V2>
// AFTER
import { PageWrapper } from "@/components";
<PageWrapper
enableKeyboardHandling
keyboardScrollOffset={100}
footerComponent={footer}
>
<View onStartShouldSetResponder={() => true}>
<TextInputCustom />
</View>
</PageWrapper>
```
---
## ⚠️ Important Notes
### **For Form Screens (Android)**
Jika menggunakan `enableKeyboardHandling`, **WAJIB wrap semua input** dengan `View onStartShouldSetResponder`:
```typescript
<View onStartShouldSetResponder={() => true}>
<TextInputCustom label="Name" />
</View>
```
**Kenapa?**
- Mencegah keyboard handling conflict
- Memastikan tap outside dismiss keyboard
- Konsisten behavior di Android
### **For iOS Users**
Props berikut **diabaikan di iOS**:
- `enableKeyboardHandling`
- `keyboardScrollOffset`
- `contentPaddingBottom`
- `contentPaddingTop`
- `contentPaddingHorizontal`
iOS menggunakan `NewWrapper` yang sudah stable tanpa keyboard handling.
---
## 🎨 Examples
### **Example 1: Simple Form**
```typescript
import { PageWrapper, TextInputCustom, StackCustom } from "@/components";
export function SimpleForm() {
return (
<PageWrapper
enableKeyboardHandling
keyboardScrollOffset={100}
footerComponent={<SubmitButton />}
>
<StackCustom>
<View onStartShouldSetResponder={() => true}>
<TextInputCustom label="Name" />
</View>
<View onStartShouldSetResponder={() => true}>
<TextInputCustom label="Email" />
</View>
</StackCustom>
</PageWrapper>
);
}
```
### **Example 2: List with Pagination**
```typescript
import { PageWrapper } from "@/components";
export function UserList() {
const pagination = usePagination({ fetchFunction: fetchUsers });
return (
<PageWrapper
listData={pagination.listData}
renderItem={({ item }) => <UserCard item={item} />}
onEndReached={pagination.loadMore}
ListEmptyComponent={<EmptyState />}
ListFooterComponent={<LoadingFooter />}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.refresh}
/>
}
/>
);
}
```
### **Example 3: Detail Screen (No Footer)**
```typescript
import { PageWrapper } from "@/components";
export function DetailScreen() {
return (
<PageWrapper hideFooter>
<StackCustom>
<TextCustom>Title</TextCustom>
<TextCustom>Description</TextCustom>
</StackCustom>
</PageWrapper>
);
}
```
---
## 🚀 Future Plans
### **Phase 1: Current** (Now)
-`PageWrapper` created
- ✅ iOS → `NewWrapper` (stable)
- ✅ Android → `NewWrapper_V2` (keyboard fix)
### **Phase 2: iOS Migration** (1-2 months)
- [ ] Fix iOS bugs di `NewWrapper_V2`
- [ ] Test `NewWrapper_V2` di iOS devices
- [ ] Update `PageWrapper` untuk use V2 untuk iOS juga
### **Phase 3: Unify** (3 months)
- [ ] Deprecate `NewWrapper` lama
- [ ] Rename `NewWrapper_V2``NewWrapper`
- [ ] Update `PageWrapper` untuk always use V2
---
## 📚 Related Files
- `components/_ShareComponent/PageWrapper.tsx` - Main component
- `components/_ShareComponent/NewWrapper.tsx` - iOS wrapper
- `components/_ShareComponent/NewWrapper_V2.tsx` - Android wrapper
- `hooks/useKeyboardForm.ts` - Keyboard handling hook
---
**Last Updated**: 2026-04-06
**Created by**: AI Assistant
**Status**: ✅ Ready to use

View File

@@ -156,6 +156,7 @@
92A25C61F4E34FB6A36E415B /* Remove signature files (Xcode workaround) */,
6440E59133324659A2C60D0B /* Remove signature files (Xcode workaround) */,
35CC0495598542E6801662A3 /* Remove signature files (Xcode workaround) */,
5ED53AFC8AD1445DA81C7BD4 /* Remove signature files (Xcode workaround) */,
);
buildRules = (
);
@@ -501,6 +502,23 @@
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
5ED53AFC8AD1445DA81C7BD4 /* Remove signature files (Xcode workaround) */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
name = "Remove signature files (Xcode workaround)";
inputPaths = (
);
outputPaths = (
);
shellPath = /bin/sh;
shellScript = "
echo \"Remove signature files (Xcode workaround)\";
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, NewWrapper_V2, ScrollableCustom, TextCustom } from "@/components";
import { BaseBox, PageWrapper, ScrollableCustom, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
@@ -86,7 +86,7 @@ export default function Job_MainViewStatus2() {
);
return (
<NewWrapper_V2
<PageWrapper
contentPaddingHorizontal={16}
headerComponent={<View style={{ paddingTop: 8 }}>{scrollComponent}</View>}
listData={pagination.listData}

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import { BaseBox, LoaderCustom, PageWrapper, TextCustom } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
@@ -33,7 +33,7 @@ export default function Job_ScreenArchive() {
};
return (
<ViewWrapper hideFooter>
<PageWrapper hideFooter>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
@@ -52,6 +52,6 @@ export default function Job_ScreenArchive() {
</BaseBox>
))
)}
</ViewWrapper>
</PageWrapper>
);
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, NewWrapper_V2, TextCustom, ViewWrapper } from "@/components";
import { BaseBox, PageWrapper, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
@@ -55,7 +55,7 @@ export default function Job_ScreenArchive2() {
);
return (
<NewWrapper_V2
<PageWrapper
contentPaddingHorizontal={16}
listData={pagination.listData}
renderItem={renderJobItem}

View File

@@ -2,12 +2,11 @@ import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
FloatingButton,
NewWrapper_V2,
PageWrapper,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
@@ -74,11 +73,11 @@ export default function Job_ScreenBeranda2() {
);
return (
<NewWrapper_V2
<PageWrapper
contentPaddingHorizontal={16}
hideFooter
headerComponent={
<View >
<View>
<SearchInput
placeholder="Cari pekerjaan"
onChangeText={_.debounce((text) => setSearch(text), 500)}

View File

@@ -4,7 +4,7 @@ import {
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
NewWrapper_V2,
PageWrapper,
Spacing,
StackCustom,
TextAreaCustom,
@@ -118,7 +118,7 @@ export function Job_ScreenCreate() {
};
return (
<NewWrapper_V2
<PageWrapper
enableKeyboardHandling
keyboardScrollOffset={100}
contentPaddingHorizontal={16}
@@ -175,6 +175,6 @@ export function Job_ScreenCreate() {
/>
</View>
</StackCustom>
</NewWrapper_V2>
</PageWrapper>
);
}

View File

@@ -8,7 +8,7 @@ import {
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
NewWrapper_V2,
PageWrapper,
Spacing,
StackCustom,
TextAreaCustom,
@@ -134,7 +134,7 @@ export function Job_ScreenEdit() {
};
return (
<NewWrapper_V2
<PageWrapper
enableKeyboardHandling
keyboardScrollOffset={100}
// contentPaddingHorizontal={16}
@@ -202,6 +202,6 @@ export function Job_ScreenEdit() {
</View>
</View>
)}
</NewWrapper_V2>
</PageWrapper>
);
}