diff --git a/app/(application)/(user)/portofolio/[id]/edit-logo.tsx b/app/(application)/(user)/portofolio/[id]/edit-logo.tsx
index 9845fb4..b52a81a 100644
--- a/app/(application)/(user)/portofolio/[id]/edit-logo.tsx
+++ b/app/(application)/(user)/portofolio/[id]/edit-logo.tsx
@@ -3,7 +3,7 @@ import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
- ViewWrapper
+ OS_Wrapper
} from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
@@ -126,7 +126,7 @@ export default function PortofolioEditLogo() {
return (
<>
-
+
Upload
-
+
>
);
}
diff --git a/app/(application)/(user)/portofolio/[id]/edit-social-media.tsx b/app/(application)/(user)/portofolio/[id]/edit-social-media.tsx
index 1770c3f..dfbb691 100644
--- a/app/(application)/(user)/portofolio/[id]/edit-social-media.tsx
+++ b/app/(application)/(user)/portofolio/[id]/edit-social-media.tsx
@@ -1,8 +1,8 @@
import {
BoxButtonOnFooter,
ButtonCustom,
+ OS_Wrapper,
TextInputCustom,
- ViewWrapper,
} from "@/components";
import {
apiGetOnePortofolio,
@@ -91,7 +91,11 @@ export default function PortofolioEditSocialMedia() {
return (
<>
-
+
setData({ ...data, tiktok: value })}
@@ -122,7 +126,7 @@ export default function PortofolioEditSocialMedia() {
label="Youtube"
placeholder="Masukkan youtube"
/>
-
+
>
);
}
diff --git a/app/(application)/(user)/portofolio/[id]/edit.tsx b/app/(application)/(user)/portofolio/[id]/edit.tsx
index b1dbfad..eeeeb4c 100644
--- a/app/(application)/(user)/portofolio/[id]/edit.tsx
+++ b/app/(application)/(user)/portofolio/[id]/edit.tsx
@@ -4,7 +4,7 @@ import {
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
- NewWrapper,
+ OS_Wrapper,
PhoneInputCustom,
SelectCustom,
Spacing,
@@ -16,7 +16,11 @@ import {
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
-import { DEFAULT_COUNTRY, type CountryData, COUNTRIES } from "@/constants/countries";
+import {
+ DEFAULT_COUNTRY,
+ type CountryData,
+ COUNTRIES,
+} from "@/constants/countries";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
@@ -61,7 +65,8 @@ export default function PortofolioEdit() {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState({});
const [phoneNumber, setPhoneNumber] = useState("");
- const [selectedCountry, setSelectedCountry] = useState(DEFAULT_COUNTRY);
+ const [selectedCountry, setSelectedCountry] =
+ useState(DEFAULT_COUNTRY);
const [bidangBisnis, setBidangBisnis] = useState<
IMasterBidangBisnis[] | null
>(null);
@@ -75,38 +80,38 @@ export default function PortofolioEdit() {
function handlePhoneChange(phone: string) {
setPhoneNumber(phone);
-
+
// Format phone number for API
const callingCode = selectedCountry.callingCode;
let fixNumber = phone.replace(/\s+/g, "").replace(/^0+/, "");
-
+
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
-
+
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
-
+
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
function handleCountryChange(country: CountryData) {
setSelectedCountry(country);
-
+
// Re-format with new country code
const callingCode = country.callingCode;
let fixNumber = phoneNumber.replace(/\s+/g, "").replace(/^0+/, "");
-
+
// Remove country code if already present
if (fixNumber.startsWith(callingCode)) {
fixNumber = fixNumber.substring(callingCode.length);
}
-
+
// Remove leading zero
fixNumber = fixNumber.replace(/^0+/, "");
-
+
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
@@ -157,7 +162,7 @@ export default function PortofolioEdit() {
const fullNumber = response.data.tlpn;
let displayNumber = fullNumber;
let detectedCountry = DEFAULT_COUNTRY;
-
+
// Try to detect country from calling code
for (const country of COUNTRIES) {
if (fullNumber.startsWith(country.callingCode)) {
@@ -166,12 +171,12 @@ export default function PortofolioEdit() {
break;
}
}
-
+
setSelectedCountry(detectedCountry);
-
+
// Remove leading zero if present
displayNumber = displayNumber.replace(/^0+/, "");
-
+
setPhoneNumber(displayNumber);
setData({ ...response.data, tlpn: displayNumber });
@@ -363,161 +368,159 @@ export default function PortofolioEdit() {
);
- if (!bidangBisnis || !subBidangBisnis) {
- return (
- <>
-
-
-
- >
- );
- }
-
return (
<>
-
-
-
- setData({ ...data, namaBisnis: value })
- }
- />
-
- ({
- label: item.name,
- value: item.id,
- }))}
- value={data.masterBidangBisnisId}
- onChange={(value: any) => {
- handleBidangBisnisChange(value);
- }}
- />
-
- {listSubBidangSelected.map((item, index) => {
- // Filter data untuk select sub bidang, menghilangkan yang sudah dipilih kecuali untuk item ini sendiri
- const selectedIds = listSubBidangSelected
- .filter((_, i) => i !== index)
- .map((s) => s.MasterSubBidangBisnis?.id)
- .filter((id) => id); // Filter hanya yang memiliki id (tidak kosong)
-
- const availableSubBidangOptions = (selectedSubBidang || [])
- .filter((sub: any) => {
- // Tampilkan jika ini adalah opsi yang dipilih saat ini atau belum dipilih di sub bidang lainnya
-
- return (
- sub.id === item.MasterSubBidangBisnis?.id ||
- !selectedIds.includes(sub.id)
- );
- })
- .map((sub: any) => ({
- value: sub.id,
- label: sub.name,
- }));
-
- return (
- {
- handleSubBidangChange(value, index);
- }}
- />
- );
- })}
-
-
-
- {
- handleAddSubBidang();
- }}
- icon={
-
- }
- size="xl"
- />
- {
- handleRemoveSubBidang(listSubBidangSelected.length - 1);
- }}
- icon={
-
- }
- size="xl"
- />
-
-
-
-
-
-
-
- Nomor Telepon
-
- *
-
-
-
+ {!bidangBisnis || !subBidangBisnis ? (
+
+ ) : (
+
+
+ setData({ ...data, namaBisnis: value })
+ }
/>
-
-
-
- setData({ ...data, alamatKantor: value })
- }
- />
+ ({
+ label: item.name,
+ value: item.id,
+ }))}
+ value={data.masterBidangBisnisId}
+ onChange={(value: any) => {
+ handleBidangBisnisChange(value);
+ }}
+ />
-
- setData({ ...data, deskripsi: value })
- }
- autosize
- minRows={2}
- maxRows={5}
- required
- showCount
- maxLength={1000}
- />
-
-
-
+ {listSubBidangSelected.map((item, index) => {
+ // Filter data untuk select sub bidang, menghilangkan yang sudah dipilih kecuali untuk item ini sendiri
+ const selectedIds = listSubBidangSelected
+ .filter((_, i) => i !== index)
+ .map((s) => s.MasterSubBidangBisnis?.id)
+ .filter((id) => id); // Filter hanya yang memiliki id (tidak kosong)
+
+ const availableSubBidangOptions = (selectedSubBidang || [])
+ .filter((sub: any) => {
+ // Tampilkan jika ini adalah opsi yang dipilih saat ini atau belum dipilih di sub bidang lainnya
+
+ return (
+ sub.id === item.MasterSubBidangBisnis?.id ||
+ !selectedIds.includes(sub.id)
+ );
+ })
+ .map((sub: any) => ({
+ value: sub.id,
+ label: sub.name,
+ }));
+
+ return (
+ {
+ handleSubBidangChange(value, index);
+ }}
+ />
+ );
+ })}
+
+
+
+ {
+ handleAddSubBidang();
+ }}
+ icon={
+
+ }
+ size="xl"
+ />
+ {
+ handleRemoveSubBidang(listSubBidangSelected.length - 1);
+ }}
+ icon={
+
+ }
+ size="xl"
+ />
+
+
+
+
+
+
+
+ Nomor Telepon
+
+ *
+
+
+
+
+
+
+
+ setData({ ...data, alamatKantor: value })
+ }
+ />
+
+
+ setData({ ...data, deskripsi: value })
+ }
+ autosize
+ minRows={2}
+ maxRows={5}
+ required
+ showCount
+ maxLength={1000}
+ />
+
+
+ )}
+
>
);
}
diff --git a/app/(application)/(user)/portofolio/[id]/index.tsx b/app/(application)/(user)/portofolio/[id]/index.tsx
index ed2c2d3..51949ec 100644
--- a/app/(application)/(user)/portofolio/[id]/index.tsx
+++ b/app/(application)/(user)/portofolio/[id]/index.tsx
@@ -4,6 +4,7 @@ import {
DrawerCustom,
DummyLandscapeImage,
LoaderCustom,
+ OS_Wrapper,
Spacing,
StackCustom,
TextCustom,
@@ -12,7 +13,6 @@ import AppHeader from "@/components/_ShareComponent/AppHeader";
import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
-import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
@@ -92,7 +92,7 @@ export default function Portofolio() {
),
}}
/>
-
+
{!data || !profileId ? (
@@ -125,7 +125,7 @@ export default function Portofolio() {
)}
-
+
{/* Drawer Komponen Eksternal */}
+
{headerComponent && (
{headerComponent}
)}
@@ -208,7 +210,7 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
showsVerticalScrollIndicator={false}
>
- {renderContainer(staticProps.children)}
+ {renderContainer(childrenWithFocus)}
@@ -239,7 +241,7 @@ export function AndroidWrapper(props: AndroidWrapperProps) {
{floatingButton && (
{floatingButton}
)}
-
+
);
}
diff --git a/hooks/useKeyboardForm.ts b/hooks/useKeyboardForm.ts
index 115f988..7830ff1 100644
--- a/hooks/useKeyboardForm.ts
+++ b/hooks/useKeyboardForm.ts
@@ -1,12 +1,25 @@
// useKeyboardForm.ts - Hook untuk keyboard handling pada form
-import { Keyboard, ScrollView, Dimensions } from "react-native";
-import { useState, useEffect, useRef } from "react";
-import React from "react";
+import { Keyboard, ScrollView, Dimensions, findNodeHandle, UIManager } from "react-native";
+import { useState, useEffect, useRef, useCallback } from "react";
export function useKeyboardForm(scrollOffset = 100) {
const scrollViewRef = useRef(null);
const [keyboardHeight, setKeyboardHeight] = useState(0);
const currentScrollY = useRef(0);
+ const inputPageY = useRef(0);
+ const screenHeight = Dimensions.get('window').height;
+
+ // Fungsi untuk mengukur posisi absolut input
+ const handleInputFocus = useCallback((target: any) => {
+ const nodeHandle = findNodeHandle(target);
+ if (nodeHandle) {
+ UIManager.measure(nodeHandle, (x, y, width, height, pageX, pageY) => {
+ if (pageY !== undefined && pageY !== null) {
+ inputPageY.current = pageY;
+ }
+ });
+ }
+ }, []);
// Listen to keyboard events
useEffect(() => {
@@ -16,20 +29,34 @@ export function useKeyboardForm(scrollOffset = 100) {
const kbHeight = e.endCoordinates.height;
setKeyboardHeight(kbHeight);
- // Simple: scroll by keyboard height saat keyboard muncul
+ // Conditional scroll: hanya scroll jika input tertutup keyboard
if (scrollViewRef.current) {
- const targetY = currentScrollY.current + kbHeight - scrollOffset;
- scrollViewRef.current.scrollTo({
- y: Math.max(0, targetY),
- animated: true,
- });
+ const touchAbsoluteY = inputPageY.current;
+
+ // Posisi Y teratas keyboard (dari atas layar)
+ const keyboardTopY = screenHeight - kbHeight;
+
+ // Jika input ADA DI BAWAH keyboard (tertutup)
+ if (touchAbsoluteY > keyboardTopY) {
+ // Hitung berapa harus scroll agar input terlihat di atas keyboard
+ const scrollBy = touchAbsoluteY - keyboardTopY + scrollOffset;
+ const targetY = currentScrollY.current + scrollBy;
+
+ scrollViewRef.current.scrollTo({
+ y: Math.max(0, targetY),
+ animated: true,
+ });
+ }
+ // Jika input SUDAH TERLIHAT (di atas keyboard), JANGAN SCROLL
}
}
);
+
const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide',
() => {
setKeyboardHeight(0);
+ inputPageY.current = 0;
}
);
@@ -37,35 +64,74 @@ export function useKeyboardForm(scrollOffset = 100) {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
};
- }, [scrollOffset]);
+ }, [scrollOffset, screenHeight]);
// Track scroll position
const handleScroll = (event: any) => {
currentScrollY.current = event.nativeEvent.contentOffset.y;
};
- // Dummy handlers (for API compatibility)
- const createFocusHandler = () => {
- return () => {};
- };
-
- const registerInputFocus = () => {};
-
return {
scrollViewRef,
keyboardHeight,
- registerInputFocus,
- createFocusHandler,
+ handleInputFocus,
handleScroll,
};
}
/**
- * Dummy helper (no-op, for API compatibility)
+ * Helper untuk inject onFocus handler ke semua TextInput/TextArea children
+ * Menggunakan UI.measure untuk mendapatkan posisi absolut input secara akurat
*/
export function cloneChildrenWithFocusHandler(
children: React.ReactNode,
- _focusHandler: (inputRef: any) => void
+ focusHandler: (target: any) => void
): React.ReactNode {
- return children;
-}
+ if (!children) return children;
+ const React = require("react");
+
+ return React.Children.map(children, (child: any) => {
+ if (!React.isValidElement(child)) return child;
+
+ const childType = child.type;
+ const childProps = child.props as Record || {};
+
+ // Check if it's a text input component
+ let isTextInput = false;
+
+ if (typeof childType === 'string') {
+ isTextInput = childType.toLowerCase().includes('textinput');
+ } else if (childType) {
+ isTextInput =
+ (childType as any).displayName?.includes('TextInput') ||
+ (childType as any).name?.includes('TextInput') ||
+ (childType as any).displayName?.includes('TextArea') ||
+ (childType as any).name?.includes('TextArea') ||
+ (childType as any).displayName?.includes('PhoneInput') ||
+ (childType as any).name?.includes('PhoneInput') ||
+ (childType as any).displayName?.includes('Select') ||
+ (childType as any).name?.includes('Select');
+ }
+
+ if (isTextInput) {
+ const existingOnFocus = childProps.onFocus;
+ return React.cloneElement(child, {
+ ...childProps,
+ onFocus: (e: any) => {
+ existingOnFocus?.(e);
+ focusHandler(e.target);
+ },
+ } as any);
+ }
+
+ // Recursively clone nested children
+ if (childProps.children) {
+ return React.cloneElement(child, {
+ ...childProps,
+ children: cloneChildrenWithFocusHandler(childProps.children, focusHandler),
+ } as any);
+ }
+
+ return child;
+ });
+}
\ No newline at end of file
diff --git a/screens/Maps/ScreenMapsCreate.tsx b/screens/Maps/ScreenMapsCreate.tsx
index 33f5dfb..ea41df4 100644
--- a/screens/Maps/ScreenMapsCreate.tsx
+++ b/screens/Maps/ScreenMapsCreate.tsx
@@ -1,4 +1,3 @@
-import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import {
BoxButtonOnFooter,
ButtonCustom,
@@ -8,6 +7,7 @@ import {
TextInputCustom,
LandscapeFrameUploaded,
ButtonCenteredOnly,
+ OS_Wrapper,
} from "@/components";
import { MapSelectedPlatform } from "@/components/Map/MapSelectedPlatform";
import DIRECTORY_ID from "@/constants/directory-id";
@@ -142,10 +142,14 @@ export function Maps_ScreenMapsCreate() {
);
/**
- * Render screen dengan NewWrapper
+ * Render screen dengan OS_Wrapper
*/
return (
-
+
@@ -179,7 +183,7 @@ export function Maps_ScreenMapsCreate() {
-
+
);
}
diff --git a/screens/Maps/ScreenMapsEdit.tsx b/screens/Maps/ScreenMapsEdit.tsx
index deee80f..b28b4ed 100644
--- a/screens/Maps/ScreenMapsEdit.tsx
+++ b/screens/Maps/ScreenMapsEdit.tsx
@@ -7,7 +7,7 @@ import {
LandscapeFrameUploaded,
Spacing,
TextInputCustom,
- ViewWrapper,
+ OS_Wrapper,
} from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MapSelectedPlatform } from "@/components/Map/MapSelectedPlatform";
@@ -166,7 +166,11 @@ export function Maps_ScreenMapsEdit() {
: defaultRegion;
return (
-
+
{/*
-
+
);
}
diff --git a/screens/Portofolio/ScreenPortofolioCreate.tsx b/screens/Portofolio/ScreenPortofolioCreate.tsx
index 6547b7b..5b7f680 100644
--- a/screens/Portofolio/ScreenPortofolioCreate.tsx
+++ b/screens/Portofolio/ScreenPortofolioCreate.tsx
@@ -4,7 +4,7 @@ import {
ButtonCenteredOnly,
CenterCustom,
InformationBox,
- NewWrapper,
+ OS_Wrapper,
PhoneInputCustom,
SelectCustom,
Spacing,
@@ -142,7 +142,9 @@ export function ScreenPortofolioCreate() {
};
return (
-
-
+
);
}
diff --git a/screens/Portofolio/ViewListPortofolio.tsx b/screens/Portofolio/ViewListPortofolio.tsx
index 2049feb..44bd907 100644
--- a/screens/Portofolio/ViewListPortofolio.tsx
+++ b/screens/Portofolio/ViewListPortofolio.tsx
@@ -1,4 +1,4 @@
-import { NewWrapper, TextCustom } from "@/components";
+import { OS_Wrapper, TextCustom } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { usePagination } from "@/hooks/use-pagination";
@@ -52,7 +52,7 @@ export default function ViewListPortofolio() {
});
return (
-
- }
+ refreshControl={}
/>
```
@@ -209,7 +205,7 @@ Setiap screen yang sudah di-migrate, test:
}
footerComponent={}
- contentPadding={PADDING_INLINE}
+ refreshControl={}
>
@@ -220,60 +216,7 @@ Setiap screen yang sudah di-migrate, test:
- Submit
-
- }
->
-
-
-```
-
-### Migration Pattern:
-
-```tsx
-// OLD
-import NewWrapper from "@/components/_ShareComponent/NewWrapper";
-
-
-
-// NEW
-import { OS_Wrapper } from "@/components";
-import { PADDING_INLINE } from "@/constants/constans-value";
-
-
-```
-
-```tsx
-// OLD (Form with keyboard handling)
-import { NewWrapper_V2 } from "@/components/_ShareComponent/NewWrapper_V2";
-
-
-
-
-
-// NEW (Unified API)
-import { OS_Wrapper } from "@/components";
-
-}
>
@@ -287,6 +230,9 @@ import { OS_Wrapper } from "@/components";
### Issue: Keyboard menutupi input di Android
**Solution**: Pastikan pakai `OS_Wrapper` dengan `enableKeyboardHandling={true}` dan `contentPaddingBottom={250}` untuk form screens.
+### Issue: Pull-to-refresh tidak berfungsi di static mode
+**Solution**: Sudah diperbaiki! `refreshControl` sekarang di-pass ke ScrollView di AndroidWrapper.
+
### Issue: Footer terlalu jauh dari bottom
**Solution**: Kurangi `contentPaddingBottom` (default: 100 untuk list). Untuk form screens tetap 250.
@@ -298,10 +244,15 @@ import { OS_Wrapper } from "@/components";
| Phase | Total Files | Migrated | Testing | Status |
|-------|-------------|----------|---------|--------|
| Phase 1 (Job) | 9 | 9 | ✅ Complete | ✅ Complete |
-| Phase 2 (User) | TBD | 0 | 0 | ⏳ Pending |
-| Phase 3 (Admin) | TBD | 0 | 0 | ⏳ Pending |
-| Phase 4 (Other) | TBD | 0 | 0 | ⏳ Pending |
-| **Total** | **9+** | **9** | **9** | **100% (Phase 1)** |
+| Phase 2 (Profile + Others) | 10 | 10 | ⏳ Pending | ✅ Complete |
+| Phase 3 (Portfolio) | 6 | 6 | ⏳ Pending | ✅ Complete |
+| Phase 4 (Maps) | 2 | 2 | ⏳ Pending | ✅ Complete |
+| Phase 5 (Event) | TBD | 0 | 0 | ⏳ Pending |
+| Phase 6 (Voting) | TBD | 0 | 0 | ⏳ Pending |
+| Phase 7 (Forum) | TBD | 0 | 0 | ⏳ Pending |
+| Phase 8 (Donation) | TBD | 0 | 0 | ⏳ Pending |
+| Phase 9 (Other) | TBD | 0 | 0 | ⏳ Pending |
+| **Total** | **27+** | **27** | **9** | **Phase 1-4 Complete** |
## 🔄 Rollback Plan
@@ -316,5 +267,5 @@ Jika ada issue yang tidak bisa di-fix dalam 1 jam:
**Co-authored-by**: Qwen-Coder
**Created**: 2026-04-06
**Last Updated**: 2026-04-08
-**Status**: Phase 1 (Job Screens) Complete ✅
-**Next**: Phase 2 - Other User Screens (Profile, Forum, Portfolio)
\ No newline at end of file
+**Status**: Phase 1-4 Complete ✅ (27 files migrated)
+**Next**: Phase 5 - Event Management Screens