Compare commits
4 Commits
notificati
...
loaddata/3
| Author | SHA1 | Date | |
|---|---|---|---|
| 771ae45f26 | |||
| 41f4a8ac99 | |||
| 48196cd46b | |||
| ec79a1fbcd |
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,104 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components";
|
||||
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";
|
||||
import Event_ScreenHistory from "@/screens/Event/ScreenHistory";
|
||||
|
||||
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 (
|
||||
<ViewWrapper headerComponent={headerComponent} hideFooter>
|
||||
{isLoadList ? (
|
||||
<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>
|
||||
<>
|
||||
<Event_ScreenHistory />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
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";
|
||||
import Event_ScreenStatus from "@/screens/Event/ScreenStatus";
|
||||
|
||||
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 (
|
||||
<ViewWrapper headerComponent={tabsComponent}>
|
||||
{loadingGetData ? (
|
||||
<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>
|
||||
<>
|
||||
<Event_ScreenStatus />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import {
|
||||
apiEventGetOne,
|
||||
@@ -48,7 +51,7 @@ export default function EventEdit() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
@@ -100,6 +103,15 @@ export default function EventEdit() {
|
||||
const startDate = new Date(selectedDate 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) {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -146,7 +158,7 @@ export default function EventEdit() {
|
||||
|
||||
const validateDateRange = (
|
||||
selectedDate: string | Date,
|
||||
selectedEndDate: string | Date
|
||||
selectedEndDate: string | Date,
|
||||
): { isValid: boolean; error?: string } => {
|
||||
const startDate = new Date(selectedDate);
|
||||
const endDate = new Date(selectedEndDate);
|
||||
@@ -174,9 +186,19 @@ export default function EventEdit() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
title="Update"
|
||||
onPress={handlerSubmit}
|
||||
/>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
<ListSkeletonComponent />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
@@ -186,26 +208,15 @@ export default function EventEdit() {
|
||||
value={data?.title}
|
||||
onChangeText={(value) => setData({ ...data, title: value })}
|
||||
/>
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe 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"
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
value={data?.lokasi}
|
||||
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
||||
showCount
|
||||
value={data?.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
/>
|
||||
|
||||
<DateTimePickerCustom
|
||||
minimumDate={new Date(Date.now())}
|
||||
label="Tanggal & Waktu Mulai"
|
||||
@@ -233,7 +244,7 @@ export default function EventEdit() {
|
||||
{
|
||||
validateDateRange(
|
||||
selectedDate as any,
|
||||
selectedEndDate as any
|
||||
selectedEndDate as any,
|
||||
).error
|
||||
}
|
||||
</TextCustom>
|
||||
@@ -242,31 +253,37 @@ export default function EventEdit() {
|
||||
{
|
||||
validateDateRange(
|
||||
selectedDate as any,
|
||||
selectedEndDate as any
|
||||
selectedEndDate as any,
|
||||
).error
|
||||
}
|
||||
</TextCustom>
|
||||
)}
|
||||
<Spacing />
|
||||
{/* <Spacing /> */}
|
||||
</StackCustom>
|
||||
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
showCount
|
||||
value={data?.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe 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 });
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
title="Update"
|
||||
onPress={handlerSubmit}
|
||||
<TextInputCustom
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi event"
|
||||
required
|
||||
value={data?.lokasi}
|
||||
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
||||
/>
|
||||
</StackCustom>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -100,7 +101,6 @@ export default function EventCreate() {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const buttonSubmit = (
|
||||
<ButtonCustom
|
||||
@@ -112,7 +112,9 @@ export default function EventCreate() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={<BoxButtonOnFooter>{buttonSubmit}</BoxButtonOnFooter>}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
placeholder="Masukkan nama event"
|
||||
@@ -121,24 +123,15 @@ export default function EventCreate() {
|
||||
onChangeText={(value: any) => setData({ ...data, title: value })}
|
||||
/>
|
||||
|
||||
<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"
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
|
||||
showCount
|
||||
value={data?.deskripsi || ""}
|
||||
onChangeText={(value: any) =>
|
||||
setData({ ...data, deskripsi: value })
|
||||
}
|
||||
/>
|
||||
|
||||
<DateTimePickerCustom
|
||||
@@ -168,22 +161,28 @@ export default function EventCreate() {
|
||||
</TextCustom>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
showCount
|
||||
value={data?.deskripsi || ""}
|
||||
onChangeText={(value: any) =>
|
||||
setData({ ...data, deskripsi: value })
|
||||
}
|
||||
/>
|
||||
|
||||
{buttonSubmit}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2";
|
||||
|
||||
export default function JobArchive() {
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<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>
|
||||
<>
|
||||
<Job_ScreenArchive2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,83 +1,10 @@
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BoxWithHeaderSection,
|
||||
FloatingButton,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda";
|
||||
import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2";
|
||||
|
||||
export default function JobBeranda() {
|
||||
const [listData, setListData] = useState<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>
|
||||
<>
|
||||
<Job_ScreenBeranda2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,91 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
LoaderCustom,
|
||||
ScrollableCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiJobGetByStatus } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Job_MainViewStatus from "@/screens/Job/MainViewStatus";
|
||||
import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2";
|
||||
|
||||
export default function JobStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
console.log("STATUS", status);
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<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>
|
||||
{/* <Job_MainViewStatus /> */}
|
||||
<Job_MainViewStatus2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper
|
||||
TextInputCustom
|
||||
} from "@/components";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -99,16 +100,17 @@ export default function JobCreate() {
|
||||
const buttonSubmit = () => {
|
||||
return (
|
||||
<>
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
<Spacing />
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper footerComponent={buttonSubmit()}>
|
||||
<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." />
|
||||
|
||||
@@ -160,9 +162,7 @@ export default function JobCreate() {
|
||||
value={data.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
/>
|
||||
|
||||
{buttonSubmit()}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,248 +1,11 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||
import { formatChatTime } from "@/utils/formatChatTime";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import ScreenNotification from "@/screens/Notification/ScreenNotification_V2";
|
||||
import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const fixPath = ({
|
||||
deepLink,
|
||||
categoryApp,
|
||||
}: {
|
||||
deepLink: string;
|
||||
categoryApp: string;
|
||||
}) => {
|
||||
if (categoryApp === "OTHER") {
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
const separator = deepLink.includes("?") ? "&" : "?";
|
||||
|
||||
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
|
||||
categoryApp
|
||||
)}`;
|
||||
|
||||
console.log("Fix Path", fixedPath);
|
||||
|
||||
return fixedPath;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
data,
|
||||
activeCategory,
|
||||
}: {
|
||||
data: any;
|
||||
activeCategory: string | null;
|
||||
}) => {
|
||||
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
|
||||
const { markAsRead } = useNotificationStore();
|
||||
export default function Notification() {
|
||||
return (
|
||||
<>
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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 Notifications() {
|
||||
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 [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: "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>
|
||||
<ScreenNotification />
|
||||
{/* <ScreenNotification_V1 /> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CenterCustom,
|
||||
Grid,
|
||||
InformationBox,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
@@ -120,7 +121,7 @@ export default function PortofolioCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<Portofolio_ButtonCreate
|
||||
id={id as string}
|
||||
@@ -357,8 +358,8 @@ export default function PortofolioCreate() {
|
||||
setDataMedsos({ ...dataMedsos, youtube: value })
|
||||
}
|
||||
/>
|
||||
<Spacing />
|
||||
{/* <Spacing /> */}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
import {
|
||||
@@ -238,7 +239,7 @@ export default function PortofolioEdit() {
|
||||
return !dataArray.some(
|
||||
(item: any) =>
|
||||
!item.MasterSubBidangBisnis.id ||
|
||||
item.MasterSubBidangBisnis.id.trim() === ""
|
||||
item.MasterSubBidangBisnis.id.trim() === "",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,16 +320,16 @@ export default function PortofolioEdit() {
|
||||
if (!bidangBisnis || !subBidangBisnis) {
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<ActivityIndicator size="large" color={MainColor.yellow} />
|
||||
</ViewWrapper>
|
||||
<NewWrapper>
|
||||
<ListSkeletonComponent height={80} />
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper footerComponent={buttonUpdate}>
|
||||
<NewWrapper footerComponent={buttonUpdate}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
required
|
||||
@@ -471,7 +472,7 @@ export default function PortofolioEdit() {
|
||||
/>
|
||||
<Spacing />
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,9 @@
|
||||
import { TextCustom, ViewWrapper } from "@/components";
|
||||
import Portofolio_BoxView from "@/screens/Portofolio/BoxPortofolioView";
|
||||
import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import ViewListPortofolio from "@/screens/Portofolio/ViewListPortofolio";
|
||||
|
||||
export default function ListPortofolio() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadPortofolio(id as string);
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadPortofolio = async (id: string) => {
|
||||
const response = await apiGetPortofolio({ id: id });
|
||||
setData(response.data);
|
||||
};
|
||||
return (
|
||||
<ViewWrapper>
|
||||
{data ? data?.map((item: any, index: number) => (
|
||||
<Portofolio_BoxView key={index} data={item} />
|
||||
)) : <TextCustom>Tidak ada portofolio</TextCustom>}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<ViewListPortofolio />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,213 +1,10 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
import Admin_ScreenNotification from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin";
|
||||
import Admin_ScreenNotification2 from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin2";
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<Admin_ScreenNotification2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export {
|
||||
|
||||
// OS Height
|
||||
const OS_ANDROID_HEIGHT = 115
|
||||
const OS_IOS_HEIGHT = 70
|
||||
const OS_IOS_HEIGHT = 90
|
||||
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
|
||||
|
||||
// Text Size
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
<!-- Start Penerapan Pagination -->
|
||||
Terapkan pagination pada file: screens/Forum/DetailForum.tsx
|
||||
Component yang digunakan: hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||
|
||||
Perbaiki fetch pada file: service/api-client/api-forum.ts
|
||||
File utama: screens/Event/ScreenHistory.tsx
|
||||
Fun fecth: apiEventGetAll
|
||||
File fetch: service/api-client/api-event.ts
|
||||
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||
File refrensi: screens/Job/MainViewStatus2.tsx
|
||||
|
||||
Terapkan pagination pada file "File utama"
|
||||
Analisa juga file "File utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
|
||||
|
||||
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||
|
||||
Perbaiki fetch "Fun fecth" , pada file "File fetch"
|
||||
Jika tidak ada props page maka tambahkan props page dan default page: "1"
|
||||
|
||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
||||
|
||||
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||
|
||||
<!-- End Penerapan Pagination -->
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
222
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal file
222
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
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 } 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 PAGE_SIZE = 10;
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value,
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
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={() => {
|
||||
console.log(
|
||||
"Notification >",
|
||||
selectedCategory(activeCategory as string),
|
||||
);
|
||||
router.push(data.deepLink);
|
||||
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: PAGE_SIZE,
|
||||
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: 5,
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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 { 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";
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
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: PAGE_SIZE,
|
||||
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: 5,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item event
|
||||
const renderEventItem = ({ item }: { item: any }) => (
|
||||
<Event_BoxPublishSection
|
||||
key={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/ScreenStatus.tsx
Normal file
121
screens/Event/ScreenStatus.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxWithHeaderSection,
|
||||
Grid,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiEventGetByStatus } from "@/service/api-client/api-event";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
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: PAGE_SIZE,
|
||||
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: 5,
|
||||
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();
|
||||
};
|
||||
|
||||
const tabsComponent = (
|
||||
<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 }}>
|
||||
{tabsComponent}
|
||||
</View>
|
||||
}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderEventItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
@@ -111,7 +112,6 @@ export default function DetailForum2() {
|
||||
|
||||
// Create Commentar
|
||||
const handlerCreateCommentar = async () => {
|
||||
|
||||
const cencorContent = censorText(text);
|
||||
|
||||
const newData = {
|
||||
@@ -155,7 +155,10 @@ export default function DetailForum2() {
|
||||
const headerComponent = () =>
|
||||
// Box Posting
|
||||
!data ? (
|
||||
<CustomSkeleton height={200} />
|
||||
<StackCustom>
|
||||
<CustomSkeleton height={200} />
|
||||
<CustomSkeleton height={100} />
|
||||
</StackCustom>
|
||||
) : (
|
||||
<>
|
||||
{/* Area Posting */}
|
||||
@@ -199,10 +202,7 @@ export default function DetailForum2() {
|
||||
);
|
||||
|
||||
// Render individual comment item
|
||||
const renderCommentItem = ({ item }: { item: TypeForum_CommentProps }) =>
|
||||
!data && !commentPagination.listData ? (
|
||||
<ListSkeletonComponent />
|
||||
) : (
|
||||
const renderCommentItem = ({ item }: { item: TypeForum_CommentProps }) =>(
|
||||
<Forum_CommentarBoxSection
|
||||
key={item.id}
|
||||
data={item}
|
||||
@@ -212,7 +212,21 @@ export default function DetailForum2() {
|
||||
setCommentAuthorId(value.setCommentAuthorId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
)
|
||||
// !data || !commentPagination.listData ? (
|
||||
// // <ListSkeletonComponent height={120} />
|
||||
// <LoaderCustom />
|
||||
// ) : (
|
||||
// <Forum_CommentarBoxSection
|
||||
// key={item.id}
|
||||
// data={item}
|
||||
// onSetData={(value) => {
|
||||
// setCommentId(value.setCommentId);
|
||||
// setOpenDrawerCommentar(value.setOpenDrawer);
|
||||
// setCommentAuthorId(value.setCommentAuthorId);
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
|
||||
// Generate pagination components using helper
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function Forum_ViewBeranda3() {
|
||||
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<View style={{ paddingHorizontal: 16, paddingTop: 8 }}>
|
||||
<View style={{ paddingTop: 8 }}>
|
||||
<SearchInput
|
||||
placeholder="Cari topik diskusi"
|
||||
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
110
screens/Job/MainViewStatus2.tsx
Normal file
110
screens/Job/MainViewStatus2.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
/* 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>
|
||||
);
|
||||
|
||||
// useFocusEffect(
|
||||
// useCallback(() => {
|
||||
// // Reset and load first page when category changes
|
||||
// pagination.reset();
|
||||
// // pagination.onRefresh();
|
||||
// }, [activeCategory]),
|
||||
// );
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// Reset pagination saat kategori berubah
|
||||
pagination.reset();
|
||||
};
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
266
screens/Notification/ScreenNotification_V2.tsx
Normal file
266
screens/Notification/ScreenNotification_V2.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import { 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,
|
||||
useLocalSearchParams,
|
||||
} from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value,
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const fixPath = ({
|
||||
deepLink,
|
||||
categoryApp,
|
||||
}: {
|
||||
deepLink: string;
|
||||
categoryApp: string;
|
||||
}) => {
|
||||
if (categoryApp === "OTHER") {
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
const separator = deepLink.includes("?") ? "&" : "?";
|
||||
|
||||
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
|
||||
categoryApp,
|
||||
)}`;
|
||||
|
||||
console.log("Fix Path", fixedPath);
|
||||
|
||||
return fixedPath;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
data,
|
||||
activeCategory,
|
||||
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,
|
||||
});
|
||||
|
||||
router.navigate(newPath as any);
|
||||
selectedCategory(activeCategory as string);
|
||||
|
||||
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 ScreenNotification() {
|
||||
const { user } = useAuth();
|
||||
const { category } = useLocalSearchParams<{ category?: string }>();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
category || "event",
|
||||
);
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const { markAsReadAll } = useNotificationStore();
|
||||
|
||||
// Initialize pagination for notifications
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiGetNotificationsById({
|
||||
id: user?.id as string,
|
||||
category: activeCategory as any,
|
||||
page: String(page), // API expects string
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [activeCategory],
|
||||
});
|
||||
|
||||
// useFocusEffect(
|
||||
// useCallback(() => {
|
||||
// // Reset and load first page when category changes
|
||||
// pagination.reset();
|
||||
// pagination.onRefresh();
|
||||
// }, [activeCategory]),
|
||||
// );
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// Reset and load first page when category changes
|
||||
// pagination.reset();
|
||||
// pagination.onRefresh();
|
||||
};
|
||||
|
||||
// Render individual notification item
|
||||
const renderItem = ({ item }: { item: any }) => (
|
||||
<View key={item.id}>
|
||||
<BoxNotification
|
||||
data={item}
|
||||
activeCategory={activeCategory as any}
|
||||
setListData={pagination.setListData}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
// Generate pagination components using helper
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
emptyMessage: "Belum ada notifikasi",
|
||||
skeletonCount: 5,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
|
||||
<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: () => {
|
||||
// Reset and refresh data after marking all as read
|
||||
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,5 +1,5 @@
|
||||
import { BaseBox, Grid, TextCustom } from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
@@ -8,7 +8,7 @@ export default function Portofolio_BoxView({ data }: { data: any }) {
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
style={{ backgroundColor: MainColor.darkblue }}
|
||||
style={{ backgroundColor: AccentColor.blue}}
|
||||
onPress={() => {
|
||||
router.push(`/portofolio/${data?.id}`);
|
||||
}}
|
||||
|
||||
74
screens/Portofolio/ViewListPortofolio.tsx
Normal file
74
screens/Portofolio/ViewListPortofolio.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NewWrapper, TextCustom } from "@/components";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import Portofolio_BoxView from "./BoxPortofolioView";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
|
||||
export default function ViewListPortofolio() {
|
||||
const { id } = useLocalSearchParams();
|
||||
|
||||
// Initialize pagination for portfolio items
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiGetPortofolio({
|
||||
id: id as string,
|
||||
page: String(page) // API expects string
|
||||
});
|
||||
// return response.data;
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
// Reset and load first page when id changes
|
||||
pagination.reset();
|
||||
pagination.onRefresh();
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
// Render individual portfolio item
|
||||
const renderItem = ({ item }: { item: any }) => (
|
||||
<Portofolio_BoxView key={item.id} data={item} />
|
||||
);
|
||||
|
||||
// Generate pagination components using helper
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
emptyMessage: "Tidak ada portofolio",
|
||||
skeletonCount: 3,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
// IOS
|
||||
tintColor={MainColor.yellow}
|
||||
// Android
|
||||
colors={[MainColor.yellow]}
|
||||
progressBackgroundColor={MainColor.yellow}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default function Profile_PortofolioSection({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<BaseBox>
|
||||
<BaseBox >
|
||||
<View>
|
||||
<TextCustom bold size="large" align="center">
|
||||
Portofolio
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import {
|
||||
AvatarComp,
|
||||
ClickableCustom,
|
||||
Grid,
|
||||
NewWrapper,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom
|
||||
AvatarComp,
|
||||
ClickableCustom,
|
||||
Grid,
|
||||
NewWrapper,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import {
|
||||
ICON_SIZE_SMALL,
|
||||
PAGINATION_DEFAULT_TAKE,
|
||||
} from "@/constants/constans-value";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiAllUser } from "@/service/api-client/api-user";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
@@ -18,7 +21,6 @@ import { useCallback, useRef, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
|
||||
|
||||
export default function UserSearchMainView_V2() {
|
||||
const isInitialMount = useRef(true);
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -30,7 +32,7 @@ export default function UserSearchMainView_V2() {
|
||||
hasMore,
|
||||
onRefresh,
|
||||
loadMore,
|
||||
isInitialLoad
|
||||
isInitialLoad,
|
||||
} = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiAllUser({
|
||||
@@ -83,7 +85,7 @@ export default function UserSearchMainView_V2() {
|
||||
padding: 12,
|
||||
marginBottom: 10,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 2,
|
||||
@@ -129,18 +131,19 @@ export default function UserSearchMainView_V2() {
|
||||
</View>
|
||||
);
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading,
|
||||
refreshing,
|
||||
listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Tidak ada pengguna ditemukan",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
skeletonCount: 5,
|
||||
skeletonHeight: 150,
|
||||
loadingFooterText: "Memuat lebih banyak pengguna...",
|
||||
isInitialLoad
|
||||
});
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading,
|
||||
refreshing,
|
||||
listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Tidak ada pengguna ditemukan",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
skeletonCount: 5,
|
||||
skeletonHeight: 150,
|
||||
loadingFooterText: "Memuat lebih banyak pengguna...",
|
||||
isInitialLoad,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -161,14 +164,4 @@ export default function UserSearchMainView_V2() {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <>
|
||||
// <ViewWrapper>
|
||||
// <View style={{ padding: 16 }}>
|
||||
// <TextCustom>{JSON.stringify(listData, null, 2)}</TextCustom>
|
||||
// </View>
|
||||
// </ViewWrapper>
|
||||
// </>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -14,12 +14,14 @@ export async function apiEventCreate(data: any) {
|
||||
export async function apiEventGetByStatus({
|
||||
id,
|
||||
status,
|
||||
page = "1",
|
||||
}: {
|
||||
id: string;
|
||||
status: string;
|
||||
page?: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await apiConfig.get(`/mobile/event/${id}/${status}`);
|
||||
const response = await apiConfig.get(`/mobile/event/${id}/${status}?page=${page}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -79,15 +81,18 @@ export async function apiEventDelete({ id }: { id: string }) {
|
||||
export async function apiEventGetAll({
|
||||
category,
|
||||
userId,
|
||||
page = "1",
|
||||
}: {
|
||||
category?: "beranda" | "contribution" | "all-history" | "my-history";
|
||||
userId?: string;
|
||||
page?: string;
|
||||
}) {
|
||||
try {
|
||||
const categoryEvent = category ? `?category=${category}` : "";
|
||||
const userIdCreator = userId ? `&userId=${userId}` : "";
|
||||
const pageParam = `&page=${page}`;
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/event${categoryEvent}${userIdCreator}`
|
||||
`/mobile/event${categoryEvent}${userIdCreator}${pageParam}`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,12 +14,14 @@ export async function apiJobCreate(data: any) {
|
||||
export async function apiJobGetByStatus({
|
||||
authorId,
|
||||
status,
|
||||
page = "1",
|
||||
}: {
|
||||
authorId: string;
|
||||
status: string;
|
||||
page?: string;
|
||||
}) {
|
||||
try {
|
||||
const response = await apiConfig.get(`/mobile/job/${authorId}/${status}`);
|
||||
const response = await apiConfig.get(`/mobile/job/${authorId}/${status}?page=${page}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -63,10 +65,12 @@ export async function apiJobGetAll({
|
||||
search,
|
||||
category,
|
||||
authorId,
|
||||
page = "1",
|
||||
}: {
|
||||
search?: string;
|
||||
category: "archive" | "beranda";
|
||||
authorId?: string;
|
||||
page?: string;
|
||||
}) {
|
||||
try {
|
||||
let categoryText = category ? `?category=${category}` : "";
|
||||
@@ -74,8 +78,9 @@ export async function apiJobGetAll({
|
||||
categoryText = `?category=${category}&authorId=${authorId}`;
|
||||
}
|
||||
const searchText = search ? `&search=${search}` : "";
|
||||
const pageText = `&page=${page}`;
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/job${categoryText}${searchText}`
|
||||
`/mobile/job${categoryText}${searchText}${pageText}`
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,9 +12,9 @@ export async function apiPortofolioCreate({ data }: { data: any }) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiGetPortofolio({ id }: { id: string }) {
|
||||
export async function apiGetPortofolio({ id, page = "1" }: { id: string; page?: string }) {
|
||||
try {
|
||||
const response = await apiConfig.get(`/mobile/portofolio?id=${id}`);
|
||||
const response = await apiConfig.get(`/mobile/portofolio?id=${id}&page=${page}`);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -41,16 +41,19 @@ export async function apiNotificationsSendById({
|
||||
export async function apiGetNotificationsById({
|
||||
id,
|
||||
category,
|
||||
page = "1",
|
||||
}: {
|
||||
id: string;
|
||||
category: TypeNotificationCategoryApp;
|
||||
page?: string;
|
||||
}) {
|
||||
console.log("ID", id);
|
||||
console.log("Category", category);
|
||||
console.log("Page", page);
|
||||
|
||||
try {
|
||||
const response = await apiConfig.get(
|
||||
`/mobile/notification/${id}?category=${category}`
|
||||
`/mobile/notification/${id}?category=${category}&page=${page}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
|
||||
Reference in New Issue
Block a user