Compare commits

...

9 Commits

Author SHA1 Message Date
c570a19d84 Fix Wrapper
UI – Investment (User)
- app/(application)/(user)/investment/create.tsx
- app/(application)/(user)/investment/[id]/edit.tsx

### No Issue"
2026-02-05 17:30:17 +08:00
7415c8c8ce Fix path Notification admin
UI – Notifications (User)
- app/(application)/(user)/notifications/index.tsx
- screens/Notification/ScreenNotification_V2.tsx

UI – Notifications (Admin)
- screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx

API
- service/api-notifications.ts

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-05 16:37:50 +08:00
72a3d42013 Fix Loaddata Voting
UI – Voting (User)
- app/(application)/(user)/voting/(tabs)/index.tsx
- app/(application)/(user)/voting/(tabs)/history.tsx
- app/(application)/(user)/voting/(tabs)/contribution.tsx
- app/(application)/(user)/voting/create.tsx
- app/(application)/(user)/voting/[id]/edit.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- app/(application)/(user)/voting/[id]/list-of-contributor.tsx

UI – Event
- app/(application)/(user)/event/[id]/edit.tsx
- screens/Event/ScreenStatus.tsx

UI – Voting Screens (New)
- screens/Voting/ScreenBeranda.tsx
- screens/Voting/ScreenContribution.tsx
- screens/Voting/ScreenHistory.tsx
- screens/Voting/ScreenListOfContributor.tsx
- screens/Voting/ScreenStatus.tsx
- screens/Voting/ButtonStatusSection.tsx

UI – Job
- screens/Job/ButtonStatusSection.tsx
- screens/Job/MainViewStatus2.tsx

API Client
- service/api-client/api-voting.ts

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-05 15:06:14 +08:00
d0abd14047 Fix Loaddata Voting
Voting – User
- app/(application)/(user)/voting/(tabs)/status.tsx
- app/(application)/(user)/voting/create.tsx

Screens – Voting
- screens/Voting/ButtonStatusSection.tsx

API Client
- service/api-client/api-voting.ts

Global
- app/+not-found.tsx
- styles/global-styles.ts

Docs
- docs/prompt-for-qwen-code.md

Untracked (New Files)
- screens/Voting/ScreenStatus.tsx

### No issue
2026-02-04 17:44:57 +08:00
5b2be20469 Fix Loaddata pada event dan perbaikan tampilan pada NewWrapper
Event – User
- app/(application)/(user)/event/(tabs)/contribution.tsx
- app/(application)/(user)/event/(tabs)/index.tsx
- app/(application)/(user)/event/[id]/list-of-participants.tsx

Voting – User
- app/(application)/(user)/voting/(tabs)/history.tsx

Components
- components/Notification/NotificationInitializer.tsx
- components/_ShareComponent/NewWrapper.tsx

Screens – Event
- screens/Event/BoxPublishSection.tsx
- screens/Event/ButtonStatusSection.tsx
- screens/Event/ScreenHistory.tsx
- screens/Event/ScreenStatus.tsx

Screens – Forum
- screens/Forum/ViewBeranda3.tsx

API Client
- service/api-client/api-event.ts

Styles
- styles/global-styles.ts

Docs
- docs/prompt-for-qwen-code.md

Untracked (New Files)
- screens/Event/ScreenBeranda.tsx
- screens/Event/ScreenContribution.tsx
- screens/Event/ScreenListOfParticipants.tsx

#### No Issue
2026-02-04 16:56:48 +08:00
60177a1087 Fix Component Datetime IOS
Components
- components/DateInput/DataTimeAndroid.tsx
- components/DateInput/DateTimeIOS.tsx
- components/Notification/NotificationInitializer.tsx

Screens
- screens/Event/ScreenStatus.tsx

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-04 12:00:00 +08:00
771ae45f26 Fix load data event
Event – User
- app/(application)/(user)/event/(tabs)/history.tsx
- app/(application)/(user)/event/(tabs)/status.tsx
- app/(application)/(user)/event/[id]/edit.tsx
- app/(application)/(user)/event/create.tsx

Event – Screens (Untracked)
- screens/Event/ScreenHistory.tsx
- screens/Event/ScreenStatus.tsx

API
- service/api-client/api-event.ts

Docs
- docs/prompt-for-qwen-code.md

### No Issue
2026-02-03 17:45:27 +08:00
41f4a8ac99 Fix load data notification
Notification – User
- app/(application)/(user)/notifications/index.tsx
- screens/Notification/ScreenNotification_V1.tsx
- screens/Notification/ScreenNotification_V2.tsx

Notification – Admin
- app/(application)/admin/notification/index.tsx
- screens/Admin/Notification-Admin/

Job
- screens/Job/MainViewStatus2.tsx

Docs
- docs/prompt-for-qwen-code.md

Deleted
- screens/Notification/ScreenNotification.tsx

### No Issue
2026-02-03 16:59:09 +08:00
48196cd46b Fix Load data pada halaman yang membutuhkan infinite load
Job – User App
- app/(application)/(user)/job/(tabs)/index.tsx
- app/(application)/(user)/job/(tabs)/status.tsx
- app/(application)/(user)/job/(tabs)/archive.tsx
- app/(application)/(user)/job/create.tsx

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
2026-02-02 17:09:58 +08:00
60 changed files with 2837 additions and 1698 deletions

179
QWEN.md
View File

@@ -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

View File

