From b3bfbc0f7e97e386b747369c04a6b6b052f730c1 Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Thu, 29 Jan 2026 11:36:24 +0800 Subject: [PATCH 1/4] Fix Infinite Load Data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forum & User Search – User - app/(application)/(user)/forum/index.tsx - app/(application)/(user)/user-search/index.tsx Global & Core - app/+not-found.tsx - screens/RootLayout/AppRoot.tsx - constants/constans-value.ts Component - components/Image/AvatarComp.tsx API Client - service/api-client/api-user.ts Untracked Files - QWEN.md - helpers/ - hooks/use-pagination.tsx - screens/Forum/ViewBeranda3.tsx - screens/UserSeach/ ### No Issue --- QWEN.md | 179 ++++++ app/(application)/(user)/forum/index.tsx | 4 +- .../(user)/user-search/index.tsx | 112 +--- app/+not-found.tsx | 4 +- components/Image/AvatarComp.tsx | 2 +- constants/constans-value.ts | 3 + helpers/PaginationGuide.md | 517 ++++++++++++++++++ helpers/paginationHelpers.tsx | 280 ++++++++++ hooks/use-pagination.tsx | 184 +++++++ screens/Forum/ViewBeranda3.tsx | 132 +++++ screens/RootLayout/AppRoot.tsx | 15 +- screens/UserSeach/MainView.tsx | 105 ++++ screens/UserSeach/MainView_V2.tsx | 174 ++++++ service/api-client/api-user.ts | 36 +- 14 files changed, 1626 insertions(+), 121 deletions(-) create mode 100644 QWEN.md create mode 100644 helpers/PaginationGuide.md create mode 100644 helpers/paginationHelpers.tsx create mode 100644 hooks/use-pagination.tsx create mode 100644 screens/Forum/ViewBeranda3.tsx create mode 100644 screens/UserSeach/MainView.tsx create mode 100644 screens/UserSeach/MainView_V2.tsx diff --git a/QWEN.md b/QWEN.md new file mode 100644 index 0000000..322c6f0 --- /dev/null +++ b/QWEN.md @@ -0,0 +1,179 @@ +# HIPMI Mobile Application - Development Guide + +## Project Overview + +HIPMI Badung Connect is a mobile application built with Expo and React Native. It serves as a connection platform for HIPMI (Himpunan Pengusaha Muda Indonesia) Badung members, featuring authentication, user management, and various business-related functionalities. + +### Key Technologies +- **Framework**: Expo (v54.0.0) with React Native (0.81.4) +- **Architecture**: File-based routing with Expo Router +- **State Management**: React Context API +- **Styling**: React Native components with custom color palettes +- **Authentication**: Token-based authentication with OTP verification +- **Database**: AsyncStorage for local storage +- **Maps**: React Native Maps and Mapbox integration +- **Notifications**: Expo Notifications and Firebase Messaging +- **Language**: TypeScript + +### Project Structure +``` +hipmi-mobile/ +├── app/ # File-based routing structure +│ ├── (application)/ # Main application screens +│ │ ├── (file)/ # File management screens +│ │ ├── (image)/ # Image management screens +│ │ ├── (user)/ # User-specific screens +│ │ └── admin/ # Admin-specific screens +│ ├── _layout.tsx # Root layout wrapper +│ ├── index.tsx # Home screen +│ ├── eula.tsx # Terms and conditions screen +│ ├── register.tsx # Registration screen +│ └── verification.tsx # OTP verification screen +├── assets/ # Static assets (images, icons) +├── components/ # Reusable UI components +├── constants/ # Configuration constants +├── context/ # React Context providers +├── hooks/ # Custom React hooks +├── screens/ # Screen components +├── service/ # API services and configurations +├── types/ # TypeScript type definitions +├── app.config.js # Expo configuration +├── package.json # Dependencies and scripts +└── ... +``` + +## Building and Running + +### Prerequisites +- Node.js (with bun >=1.0.0 as specified in package.json) +- Expo CLI or bun installed globally + +### Setup Instructions +1. **Install dependencies**: + ```bash + bun install + # or if using npm + npm install + ``` + +2. **Environment Variables**: + Create a `.env` file with the following variables: + ``` + API_BASE_URL=your_api_base_url + BASE_URL=your_base_url + DEEP_LINK_URL=your_deep_link_url + ``` + +3. **Start the development server**: + ```bash + # Using bun (as specified in package.json) + bun run start + # or using expo directly + npx expo start + ``` + +4. **Platform-specific commands**: + ```bash + # Android + bun run android + # iOS + bun run ios + # Web + bun run web + ``` + +### EAS Build Configuration +The project uses Expo Application Services (EAS) for building and deployment: +- Development builds: `eas build --profile development` +- Preview builds: `eas build --profile preview` +- Production builds: `eas build --profile production` + +## Authentication Flow + +The application implements a phone number-based authentication system with OTP verification: + +1. **Login**: User enters phone number → OTP sent via SMS +2. **Verification**: User enters OTP code → Validates and creates session +3. **Registration**: If user doesn't exist, registration flow is triggered +4. **Terms Agreement**: User must accept terms and conditions +5. **Session Management**: Tokens stored in AsyncStorage + +### Key Authentication Functions +- `loginWithNomor()`: Initiates OTP sending +- `validateOtp()`: Verifies OTP and creates session +- `registerUser()`: Registers new users +- `logout()`: Clears session and removes tokens +- `acceptedTerms()`: Handles terms acceptance + +## Key Features + +### User Management +- Phone number-based registration and login +- OTP verification system +- Terms and conditions agreement +- User profile management + +### Business Features +- Business field management (admin section) +- File and image management capabilities +- Location services integration +- Push notifications + +### UI Components +- Custom color palette with blue/yellow theme +- Responsive layouts using SafeAreaView +- Toast notifications for user feedback +- Bottom tab navigation and drawer navigation + +## Development Conventions + +### Naming Conventions +- Components: PascalCase (e.g., `UserProfile.tsx`) +- Functions: camelCase (e.g., `getUserData()`) +- Constants: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`) +- Files: kebab-case or camelCase for utility files + +### Code Organization +- Components are organized by feature/functionality +- API services are centralized in the `service/` directory +- Type definitions are maintained in the `types/` directory +- Constants are grouped by category in the `constants/` directory + +### Styling Approach +- Color palette defined in `constants/color-palet.ts` +- Reusable styles and themes centralized +- Responsive design using React Native's flexbox system + +### Testing +- Linting: `bun run lint` (uses ESLint with Expo config) +- No specific test framework mentioned in package.json + +## Environment Configuration + +The application supports multiple environments through: +- Environment variables loaded via dotenv +- Expo's extra configuration in `app.config.js` +- Platform-specific configurations for iOS and Android + +### Supported Platforms +- iOS (with tablet support) +- Android (with adaptive icons) +- Web (static output) + +## Third-party Integrations + +- **Firebase**: Authentication, messaging, and analytics +- **Mapbox**: Advanced mapping capabilities +- **React Navigation**: Screen navigation and routing +- **React Native Paper**: Material Design components +- **Axios**: HTTP client for API requests +- **Lodash**: Utility functions +- **QR Code SVG**: QR code generation + +## Important Configuration Files + +- `app.config.js`: Expo configuration, app metadata, and plugin setup +- `eas.json`: EAS build profiles and submission configuration +- `tsconfig.json`: TypeScript compiler options +- `package.json`: Dependencies, scripts, and project metadata +- `metro.config.js`: Metro bundler configuration \ No newline at end of file diff --git a/app/(application)/(user)/forum/index.tsx b/app/(application)/(user)/forum/index.tsx index a4fe51e..fadbd76 100644 --- a/app/(application)/(user)/forum/index.tsx +++ b/app/(application)/(user)/forum/index.tsx @@ -1,12 +1,14 @@ /* eslint-disable react-hooks/exhaustive-deps */ import Forum_ViewBeranda from "@/screens/Forum/ViewBeranda"; import Forum_ViewBeranda2 from "@/screens/Forum/ViewBeranda2"; +import Forum_ViewBeranda3 from "@/screens/Forum/ViewBeranda3"; export default function Forum() { return ( <> {/* */} - + {/* */} + ); } diff --git a/app/(application)/(user)/user-search/index.tsx b/app/(application)/(user)/user-search/index.tsx index b2880b9..d7de2c7 100644 --- a/app/(application)/(user)/user-search/index.tsx +++ b/app/(application)/(user)/user-search/index.tsx @@ -1,115 +1,11 @@ -import { - AvatarComp, - ClickableCustom, - Grid, - LoaderCustom, - Spacing, - StackCustom, - TextCustom, - TextInputCustom, - ViewWrapper, -} from "@/components"; -import { MainColor } from "@/constants/color-palet"; -import { ICON_SIZE_SMALL } from "@/constants/constans-value"; -import { apiAllUser } from "@/service/api-client/api-user"; -import { Ionicons } from "@expo/vector-icons"; -import { router } from "expo-router"; -import _ from "lodash"; -import { useEffect, useState } from "react"; +import UserSearchMainView from "@/screens/UserSeach/MainView"; +import UserSearchMainView_V2 from "@/screens/UserSeach/MainView_V2"; export default function UserSearch() { - const [data, setData] = useState([]); - const [search, setSearch] = useState(""); - const [isLoadList, setIsLoadList] = useState(false); - - useEffect(() => { - onLoadData(search); - }, [search]); - - const onLoadData = async (search: string) => { - try { - setIsLoadList(true); - const response = await apiAllUser({ search: search }); - console.log("[DATA USER] >", JSON.stringify(response.data, null, 2)); - setData(response.data); - } catch (error) { - console.log("Error fetching data", error); - } finally { - setIsLoadList(false); - } - }; - - const handleSearch = (search: string) => { - setSearch(search); - onLoadData(search); - }; - return ( <> - - } - placeholder="Cari Pengguna" - borderRadius={50} - containerStyle={{ marginBottom: 0 }} - /> - } - > - - {isLoadList ? ( - - ) : !_.isEmpty(data) ? ( - data?.map((e, index) => { - return ( - { - console.log("Ke Profile"); - router.push(`/profile/${e?.Profile?.id}`); - }} - > - - - - - - - {e?.username} - +{e?.nomor} - - - - - - - - ); - }) - ) : ( - Tidak ditemukan - )} - - - + {/* */} + ); } diff --git a/app/+not-found.tsx b/app/+not-found.tsx index 8356007..20371a6 100644 --- a/app/+not-found.tsx +++ b/app/+not-found.tsx @@ -5,7 +5,7 @@ export default function NotFoundScreen() { return ( <> }} + options={{ headerShown: true, title: "", headerLeft: () => }} /> - Sorry, File Not Found + Sorry, Page Not Found diff --git a/components/Image/AvatarComp.tsx b/components/Image/AvatarComp.tsx index 7936591..59b9542 100644 --- a/components/Image/AvatarComp.tsx +++ b/components/Image/AvatarComp.tsx @@ -52,7 +52,7 @@ export default function AvatarComp({ router.navigate(href as any) : onPress + href || fileId ? () => router.navigate(href as any) : onPress } > {avatarImage()} diff --git a/constants/constans-value.ts b/constants/constans-value.ts index 760ce98..1cef856 100644 --- a/constants/constans-value.ts +++ b/constants/constans-value.ts @@ -19,6 +19,7 @@ export { PADDING_SMALL, PADDING_MEDIUM, PADDING_LARGE, + PAGINATION_DEFAULT_TAKE }; // OS Height @@ -51,3 +52,5 @@ const PADDING_SMALL = 12 const PADDING_MEDIUM = 16 const PADDING_LARGE = 20 +// Pagination +const PAGINATION_DEFAULT_TAKE = 10; diff --git a/helpers/PaginationGuide.md b/helpers/PaginationGuide.md new file mode 100644 index 0000000..38f2b55 --- /dev/null +++ b/helpers/PaginationGuide.md @@ -0,0 +1,517 @@ +# 📱 Reusable Pagination untuk React Native + Expo + +Komponen pagination yang terintegrasi dengan **NewWrapper** untuk infinite scroll, pull-to-refresh, skeleton loading, dan empty state. + +--- + +## 📦 File Structure + +``` +/hooks/ + └── usePagination.tsx # Custom hook untuk logika pagination + +/helpers/ + └── paginationHelpers.tsx # Helper functions untuk komponen UI + +/components/ + └── NewWrapper.tsx # Komponen wrapper utama (existing) +``` + +--- + +## 🚀 Cara Penggunaan + +### **Step 1: Import Hook dan Helpers** + +```tsx +import { usePagination } from "@/hooks/usePagination"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +``` + +### **Step 2: Setup Pagination Hook** + +```tsx +const pagination = usePagination({ + // ✅ Fungsi untuk fetch data (harus return { data: T[] }) + fetchFunction: async (page, searchQuery) => { + return await apiForumGetAll({ + category: "beranda", + search: searchQuery || "", + userLoginId: user.id, + page: String(page), + }); + }, + + // ✅ Page size (harus sama dengan API) + pageSize: 5, + + // ✅ Query pencarian + searchQuery: search, + + // ✅ Dependencies (reload saat berubah) + dependencies: [user?.id, category], + + // ⚙️ Optional callbacks + onDataFetched: (data) => console.log("Loaded:", data.length), + onError: (error) => console.error("Error:", error), +}); +``` + +### **Step 3: Generate Komponen Pagination** + +```tsx +const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + searchQuery: search, + emptyMessage: "Tidak ada data", + emptySearchMessage: "Tidak ada hasil pencarian", + skeletonCount: 5, + skeletonHeight: 200, +}); +``` + +### **Step 4: Gunakan dengan NewWrapper** + +```tsx + + } + onEndReached={pagination.loadMore} + + // Komponen dari helpers + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + + // Render item + renderItem={({ item }) => } + + // Props lain dari NewWrapper + headerComponent={} + floatingButton={} +/> +``` + +--- + +## 📖 Contoh Implementasi Lengkap + +### **Contoh 1: Forum Page (Basic)** + +```tsx +import { usePagination } from "@/hooks/usePagination"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +import { MainColor } from "@/constants/color-palet"; + +export default function ForumPage() { + const { user } = useAuth(); + const [search, setSearch] = useState(""); + + // Setup pagination + const pagination = usePagination({ + fetchFunction: async (page, searchQuery) => { + if (!user?.id) return { data: [] }; + + return await apiForumGetAll({ + category: "beranda", + search: searchQuery || "", + userLoginId: user.id, + page: String(page), + }); + }, + pageSize: 5, + searchQuery: search, + dependencies: [user?.id], + }); + + // Generate komponen + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + searchQuery: search, + emptyMessage: "Tidak ada diskusi", + emptySearchMessage: "Tidak ada hasil pencarian", + }); + + return ( + + } + listData={pagination.listData} + renderItem={({ item }) => } + refreshControl={ + + } + onEndReached={pagination.loadMore} + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + /> + ); +} +``` + +### **Contoh 2: Product Page (Dengan Filter)** + +```tsx +export default function ProductPage() { + const [search, setSearch] = useState(""); + const [category, setCategory] = useState("all"); + + const pagination = usePagination({ + fetchFunction: async (page, searchQuery) => { + return await apiProductGetAll({ + page: String(page), + search: searchQuery || "", + category: category !== "all" ? category : undefined, + }); + }, + pageSize: 10, + searchQuery: search, + dependencies: [category], // Reload saat category berubah + }); + + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + searchQuery: search, + emptyMessage: "Belum ada produk", + skeletonCount: 8, + skeletonHeight: 100, + }); + + return ( + + + + + } + listData={pagination.listData} + renderItem={({ item }) => } + refreshControl={ + + } + onEndReached={pagination.loadMore} + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + /> + ); +} +``` + +--- + +## ⚙️ API Reference + +### **usePagination Hook** + +#### Props + +| Prop | Type | Required | Default | Deskripsi | +|------|------|----------|---------|-----------| +| `fetchFunction` | `(page, search?) => Promise<{data: T[]}>` | ✅ | - | Fungsi fetch data dari API | +| `pageSize` | `number` | ❌ | `5` | Jumlah data per halaman | +| `searchQuery` | `string` | ❌ | `""` | Query pencarian | +| `dependencies` | `any[]` | ❌ | `[]` | Dependencies untuk trigger reload | +| `onDataFetched` | `(data: T[]) => void` | ❌ | - | Callback saat data berhasil di-fetch | +| `onError` | `(error: any) => void` | ❌ | - | Callback saat terjadi error | + +#### Return Value + +```tsx +{ + listData: T[]; // Array data untuk NewWrapper + loading: boolean; // Loading state + refreshing: boolean; // Refreshing state + hasMore: boolean; // Apakah masih ada data + page: number; // Current page + onRefresh: () => void; // Function untuk refresh + loadMore: () => void; // Function untuk load more + reset: () => void; // Function untuk reset state + setListData: (data) => void; // Function untuk set data manual +} +``` + +--- + +### **createPaginationComponents Helper** + +#### Props + +| Prop | Type | Required | Default | Deskripsi | +|------|------|----------|---------|-----------| +| `loading` | `boolean` | ✅ | - | Loading state | +| `refreshing` | `boolean` | ✅ | - | Refreshing state | +| `listData` | `any[]` | ✅ | - | List data | +| `searchQuery` | `string` | ❌ | `""` | Query pencarian | +| `emptyMessage` | `string` | ❌ | `"Tidak ada data"` | Pesan empty state | +| `emptySearchMessage` | `string` | ❌ | `"Tidak ada hasil pencarian"` | Pesan empty saat search | +| `skeletonCount` | `number` | ❌ | `5` | Jumlah skeleton items | +| `skeletonHeight` | `number` | ❌ | `200` | Tinggi skeleton items | +| `loadingFooterText` | `string` | ❌ | - | Text loading footer | + +#### Return Value + +```tsx +{ + ListEmptyComponent: React.ReactElement; // Component untuk empty state + ListFooterComponent: React.ReactElement; // Component untuk loading footer +} +``` + +--- + +### **Helper Functions Lain** + +#### `createSkeletonList(options)` +Generate skeleton list untuk loading state. + +```tsx +const SkeletonComponent = createSkeletonList({ + count: 5, + height: 200 +}); +``` + +#### `createEmptyState(options)` +Generate empty state component. + +```tsx +const EmptyComponent = createEmptyState({ + message: "Tidak ada data", + searchMessage: "Tidak ada hasil pencarian", + searchQuery: search +}); +``` + +#### `createLoadingFooter(options)` +Generate loading footer component. + +```tsx +const FooterComponent = createLoadingFooter({ + show: loading && listData.length > 0, + text: "Memuat data..." +}); +``` + +--- + +## 🎨 Custom Components + +### **Custom Empty State** + +```tsx +import { createSkeletonList } from "@/helpers/paginationHelpers"; + +const CustomEmpty = ( + + 🔍 + Data tidak ditemukan + +); + +const ListEmptyComponent = + pagination.loading && pagination.listData.length === 0 + ? createSkeletonList({ count: 5, height: 200 }) + : CustomEmpty; + + +``` + +### **Custom Loading Footer** + +```tsx +import { createLoadingFooter } from "@/helpers/paginationHelpers"; + +const CustomFooter = createLoadingFooter({ + show: pagination.loading && !pagination.refreshing && pagination.listData.length > 0, + customComponent: ( + + + Loading more... + + ) +}); + + +``` + +--- + +## ✨ Fitur-Fitur + +✅ **Infinite Scroll** - Auto load saat scroll ke bawah +✅ **Pull to Refresh** - Swipe down untuk refresh +✅ **Skeleton Loading** - Smooth loading animation +✅ **Empty State** - Tampilan saat data kosong +✅ **Search Integration** - Support search dengan debounce +✅ **Multi Dependencies** - Reload berdasarkan filter apapun +✅ **Error Handling** - Built-in error handling +✅ **TypeScript** - Full type safety +✅ **Fully Customizable** - Custom components untuk semua state + +--- + +## 🎯 Best Practices + +### 1. **Gunakan Debounce untuk Search** +```tsx + setSearch(text), 500)} +/> +``` + +### 2. **Sesuaikan Page Size dengan API** +```tsx +const pagination = usePagination({ + pageSize: 5, // Harus sama dengan takeData di API +}); +``` + +### 3. **Tambahkan Dependencies yang Relevan** +```tsx +const pagination = usePagination({ + dependencies: [userId, category, sortBy], // Reload saat berubah +}); +``` + +### 4. **Handle Error dengan Baik** +```tsx +const pagination = usePagination({ + onError: (error) => { + console.error("Error:", error); + Alert.alert("Error", "Gagal memuat data"); + }, +}); +``` + +### 5. **Pastikan API Return Format yang Benar** +```tsx +// ❌ SALAH +fetchFunction: async () => [data1, data2]; + +// ✅ BENAR +fetchFunction: async () => ({ data: [data1, data2] }); +``` + +--- + +## 🔧 Troubleshooting + +### **Data tidak muncul?** +- Pastikan `fetchFunction` return `{ data: T[] }` +- Cek apakah API return format yang benar +- Pastikan `pageSize` sesuai dengan API + +### **Infinite scroll tidak jalan?** +- Pastikan API return data sesuai `pageSize` +- Cek `hasMore` state +- Pastikan `onEndReachedThreshold` tidak terlalu kecil (default 0.5) + +### **Skeleton terus muncul?** +- Cek `loading` state +- Pastikan `fetchFunction` resolve dengan benar +- Cek error di console + +### **Refresh tidak bekerja?** +- Pastikan `RefreshControl` menggunakan `pagination.refreshing` dan `pagination.onRefresh` +- Cek apakah API dipanggil saat pull-to-refresh + +--- + +## 📝 Migration Guide + +### **Dari Code Lama ke Code Baru** + +#### **BEFORE:** +```tsx +const [listData, setListData] = useState([]); +const [loading, setLoading] = useState(false); +const [refreshing, setRefreshing] = useState(false); +const [hasMore, setHasMore] = useState(true); +const [page, setPage] = useState(1); + +const fetchData = async (pageNumber, clear) => { + // ... 30+ lines of code +}; + +useEffect(() => { + setPage(1); + setListData([]); + setHasMore(true); + fetchData(1, true); +}, [search, user?.id]); + +const onRefresh = useCallback(() => { + fetchData(1, true); +}, [search, user?.id]); + +const loadMore = useCallback(() => { + if (hasMore && !loading && !refreshing) { + fetchData(page + 1, false); + } +}, [hasMore, loading, refreshing, page, search, user?.id]); + +// ... skeleton, empty, footer components +``` + +#### **AFTER:** +```tsx +const pagination = usePagination({ + fetchFunction: async (page, search) => await apiGetData({ page, search }), + pageSize: 5, + searchQuery: search, + dependencies: [user?.id] +}); + +const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + searchQuery: search, +}); +``` + +**Result:** 50+ lines → 15 lines! 🎉 + +--- + +## 👨‍💻 Author + +Created by Full-Stack Developer +React Native + Expo Specialist + +--- + +## 📄 License + +MIT License - Feel free to use in your projects! \ No newline at end of file diff --git a/helpers/paginationHelpers.tsx b/helpers/paginationHelpers.tsx new file mode 100644 index 0000000..a547746 --- /dev/null +++ b/helpers/paginationHelpers.tsx @@ -0,0 +1,280 @@ +import { View } from "react-native"; +import { LoaderCustom, TextCustom, StackCustom } from "@/components"; +import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom"; +import _ from "lodash"; + +/** + * Pagination Helpers + * + * Helper functions untuk membuat komponen-komponen pagination + * yang sering digunakan (Skeleton, Empty State, Loading Footer) + */ + +interface SkeletonListOptions { + /** + * Jumlah skeleton items + * @default 5 + */ + count?: number; + + /** + * Tinggi setiap skeleton item + * @default 200 + */ + height?: number; +} + +/** + * Generate Skeleton List Component untuk loading state + * + * @example + * ```tsx + * + * ``` + */ +export const createSkeletonList = (options: SkeletonListOptions = {}) => { + const { count = 5, height = 200 } = options; + + return ( + + + {Array.from({ length: count }).map((_, i) => ( + + ))} + + + ); +}; + +interface EmptyStateOptions { + /** + * Pesan untuk empty state + * @default "Tidak ada data" + */ + message?: string; + + /** + * Pesan untuk empty state saat search + */ + searchMessage?: string; + + /** + * Query pencarian (untuk menentukan pesan mana yang ditampilkan) + */ + searchQuery?: string; + + /** + * Custom component untuk empty state + */ + customComponent?: React.ReactElement; +} + +/** + * Generate Empty State Component + * + * @example + * ```tsx + * ListEmptyComponent={ + * createEmptyState({ + * message: "Tidak ada diskusi", + * searchMessage: "Tidak ada hasil pencarian", + * searchQuery: search + * }) + * } + * ``` + */ +export const createEmptyState = (options: EmptyStateOptions = {}) => { + const { + message = "Tidak ada data", + searchMessage = "Tidak ada hasil pencarian", + searchQuery = "", + customComponent, + } = options; + + if (customComponent) return customComponent; + + return ( + + + {searchQuery ? searchMessage : message} + + + ); +}; + +interface LoadingFooterOptions { + /** + * Tampilkan loading footer + */ + show: boolean; + + /** + * Custom text untuk loading footer + */ + text?: string; + + /** + * Custom component untuk loading footer + */ + customComponent?: React.ReactElement; +} + +/** + * Generate Loading Footer Component + * + * @example + * ```tsx + * ListFooterComponent={ + * createLoadingFooter({ + * show: loading && !refreshing && listData.length > 0, + * text: "Memuat data..." + * }) + * } + * ``` + */ +export const createLoadingFooter = (options: LoadingFooterOptions) => { + const { show, text, customComponent } = options; + + if (!show) return null; + + if (customComponent) return customComponent; + + return ( + + {text ? ( + + {text} + + ) : ( + + )} + + ); +}; + +interface PaginationComponentsOptions { + /** + * Loading state + */ + loading: boolean; + + /** + * Refreshing state + */ + refreshing: boolean; + + /** + * List data + */ + listData: any[]; + + /** + * Query pencarian + */ + searchQuery?: string; + + /** + * Pesan empty state + */ + emptyMessage?: string; + + /** + * Pesan empty state saat search + */ + emptySearchMessage?: string; + + /** + * Jumlah skeleton items + */ + skeletonCount?: number; + + /** + * Tinggi skeleton items + */ + skeletonHeight?: number; + + /** + * Text loading footer + */ + loadingFooterText?: string; + + /** + * Loading pertama + */ + isInitialLoad?: boolean; +} + +/** + * Generate semua komponen pagination sekaligus + * + * @example + * ```tsx + * const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + * loading, + * refreshing, + * listData, + * searchQuery: search, + * emptyMessage: "Tidak ada diskusi", + * emptySearchMessage: "Tidak ada hasil pencarian", + * skeletonCount: 5, + * skeletonHeight: 200 + * }); + * + * + * ``` + */ +export const createPaginationComponents = ( + options: PaginationComponentsOptions +) => { + const { + loading, + refreshing, + listData, + searchQuery = "", + emptyMessage = "Tidak ada data", + emptySearchMessage = "Tidak ada hasil pencarian", + skeletonCount = 5, + skeletonHeight = 200, + loadingFooterText, + isInitialLoad, + } = options; + + // Empty Compotnent: Skeleton saat loading pertama, Empty State saat data kosong + const ListEmptyComponent = + loading && _.isEmpty(listData) + ? createSkeletonList({ count: skeletonCount, height: skeletonHeight }) + : createEmptyState({ + message: emptyMessage, + searchMessage: emptySearchMessage, + searchQuery, + }); + + // Footer Component: Loading indicator saat load more + const ListFooterComponent = createLoadingFooter({ + show: loading && !refreshing && listData.length > 0, + text: loadingFooterText, + }); + + return { + ListEmptyComponent, + ListFooterComponent, + }; +}; \ No newline at end of file diff --git a/hooks/use-pagination.tsx b/hooks/use-pagination.tsx new file mode 100644 index 0000000..3b9a3a7 --- /dev/null +++ b/hooks/use-pagination.tsx @@ -0,0 +1,184 @@ +import { useState, useCallback, useEffect } from "react"; + +interface UsePaginationProps { + /** + * Fungsi API untuk fetch data + * @param page - nomor halaman + * @param search - query pencarian (opsional) + * @returns Promise dengan response API (bukan langsung array) + */ + fetchFunction: (page: number, search?: string) => Promise<{ data: T[] }>; + + /** + * Jumlah data per halaman (harus sama dengan API) + * @default 5 + */ + pageSize?: number; + + /** + * Query pencarian + */ + searchQuery?: string; + + /** + * Dependencies tambahan untuk trigger reload + * Contoh: [userId, categoryId] + */ + dependencies?: any[]; + + /** + * Callback saat data berhasil di-fetch + */ + onDataFetched?: (data: T[]) => void; + + /** + * Callback saat terjadi error + */ + onError?: (error: any) => void; +} + +interface UsePaginationReturn { + // Data state + listData: T[]; + loading: boolean; + refreshing: boolean; + hasMore: boolean; + page: number; + + // Actions + onRefresh: () => void; + loadMore: () => void; + reset: () => void; + setListData: React.Dispatch>; + isInitialLoad: boolean; +} + +/** + * Custom Hook untuk menangani pagination dengan infinite scroll + * + * Hook ini mengembalikan props yang siap digunakan langsung dengan NewWrapper + * + * @example + * ```tsx + * const pagination = usePagination({ + * fetchFunction: async (page, search) => { + * return await apiForumGetAll({ + * category: "beranda", + * search: search || "", + * userLoginId: user.id, + * page: String(page), + * }); + * }, + * pageSize: 5, + * searchQuery: search, + * dependencies: [user?.id] + * }); + * + * // Lalu gunakan langsung di NewWrapper: + * } + * onEndReached={pagination.loadMore} + * // ... props lainnya + * /> + * ``` + */ +export function usePagination({ + fetchFunction, + pageSize = 5, + searchQuery = "", + dependencies = [], + onDataFetched, + onError, +}: UsePaginationProps): UsePaginationReturn { + const [listData, setListData] = useState([]); + const [loading, setLoading] = useState(true); // Set true untuk initial load + const [isInitialLoad, setIsInitialLoad] = useState(true); // Track initial load + const [refreshing, setRefreshing] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + + /** + * Fungsi utama untuk fetch data + */ + const fetchData = async (pageNumber: number, clear: boolean) => { + // Cegah multiple call + if (!clear && (loading || refreshing)) return; + + const isRefresh = clear; + if (isRefresh) setRefreshing(true); + if (!isRefresh) setLoading(true); + + try { + const response = await fetchFunction(pageNumber, searchQuery); + const newData = response.data || []; + // console.log("newData", newData); + setListData((prev) => { + const current = Array.isArray(prev) ? prev : []; + return clear ? newData : [...current, ...newData]; + }); + // setTimeout(() => { + // }, 4000); + + setHasMore(newData.length === pageSize); + setPage(pageNumber); + + // Callback jika ada + onDataFetched?.(newData); + } catch (error) { + console.error("[usePagination] Error fetching data:", error); + setHasMore(false); + onError?.(error); + } finally { + setRefreshing(false); + setLoading(false); + setIsInitialLoad(false); // Set false setelah initial load + } + }; + + /** + * Reset dan reload saat search atau dependencies berubah + */ + useEffect(() => { + reset(); + fetchData(1, true); + }, [searchQuery, ...dependencies]); + + /** + * Pull-to-refresh + */ + const onRefresh = useCallback(() => { + fetchData(1, true); + }, [searchQuery, ...dependencies]); + + /** + * Load more (infinite scroll) + */ + const loadMore = useCallback(() => { + if (hasMore && !loading && !refreshing) { + fetchData(page + 1, false); + } + }, [hasMore, loading, refreshing, page, searchQuery, ...dependencies]); + + /** + * Reset state pagination + */ + const reset = useCallback(() => { + setPage(1); + setListData([]); + setHasMore(true); + }, []); + + return { + listData, + loading, + refreshing, + hasMore, + page, + onRefresh, + loadMore, + reset, + setListData, + isInitialLoad + }; +} diff --git a/screens/Forum/ViewBeranda3.tsx b/screens/Forum/ViewBeranda3.tsx new file mode 100644 index 0000000..997e3fd --- /dev/null +++ b/screens/Forum/ViewBeranda3.tsx @@ -0,0 +1,132 @@ +import { + AvatarComp, + BackButton, + FloatingButton, + SearchInput, + TextCustom, +} from "@/components"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +import { useAuth } from "@/hooks/use-auth"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import { apiForumGetAll } from "@/service/api-client/api-forum"; +import { apiUser } from "@/service/api-client/api-user"; +import { router, Stack } from "expo-router"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { RefreshControl, View } from "react-native"; +import { MainColor } from "@/constants/color-palet"; +import { usePagination } from "@/hooks/use-pagination"; + +const PAGE_SIZE = 5; + +export default function Forum_ViewBeranda3() { + const { user } = useAuth(); + const [dataUser, setDataUser] = useState(null); + const [search, setSearch] = useState(""); + + // Load data profil user + useEffect(() => { + if (user?.id) { + apiUser(user.id).then((res) => setDataUser(res.data)); + } + }, [user?.id]); + + // Setup pagination (menggantikan 50+ lines code!) + const pagination = usePagination({ + fetchFunction: async (page, searchQuery) => { + if (!user?.id) return { data: [] }; + + return await apiForumGetAll({ + category: "beranda", + search: searchQuery || "", + userLoginId: user.id, + page: String(page), + }); + }, + pageSize: PAGE_SIZE, + searchQuery: search, + dependencies: [user?.id], + onError: (error) => console.error("[ERROR] Fetch forum:", error), + }); + + // Generate komponen (menggantikan 40+ lines code!) + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + searchQuery: search, + emptyMessage: "Tidak ada diskusi", + emptySearchMessage: "Tidak ada hasil pencarian", + skeletonCount: 5, + skeletonHeight: 150, + }); + + // Render item forum + const renderForumItem = ({ item }: { item: any }) => ( + {}} + isTruncate={true} + href={`/forum/${item.id}`} + isRightComponent={false} + /> + ); + +// const ListHeaderComponent = ( +// +// Diskusi Terbaru +// +// ); + + return ( + <> + , + headerRight: () => ( + + ), + }} + /> + + + setSearch(text), 500)} + /> + + } + floatingButton={ + + router.navigate("/(application)/(user)/forum/create") + } + /> + } + listData={pagination.listData} + renderItem={renderForumItem} + refreshControl={ + + } + onEndReached={pagination.loadMore} + // ListHeaderComponent={ListHeaderComponent} + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + /> + + ); +} \ No newline at end of file diff --git a/screens/RootLayout/AppRoot.tsx b/screens/RootLayout/AppRoot.tsx index 203d43c..7f2c4d2 100644 --- a/screens/RootLayout/AppRoot.tsx +++ b/screens/RootLayout/AppRoot.tsx @@ -1,3 +1,4 @@ +import { BackButton } from "@/components"; import { MainColor } from "@/constants/color-palet"; import { Stack } from "expo-router"; @@ -15,8 +16,18 @@ export default function AppRoot() { name="index" options={{ title: "", headerBackVisible: false }} /> - - + + {/* CEK PADA FILE */} + {/* , + }} + /> */} ([]); + const [search, setSearch] = useState(""); + const [isLoadList, setIsLoadList] = useState(false); + + useEffect(() => { + onLoadData(search); + }, [search]); + + const onLoadData = async (search: string) => { + try { + setIsLoadList(true); + const response = await apiAllUser({ search: search }); + console.log("[DATA USER] >", JSON.stringify(response.data, null, 2)); + setData(response.data); + } catch (error) { + console.log("Error fetching data", error); + } finally { + setIsLoadList(false); + } + }; + + const handleSearch = (search: string) => { + setSearch(search); + onLoadData(search); + }; + + return ( + <> + + } + placeholder="Cari Pengguna" + borderRadius={50} + containerStyle={{ marginBottom: 0 }} + /> + } + > + + {isLoadList ? ( + + ) : !_.isEmpty(data) ? ( + data?.map((e, index) => { + return ( + { + console.log("Ke Profile"); + router.push(`/profile/${e?.Profile?.id}`); + }} + > + + + + + + + {e?.username} + +{e?.nomor} + + + + + + + + ); + }) + ) : ( + Tidak ditemukan + )} + + + + + ); +} \ No newline at end of file diff --git a/screens/UserSeach/MainView_V2.tsx b/screens/UserSeach/MainView_V2.tsx new file mode 100644 index 0000000..22539b0 --- /dev/null +++ b/screens/UserSeach/MainView_V2.tsx @@ -0,0 +1,174 @@ +import { + AvatarComp, + ClickableCustom, + Grid, + NewWrapper, + StackCustom, + TextCustom, + TextInputCustom +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; +import { usePagination } from "@/hooks/use-pagination"; +import { apiAllUser } from "@/service/api-client/api-user"; +import { Ionicons } from "@expo/vector-icons"; +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"; + + +export default function UserSearchMainView_V2() { + const isInitialMount = useRef(true); + const [search, setSearch] = useState(""); + + const { + listData, + loading, + refreshing, + hasMore, + onRefresh, + loadMore, + isInitialLoad + } = usePagination({ + fetchFunction: async (page, searchQuery) => { + const response = await apiAllUser({ + page: String(page), + search: searchQuery || "", + }); + return response; + }, + pageSize: PAGINATION_DEFAULT_TAKE, + searchQuery: search, + }); + + // 🔁 Refresh otomatis saat kembali ke halaman ini + 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} + + )} + + + + + + + + + ); + + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading, + refreshing, + listData, + searchQuery: search, + emptyMessage: "Tidak ada pengguna ditemukan", + emptySearchMessage: "Tidak ada hasil pencarian", + skeletonCount: 5, + skeletonHeight: 150, + loadingFooterText: "Memuat lebih banyak pengguna...", + isInitialLoad + }); + + return ( + <> + + } + ListFooterComponent={ListFooterComponent} + ListEmptyComponent={ListEmptyComponent} + /> + + ); + + // return ( + // <> + // + // + // {JSON.stringify(listData, null, 2)} + // + // + // + // ); +} diff --git a/service/api-client/api-user.ts b/service/api-client/api-user.ts index 4775486..8cd3155 100644 --- a/service/api-client/api-user.ts +++ b/service/api-client/api-user.ts @@ -5,12 +5,27 @@ export async function apiUser(id: string) { return response.data; } -export async function apiAllUser({ search }: { search: string }) { - const response = await apiConfig.get(`/mobile/user?search=${search}`); - return response.data; +export async function apiAllUser({ + page, + search, +}: { + page?: string; + search?: string; +}) { + const pageQuery = page ? `?page=${page}` : ""; + const searchQuery = search ? `&search=${search}` : ""; + + try { + const response = await apiConfig.get( + `/mobile/user${pageQuery}${searchQuery}`, + ); + return response.data; + } catch (error) { + throw error; + } } -export async function apiDeleteUser({id}:{id: string}) { +export async function apiDeleteUser({ id }: { id: string }) { const response = await apiConfig.delete(`/mobile/user/${id}`); return response.data; } @@ -37,12 +52,19 @@ export async function apiForumBlockUser({ } } -export async function apiAcceptForumTerms({category, userId}:{category:"Forum" | "Event", userId: string}) { +export async function apiAcceptForumTerms({ + category, + userId, +}: { + category: "Forum" | "Event"; + userId: string; +}) { try { - const response = await apiConfig.post(`/mobile/user/${userId}/terms-of-app?category=${category}`); + const response = await apiConfig.post( + `/mobile/user/${userId}/terms-of-app?category=${category}`, + ); return response.data; } catch (error) { throw error; } } - -- 2.49.1 From d693550a1f79897d2bb0260f2972480bb2c405dc Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Thu, 29 Jan 2026 15:08:00 +0800 Subject: [PATCH 2/4] Fix Alur Login & Load data forum , user search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin – User Access - app/(application)/admin/user-access/[id]/index.tsx Authentication - context/AuthContext.tsx - screens/Authentication/EULASection.tsx - screens/Authentication/LoginView.tsx Forum - screens/Forum/ViewBeranda3.tsx Profile & UI Components - components/Image/AvatarComp.tsx - screens/Profile/AvatarAndBackground.tsx ### No Issue --- .../admin/user-access/[id]/index.tsx | 18 ------ components/Image/AvatarComp.tsx | 2 +- context/AuthContext.tsx | 46 ++++---------- screens/Authentication/EULASection.tsx | 32 ++++++++-- screens/Authentication/LoginView.tsx | 33 +++++----- screens/Forum/ViewBeranda3.tsx | 60 ++++++++++--------- screens/Profile/AvatarAndBackground.tsx | 1 + 7 files changed, 87 insertions(+), 105 deletions(-) diff --git a/app/(application)/admin/user-access/[id]/index.tsx b/app/(application)/admin/user-access/[id]/index.tsx index 17c8468..827b3d4 100644 --- a/app/(application)/admin/user-access/[id]/index.tsx +++ b/app/(application)/admin/user-access/[id]/index.tsx @@ -10,14 +10,10 @@ import { import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import GridTwoView from "@/components/_ShareComponent/GridTwoView"; import { useAuth } from "@/hooks/use-auth"; -import { routeUser } from "@/lib/routeApp"; import { apiAdminUserAccessGetById, apiAdminUserAccessUpdateStatus, } from "@/service/api-admin/api-admin-user-access"; -import { - apiNotificationsSendById -} from "@/service/api-notifications"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { useCallback, useState } from "react"; import Toast from "react-native-toast-message"; @@ -70,20 +66,6 @@ export default function AdminUserAccessDetail() { text1: "Update aktifasi berhasil ", }); - if (data.active === false) { - await apiNotificationsSendById({ - data: { - title: "Akun anda telah diaktifkan", - body: "Selamat menjelajahi HIConnect", - userLoginId: user?.id || "", - kategoriApp: "OTHER", - type: "announcement", - deepLink: routeUser.home, - }, - id: id as string, - }); - } - router.back(); } catch (error) { console.log("[ERROR UPDATE STATUS]", error); diff --git a/components/Image/AvatarComp.tsx b/components/Image/AvatarComp.tsx index 59b9542..1c88c23 100644 --- a/components/Image/AvatarComp.tsx +++ b/components/Image/AvatarComp.tsx @@ -30,7 +30,6 @@ export default function AvatarComp({ href = `/(application)/(image)/preview-image/${fileId}`, }: AvatarCompProps) { const dimension = sizeMap[size]; - const avatarImage = () => { return ( router.navigate(href as any) : onPress } + disabled={!fileId} > {avatarImage()} diff --git a/context/AuthContext.tsx b/context/AuthContext.tsx index 39b67af..8dd2818 100644 --- a/context/AuthContext.tsx +++ b/context/AuthContext.tsx @@ -30,7 +30,10 @@ type AuthContextType = { termsOfServiceAccepted: boolean; }) => Promise; userData: (token: string) => Promise; - acceptedTerms: (nomor: string, onSetModalVisible: (visible: boolean) => void) => Promise; + acceptedTerms: ( + nomor: string, + onSetModalVisible: (visible: boolean) => void, + ) => Promise; }; // --- Create Context --- @@ -80,34 +83,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { console.log("[RESPONSE AUTH]", JSON.stringify(response, null, 2)); if (response.success && response.isAcceptTerms) { - await AsyncStorage.setItem("kode_otp", response.kodeId); router.push(`/verification?nomor=${nomor}`); return true; } else { - // router.push(`/eula?nomor=${nomor}`); return false; } - - // if (response.success) { - // if (response.isAcceptTerms) { - // Toast.show({ - // type: "success", - // text1: "Sukses", - // text2: "Kode OTP berhasil dikirim", - // }); - - // await AsyncStorage.setItem("kode_otp", response.kodeId); - // router.push(`/verification?nomor=${nomor}`); - // return false - // } else { - // // router.push(`/eula?nomor=${nomor}`); - // return true - // } - // } else { - // router.push(`/eula?nomor=${nomor}`); - // return true; - // } } catch (error: any) { throw new Error(error.response?.data?.message || "Gagal kirim OTP"); } finally { @@ -266,29 +247,24 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { }; // --- 6. Accept Terms --- - const acceptedTerms = async (nomor: string, onSetModalVisible: (visible: boolean) => void) => { + const acceptedTerms = async ( + nomor: string, + onSetModalVisible: (visible: boolean) => void, + ) => { try { setIsLoading(true); const response = await apiUpdatedTermCondition({ nomor: nomor }); if (response.success) { - router.replace(`/verification?nomor=${nomor}`); + return `/verification?nomor=${nomor}`; } else { - if (response.status === 404) { - router.replace(`/register?nomor=${nomor}`); - } else { - Toast.show({ - type: "error", - text1: "Error", - text2: response.message, - }); - } + return `/register?nomor=${nomor}`; } } catch (error) { console.log("Error accept terms", error); } finally { setIsLoading(false); - // onSetModalVisible(false); + onSetModalVisible(false); } }; diff --git a/screens/Authentication/EULASection.tsx b/screens/Authentication/EULASection.tsx index ad4a662..d73e6e4 100644 --- a/screens/Authentication/EULASection.tsx +++ b/screens/Authentication/EULASection.tsx @@ -7,13 +7,21 @@ import { StyleSheet, } from "react-native"; import { useState, useRef } from "react"; -import { useLocalSearchParams, useRouter } from "expo-router"; +import { router, useLocalSearchParams, useRouter } from "expo-router"; import { SafeAreaView } from "react-native-safe-area-context"; import { AccentColor, MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth"; +import Toast from "react-native-toast-message"; - -export default function EULASection({ nomor, onSetModalVisible }: { nomor: string, onSetModalVisible: (visible: boolean) => void }) { +export default function EULASection({ + nomor, + onSetModalVisible, + setLoadingTerm, +}: { + nomor: string; + onSetModalVisible: (visible: boolean) => void; + setLoadingTerm: (loading: boolean) => void; +}) { const { acceptedTerms } = useAuth(); const [isLoading, setIsLoading] = useState(false); const [isAtBottom, setIsAtBottom] = useState(false); @@ -35,12 +43,26 @@ export default function EULASection({ nomor, onSetModalVisible }: { nomor: strin if (!isAtBottom) return; setIsLoading(true); - await acceptedTerms(nomor as string, onSetModalVisible); + const responseAccept = await acceptedTerms( + nomor as string, + onSetModalVisible, + ); + + console.log("Accept terms", responseAccept); + setLoadingTerm(true); + + setTimeout(() => { + router.replace(responseAccept); + }, 500); } catch (error) { console.log("Error accept terms", error); + Toast.show({ + type: "error", + text1: "Error", + text2: "Terjadi kesalahan saat menerima syarat dan ketentuan", + }); } finally { setIsLoading(false); - } }; diff --git a/screens/Authentication/LoginView.tsx b/screens/Authentication/LoginView.tsx index 1ea8558..f52db47 100644 --- a/screens/Authentication/LoginView.tsx +++ b/screens/Authentication/LoginView.tsx @@ -1,18 +1,16 @@ -import { NewWrapper, TextCustom } from "@/components"; +import { NewWrapper } from "@/components"; import ButtonCustom from "@/components/Button/ButtonCustom"; import ModalReactNative from "@/components/Modal/ModalReactNative"; import Spacing from "@/components/_ShareComponent/Spacing"; -import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth"; import { apiVersion, BASE_URL } from "@/service/api-config"; import { GStyles } from "@/styles/global-styles"; import { openBrowser } from "@/utils/openBrower"; import versionBadge from "@/utils/viersionBadge"; -import VersionBadge from "@/utils/viersionBadge"; import { Redirect } from "expo-router"; import { useEffect, useState } from "react"; -import { Modal, RefreshControl, Text, View } from "react-native"; +import { RefreshControl, Text, View } from "react-native"; import PhoneInput, { ICountry } from "react-native-international-phone-number"; import Toast from "react-native-toast-message"; import EULASection from "./EULASection"; @@ -26,6 +24,7 @@ export default function LoginView() { const [refreshing, setRefreshing] = useState(false); const [modalVisible, setModalVisible] = useState(false); const [numberToEULA, setNumberToEULA] = useState(""); + const [loadingTerm, setLoadingTerm] = useState(false); const { loginWithNomor, token, isAdmin, isUserActive } = useAuth(); @@ -90,7 +89,6 @@ export default function LoginView() { let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, ""); const realNumber = callingCode + fixNumber; - try { setLoading(true); @@ -129,6 +127,8 @@ export default function LoginView() { return ; } + console.log("load term", loadingTerm); + return ( - WELCOME TO @@ -172,21 +171,15 @@ export default function LoginView() { - + Login - {/* { - setModalVisible(true); - console.log("Show modal", modalVisible); - }} - > - Show Modal - */} - {/* setTerm(!term)} /> */} - @@ -208,7 +201,11 @@ export default function LoginView() { - + ); diff --git a/screens/Forum/ViewBeranda3.tsx b/screens/Forum/ViewBeranda3.tsx index 997e3fd..f119d65 100644 --- a/screens/Forum/ViewBeranda3.tsx +++ b/screens/Forum/ViewBeranda3.tsx @@ -2,21 +2,20 @@ import { AvatarComp, BackButton, FloatingButton, - SearchInput, - TextCustom, + SearchInput } from "@/components"; import NewWrapper from "@/components/_ShareComponent/NewWrapper"; -import { useAuth } from "@/hooks/use-auth"; +import { MainColor } from "@/constants/color-palet"; import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import { useAuth } from "@/hooks/use-auth"; +import { usePagination } from "@/hooks/use-pagination"; import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; import { apiForumGetAll } from "@/service/api-client/api-forum"; import { apiUser } from "@/service/api-client/api-user"; import { router, Stack } from "expo-router"; import _ from "lodash"; import { useEffect, useState } from "react"; -import { RefreshControl, View } from "react-native"; -import { MainColor } from "@/constants/color-palet"; -import { usePagination } from "@/hooks/use-pagination"; +import { RefreshControl, TouchableOpacity, View } from "react-native"; const PAGE_SIZE = 5; @@ -36,7 +35,7 @@ export default function Forum_ViewBeranda3() { const pagination = usePagination({ fetchFunction: async (page, searchQuery) => { if (!user?.id) return { data: [] }; - + return await apiForumGetAll({ category: "beranda", search: searchQuery || "", @@ -51,16 +50,17 @@ export default function Forum_ViewBeranda3() { }); // Generate komponen (menggantikan 40+ lines code!) - const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ - loading: pagination.loading, - refreshing: pagination.refreshing, - listData: pagination.listData, - searchQuery: search, - emptyMessage: "Tidak ada diskusi", - emptySearchMessage: "Tidak ada hasil pencarian", - skeletonCount: 5, - skeletonHeight: 150, - }); + const { ListEmptyComponent, ListFooterComponent } = + createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + searchQuery: search, + emptyMessage: "Tidak ada diskusi", + emptySearchMessage: "Tidak ada hasil pencarian", + skeletonCount: 5, + skeletonHeight: 150, + }); // Render item forum const renderForumItem = ({ item }: { item: any }) => ( @@ -74,11 +74,11 @@ export default function Forum_ViewBeranda3() { /> ); -// const ListHeaderComponent = ( -// -// Diskusi Terbaru -// -// ); + // const ListHeaderComponent = ( + // + // Diskusi Terbaru + // + // ); return ( <> @@ -87,11 +87,15 @@ export default function Forum_ViewBeranda3() { title: "Forum", headerLeft: () => , headerRight: () => ( - + router.navigate(`/forum/${user?.id}/forumku`)} + > + + ), }} /> @@ -129,4 +133,4 @@ export default function Forum_ViewBeranda3() { /> ); -} \ No newline at end of file +} diff --git a/screens/Profile/AvatarAndBackground.tsx b/screens/Profile/AvatarAndBackground.tsx index 4d490c1..3090b91 100644 --- a/screens/Profile/AvatarAndBackground.tsx +++ b/screens/Profile/AvatarAndBackground.tsx @@ -21,6 +21,7 @@ const AvatarAndBackground = ({ `/(application)/(image)/preview-image/${backgroundId}` ); }} + disabled={!backgroundId} > Date: Thu, 29 Jan 2026 17:36:17 +0800 Subject: [PATCH 3/4] Fix forum detail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Forum (User) - app/(application)/(user)/forum/[id]/index.tsx - screens/Forum/ViewForumku2.tsx - service/api-client/api-forum.ts Forum – New / Refactor - screens/Forum/DetailForum.tsx - screens/Forum/DetailForum2.tsx Documentation - docs/ Removed - hipmi-note.md ### No Issue' --- app/(application)/(user)/forum/[id]/index.tsx | 257 +-------------- hipmi-note.md => docs/hipmi-note.md | 0 docs/prompt-for-qwen-code.md | 14 + screens/Authentication/LoginView.tsx | 2 - screens/Forum/DetailForum.tsx | 276 ++++++++++++++++ screens/Forum/DetailForum2.tsx | 306 ++++++++++++++++++ screens/Forum/ViewForumku2.tsx | 144 +++------ service/api-client/api-forum.ts | 4 +- 8 files changed, 644 insertions(+), 359 deletions(-) rename hipmi-note.md => docs/hipmi-note.md (100%) create mode 100644 docs/prompt-for-qwen-code.md create mode 100644 screens/Forum/DetailForum.tsx create mode 100644 screens/Forum/DetailForum2.tsx diff --git a/app/(application)/(user)/forum/[id]/index.tsx b/app/(application)/(user)/forum/[id]/index.tsx index 3cf093d..b0db0e8 100644 --- a/app/(application)/(user)/forum/[id]/index.tsx +++ b/app/(application)/(user)/forum/[id]/index.tsx @@ -1,258 +1,11 @@ -import { - ButtonCustom, - DrawerCustom, - LoaderCustom, - Spacing, - TextAreaCustom, - TextCustom, - ViewWrapper, -} from "@/components"; -import AlertWarning from "@/components/Alert/AlertWarning"; -import { useAuth } from "@/hooks/use-auth"; -import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection"; -import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; -import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; -import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar"; -import { - apiForumCreateComment, - apiForumGetComment, - apiForumGetOne, - apiForumUpdateStatus, -} from "@/service/api-client/api-forum"; -import { TypeForum_CommentProps } from "@/types/type-forum"; -import { isBadContent } from "@/utils/badWordsIndonesia"; -import { useFocusEffect, useLocalSearchParams } from "expo-router"; -import _ from "lodash"; -import { useCallback, useEffect, useState } from "react"; - - +import DetailForum from "@/screens/Forum/DetailForum"; +import DetailForum2 from "@/screens/Forum/DetailForum2"; export default function ForumDetail() { - const { id } = useLocalSearchParams(); - const { user } = useAuth(); - const [openDrawer, setOpenDrawer] = useState(false); - const [data, setData] = useState(null); - const [listComment, setListComment] = useState(null); - const [isLoadingComment, setLoadingComment] = useState(false); - - // Status - const [status, setStatus] = useState(""); - const [text, setText] = useState(""); - const [authorId, setAuthorId] = useState(""); - const [dataId, setDataId] = useState(""); - - // Comentar - const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false); - const [commentId, setCommentId] = useState(""); - const [commentAuthorId, setCommentAuthorId] = useState(""); - - useFocusEffect( - useCallback(() => { - onLoadData(id as string); - }, [id]) - ); - - const onLoadData = async (id: string) => { - try { - const response = await apiForumGetOne({ id }); - setData(response.data); - } catch (error) { - console.log("[ERROR]", error); - } - }; - - useEffect(() => { - onLoadListComment(id as string); - }, [id]); - - const onLoadListComment = async (id: string) => { - try { - const response = await apiForumGetComment({ - id: id as string, - }); - setListComment(response.data); - } catch (error) { - console.log("[ERROR]", error); - } - }; - - // Update Status - const handlerUpdateStatus = async (value: any) => { - try { - const response = await apiForumUpdateStatus({ - id: id as string, - data: value, - }); - if (response.success) { - setStatus(response.data); - setData({ - ...data, - ForumMaster_StatusPosting: { - status: response.data, - }, - }); - } - } catch (error) { - console.log("[ERROR]", error); - } - }; - - // Create Commentar - const handlerCreateCommentar = async () => { - if (isBadContent(text)) { - AlertWarning({}); - return; - } - - const newData = { - comment: text, - authorId: user?.id, - }; - - try { - setLoadingComment(true); - const response = await apiForumCreateComment({ - id: id as string, - data: newData, - }); - - if (response.success) { - setText(""); - const newComment = { - id: response.data.id, - isActive: response.data.isActive, - komentar: response.data.komentar, - createdAt: response.data.createdAt, - authorId: response.data.authorId, - Author: response.data.Author, - }; - setListComment((prev) => [newComment, ...(prev || [])]); - setData({ - ...data, - count: data.count + 1, - }); - } - } catch (error) { - console.log("[ERROR]", error); - } finally { - setLoadingComment(false); - } - }; - return ( <> - - {!data && !listComment ? ( - - ) : ( - <> - {/* Box Posting */} - { - setOpenDrawer(true); - setStatus(data.ForumMaster_StatusPosting?.status); - setAuthorId(data.Author?.id); - setDataId(data.id); - }} - /> - - {/* Area Commentar */} - {data?.ForumMaster_StatusPosting?.status === "Open" && ( - <> - - { - handlerCreateCommentar(); - }} - > - Balas - - - )} - - - {/* List Commentar */} - {_.isEmpty(listComment) ? ( - - Tidak ada komentar - - ) : ( - Komentar : - )} - - {listComment?.map((item: any, index: number) => ( - { - setCommentId(value.setCommentId); - setOpenDrawerCommentar(value.setOpenDrawer); - setCommentAuthorId(value.setCommentAuthorId); - }} - /> - ))} - - )} - - - {/* Posting Drawer */} - setOpenDrawer(false)} - > - { - setOpenDrawer(false); - }} - authorId={authorId} - handlerUpdateStatus={(value: any) => { - handlerUpdateStatus(value); - }} - /> - - - {/* Commentar Drawer */} - setOpenDrawerCommentar(false)} - > - { - setOpenDrawerCommentar(false); - }} - listComment={listComment} - setListComment={setListComment} - countComment={data?.count} - setCountComment={(val: any) => { - setData((prev: any) => ({ - ...prev, - count: val, - })); - }} - /> - + {/* ; */} + - ); + ) } diff --git a/hipmi-note.md b/docs/hipmi-note.md similarity index 100% rename from hipmi-note.md rename to docs/hipmi-note.md diff --git a/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md new file mode 100644 index 0000000..048a125 --- /dev/null +++ b/docs/prompt-for-qwen-code.md @@ -0,0 +1,14 @@ + +Terapkan pagination pada file: screens/Forum/DetailForum.tsx +Component yang digunakan: hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx + +Perbaiki fetch pada file: service/api-client/api-forum.ts +Jika tidak ada props page maka tambahkan props page dan default page: "1" + +Gunakan bahasa indonesia pada cli agar saya mudah membacanya. + + + + +Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx +Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti \ No newline at end of file diff --git a/screens/Authentication/LoginView.tsx b/screens/Authentication/LoginView.tsx index f52db47..809f07e 100644 --- a/screens/Authentication/LoginView.tsx +++ b/screens/Authentication/LoginView.tsx @@ -127,8 +127,6 @@ export default function LoginView() { return ; } - console.log("load term", loadingTerm); - return ( (null); + const [listComment, setListComment] = useState< + TypeForum_CommentProps[] | null + >(null); + const [isLoadingComment, setLoadingComment] = useState(false); + + // Status + const [status, setStatus] = useState(""); + const [text, setText] = useState(""); + const [authorId, setAuthorId] = useState(""); + const [dataId, setDataId] = useState(""); + + // Comentar + const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false); + const [commentId, setCommentId] = useState(""); + const [commentAuthorId, setCommentAuthorId] = useState(""); + + useFocusEffect( + useCallback(() => { + setTimeout(() => { + onLoadData(id as string); + }, 3000); + }, [id]), + ); + + useEffect(() => { + setTimeout(() => { + onLoadListComment(id as string); + }, 3000); + }, [id]); + + const onLoadData = async (id: string) => { + try { + const response = await apiForumGetOne({ id }); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + const onLoadListComment = async (id: string) => { + try { + const response = await apiForumGetComment({ + id: id as string, + }); + setListComment(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + // Update Status + const handlerUpdateStatus = async (value: any) => { + try { + const response = await apiForumUpdateStatus({ + id: id as string, + data: value, + }); + if (response.success) { + setStatus(response.data); + setData({ + ...data, + ForumMaster_StatusPosting: { + status: response.data, + }, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + // Create Commentar + const handlerCreateCommentar = async () => { + if (isBadContent(text)) { + AlertWarning({}); + return; + } + + const newData = { + comment: text, + authorId: user?.id, + }; + + try { + setLoadingComment(true); + const response = await apiForumCreateComment({ + id: id as string, + data: newData, + }); + + if (response.success) { + setText(""); + const newComment = { + id: response.data.id, + isActive: response.data.isActive, + komentar: response.data.komentar, + createdAt: response.data.createdAt, + authorId: response.data.authorId, + Author: response.data.Author, + }; + setListComment((prev) => [newComment, ...(prev || [])]); + setData({ + ...data, + count: data.count + 1, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingComment(false); + } + }; + + const headerComponent = () => + // Box Posting + !data && !listComment ? ( + + ) : ( + <> + { + setOpenDrawer(true); + setStatus(data.ForumMaster_StatusPosting?.status); + setAuthorId(data.Author?.id); + setDataId(data.id); + }} + /> + + {data?.ForumMaster_StatusPosting?.status === "Open" && ( + <> + + { + handlerCreateCommentar(); + }} + > + Balas + + + )} + + ); + + return ( + <> + + {!data && !listComment ? ( + + ) : ( + <> + {/* Area Commentar */} + {headerComponent()} + + {/* List Commentar */} + {_.isEmpty(listComment) ? ( + + Tidak ada komentar + + ) : ( + Komentar : + )} + + {listComment?.map((item: any, index: number) => ( + { + setCommentId(value.setCommentId); + setOpenDrawerCommentar(value.setOpenDrawer); + setCommentAuthorId(value.setCommentAuthorId); + }} + /> + ))} + + )} + + + {/* Posting Drawer */} + setOpenDrawer(false)} + > + { + setOpenDrawer(false); + }} + authorId={authorId} + handlerUpdateStatus={(value: any) => { + handlerUpdateStatus(value); + }} + /> + + + {/* Commentar Drawer */} + setOpenDrawerCommentar(false)} + > + { + setOpenDrawerCommentar(false); + }} + listComment={listComment} + setListComment={setListComment} + countComment={data?.count} + setCountComment={(val: any) => { + setData((prev: any) => ({ + ...prev, + count: val, + })); + }} + /> + + + ); +} diff --git a/screens/Forum/DetailForum2.tsx b/screens/Forum/DetailForum2.tsx new file mode 100644 index 0000000..610c911 --- /dev/null +++ b/screens/Forum/DetailForum2.tsx @@ -0,0 +1,306 @@ +import { + BoxButtonOnFooter, + ButtonCustom, + DrawerCustom, + LoaderCustom, + NewWrapper, + Spacing, + TextAreaCustom, + TextCustom, + TextInputCustom, + ViewWrapper, +} from "@/components"; +import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom"; +import AlertWarning from "@/components/Alert/AlertWarning"; +import { MainColor } from "@/constants/color-palet"; +import { useAuth } from "@/hooks/use-auth"; +import { usePagination } from "@/hooks/use-pagination"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection"; +import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; +import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; +import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar"; +import { + apiForumCreateComment, + apiForumGetComment, + apiForumGetOne, + apiForumUpdateStatus, +} from "@/service/api-client/api-forum"; +import { TypeForum_CommentProps } from "@/types/type-forum"; +import { censorText, isBadContent } from "@/utils/badWordsIndonesia"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useEffect, useState } from "react"; +import { RefreshControl } from "react-native"; +import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; + +export default function DetailForum2() { + const { id } = useLocalSearchParams(); + const { user } = useAuth(); + const [openDrawer, setOpenDrawer] = useState(false); + const [data, setData] = useState(null); + const [isLoadingComment, setLoadingComment] = useState(false); + + // Status + const [status, setStatus] = useState(""); + const [text, setText] = useState(""); + const [authorId, setAuthorId] = useState(""); + const [dataId, setDataId] = useState(""); + + // Comentar + const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false); + const [commentId, setCommentId] = useState(""); + const [commentAuthorId, setCommentAuthorId] = useState(""); + + // Initialize pagination for comments + const commentPagination = usePagination({ + fetchFunction: async (page) => { + return await apiForumGetComment({ + id: id as string, + page: String(page), // API expects string + }); + }, + pageSize: 5, + dependencies: [id], + onError: (error) => console.error("[ERROR] Fetch forum comment:", error), + }); + + useFocusEffect( + useCallback(() => { + setTimeout(() => { + onLoadData(id as string); + }, 3000); + }, [id]), + ); + + useEffect(() => { + // Reset and load first page of comments when id changes + commentPagination.reset(); + commentPagination.onRefresh(); + }, [id]); + + const onLoadData = async (id: string) => { + try { + const response = await apiForumGetOne({ id }); + setData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } + }; + + // Update Status + const handlerUpdateStatus = async (value: any) => { + try { + const response = await apiForumUpdateStatus({ + id: id as string, + data: value, + }); + if (response.success) { + setStatus(response.data); + setData({ + ...data, + ForumMaster_StatusPosting: { + status: response.data, + }, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } + }; + + // Create Commentar + const handlerCreateCommentar = async () => { + + const cencorContent = censorText(text); + + const newData = { + comment: cencorContent, + authorId: user?.id, + }; + + try { + setLoadingComment(true); + const response = await apiForumCreateComment({ + id: id as string, + data: newData, + }); + + if (response.success) { + setText(""); + const newComment = { + id: response.data.id, + isActive: response.data.isActive, + komentar: response.data.komentar, + createdAt: response.data.createdAt, + authorId: response.data.authorId, + Author: response.data.Author, + }; + + // Add new comment to the top of the list + commentPagination.setListData((prev) => [newComment, ...prev]); + + setData({ + ...data, + count: data.count + 1, + }); + } + } catch (error) { + console.log("[ERROR]", error); + } finally { + setLoadingComment(false); + } + }; + + const headerComponent = () => + // Box Posting + !data ? ( + + ) : ( + <> + {/* Area Posting */} + { + setOpenDrawer(true); + setStatus(data.ForumMaster_StatusPosting?.status); + setAuthorId(data.Author?.id); + setDataId(data.id); + }} + /> + + {/* Area Commentar */} + {data?.ForumMaster_StatusPosting?.status === "Open" && ( + <> + + { + handlerCreateCommentar(); + }} + > + Balas + + + )} + + ); + + // Render individual comment item + const renderCommentItem = ({ item }: { item: TypeForum_CommentProps }) => + !data && !commentPagination.listData ? ( + + ) : ( + { + setCommentId(value.setCommentId); + setOpenDrawerCommentar(value.setOpenDrawer); + setCommentAuthorId(value.setCommentAuthorId); + }} + /> + ); + + // Generate pagination components using helper + const { ListEmptyComponent, ListFooterComponent } = + createPaginationComponents({ + loading: commentPagination.loading, + refreshing: commentPagination.refreshing, + listData: commentPagination.listData, + isInitialLoad: commentPagination.isInitialLoad, + emptyMessage: "Tidak ada komentar", + skeletonCount: 3, + skeletonHeight: 120, + }); + + return ( + <> + + } + onEndReached={commentPagination.loadMore} + ListHeaderComponent={ + <> + {/* + Komentar : + */} + {headerComponent()} + + + } + ListFooterComponent={ListFooterComponent} + ListEmptyComponent={ListEmptyComponent} + /> + + {/* Posting Drawer */} + setOpenDrawer(false)} + > + { + setOpenDrawer(false); + }} + authorId={authorId} + handlerUpdateStatus={(value: any) => { + handlerUpdateStatus(value); + }} + /> + + + {/* Commentar Drawer */} + setOpenDrawerCommentar(false)} + > + { + setOpenDrawerCommentar(false); + }} + listComment={commentPagination.listData} + setListComment={commentPagination.setListData} + countComment={data?.count} + setCountComment={(val: any) => { + setData((prev: any) => ({ + ...prev, + count: val, + })); + }} + /> + + + ); +} diff --git a/screens/Forum/ViewForumku2.tsx b/screens/Forum/ViewForumku2.tsx index 535b3b1..67594c0 100644 --- a/screens/Forum/ViewForumku2.tsx +++ b/screens/Forum/ViewForumku2.tsx @@ -15,38 +15,49 @@ import NoDataText from "@/components/_ShareComponent/NoDataText"; import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom"; import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth"; +import { usePagination } from "@/hooks/use-pagination"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; import { apiForumGetAll } from "@/service/api-client/api-forum"; import { apiUser } from "@/service/api-client/api-user"; import { router, useLocalSearchParams } from "expo-router"; import _ from "lodash"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { RefreshControl, View } from "react-native"; -const PAGE_SIZE = 5; - export default function View_Forumku2() { const { id } = useLocalSearchParams(); const { user } = useAuth(); - const [listData, setListData] = useState([]); const [dataUser, setDataUser] = useState(null); - const [loading, setLoading] = useState(false); - const [refreshing, setRefreshing] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [page, setPage] = useState(1); const [count, setCount] = useState(0); + // Initialize pagination hook + const pagination = usePagination({ + fetchFunction: async (page) => { + if (!user?.id) throw new Error("User not authenticated"); + + const response = await apiForumGetAll({ + category: "forumku", + authorId: id as string, + userLoginId: user.id, + page: String(page), // API terima string + }); + + // Update count when fetching page 1 + if (page === 1) { + setCount(response.data.count); + } + + return response.data; + }, + pageSize: 5, + dependencies: [user?.id], + }); + useEffect(() => { onLoadDataProfile(id as string); }, [id]); - useEffect(() => { - setPage(1); - setListData([]); - setHasMore(true); - fetchData(1, true); - }, [user?.id]); - const onLoadDataProfile = async (id: string) => { try { const response = await apiUser(id); @@ -58,54 +69,6 @@ export default function View_Forumku2() { } }; - // 🔹 Fungsi fetch data - const fetchData = async (pageNumber: number, clear: boolean) => { - if (!user?.id) return; - - // Cegah multiple call - if (!clear && (loading || refreshing)) return; - - const isRefresh = clear; - if (isRefresh) setRefreshing(true); - if (!isRefresh) setLoading(true); - - try { - const response = await apiForumGetAll({ - category: "forumku", - authorId: id as string, - userLoginId: user.id, - page: String(pageNumber), // API terima string - }); - - const newData = response.data.data || []; - setListData((prev) => { - const current = Array.isArray(prev) ? prev : []; - return clear ? newData : [...current, ...newData]; - }); - setHasMore(newData.length === PAGE_SIZE); - setPage(pageNumber); - setCount(response.data.count); - } catch (error) { - console.error("[ERROR] Fetch forum:", error); - setHasMore(false); - } finally { - setRefreshing(false); - setLoading(false); - } - }; - - // 🔹 Pull-to-refresh - const onRefresh = useCallback(() => { - fetchData(1, true); - }, [user?.id]); - - // 🔹 Infinite scroll - const loadMore = useCallback(() => { - if (hasMore && !loading && !refreshing) { - fetchData(page + 1, false); - } - }, [hasMore, loading, refreshing, page, user?.id]); - const randerHeaderComponent = () => ( <> @@ -144,39 +107,16 @@ export default function View_Forumku2() { /> ); - // Skeleton List (untuk initial load) - const SkeletonListComponent = () => ( - - - {Array.from({ length: 5 }).map((_, i) => ( - - ))} - - - ); - - // Komponen Empty - const EmptyComponent = () => ( - - - - ); - - // 🔹 Komponen Footer List (loading indicator) - const ListFooterComponent = - loading && !refreshing && listData.length > 0 ? ( - - {/* Memuat diskusi... */} - - - ) : null; + // Generate pagination components using helper + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + isInitialLoad: pagination.isInitialLoad, + emptyMessage: "Tidak ada postingan", + skeletonCount: 5, + skeletonHeight: 200, + }); return ( <> @@ -190,7 +130,7 @@ export default function View_Forumku2() { /> ) } - listData={listData} + listData={pagination.listData} renderItem={renderList} refreshControl={ } - onEndReached={loadMore} + onEndReached={pagination.loadMore} ListHeaderComponent={randerHeaderComponent()} ListFooterComponent={ListFooterComponent} - ListEmptyComponent={ - loading && _.isEmpty(listData) ? : - } + ListEmptyComponent={ListEmptyComponent} /> ); diff --git a/service/api-client/api-forum.ts b/service/api-client/api-forum.ts index 28c60f3..1a39f0e 100644 --- a/service/api-client/api-forum.ts +++ b/service/api-client/api-forum.ts @@ -102,9 +102,9 @@ export async function apiForumCreateComment({ } } -export async function apiForumGetComment({ id }: { id: string }) { +export async function apiForumGetComment({ id, page = "1" }: { id: string, page?: string }) { try { - const response = await apiConfig.get(`/mobile/forum/${id}/comment`); + const response = await apiConfig.get(`/mobile/forum/${id}/comment?page=${page}`); return response.data; } catch (error) { throw error; -- 2.49.1 From ec79a1fbcd90a873d38842a17544467a3c838df2 Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Fri, 30 Jan 2026 17:18:47 +0800 Subject: [PATCH 4/4] =?UTF-8?q?Fix=20semua=20tampilan=20yang=20memiliki=20?= =?UTF-8?q?fungsi=20infitine=20load=20UI=20=E2=80=93=20User=20Notification?= =?UTF-8?q?s=20-=20app/(application)/(user)/notifications/index.tsx=20-=20?= =?UTF-8?q?service/api-notifications.ts=20-=20screens/Notification/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI – Portofolio (User) - app/(application)/(user)/portofolio/[id]/create.tsx - app/(application)/(user)/portofolio/[id]/edit.tsx - app/(application)/(user)/portofolio/[id]/list.tsx - screens/Portofolio/BoxPortofolioView.tsx - screens/Portofolio/ViewListPortofolio.tsx - screens/Profile/PortofolioSection.tsx - service/api-client/api-portofolio.ts Forum & User Search - screens/Forum/DetailForum2.tsx - screens/Forum/ViewBeranda3.tsx - screens/UserSeach/MainView_V2.tsx Constants & Docs - constants/constans-value.ts - docs/prompt-for-qwen-code.md ### No Issue --- .../(user)/notifications/index.tsx | 245 +---------------- .../(user)/portofolio/[id]/create.tsx | 7 +- .../(user)/portofolio/[id]/edit.tsx | 15 +- .../(user)/portofolio/[id]/list.tsx | 27 +- constants/constans-value.ts | 2 +- docs/prompt-for-qwen-code.md | 14 +- screens/Forum/DetailForum2.tsx | 28 +- screens/Forum/ViewBeranda3.tsx | 2 +- screens/Notification/ScreenNotification.tsx | 249 ++++++++++++++++++ screens/Portofolio/BoxPortofolioView.tsx | 4 +- screens/Portofolio/ViewListPortofolio.tsx | 74 ++++++ screens/Profile/PortofolioSection.tsx | 2 +- screens/UserSeach/MainView_V2.tsx | 59 ++--- service/api-client/api-portofolio.ts | 4 +- service/api-notifications.ts | 5 +- 15 files changed, 411 insertions(+), 326 deletions(-) create mode 100644 screens/Notification/ScreenNotification.tsx create mode 100644 screens/Portofolio/ViewListPortofolio.tsx diff --git a/app/(application)/(user)/notifications/index.tsx b/app/(application)/(user)/notifications/index.tsx index dbec998..b10ee39 100644 --- a/app/(application)/(user)/notifications/index.tsx +++ b/app/(application)/(user)/notifications/index.tsx @@ -1,248 +1,9 @@ -/* eslint-disable react-hooks/exhaustive-deps */ -import { - AlertDefaultSystem, - BackButton, - BaseBox, - DrawerCustom, - MenuDrawerDynamicGrid, - NewWrapper, - ScrollableCustom, - StackCustom, - TextCustom, -} from "@/components"; -import { IconDot } from "@/components/_Icon/IconComponent"; -import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; -import NoDataText from "@/components/_ShareComponent/NoDataText"; -import { AccentColor, MainColor } from "@/constants/color-palet"; -import { ICON_SIZE_SMALL } from "@/constants/constans-value"; -import { useAuth } from "@/hooks/use-auth"; -import { useNotificationStore } from "@/hooks/use-notification-store"; -import { apiGetNotificationsById } from "@/service/api-notifications"; -import { listOfcategoriesAppNotification } from "@/types/type-notification-category"; -import { formatChatTime } from "@/utils/formatChatTime"; -import { Ionicons } from "@expo/vector-icons"; -import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; -import _ from "lodash"; -import { useCallback, useState } from "react"; -import { RefreshControl, View } from "react-native"; +import ScreenNotification from "@/screens/Notification/ScreenNotification"; -const selectedCategory = (value: string) => { - const category = listOfcategoriesAppNotification.find( - (c) => c.value === value - ); - return category?.label; -}; - -const fixPath = ({ - deepLink, - categoryApp, -}: { - deepLink: string; - categoryApp: string; -}) => { - if (categoryApp === "OTHER") { - return deepLink; - } - - const separator = deepLink.includes("?") ? "&" : "?"; - - const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase( - categoryApp - )}`; - - console.log("Fix Path", fixedPath); - - return fixedPath; -}; - -const BoxNotification = ({ - data, - activeCategory, -}: { - data: any; - activeCategory: string | null; -}) => { - // console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2)); - const { markAsRead } = useNotificationStore(); +export default function Notification() { return ( <> - { - // console.log( - // "Notification >", - // selectedCategory(activeCategory as string) - // ); - const newPath = fixPath({ - deepLink: data.deepLink, - categoryApp: data.kategoriApp, - }); - - router.navigate(newPath as any); - selectedCategory(activeCategory as string); - - if (!data.isRead) { - markAsRead(data.id); - } - }} - > - - - {data.title} - - - {data.pesan} - - - {formatChatTime(data.createdAt)} - - - - - ); -}; - -export default function Notifications() { - const { user } = useAuth(); - const { category } = useLocalSearchParams<{ category?: string }>(); - const [activeCategory, setActiveCategory] = useState( - category || "event" - ); - const [listData, setListData] = useState([]); - const [refreshing, setRefreshing] = useState(false); - const [loading, setLoading] = useState(false); - const [openDrawer, setOpenDrawer] = useState(false); - - const { markAsReadAll } = useNotificationStore(); - - const handlePress = (item: any) => { - setActiveCategory(item.value); - // tambahkan logika lain seperti filter dsb. - }; - - useFocusEffect( - useCallback(() => { - fecthData(); - }, [activeCategory]) - ); - - const fecthData = async () => { - try { - setLoading(true); - const response = await apiGetNotificationsById({ - id: user?.id as any, - category: activeCategory as any, - }); - - if (response.success) { - setListData(response.data); - } else { - setListData([]); - } - } catch (error) { - console.log("Error Notification", error); - } finally { - setLoading(false); - } - }; - - const onRefresh = () => { - setRefreshing(true); - fecthData(); - setRefreshing(false); - }; - - return ( - <> - , - headerRight: () => ( - setOpenDrawer(true)} - /> - ), - }} - /> - - ({ - id: i, - label: e.label, - value: e.value, - }))} - onButtonPress={handlePress} - activeId={activeCategory as string} - /> - } - refreshControl={ - - } - > - {loading ? ( - - ) : _.isEmpty(listData) ? ( - - ) : ( - listData.map((e, i) => ( - - - - )) - )} - - - setOpenDrawer(false)} - height={"auto"} - > - - ), - path: "", - }, - ]} - onPressItem={(item: any) => { - console.log("Item", item.value); - if (item.value === "read-all") { - AlertDefaultSystem({ - title: "Tandai Semua Dibaca", - message: - "Apakah Anda yakin ingin menandai semua notifikasi dibaca?", - textLeft: "Batal", - textRight: "Ya", - onPressRight: () => { - markAsReadAll(user?.id as any); - const data = _.cloneDeep(listData); - data.forEach((e) => { - e.isRead = true; - }); - setListData(data); - onRefresh(); - setOpenDrawer(false); - }, - }); - } - }} - /> - + ); } diff --git a/app/(application)/(user)/portofolio/[id]/create.tsx b/app/(application)/(user)/portofolio/[id]/create.tsx index ea32573..79af553 100644 --- a/app/(application)/(user)/portofolio/[id]/create.tsx +++ b/app/(application)/(user)/portofolio/[id]/create.tsx @@ -7,6 +7,7 @@ import { CenterCustom, Grid, InformationBox, + NewWrapper, SelectCustom, Spacing, StackCustom, @@ -120,7 +121,7 @@ export default function PortofolioCreate() { }; return ( - - + {/* */} - + ); } diff --git a/app/(application)/(user)/portofolio/[id]/edit.tsx b/app/(application)/(user)/portofolio/[id]/edit.tsx index bf4b147..6b6f70c 100644 --- a/app/(application)/(user)/portofolio/[id]/edit.tsx +++ b/app/(application)/(user)/portofolio/[id]/edit.tsx @@ -4,14 +4,15 @@ import { BoxButtonOnFooter, ButtonCustom, CenterCustom, + NewWrapper, SelectCustom, Spacing, StackCustom, TextAreaCustom, TextCustom, TextInputCustom, - ViewWrapper, } from "@/components"; +import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; import { MainColor } from "@/constants/color-palet"; import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; import { @@ -238,7 +239,7 @@ export default function PortofolioEdit() { return !dataArray.some( (item: any) => !item.MasterSubBidangBisnis.id || - item.MasterSubBidangBisnis.id.trim() === "" + item.MasterSubBidangBisnis.id.trim() === "", ); } @@ -319,16 +320,16 @@ export default function PortofolioEdit() { if (!bidangBisnis || !subBidangBisnis) { return ( <> - - - + + + ); } return ( <> - + - + ); } diff --git a/app/(application)/(user)/portofolio/[id]/list.tsx b/app/(application)/(user)/portofolio/[id]/list.tsx index 7cd0888..c9642ea 100644 --- a/app/(application)/(user)/portofolio/[id]/list.tsx +++ b/app/(application)/(user)/portofolio/[id]/list.tsx @@ -1,28 +1,9 @@ -import { TextCustom, ViewWrapper } from "@/components"; -import Portofolio_BoxView from "@/screens/Portofolio/BoxPortofolioView"; -import { apiGetPortofolio } from "@/service/api-client/api-portofolio"; -import { useFocusEffect, useLocalSearchParams } from "expo-router"; -import { useCallback, useState } from "react"; +import ViewListPortofolio from "@/screens/Portofolio/ViewListPortofolio"; export default function ListPortofolio() { - const { id } = useLocalSearchParams(); - const [data, setData] = useState([]); - - useFocusEffect( - useCallback(() => { - onLoadPortofolio(id as string); - }, [id]) - ); - - const onLoadPortofolio = async (id: string) => { - const response = await apiGetPortofolio({ id: id }); - setData(response.data); - }; return ( - - {data ? data?.map((item: any, index: number) => ( - - )) : Tidak ada portofolio} - + <> + + ); } diff --git a/constants/constans-value.ts b/constants/constans-value.ts index 1cef856..da76671 100644 --- a/constants/constans-value.ts +++ b/constants/constans-value.ts @@ -24,7 +24,7 @@ export { // OS Height const OS_ANDROID_HEIGHT = 115 -const OS_IOS_HEIGHT = 70 +const OS_IOS_HEIGHT = 90 const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT // Text Size diff --git a/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md index 048a125..3bab661 100644 --- a/docs/prompt-for-qwen-code.md +++ b/docs/prompt-for-qwen-code.md @@ -1,8 +1,16 @@ -Terapkan pagination pada file: screens/Forum/DetailForum.tsx -Component yang digunakan: hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx -Perbaiki fetch pada file: service/api-client/api-forum.ts +File utama: screens/Notification/ScreenNotification.tsx +Fun fecth: apiGetNotificationsById +File fetch: service/api-notifications.ts +File komponen wrapper: components/_ShareComponent/NewWrapper.tsx + +Terapkan pagination pada file "File utama" +Analisa juga file "File utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper + +Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx + +Perbaiki fetch "Fun fecth" , pada file "File fetch" Jika tidak ada props page maka tambahkan props page dan default page: "1" Gunakan bahasa indonesia pada cli agar saya mudah membacanya. diff --git a/screens/Forum/DetailForum2.tsx b/screens/Forum/DetailForum2.tsx index 610c911..d6ede3b 100644 --- a/screens/Forum/DetailForum2.tsx +++ b/screens/Forum/DetailForum2.tsx @@ -5,6 +5,7 @@ import { LoaderCustom, NewWrapper, Spacing, + StackCustom, TextAreaCustom, TextCustom, TextInputCustom, @@ -111,7 +112,6 @@ export default function DetailForum2() { // Create Commentar const handlerCreateCommentar = async () => { - const cencorContent = censorText(text); const newData = { @@ -155,7 +155,10 @@ export default function DetailForum2() { const headerComponent = () => // Box Posting !data ? ( - + + + + ) : ( <> {/* Area Posting */} @@ -199,10 +202,7 @@ export default function DetailForum2() { ); // Render individual comment item - const renderCommentItem = ({ item }: { item: TypeForum_CommentProps }) => - !data && !commentPagination.listData ? ( - - ) : ( + const renderCommentItem = ({ item }: { item: TypeForum_CommentProps }) =>( - ); + ) + // !data || !commentPagination.listData ? ( + // // + // + // ) : ( + // { + // setCommentId(value.setCommentId); + // setOpenDrawerCommentar(value.setOpenDrawer); + // setCommentAuthorId(value.setCommentAuthorId); + // }} + // /> + // ); // Generate pagination components using helper const { ListEmptyComponent, ListFooterComponent } = diff --git a/screens/Forum/ViewBeranda3.tsx b/screens/Forum/ViewBeranda3.tsx index f119d65..1b5455f 100644 --- a/screens/Forum/ViewBeranda3.tsx +++ b/screens/Forum/ViewBeranda3.tsx @@ -102,7 +102,7 @@ export default function Forum_ViewBeranda3() { + setSearch(text), 500)} diff --git a/screens/Notification/ScreenNotification.tsx b/screens/Notification/ScreenNotification.tsx new file mode 100644 index 0000000..bdef3c9 --- /dev/null +++ b/screens/Notification/ScreenNotification.tsx @@ -0,0 +1,249 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + AlertDefaultSystem, + BackButton, + BaseBox, + DrawerCustom, + MenuDrawerDynamicGrid, + NewWrapper, + ScrollableCustom, + StackCustom, + TextCustom, +} from "@/components"; +import { IconDot } from "@/components/_Icon/IconComponent"; +import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; +import NoDataText from "@/components/_ShareComponent/NoDataText"; +import { AccentColor, MainColor } from "@/constants/color-palet"; +import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; +import { useAuth } from "@/hooks/use-auth"; +import { useNotificationStore } from "@/hooks/use-notification-store"; +import { usePagination } from "@/hooks/use-pagination"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import { apiGetNotificationsById } from "@/service/api-notifications"; +import { listOfcategoriesAppNotification } from "@/types/type-notification-category"; +import { formatChatTime } from "@/utils/formatChatTime"; +import { Ionicons } from "@expo/vector-icons"; +import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import { RefreshControl, View } from "react-native"; + +const selectedCategory = (value: string) => { + const category = listOfcategoriesAppNotification.find( + (c) => c.value === value + ); + return category?.label; +}; + +const fixPath = ({ + deepLink, + categoryApp, +}: { + deepLink: string; + categoryApp: string; +}) => { + if (categoryApp === "OTHER") { + return deepLink; + } + + const separator = deepLink.includes("?") ? "&" : "?"; + + const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase( + categoryApp + )}`; + + console.log("Fix Path", fixedPath); + + return fixedPath; +}; + +const BoxNotification = ({ + data, + activeCategory, +}: { + data: any; + activeCategory: string | null; +}) => { + // console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2)); + const { markAsRead } = useNotificationStore(); + return ( + <> + { + // console.log( + // "Notification >", + // selectedCategory(activeCategory as string) + // ); + const newPath = fixPath({ + deepLink: data.deepLink, + categoryApp: data.kategoriApp, + }); + + router.navigate(newPath as any); + selectedCategory(activeCategory as string); + + if (!data.isRead) { + markAsRead(data.id); + } + }} + > + + + {data.title} + + + {data.pesan} + + + {formatChatTime(data.createdAt)} + + + + + ); +}; + +export default function ScreenNotification() { + const { user } = useAuth(); + const { category } = useLocalSearchParams<{ category?: string }>(); + const [activeCategory, setActiveCategory] = useState( + category || "event" + ); + const [openDrawer, setOpenDrawer] = useState(false); + + const { markAsReadAll } = useNotificationStore(); + + // Initialize pagination for notifications + const pagination = usePagination({ + fetchFunction: async (page) => { + return await apiGetNotificationsById({ + id: user?.id as string, + category: activeCategory as any, + page: String(page), // API expects string + }); + }, + pageSize: PAGINATION_DEFAULT_TAKE, + dependencies: [activeCategory], + }); + + useFocusEffect( + useCallback(() => { + // Reset and load first page when category changes + pagination.reset(); + pagination.onRefresh(); + }, [activeCategory]) + ); + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // Reset and load first page when category changes + pagination.reset(); + pagination.onRefresh(); + }; + + // Render individual notification item + const renderItem = ({ item }: { item: any }) => ( + + + + ); + + // Generate pagination components using helper + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + isInitialLoad: pagination.isInitialLoad, + emptyMessage: "Belum ada notifikasi", + skeletonCount: 5, + skeletonHeight: 100, + }); + + return ( + <> + , + headerRight: () => ( + setOpenDrawer(true)} + /> + ), + }} + /> + + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as string} + /> + } + listData={pagination.listData} + renderItem={renderItem} + refreshControl={ + + } + onEndReached={pagination.loadMore} + ListFooterComponent={ListFooterComponent} + ListEmptyComponent={ListEmptyComponent} + /> + + setOpenDrawer(false)} + height={"auto"} + > + + ), + path: "", + }, + ]} + onPressItem={(item: any) => { + console.log("Item", item.value); + if (item.value === "read-all") { + AlertDefaultSystem({ + title: "Tandai Semua Dibaca", + message: + "Apakah Anda yakin ingin menandai semua notifikasi dibaca?", + textLeft: "Batal", + textRight: "Ya", + onPressRight: () => { + markAsReadAll(user?.id as any); + // Reset and refresh data after marking all as read + pagination.reset(); + pagination.onRefresh(); + setOpenDrawer(false); + }, + }); + } + }} + /> + + + ); +} diff --git a/screens/Portofolio/BoxPortofolioView.tsx b/screens/Portofolio/BoxPortofolioView.tsx index 02142bf..ce5e92d 100644 --- a/screens/Portofolio/BoxPortofolioView.tsx +++ b/screens/Portofolio/BoxPortofolioView.tsx @@ -1,5 +1,5 @@ import { BaseBox, Grid, TextCustom } from "@/components"; -import { MainColor } from "@/constants/color-palet"; +import { AccentColor, MainColor } from "@/constants/color-palet"; import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { Ionicons } from "@expo/vector-icons"; import { router } from "expo-router"; @@ -8,7 +8,7 @@ export default function Portofolio_BoxView({ data }: { data: any }) { return ( <> { router.push(`/portofolio/${data?.id}`); }} diff --git a/screens/Portofolio/ViewListPortofolio.tsx b/screens/Portofolio/ViewListPortofolio.tsx new file mode 100644 index 0000000..2049feb --- /dev/null +++ b/screens/Portofolio/ViewListPortofolio.tsx @@ -0,0 +1,74 @@ +import { NewWrapper, TextCustom } from "@/components"; +import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom"; +import { MainColor } from "@/constants/color-palet"; +import { usePagination } from "@/hooks/use-pagination"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import { apiGetPortofolio } from "@/service/api-client/api-portofolio"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import { useCallback } from "react"; +import { RefreshControl } from "react-native"; +import Portofolio_BoxView from "./BoxPortofolioView"; +import NoDataText from "@/components/_ShareComponent/NoDataText"; +import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; + +export default function ViewListPortofolio() { + const { id } = useLocalSearchParams(); + + // Initialize pagination for portfolio items + const pagination = usePagination({ + fetchFunction: async (page) => { + return await apiGetPortofolio({ + id: id as string, + page: String(page) // API expects string + }); + // return response.data; + }, + pageSize: PAGINATION_DEFAULT_TAKE, + dependencies: [id], + }); + + useFocusEffect( + useCallback(() => { + // Reset and load first page when id changes + pagination.reset(); + pagination.onRefresh(); + }, [id]), + ); + + // Render individual portfolio item + const renderItem = ({ item }: { item: any }) => ( + + ); + + // Generate pagination components using helper + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + isInitialLoad: pagination.isInitialLoad, + emptyMessage: "Tidak ada portofolio", + skeletonCount: 3, + skeletonHeight: 100, + }); + + return ( + + } + onEndReached={pagination.loadMore} + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + /> + ); +} diff --git a/screens/Profile/PortofolioSection.tsx b/screens/Profile/PortofolioSection.tsx index 05d8cc5..edbde25 100644 --- a/screens/Profile/PortofolioSection.tsx +++ b/screens/Profile/PortofolioSection.tsx @@ -12,7 +12,7 @@ export default function Profile_PortofolioSection({ }) { return ( <> - + Portofolio diff --git a/screens/UserSeach/MainView_V2.tsx b/screens/UserSeach/MainView_V2.tsx index 22539b0..0be71d8 100644 --- a/screens/UserSeach/MainView_V2.tsx +++ b/screens/UserSeach/MainView_V2.tsx @@ -1,14 +1,17 @@ import { - AvatarComp, - ClickableCustom, - Grid, - NewWrapper, - StackCustom, - TextCustom, - TextInputCustom + AvatarComp, + ClickableCustom, + Grid, + NewWrapper, + StackCustom, + TextCustom, + TextInputCustom, } from "@/components"; import { MainColor } from "@/constants/color-palet"; -import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; +import { + ICON_SIZE_SMALL, + PAGINATION_DEFAULT_TAKE, +} from "@/constants/constans-value"; import { usePagination } from "@/hooks/use-pagination"; import { apiAllUser } from "@/service/api-client/api-user"; import { Ionicons } from "@expo/vector-icons"; @@ -18,7 +21,6 @@ import { useCallback, useRef, useState } from "react"; import { RefreshControl, View } from "react-native"; import { createPaginationComponents } from "@/helpers/paginationHelpers"; - export default function UserSearchMainView_V2() { const isInitialMount = useRef(true); const [search, setSearch] = useState(""); @@ -30,7 +32,7 @@ export default function UserSearchMainView_V2() { hasMore, onRefresh, loadMore, - isInitialLoad + isInitialLoad, } = usePagination({ fetchFunction: async (page, searchQuery) => { const response = await apiAllUser({ @@ -83,7 +85,7 @@ export default function UserSearchMainView_V2() { padding: 12, marginBottom: 10, elevation: 2, - shadowColor: '#000', + shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2, @@ -129,18 +131,19 @@ export default function UserSearchMainView_V2() { ); - const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ - loading, - refreshing, - listData, - searchQuery: search, - emptyMessage: "Tidak ada pengguna ditemukan", - emptySearchMessage: "Tidak ada hasil pencarian", - skeletonCount: 5, - skeletonHeight: 150, - loadingFooterText: "Memuat lebih banyak pengguna...", - isInitialLoad - }); + const { ListEmptyComponent, ListFooterComponent } = + createPaginationComponents({ + loading, + refreshing, + listData, + searchQuery: search, + emptyMessage: "Tidak ada pengguna ditemukan", + emptySearchMessage: "Tidak ada hasil pencarian", + skeletonCount: 5, + skeletonHeight: 150, + loadingFooterText: "Memuat lebih banyak pengguna...", + isInitialLoad, + }); return ( <> @@ -161,14 +164,4 @@ export default function UserSearchMainView_V2() { /> ); - - // return ( - // <> - // - // - // {JSON.stringify(listData, null, 2)} - // - // - // - // ); } diff --git a/service/api-client/api-portofolio.ts b/service/api-client/api-portofolio.ts index 5424ea6..adfee66 100644 --- a/service/api-client/api-portofolio.ts +++ b/service/api-client/api-portofolio.ts @@ -12,9 +12,9 @@ export async function apiPortofolioCreate({ data }: { data: any }) { } } -export async function apiGetPortofolio({ id }: { id: string }) { +export async function apiGetPortofolio({ id, page = "1" }: { id: string; page?: string }) { try { - const response = await apiConfig.get(`/mobile/portofolio?id=${id}`); + const response = await apiConfig.get(`/mobile/portofolio?id=${id}&page=${page}`); return response.data; } catch (error) { diff --git a/service/api-notifications.ts b/service/api-notifications.ts index a20dbeb..212e335 100644 --- a/service/api-notifications.ts +++ b/service/api-notifications.ts @@ -41,16 +41,19 @@ export async function apiNotificationsSendById({ export async function apiGetNotificationsById({ id, category, + page = "1", }: { id: string; category: TypeNotificationCategoryApp; + page?: string; }) { console.log("ID", id); console.log("Category", category); + console.log("Page", page); try { const response = await apiConfig.get( - `/mobile/notification/${id}?category=${category}` + `/mobile/notification/${id}?category=${category}&page=${page}` ); return response.data; -- 2.49.1