From b6cd308b0b8c9bb978556a37f5af63b328a72ff1 Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Wed, 25 Mar 2026 16:22:54 +0800 Subject: [PATCH] Refactor pagination implementation dan perbaikan UI - Add default page parameter di apiAllUser - Refactor MainView_V2.tsx dengan separate render functions - Update pagination pageSize menjadi 10 di Forum - Fix iOS height constant dan tab styling - Rename Admin_ScreenPortofolioCreate ke ScreenPortofolioCreate - Add TASKS documentation folder Co-authored-by: Qwen-Coder --- TASKS/crowdfunding-redesign.md | 337 ++++++++++++++++++ app/(application)/(user)/home.tsx | 33 +- .../(user)/portofolio/[id]/create.tsx | 4 +- components/_ShareComponent/NewWrapper.tsx | 20 +- constants/constans-value.ts | 2 +- .../project.pbxproj | 18 + screens/Authentication/LoginView.tsx | 6 +- screens/Forum/ViewBeranda3.tsx | 2 +- screens/Home/homeViewStyle.tsx | 2 +- screens/Home/tabSection.tsx | 7 +- screens/Portofolio/ScreenPortofolioCreate.tsx | 13 +- screens/UserSeach/MainView_V2.tsx | 210 ++++++----- service/api-client/api-user.ts | 4 +- styles/global-styles.ts | 2 +- styles/tabs-styles.ts | 2 +- 15 files changed, 511 insertions(+), 151 deletions(-) create mode 100644 TASKS/crowdfunding-redesign.md diff --git a/TASKS/crowdfunding-redesign.md b/TASKS/crowdfunding-redesign.md new file mode 100644 index 0000000..f990431 --- /dev/null +++ b/TASKS/crowdfunding-redesign.md @@ -0,0 +1,337 @@ +# Crowdfunding Page Redesign - Modern UI Enhancement + +## 📋 Ringkasan Task +Redesign halaman Crowdfunding (`app/(application)/(user)/crowdfunding/index.tsx`) untuk menciptakan tampilan yang lebih modern, menarik, dan user-friendly dengan visual hierarchy yang lebih baik. + +--- + +## 🎯 Tujuan +1. Meningkatkan visual appeal dengan desain card yang modern +2. Memperbaiki spacing dan layout untuk readability yang lebih baik +3. Menambahkan elemen visual (icon, color accent) untuk navigasi yang lebih intuitif +4. Menciptakan konsistensi dengan design system aplikasi HIPMI + +--- + +## 📁 File yang Terlibat + +### File Utama +- **Target**: `app/(application)/(user)/crowdfunding/index.tsx` + +### Komponen yang Mungkin Digunakan +- `ViewWrapper` - Wrapper utama +- `StackCustom` - Layout vertikal +- `Grid` & `Grid.Col` - Layout horizontal +- `TextCustom` - Typography +- `BaseBox` / `AdminBasicBox` - Card container +- `ClickableCustom` - Interactive element +- `Feather` / `Ionicons` - Icons + +### Constants +- `MainColor` - Color palette +- `ICON_SIZE_SMALL`, `ICON_SIZE_BASE` - Icon sizing + +### Assets +- `@/assets/images/constants/crowd-hipmi.png` - Header image + +--- + +## 🔍 Analisis Kondisi Saat Ini + +### Kelemahan Design Sekarang +1. **Header Image** + - Plain image tanpa overlay atau title + - Tidak ada visual hierarchy + +2. **Card List** + - BaseBox terlalu simple, kurang depth + - Tidak ada shadow atau elevation + - Border radius mungkin kurang smooth + - Spacing antar elemen kurang konsisten + +3. **Typography** + - Judul dan deskripsi kurang kontras + - Tidak ada visual emphasis yang kuat + +4. **Navigation** + - Chevron icon terlalu plain + - Tidak ada visual feedback saat hover/press + +5. **Color Usage** + - Kurang color accent untuk membedakan sections + - Monoton dengan warna yang ada + +--- + +## 🎨 Rencana Desain + +### 1. Header Section Enhancement +``` +┌─────────────────────────────────────┐ +│ [Hero Image dengan Overlay] │ +│ ┌─────────────────────────────┐ │ +│ │ Crowdfunding │ │ +│ │ Platform Investasi & Donasi │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +**Implementasi:** +- Image dengan overlay gradient untuk text readability +- Title "Crowdfunding" dengan subtitle +- Rounded corners dengan overflow hidden +- Height: ~180-200px + +### 2. Card Design Modern +``` +┌─────────────────────────────────────┐ +│ [Icon] Investasi → │ +│ Deskripsi singkat... │ +│ (2-3 baris max) │ +└─────────────────────────────────────┘ +``` + +**Implementasi:** +- Card dengan: + - Background gradient atau solid color dengan tint + - Shadow/elevation untuk depth + - Border radius: 12-16px + - Padding: 16-20px + - Icon di kiri (Investasi & Donasi) + - Chevron di kanan dengan style yang lebih modern + +### 3. Icon Integration +- **Investasi**: Icon grafik/trending (contoh: `trending-up`, `pie-chart`) +- **Donasi**: Icon hati/tangan (contoh: `heart`, `hand-heart`) +- Size: 40-48px untuk icon card +- Background icon: Circle dengan color accent + +### 4. Color Scheme +``` +Investasi: +- Primary: Blue/Teal gradient +- Accent: Soft blue background +- Icon: White on blue + +Donasi: +- Primary: Orange/Red gradient +- Accent: Soft orange background +- Icon: White on orange +``` + +### 5. Typography Hierarchy +``` +Header Title: bold, x-large (20-22px) +Header Subtitle: regular, small (14-15px), gray +Card Title: bold, large (17-18px) +Card Description: regular, base (14px), gray +``` + +### 6. Spacing & Layout +``` +Container padding: 16px +Card margin bottom: 12-16px +Card internal padding: 16px +Gap between elements: 8-12px +``` + +--- + +## 📝 Breakdown Task + +### Task 1: Persiapan & Research +- [ ] Review komponen yang tersedia di `components/` +- [ ] Cek color palette di `constants/color-palet.ts` +- [ ] Identifikasi icon yang tersedia (Feather, Ionicons) +- [ ] Screenshot design sekarang untuk perbandingan + +### Task 2: Setup Structure +- [ ] Buat constant untuk list menu (pindahkan dari component body) +- [ ] Tambahkan icon mapping untuk setiap menu item +- [ ] Setup color scheme untuk setiap card + +### Task 3: Header Redesign +- [ ] Buat container dengan overflow hidden +- [ ] Tambahkan image dengan overlay gradient +- [ ] Tambahkan title "Crowdfunding" dengan text white +- [ ] Tambahkan subtitle (opsional) +- [ ] Test di berbagai ukuran layar + +### Task 4: Card Component +- [ ] Buat custom card component atau modify BaseBox +- [ ] Tambahkan shadow/elevation +- [ ] Tambahkan border radius yang smooth +- [ ] Setup gradient background (opsional) +- [ ] Tambahkan visual feedback saat press + +### Task 5: Icon Integration +- [ ] Pilih icon yang sesuai untuk setiap menu +- [ ] Buat icon container dengan background color +- [ ] Setup icon size dan positioning +- [ ] Test visibility di berbagai device + +### Task 6: Typography & Content +- [ ] Apply text hierarchy (title, subtitle, desc) +- [ ] Truncate description jika terlalu panjang (max 2-3 baris) +- [ ] Ensure text contrast yang baik +- [ ] Test dengan text panjang + +### Task 7: Polish & Refinement +- [ ] Adjust spacing dan padding +- [ ] Test di light/dark mode (jika applicable) +- [ ] Test di berbagai ukuran layar (responsive) +- [ ] Add smooth transitions/animations + +### Task 8: Testing +- [ ] Test navigation ke setiap halaman +- [ ] Test di iOS simulator +- [ ] Test di Android emulator +- [ ] Test di device fisik (jika memungkinkan) +- [ ] Check performance (no lag saat scroll) + +--- + +## 💻 Implementation Guidelines + +### Code Structure Example +```typescript +// Constants +const CROWDFUNDING_MENU = [ + { + title: "Investasi", + desc: "Buat investasi dan jual beli saham lebih mudah dengan pengguna lain.", + path: "investment/(tabs)", + icon: "trending-up", + color: MainColor.blue, + gradient: ["#667eea", "#764ba2"], + }, + // ... +]; + +// Component +export default function Crowdfunding() { + const renderHeader = () => ( + + + + Crowdfunding + Platform Investasi & Donasi + + + ); + + const renderCard = (item: CrowdfundingMenuItem) => ( + + + + + + {item.title} + {item.desc} + + + + ); + + return ( + + + {renderHeader()} + {CROWDFUNDING_MENU.map(renderCard)} + + + ); +} +``` + +### Style Guidelines +```typescript +const styles = StyleSheet.create({ + headerContainer: { + position: 'relative', + borderRadius: 16, + overflow: 'hidden', + marginBottom: 20, + }, + headerOverlay: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + padding: 16, + backgroundColor: 'rgba(0,0,0,0.6)', + }, + card: { + backgroundColor: MainColor.white, + borderRadius: 16, + padding: 16, + marginBottom: 12, + elevation: 4, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + iconContainer: { + width: 48, + height: 48, + borderRadius: 24, + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, +}); +``` + +--- + +## ✅ Acceptance Criteria + +### Visual +- [ ] Header dengan overlay text yang readable +- [ ] Card dengan shadow/elevation yang jelas +- [ ] Icon untuk setiap menu item +- [ ] Color accent yang berbeda untuk Investasi & Donasi +- [ ] Typography hierarchy yang jelas + +### Functional +- [ ] Navigation ke halaman tujuan berfungsi +- [ ] Responsive di berbagai ukuran layar +- [ ] No console errors atau warnings +- [ ] Smooth scroll (60fps) + +### Code Quality +- [ ] Code terorganisir dengan baik +- [ ] Components terpisah untuk reusability +- [ ] Constants untuk data statis +- [ ] Comments untuk logic yang kompleks +- [ ] TypeScript types yang proper + +--- + +## 📊 Estimated Effort +- **Complexity**: Low-Medium +- **Time Estimate**: 2-3 jam +- **Risk Level**: Low (tidak ada breaking changes) + +--- + +## 🔗 References +- Design Reference: `screens/Forum/ViewBeranda3.tsx` (untuk pagination pattern) +- Color Palette: `constants/color-palet.ts` +- Icons: Feather Icons, Ionicons +- Components: `components/_ShareComponent/` + +--- + +## 📝 Notes +- Pastikan backward compatibility (tidak breaking existing features) +- Test di device dengan screen size berbeda +- Consider accessibility (text contrast, touch target size) +- Jika ada time, tambahkan micro-interactions (scale on press, dll) + +--- + +**Created**: 2026-03-25 +**Status**: Pending +**Priority**: Medium diff --git a/app/(application)/(user)/home.tsx b/app/(application)/(user)/home.tsx index e2c01c7..a693c3d 100644 --- a/app/(application)/(user)/home.tsx +++ b/app/(application)/(user)/home.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable react-hooks/exhaustive-deps */ -import { BasicWrapper, StackCustom, ViewWrapper } from "@/components"; +import { BasicWrapper, NewWrapper, StackCustom, ViewWrapper } from "@/components"; import AppHeader from "@/components/_ShareComponent/AppHeader"; import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom"; import { MainColor } from "@/constants/color-palet"; @@ -148,7 +148,7 @@ export default function Application() { }} /> - ) : ( - - - {Array.from({ length: 4 }).map((e, index) => ( - - ))} - - + null + // + // + // {Array.from({ length: 4 }).map((e, index) => ( + // + // ))} + // + // ) } > @@ -201,10 +202,10 @@ export default function Application() { {data ? ( ) : ( - + )} - + ); } diff --git a/app/(application)/(user)/portofolio/[id]/create.tsx b/app/(application)/(user)/portofolio/[id]/create.tsx index e9f2ae4..bc52b95 100644 --- a/app/(application)/(user)/portofolio/[id]/create.tsx +++ b/app/(application)/(user)/portofolio/[id]/create.tsx @@ -1,5 +1,5 @@ -import { Admin_ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate"; +import { ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate"; export default function PortofolioCreate() { - return ; + return ; } diff --git a/components/_ShareComponent/NewWrapper.tsx b/components/_ShareComponent/NewWrapper.tsx index 2d7d56d..3748ece 100644 --- a/components/_ShareComponent/NewWrapper.tsx +++ b/components/_ShareComponent/NewWrapper.tsx @@ -19,6 +19,7 @@ import { SafeAreaView, } from "react-native-safe-area-context"; import type { ScrollViewProps, FlatListProps } from "react-native"; +import Spacing from "./Spacing"; // --- ✅ Tambahkan refreshControl ke BaseProps --- interface BaseProps { @@ -111,7 +112,6 @@ const NewWrapper = (props: NewWrapperProps) => { return `${String(item.id)}-${index}`; }) } - refreshControl={refreshControl} // ✅ dari BaseProps onEndReached={listProps.onEndReached} onEndReachedThreshold={0.5} @@ -156,15 +156,27 @@ const NewWrapper = (props: NewWrapperProps) => { {headerComponent} )} - + + + {renderContainer(staticProps.children)} + + + + + {/* {renderContainer(staticProps.children)} - + */} {footerComponent ? ( @@ -205,6 +205,6 @@ export default function LoginView() { setLoadingTerm={setLoadingTerm} /> - + ); } diff --git a/screens/Forum/ViewBeranda3.tsx b/screens/Forum/ViewBeranda3.tsx index 2d8c551..c57de32 100644 --- a/screens/Forum/ViewBeranda3.tsx +++ b/screens/Forum/ViewBeranda3.tsx @@ -18,7 +18,7 @@ import _ from "lodash"; import { useEffect, useState } from "react"; import { RefreshControl, TouchableOpacity, View } from "react-native"; -const PAGE_SIZE = 5; +const PAGE_SIZE = 10; export default function Forum_ViewBeranda3() { const { user } = useAuth(); diff --git a/screens/Home/homeViewStyle.tsx b/screens/Home/homeViewStyle.tsx index dac67ff..8949ca6 100644 --- a/screens/Home/homeViewStyle.tsx +++ b/screens/Home/homeViewStyle.tsx @@ -94,7 +94,7 @@ export const stylesHome = StyleSheet.create({ jobVacancyHeader: { flexDirection: "row", alignItems: "center", - marginBottom: 16, + marginBottom: 20, }, jobVacancyTitle: { fontSize: 18, diff --git a/screens/Home/tabSection.tsx b/screens/Home/tabSection.tsx index 23c0a9f..0011a9b 100644 --- a/screens/Home/tabSection.tsx +++ b/screens/Home/tabSection.tsx @@ -5,7 +5,6 @@ import { router } from "expo-router"; import React from "react"; import { Text, TouchableOpacity, View } from "react-native"; - const CustomTab = ({ icon, label, isActive, onPress }: ICustomTab) => ( ( > @@ -30,8 +29,8 @@ const CustomTab = ({ icon, label, isActive, onPress }: ICustomTab) => ( export default function TabSection({ tabs }: { tabs: ITabs[] }) { return ( <> - - + + {tabs.map((e) => ( (null); const [inputValue, setInputValue] = useState(""); @@ -72,7 +72,7 @@ export function Admin_ScreenPortofolioCreate() { useCallback(() => { onLoadMaster(); onLoadMasterSubBidangBisnis(); - }, []) + }, []), ); const onLoadMaster = async () => { @@ -97,7 +97,7 @@ export function Admin_ScreenPortofolioCreate() { const handlerSelectedSubBidang = ({ id }: { id: string }) => { const selectedList = subBidangBisnis?.filter( - (item) => (item?.masterBidangBisnisId as any) === id + (item) => (item?.masterBidangBisnisId as any) === id, ); setSelectedSubBidang(selectedList as any[]); }; @@ -168,8 +168,7 @@ export function Admin_ScreenPortofolioCreate() { .filter((option: any) => { const selectedValues = listSubBidangSelected.map((s) => s.id); return ( - option.id === item.id || - !selectedValues.includes(option.id) + option.id === item.id || !selectedValues.includes(option.id) ); }) .map((e: any) => ({ @@ -188,7 +187,9 @@ export function Admin_ScreenPortofolioCreate() { { setListSubBidangSelected([ ...listSubBidangSelected, diff --git a/screens/UserSeach/MainView_V2.tsx b/screens/UserSeach/MainView_V2.tsx index 9632072..a21c3c4 100644 --- a/screens/UserSeach/MainView_V2.tsx +++ b/screens/UserSeach/MainView_V2.tsx @@ -12,6 +12,7 @@ import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE, } from "@/constants/constans-value"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { usePagination } from "@/hooks/use-pagination"; import { apiAllUser } from "@/service/api-client/api-user"; import { Ionicons } from "@expo/vector-icons"; @@ -19,21 +20,89 @@ import { router, useFocusEffect } from "expo-router"; import _ from "lodash"; import { useCallback, useRef, useState } from "react"; import { RefreshControl, View } from "react-native"; -import { createPaginationComponents } from "@/helpers/paginationHelpers"; + +const PAGE_SIZE = PAGINATION_DEFAULT_TAKE; + +/** + * Render header dengan search input + */ +const renderHeader = (search: string, setSearch: (text: string) => void) => ( + + } + placeholder="Cari Pengguna" + borderRadius={50} + containerStyle={{ marginBottom: 0 }} + /> +); + +/** + * Render item user + */ +const renderItem = ({ item }: { item: any }) => ( + + { + router.push(`/profile/${item?.Profile?.id}`); + }} + > + + + + + + + {item?.username} + +{item?.nomor} + {item?.Profile?.businessField && ( + + {item?.Profile?.businessField} + + )} + + + + + + + + +); export default function UserSearchMainView_V2() { const isInitialMount = useRef(true); const [search, setSearch] = useState(""); - const { - listData, - loading, - refreshing, - hasMore, - onRefresh, - loadMore, - isInitialLoad, - } = usePagination({ + const pagination = usePagination({ fetchFunction: async (page, searchQuery) => { const response = await apiAllUser({ page: String(page), @@ -41,7 +110,7 @@ export default function UserSearchMainView_V2() { }); return response; }, - pageSize: PAGINATION_DEFAULT_TAKE, + pageSize: PAGE_SIZE, searchQuery: search, }); @@ -49,119 +118,42 @@ export default function UserSearchMainView_V2() { useFocusEffect( useCallback(() => { if (isInitialMount.current) { - // Skip saat pertama kali mount isInitialMount.current = false; return; } - // Hanya refresh saat kembali dari screen lain - onRefresh(); - }, [onRefresh]), - ); - - const renderHeader = () => ( - <> - - } - placeholder="Cari Pengguna" - borderRadius={50} - containerStyle={{ marginBottom: 0 }} - /> - - ); - - const renderItem = ({ item }: { item: any }) => ( - - { - console.log("Ke Profile"); - router.push(`/profile/${item?.Profile?.id}`); - }} - > - - - - - - - {item?.username} - +{item?.nomor} - {item?.Profile?.businessField && ( - - {item?.Profile?.businessField} - - )} - - - - - - - - + pagination.onRefresh(); + }, [pagination.onRefresh]), ); const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ - loading, - refreshing, - listData, + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, searchQuery: search, emptyMessage: "Tidak ada pengguna ditemukan", emptySearchMessage: "Tidak ada hasil pencarian", skeletonCount: PAGINATION_DEFAULT_TAKE, skeletonHeight: 100, loadingFooterText: "Memuat lebih banyak pengguna...", - isInitialLoad, + isInitialLoad: pagination.isInitialLoad, }); return ( - <> - - } - ListFooterComponent={ListFooterComponent} - ListEmptyComponent={ListEmptyComponent} - /> - + + } + ListFooterComponent={ListFooterComponent} + ListEmptyComponent={ListEmptyComponent} + /> ); } diff --git a/service/api-client/api-user.ts b/service/api-client/api-user.ts index 8cd3155..f1ffab1 100644 --- a/service/api-client/api-user.ts +++ b/service/api-client/api-user.ts @@ -6,13 +6,13 @@ export async function apiUser(id: string) { } export async function apiAllUser({ - page, + page = "1", search, }: { page?: string; search?: string; }) { - const pageQuery = page ? `?page=${page}` : ""; + const pageQuery = `?page=${page}`; const searchQuery = search ? `&search=${search}` : ""; try { diff --git a/styles/global-styles.ts b/styles/global-styles.ts index 30f8781..efdd806 100644 --- a/styles/global-styles.ts +++ b/styles/global-styles.ts @@ -159,7 +159,7 @@ export const GStyles = StyleSheet.create({ transform: [{ scale: 1.05 }], }, iconContainer: { - padding: 8, + padding: 5, borderRadius: 20, // marginBottom: 4, }, diff --git a/styles/tabs-styles.ts b/styles/tabs-styles.ts index 70fe42b..4788d93 100644 --- a/styles/tabs-styles.ts +++ b/styles/tabs-styles.ts @@ -11,7 +11,7 @@ export const TabsStyles: BottomTabNavigationOptions = { tabBarStyle: Platform.select({ ios: { borderTopWidth: 0, - paddingTop: 5, + paddingTop: 12, height: OS_IOS_HEIGHT, }, android: {