@@ -1,115 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Event_ScreenContribution from "@/screens/Event/ScreenContribution";
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import {
apiEventGetAll
} from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useState } from "react";
export default function EventContribution() { export default function EventContribution() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
async function onLoadData() {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: "contribution",
userId: user?.id,
});
console.log("[DATA] ", JSON.stringify(response.data, null, 2));
if (response.success) {
setListData(response.data);
// const responseListParticipants = await apiEventListOfParticipants({
// id: response?.data?.Event?.id,
// });
// console.log(
// "[LIST PARTICIPANTS]",
// JSON.stringify(responseListParticipants.data, null, 2)
// );
// if (responseListParticipants.success) {
// setListParticipants(responseListParticipants.data);
// }
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
return ( return (
<ViewWrapper hideFooter> <>
{isLoadList ? ( <Event_ScreenContribution />
<LoaderCustom /> </>
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada kontribusi</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BoxWithHeaderSection
key={index}
href={`/event/${item?.Event?.id}/contribution`}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Event?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
name={item?.Event?.Author?.username}
rightComponent={
<TextCustom truncate>
{dateTimeView({
date: item?.Event?.tanggal,
withoutTime: true,
})}
</TextCustom>
}
/>
<TextCustom bold align="center" size="xlarge" truncate={2}>
{item?.Event?.title}
</TextCustom>
<Spacing height={0} />
{/* <Grid>
{item?.Event?.Event_Peserta?.map(
(item2: any, index2: number) => (
<Grid.Col
style={{ alignItems: "center" }}
span={12 / item?.Event?.Event_Peserta?.length}
key={index2}
>
<AvatarComp
size="base"
href={`/profile/${item2?.User?.Profile?.id}`}
fileId={item2?.User?.Profile?.imageId}
/>
</Grid.Col>
)
)}
</Grid> */}
</StackCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,104 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components"; import Event_ScreenHistory from "@/screens/Event/ScreenHistory";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import _ from "lodash";
import { useEffect, useState } from "react";
import { View } from "react-native";
export default function EventHistory() { export default function EventHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useEffect(() => {
onLoadData({ userId: user?.id });
}, [user?.id, activeCategory]);
async function onLoadData({ userId }: { userId?: string }) {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
userId: userId,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
};
const headerComponent = (
<View
style={{
flexDirection: "row",
alignItems: "center",
padding: 5,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 50,
width: "100%",
}}
>
<ButtonCustom
backgroundColor={
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
}
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
style={{ width: "49%" }}
onPress={() => handlePress("all")}
>
Semua Riwayat
</ButtonCustom>
<Spacing width={"2%"} />
<ButtonCustom
backgroundColor={
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === "main" ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress("main")}
>
Riwayat Saya
</ButtonCustom>
</View>
);
return ( return (
<ViewWrapper headerComponent={headerComponent} hideFooter> <>
{isLoadList ? ( <Event_ScreenHistory />
<LoaderCustom /> </>
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada riwayat</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Event_BoxPublishSection
key={index.toString()}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
href={`/event/${item.id}/history`}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,63 +1,9 @@
import { LoaderCustom, TextCustom } from "@/components"; import Event_ScreenBeranda from "@/screens/Event/ScreenBeranda";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import FloatingButton from "@/components/Button/FloatingButton";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function EventBeranda() { export default function EventBeranda() {
const [listData, setListData] = useState([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiEventGetAll({category: "beranda"});
// console.log("Response", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return ( return (
<ViewWrapper <>
hideFooter <Event_ScreenBeranda />
floatingButton={ </>
<FloatingButton onPress={() => router.push("/event/create")} />
}
>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada event</TextCustom>
) : (
listData.map((item: any, index) => (
<Event_BoxPublishSection
key={index}
href={`/event/${item.id}/publish`}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,101 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Event_ScreenStatus from "@/screens/Event/ScreenStatus";
BoxWithHeaderSection,
Grid,
LoaderCustom,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function EventStatus() { export default function EventStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory, id])
);
async function onLoadData() {
try {
setLoadingGetData(true);
const response = await apiEventGetByStatus({
id: id!,
status: activeCategory!,
});
// console.log("Response", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
console.log(error);
} finally {
setLoadingGetData(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const tabsComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return ( return (
<ViewWrapper headerComponent={tabsComponent}> <>
{loadingGetData ? ( <Event_ScreenStatus />
<LoaderCustom /> </>
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, i) => (
<BoxWithHeaderSection
key={i}
href={`/event/${item.id}/${activeCategory}/detail-event`}
>
<StackCustom gap={"xs"}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>
{new Date(item?.tanggal).toLocaleDateString()}
</TextCustom>
</Grid.Col>
</Grid>
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,7 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import {
BoxButtonOnFooter,
ButtonCustom, ButtonCustom,
LoaderCustom, LoaderCustom,
NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
@@ -10,6 +12,7 @@ import {
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { import {
apiEventGetOne, apiEventGetOne,
@@ -48,14 +51,14 @@ export default function EventEdit() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
}, [id]) }, [id]),
); );
async function onLoadData() { async function onLoadData() {
try { try {
setIsLoadData(true); setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string }); const response = await apiEventGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setData(response.data); setData(response.data);
setSelectedDate(new Date(response.data.tanggal)); setSelectedDate(new Date(response.data.tanggal));
@@ -100,6 +103,15 @@ export default function EventEdit() {
const startDate = new Date(selectedDate as any); const startDate = new Date(selectedDate as any);
const endDate = new Date(selectedEndDate as any); const endDate = new Date(selectedEndDate as any);
if (!startDate) {
Toast.show({
type: "info",
text1: "Info",
text2: "Tanggal mulai tidak valid",
});
return false;
}
if (startDate >= endDate) { if (startDate >= endDate) {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -146,7 +158,7 @@ export default function EventEdit() {
const validateDateRange = ( const validateDateRange = (
selectedDate: string | Date, selectedDate: string | Date,
selectedEndDate: string | Date selectedEndDate: string | Date,
): { isValid: boolean; error?: string } => { ): { isValid: boolean; error?: string } => {
const startDate = new Date(selectedDate); const startDate = new Date(selectedDate);
const endDate = new Date(selectedEndDate); const endDate = new Date(selectedEndDate);
@@ -174,9 +186,19 @@ export default function EventEdit() {
return ( return (
<> <>
<ViewWrapper> <NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
title="Update"
onPress={handlerSubmit}
/>
</BoxButtonOnFooter>
}
>
{isLoadData ? ( {isLoadData ? (
<LoaderCustom /> <ListSkeletonComponent />
) : ( ) : (
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextInputCustom <TextInputCustom
@@ -186,26 +208,15 @@ export default function EventEdit() {
value={data?.title} value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })} onChangeText={(value) => setData({ ...data, title: value })}
/> />
<SelectCustom <TextAreaCustom
label="Tipe Event" label="Deskripsi"
placeholder="Pilih tipe event" placeholder="Masukkan deskripsi event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value) => {
console.log(value);
setData({ ...data, eventMaster_TipeAcaraId: value });
}}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required required
value={data?.lokasi} showCount
onChangeText={(value) => setData({ ...data, lokasi: value })} value={data?.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/> />
<DateTimePickerCustom <DateTimePickerCustom
minimumDate={new Date(Date.now())} minimumDate={new Date(Date.now())}
label="Tanggal & Waktu Mulai" label="Tanggal & Waktu Mulai"
@@ -233,7 +244,7 @@ export default function EventEdit() {
{ {
validateDateRange( validateDateRange(
selectedDate as any, selectedDate as any,
selectedEndDate as any selectedEndDate as any,
).error ).error
} }
</TextCustom> </TextCustom>
@@ -242,31 +253,37 @@ export default function EventEdit() {
{ {
validateDateRange( validateDateRange(
selectedDate as any, selectedDate as any,
selectedEndDate as any selectedEndDate as any,
).error ).error
} }
</TextCustom> </TextCustom>
)} )}
<Spacing /> {/* <Spacing /> */}
</StackCustom> </StackCustom>
<TextAreaCustom <SelectCustom
label="Deskripsi" label="Tipe Event"
placeholder="Masukkan deskripsi event" placeholder="Pilih tipe event"
required data={listTypeEvent.map((item: any) => ({
showCount label: item.name,
value={data?.deskripsi} value: item.id,
onChangeText={(value) => setData({ ...data, deskripsi: value })} }))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value) => {
console.log(value);
setData({ ...data, eventMaster_TipeAcaraId: value });
}}
/> />
<TextInputCustom
<ButtonCustom label="Lokasi"
isLoading={isLoading} placeholder="Masukkan lokasi event"
title="Update" required
onPress={handlerSubmit} value={data?.lokasi}
onChangeText={(value) => setData({ ...data, lokasi: value })}
/> />
</StackCustom> </StackCustom>
)} )}
</ViewWrapper> </NewWrapper>
</> </>
); );
} }

View File

@@ -1,110 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Event_ScreenListOfParticipants from "@/screens/Event/ScreenListOfParticipants";
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import {
apiEventGetOne,
apiEventListOfParticipants,
} from "@/service/api-client/api-event";
import dayjs, { Dayjs } from "dayjs";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
export default function EventListOfParticipants() { export default function EventListOfParticipants() {
const { id } = useLocalSearchParams();
const [startDate, setStartDate] = useState<Dayjs | undefined>();
const [listData, setListData] = useState<any[] | null>(null);
const [loadtData, setLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
handlerLoadData();
}, [id])
);
const handlerLoadData = () => {
try {
onLoadData();
onLoadList();
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadData = async () => {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
const date = dayjs(response.data.tanggal);
setStartDate(date);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadList = async () => {
try {
setLoadData(true);
const response = await apiEventListOfParticipants({ id: id as string });
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return ( return (
<ViewWrapper> <>
{loadtData && !listData ? ( <Event_ScreenListOfParticipants />
<LoaderCustom /> </>
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada peserta
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<BaseBox key={index}>
<AvatarUsernameAndOtherComponent
avatar={item?.User?.Profile?.imageId}
name={item?.User?.username}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
rightComponent={
startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
</View>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color="gray">-</BadgeCustom>
</View>
)
}
/>
</BaseBox>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,12 +1,13 @@
import { import {
BoxButtonOnFooter,
ButtonCustom, ButtonCustom,
NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextCustom, TextCustom,
TextInputCustom, TextInputCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -100,7 +101,6 @@ export default function EventCreate() {
setIsLoading(false); setIsLoading(false);
} }
}; };
const buttonSubmit = ( const buttonSubmit = (
<ButtonCustom <ButtonCustom
@@ -112,7 +112,9 @@ export default function EventCreate() {
return ( return (
<> <>
<ViewWrapper> <NewWrapper
footerComponent={<BoxButtonOnFooter>{buttonSubmit}</BoxButtonOnFooter>}
>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextInputCustom <TextInputCustom
placeholder="Masukkan nama event" placeholder="Masukkan nama event"
@@ -121,24 +123,15 @@ export default function EventCreate() {
onChangeText={(value: any) => setData({ ...data, title: value })} onChangeText={(value: any) => setData({ ...data, title: value })}
/> />
<SelectCustom <TextAreaCustom
label="Tipe Event" label="Deskripsi"
placeholder="Pilih tipe event" placeholder="Masukkan deskripsi event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || null}
onChange={(value: any) =>
setData({ ...data, eventMaster_TipeAcaraId: value })
}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required required
onChangeText={(value: any) => setData({ ...data, lokasi: value })} showCount
value={data?.deskripsi || ""}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
/> />
<DateTimePickerCustom <DateTimePickerCustom
@@ -168,22 +161,28 @@ export default function EventCreate() {
</TextCustom> </TextCustom>
)} )}
<Spacing /> <Spacing />
<SelectCustom
label="Tipe Event"
placeholder="Pilih tipe event"
data={listTypeEvent.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || null}
onChange={(value: any) =>
setData({ ...data, eventMaster_TipeAcaraId: value })
}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
/>
</StackCustom> </StackCustom>
<TextAreaCustom
label="Deskripsi"
placeholder="Masukkan deskripsi event"
required
showCount
value={data?.deskripsi || ""}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
/>
{buttonSubmit}
</StackCustom> </StackCustom>
</ViewWrapper> </NewWrapper>
</> </>
); );
} }

View File

@@ -1,15 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import {
BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom, LoaderCustom,
NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextInputCustom, TextInputCustom
ViewWrapper,
} from "@/components"; } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage"; import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
@@ -198,7 +199,15 @@ export default function InvestmentEdit() {
}; };
return ( return (
<ViewWrapper> <NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded <LandscapeFrameUploaded
@@ -253,7 +262,8 @@ export default function InvestmentEdit() {
/> />
<TextInputCustom <TextInputCustom
disabled iconLeft="Rp."
// disabled
required required
placeholder="0" placeholder="0"
label="Total Lembar" label="Total Lembar"
@@ -339,11 +349,7 @@ export default function InvestmentEdit() {
)} )}
<Spacing /> <Spacing />
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</StackCustom> </StackCustom>
<Spacing height={50} /> </NewWrapper>
</ViewWrapper>
); );
} }

View File

@@ -1,18 +1,19 @@
import { import {
BaseBox, BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom, NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
TextInputCustom, TextInputCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -184,7 +185,19 @@ export default function InvestmentCreate() {
// const [coba, setCoba] = useState(""); // const [coba, setCoba] = useState("");
return ( return (
<ViewWrapper> <NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded image={image as string} /> <LandscapeFrameUploaded image={image as string} />
@@ -264,7 +277,9 @@ export default function InvestmentCreate() {
<StackCustom gap={0}> <StackCustom gap={0}>
<TextInputCustom <TextInputCustom
disabled iconLeft="Rp."
// disabled
editable={false}
required required
placeholder="0" placeholder="0"
label="Total Lembar" label="Total Lembar"
@@ -291,7 +306,7 @@ export default function InvestmentCreate() {
/> />
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -313,7 +328,7 @@ export default function InvestmentCreate() {
)} )}
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -335,7 +350,7 @@ export default function InvestmentCreate() {
)} )}
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -357,15 +372,8 @@ export default function InvestmentCreate() {
)} )}
<Spacing /> <Spacing />
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</StackCustom> </StackCustom>
<Spacing height={50} /> {/* <Spacing height={50} /> */}
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,57 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components"; import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2";
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 JobArchive() { export default function JobArchive() {
const { user } = useAuth();
const [listData, setListData] = useState<any[]>([]);
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 ( return (
<ViewWrapper hideFooter> <>
{isLoadData ? ( <Job_ScreenArchive2 />
<LoaderCustom /> </>
) : _.isEmpty(listData) ? (
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
) : (
listData.map((item, index) => (
<BaseBox
key={index}
paddingTop={20}
paddingBottom={20}
href={`/job/${item.id}/archive`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title || "-"}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,83 +1,10 @@
import { import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda";
AvatarUsernameAndOtherComponent, import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2";
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 JobBeranda() { export default function JobBeranda() {
const [listData, setListData] = useState<any[]>([]);
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 ( return (
<ViewWrapper <>
hideFooter <Job_ScreenBeranda2 />
floatingButton={ </>
<FloatingButton onPress={() => router.push("/job/create")} />
}
headerComponent={
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
}
>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada lowongan</TextCustom>
) : (
listData.map((item, index) => (
<BoxWithHeaderSection
key={index}
onPress={() => router.push(`/job/${item.id}`)}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
name={item?.Author?.username}
/>
<TextCustom truncate={2} align="center" bold size="large">
{item?.title || "-"}
</TextCustom>
</StackCustom>
<Spacing />
</BoxWithHeaderSection>
))
)}
<Spacing />
</ViewWrapper>
); );
} }

View File

@@ -1,91 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Job_MainViewStatus from "@/screens/Job/MainViewStatus";
BaseBox, import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2";
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 JobStatus() { export default function JobStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
console.log("STATUS", status);
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState<any[]>([]);
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 = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return ( return (
<> <>
<ViewWrapper headerComponent={scrollComponent} hideFooter> {/* <Job_MainViewStatus /> */}
{isLoadList ? ( <Job_MainViewStatus2 />
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">
Tidak ada data {activeCategory}
</TextCustom>
) : (
listData.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
</> </>
); );
} }

View File

@@ -1,13 +1,14 @@
import { import {
BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
NewWrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom
ViewWrapper
} from "@/components"; } from "@/components";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -99,16 +100,17 @@ export default function JobCreate() {
const buttonSubmit = () => { const buttonSubmit = () => {
return ( return (
<> <>
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}> <BoxButtonOnFooter>
Simpan <ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
</ButtonCustom> Simpan
<Spacing /> </ButtonCustom>
</BoxButtonOnFooter>
</> </>
); );
}; };
return ( return (
<ViewWrapper> <NewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." /> <InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
@@ -160,9 +162,7 @@ export default function JobCreate() {
value={data.deskripsi} value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })} onChangeText={(value) => setData({ ...data, deskripsi: value })}
/> />
{buttonSubmit()}
</StackCustom> </StackCustom>
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,9 +1,11 @@
import ScreenNotification from "@/screens/Notification/ScreenNotification"; import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1";
import ScreenNotification_V2 from "@/screens/Notification/ScreenNotification_V2";
export default function Notification() { export default function Notification() {
return ( return (
<> <>
<ScreenNotification /> <ScreenNotification_V2 />
{/* <ScreenNotification_V1 /> */}
</> </>
); );
} }

View File

@@ -1,59 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenContribution from "@/screens/Voting/ScreenContribution";
LoaderCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState, useCallback } from "react";
export default function VotingContribution() { export default function VotingContribution() {
const { user } = useAuth(); return <Voting_ScreenContribution />;
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return (
<ViewWrapper hideFooter>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontribusi</TextCustom>
) : listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}/contribution`}
/>
))}
</ViewWrapper>
);
} }

View File

@@ -1,77 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components"; import Voting_ScreenHistory from "@/screens/Voting/ScreenHistory";
import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { useAuth } from "@/hooks/use-auth";
import { useCallback, useState } from "react";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
export default function VotingHistory() { export default function VotingHistory() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item);
// tambahkan logika lain seperti filter dsb.
};
return ( return (
<ViewWrapper <>
hideFooter <Voting_ScreenHistory />
headerComponent={ </>
<TabsTwoButtonCustom
leftValue="all"
rightValue="main"
leftText="Semua Riwayat"
rightText="Riwayat Saya"
activeCategory={activeCategory}
handlePress={handlePress}
/>
}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada riwayat</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
key={index}
id={item.id}
data={item}
href={`/voting/${item.id}/history`}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,71 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenBeranda from "@/screens/Voting/ScreenBeranda";
FloatingButton,
LoaderCustom,
SearchInput,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingBeranda() { export default function VotingBeranda() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
search,
category: "beranda",
userLoginId: user?.id,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return ( return (
<ViewWrapper <>
hideFooter <Voting_ScreenBeranda />
floatingButton={ </>
<FloatingButton onPress={() => router.push("/voting/create")} />
}
headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}`}
/>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -1,106 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenStatus from "@/screens/Voting/ScreenStatus";
BadgeCustom,
BaseBox,
LoaderCustom,
ScrollableCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingStatus() { export default function VotingStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish"
);
const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory, id])
);
async function onLoadData() {
try {
setLoadingGetData(true);
const response = await apiVotingGetByStatus({
id: id as string,
status: activeCategory!,
});
setListData(response.data);
} catch (error) {
console.log(error);
} finally {
setLoadingGetData(false);
}
}
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return ( return (
<ViewWrapper headerComponent={scrollComponent} hideFooter> <>
{loadingGetData ? ( <Voting_ScreenStatus />
<LoaderCustom /> </>
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData.map((item: any, i: number) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/voting/${item.id}/${activeCategory}/detail`}
>
<StackCustom>
<TextCustom align="center" bold truncate={2} size="large">
{item?.title || ""}
</TextCustom>
<BadgeCustom
style={{ width: "70%", alignSelf: "center" }}
variant="light"
>
{item?.awalVote &&
dateTimeView({
date: item?.awalVote,
withoutTime: true,
})}{" "}
-{" "}
{item?.akhirVote &&
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
</BadgeCustom>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper>
); );
} }

View File

@@ -51,8 +51,6 @@ export default function VotingDetailStatus() {
setLoadingGetData(true); setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string }); const response = await apiVotingGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setData(response.data); setData(response.data);
} }

View File

@@ -5,13 +5,14 @@ import {
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
LoaderCustom, LoaderCustom,
NewWrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextCustom, TextCustom,
TextInputCustom, TextInputCustom
ViewWrapper,
} from "@/components"; } from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
@@ -34,7 +35,7 @@ interface IEditData {
Voting_DaftarNamaVote?: [ Voting_DaftarNamaVote?: [
{ {
value?: string; value?: string;
} },
]; ];
} }
@@ -47,7 +48,7 @@ export default function VotingEdit() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
}, [id]) }, [id]),
); );
const onLoadData = async () => { const onLoadData = async () => {
@@ -188,9 +189,9 @@ export default function VotingEdit() {
}; };
return ( return (
<ViewWrapper footerComponent={buttonSubmit()}> <NewWrapper footerComponent={buttonSubmit()}>
{loadingGetData ? ( {loadingGetData ? (
<LoaderCustom /> <ListSkeletonComponent />
) : ( ) : (
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextInputCustom <TextInputCustom
@@ -210,7 +211,7 @@ export default function VotingEdit() {
onChangeText={(text) => setData({ ...data, deskripsi: text })} onChangeText={(text) => setData({ ...data, deskripsi: text })}
/> />
<Spacing />
<DateTimePickerCustom <DateTimePickerCustom
minimumDate={new Date(Date.now())} minimumDate={new Date(Date.now())}
@@ -255,7 +256,7 @@ export default function VotingEdit() {
} }
</TextCustom> </TextCustom>
)} )}
<Spacing />
</StackCustom> </StackCustom>
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => ( {data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
@@ -270,7 +271,7 @@ export default function VotingEdit() {
...(data as any), ...(data as any),
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map( Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
(item: any, i: any) => (item: any, i: any) =>
i === index ? { ...item, value } : item i === index ? { ...item, value } : item,
), ),
}) })
} }
@@ -327,6 +328,6 @@ export default function VotingEdit() {
<Spacing /> <Spacing />
</StackCustom> </StackCustom>
)} )}
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,69 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenListOfContributor from "@/screens/Voting/ScreenListOfContributor";
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function Voting_ListOfContributor() { export default function VotingListOfContributor() {
const { id } = useLocalSearchParams(); return <Voting_ScreenListOfContributor />;
const [listData, setListData] = useState<any>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setIsLoadData(true);
const response = await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return (
<ViewWrapper>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontributor</TextCustom>
) : (
listData.map((item: any, index: number) => (
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId || ""}
name={item?.Author?.username || "Username"}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
rightComponent={
<BadgeCustom style={{ alignSelf: "flex-end" }}>
{item?.Voting_DaftarNamaVote?.value}
</BadgeCustom>
}
/>
</BaseBox>
))
)}
</ViewWrapper>
);
} }

View File

@@ -3,11 +3,12 @@ import {
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
NewWrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
ViewWrapper ViewWrapper,
} from "@/components"; } from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -79,7 +80,7 @@ export default function VotingCreate() {
type: "success", type: "success",
text1: "Data berhasil disimpan", text1: "Data berhasil disimpan",
}); });
router.replace("/(application)/(user)/voting/(tabs)/status?status=review"); router.replace("/voting/(tabs)/status?status=review");
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",
@@ -106,7 +107,7 @@ export default function VotingCreate() {
}; };
return ( return (
<ViewWrapper footerComponent={buttonSubmit()}> <NewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextInputCustom <TextInputCustom
label="Judul Voting" label="Judul Voting"
@@ -142,7 +143,6 @@ export default function VotingCreate() {
} }
/> />
{listVote.map((item, index) => ( {listVote.map((item, index) => (
<TextInputCustom <TextInputCustom
key={index} key={index}
@@ -153,8 +153,8 @@ export default function VotingCreate() {
onChangeText={(value: any) => onChangeText={(value: any) =>
setListVote( setListVote(
listVote.map((item, i) => listVote.map((item, i) =>
i === index ? { ...item, value } : item i === index ? { ...item, value } : item,
) ),
) )
} }
/> />
@@ -198,6 +198,6 @@ export default function VotingCreate() {
<Spacing /> <Spacing />
</StackCustom> </StackCustom>
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,213 +1,10 @@
import { import Admin_ScreenNotification from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin";
AlertDefaultSystem, import Admin_ScreenNotification2 from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin2";
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
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 } 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 BoxNotification = ({
data,
activeCategory,
}: {
data: any;
activeCategory: string | null;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
console.log(
"Notification >",
selectedCategory(activeCategory as string)
);
router.push(data.deepLink);
markAsRead(data.id);
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function AdminNotification() { export default function AdminNotification() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [listData, setListData] = useState<any[]>([]);
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 ( return (
<> <>
<Stack.Screen <Admin_ScreenNotification2 />
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
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);
},
});
}
}}
/>
</DrawerCustom>
</> </>
); );
} }

View File

@@ -1,11 +1,21 @@
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components"; import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
import { Stack } from "expo-router"; import { router, Stack } from "expo-router";
export default function NotFoundScreen() { export default function NotFoundScreen() {
// Setelah (dengan penanganan):
const handleBack = () => {
if (router.canGoBack()) {
router.back();
} else {
// Alternatif action ketika tidak bisa kembali
router.replace('/'); // atau navigasi ke halaman default
}
};
return ( return (
<> <>
<Stack.Screen <Stack.Screen
options={{ headerShown: true, title: "", headerLeft: () => <BackButton /> }} options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
/> />
<ViewWrapper> <ViewWrapper>
<StackCustom <StackCustom

View File

@@ -1,4 +1,3 @@
// DateTimeInput.tsx // DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
@@ -7,7 +6,14 @@ import DateTimePicker, {
DateTimePickerEvent, DateTimePickerEvent,
} from "@react-native-community/datetimepicker"; } from "@react-native-community/datetimepicker";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { Pressable, StyleProp, Text, View, ViewStyle } from "react-native"; import {
Keyboard,
Pressable,
StyleProp,
Text,
View,
ViewStyle,
} from "react-native";
import Grid from "../Grid/GridCustom"; import Grid from "../Grid/GridCustom";
import TextCustom from "../Text/TextCustom"; import TextCustom from "../Text/TextCustom";
@@ -53,7 +59,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
const [selectedDate, setSelectedDate] = useState<Date>(value as any); const [selectedDate, setSelectedDate] = useState<Date>(value as any);
const [selectedTime, setSelectedTime] = useState<Date>(value as any); const [selectedTime, setSelectedTime] = useState<Date>(value as any);
console.log("Date Android", value) console.log("Date Android", value);
// Fungsi untuk menggabungkan tanggal dan waktu // Fungsi untuk menggabungkan tanggal dan waktu
const combineDateAndTime = useCallback((date: Date, time: Date): Date => { const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
@@ -62,7 +68,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
time.getHours(), time.getHours(),
time.getMinutes(), time.getMinutes(),
time.getSeconds(), time.getSeconds(),
time.getMilliseconds() time.getMilliseconds(),
); );
return combined; return combined;
}, []); }, []);
@@ -92,10 +98,12 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
}; };
const toggleDatePicker = () => { const toggleDatePicker = () => {
Keyboard.dismiss();
setShowDate(!showDate); setShowDate(!showDate);
}; };
const toggleTimePicker = () => { const toggleTimePicker = () => {
Keyboard.dismiss();
setShowTime(!showTime); setShowTime(!showTime);
}; };

View File

@@ -1,13 +1,22 @@
// DateTimeInput.tsx // DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import DateTimePicker, { import DateTimePicker, {
DateTimePickerEvent, DateTimePickerEvent,
} from "@react-native-community/datetimepicker"; } from "@react-native-community/datetimepicker";
import dayjs from "dayjs"; import dayjs from "dayjs";
import React, { useState } from "react"; import React, { useState, useRef } from "react";
import { Button, StyleProp, Text, View, ViewStyle } from "react-native"; import {
Button,
StyleProp,
Text,
View,
ViewStyle,
Keyboard,
TouchableOpacity,
Modal,
} from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom"; import ClickableCustom from "../Clickable/ClickableCustom";
import TextCustom from "../Text/TextCustom"; import TextCustom from "../Text/TextCustom";
@@ -50,20 +59,35 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
}) => { }) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>( const [selectedDate, setSelectedDate] = useState<Date | undefined>(
value as any value as any,
);
// State sementara untuk menyimpan nilai yang dipilih
const [tempSelectedDate, setTempSelectedDate] = useState<Date | undefined>(
value as any,
); );
const handleConfirm = (event: any, date?: Date) => { const handleConfirm = (event: any, date?: Date) => {
if (event.type === "set" && date !== undefined) { if (event.type === "set" && date !== undefined) {
setSelectedDate(date); // Hanya perbarui state sementara, bukan state utama
onChange(date as any); setTempSelectedDate(date);
} }
}; };
const handlePress = () => { const handlePress = () => {
// Sembunyikan keyboard sebelum menampilkan date picker
Keyboard.dismiss();
// Set state sementara ke nilai saat ini
setTempSelectedDate(selectedDate);
setShow(!show); setShow(!show);
}; };
// Fungsi untuk menangani klik di luar area picker
const handleOutsidePress = () => {
if (show) {
setShow(false);
}
};
return ( return (
<> <>
<ClickableCustom <ClickableCustom
@@ -112,84 +136,125 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
))} ))}
</ClickableCustom> </ClickableCustom>
{show && ( <Modal
<> animationType="fade"
<View transparent={true}
visible={show}
onRequestClose={() => setShow(false)}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.5)", // Efek blur dengan background semi-transparan
}}
>
<TouchableOpacity
style={{ style={{
position: "absolute", position: "absolute",
zIndex: 15, top: 0,
backgroundColor: "white",
borderRadius: 8,
padding: 10,
// top: 0,
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
borderColor: "#ccc", }}
activeOpacity={1}
onPress={handleOutsidePress}
>
<View style={{ flex: 1 }} />
</TouchableOpacity>
<View
style={{
zIndex: 15,
backgroundColor: MainColor.white_gray,
borderRadius: 16,
padding: 20,
marginHorizontal: 20,
width: "95%",
maxWidth: 400,
borderColor: MainColor.placeholder,
borderWidth: 1, borderWidth: 1,
}} }}
onStartShouldSetResponder={() => true} // Mencegah event bubbling ke TouchableOpacity induk
onResponderRelease={() => {}} // Handler kosong untuk mencegah event bubbling
> >
{/* <View style={{ alignItems: "flex-start" }}> {/* <View
<Ionicons style={{
name="close" flexDirection: "row",
size={20} justifyContent: "space-between",
color="black" alignItems: "center",
onPress={() => { marginBottom: 10,
setShow(false); }}
setSelectedDate(undefined); >
<Text
style={{
fontSize: 18,
fontWeight: "bold",
color: MainColor.black,
}} }}
/> >
Pilih Tanggal & Waktu
</Text>
<TouchableOpacity onPress={() => setShow(false)}>
<Ionicons
name="close"
size={24}
color={AccentColor.blackgray}
/>
</TouchableOpacity>
</View> */} </View> */}
<DateTimePicker <DateTimePicker
value={selectedDate || new Date()} value={tempSelectedDate || new Date()}
mode={"datetime"} mode={"datetime"}
display="spinner" display="inline"
onChange={handleConfirm} onChange={handleConfirm}
minimumDate={minimumDate} minimumDate={minimumDate}
maximumDate={maximumDate} maximumDate={maximumDate}
themeVariant="light" themeVariant="light"
/> />
<View style={{ flexDirection: "row", gap: 10 }}> <View style={{ flexDirection: "row", gap: 10, marginTop: 15 }}>
<ClickableCustom <TouchableOpacity
onPress={() => { onPress={() => {
setShow(false) setShow(false);
setSelectedDate(undefined) // Kembalikan ke nilai sebelumnya jika batal
setTempSelectedDate(selectedDate);
}} }}
style={{ style={{
flex: 1,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
padding: 12, padding: 12,
borderRadius: 10, borderRadius: 10,
backgroundColor: MainColor.placeholder, backgroundColor: MainColor.placeholder,
marginTop: 10,
width: "48%",
}} }}
> >
<TextCustom color="black">Batal</TextCustom> <TextCustom color="black">Batal</TextCustom>
</ClickableCustom> </TouchableOpacity>
<ClickableCustom <TouchableOpacity
onPress={() => { onPress={() => {
setShow(false) // Simpan nilai yang dipilih ke state utama hanya saat OK ditekan
onChange(selectedDate as any) setSelectedDate(tempSelectedDate);
onChange(tempSelectedDate as any);
setShow(false);
}} }}
style={{ style={{
flex: 1,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
padding: 12, padding: 12,
borderRadius: 10, borderRadius: 10,
backgroundColor: MainColor.darkblue, backgroundColor: MainColor.darkblue,
marginTop: 10,
width: "48%",
}} }}
> >
<TextCustom>OK</TextCustom> <TextCustom>OK</TextCustom>
</ClickableCustom> </TouchableOpacity>
</View> </View>
</View> </View>
</> </View>
)} </Modal>
</> </>
); );
}; };

View File

@@ -49,9 +49,8 @@ export default function NotificationInitializer() {
const fcmToken = await getToken(messagingInstance); const fcmToken = await getToken(messagingInstance);
if (!fcmToken) { if (!fcmToken) {
console.warn("Tidak bisa mendapatkan FCM token"); console.log("Tidak bisa mendapatkan FCM token");
// logout(); // logout();
return;
} }
console.log("✅ FCM Token:", fcmToken); console.log("✅ FCM Token:", fcmToken);

View File

@@ -101,12 +101,14 @@ const NewWrapper = (props: NewWrapperProps) => {
renderItem={listProps.renderItem} renderItem={listProps.renderItem}
keyExtractor={ keyExtractor={
listProps.keyExtractor || listProps.keyExtractor ||
((item) => { ((item, index) => {
if (item.id == null) { if (item.id == null) {
console.warn("Item tanpa 'id':", item); console.warn("Item tanpa 'id':", item);
return `fallback-${JSON.stringify(item)}`; return `fallback-${index}-${JSON.stringify(item)}`;
} }
return String(item.id);
// Gabungkan ID dengan indeks untuk mencegah duplikasi
return `${String(item.id)}-${index}`;
}) })
} }

View File

@@ -1,8 +1,8 @@
<!-- Start Penerapan Pagination --> <!-- ===================== Start Penerapan Pagination ===================== -->
File utama: screens/Notification/ScreenNotification.tsx File utama: screens/Voting/ScreenListOfContributor.tsx
Fun fecth: apiGetNotificationsById Function fecth: apiVotingContribution
File fetch: service/api-notifications.ts File function fetch: service/api-client/api-voting.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Terapkan pagination pada file "File utama" Terapkan pagination pada file "File utama"
@@ -10,13 +10,22 @@ Analisa juga file "File utama" , jika belum menggunakan NewWrapper pada file "Fi
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Fun fecth" , pada file "File fetch" Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1" Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya. Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- End Penerapan Pagination --> <!-- Additional Prompt -->
File refrensi: app/(application)/(user)/event/[id]/list-of-participants.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
<!-- ===================== End Penerapan Pagination ===================== -->
<!-- Start Penerapan NewWrapper --> <!-- Start Penerapan NewWrapper -->
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx 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 Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti
<!-- -->
Bantu saya untuk memperbaiki logika path yang ada di dalam file "screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx" , pada function fixPath
Saya ingin jika didalam deeplink ada "/admin/..." contoh "/admin/event/review/status" maka path yang akan di redirect adalah "/admin/event/review/status"
jika tidak maka terapkan sesuai dengan logika yang sudah ada

View File

@@ -0,0 +1,213 @@
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
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 } 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 BoxNotification = ({
data,
activeCategory,
}: {
data: any;
activeCategory: string | null;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
console.log(
"Notification >",
selectedCategory(activeCategory as string)
);
router.push(data.deepLink);
markAsRead(data.id);
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function Admin_ScreenNotification() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [listData, setListData] = useState<any[]>([]);
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 (
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
{loading ? (
<ListSkeletonComponent />
) : _.isEmpty(listData) ? (
<NoDataText text="Belum ada notifikasi" />
) : (
listData.map((e, i) => (
<View key={i}>
<BoxNotification
data={e}
activeCategory={activeCategory as any}
/>
</View>
))
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
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);
},
});
}
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,256 @@
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
import { usePagination } from "@/hooks/use-pagination";
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 } from "expo-router";
import _ from "lodash";
import { 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;
}) => {
// Jika categoryApp adalah "OTHER", kembalikan deepLink tanpa perubahan
if (categoryApp === "OTHER") {
return deepLink;
}
// Jika dalam deepLink terdapat "/admin/", kembalikan path tersebut tanpa modifikasi tambahan
if (deepLink.includes("/admin/")) {
return deepLink;
}
console.log("Category App", categoryApp);
console.log("Deep Link", 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,
setListData,
}: {
data: any;
activeCategory: string | null;
setListData: (data: any) => void;
}) => {
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
const newPath = fixPath({
deepLink: data.deepLink,
categoryApp: data.kategoriApp,
});
selectedCategory(activeCategory as string);
router.navigate(newPath as any);
if (!data.isRead) {
markAsRead(data.id);
setListData((prev: any) =>
prev.map((item: any) =>
item.id === data.id ? { ...item, isRead: true } : item,
),
);
}
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function Admin_ScreenNotification2() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("event");
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!user?.id) return { data: [] };
return await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory],
onError: (error) =>
console.error("[ERROR] Fetch admin notifications:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada notifikasi",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item notification
const renderNotificationItem = ({ item }: { item: any }) => (
<View key={item.id}>
<BoxNotification
data={item}
activeCategory={activeCategory as any}
setListData={pagination.setListData}
/>
</View>
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// Reset pagination saat kategori berubah
pagination.reset();
};
return (
<>
<Stack.Screen
options={{
title: "Admin Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
listData={pagination.listData}
renderItem={renderNotificationItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
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(pagination.listData);
data.forEach((e) => {
e.isRead = true;
});
pagination.setListData(data);
pagination.onRefresh();
setOpenDrawer(false);
},
});
}
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -1,6 +1,7 @@
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BoxWithHeaderSection, BoxWithHeaderSection,
Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
} from "@/components"; } from "@/components";

View File

@@ -31,7 +31,7 @@ export default function Event_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(`/event/(tabs)/status?status=draft`);
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -65,7 +65,7 @@ export default function Event_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(`/event/(tabs)/status?status=review`);
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -99,7 +99,7 @@ export default function Event_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(`/event/(tabs)/status?status=draft`);
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",

View File

@@ -0,0 +1,75 @@
import { TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import FloatingButton from "@/components/Button/FloatingButton";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { router } from "expo-router";
import { RefreshControl } from "react-native";
export default function Event_ScreenBeranda() {
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiEventGetAll({
category: "beranda",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [],
onError: (error) => console.error("[ERROR] Fetch event beranda:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada event",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item event
const renderEventItem = ({ item }: { item: any }) => (
<Event_BoxPublishSection
key={item.id}
href={`/event/${item.id}/publish`}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
/>
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderEventItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/event/create")} />
}
/>
);
}

View File

@@ -0,0 +1,102 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BoxWithHeaderSection,
Spacing,
StackCustom,
TextCustom
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { RefreshControl } from "react-native";
export default function Event_ScreenContribution() {
const { user } = useAuth();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!user?.id) return { data: [] };
return await apiEventGetAll({
category: "contribution",
userId: user?.id,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id],
onError: (error) =>
console.error("[ERROR] Fetch event contribution:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada kontribusi",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item event
const renderEventItem = ({ item }: { item: any }) => (
<BoxWithHeaderSection
key={item?.id}
href={`/event/${item?.Event?.id}/contribution`}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Event?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
name={item?.Event?.Author?.username}
rightComponent={
<TextCustom truncate>
{dateTimeView({
date: item?.Event?.tanggal,
withoutTime: true,
})}
</TextCustom>
}
/>
<TextCustom bold align="center" size="xlarge" truncate={2}>
{item?.Event?.title}
</TextCustom>
<Spacing height={0} />
</StackCustom>
</BoxWithHeaderSection>
);
// useEffect(() => {
// pagination.onRefresh();
// }, []);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderEventItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -0,0 +1,121 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, Spacing, TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import _ from "lodash";
import { useState } from "react";
import { RefreshControl, View } from "react-native";
export default function Event_ScreenHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiEventGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
userId: user?.id,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch event history:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada riwayat",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item event
const renderEventItem = ({ item }: { item: any }) => (
<Event_BoxPublishSection
key={item && item?.id}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
href={`/event/${item.id}/history`}
/>
);
const handlePress = (item: any) => {
setActiveCategory(item);
// Reset pagination saat kategori berubah
pagination.reset();
};
const headerComponent = (
<View
style={{
flexDirection: "row",
alignItems: "center",
padding: 5,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 50,
width: "100%",
}}
>
<ButtonCustom
backgroundColor={
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
}
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
style={{ width: "49%" }}
onPress={() => handlePress("all")}
>
Semua Riwayat
</ButtonCustom>
<Spacing width={"2%"} />
<ButtonCustom
backgroundColor={
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === "main" ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress("main")}
>
Riwayat Saya
</ButtonCustom>
</View>
);
return (
<NewWrapper
headerComponent={headerComponent}
listData={pagination.listData}
renderItem={renderEventItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -0,0 +1,121 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import {
apiEventGetOne,
apiEventListOfParticipants,
} from "@/service/api-client/api-event";
import dayjs, { Dayjs } from "dayjs";
import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { RefreshControl, View } from "react-native";
export default function Event_ScreenListOfParticipants() {
const { id } = useLocalSearchParams();
const [startDate, setStartDate] = useState<Dayjs | undefined>();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiEventListOfParticipants({
id: id as string,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) =>
console.error("[ERROR] Fetch event participants:", error),
});
useEffect(() => {
onLoadData();
}, []);
// Fetch event data separately (not part of pagination)
// useFocusEffect(() => {
// onLoadData();
// pagination.onRefresh();
// });
const onLoadData = async () => {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
const date = dayjs(response.data.tanggal);
setStartDate(date);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada peserta",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item participant
const renderParticipantItem = ({ item }: { item: any }) => (
<BaseBox key={item.id}>
<AvatarUsernameAndOtherComponent
avatar={item?.User?.Profile?.imageId}
name={item?.User?.username}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
rightComponent={
startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
</View>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color="gray">-</BadgeCustom>
</View>
)
}
/>
</BaseBox>
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderParticipantItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -0,0 +1,122 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxWithHeaderSection,
Grid,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
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 { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
export default function Event_ScreenStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish",
);
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!id) return { data: [] };
return await apiEventGetByStatus({
id: id!,
status: activeCategory!,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch event 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 event
const renderEventItem = ({ item }: { item: any }) => (
<BoxWithHeaderSection
key={item.id}
href={`/event/${item.id}/${activeCategory}/detail-event`}
>
<StackCustom gap={"xs"}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>
{new Date(item?.tanggal).toLocaleDateString()}
</TextCustom>
</Grid.Col>
</Grid>
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// Reset pagination saat kategori berubah
pagination.reset();
};
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const tabsComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<NewWrapper
hideFooter
headerComponent={<View style={{ paddingTop: 8 }}>{tabsComponent}</View>}
listData={pagination.listData}
renderItem={renderEventItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -2,7 +2,7 @@ import {
AvatarComp, AvatarComp,
BackButton, BackButton,
FloatingButton, FloatingButton,
SearchInput SearchInput,
} from "@/components"; } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper"; import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -101,8 +101,9 @@ export default function Forum_ViewBeranda3() {
/> />
<NewWrapper <NewWrapper
hideFooter
headerComponent={ headerComponent={
<View style={{ paddingTop: 8 }}> <View style={{ paddingTop: 8 }}>
<SearchInput <SearchInput
placeholder="Cari topik diskusi" placeholder="Cari topik diskusi"
onChangeText={_.debounce((text) => setSearch(text), 500)} onChangeText={_.debounce((text) => setSearch(text), 500)}

View File

@@ -20,6 +20,9 @@ export default function Job_ButtonStatusSection({
onSetLoading: (value: boolean) => void; onSetLoading: (value: boolean) => void;
isArchive?: boolean; isArchive?: boolean;
}) { }) {
const path : any =(status: string) => {
return `/job/(tabs)/status?status=${status}`
}
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Batalkan Review", title: "Batalkan Review",
@@ -39,7 +42,7 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -76,14 +79,14 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("review"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router.back()
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
@@ -113,14 +116,14 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router. back();
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -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<string | null>(
status || "publish"
);
const [listData, setListData] = useState<any[]>([]);
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 = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<>
<ViewWrapper headerComponent={scrollComponent} hideFooter>
{isLoadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">
Tidak ada data {activeCategory}
</TextCustom>
) : (
listData.map((e, i) => (
<BaseBox
key={i}
paddingTop={20}
paddingBottom={20}
href={`/job/${e?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,108 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BaseBox, ScrollableCustom, TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
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 { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
export default function Job_MainViewStatus2() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
console.log("STATUS", status);
const [activeCategory, setActiveCategory] = useState<string | null>(
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 }) => (
<BaseBox
key={item.id}
paddingTop={20}
paddingBottom={20}
href={`/job/${item?.id}/${activeCategory}/detail`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title}
</TextCustom>
</BaseBox>
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// Reset pagination saat kategori berubah
pagination.reset();
};
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<NewWrapper
headerComponent={<View style={{ paddingTop: 8 }}>{scrollComponent}</View>}
listData={pagination.listData}
renderItem={renderJobItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -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<any[]>([]);
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 (
<ViewWrapper hideFooter>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
) : (
listData.map((item, index) => (
<BaseBox
key={index}
paddingTop={20}
paddingBottom={20}
href={`/job/${item.id}/archive`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title || "-"}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper>
);
}

View File

@@ -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 }) => (
<BaseBox
key={item.id}
paddingTop={20}
paddingBottom={20}
href={`/job/${item.id}/archive`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title || "-"}
</TextCustom>
</BaseBox>
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderJobItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -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<any[]>([]);
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 (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/job/create")} />
}
headerComponent={
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
}
>
{isLoadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Belum ada lowongan</TextCustom>
) : (
listData.map((item, index) => (
<BoxWithHeaderSection
key={index}
onPress={() => router.push(`/job/${item.id}`)}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
name={item?.Author?.username}
/>
<TextCustom truncate={2} align="center" bold size="large">
{item?.title || "-"}
</TextCustom>
</StackCustom>
<Spacing />
</BoxWithHeaderSection>
))
)}
<Spacing />
</ViewWrapper>
);
}

View File

@@ -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 }) => (
<BoxWithHeaderSection
key={item.id}
onPress={() => router.push(`/job/${item.id}`)}
>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
name={item?.Author?.username}
/>
<TextCustom truncate={2} align="center" bold size="large">
{item?.title || "-"}
</TextCustom>
</StackCustom>
<Spacing />
</BoxWithHeaderSection>
);
return (
<NewWrapper
hideFooter
headerComponent={
<View style={{ paddingTop: 8 }}>
<SearchInput
placeholder="Cari pekerjaan"
onChangeText={_.debounce((text) => setSearch(text), 500)}
/>
</View>
}
floatingButton={
<FloatingButton onPress={() => router.push("/job/create")} />
}
listData={pagination.listData}
renderItem={renderJobItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -0,0 +1,321 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BackButton,
BaseBox,
DrawerCustom,
LoaderCustom,
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, useEffect, 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,
setListData,
}: {
data: any;
activeCategory: string | null;
setListData: (data: any) => void;
}) => {
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
const { markAsRead } = useNotificationStore();
return (
<>
<BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => {
// 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);
const updatedData = {
...data,
isRead: true,
};
console.log("Updated Data", updatedData);
setListData((prev: any) =>
prev.map((item: any) =>
item.id === data.id ? updatedData : item,
),
);
}
}}
>
<StackCustom>
<TextCustom truncate={2} bold>
{data.title}
</TextCustom>
<TextCustom truncate={2}>{data.pesan}</TextCustom>
<TextCustom size="small" color="gray">
{formatChatTime(data.createdAt)}
</TextCustom>
</StackCustom>
</BaseBox>
</>
);
};
export default function ScreenNotification_V1() {
const { user } = useAuth();
const { category } = useLocalSearchParams<{ category?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
category || "event",
);
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
const handlePress = (item: any) => {
setActiveCategory(item.value);
// Reset pagination saat kategori berubah
setListData([]);
setPage(1);
setHasMore(true);
};
// useFocusEffect(
// useCallback(() => {
// fecthData(1, true);
// }, [activeCategory])
// );
useEffect(() => {
fecthData(1, true);
}, [activeCategory]);
const fecthData = async (pageNum: number, clear: boolean = false) => {
if (pageNum === 1 && !clear) return; // Hindari duplicate call untuk page 1
try {
if (pageNum === 1) {
setLoading(true);
} else {
setLoadingMore(true);
}
const response = await apiGetNotificationsById({
id: user?.id as any,
category: activeCategory as any,
page: String(pageNum),
});
if (response.success) {
if (clear || pageNum === 1) {
setListData(response.data);
} else {
setListData((prev) => [...prev, ...response.data]);
}
// Jika data yang dikembalikan kurang dari ukuran halaman, maka tidak ada halaman berikutnya
setHasMore(response.data.length >= 10); // Asumsikan ukuran halaman 10
} else {
if (pageNum === 1) {
setListData([]);
}
}
} catch (error) {
console.log("Error Notification", error);
if (pageNum === 1) {
setListData([]);
}
} finally {
if (pageNum === 1) {
setLoading(false);
} else {
setLoadingMore(false);
}
}
};
const onRefresh = async () => {
setRefreshing(true);
await fecthData(1, true);
setRefreshing(false);
};
const loadMore = () => {
if (!loadingMore && hasMore) {
const nextPage = page + 1;
setPage(nextPage);
fecthData(nextPage, false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Notifikasi",
headerLeft: () => <BackButton />,
headerRight: () => (
<IconDot
color={MainColor.yellow}
onPress={() => setOpenDrawer(true)}
/>
),
}}
/>
<NewWrapper
hideFooter
headerComponent={
<ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as string}
/>
}
listData={listData}
renderItem={({ item }) => (
<View key={item.id}>
<BoxNotification
data={item}
activeCategory={activeCategory as any}
setListData={setListData}
/>
</View>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
onEndReached={loadMore}
// onEndReachedThreshold={0.1}
ListEmptyComponent={
loading ? (
<ListSkeletonComponent />
) : (
<NoDataText text="Belum ada notifikasi" />
)
}
ListFooterComponent={
loadingMore ? (
<View style={{ padding: 16, alignItems: "center" }}>
<LoaderCustom />
</View>
) : null
}
/>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
label: "Tandai Semua Dibaca",
value: "read-all",
icon: (
<Ionicons
name="reader-outline"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
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);
},
});
}
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -11,26 +11,32 @@ import {
TextCustom, TextCustom,
} from "@/components"; } from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent"; 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 { AccentColor, 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 { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store"; import { useNotificationStore } from "@/hooks/use-notification-store";
import { usePagination } from "@/hooks/use-pagination"; import { usePagination } from "@/hooks/use-pagination";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { apiGetNotificationsById } from "@/service/api-notifications"; import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category"; import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
const selectedCategory = (value: string) => { const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find( const category = listOfcategoriesAppNotification.find(
(c) => c.value === value (c) => c.value === value,
); );
return category?.label; return category?.label;
}; };
@@ -46,10 +52,13 @@ const fixPath = ({
return deepLink; return deepLink;
} }
console.log("Category App", categoryApp);
console.log("Deep Link", deepLink);
const separator = deepLink.includes("?") ? "&" : "?"; const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase( const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
categoryApp categoryApp,
)}`; )}`;
console.log("Fix Path", fixedPath); console.log("Fix Path", fixedPath);
@@ -60,31 +69,33 @@ const fixPath = ({
const BoxNotification = ({ const BoxNotification = ({
data, data,
activeCategory, activeCategory,
setListData,
}: { }: {
data: any; data: any;
activeCategory: string | null; activeCategory: string | null;
setListData: (data: any) => void;
}) => { }) => {
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
const { markAsRead } = useNotificationStore(); const { markAsRead } = useNotificationStore();
return ( return (
<> <>
<BaseBox <BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue} backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => { onPress={() => {
// console.log(
// "Notification >",
// selectedCategory(activeCategory as string)
// );
const newPath = fixPath({ const newPath = fixPath({
deepLink: data.deepLink, deepLink: data.deepLink,
categoryApp: data.kategoriApp, categoryApp: data.kategoriApp,
}); });
router.navigate(newPath as any);
selectedCategory(activeCategory as string); selectedCategory(activeCategory as string);
router.navigate(newPath as any);
if (!data.isRead) { if (!data.isRead) {
markAsRead(data.id); markAsRead(data.id);
setListData((prev: any) =>
prev.map((item: any) =>
item.id === data.id ? { ...item, isRead: true } : item,
),
);
} }
}} }}
> >
@@ -104,11 +115,11 @@ const BoxNotification = ({
); );
}; };
export default function ScreenNotification() { export default function ScreenNotification_V2() {
const { user } = useAuth(); const { user } = useAuth();
const { category } = useLocalSearchParams<{ category?: string }>(); const { category } = useLocalSearchParams<{ category?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
category || "event" category || "event",
); );
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
@@ -117,6 +128,8 @@ export default function ScreenNotification() {
// Initialize pagination for notifications // Initialize pagination for notifications
const pagination = usePagination({ const pagination = usePagination({
fetchFunction: async (page) => { fetchFunction: async (page) => {
if (!user?.id) return { data: [] };
return await apiGetNotificationsById({ return await apiGetNotificationsById({
id: user?.id as string, id: user?.id as string,
category: activeCategory as any, category: activeCategory as any,
@@ -127,19 +140,19 @@ export default function ScreenNotification() {
dependencies: [activeCategory], dependencies: [activeCategory],
}); });
useFocusEffect( // useFocusEffect(
useCallback(() => { // useCallback(() => {
// Reset and load first page when category changes // // Reset and load first page when category changes
pagination.reset();
pagination.onRefresh(); // pagination.onRefresh();
}, [activeCategory]) // }, [activeCategory]),
); // );
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
// Reset and load first page when category changes // Reset and load first page when category changes
pagination.reset(); // pagination.reset();
pagination.onRefresh(); // pagination.onRefresh();
}; };
// Render individual notification item // Render individual notification item
@@ -148,24 +161,26 @@ export default function ScreenNotification() {
<BoxNotification <BoxNotification
data={item} data={item}
activeCategory={activeCategory as any} activeCategory={activeCategory as any}
setListData={pagination.setListData}
/> />
</View> </View>
); );
// Generate pagination components using helper // Generate pagination components using helper
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ const { ListEmptyComponent, ListFooterComponent } =
loading: pagination.loading, createPaginationComponents({
refreshing: pagination.refreshing, loading: pagination.loading,
listData: pagination.listData, refreshing: pagination.refreshing,
isInitialLoad: pagination.isInitialLoad, listData: pagination.listData,
emptyMessage: "Belum ada notifikasi", isInitialLoad: pagination.isInitialLoad,
skeletonCount: 5, emptyMessage: "Belum ada notifikasi",
skeletonHeight: 100, skeletonCount: PAGINATION_DEFAULT_TAKE,
}); skeletonHeight: 100,
});
return ( return (
<> <>
<Stack.Screen <Stack.Screen
options={{ options={{
title: "Notifikasi", title: "Notifikasi",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
@@ -179,6 +194,7 @@ export default function ScreenNotification() {
/> />
<NewWrapper <NewWrapper
hideFooter
headerComponent={ headerComponent={
<ScrollableCustom <ScrollableCustom
data={listOfcategoriesAppNotification.map((e, i) => ({ data={listOfcategoriesAppNotification.map((e, i) => ({
@@ -224,7 +240,6 @@ export default function ScreenNotification() {
}, },
]} ]}
onPressItem={(item: any) => { onPressItem={(item: any) => {
console.log("Item", item.value);
if (item.value === "read-all") { if (item.value === "read-all") {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Tandai Semua Dibaca", title: "Tandai Semua Dibaca",
@@ -233,9 +248,13 @@ export default function ScreenNotification() {
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: () => {
markAsReadAll(user?.id as any);
// Reset and refresh data after marking all as read // Reset and refresh data after marking all as read
pagination.reset(); markAsReadAll(user?.id as any);
const data = _.cloneDeep(pagination.listData);
data.forEach((e) => {
e.isRead = true;
});
pagination.setListData(data);
pagination.onRefresh(); pagination.onRefresh();
setOpenDrawer(false); setOpenDrawer(false);
}, },

View File

@@ -3,7 +3,7 @@ import {
apiVotingDelete, apiVotingDelete,
apiVotingUpdateStatus, apiVotingUpdateStatus,
} from "@/service/api-client/api-voting"; } from "@/service/api-client/api-voting";
import { router } from "expo-router"; import { RelativePathString, router } from "expo-router";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function Voting_ButtonStatusSection({ export default function Voting_ButtonStatusSection({
@@ -17,6 +17,10 @@ export default function Voting_ButtonStatusSection({
isLoading: boolean; isLoading: boolean;
onSetLoading: (value: boolean) => void; onSetLoading: (value: boolean) => void;
}) { }) {
const path: any = (status: string) => {
return `/voting/(tabs)/status?status=${status}`;
};
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Batalkan Review", title: "Batalkan Review",
@@ -34,9 +38,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil batalkan review",
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -71,9 +75,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil ajukan review",
}); });
router.back(); router.replace(path("review"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -108,9 +112,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil edit kembali",
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -135,31 +139,31 @@ export default function Voting_ButtonStatusSection({
textLeft: "Batal", textLeft: "Batal",
textRight: "Hapus", textRight: "Hapus",
onPressRight: async () => { onPressRight: async () => {
try { try {
onSetLoading(true); onSetLoading(true);
const response = await apiVotingDelete({ const response = await apiVotingDelete({
id: id as string, id: id as string,
}); });
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil hapus data",
}); });
router.back(); router.back();
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router.back();
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
} finally { } finally {
onSetLoading(false); onSetLoading(false);
} }
}, },
}); });
}; };

View File

@@ -0,0 +1,82 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { FloatingButton, SearchInput } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router } from "expo-router";
import { useMemo, useState } from "react";
import { RefreshControl } from "react-native";
export default function Voting_ScreenBeranda() {
const { user } = useAuth();
const [search, setSearch] = useState("");
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
return await apiVotingGetAll({
search: searchQuery || "",
category: "beranda",
userLoginId: user?.id,
page: String(page),
});
},
pageSize: 5,
searchQuery: search,
dependencies: [user?.id],
onError: (error) => console.error("[ERROR] Fetch voting:", error),
});
// Gunakan helper untuk membuat komponen-komponen pagination
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,
isInitialLoad: pagination.isInitialLoad,
});
// Render item untuk FlatList
const renderItem = useMemo(
() =>
({ item }: { item: any }) =>
(
<Voting_BoxPublishSection
data={item}
key={item.id}
href={`/voting/${item.id}`}
/>
),
[],
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/voting/create")} />
}
headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
/>
);
}

View File

@@ -0,0 +1,70 @@
/* eslint-disable react-hooks/exhaustive-deps */
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useMemo } from "react";
import { RefreshControl } from "react-native";
export default function Voting_ScreenContribution() {
const { user } = useAuth();
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
page: String(page),
});
},
pageSize: 4,
dependencies: [user?.id],
onError: (error) => console.error("[ERROR] Fetch contribution:", error),
});
// Gunakan helper untuk membuat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada kontribusi",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: 5,
skeletonHeight: 200,
isInitialLoad: pagination.isInitialLoad,
});
// Render item untuk FlatList
const renderItem = useMemo(
() =>
({ item }: { item: any }) =>
(
<Voting_BoxPublishSection
data={item}
key={item.id}
href={`/voting/${item.id}/contribution`}
/>
),
[],
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
/>
);
}

View File

@@ -0,0 +1,114 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, Spacing, TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useState } from "react";
import { RefreshControl, View } from "react-native";
export default function Voting_ScreenHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch voting history:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Belum ada riwayat",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item voting
const renderVotingItem = ({ item }: { item: any }) => (
<Voting_BoxPublishSection
key={item && item?.id}
data={item}
href={`/voting/${item.id}/history`}
/>
);
const handlePress = (item: any) => {
setActiveCategory(item);
// Reset pagination saat kategori berubah
pagination.reset();
};
const headerComponent = (
<View
style={{
flexDirection: "row",
alignItems: "center",
padding: 5,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 50,
width: "100%",
}}
>
<ButtonCustom
backgroundColor={
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
}
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
style={{ width: "49%" }}
onPress={() => handlePress("all")}
>
Semua Riwayat
</ButtonCustom>
<Spacing width={"2%"} />
<ButtonCustom
backgroundColor={
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
}
textColor={
activeCategory === "main" ? MainColor.black : MainColor.white
}
style={{ width: "49%" }}
onPress={() => handlePress("main")}
>
Riwayat Saya
</ButtonCustom>
</View>
);
return (
<NewWrapper
headerComponent={headerComponent}
listData={pagination.listData}
renderItem={renderVotingItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
Spacing,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { RefreshControl, View } from "react-native";
export default function Voting_ScreenListOfContributor() {
const { id } = useLocalSearchParams();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) =>
console.error("[ERROR] Fetch voting contributors:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada kontributor",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item contributor
const renderContributorItem = ({ item }: { item: any }) => (
<BaseBox paddingTop={5} paddingBottom={5} key={item?.id}>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId || ""}
name={item?.Author?.username || "Username"}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
rightComponent={
<BadgeCustom style={{ alignSelf: "flex-end" }}>
{item?.Voting_DaftarNamaVote?.value}
</BadgeCustom>
}
/>
</BaseBox>
);
return (
<NewWrapper
hideFooter
listData={pagination.listData}
renderItem={renderContributorItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -0,0 +1,132 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
ScrollableCustom,
StackCustom,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
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 { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 6
export default function Voting_ScreenStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish",
);
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
if (!id) return { data: [] };
return await apiVotingGetByStatus({
id: id as string,
status: activeCategory!,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch voting by status:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: `Tidak ada data ${activeCategory}`,
skeletonCount: 5,
skeletonHeight: 100,
});
// Render item voting
const renderVotingItem = ({ item }: { item: any }) => (
<BaseBox
key={item.id}
paddingTop={20}
paddingBottom={20}
href={`/voting/${item.id}/${activeCategory}/detail`}
>
<StackCustom>
<TextCustom align="center" bold truncate={2} size="large">
{item?.title || ""}
</TextCustom>
<BadgeCustom
style={{ width: "70%", alignSelf: "center" }}
variant="light"
>
{item?.awalVote &&
dateTimeView({
date: item?.awalVote,
withoutTime: true,
})}{" "}
-{" "}
{item?.akhirVote &&
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
</BadgeCustom>
</StackCustom>
</BaseBox>
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
// Reset pagination saat kategori berubah
pagination.reset();
};
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<NewWrapper
headerComponent={<View style={{ paddingTop: 8 }}>{scrollComponent}</View>}
listData={pagination.listData}
renderItem={renderVotingItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -14,12 +14,14 @@ export async function apiEventCreate(data: any) {
export async function apiEventGetByStatus({ export async function apiEventGetByStatus({
id, id,
status, status,
page = "1",
}: { }: {
id: string; id: string;
status: string; status: string;
page?: string;
}) { }) {
try { try {
const response = await apiConfig.get(`/mobile/event/${id}/${status}`); const response = await apiConfig.get(`/mobile/event/${id}/${status}?page=${page}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -79,15 +81,18 @@ export async function apiEventDelete({ id }: { id: string }) {
export async function apiEventGetAll({ export async function apiEventGetAll({
category, category,
userId, userId,
page = "1",
}: { }: {
category?: "beranda" | "contribution" | "all-history" | "my-history"; category?: "beranda" | "contribution" | "all-history" | "my-history";
userId?: string; userId?: string;
page?: string;
}) { }) {
try { try {
const categoryEvent = category ? `?category=${category}` : ""; const categoryEvent = category ? `?category=${category}` : "";
const userIdCreator = userId ? `&userId=${userId}` : ""; const userIdCreator = userId ? `&userId=${userId}` : "";
const pageParam = `&page=${page}`;
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/event${categoryEvent}${userIdCreator}` `/mobile/event${categoryEvent}${userIdCreator}${pageParam}`
); );
return response.data; return response.data;
} catch (error) { } catch (error) {
@@ -112,9 +117,16 @@ export async function apiEventJoin({
} }
} }
export async function apiEventListOfParticipants({ id }: { id?: string }) { export async function apiEventListOfParticipants({
id,
page = "1"
}: {
id?: string;
page?: string;
}) {
try { try {
const response = await apiConfig.get(`/mobile/event/${id}/participants`); const pageParam = page ? `?page=${page}` : "";
const response = await apiConfig.get(`/mobile/event/${id}/participants${pageParam}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;

View File

@@ -14,12 +14,14 @@ export async function apiJobCreate(data: any) {
export async function apiJobGetByStatus({ export async function apiJobGetByStatus({
authorId, authorId,
status, status,
page = "1",
}: { }: {
authorId: string; authorId: string;
status: string; status: string;
page?: string;
}) { }) {
try { try {
const response = await apiConfig.get(`/mobile/job/${authorId}/${status}`); const response = await apiConfig.get(`/mobile/job/${authorId}/${status}?page=${page}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -63,10 +65,12 @@ export async function apiJobGetAll({
search, search,
category, category,
authorId, authorId,
page = "1",
}: { }: {
search?: string; search?: string;
category: "archive" | "beranda"; category: "archive" | "beranda";
authorId?: string; authorId?: string;
page?: string;
}) { }) {
try { try {
let categoryText = category ? `?category=${category}` : ""; let categoryText = category ? `?category=${category}` : "";
@@ -74,8 +78,9 @@ export async function apiJobGetAll({
categoryText = `?category=${category}&authorId=${authorId}`; categoryText = `?category=${category}&authorId=${authorId}`;
} }
const searchText = search ? `&search=${search}` : ""; const searchText = search ? `&search=${search}` : "";
const pageText = `&page=${page}`;
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/job${categoryText}${searchText}` `/mobile/job${categoryText}${searchText}${pageText}`
); );
return response.data; return response.data;
} catch (error) { } catch (error) {

View File

@@ -14,12 +14,14 @@ export async function apiVotingCreate(data: any) {
export async function apiVotingGetByStatus({ export async function apiVotingGetByStatus({
id, id,
status, status,
page = "1",
}: { }: {
id: string; id: string;
status: string; status: string;
page?: string;
}) { }) {
try { try {
const response = await apiConfig.get(`/mobile/voting/${id}/${status}`); const response = await apiConfig.get(`/mobile/voting/${id}/${status}?page=${page}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -79,14 +81,15 @@ export async function apiVotingUpdateData({
} }
} }
export async function apiVotingGetAll({ search, category, authorId, userLoginId }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string, userLoginId?: string }) { export async function apiVotingGetAll({ search, category, authorId, userLoginId, page = "1" }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string, userLoginId?: string, page?: string }) {
try { try {
console.log("userLoginId", userLoginId); console.log("userLoginId", userLoginId);
const categoryQuery = category ? `?category=${category}` : ""; const categoryQuery = category ? `?category=${category}` : "";
const searchQuery = search ? `&search=${search}` : ""; const searchQuery = search ? `&search=${search}` : "";
const authorIdQuery = authorId ? `&authorId=${authorId}` : ""; const authorIdQuery = authorId ? `&authorId=${authorId}` : "";
const userLoginIdQuery = userLoginId ? `&userLoginId=${userLoginId}` : ""; const userLoginIdQuery = userLoginId ? `&userLoginId=${userLoginId}` : "";
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}${userLoginIdQuery}`); const pageQuery = `&page=${page}`;
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}${userLoginIdQuery}${pageQuery}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -108,15 +111,17 @@ export async function apiVotingContribution({
id, id,
authorId, authorId,
category, category,
page = "1",
}: { }: {
id: string; id: string;
authorId: string; authorId: string;
category: "list" | "checked"; category: "list" | "checked";
page?: string;
}) { }) {
const query = const query =
category === "list" category === "list"
? "?category=list" ? `?category=list&page=${page}`
: `?category=checked&authorId=${authorId}`; : `?category=checked&authorId=${authorId}&page=${page}`;
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/voting/${id}/contribution${query}` `/mobile/voting/${id}/contribution${query}`

View File

@@ -47,9 +47,8 @@ export async function apiGetNotificationsById({
category: TypeNotificationCategoryApp; category: TypeNotificationCategoryApp;
page?: string; page?: string;
}) { }) {
console.log("ID", id);
console.log("Category", category); console.log("Category", category);
console.log("Page", page);
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(

View File

@@ -17,7 +17,9 @@ export const GStyles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
paddingInline: PADDING_MEDIUM, paddingInline: PADDING_MEDIUM,
paddingBlock: PADDING_EXTRA_SMALL, paddingTop: PADDING_EXTRA_SMALL,
paddingBottom: 5,
// paddingBlock: PADDING_EXTRA_SMALL,
backgroundColor: MainColor.darkblue, backgroundColor: MainColor.darkblue,
}, },
containerWithBackground: { containerWithBackground: {