Compare commits
9 Commits
loaddata/3
...
loaddata/5
| Author | SHA1 | Date | |
|---|---|---|---|
| c570a19d84 | |||
| 7415c8c8ce | |||
| 72a3d42013 | |||
| d0abd14047 | |||
| 5b2be20469 | |||
| 60177a1087 | |||
| 771ae45f26 | |||
| 41f4a8ac99 | |||
| 48196cd46b |
179
QWEN.md
179
QWEN.md
@@ -1,179 +0,0 @@
|
|||||||
# HIPMI Mobile Application - Development Guide
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
HIPMI Badung Connect is a mobile application built with Expo and React Native. It serves as a connection platform for HIPMI (Himpunan Pengusaha Muda Indonesia) Badung members, featuring authentication, user management, and various business-related functionalities.
|
|
||||||
|
|
||||||
### Key Technologies
|
|
||||||
- **Framework**: Expo (v54.0.0) with React Native (0.81.4)
|
|
||||||
- **Architecture**: File-based routing with Expo Router
|
|
||||||
- **State Management**: React Context API
|
|
||||||
- **Styling**: React Native components with custom color palettes
|
|
||||||
- **Authentication**: Token-based authentication with OTP verification
|
|
||||||
- **Database**: AsyncStorage for local storage
|
|
||||||
- **Maps**: React Native Maps and Mapbox integration
|
|
||||||
- **Notifications**: Expo Notifications and Firebase Messaging
|
|
||||||
- **Language**: TypeScript
|
|
||||||
|
|
||||||
### Project Structure
|
|
||||||
```
|
|
||||||
hipmi-mobile/
|
|
||||||
├── app/ # File-based routing structure
|
|
||||||
│ ├── (application)/ # Main application screens
|
|
||||||
│ │ ├── (file)/ # File management screens
|
|
||||||
│ │ ├── (image)/ # Image management screens
|
|
||||||
│ │ ├── (user)/ # User-specific screens
|
|
||||||
│ │ └── admin/ # Admin-specific screens
|
|
||||||
│ ├── _layout.tsx # Root layout wrapper
|
|
||||||
│ ├── index.tsx # Home screen
|
|
||||||
│ ├── eula.tsx # Terms and conditions screen
|
|
||||||
│ ├── register.tsx # Registration screen
|
|
||||||
│ └── verification.tsx # OTP verification screen
|
|
||||||
├── assets/ # Static assets (images, icons)
|
|
||||||
├── components/ # Reusable UI components
|
|
||||||
├── constants/ # Configuration constants
|
|
||||||
├── context/ # React Context providers
|
|
||||||
├── hooks/ # Custom React hooks
|
|
||||||
├── screens/ # Screen components
|
|
||||||
├── service/ # API services and configurations
|
|
||||||
├── types/ # TypeScript type definitions
|
|
||||||
├── app.config.js # Expo configuration
|
|
||||||
├── package.json # Dependencies and scripts
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building and Running
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
- Node.js (with bun >=1.0.0 as specified in package.json)
|
|
||||||
- Expo CLI or bun installed globally
|
|
||||||
|
|
||||||
### Setup Instructions
|
|
||||||
1. **Install dependencies**:
|
|
||||||
```bash
|
|
||||||
bun install
|
|
||||||
# or if using npm
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Environment Variables**:
|
|
||||||
Create a `.env` file with the following variables:
|
|
||||||
```
|
|
||||||
API_BASE_URL=your_api_base_url
|
|
||||||
BASE_URL=your_base_url
|
|
||||||
DEEP_LINK_URL=your_deep_link_url
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Start the development server**:
|
|
||||||
```bash
|
|
||||||
# Using bun (as specified in package.json)
|
|
||||||
bun run start
|
|
||||||
# or using expo directly
|
|
||||||
npx expo start
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Platform-specific commands**:
|
|
||||||
```bash
|
|
||||||
# Android
|
|
||||||
bun run android
|
|
||||||
# iOS
|
|
||||||
bun run ios
|
|
||||||
# Web
|
|
||||||
bun run web
|
|
||||||
```
|
|
||||||
|
|
||||||
### EAS Build Configuration
|
|
||||||
The project uses Expo Application Services (EAS) for building and deployment:
|
|
||||||
- Development builds: `eas build --profile development`
|
|
||||||
- Preview builds: `eas build --profile preview`
|
|
||||||
- Production builds: `eas build --profile production`
|
|
||||||
|
|
||||||
## Authentication Flow
|
|
||||||
|
|
||||||
The application implements a phone number-based authentication system with OTP verification:
|
|
||||||
|
|
||||||
1. **Login**: User enters phone number → OTP sent via SMS
|
|
||||||
2. **Verification**: User enters OTP code → Validates and creates session
|
|
||||||
3. **Registration**: If user doesn't exist, registration flow is triggered
|
|
||||||
4. **Terms Agreement**: User must accept terms and conditions
|
|
||||||
5. **Session Management**: Tokens stored in AsyncStorage
|
|
||||||
|
|
||||||
### Key Authentication Functions
|
|
||||||
- `loginWithNomor()`: Initiates OTP sending
|
|
||||||
- `validateOtp()`: Verifies OTP and creates session
|
|
||||||
- `registerUser()`: Registers new users
|
|
||||||
- `logout()`: Clears session and removes tokens
|
|
||||||
- `acceptedTerms()`: Handles terms acceptance
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### User Management
|
|
||||||
- Phone number-based registration and login
|
|
||||||
- OTP verification system
|
|
||||||
- Terms and conditions agreement
|
|
||||||
- User profile management
|
|
||||||
|
|
||||||
### Business Features
|
|
||||||
- Business field management (admin section)
|
|
||||||
- File and image management capabilities
|
|
||||||
- Location services integration
|
|
||||||
- Push notifications
|
|
||||||
|
|
||||||
### UI Components
|
|
||||||
- Custom color palette with blue/yellow theme
|
|
||||||
- Responsive layouts using SafeAreaView
|
|
||||||
- Toast notifications for user feedback
|
|
||||||
- Bottom tab navigation and drawer navigation
|
|
||||||
|
|
||||||
## Development Conventions
|
|
||||||
|
|
||||||
### Naming Conventions
|
|
||||||
- Components: PascalCase (e.g., `UserProfile.tsx`)
|
|
||||||
- Functions: camelCase (e.g., `getUserData()`)
|
|
||||||
- Constants: UPPER_SNAKE_CASE (e.g., `API_BASE_URL`)
|
|
||||||
- Files: kebab-case or camelCase for utility files
|
|
||||||
|
|
||||||
### Code Organization
|
|
||||||
- Components are organized by feature/functionality
|
|
||||||
- API services are centralized in the `service/` directory
|
|
||||||
- Type definitions are maintained in the `types/` directory
|
|
||||||
- Constants are grouped by category in the `constants/` directory
|
|
||||||
|
|
||||||
### Styling Approach
|
|
||||||
- Color palette defined in `constants/color-palet.ts`
|
|
||||||
- Reusable styles and themes centralized
|
|
||||||
- Responsive design using React Native's flexbox system
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- Linting: `bun run lint` (uses ESLint with Expo config)
|
|
||||||
- No specific test framework mentioned in package.json
|
|
||||||
|
|
||||||
## Environment Configuration
|
|
||||||
|
|
||||||
The application supports multiple environments through:
|
|
||||||
- Environment variables loaded via dotenv
|
|
||||||
- Expo's extra configuration in `app.config.js`
|
|
||||||
- Platform-specific configurations for iOS and Android
|
|
||||||
|
|
||||||
### Supported Platforms
|
|
||||||
- iOS (with tablet support)
|
|
||||||
- Android (with adaptive icons)
|
|
||||||
- Web (static output)
|
|
||||||
|
|
||||||
## Third-party Integrations
|
|
||||||
|
|
||||||
- **Firebase**: Authentication, messaging, and analytics
|
|
||||||
- **Mapbox**: Advanced mapping capabilities
|
|
||||||
- **React Navigation**: Screen navigation and routing
|
|
||||||
- **React Native Paper**: Material Design components
|
|
||||||
- **Axios**: HTTP client for API requests
|
|
||||||
- **Lodash**: Utility functions
|
|
||||||
- **QR Code SVG**: QR code generation
|
|
||||||
|
|
||||||
## Important Configuration Files
|
|
||||||
|
|
||||||
- `app.config.js`: Expo configuration, app metadata, and plugin setup
|
|
||||||
- `eas.json`: EAS build profiles and submission configuration
|
|
||||||
- `tsconfig.json`: TypeScript compiler options
|
|
||||||
- `package.json`: Dependencies, scripts, and project metadata
|
|
||||||
- `metro.config.js`: Metro bundler configuration
|
|
||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 /> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}`;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
213
screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
Normal file
213
screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
256
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal file
256
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
AvatarUsernameAndOtherComponent,
|
AvatarUsernameAndOtherComponent,
|
||||||
BoxWithHeaderSection,
|
BoxWithHeaderSection,
|
||||||
|
Spacing,
|
||||||
StackCustom,
|
StackCustom,
|
||||||
TextCustom,
|
TextCustom,
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
75
screens/Event/ScreenBeranda.tsx
Normal file
75
screens/Event/ScreenBeranda.tsx
Normal 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")} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
screens/Event/ScreenContribution.tsx
Normal file
102
screens/Event/ScreenContribution.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
screens/Event/ScreenHistory.tsx
Normal file
121
screens/Event/ScreenHistory.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
121
screens/Event/ScreenListOfParticipants.tsx
Normal file
121
screens/Event/ScreenListOfParticipants.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
122
screens/Event/ScreenStatus.tsx
Normal file
122
screens/Event/ScreenStatus.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
91
screens/Job/MainViewStatus.tsx
Normal file
91
screens/Job/MainViewStatus.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
screens/Job/MainViewStatus2.tsx
Normal file
108
screens/Job/MainViewStatus2.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
screens/Job/ScreenArchive.tsx
Normal file
57
screens/Job/ScreenArchive.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
76
screens/Job/ScreenArchive2.tsx
Normal file
76
screens/Job/ScreenArchive2.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
screens/Job/ScreenBeranda.tsx
Normal file
83
screens/Job/ScreenBeranda.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
screens/Job/ScreenBeranda2.tsx
Normal file
105
screens/Job/ScreenBeranda2.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
321
screens/Notification/ScreenNotification_V1.tsx
Normal file
321
screens/Notification/ScreenNotification_V1.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
},
|
},
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
82
screens/Voting/ScreenBeranda.tsx
Normal file
82
screens/Voting/ScreenBeranda.tsx
Normal 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} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
screens/Voting/ScreenContribution.tsx
Normal file
70
screens/Voting/ScreenContribution.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
114
screens/Voting/ScreenHistory.tsx
Normal file
114
screens/Voting/ScreenHistory.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
screens/Voting/ScreenListOfContributor.tsx
Normal file
80
screens/Voting/ScreenListOfContributor.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
screens/Voting/ScreenStatus.tsx
Normal file
132
screens/Voting/ScreenStatus.tsx
Normal 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
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}`
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user