From 48196cd46baa8d990da1e7350c4e4825447a3f23 Mon Sep 17 00:00:00 2001 From: bagasbanuna Date: Mon, 2 Feb 2026 17:09:58 +0800 Subject: [PATCH] =?UTF-8?q?Fix=20Load=20data=20pada=20halaman=20yang=20mem?= =?UTF-8?q?butuhkan=20infinite=20load=20Job=20=E2=80=93=20User=20App=20-?= =?UTF-8?q?=20app/(application)/(user)/job/(tabs)/index.tsx=20-=20app/(app?= =?UTF-8?q?lication)/(user)/job/(tabs)/status.tsx=20-=20app/(application)/?= =?UTF-8?q?(user)/job/(tabs)/archive.tsx=20-=20app/(application)/(user)/jo?= =?UTF-8?q?b/create.tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Job – Screens - screens/Job/ScreenBeranda.tsx - screens/Job/ScreenBeranda2.tsx - screens/Job/MainViewStatus.tsx - screens/Job/MainViewStatus2.tsx - screens/Job/ScreenArchive.tsx - screens/Job/ScreenArchive2.tsx API – Job (Client) - service/api-client/api-job.ts Notification - screens/Notification/ScreenNotification.tsx Docs - QWEN.md - docs/prompt-for-qwen-code.md ### No Issue --- QWEN.md | 179 ------------------ .../(user)/job/(tabs)/archive.tsx | 55 +----- app/(application)/(user)/job/(tabs)/index.tsx | 83 +------- .../(user)/job/(tabs)/status.tsx | 87 +-------- app/(application)/(user)/job/create.tsx | 20 +- docs/prompt-for-qwen-code.md | 6 +- screens/Job/MainViewStatus.tsx | 91 +++++++++ screens/Job/MainViewStatus2.tsx | 111 +++++++++++ screens/Job/ScreenArchive.tsx | 57 ++++++ screens/Job/ScreenArchive2.tsx | 76 ++++++++ screens/Job/ScreenBeranda.tsx | 83 ++++++++ screens/Job/ScreenBeranda2.tsx | 105 ++++++++++ screens/Notification/ScreenNotification.tsx | 1 + service/api-client/api-job.ts | 9 +- 14 files changed, 557 insertions(+), 406 deletions(-) create mode 100644 screens/Job/MainViewStatus.tsx create mode 100644 screens/Job/MainViewStatus2.tsx create mode 100644 screens/Job/ScreenArchive.tsx create mode 100644 screens/Job/ScreenArchive2.tsx create mode 100644 screens/Job/ScreenBeranda.tsx create mode 100644 screens/Job/ScreenBeranda2.tsx diff --git a/QWEN.md b/QWEN.md index 322c6f0..e69de29 100644 --- a/QWEN.md +++ b/QWEN.md @@ -1,179 +0,0 @@ -# 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)/job/(tabs)/archive.tsx b/app/(application)/(user)/job/(tabs)/archive.tsx index c885bcd..050a3c1 100644 --- a/app/(application)/(user)/job/(tabs)/archive.tsx +++ b/app/(application)/(user)/job/(tabs)/archive.tsx @@ -1,57 +1,10 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components"; -import { useAuth } from "@/hooks/use-auth"; -import { apiJobGetAll } from "@/service/api-client/api-job"; -import { useFocusEffect } from "expo-router"; -import _ from "lodash"; -import { useCallback, useState } from "react"; +import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2"; export default function JobArchive() { - const { user } = useAuth(); - const [listData, setListData] = useState([]); - const [isLoadData, setIsLoadData] = useState(false); - - useFocusEffect( - useCallback(() => { - onLoadData(); - }, [user?.id]) - ); - - const onLoadData = async () => { - try { - setIsLoadData(true); - const response = await apiJobGetAll({ - category: "archive", - authorId: user?.id, - }); - setListData(response.data); - } catch (error) { - console.log("[ERROR]", error); - } finally { - setIsLoadData(false); - } - }; - return ( - - {isLoadData ? ( - - ) : _.isEmpty(listData) ? ( - Anda tidak memiliki arsip - ) : ( - listData.map((item, index) => ( - - - {item?.title || "-"} - - - )) - )} - + <> + + ); } diff --git a/app/(application)/(user)/job/(tabs)/index.tsx b/app/(application)/(user)/job/(tabs)/index.tsx index 6910470..32dfbb6 100644 --- a/app/(application)/(user)/job/(tabs)/index.tsx +++ b/app/(application)/(user)/job/(tabs)/index.tsx @@ -1,83 +1,10 @@ -import { - AvatarUsernameAndOtherComponent, - BoxWithHeaderSection, - FloatingButton, - LoaderCustom, - SearchInput, - Spacing, - StackCustom, - TextCustom, - ViewWrapper, -} from "@/components"; -import { apiJobGetAll } from "@/service/api-client/api-job"; -import { router, useFocusEffect } from "expo-router"; -import _ from "lodash"; -import { useCallback, useState } from "react"; +import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda"; +import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2"; export default function JobBeranda() { - const [listData, setListData] = useState([]); - const [isLoadData, setIsLoadData] = useState(false); - const [search, setSearch] = useState(""); - - useFocusEffect( - useCallback(() => { - onLoadData(search); - }, [search]) - ); - - const onLoadData = async (search: string) => { - try { - setIsLoadData(true); - const response = await apiJobGetAll({ search, category: "beranda" }); - setListData(response.data); - } catch (error) { - console.log("[ERROR]", error); - } finally { - setIsLoadData(false); - } - }; - - const handleSearch = (search: string) => { - setSearch(search); - onLoadData(search); - }; - return ( - router.push("/job/create")} /> - } - headerComponent={ - - } - > - {isLoadData ? ( - - ) : _.isEmpty(listData) ? ( - Belum ada lowongan - ) : ( - listData.map((item, index) => ( - router.push(`/job/${item.id}`)} - > - - - - - {item?.title || "-"} - - - - - )) - )} - - + <> + + ); } diff --git a/app/(application)/(user)/job/(tabs)/status.tsx b/app/(application)/(user)/job/(tabs)/status.tsx index 87e0f22..6d5dd4d 100644 --- a/app/(application)/(user)/job/(tabs)/status.tsx +++ b/app/(application)/(user)/job/(tabs)/status.tsx @@ -1,91 +1,12 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import { - BaseBox, - LoaderCustom, - ScrollableCustom, - TextCustom, - ViewWrapper, -} from "@/components"; -import { useAuth } from "@/hooks/use-auth"; -import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; -import { apiJobGetByStatus } from "@/service/api-client/api-job"; -import { useFocusEffect, useLocalSearchParams } from "expo-router"; -import _ from "lodash"; -import { useCallback, useState } from "react"; +import Job_MainViewStatus from "@/screens/Job/MainViewStatus"; +import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2"; export default function JobStatus() { - const { user } = useAuth(); - const { status } = useLocalSearchParams<{ status?: string }>(); - console.log("STATUS", status); - - const [activeCategory, setActiveCategory] = useState( - status || "publish" - ); - const [listData, setListData] = useState([]); - const [isLoadList, setIsLoadList] = useState(false); - - useFocusEffect( - useCallback(() => { - onLoadData(); - }, [user?.id, activeCategory]) - ); - - const onLoadData = async () => { - try { - setIsLoadList(true); - const response = await apiJobGetByStatus({ - authorId: user?.id as string, - status: activeCategory as string, - }); - setListData(response.data); - } catch (error) { - console.log("[ERROR]", error); - } finally { - setIsLoadList(false); - } - }; - - const handlePress = (item: any) => { - setActiveCategory(item.value); - // tambahkan logika lain seperti filter dsb. - }; - - const scrollComponent = ( - ({ - id: i, - label: e.label, - value: e.value, - }))} - onButtonPress={handlePress} - activeId={activeCategory as any} - /> - ); - return ( <> - - {isLoadList ? ( - - ) : _.isEmpty(listData) ? ( - - Tidak ada data {activeCategory} - - ) : ( - listData.map((e, i) => ( - - - {e?.title} - - - )) - )} - + {/* */} + ); } diff --git a/app/(application)/(user)/job/create.tsx b/app/(application)/(user)/job/create.tsx index 1879e2f..14fc947 100644 --- a/app/(application)/(user)/job/create.tsx +++ b/app/(application)/(user)/job/create.tsx @@ -1,13 +1,14 @@ import { + BoxButtonOnFooter, ButtonCenteredOnly, ButtonCustom, InformationBox, LandscapeFrameUploaded, + NewWrapper, Spacing, StackCustom, TextAreaCustom, - TextInputCustom, - ViewWrapper + TextInputCustom } from "@/components"; import DIRECTORY_ID from "@/constants/directory-id"; import { useAuth } from "@/hooks/use-auth"; @@ -99,16 +100,17 @@ export default function JobCreate() { const buttonSubmit = () => { return ( <> - handlerOnSubmit()}> - Simpan - - + + handlerOnSubmit()}> + Simpan + + ); }; return ( - + @@ -160,9 +162,7 @@ export default function JobCreate() { value={data.deskripsi} onChangeText={(value) => setData({ ...data, deskripsi: value })} /> - - {buttonSubmit()} - + ); } diff --git a/docs/prompt-for-qwen-code.md b/docs/prompt-for-qwen-code.md index 3bab661..6cca10e 100644 --- a/docs/prompt-for-qwen-code.md +++ b/docs/prompt-for-qwen-code.md @@ -1,8 +1,8 @@ -File utama: screens/Notification/ScreenNotification.tsx -Fun fecth: apiGetNotificationsById -File fetch: service/api-notifications.ts +File utama: screens/Job/ScreenArchive2.tsx +Fun fecth: apiJobGetByStatus +File fetch: service/api-client/api-job.ts File komponen wrapper: components/_ShareComponent/NewWrapper.tsx Terapkan pagination pada file "File utama" diff --git a/screens/Job/MainViewStatus.tsx b/screens/Job/MainViewStatus.tsx new file mode 100644 index 0000000..4be96f4 --- /dev/null +++ b/screens/Job/MainViewStatus.tsx @@ -0,0 +1,91 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + LoaderCustom, + ScrollableCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; +import { apiJobGetByStatus } from "@/service/api-client/api-job"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function Job_MainViewStatus() { + const { user } = useAuth(); + const { status } = useLocalSearchParams<{ status?: string }>(); + console.log("STATUS", status); + + const [activeCategory, setActiveCategory] = useState( + status || "publish" + ); + const [listData, setListData] = useState([]); + const [isLoadList, setIsLoadList] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id, activeCategory]) + ); + + const onLoadData = async () => { + try { + setIsLoadList(true); + const response = await apiJobGetByStatus({ + authorId: user?.id as string, + status: activeCategory as string, + }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadList(false); + } + }; + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // tambahkan logika lain seperti filter dsb. + }; + + const scrollComponent = ( + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as any} + /> + ); + + return ( + <> + + {isLoadList ? ( + + ) : _.isEmpty(listData) ? ( + + Tidak ada data {activeCategory} + + ) : ( + listData.map((e, i) => ( + + + {e?.title} + + + )) + )} + + + ); +} diff --git a/screens/Job/MainViewStatus2.tsx b/screens/Job/MainViewStatus2.tsx new file mode 100644 index 0000000..13385e3 --- /dev/null +++ b/screens/Job/MainViewStatus2.tsx @@ -0,0 +1,111 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { + BaseBox, + ScrollableCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import { useAuth } from "@/hooks/use-auth"; +import { usePagination } from "@/hooks/use-pagination"; +import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; +import { apiJobGetByStatus } from "@/service/api-client/api-job"; +import { useFocusEffect, useLocalSearchParams } from "expo-router"; +import _ from "lodash"; +import { useState } from "react"; +import { RefreshControl, View } from "react-native"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; + +export default function Job_MainViewStatus2() { + const { user } = useAuth(); + const { status } = useLocalSearchParams<{ status?: string }>(); + console.log("STATUS", status); + + const [activeCategory, setActiveCategory] = useState( + status || "publish" + ); + + // Setup pagination + const pagination = usePagination({ + fetchFunction: async (page) => { + if (!user?.id) return { data: [] }; + + return await apiJobGetByStatus({ + authorId: user?.id as string, + status: activeCategory as string, + page: String(page), + }); + }, + pageSize: PAGINATION_DEFAULT_TAKE, + dependencies: [user?.id, activeCategory], + onError: (error) => console.error("[ERROR] Fetch job by status:", error), + }); + + // Generate komponen + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + emptyMessage: `Tidak ada data ${activeCategory}`, + skeletonCount: PAGINATION_DEFAULT_TAKE, + skeletonHeight: 100, + }); + + // Render item job + const renderJobItem = ({ item }: { item: any }) => ( + + + {item?.title} + + + ); + + const handlePress = (item: any) => { + setActiveCategory(item.value); + // Reset pagination saat kategori berubah + pagination.reset(); + }; + + const scrollComponent = ( + ({ + id: i, + label: e.label, + value: e.value, + }))} + onButtonPress={handlePress} + activeId={activeCategory as any} + /> + ); + + return ( + + {scrollComponent} + + } + listData={pagination.listData} + renderItem={renderJobItem} + refreshControl={ + + } + onEndReached={pagination.loadMore} + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + hideFooter + /> + ); +} diff --git a/screens/Job/ScreenArchive.tsx b/screens/Job/ScreenArchive.tsx new file mode 100644 index 0000000..f9d93fd --- /dev/null +++ b/screens/Job/ScreenArchive.tsx @@ -0,0 +1,57 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components"; +import { useAuth } from "@/hooks/use-auth"; +import { apiJobGetAll } from "@/service/api-client/api-job"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function Job_ScreenArchive() { + const { user } = useAuth(); + const [listData, setListData] = useState([]); + const [isLoadData, setIsLoadData] = useState(false); + + useFocusEffect( + useCallback(() => { + onLoadData(); + }, [user?.id]) + ); + + const onLoadData = async () => { + try { + setIsLoadData(true); + const response = await apiJobGetAll({ + category: "archive", + authorId: user?.id, + }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + return ( + + {isLoadData ? ( + + ) : _.isEmpty(listData) ? ( + Anda tidak memiliki arsip + ) : ( + listData.map((item, index) => ( + + + {item?.title || "-"} + + + )) + )} + + ); +} diff --git a/screens/Job/ScreenArchive2.tsx b/screens/Job/ScreenArchive2.tsx new file mode 100644 index 0000000..256a831 --- /dev/null +++ b/screens/Job/ScreenArchive2.tsx @@ -0,0 +1,76 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { BaseBox, TextCustom, ViewWrapper } from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import { useAuth } from "@/hooks/use-auth"; +import { usePagination } from "@/hooks/use-pagination"; +import { apiJobGetAll } from "@/service/api-client/api-job"; +import { useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useState } from "react"; +import { RefreshControl } from "react-native"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; + +export default function Job_ScreenArchive2() { + const { user } = useAuth(); + + // Setup pagination + const pagination = usePagination({ + fetchFunction: async (page) => { + if (!user?.id) return { data: [] }; + + return await apiJobGetAll({ + category: "archive", + authorId: user?.id, + page: String(page), + }); + }, + pageSize: PAGINATION_DEFAULT_TAKE, + dependencies: [user?.id], + onError: (error) => console.error("[ERROR] Fetch job archive:", error), + }); + + // Generate komponen + const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + emptyMessage: "Anda tidak memiliki arsip", + skeletonCount: PAGINATION_DEFAULT_TAKE, + skeletonHeight: 80, + }); + + // Render item job + const renderJobItem = ({ item }: { item: any }) => ( + + + {item?.title || "-"} + + + ); + + return ( + + } + onEndReached={pagination.loadMore} + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + hideFooter + /> + ); +} diff --git a/screens/Job/ScreenBeranda.tsx b/screens/Job/ScreenBeranda.tsx new file mode 100644 index 0000000..452b329 --- /dev/null +++ b/screens/Job/ScreenBeranda.tsx @@ -0,0 +1,83 @@ +import { + AvatarUsernameAndOtherComponent, + BoxWithHeaderSection, + FloatingButton, + LoaderCustom, + SearchInput, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { apiJobGetAll } from "@/service/api-client/api-job"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useCallback, useState } from "react"; + +export default function Job_ScreenBeranda() { + const [listData, setListData] = useState([]); + const [isLoadData, setIsLoadData] = useState(false); + const [search, setSearch] = useState(""); + + useFocusEffect( + useCallback(() => { + onLoadData(search); + }, [search]) + ); + + const onLoadData = async (search: string) => { + try { + setIsLoadData(true); + const response = await apiJobGetAll({ search, category: "beranda" }); + setListData(response.data); + } catch (error) { + console.log("[ERROR]", error); + } finally { + setIsLoadData(false); + } + }; + + const handleSearch = (search: string) => { + setSearch(search); + onLoadData(search); + }; + + return ( + router.push("/job/create")} /> + } + headerComponent={ + + } + > + {isLoadData ? ( + + ) : _.isEmpty(listData) ? ( + Belum ada lowongan + ) : ( + listData.map((item, index) => ( + router.push(`/job/${item.id}`)} + > + + + + + {item?.title || "-"} + + + + + )) + )} + + + ); +} diff --git a/screens/Job/ScreenBeranda2.tsx b/screens/Job/ScreenBeranda2.tsx new file mode 100644 index 0000000..77a09b5 --- /dev/null +++ b/screens/Job/ScreenBeranda2.tsx @@ -0,0 +1,105 @@ +import { + AvatarUsernameAndOtherComponent, + BoxWithHeaderSection, + FloatingButton, + SearchInput, + Spacing, + StackCustom, + TextCustom, + ViewWrapper, +} from "@/components"; +import { MainColor } from "@/constants/color-palet"; +import { createPaginationComponents } from "@/helpers/paginationHelpers"; +import { usePagination } from "@/hooks/use-pagination"; +import { apiJobGetAll } from "@/service/api-client/api-job"; +import { router, useFocusEffect } from "expo-router"; +import _ from "lodash"; +import { useState } from "react"; +import { RefreshControl, View } from "react-native"; +import NewWrapper from "@/components/_ShareComponent/NewWrapper"; +import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value"; + +const PAGE_SIZE = 10; + +export default function Job_ScreenBeranda2() { + const [search, setSearch] = useState(""); + + // Setup pagination + const pagination = usePagination({ + fetchFunction: async (page, searchQuery) => { + return await apiJobGetAll({ + search: searchQuery || "", + category: "beranda", + page: String(page), + }); + }, + pageSize: PAGINATION_DEFAULT_TAKE, + searchQuery: search, + dependencies: [], + onError: (error) => console.error("[ERROR] Fetch job:", error), + }); + + // Generate komponen + const { ListEmptyComponent, ListFooterComponent } = + createPaginationComponents({ + loading: pagination.loading, + refreshing: pagination.refreshing, + listData: pagination.listData, + searchQuery: search, + emptyMessage: "Belum ada lowongan", + emptySearchMessage: "Tidak ada hasil pencarian", + skeletonCount: 5, + skeletonHeight: 150, + }); + + // Render item job + const renderJobItem = ({ item }: { item: any }) => ( + router.push(`/job/${item.id}`)} + > + + + + + {item?.title || "-"} + + + + + ); + + return ( + + setSearch(text), 500)} + /> + + } + floatingButton={ + router.push("/job/create")} /> + } + listData={pagination.listData} + renderItem={renderJobItem} + refreshControl={ + + } + onEndReached={pagination.loadMore} + ListEmptyComponent={ListEmptyComponent} + ListFooterComponent={ListFooterComponent} + /> + ); +} diff --git a/screens/Notification/ScreenNotification.tsx b/screens/Notification/ScreenNotification.tsx index bdef3c9..e8dfcd7 100644 --- a/screens/Notification/ScreenNotification.tsx +++ b/screens/Notification/ScreenNotification.tsx @@ -136,6 +136,7 @@ export default function ScreenNotification() { ); const handlePress = (item: any) => { + console.log("ITEM", item.value); setActiveCategory(item.value); // Reset and load first page when category changes pagination.reset(); diff --git a/service/api-client/api-job.ts b/service/api-client/api-job.ts index e3cde9e..22e31a7 100644 --- a/service/api-client/api-job.ts +++ b/service/api-client/api-job.ts @@ -14,12 +14,14 @@ export async function apiJobCreate(data: any) { export async function apiJobGetByStatus({ authorId, status, + page = "1", }: { authorId: string; status: string; + page?: string; }) { try { - const response = await apiConfig.get(`/mobile/job/${authorId}/${status}`); + const response = await apiConfig.get(`/mobile/job/${authorId}/${status}?page=${page}`); return response.data; } catch (error) { throw error; @@ -63,10 +65,12 @@ export async function apiJobGetAll({ search, category, authorId, + page = "1", }: { search?: string; category: "archive" | "beranda"; authorId?: string; + page?: string; }) { try { let categoryText = category ? `?category=${category}` : ""; @@ -74,8 +78,9 @@ export async function apiJobGetAll({ categoryText = `?category=${category}&authorId=${authorId}`; } const searchText = search ? `&search=${search}` : ""; + const pageText = `&page=${page}`; const response = await apiConfig.get( - `/mobile/job${categoryText}${searchText}` + `/mobile/job${categoryText}${searchText}${pageText}` ); return response.data; } catch (error) {