Compare commits
37 Commits
fixed-admi
...
clean/31-m
| Author | SHA1 | Date | |
|---|---|---|---|
| 81bbd8e6b0 | |||
| 57159d2c45 | |||
| 66373fa65b | |||
| 6d545f2af9 | |||
| eeb95336f2 | |||
| 6fb3b229c3 | |||
| 76deec9c53 | |||
| 31948f71db | |||
| 16decd89c8 | |||
| ecbcc12abf | |||
| 0cb734e790 | |||
| 0d2fef1878 | |||
| 2e58f8c7b4 | |||
| b6cd308b0b | |||
| f68deab8c0 | |||
| 37d2fbe48a | |||
| 57c9215771 | |||
| 4efdbd3c7b | |||
| ad32eb6fe6 | |||
| a5026cc285 | |||
| 836ef709d2 | |||
| 3bbee15c3a | |||
| ad7dbaf162 | |||
| 9c94ec0454 | |||
| 4c63485a5b | |||
| f5d09a2906 | |||
| 67070bb2f1 | |||
| fb19ec60b2 | |||
| e8f5c5b174 | |||
| 74a4d88277 | |||
| 2ad93a26a8 | |||
| 768b0caa9e | |||
| 208b0ce813 | |||
| 66e6aebf41 | |||
| 32a42d1b60 | |||
| 107d4312e1 | |||
| 4862975402 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -81,4 +81,7 @@ yarn-error.*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
# secrets
|
||||
secrets/
|
||||
|
||||
# @end expo-cli
|
||||
8
.qwen/settings.json
Normal file
8
.qwen/settings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add *)"
|
||||
]
|
||||
},
|
||||
"$version": 3
|
||||
}
|
||||
7
.qwen/settings.json.orig
Normal file
7
.qwen/settings.json.orig
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(git add *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
545
QWEN.md
545
QWEN.md
@@ -2,56 +2,68 @@
|
||||
|
||||
## Project Overview
|
||||
|
||||
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named "HIPMI Badung Connect" and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
|
||||
HIPMI Mobile is a cross-platform mobile application built with Expo and React Native. The application is named **"HIPMI Badung Connect"** and serves as a platform for the HIPMI (Himpunan Pengusaha dan Pengusaha Indonesia) Badung chapter. It's designed to run on iOS, Android, and web platforms using a single codebase.
|
||||
|
||||
### Key Technologies
|
||||
- **Framework**: Expo (v54.0.0) with React Native (v0.81.4)
|
||||
- **Framework**: Expo (v54.0.0) with React Native (v0.81.5)
|
||||
- **Language**: TypeScript
|
||||
- **Architecture**: File-based routing with Expo Router
|
||||
- **State Management**: Context API
|
||||
- **State Management**: Context API (AuthContext)
|
||||
- **UI Components**: React Native Paper, custom components
|
||||
- **Maps Integration**: Mapbox Maps for React Native
|
||||
- **Maps Integration**: Maplibre Maps for React Native (`@maplibre/maplibre-react-native` v10.4.2)
|
||||
- **Push Notifications**: React Native Firebase Messaging
|
||||
- **Build System**: Metro bundler
|
||||
- **Package Manager**: Bun
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
hipmi-mobile/
|
||||
├── app/ # Main application screens and routing
|
||||
├── app/ # Main application screens and routing (Expo Router)
|
||||
│ ├── _layout.tsx # Root layout component
|
||||
│ ├── index.tsx # Entry point (Login screen)
|
||||
│ └── ...
|
||||
│ └── (application)/ # Main app screens
|
||||
│ ├── admin/ # Admin panel screens
|
||||
│ ├── (user)/ # User screens
|
||||
│ └── ...
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── _ShareComponent/ # Shared components (NewWrapper, Admin components)
|
||||
│ ├── _Icon/ # Icon components
|
||||
│ └── ...
|
||||
├── context/ # State management (AuthContext)
|
||||
├── screens/ # Screen components organized by feature
|
||||
│ ├── Admin/ # Admin panel screens
|
||||
│ │ ├── Donation/ # Donation management screens
|
||||
│ │ ├── Voting/ # Voting management screens
|
||||
│ │ ├── Event/ # Event management screens
|
||||
│ │ └── ...
|
||||
│ ├── Authentication/ # Login, registration flows
|
||||
│ ├── Collaboration/ # Collaboration features
|
||||
│ ├── Event/ # Event management
|
||||
│ ├── Forum/ # Forum functionality
|
||||
│ ├── Home/ # Home screen
|
||||
│ ├── Maps/ # Map integration
|
||||
│ ├── Profile/ # User profile
|
||||
│ ├── RootLayout/ # Root layout components
|
||||
│ └── ...
|
||||
├── assets/ # Images, icons, and static assets
|
||||
├── constants/ # Constants and configuration values
|
||||
├── helpers/ # Helper functions (pagination, etc.)
|
||||
├── hooks/ # Custom React hooks
|
||||
├── lib/ # Utility libraries
|
||||
├── navigation/ # Navigation configuration
|
||||
├── service/ # API services and business logic
|
||||
│ ├── api-admin/ # Admin API endpoints
|
||||
│ ├── api-client/ # Client API endpoints
|
||||
│ └── api-config.ts # Axios configuration
|
||||
├── hooks/ # Custom React hooks
|
||||
│ ├── use-pagination.tsx # Pagination hook
|
||||
│ └── ...
|
||||
├── helpers/ # Helper functions
|
||||
│ ├── paginationHelpers.tsx # Pagination UI helpers
|
||||
│ └── ...
|
||||
├── types/ # TypeScript type definitions
|
||||
└── utils/ # Helper functions
|
||||
├── utils/ # Utility functions
|
||||
├── constants/ # Constants and configuration values
|
||||
├── styles/ # Global styles
|
||||
├── assets/ # Images, icons, and static assets
|
||||
└── docs/ # Documentation files
|
||||
```
|
||||
|
||||
## Building and Running
|
||||
|
||||
### Prerequisites
|
||||
- Node.js (with bun as the package manager)
|
||||
- Expo CLI
|
||||
- iOS Simulator or Android Emulator (for native builds)
|
||||
- Android Studio (for Android builds)
|
||||
- Xcode (for iOS builds, macOS only)
|
||||
- **Node.js**: v18+ with Bun package manager
|
||||
- **Expo CLI**: Installed globally or via npx
|
||||
- **iOS**: Xcode (macOS only) for iOS simulator/builds
|
||||
- **Android**: Android Studio for Android emulator/builds
|
||||
|
||||
### Setup and Development
|
||||
|
||||
@@ -63,16 +75,27 @@ hipmi-mobile/
|
||||
2. **Run Development Server**
|
||||
```bash
|
||||
bun run start
|
||||
```
|
||||
Or use the shorthand:
|
||||
```bash
|
||||
# or
|
||||
bunx expo start
|
||||
```
|
||||
|
||||
3. **Platform-Specific Commands**
|
||||
- iOS: `bun run ios` or `bunx expo start --ios`
|
||||
- Android: `bun run android` or `bunx expo start --android`
|
||||
- Web: `bun run web` or `bunx expo start --web`
|
||||
```bash
|
||||
# iOS Simulator
|
||||
bun run ios
|
||||
# or
|
||||
bunx expo start --ios
|
||||
|
||||
# Android Emulator
|
||||
bun run android
|
||||
# or
|
||||
bunx expo start --android
|
||||
|
||||
# Web Browser
|
||||
bun run web
|
||||
# or
|
||||
bunx expo start --web
|
||||
```
|
||||
|
||||
4. **Linting**
|
||||
```bash
|
||||
@@ -83,13 +106,13 @@ hipmi-mobile/
|
||||
|
||||
#### EAS Build (Production)
|
||||
```bash
|
||||
# Production build
|
||||
# Production build (App Store / Play Store)
|
||||
eas build --profile production
|
||||
|
||||
# Preview build
|
||||
# Preview build (Internal distribution)
|
||||
eas build --profile preview
|
||||
|
||||
# Development build
|
||||
# Development build (Development client)
|
||||
eas build --profile development
|
||||
```
|
||||
|
||||
@@ -100,7 +123,7 @@ npx expo prebuild
|
||||
|
||||
# iOS specific
|
||||
bunx expo prebuild --platform ios
|
||||
open ios/HIPMIBADUNG.xcworkspace
|
||||
open ios/HIPMIBadungConnect.xcworkspace
|
||||
|
||||
# Android specific
|
||||
bunx expo prebuild --platform android
|
||||
@@ -110,6 +133,12 @@ bunx expo prebuild --platform android
|
||||
```bash
|
||||
# Patch version update
|
||||
npm version patch
|
||||
|
||||
# Update iOS build number
|
||||
bunx expo prebuild --platform ios
|
||||
|
||||
# Update Android version code
|
||||
bunx expo prebuild --platform android
|
||||
```
|
||||
|
||||
### Android Debugging
|
||||
@@ -126,157 +155,361 @@ adb -s <device_id> install android/app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The application uses environment variables defined in the `app.config.js` file:
|
||||
- `API_BASE_URL`: Base URL for API endpoints
|
||||
- `BASE_URL`: Base application URL
|
||||
- `DEEP_LINK_URL`: URL for deep linking functionality
|
||||
Create a `.env` file in the project root with:
|
||||
|
||||
Create a `.env` file in the project root with these variables.
|
||||
```env
|
||||
API_BASE_URL=https://your-api-base-url.com
|
||||
BASE_URL=https://your-app-url.com
|
||||
DEEP_LINK_URL=hipmimobile://
|
||||
```
|
||||
|
||||
## EAS Build Configuration
|
||||
These are loaded in `app.config.js` and accessible via `Constants.expoConfig.extra`.
|
||||
|
||||
The project uses Expo Application Services (EAS) for building and deploying:
|
||||
- **Development**: Development builds with development client
|
||||
- **Preview**: Internal distribution builds (APK for Android)
|
||||
- **Production**: App store builds (App Bundle for Android, IPA for iOS)
|
||||
## Architecture Patterns
|
||||
|
||||
Configuration is in `eas.json`.
|
||||
### 1. Separation of Concerns
|
||||
|
||||
## Features and Functionality
|
||||
**Route Files** (`app/`) should be minimal (max 5 lines):
|
||||
```typescript
|
||||
import { Admin_ScreenXXX } from "@/screens/Admin/XXX/ScreenXXX";
|
||||
|
||||
The application includes several key modules:
|
||||
- **Authentication**: Login with phone number, OTP verification, registration, terms acceptance
|
||||
- **Admin Panel**: Administrative functions for managing content and users
|
||||
- **Collaboration**: Tools for member collaboration
|
||||
- **Events**: Event management and calendar
|
||||
- **Forum**: Discussion forums
|
||||
- **Maps**: Location-based services with Mapbox integration
|
||||
- **Donations**: Donation functionality with fund disbursement tracking
|
||||
- **Job Board**: Employment opportunities
|
||||
- **Investment**: Investment-related features
|
||||
- **Voting**: Voting systems
|
||||
- **Portfolio**: Member portfolio showcase
|
||||
- **Notifications**: Push notifications via Firebase
|
||||
export default function AdminXXX() {
|
||||
return <Admin_ScreenXXX />;
|
||||
}
|
||||
```
|
||||
|
||||
**Screen Components** (`screens/`) contain all business logic:
|
||||
```typescript
|
||||
export function Admin_ScreenXXX() {
|
||||
// Logic, hooks, state management
|
||||
return <NewWrapper ... />;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Pagination Pattern
|
||||
|
||||
Using `usePagination` hook with infinite scroll:
|
||||
|
||||
```typescript
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiXXX({ page: String(page) });
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
}
|
||||
return { data: [] };
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE, // 10
|
||||
searchQuery: search,
|
||||
dependencies: [dependency],
|
||||
});
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada data",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Wrapper Components
|
||||
|
||||
**NewWrapper** (preferred for lists):
|
||||
```typescript
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
headerComponent={headerComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={<RefreshControl ... />}
|
||||
/>
|
||||
```
|
||||
|
||||
**AdminBasicBox** (for card layouts):
|
||||
```typescript
|
||||
<AdminBasicBox
|
||||
onPress={() => router.push(`/path/${item.id}`)}
|
||||
style={{ marginHorizontal: 10, marginVertical: 5 }}
|
||||
>
|
||||
<StackCustom gap={0}>
|
||||
<GridSpan_4_8 label="Label" value={<TextCustom>Value</TextCustom>} />
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
```
|
||||
|
||||
### 4. API Service Structure
|
||||
|
||||
```typescript
|
||||
// service/api-admin/api-xxx.ts
|
||||
export async function apiXXX({ page = "1" }: { page?: string }) {
|
||||
try {
|
||||
const response = await apiConfig.get(`/mobile/admin/xxx?page=${page}`);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: All list APIs should support pagination with `page` parameter (default: "1").
|
||||
|
||||
### 5. Authentication Flow
|
||||
|
||||
Managed by `AuthContext`:
|
||||
- `loginWithNomor()` - Send phone number, receive OTP
|
||||
- `validateOtp()` - Validate OTP, get token
|
||||
- `registerUser()` - Register new user
|
||||
- `logout()` - Clear session and logout
|
||||
- `userData()` - Fetch user data by token
|
||||
|
||||
## Development Conventions
|
||||
|
||||
### Coding Standards
|
||||
- TypeScript is used throughout the project for type safety
|
||||
- Component-based architecture with reusable components
|
||||
- Context API for state management (AuthContext)
|
||||
- File-based routing with Expo Router
|
||||
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
||||
- Path aliases: `@/*` maps to project root
|
||||
- **TypeScript**: Strict mode enabled
|
||||
- **Naming**:
|
||||
- Components: PascalCase (`Admin_ScreenDonationStatus`)
|
||||
- Files: PascalCase for components (`ScreenDonationStatus.tsx`)
|
||||
- Variables: camelCase
|
||||
- Constants: UPPER_SNAKE_CASE
|
||||
- **Path Aliases**: `@/*` maps to project root
|
||||
- **Imports**: Group imports by type (components, hooks, services, etc.)
|
||||
|
||||
### Architecture Patterns
|
||||
### Component Structure
|
||||
```typescript
|
||||
// 1. Imports (grouped)
|
||||
import { ... } from "@/components";
|
||||
import { ... } from "@/hooks";
|
||||
import { ... } from "@/service";
|
||||
|
||||
#### Screen Components
|
||||
- Screen components are stored in `/screens` directory organized by feature
|
||||
- Route files in `/app` import and use screen components
|
||||
- Example pattern:
|
||||
```tsx
|
||||
// app/some-route.tsx
|
||||
import SomeScreen from "@/screens/Feature/ScreenSome";
|
||||
|
||||
export default function SomeRoute() {
|
||||
return <SomeScreen />;
|
||||
}
|
||||
```
|
||||
// 2. Types/Interfaces
|
||||
interface Props { ... }
|
||||
|
||||
#### Wrapper Components
|
||||
- `NewWrapper` component is used for consistent screen layouts
|
||||
- Located at `components/_ShareComponent/NewWrapper.tsx`
|
||||
|
||||
#### Pagination Pattern
|
||||
- Use `hooks/use-pagination.tsx` and `helpers/paginationHelpers.tsx`
|
||||
- Helper functions: `createSkeletonList`, `createEmptyState`, `createLoadingFooter`, `createPaginationComponents`
|
||||
- API functions should accept `page` parameter (default: "1")
|
||||
|
||||
### API Service Structure
|
||||
- Base API configuration: `service/api-config.ts`
|
||||
- Client APIs: `service/api-client/`
|
||||
- Admin APIs: `service/api-admin/`
|
||||
- All API calls use axios with interceptors for auth token injection
|
||||
// 3. Main Component
|
||||
export function ComponentName() {
|
||||
// State
|
||||
// Hooks
|
||||
// Functions
|
||||
// Render
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
- Linting is configured with ESLint
|
||||
- Standard Expo linting configuration
|
||||
- Linting: `bun run lint`
|
||||
- No formal test suite configured yet
|
||||
|
||||
### Security
|
||||
- Firebase is integrated for authentication and messaging
|
||||
- Camera and location permissions are properly configured
|
||||
- Deep linking is secured with app domain associations
|
||||
- Auth tokens stored in AsyncStorage
|
||||
### Git Workflow
|
||||
- Feature branches: `feature/xxx` or `fixed-admin/xxx`
|
||||
- Commit messages: Clear and descriptive
|
||||
- Use CHANGE_LOG.md for tracking changes
|
||||
|
||||
## Key Dependencies
|
||||
## Key Features
|
||||
|
||||
### Core Dependencies
|
||||
- `@react-navigation/*`: Navigation solution for React Native
|
||||
- `@react-native-firebase/*`: Firebase integration for React Native
|
||||
- `@rnmapbox/maps`: Mapbox integration for React Native
|
||||
- `expo-router`: File-based routing for Expo applications
|
||||
- `react-native-paper`: Material Design components for React Native
|
||||
- `react-native-toast-message`: Toast notifications
|
||||
- `react-native-otp-entry`: OTP input components
|
||||
- `react-native-qrcode-svg`: QR code generation
|
||||
- `axios`: HTTP client for API calls
|
||||
- `lodash`: Utility library
|
||||
- `moti`: Animation library
|
||||
### Authentication
|
||||
- Phone number login with OTP
|
||||
- User registration
|
||||
- Terms & Conditions acceptance
|
||||
- Session persistence with AsyncStorage
|
||||
|
||||
### Development Dependencies
|
||||
- `@types/*`: TypeScript type definitions
|
||||
- `eslint-config-expo`: Expo-specific ESLint configuration
|
||||
- `typescript`: Type checking
|
||||
### Admin Module
|
||||
- **Dashboard**: Overview and statistics
|
||||
- **User Access**: User management
|
||||
- **Event**: Event CRUD with status management
|
||||
- **Voting**: Voting management (publish/review/reject)
|
||||
- **Donation**: Donation management with categories and transaction tracking
|
||||
- **Collaboration**: Collaboration requests
|
||||
- **Investment**: Investment management
|
||||
- **Maps**: Location-based features
|
||||
- **App Information**: Bank and business field management
|
||||
|
||||
## Platform Support
|
||||
### User Module
|
||||
- **Home**: Main dashboard
|
||||
- **Forum**: Discussion forums
|
||||
- **Profile**: User profile management
|
||||
- **Portfolio**: Member portfolio
|
||||
- **Notifications**: Push notifications via Firebase
|
||||
|
||||
The application is configured to support:
|
||||
- **iOS**:
|
||||
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
||||
- Supports tablets
|
||||
- Build number: 21
|
||||
- Google Services integration
|
||||
- Associated domains for deep linking
|
||||
- **Android**:
|
||||
- Package name: `com.bip.hipmimobileapp`
|
||||
- Version code: 4
|
||||
- Adaptive icons
|
||||
- Edge-to-edge display enabled
|
||||
- Intent filters for HTTPS deep linking
|
||||
- **Web**: Static output configuration for web deployment
|
||||
## API Configuration
|
||||
|
||||
## Special Configurations
|
||||
### Base URLs
|
||||
```typescript
|
||||
// From app.config.js extra
|
||||
API_BASE_URL: process.env.API_BASE_URL
|
||||
BASE_URL: process.env.BASE_URL
|
||||
DEEP_LINK_URL: process.env.DEEP_LINK_URL
|
||||
```
|
||||
|
||||
### Axios Interceptor
|
||||
All API calls use `apiConfig` with automatic token injection:
|
||||
```typescript
|
||||
apiConfig.interceptors.request.use(async (config) => {
|
||||
const token = await AsyncStorage.getItem("authToken");
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
```
|
||||
|
||||
## Platform Configuration
|
||||
|
||||
### iOS
|
||||
- **Bundle ID**: `com.anonymous.hipmi-mobile`
|
||||
- **Build Number**: 21
|
||||
- **Google Services**: Configured
|
||||
- **Associated Domains**: `applinks:cld-dkr-staging-hipmi.wibudev.com`
|
||||
- **Tablet Support**: Enabled
|
||||
|
||||
### Android
|
||||
- **Package**: `com.bip.hipmimobileapp`
|
||||
- **Version Code**: 4
|
||||
- **Google Services**: Configured (`google-services.json`)
|
||||
- **Deep Links**: HTTPS intent filters configured
|
||||
- **Edge-to-Edge**: Enabled
|
||||
|
||||
### Web
|
||||
- **Output**: Static
|
||||
- **Bundler**: Metro
|
||||
|
||||
## Special Integrations
|
||||
|
||||
### Firebase
|
||||
- Authentication
|
||||
- Push Notifications (FCM)
|
||||
- Configured for both iOS and Android
|
||||
|
||||
### Maplibre
|
||||
- Map integration via `@maplibre/maplibre-react-native`
|
||||
- Location permissions configured
|
||||
|
||||
### Deep Linking
|
||||
- Scheme: `hipmimobile://`
|
||||
- Associated domains: `applinks:cld-dkr-staging-hipmi.wibudev.com`
|
||||
- Configured for both iOS and Android
|
||||
|
||||
### Maps Integration
|
||||
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
||||
|
||||
### Push Notifications
|
||||
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
||||
- HTTPS: `cld-dkr-hipmi-stg.wibudev.com`
|
||||
- Configured for both platforms
|
||||
|
||||
### Camera
|
||||
Camera permissions configured for both iOS and Android with microphone access for recording.
|
||||
- Camera and microphone permissions
|
||||
- QR code generation support
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Screen
|
||||
1. Create screen component in appropriate `/screens` subdirectory
|
||||
2. Add route in `/app` directory if needed
|
||||
3. Configure navigation in `AppRoot.tsx` if custom header is needed
|
||||
### Adding a New Admin Screen
|
||||
|
||||
### Adding API Endpoint
|
||||
1. Add function in appropriate service file (`service/api-client/` or `service/api-admin/`)
|
||||
2. Use `apiConfig` axios instance for requests
|
||||
3. Include proper error handling
|
||||
1. **Create Screen Component** (`screens/Admin/Feature/ScreenXXX.tsx`):
|
||||
```typescript
|
||||
export function Admin_ScreenXXX() {
|
||||
const pagination = usePagination({...});
|
||||
const renderItem = useCallback(...);
|
||||
const headerComponent = useMemo(...);
|
||||
|
||||
return <NewWrapper ... />;
|
||||
}
|
||||
```
|
||||
|
||||
### Refactoring Pattern (from docs/prompt-for-qwen-code.md)
|
||||
When moving code from route files to screen components:
|
||||
1. Create new file in `screens/<Feature>/` directory
|
||||
2. Rename function with prefix (e.g., `Admin_`, `Donation_`)
|
||||
3. Use `NewWrapper` component for consistent layout
|
||||
4. Apply pagination helpers if displaying lists
|
||||
5. Import and call from original route file
|
||||
2. **Create Box Component** (optional, for custom item rendering):
|
||||
```typescript
|
||||
export default function Admin_BoxXXX({ item }: { item: any }) {
|
||||
return (
|
||||
<AdminBasicBox onPress={() => router.push(...)}>
|
||||
...
|
||||
</AdminBasicBox>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update API** (add pagination if needed):
|
||||
```typescript
|
||||
export async function apiXXX({ page = "1" }: { page?: string }) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
4. **Create Route File** (`app/(application)/admin/feature/xxx.tsx`):
|
||||
```typescript
|
||||
import { Admin_ScreenXXX } from "@/screens/Admin/Feature/ScreenXXX";
|
||||
|
||||
export default function AdminXXX() {
|
||||
return <Admin_ScreenXXX />;
|
||||
}
|
||||
```
|
||||
|
||||
### Updating API Endpoints
|
||||
|
||||
1. Add function in appropriate service file
|
||||
2. Include `page` parameter for list endpoints
|
||||
3. Use `apiConfig` axios instance
|
||||
4. Handle errors properly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Issues
|
||||
```bash
|
||||
# Clean and rebuild
|
||||
rm -rf node_modules
|
||||
bun install
|
||||
bunx expo prebuild --clean
|
||||
|
||||
# iOS specific
|
||||
cd ios && pod install && cd ..
|
||||
|
||||
# Android specific
|
||||
cd android && ./gradlew clean && cd ..
|
||||
```
|
||||
|
||||
### Cache Issues
|
||||
```bash
|
||||
# Clear Expo cache
|
||||
bunx expo start -c
|
||||
|
||||
# Clear Metro cache
|
||||
bunx expo start --clear
|
||||
```
|
||||
|
||||
### Dependency Issues
|
||||
```bash
|
||||
# Reinstall dependencies
|
||||
rm -rf node_modules bun.lock
|
||||
bun install
|
||||
```
|
||||
|
||||
### iOS Maplibre Crash Fix
|
||||
|
||||
When using Maplibre MapView on iOS, prevent "Attempt to recycle a mounted view" crash:
|
||||
|
||||
1. **Always render PointAnnotation** (not conditional)
|
||||
2. **Use opacity for visibility** instead of conditional rendering
|
||||
3. **Avoid key prop changes** that force remounting
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: Stable PointAnnotation
|
||||
<PointAnnotation
|
||||
coordinate={annotationCoordinate} // Always rendered
|
||||
...
|
||||
>
|
||||
<View style={{ opacity: selectedLocation ? 1 : 0 }}>
|
||||
<SelectedLocationMarker />
|
||||
</View>
|
||||
</PointAnnotation>
|
||||
|
||||
// ❌ BAD: Conditional rendering causes crash
|
||||
{selectedLocation && (
|
||||
<PointAnnotation coordinate={selectedLocation} ... />
|
||||
)}
|
||||
```
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- `docs/CHANGE_LOG.md` - Change log for recent updates
|
||||
- `docs/hipmi-note.md` - Build and deployment notes
|
||||
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns
|
||||
|
||||
## Resources
|
||||
|
||||
- [Expo Documentation](https://docs.expo.dev/)
|
||||
- [React Native Documentation](https://reactnative.dev/)
|
||||
- [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
|
||||
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
||||
- [Maplibre React Native](https://github.com/maplibre/maplibre-react-native)
|
||||
|
||||
@@ -100,8 +100,8 @@ packagingOptions {
|
||||
applicationId 'com.bip.hipmimobileapp'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 4
|
||||
versionName "1.0.1"
|
||||
versionCode 5
|
||||
versionName "1.0.2"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
|
||||
7
android/app/src/debugOptimized/AndroidManifest.xml
Normal file
7
android/app/src/debugOptimized/AndroidManifest.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
|
||||
</manifest>
|
||||
@@ -37,7 +37,7 @@
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true" data-generated="true">
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https" android:host="cld-dkr-staging-hipmi.wibudev.com" android:pathPrefix="/"/>
|
||||
<data android:scheme="https" android:host="hipmi.muku.id" android:pathPrefix="/"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
|
||||
@@ -6,32 +6,17 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.google.gms:google-services:4.4.1'
|
||||
classpath 'com.google.gms:google-services:4.4.1'
|
||||
classpath('com.android.tools.build:gradle')
|
||||
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "expo-root-project"
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
// @generated begin @rnmapbox/maps-v2-maven - expo prebuild (DO NOT MODIFY) sync-d4ccbfdff48fdba3138b02a8ba41b9722af001d8
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://api.mapbox.com/downloads/v2/releases/maven'
|
||||
// Authentication is no longer required as per Mapbox's removal of download token requirement
|
||||
// See: https://github.com/mapbox/mapbox-maps-flutter/issues/775
|
||||
// Keeping this as optional for backward compatibility
|
||||
def token = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv('RNMAPBOX_MAPS_DOWNLOAD_TOKEN')
|
||||
if (token) {
|
||||
authentication { basic(BasicAuthentication) }
|
||||
@@ -41,7 +26,11 @@ allprojects {
|
||||
}
|
||||
}
|
||||
}
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
||||
// @generated end @rnmapbox/maps-v2-maven
|
||||
apply plugin: "expo-root-project"
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
// app.config.js
|
||||
require("dotenv").config();
|
||||
|
||||
// const isDev = process.env.NODE_ENV === "development";
|
||||
// const isStaging = process.env.NEXT_PUBLIC_ENV === "staging";
|
||||
// const isProd = process.env.NEXT_PUBLIC_ENV === "production";
|
||||
|
||||
// Domain berdasarkan environment
|
||||
// const domain = isDev
|
||||
// ? "localhost:3000"
|
||||
// : isStaging
|
||||
// ? "cld-dkr-hipmi-stg.wibudev.com"
|
||||
// : "hipmi.muku.id"; // Production domain
|
||||
|
||||
export default {
|
||||
name: "HIPMI Badung Connect",
|
||||
slug: "hipmi-mobile",
|
||||
version: "1.0.1",
|
||||
version: "1.0.2",
|
||||
orientation: "portrait",
|
||||
icon: "./assets/images/icon.png",
|
||||
scheme: "hipmimobile",
|
||||
@@ -14,25 +25,27 @@ export default {
|
||||
ios: {
|
||||
supportsTablet: true,
|
||||
bundleIdentifier: "com.anonymous.hipmi-mobile",
|
||||
googleServicesFile: "./ios/HIPMIBadungConnect/GoogleService-Info.plist",
|
||||
googleServicesFile: "./secrets/GoogleService-Info.plist",
|
||||
infoPlist: {
|
||||
ITSAppUsesNonExemptEncryption: false,
|
||||
NSLocationWhenInUseUsageDescription:
|
||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||
},
|
||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||
buildNumber: "21",
|
||||
associatedDomains: [
|
||||
"applinks:hipmi.muku.id",
|
||||
],
|
||||
buildNumber: "7",
|
||||
},
|
||||
|
||||
android: {
|
||||
googleServicesFile: "./google-services.json",
|
||||
googleServicesFile: "./secrets/google-services.json",
|
||||
adaptiveIcon: {
|
||||
foregroundImage: "./assets/images/splash-icon.png",
|
||||
backgroundColor: "#ffffff",
|
||||
},
|
||||
edgeToEdgeEnabled: true,
|
||||
package: "com.bip.hipmimobileapp",
|
||||
versionCode: 4,
|
||||
versionCode: 5,
|
||||
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
|
||||
intentFilters: [
|
||||
{
|
||||
@@ -41,7 +54,7 @@ export default {
|
||||
data: [
|
||||
{
|
||||
scheme: "https",
|
||||
host: "cld-dkr-staging-hipmi.wibudev.com",
|
||||
host: "hipmi.muku.id",
|
||||
pathPrefix: "/",
|
||||
},
|
||||
],
|
||||
@@ -57,6 +70,7 @@ export default {
|
||||
},
|
||||
|
||||
plugins: [
|
||||
"./plugins/withCustomConfig",
|
||||
"expo-router",
|
||||
"expo-web-browser",
|
||||
[
|
||||
@@ -77,7 +91,6 @@ export default {
|
||||
},
|
||||
],
|
||||
"expo-font",
|
||||
"@rnmapbox/maps",
|
||||
"@react-native-firebase/app",
|
||||
[
|
||||
"expo-notifications",
|
||||
@@ -87,6 +100,7 @@ export default {
|
||||
iosDisplayInForeground: true,
|
||||
},
|
||||
],
|
||||
"@maplibre/maplibre-react-native",
|
||||
],
|
||||
|
||||
experiments: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BackButton } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import PdfViewer from "@/components/_ShareComponent/PdfViewer";
|
||||
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
@@ -7,13 +8,12 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
||||
export default function FileScreen() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const url = API_STRORAGE.GET({ fileId: id as string });
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "File",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="File" left={<BackButton />} />,
|
||||
}}
|
||||
/>
|
||||
<SafeAreaView style={{ flex: 1 }} edges={["bottom"]}>
|
||||
|
||||
@@ -105,7 +105,7 @@ export default function TakePicture() {
|
||||
</Pressable>
|
||||
|
||||
<Pressable onPress={pickImage}>
|
||||
<AntDesign name="folderopen" size={32} color="white" />
|
||||
<AntDesign name="folder-open" size={32} color="white" />
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { BackButton } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { HeaderStyles } from "@/styles/header-styles";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack } from "expo-router";
|
||||
|
||||
export default function UserLayout() {
|
||||
return (
|
||||
<>
|
||||
<Stack screenOptions={HeaderStyles}>
|
||||
<Stack>
|
||||
<Stack.Screen
|
||||
name="delete-account"
|
||||
options={{
|
||||
title: "Hapus Akun",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Hapus Akun" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -47,8 +46,7 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="user-search/index"
|
||||
options={{
|
||||
title: "Pencarian Pengguna",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Pencarian Pengguna" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -71,10 +69,18 @@ export default function UserLayout() {
|
||||
|
||||
{/* ========== Event Section ========= */}
|
||||
|
||||
{/* <Stack.Screen
|
||||
name="event/(tabs)"
|
||||
options={{
|
||||
header: () => <AppHeader title="Event" left={<BackButton path="/home" />} />,
|
||||
}}
|
||||
/> */}
|
||||
|
||||
<Stack.Screen
|
||||
name="event/(tabs)"
|
||||
options={{
|
||||
title: "Event",
|
||||
header: () => <AppHeader title="Event" left={<BackButton path="/home" />} />,
|
||||
// NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx
|
||||
// headerLeft: () => (
|
||||
// <LeftButtonCustom path="/(application)/(user)/home" />
|
||||
@@ -85,32 +91,28 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="event/create"
|
||||
options={{
|
||||
title: "Tambah Event",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Event" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="event/detail/[id]"
|
||||
options={{
|
||||
title: "Event Detail",
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
header: () => <AppHeader title="Event Detail" left={<LeftButtonCustom />} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="event/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Event",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Event" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="event/[id]/list-of-participants"
|
||||
options={{
|
||||
title: "Daftar peserta",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Daftar peserta" />,
|
||||
}}
|
||||
/>
|
||||
{/* ========== End Event Section ========= */}
|
||||
@@ -119,22 +121,19 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="collaboration/(tabs)"
|
||||
options={{
|
||||
title: "Collaboration",
|
||||
headerLeft: () => <BackButton path="/home" />,
|
||||
header: () => <AppHeader title="Collaboration" left={<BackButton path="/home" />} />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="collaboration/create"
|
||||
options={{
|
||||
title: "Tambah Proyek",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Proyek" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="collaboration/[id]/list-of-participants"
|
||||
options={{
|
||||
title: "Daftar Partisipan",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Daftar Partisipan" />,
|
||||
}}
|
||||
/>
|
||||
{/* <Stack.Screen
|
||||
@@ -147,22 +146,19 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="collaboration/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Proyek",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Proyek" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="collaboration/[id]/create-pacticipants"
|
||||
options={{
|
||||
title: "Ajukan Partisipasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Ajukan Partisipasi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="collaboration/[id]/select-of-participants"
|
||||
options={{
|
||||
title: "Pilih Partisipan",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Pilih Partisipan" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -172,29 +168,25 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="voting/create"
|
||||
options={{
|
||||
title: "Tambah Voting",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Voting" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="voting/(tabs)"
|
||||
options={{
|
||||
title: "Voting",
|
||||
headerLeft: () => <BackButton path="/home" />,
|
||||
header: () => <AppHeader title="Voting" left={<BackButton path="/home" />} />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="voting/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Voting",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Voting" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="voting/[id]/list-of-contributor"
|
||||
options={{
|
||||
title: "Daftar Kontributor",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Daftar Kontributor" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -204,8 +196,7 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="crowdfunding/index"
|
||||
options={{
|
||||
title: "Crowdfunding",
|
||||
headerLeft: () => <BackButton path="/home" />,
|
||||
header: () => <AppHeader title="Crowdfunding" left={<BackButton path="/home" />} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -215,103 +206,95 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="investment/(tabs)"
|
||||
options={{
|
||||
title: "Investasi",
|
||||
headerLeft: () => <BackButton path="/crowdfunding" />,
|
||||
header: () => <AppHeader title="Investasi" left={<BackButton path="/crowdfunding" />} />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/create"
|
||||
options={{
|
||||
title: "Tambah Investasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Investasi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/[id]/index"
|
||||
options={{
|
||||
title: "Detail Investasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Detail Investasi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Investasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Investasi" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="investment/[id]/edit-prospectus"
|
||||
options={{
|
||||
title: "Edit Prospektus",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Prospektus" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(document)/list-of-document"
|
||||
options={{
|
||||
title: "Daftar Dokumen",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Daftar Dokumen" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(document)/add-document"
|
||||
options={{
|
||||
title: "Tambah Dokumen",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Dokumen" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(document)/edit-document"
|
||||
options={{
|
||||
title: "Edit Dokumen",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Dokumen" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(news)/add-news"
|
||||
options={{
|
||||
title: "Tambah Berita",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Berita" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="investment/[id]/investor"
|
||||
options={{
|
||||
title: "Investor",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Investor" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(transaction-flow)/index"
|
||||
options={{
|
||||
title: "Pembelian Saham",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Pembelian Saham" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(transaction-flow)/select-bank"
|
||||
options={{
|
||||
title: "Pilih Bank",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Pilih Bank" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(transaction-flow)/invoice"
|
||||
options={{
|
||||
title: "Invoice",
|
||||
headerLeft: () => (
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() =>
|
||||
router.navigate(`/investment/(tabs)/transaction`)
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Invoice"
|
||||
left={
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() =>
|
||||
router.navigate(`/investment/(tabs)/transaction`)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
@@ -320,14 +303,18 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(transaction-flow)/process"
|
||||
options={{
|
||||
title: "Proses",
|
||||
headerLeft: () => (
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() =>
|
||||
router.navigate(`/investment/(tabs)/transaction`)
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Proses"
|
||||
left={
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() =>
|
||||
router.navigate(`/investment/(tabs)/transaction`)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
@@ -336,23 +323,20 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(transaction-flow)/success"
|
||||
options={{
|
||||
title: "Transaksi Berhasil",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Transaksi Berhasil" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(transaction-flow)/failed"
|
||||
options={{
|
||||
title: "Transaksi Gagal",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Transaksi Gagal" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="investment/[id]/(my-holding)/[id]"
|
||||
options={{
|
||||
title: "Detail Saham Saya",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Detail Saham Saya" />,
|
||||
}}
|
||||
/>
|
||||
{/* ========== End Investment Section ========= */}
|
||||
@@ -361,122 +345,111 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="donation/(tabs)"
|
||||
options={{
|
||||
title: "Donasi",
|
||||
headerLeft: () => <BackButton path="/crowdfunding" />,
|
||||
header: () => <AppHeader title="Donasi" left={<BackButton path="/crowdfunding" />} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="donation/create"
|
||||
options={{
|
||||
title: "Tambah Donasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Donasi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/create-story"
|
||||
options={{
|
||||
title: "Tambah Donasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Donasi" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="donation/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Donasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Donasi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/edit-story"
|
||||
options={{
|
||||
title: "Edit Donasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Donasi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/edit-rekening"
|
||||
options={{
|
||||
title: "Edit Rekening",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Rekening" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/detail-story"
|
||||
options={{
|
||||
title: "Cerita Penggalang",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Cerita Penggalang" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/infromation-fundrising"
|
||||
options={{
|
||||
title: "Informasi Penggalang Dana",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Informasi Penggalang Dana" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/list-of-donatur"
|
||||
options={{
|
||||
title: "Daftar Donatur",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Daftar Donatur" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/fund-disbursement"
|
||||
options={{
|
||||
title: "Pencairan Dana",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Pencairan Dana" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(news)/recap-of-news"
|
||||
options={{
|
||||
title: "Rekap Kabar",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Rekap Kabar" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(news)/add-news"
|
||||
options={{
|
||||
title: "Tambah Berita",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Berita" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(news)/[news]/edit-news"
|
||||
options={{
|
||||
title: "Edit Berita",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Berita" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(transaction-flow)/index"
|
||||
options={{
|
||||
title: "Donasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Donasi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(transaction-flow)/select-bank"
|
||||
options={{
|
||||
title: "Pilih Bank",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Pilih Bank" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(transaction-flow)/[invoiceId]/invoice"
|
||||
options={{
|
||||
title: "Invoice",
|
||||
headerLeft: () => (
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Invoice"
|
||||
left={
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -484,13 +457,17 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(transaction-flow)/[invoiceId]/process"
|
||||
options={{
|
||||
title: "Proses",
|
||||
headerLeft: () => (
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Proses"
|
||||
left={
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => router.navigate(`/donation/(tabs)/my-donation`)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -498,55 +475,51 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(transaction-flow)/[invoiceId]/success"
|
||||
options={{
|
||||
title: "Donasi Berhasil",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Donasi Berhasil" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="donation/[id]/(transaction-flow)/[invoiceId]/failed"
|
||||
options={{
|
||||
title: "Donasi Gagal",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Donasi Gagal" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ========== End Donation Section ========= */}
|
||||
|
||||
{/* ========== Job Section ========= */}
|
||||
|
||||
|
||||
<Stack.Screen
|
||||
name="job/create"
|
||||
options={{
|
||||
title: "Tambah Job",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Job" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="job/(tabs)"
|
||||
options={{
|
||||
title: "Job Vacancy",
|
||||
// headerLeft: () => <BackButton path="/home" />,
|
||||
// NOTE: headerLeft di pindahkan ke Tabs Layout
|
||||
header: () => <AppHeader title="Job Vacancy" left={<BackButton path="/home" />} />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="job/[id]/index"
|
||||
options={{
|
||||
title: "Detail Job",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Detail Job" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="job/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Job",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Job" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="job/[id]/archive"
|
||||
options={{
|
||||
title: "Arsip Job",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Arsip Job" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -556,78 +529,67 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="forum/create"
|
||||
options={{
|
||||
title: "Tambah Diskusi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Diskusi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Diskusi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Diskusi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/forumku"
|
||||
options={{
|
||||
title: "Forumku",
|
||||
headerLeft: () => <BackButton icon={"close"} />,
|
||||
header: () => <AppHeader title="Forumku" left={<BackButton icon={"close"} />} />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/index"
|
||||
options={{
|
||||
title: "Detail",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Detail" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/report-commentar"
|
||||
options={{
|
||||
title: "Laporkan Komentar",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Laporkan Komentar" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/other-report-commentar"
|
||||
options={{
|
||||
title: "Laporkan Komentar",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Laporkan Komentar" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/report-posting"
|
||||
options={{
|
||||
title: "Laporkan Diskusi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Laporkan Diskusi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/other-report-posting"
|
||||
options={{
|
||||
title: "Laporkan Diskusi",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Laporkan Diskusi" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/terms"
|
||||
options={{
|
||||
title: "Syarat & Ketentuan Forum",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Syarat & Ketentuan Forum" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/preview-report-posting"
|
||||
options={{
|
||||
title: "Laporan Postingan",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Laporan Postingan" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/preview-report-comment"
|
||||
options={{
|
||||
title: "Laporan Komentar",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Laporan Komentar" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -635,29 +597,25 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="maps/index"
|
||||
options={{
|
||||
title: "Maps",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Maps" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="maps/create"
|
||||
options={{
|
||||
title: "Tambah Maps",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Tambah Maps" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="maps/[id]/edit"
|
||||
options={{
|
||||
title: "Edit Maps",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Edit Maps" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="maps/[id]/custom-pin"
|
||||
options={{
|
||||
title: "Custom Pin Maps",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Custom Pin Maps" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -665,8 +623,7 @@ export default function UserLayout() {
|
||||
<Stack.Screen
|
||||
name="marketplace/index"
|
||||
options={{
|
||||
title: "Market Place",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Market Place" />,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { apiCollaborationGroup } from "@/service/api-client/api-collaboration";
|
||||
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useState, useCallback } from "react";
|
||||
@@ -40,8 +41,7 @@ export default function CollaborationRoomInfo() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Info`,
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Info" left={<BackButton />} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BackButton } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import ChatScreen from "@/screens/Collaboration/GroupChatSection";
|
||||
@@ -12,14 +13,18 @@ export default function CollaborationRoomChat() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Proyek ${detail}`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<Feather
|
||||
name="info"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => router.push(`/collaboration/${id}/${detail}/info`)}
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title={`Proyek ${detail}`}
|
||||
left={<BackButton />}
|
||||
right={
|
||||
<Feather
|
||||
name="info"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => router.push(`/collaboration/${id}/${detail}/info`)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuDrawerDynamicGrid,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
||||
import { apiCollaborationGetOne } from "@/service/api-client/api-collaboration";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
@@ -38,10 +39,14 @@ export default function CollaborationDetailParticipant() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Detail Proyek",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Proyek"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Spacing,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
||||
import {
|
||||
@@ -66,9 +67,13 @@ export default function CollaborationDetailProjectMain() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Proyek Saya",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Proyek Saya"
|
||||
left={<BackButton />}
|
||||
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
MenuDrawerDynamicGrid,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
|
||||
import {
|
||||
@@ -74,10 +75,14 @@ export default function CollaborationDetail() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Detail Proyek",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<DotButton onPress={() => setOpenDrawerMenu(true)} />
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Proyek"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
<DotButton onPress={() => setOpenDrawerMenu(true)} />
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -57,12 +58,17 @@ export default function DonationNews() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Detail Kabar",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
user?.id === data?.authorId && (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
),
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Kabar"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
user?.id === data?.authorId && (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconEdit, IconNews } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
@@ -97,14 +98,19 @@ export default function DonasiDetailStatus() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail ${_.startCase(status as string)}`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title={`Detail ${_.startCase(status as string)}`}
|
||||
left={<BackButton />}
|
||||
right={
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<NewWrapper
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
StackCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconNews } from "@/components/_Icon";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -90,12 +91,17 @@ export default function DonasiDetailBeranda() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail Donasi`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
user?.id === data?.Author?.id ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Donasi"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
user?.id === data?.Author?.id ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<NewWrapper footerComponent={buttonSection}>
|
||||
|
||||
@@ -4,36 +4,34 @@ import {
|
||||
IconHome,
|
||||
IconStatus,
|
||||
} from "@/components/_Icon";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
import { router, Tabs, useLocalSearchParams } from "expo-router";
|
||||
|
||||
export default function EventTabsLayout() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { from, category } = useLocalSearchParams<{
|
||||
from?: string;
|
||||
category?: string;
|
||||
}>();
|
||||
|
||||
console.log("from", from);
|
||||
console.log("category", category);
|
||||
|
||||
// Atur header secara dinamis
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<BackButtonFromNotification
|
||||
from={from as string}
|
||||
category={category as string}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
...TabsStyles,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Event"
|
||||
left={
|
||||
<BackButtonFromNotification
|
||||
from={from as string}
|
||||
category={category as string}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection";
|
||||
@@ -81,12 +82,17 @@ export default function EventDetailStatus() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail ${status === "publish" ? "" : status}`,
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
headerRight: () =>
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title={`Detail ${status === "publish" ? "" : status}`}
|
||||
left={<LeftButtonCustom />}
|
||||
right={
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiEventConfirmationAction,
|
||||
@@ -60,7 +61,7 @@ export default function UserEventConfirmation() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
checkTokenAndDataParticipants() || console.log("Token is null");
|
||||
}, [token, id, user?.id])
|
||||
}, [token, id, user?.id]),
|
||||
);
|
||||
|
||||
const checkTokenAndDataParticipants = async () => {
|
||||
@@ -113,7 +114,7 @@ export default function UserEventConfirmation() {
|
||||
confirmationStart,
|
||||
confirmationEnd,
|
||||
null,
|
||||
"[]"
|
||||
"[]",
|
||||
);
|
||||
|
||||
// --- [4] Status waktu event (untuk pesan UI) ---
|
||||
@@ -218,9 +219,14 @@ export default function UserEventConfirmation() {
|
||||
if (isWithinConfirmationWindow) {
|
||||
if (konfirmasi === false) {
|
||||
return (
|
||||
<TamplateBox data={data}>
|
||||
<TamplateText text="Konfirmasi Kehadiran" />
|
||||
</TamplateBox>
|
||||
// <TamplateBox data={data}>
|
||||
// <TamplateText text="Konfirmasi Kehadiran" />
|
||||
// </TamplateBox>
|
||||
<UserParticipan_And_DuringEvent
|
||||
id={data.id}
|
||||
userId={user?.id as string}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
@@ -260,18 +266,20 @@ export default function UserEventConfirmation() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Konfirmasi Event",
|
||||
// headerLeft: () => (
|
||||
// <Ionicons
|
||||
// name="arrow-back"
|
||||
// size={20}
|
||||
// color={MainColor.yellow}
|
||||
// onPress={() =>
|
||||
// router.navigate("/(application)/(user)/event/create")
|
||||
// }
|
||||
// />
|
||||
// ),
|
||||
}}
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Konfirmasi Event"
|
||||
left={
|
||||
<Ionicons
|
||||
name="arrow-back"
|
||||
size={20}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => router.navigate("/")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>{handlerReturn()}</ViewWrapper>
|
||||
</>
|
||||
@@ -497,7 +505,6 @@ const UserNotParticipan_And_DuringEvent = ({
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// 🟡 ZONA ACARA BERLANGSUN
|
||||
// User sudah terdaftar & Event sedang berlangsung & user harus konfirmasi
|
||||
const UserParticipan_And_DuringEvent = ({
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Spacing,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
|
||||
@@ -49,9 +50,13 @@ export default function EventDetailContribution() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail kontribusi`,
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail kontribusi"
|
||||
left={<LeftButtonCustom />}
|
||||
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ViewWrapper,
|
||||
Spacing,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
|
||||
@@ -44,9 +45,13 @@ export default function EventDetailHistory() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail riwayat`,
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail riwayat"
|
||||
left={<LeftButtonCustom />}
|
||||
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
MenuDrawerDynamicGrid,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
@@ -156,9 +157,13 @@ export default function EventDetailPublish() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Event Publish`,
|
||||
headerLeft: () => <BackButton onPress={() => router.back()} />,
|
||||
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Event Publish"
|
||||
left={<BackButton onPress={() => router.back()} />}
|
||||
right={<DotButton onPress={() => setOpenDrawer(true)} />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -1,66 +1,94 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
|
||||
import { BasicWrapper, NewWrapper, StackCustom, ViewWrapper } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import Home_BottomFeatureSection from "@/screens/Home/bottomFeatureSection";
|
||||
import HeaderBell from "@/screens/Home/HeaderBell";
|
||||
import { stylesHome } from "@/screens/Home/homeViewStyle";
|
||||
import Home_ImageSection from "@/screens/Home/imageSection";
|
||||
import TabSection from "@/screens/Home/tabSection";
|
||||
import { tabsHome } from "@/screens/Home/tabsList";
|
||||
import Home_FeatureSection from "@/screens/Home/topFeatureSection";
|
||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import { apiUser } from "@/service/api-client/api-user";
|
||||
import { apiVersion } from "@/service/api-config";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
export default function Application() {
|
||||
const { token, user, userData } = useAuth();
|
||||
const [data, setData] = useState<any>();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const { syncUnreadCount } = useNotificationStore();
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
onLoadDataJob();
|
||||
checkVersion();
|
||||
userData(token as string);
|
||||
userData(token as string).catch((error) => {
|
||||
console.log("[ERROR userData]", error?.message);
|
||||
console.log("[ERROR userData Response]", error?.response?.data);
|
||||
});
|
||||
syncUnreadCount();
|
||||
}, [user?.id, token]),
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
const response = await apiUser(user?.id as string);
|
||||
console.log(
|
||||
"[Profile ID]>>",
|
||||
JSON.stringify(response?.data?.Profile?.id, null, 2),
|
||||
);
|
||||
|
||||
setData(response.data);
|
||||
try {
|
||||
const response = await apiUser(user?.id as string);
|
||||
setData(response.data);
|
||||
} catch (error: any) {
|
||||
console.log("[ERROR onLoadData]", error?.message);
|
||||
console.log("[ERROR Response]", error?.response?.data);
|
||||
// Set data tetap agar UI tidak stuck di loading
|
||||
setData(null);
|
||||
}
|
||||
}
|
||||
|
||||
const onLoadDataJob = async () => {
|
||||
try {
|
||||
const response = await apiJobGetAll({
|
||||
category: "beranda",
|
||||
});
|
||||
const result = response.data
|
||||
.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
)
|
||||
.slice(0, 2);
|
||||
setListData(result);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
const checkVersion = async () => {
|
||||
const response = await apiVersion();
|
||||
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
|
||||
try {
|
||||
const response = await apiVersion();
|
||||
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
|
||||
} catch (error: any) {
|
||||
console.log("[ERROR checkVersion]", error?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
onLoadData();
|
||||
onLoadDataJob();
|
||||
checkVersion();
|
||||
setRefreshing(false);
|
||||
}, []);
|
||||
|
||||
// if (user && user?.termsOfServiceAccepted === false) {
|
||||
// console.log("User is not accept term service");
|
||||
// return <Redirect href={`/terms-agreement`} />;
|
||||
// }
|
||||
|
||||
if (data && data?.active === false) {
|
||||
console.log("User is not active");
|
||||
console.warn("User is not active");
|
||||
return (
|
||||
<BasicWrapper>
|
||||
<Redirect href={`/waiting-room`} />
|
||||
@@ -69,7 +97,7 @@ export default function Application() {
|
||||
}
|
||||
|
||||
if (data && data?.Profile === null) {
|
||||
console.log("Profile is null");
|
||||
console.warn("Profile is null");
|
||||
return (
|
||||
<BasicWrapper>
|
||||
<Redirect href={`/profile/create`} />
|
||||
@@ -90,21 +118,37 @@ export default function Application() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `HIPMI`,
|
||||
headerLeft: () => (
|
||||
<Ionicons
|
||||
name="search"
|
||||
size={20}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => {
|
||||
router.push("/user-search");
|
||||
}}
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="HIPMI"
|
||||
showBack={false}
|
||||
left={
|
||||
data ? (
|
||||
<Ionicons
|
||||
name="search"
|
||||
size={20}
|
||||
color={MainColor.yellow}
|
||||
onPress={() => {
|
||||
router.push("/user-search");
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<CustomSkeleton height={30} width={30} radius={100} />
|
||||
)
|
||||
}
|
||||
right={
|
||||
data ? (
|
||||
<HeaderBell />
|
||||
) : (
|
||||
<CustomSkeleton height={30} width={30} radius={100} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
headerRight: () => <HeaderBell />,
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper
|
||||
|
||||
<NewWrapper
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
@@ -114,26 +158,54 @@ export default function Application() {
|
||||
/>
|
||||
}
|
||||
footerComponent={
|
||||
<TabSection
|
||||
tabs={tabsHome({
|
||||
acceptedForumTermsAt: data?.acceptedForumTermsAt,
|
||||
profileId: data?.Profile?.id,
|
||||
})}
|
||||
/>
|
||||
data && data ? (
|
||||
<TabSection
|
||||
tabs={tabsHome({
|
||||
acceptedForumTermsAt: data?.acceptedForumTermsAt,
|
||||
profileId: data?.Profile?.id,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
null
|
||||
// <View style={GStyles.tabBar}>
|
||||
// <View style={[GStyles.tabContainer, { paddingTop: 10 }]}>
|
||||
// {Array.from({ length: 4 }).map((e, index) => (
|
||||
// <CustomSkeleton
|
||||
// key={index}
|
||||
// height={40}
|
||||
// width={40}
|
||||
// radius={100}
|
||||
// />
|
||||
// ))}
|
||||
// </View>
|
||||
// </View>
|
||||
)
|
||||
}
|
||||
>
|
||||
<StackCustom>
|
||||
{/* <ButtonCustom onPress={() => router.push("./test-notifications")}>
|
||||
Test Notif
|
||||
</ButtonCustom> */}
|
||||
|
||||
<Home_ImageSection />
|
||||
|
||||
<Home_FeatureSection />
|
||||
{data && data ? (
|
||||
<Home_FeatureSection />
|
||||
) : (
|
||||
<View style={stylesHome.gridContainer}>
|
||||
{Array.from({ length: 4 }).map((item, index) => (
|
||||
<CustomSkeleton
|
||||
key={index}
|
||||
style={stylesHome.gridItem}
|
||||
radius={50}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Home_BottomFeatureSection />
|
||||
{data ? (
|
||||
<Home_BottomFeatureSection listData={listData} />
|
||||
) : (
|
||||
<CustomSkeleton height={150} />
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -30,13 +31,13 @@ export default function InvestmentDetailHolding() {
|
||||
const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
|
||||
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
|
||||
const [data, setData] = useState<any>(null);
|
||||
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id, status])
|
||||
);
|
||||
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiInvestmentGetInvoice({
|
||||
@@ -44,7 +45,7 @@ export default function InvestmentDetailHolding() {
|
||||
authorId: user?.id,
|
||||
category: "invoice",
|
||||
});
|
||||
|
||||
|
||||
console.log("[DATA]", JSON.stringify(response.data, null, 2));
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
@@ -76,14 +77,19 @@ export default function InvestmentDetailHolding() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail ${_.startCase(status as string)}`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title={`Detail ${_.startCase(status as string)}`}
|
||||
left={<BackButton />}
|
||||
right={
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
@@ -56,12 +57,17 @@ export default function InvestmentNews() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Detail Berita",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
user?.id === data?.authorId && (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
),
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Berita"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
user?.id === data?.authorId && (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuDrawerDynamicGrid,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -106,14 +107,19 @@ export default function InvestmentDetailStatus() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail ${_.startCase(status as string)}`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title={`Detail ${_.startCase(status as string)}`}
|
||||
left={<BackButton />}
|
||||
right={
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuDrawerDynamicGrid,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconDocument, IconEdit, IconNews } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -105,14 +106,19 @@ export default function InvestmentDetail() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail ${_.startCase(status as string)}`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title={`Detail ${_.startCase(status as string)}`}
|
||||
left={<BackButton />}
|
||||
right={
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BackButton } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconHome, IconStatus } from "@/components/_Icon";
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
@@ -7,31 +8,30 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import {
|
||||
router,
|
||||
Tabs,
|
||||
useLocalSearchParams,
|
||||
useNavigation
|
||||
useLocalSearchParams
|
||||
} from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export default function JobTabsLayout() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { from, category } = useLocalSearchParams<{
|
||||
from?: string;
|
||||
category?: string;
|
||||
}>();
|
||||
|
||||
// Atur header secara dinamis
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<BackButtonFromNotification from={from as string} category={category as string} />
|
||||
),
|
||||
});
|
||||
}, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
...TabsStyles,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Job Vacancy"
|
||||
left={
|
||||
<BackButtonFromNotification from={from as string} category={category as string} />
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
StackCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
@@ -58,12 +59,17 @@ export default function JobDetailStatus() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawer(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -1,244 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
Spacing,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import API_IMAGE from "@/constants/api-storage";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { apiMapsGetOne, apiMapsUpdate } from "@/service/api-client/api-maps";
|
||||
import { uploadFileService } from "@/service/upload-service";
|
||||
import pickFile, { IFileData } from "@/utils/pickFile";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { StyleSheet, View } from "react-native";
|
||||
import MapView, { LatLng, Marker } from "react-native-maps";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { Maps_ScreenMapsEdit } from "@/screens/Maps/ScreenMapsEdit";
|
||||
|
||||
const defaultRegion = {
|
||||
latitude: -8.737109,
|
||||
longitude: 115.1756897,
|
||||
latitudeDelta: 0.1,
|
||||
longitudeDelta: 0.1,
|
||||
};
|
||||
export default function MapsEdit() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>({
|
||||
id: "",
|
||||
namePin: "",
|
||||
latitude: "",
|
||||
longitude: "",
|
||||
imageId: "",
|
||||
});
|
||||
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
|
||||
const [image, setImage] = useState<IFileData | null>(null);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiMapsGetOne({ id: id as string });
|
||||
|
||||
if (response.success) {
|
||||
setData({
|
||||
id: response.data.id,
|
||||
namePin: response.data.namePin,
|
||||
latitude: response.data.latitude,
|
||||
longitude: response.data.longitude,
|
||||
imageId: response.data.imageId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMapPress = (event: any) => {
|
||||
const { latitude, longitude } = event.nativeEvent.coordinate;
|
||||
const location = { latitude, longitude };
|
||||
setSelectedLocation(location);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
let newData: any;
|
||||
if (!data.namePin) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Nama pin harus diisi",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
newData = {
|
||||
namePin: data?.namePin,
|
||||
latitude: selectedLocation?.latitude || data?.latitude,
|
||||
longitude: selectedLocation?.longitude || data?.longitude,
|
||||
};
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
if (image) {
|
||||
const responseUpload = await uploadFileService({
|
||||
dirId: DIRECTORY_ID.map_image,
|
||||
imageUri: image?.uri,
|
||||
});
|
||||
|
||||
if (!responseUpload?.data?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mengunggah gambar",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = responseUpload?.data?.id;
|
||||
|
||||
newData = {
|
||||
namePin: data?.namePin,
|
||||
latitude: selectedLocation?.latitude,
|
||||
longitude: selectedLocation?.longitude,
|
||||
newImageId: imageId,
|
||||
};
|
||||
}
|
||||
|
||||
const responseUpdate = await apiMapsUpdate({
|
||||
id: data?.id,
|
||||
data: newData,
|
||||
});
|
||||
|
||||
if (!responseUpdate.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mengupdate map",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Map berhasil diupdate",
|
||||
});
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonFooter = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data.namePin}
|
||||
onPress={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonFooter}>
|
||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
||||
|
||||
<View style={[styles.container, { height: 400 }]}>
|
||||
<MapView
|
||||
style={styles.map}
|
||||
initialRegion={
|
||||
data?.latitude && data?.longitude
|
||||
? {
|
||||
latitude: data?.latitude,
|
||||
longitude: data?.longitude,
|
||||
latitudeDelta: 0.1,
|
||||
longitudeDelta: 0.1,
|
||||
}
|
||||
: defaultRegion
|
||||
}
|
||||
onPress={handleMapPress}
|
||||
showsUserLocation={true}
|
||||
showsMyLocationButton={true}
|
||||
loadingEnabled={true}
|
||||
loadingIndicatorColor="#666"
|
||||
loadingBackgroundColor="#f0f0f0"
|
||||
>
|
||||
{selectedLocation ? (
|
||||
<Marker
|
||||
coordinate={selectedLocation}
|
||||
title="Lokasi Dipilih"
|
||||
description={`Lat: ${selectedLocation.latitude.toFixed(
|
||||
6
|
||||
)}, Lng: ${selectedLocation.longitude.toFixed(6)}`}
|
||||
pinColor="red"
|
||||
/>
|
||||
) : (
|
||||
<Marker
|
||||
coordinate={defaultRegion}
|
||||
title="Lokasi Dipilih"
|
||||
description={`Lat: ${defaultRegion.latitude.toFixed(
|
||||
6
|
||||
)}, Lng: ${defaultRegion.longitude.toFixed(6)}`}
|
||||
pinColor="red"
|
||||
/>
|
||||
)}
|
||||
</MapView>
|
||||
</View>
|
||||
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Nama Pin"
|
||||
placeholder="Masukkan nama pin maps"
|
||||
value={data?.namePin}
|
||||
onChangeText={(value) => setData({ ...data, namePin: value })}
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
|
||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
||||
<LandscapeFrameUploaded
|
||||
image={
|
||||
image
|
||||
? image?.uri
|
||||
: API_IMAGE.GET({ fileId: data?.imageId as string })
|
||||
}
|
||||
/>
|
||||
<ButtonCenteredOnly
|
||||
icon="upload"
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file) {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
);
|
||||
return <Maps_ScreenMapsEdit />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
backgroundColor: "#f5f5f5",
|
||||
overflow: "hidden",
|
||||
borderRadius: 8,
|
||||
marginBottom: 20,
|
||||
},
|
||||
map: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,143 +1,9 @@
|
||||
import {
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
Spacing,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import MapSelected from "@/components/Map/MapSelected";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiMapsCreate } from "@/service/api-client/api-maps";
|
||||
import { uploadFileService } from "@/service/upload-service";
|
||||
import pickFile, { IFileData } from "@/utils/pickFile";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { LatLng } from "react-native-maps";
|
||||
import Toast from "react-native-toast-message";
|
||||
import Maps_ScreenMapsCreate from "@/screens/Maps/ScreenMapsCreate";
|
||||
|
||||
export default function MapsCreate() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [image, setImage] = useState<IFileData | null>(null);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
let newData: any;
|
||||
newData = {
|
||||
authorId: user?.id,
|
||||
portofolioId: id,
|
||||
namePin: name,
|
||||
latitude: selectedLocation?.latitude,
|
||||
longitude: selectedLocation?.longitude,
|
||||
};
|
||||
|
||||
if (image) {
|
||||
const responseUpload = await uploadFileService({
|
||||
dirId: DIRECTORY_ID.map_image,
|
||||
imageUri: image?.uri,
|
||||
});
|
||||
|
||||
if (!responseUpload?.data?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mengunggah gambar",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = responseUpload?.data?.id;
|
||||
|
||||
newData = {
|
||||
authorId: user?.id,
|
||||
portofolioId: id,
|
||||
namePin: name,
|
||||
latitude: selectedLocation?.latitude,
|
||||
longitude: selectedLocation?.longitude,
|
||||
imageId: imageId,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await apiMapsCreate({
|
||||
data: newData,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal menambahkan map",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Map berhasil ditambahkan",
|
||||
});
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonFooter = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
disabled={!selectedLocation || name === ""}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonFooter}>
|
||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
||||
|
||||
<BaseBox>
|
||||
<MapSelected
|
||||
selectedLocation={selectedLocation as any}
|
||||
setSelectedLocation={setSelectedLocation}
|
||||
/>
|
||||
</BaseBox>
|
||||
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Nama Pin"
|
||||
placeholder="Masukkan nama pin maps"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
/>
|
||||
|
||||
<Spacing height={50} />
|
||||
|
||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
||||
<LandscapeFrameUploaded image={image?.uri} />
|
||||
<ButtonCenteredOnly
|
||||
icon="upload"
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file) {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Maps_ScreenMapsCreate />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import MapsView from "@/screens/Maps/MapsView";
|
||||
import MapsView2 from "@/screens/Maps/MapsView2";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export interface LocationItem {
|
||||
id: string | number;
|
||||
@@ -13,8 +11,14 @@ export interface LocationItem {
|
||||
export default function Maps() {
|
||||
return (
|
||||
<>
|
||||
<MapsView />
|
||||
{/* <MapsView2 />, */}
|
||||
{/* <Stack.Screen
|
||||
options={{
|
||||
title: "Maps",
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/> */}
|
||||
{/* {Platform.OS === "ios" ? <MapsView /> : <MapsView2 />} */}
|
||||
<MapsView2 />
|
||||
{/* <View style={{ flex: 1, backgroundColor: "gray" }}><Text style={{ color: "white" }}>Map disabled</Text></View> */}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,365 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
ActionIcon,
|
||||
AvatarComp,
|
||||
BaseBox,
|
||||
ButtonCenteredOnly,
|
||||
CenterCustom,
|
||||
Grid,
|
||||
InformationBox,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
import DUMMY_IMAGE from "@/constants/dummy-image-value";
|
||||
import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
|
||||
import {
|
||||
apiMasterBidangBisnis,
|
||||
apiMasterSubBidangBisnis,
|
||||
} from "@/service/api-client/api-master";
|
||||
import {
|
||||
IMasterBidangBisnis,
|
||||
IMasterSubBidangBisnis,
|
||||
} from "@/types/Type-Master";
|
||||
import pickImage from "@/utils/pickImage";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Image } from "expo-image";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import PhoneInput, { ICountry } from "react-native-international-phone-number";
|
||||
import { Avatar } from "react-native-paper";
|
||||
import { ScreenPortofolioCreate } from "@/screens/Portofolio/ScreenPortofolioCreate";
|
||||
|
||||
export default function PortofolioCreate() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
|
||||
const [inputValue, setInputValue] = useState<string>("");
|
||||
const [data, setData] = useState({
|
||||
namaBisnis: "",
|
||||
masterBidangBisnisId: "",
|
||||
alamatKantor: "",
|
||||
tlpn: "",
|
||||
deskripsi: "",
|
||||
});
|
||||
const [imageUri, setImageUri] = useState<string | null>(null);
|
||||
|
||||
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
|
||||
const [subBidangBisnis, setSubBidangBisnis] = useState<
|
||||
IMasterSubBidangBisnis[]
|
||||
>([]);
|
||||
|
||||
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
|
||||
const [listSubBidangSelected, setListSubBidangSelected] = useState([
|
||||
{
|
||||
id: "",
|
||||
},
|
||||
]);
|
||||
|
||||
const [dataMedsos, setDataMedsos] = useState({
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
instagram: "",
|
||||
youtube: "",
|
||||
tiktok: "",
|
||||
});
|
||||
|
||||
const [isLoadingCreate, setIsLoadingCreate] = useState(false);
|
||||
|
||||
function handleInputValue(phoneNumber: string) {
|
||||
setInputValue(phoneNumber);
|
||||
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
|
||||
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
|
||||
const realNumber = callingCode + fixNumber;
|
||||
setData({ ...data, tlpn: realNumber });
|
||||
}
|
||||
|
||||
function handleSelectedCountry(country: ICountry) {
|
||||
setSelectedCountry(country);
|
||||
}
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadMaster();
|
||||
onLoadMasterSubBidangBisnis();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
try {
|
||||
const response = await apiMasterBidangBisnis();
|
||||
setBidangBisnis(response.data);
|
||||
} catch (error) {
|
||||
setBidangBisnis([]);
|
||||
console.log("Error onLoadMasterBidangBisnis", error);
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadMasterSubBidangBisnis = async () => {
|
||||
try {
|
||||
const response = await apiMasterSubBidangBisnis({});
|
||||
setSubBidangBisnis(response.data);
|
||||
} catch (error) {
|
||||
setSubBidangBisnis([]);
|
||||
console.log("Error onLoadMasterBidangBisnis", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
|
||||
const selectedList = subBidangBisnis?.filter(
|
||||
(item) => (item?.masterBidangBisnisId as any) === id
|
||||
);
|
||||
setSelectedSubBidang(selectedList as any[]);
|
||||
};
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<Portofolio_ButtonCreate
|
||||
id={id as string}
|
||||
data={data}
|
||||
dataMedsos={dataMedsos}
|
||||
imageUri={imageUri}
|
||||
subBidangSelected={listSubBidangSelected}
|
||||
isLoadingCreate={isLoadingCreate}
|
||||
setIsLoadingCreate={setIsLoadingCreate}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{/* <TextCustom>Portofolio Create {id}</TextCustom> */}
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Lengkapi data bisnis anda." />
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Nama Bisnis"
|
||||
placeholder="Masukkan nama bisnis"
|
||||
onChangeText={(value: any) => setData({ ...data, namaBisnis: value })}
|
||||
/>
|
||||
|
||||
<SelectCustom
|
||||
label="Bidang Usaha"
|
||||
required
|
||||
data={bidangBisnis.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={data.masterBidangBisnisId}
|
||||
onChange={(value) => {
|
||||
const isSameBidang = data.masterBidangBisnisId === value;
|
||||
|
||||
if (!isSameBidang) {
|
||||
setListSubBidangSelected([{ id: "" }]);
|
||||
}
|
||||
|
||||
setData({ ...(data as any), masterBidangBisnisId: value });
|
||||
handlerSelectedSubBidang({ id: value as string });
|
||||
}}
|
||||
/>
|
||||
|
||||
{listSubBidangSelected.map((item, index) => (
|
||||
<SelectCustom
|
||||
key={index}
|
||||
disabled={data.masterBidangBisnisId === ""}
|
||||
label="Sub Bidang Usaha"
|
||||
required
|
||||
data={_.map(selectedSubBidang as any)
|
||||
.filter((option: any) => {
|
||||
const selectedValues = listSubBidangSelected.map((s) => s.id);
|
||||
return (
|
||||
option.id === item.id || // biarkan tetap muncul kalau ini valuenya sendiri
|
||||
!selectedValues.includes(option.id)
|
||||
);
|
||||
})
|
||||
.map((e: any) => ({
|
||||
value: e.id,
|
||||
label: e.name,
|
||||
}))}
|
||||
value={item.id || null}
|
||||
onChange={(value) => {
|
||||
const list = _.clone(listSubBidangSelected);
|
||||
list[index].id = value as any;
|
||||
setListSubBidangSelected(list);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<CenterCustom>
|
||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
|
||||
<ActionIcon
|
||||
disabled={
|
||||
selectedSubBidang.length === listSubBidangSelected.length
|
||||
}
|
||||
onPress={() => {
|
||||
setListSubBidangSelected([
|
||||
...listSubBidangSelected,
|
||||
{ id: "" },
|
||||
]);
|
||||
}}
|
||||
icon={
|
||||
<Ionicons
|
||||
name="add-circle-outline"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.black}
|
||||
/>
|
||||
}
|
||||
size="xl"
|
||||
/>
|
||||
<ActionIcon
|
||||
disabled={listSubBidangSelected.length <= 1}
|
||||
onPress={() => {
|
||||
const list = _.clone(listSubBidangSelected);
|
||||
list.pop();
|
||||
setListSubBidangSelected(list);
|
||||
}}
|
||||
icon={
|
||||
<Ionicons
|
||||
name="remove-circle-outline"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.black}
|
||||
/>
|
||||
}
|
||||
size="xl"
|
||||
/>
|
||||
</View>
|
||||
</CenterCustom>
|
||||
<Spacing />
|
||||
|
||||
{/* <SelectCustom
|
||||
label="Bidang Usaha"
|
||||
required
|
||||
data={bidangBisnis.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={null}
|
||||
onChange={(value) => {
|
||||
setData({ ...(data as any), masterBidangBisnisId: value });
|
||||
handlerSelectedSubBidang({ id: value as string });
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{/* <ButtonCenteredOnly
|
||||
onPress={() => {
|
||||
setListSubBidangSelected([...listSubBidangSelected, { id: "" }]);
|
||||
}}
|
||||
>
|
||||
Tambah Pilihan
|
||||
</ButtonCenteredOnly>
|
||||
<Spacing /> */}
|
||||
|
||||
{/* <TextCustom>{JSON.stringify(bidangBisnis, null, 2)}</TextCustom> */}
|
||||
|
||||
<View>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
<TextCustom semiBold style={{ color: MainColor.white_gray }}>
|
||||
Nomor Telepon
|
||||
</TextCustom>
|
||||
<Text style={{ color: "red" }}> *</Text>
|
||||
</View>
|
||||
<Spacing height={5} />
|
||||
<PhoneInput
|
||||
value={inputValue}
|
||||
onChangePhoneNumber={handleInputValue}
|
||||
selectedCountry={selectedCountry}
|
||||
onChangeSelectedCountry={handleSelectedCountry}
|
||||
defaultCountry="ID"
|
||||
placeholder="xxx-xxx-xxx"
|
||||
/>
|
||||
</View>
|
||||
<Spacing />
|
||||
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Alamat Bisnis"
|
||||
placeholder="Masukkan alamat bisnis"
|
||||
onChangeText={(value: any) =>
|
||||
setData({ ...data, alamatKantor: value })
|
||||
}
|
||||
/>
|
||||
|
||||
<TextAreaCustom
|
||||
label="Deskripsi Bisnis"
|
||||
placeholder="Masukkan deskripsi bisnis"
|
||||
value={data.deskripsi}
|
||||
onChangeText={(value: any) => setData({ ...data, deskripsi: value })}
|
||||
autosize
|
||||
minRows={2}
|
||||
maxRows={5}
|
||||
required
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
<Spacing />
|
||||
|
||||
{/* Logo */}
|
||||
<InformationBox text="Upload logo bisnis anda untuk di tampilaka pada portofolio." />
|
||||
|
||||
<CenterCustom>
|
||||
<Avatar.Image
|
||||
source={imageUri ? { uri: imageUri } : DUMMY_IMAGE.dummy_image}
|
||||
size={200}
|
||||
/>
|
||||
</CenterCustom>
|
||||
<Spacing />
|
||||
<ButtonCenteredOnly
|
||||
icon="upload"
|
||||
onPress={() => {
|
||||
pickImage({
|
||||
setImageUri,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
<Spacing height={40} />
|
||||
|
||||
{/* Social Media */}
|
||||
<InformationBox text="Isi hanya pada sosial media yang anda miliki." />
|
||||
<TextInputCustom
|
||||
label="Tiktok"
|
||||
placeholder="Masukkan username tiktok"
|
||||
onChangeText={(value: any) =>
|
||||
setDataMedsos({ ...dataMedsos, tiktok: value })
|
||||
}
|
||||
/>
|
||||
<TextInputCustom
|
||||
label="Facebook"
|
||||
placeholder="Masukkan username facebook"
|
||||
onChangeText={(value: any) =>
|
||||
setDataMedsos({ ...dataMedsos, facebook: value })
|
||||
}
|
||||
/>
|
||||
<TextInputCustom
|
||||
label="Instagram"
|
||||
placeholder="Masukkan username instagram"
|
||||
onChangeText={(value: any) =>
|
||||
setDataMedsos({ ...dataMedsos, instagram: value })
|
||||
}
|
||||
/>
|
||||
<TextInputCustom
|
||||
label="Twitter"
|
||||
placeholder="Masukkan username twitter"
|
||||
onChangeText={(value: any) =>
|
||||
setDataMedsos({ ...dataMedsos, twitter: value })
|
||||
}
|
||||
/>
|
||||
<TextInputCustom
|
||||
label="Youtube"
|
||||
placeholder="Masukkan username youtube"
|
||||
onChangeText={(value: any) =>
|
||||
setDataMedsos({ ...dataMedsos, youtube: value })
|
||||
}
|
||||
/>
|
||||
{/* <Spacing /> */}
|
||||
</StackCustom>
|
||||
</NewWrapper>
|
||||
);
|
||||
return <ScreenPortofolioCreate />;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
NewWrapper,
|
||||
PhoneInputCustom,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
import { DEFAULT_COUNTRY, type CountryData, COUNTRIES } from "@/constants/countries";
|
||||
import {
|
||||
apiMasterBidangBisnis,
|
||||
apiMasterSubBidangBisnis,
|
||||
@@ -32,7 +34,6 @@ import { router, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import PhoneInput, { ICountry } from "react-native-international-phone-number";
|
||||
import { ActivityIndicator } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
@@ -59,8 +60,8 @@ export default function PortofolioEdit() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [data, setData] = useState<any>({});
|
||||
|
||||
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
|
||||
const [phoneNumber, setPhoneNumber] = useState<string>("");
|
||||
const [selectedCountry, setSelectedCountry] = useState<CountryData>(DEFAULT_COUNTRY);
|
||||
const [bidangBisnis, setBidangBisnis] = useState<
|
||||
IMasterBidangBisnis[] | null
|
||||
>(null);
|
||||
@@ -72,12 +73,42 @@ export default function PortofolioEdit() {
|
||||
IListSubBidangSelected[]
|
||||
>([]);
|
||||
|
||||
function handleInputValue(phoneNumber: string) {
|
||||
setData({ ...data, tlpn: phoneNumber });
|
||||
function handlePhoneChange(phone: string) {
|
||||
setPhoneNumber(phone);
|
||||
|
||||
// Format phone number for API
|
||||
const callingCode = selectedCountry.callingCode;
|
||||
let fixNumber = phone.replace(/\s+/g, "").replace(/^0+/, "");
|
||||
|
||||
// Remove country code if already present
|
||||
if (fixNumber.startsWith(callingCode)) {
|
||||
fixNumber = fixNumber.substring(callingCode.length);
|
||||
}
|
||||
|
||||
// Remove leading zero
|
||||
fixNumber = fixNumber.replace(/^0+/, "");
|
||||
|
||||
const realNumber = callingCode + fixNumber;
|
||||
setData({ ...data, tlpn: realNumber });
|
||||
}
|
||||
|
||||
function handleSelectedCountry(country: ICountry) {
|
||||
function handleCountryChange(country: CountryData) {
|
||||
setSelectedCountry(country);
|
||||
|
||||
// Re-format with new country code
|
||||
const callingCode = country.callingCode;
|
||||
let fixNumber = phoneNumber.replace(/\s+/g, "").replace(/^0+/, "");
|
||||
|
||||
// Remove country code if already present
|
||||
if (fixNumber.startsWith(callingCode)) {
|
||||
fixNumber = fixNumber.substring(callingCode.length);
|
||||
}
|
||||
|
||||
// Remove leading zero
|
||||
fixNumber = fixNumber.replace(/^0+/, "");
|
||||
|
||||
const realNumber = callingCode + fixNumber;
|
||||
setData({ ...data, tlpn: realNumber });
|
||||
}
|
||||
|
||||
const onLoadMasterBidang = async () => {
|
||||
@@ -122,8 +153,27 @@ export default function PortofolioEdit() {
|
||||
const response = await apiGetOnePortofolio({ id: id });
|
||||
|
||||
if (response.success) {
|
||||
const fixNumber = response.data.tlpn.replace("62", "");
|
||||
setData({ ...response.data, tlpn: fixNumber });
|
||||
// Extract phone number without country code for display
|
||||
const fullNumber = response.data.tlpn;
|
||||
let displayNumber = fullNumber;
|
||||
let detectedCountry = DEFAULT_COUNTRY;
|
||||
|
||||
// Try to detect country from calling code
|
||||
for (const country of COUNTRIES) {
|
||||
if (fullNumber.startsWith(country.callingCode)) {
|
||||
detectedCountry = country;
|
||||
displayNumber = fullNumber.substring(country.callingCode.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedCountry(detectedCountry);
|
||||
|
||||
// Remove leading zero if present
|
||||
displayNumber = displayNumber.replace(/^0+/, "");
|
||||
|
||||
setPhoneNumber(displayNumber);
|
||||
setData({ ...response.data, tlpn: displayNumber });
|
||||
|
||||
// Cek apakah ada sub bidang bisnis yang terpilih
|
||||
const prevSubBidang = response.data.Portofolio_BidangDanSubBidangBisnis;
|
||||
@@ -244,15 +294,11 @@ export default function PortofolioEdit() {
|
||||
}
|
||||
|
||||
const handleSubmitUpdate = async () => {
|
||||
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
|
||||
let fixNumber = data.tlpn.replace(/\s+/g, "").replace(/^0+/, "");
|
||||
const realNumber = callingCode + fixNumber;
|
||||
|
||||
const newData: IFormData = {
|
||||
id_Portofolio: data.id_Portofolio,
|
||||
namaBisnis: data.namaBisnis,
|
||||
alamatKantor: data.alamatKantor,
|
||||
tlpn: realNumber,
|
||||
tlpn: data.tlpn, // Already formatted by PhoneInputCustom
|
||||
deskripsi: data.deskripsi,
|
||||
masterBidangBisnisId: data.masterBidangBisnisId,
|
||||
subBidang: listSubBidangSelected,
|
||||
@@ -435,12 +481,11 @@ export default function PortofolioEdit() {
|
||||
<Text style={{ color: "red" }}> *</Text>
|
||||
</View>
|
||||
<Spacing height={5} />
|
||||
<PhoneInput
|
||||
value={data.tlpn}
|
||||
onChangePhoneNumber={handleInputValue}
|
||||
<PhoneInputCustom
|
||||
value={phoneNumber}
|
||||
onChangePhoneNumber={handlePhoneChange}
|
||||
selectedCountry={selectedCountry}
|
||||
onChangeSelectedCountry={handleSelectedCountry}
|
||||
defaultCountry="ID"
|
||||
onChangeCountry={handleCountryChange}
|
||||
placeholder="xxx-xxx-xxx"
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ButtonCustom,
|
||||
DrawerCustom,
|
||||
DummyLandscapeImage,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ButtonCustom,
|
||||
DrawerCustom,
|
||||
DummyLandscapeImage,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
@@ -64,41 +66,52 @@ export default function Portofolio() {
|
||||
setProfileId(response?.data?.Profile?.id);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Header */}
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Portofolio",
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
headerRight: () =>
|
||||
data?.Profile?.id !== profileId ? null : (
|
||||
<TouchableOpacity onPress={openDrawer}>
|
||||
<Ionicons
|
||||
name="ellipsis-vertical"
|
||||
size={20}
|
||||
color={MainColor.yellow}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerStyle: GStyles.headerStyle,
|
||||
headerTitleStyle: GStyles.headerTitleStyle,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Portofolio"
|
||||
left={<LeftButtonCustom />}
|
||||
right={
|
||||
data?.Profile?.id !== profileId ? null : (
|
||||
<TouchableOpacity onPress={openDrawer}>
|
||||
<Ionicons
|
||||
name="ellipsis-vertical"
|
||||
size={20}
|
||||
color={MainColor.yellow}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
{!data || !profileId ? (
|
||||
<LoaderCustom />
|
||||
<StackCustom>
|
||||
<CustomSkeleton height={400} />
|
||||
<CustomSkeleton height={300} />
|
||||
</StackCustom>
|
||||
) : (
|
||||
<StackCustom>
|
||||
<Portofolio_Data
|
||||
data={data}
|
||||
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
|
||||
/>
|
||||
<Portofolio_BusinessLocation
|
||||
data={data?.BusinessMaps}
|
||||
imageId={data?.logoId}
|
||||
setOpenDrawerLocation={setOpenDrawerLocation}
|
||||
/>
|
||||
{data?.BusinessMaps && (
|
||||
<Portofolio_BusinessLocation
|
||||
data={data?.BusinessMaps}
|
||||
imageId={data?.logoId}
|
||||
setOpenDrawerLocation={setOpenDrawerLocation}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Portofolio_SocialMediaSection
|
||||
data={data?.Portofolio_MediaSosial}
|
||||
/>
|
||||
@@ -135,10 +148,12 @@ export default function Portofolio() {
|
||||
closeDrawer={() => setOpenDrawerLocation(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<DummyLandscapeImage
|
||||
height={200}
|
||||
imageId={data?.BusinessMaps?.imageId}
|
||||
/>
|
||||
{data?.BusinessMaps?.imageId && (
|
||||
<DummyLandscapeImage
|
||||
height={200}
|
||||
imageId={data?.BusinessMaps?.imageId}
|
||||
/>
|
||||
)}
|
||||
<Spacing />
|
||||
<StackCustom gap={"xs"}>
|
||||
<GridTwoView
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import { HeaderStyles } from "@/styles/header-styles";
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function PortofolioLayout() {
|
||||
@@ -7,8 +7,9 @@ export default function PortofolioLayout() {
|
||||
<>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
...HeaderStyles,
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
header: () => (
|
||||
<AppHeader title="Portofolio" left={<LeftButtonCustom />} />
|
||||
),
|
||||
}}
|
||||
>
|
||||
{/* <Stack.Screen name="[id]/index" options={{ title: "Portofolio" }} /> */}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { LoaderCustom } from "@/components";
|
||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||
import { NewWrapper, StackCustom } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import LeftButtonCustom from "@/components/Button/BackButton";
|
||||
import DrawerCustom from "@/components/Drawer/DrawerCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -16,8 +17,8 @@ import { GStyles } from "@/styles/global-styles";
|
||||
import { IProfile } from "@/types/Type-Profile";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, TouchableOpacity } from "react-native";
|
||||
|
||||
export default function Profile() {
|
||||
const { id } = useLocalSearchParams();
|
||||
@@ -25,6 +26,7 @@ export default function Profile() {
|
||||
const [data, setData] = useState<IProfile>();
|
||||
const [dataToken, setDataToken] = useState<IProfile>();
|
||||
const [listPortofolio, setListPortofolio] = useState<any[]>();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const { token, logout, isAdmin, user, userData } = useAuth();
|
||||
|
||||
@@ -43,7 +45,7 @@ export default function Profile() {
|
||||
onLoadUserByToken();
|
||||
isUserCheck();
|
||||
userData(token as string);
|
||||
}, [id, token])
|
||||
}, [id, token]),
|
||||
);
|
||||
|
||||
const isUserCheck = () => {
|
||||
@@ -54,13 +56,21 @@ export default function Profile() {
|
||||
};
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
const response = await apiProfile({ id: id });
|
||||
setData(response.data);
|
||||
try {
|
||||
const response = await apiProfile({ id: id });
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR onLoadData]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadUserByToken = async () => {
|
||||
const response = await apiUser(user?.id as string);
|
||||
setDataToken(response?.data?.Profile);
|
||||
try {
|
||||
const response = await apiUser(user?.id as string);
|
||||
setDataToken(response?.data?.Profile);
|
||||
} catch (error) {
|
||||
console.log("[ERROR onLoadUserByToken]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadPortofolio = async (id: string) => {
|
||||
@@ -69,37 +79,61 @@ export default function Profile() {
|
||||
const lastTwoByDate = response.data
|
||||
.sort(
|
||||
(a: any, b: any) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
) // urut desc
|
||||
.slice(0, 2);
|
||||
setListPortofolio(lastTwoByDate);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
console.log("[ERROR onLoadPortofolio]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
onLoadData(id as string);
|
||||
onLoadPortofolio(id as string);
|
||||
onLoadUserByToken();
|
||||
isUserCheck();
|
||||
userData(token as string);
|
||||
setRefreshing(false);
|
||||
}, [id, token]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Profile`,
|
||||
headerLeft: () => <LeftButtonCustom />,
|
||||
headerRight: () => (
|
||||
<ButtonnDot
|
||||
id={id as string}
|
||||
openDrawer={openDrawer}
|
||||
isUserCheck={isUserCheck()}
|
||||
logout={logout}
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Profile"
|
||||
left={<LeftButtonCustom />}
|
||||
right={
|
||||
<ButtonnDot
|
||||
id={id as string}
|
||||
openDrawer={openDrawer}
|
||||
isUserCheck={isUserCheck()}
|
||||
logout={logout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
headerStyle: GStyles.headerStyle,
|
||||
headerTitleStyle: GStyles.headerTitleStyle,
|
||||
}}
|
||||
/>
|
||||
{/* Main View */}
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{!data || !dataToken ? (
|
||||
<LoaderCustom />
|
||||
<StackCustom>
|
||||
<CustomSkeleton height={400} />
|
||||
<CustomSkeleton height={200} />
|
||||
</StackCustom>
|
||||
) : (
|
||||
<>
|
||||
<ProfileSection data={data as any} />
|
||||
@@ -110,7 +144,7 @@ export default function Profile() {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
|
||||
{/* Drawer Komponen Eksternal */}
|
||||
<DrawerCustom
|
||||
|
||||
@@ -1,47 +1,39 @@
|
||||
import { BackButton } from "@/components";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function ProfileLayout() {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: GStyles.headerStyle,
|
||||
headerTitleStyle: GStyles.headerTitleStyle,
|
||||
headerTitleAlign: "center",
|
||||
headerBackButtonDisplayMode: "minimal",
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
{/* <Stack.Screen name="[id]/index" options={{ headerShown: false }} /> */}
|
||||
<Stack.Screen
|
||||
name="[id]/edit"
|
||||
options={{ title: "Edit Profile", headerLeft: () => <BackButton /> }}
|
||||
options={{ header: () => <AppHeader title="Edit Profile" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="[id]/update-photo"
|
||||
options={{ title: "Update Foto", headerLeft: () => <BackButton /> }}
|
||||
options={{ header: () => <AppHeader title="Update Foto" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="[id]/update-background"
|
||||
options={{
|
||||
title: "Update Latar Belakang",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Update Latar Belakang" />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="create"
|
||||
options={{ title: "Buat Profile", headerBackVisible: false }}
|
||||
options={{ headerBackVisible: false }}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="[id]/blocked-list"
|
||||
options={{ title: "Daftar Blokir", headerLeft: () => <BackButton /> }}
|
||||
options={{ header: () => <AppHeader title="Daftar Blokir" /> }}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="[id]/detail-blocked"
|
||||
options={{ title: "Detail Blokir", headerLeft: () => <BackButton /> }}
|
||||
options={{ header: () => <AppHeader title="Detail Blokir" /> }}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
|
||||
@@ -4,36 +4,34 @@ import {
|
||||
IconHome,
|
||||
IconStatus,
|
||||
} from "@/components/_Icon";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
import { Tabs, useLocalSearchParams, useNavigation, router } from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
import { router, Tabs, useLocalSearchParams } from "expo-router";
|
||||
|
||||
export default function VotingTabsLayout() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { from, category } = useLocalSearchParams<{
|
||||
from?: string;
|
||||
category?: string;
|
||||
}>();
|
||||
|
||||
console.log("from", from);
|
||||
console.log("category", category);
|
||||
|
||||
// Atur header secara dinamis
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<BackButtonFromNotification
|
||||
from={from as string}
|
||||
category={category as string}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
...TabsStyles,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Voting"
|
||||
left={
|
||||
<BackButtonFromNotification
|
||||
from={from as string}
|
||||
category={category as string}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
@@ -103,14 +104,19 @@ export default function VotingDetailStatus() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () =>
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null,
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
status === "draft" ? (
|
||||
<DotButton onPress={() => setOpenDrawerDraft(true)} />
|
||||
) : status === "publish" ? (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Spacing,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconContribution } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -81,10 +82,14 @@ export default function VotingDetailContribution() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Detail Kontribusi",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Kontribusi"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Spacing,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconContribution } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -82,10 +83,14 @@ export default function VotingDetailHistory() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Riwayat Voting",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Riwayat Voting"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
StackCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { IconArchive, IconContribution } from "@/components/_Icon";
|
||||
import { IMenuDrawerItem } from "@/components/_Interface/types";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
@@ -142,10 +143,14 @@ export default function VotingDetail() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: `Detail Voting`,
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Detail Voting"
|
||||
left={<BackButton />}
|
||||
right={
|
||||
<DotButton onPress={() => setOpenDrawerPublish(true)} />
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { BackButton } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import BackgroundNotificationHandler from "@/components/Notification/BackgroundNotificationHandler";
|
||||
import NotificationInitializer from "@/components/Notification/NotificationInitializer";
|
||||
import { NotificationProvider } from "@/hooks/use-notification-store";
|
||||
import { HeaderStyles } from "@/styles/header-styles";
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function ApplicationLayout() {
|
||||
@@ -20,7 +20,7 @@ export default function ApplicationLayout() {
|
||||
function ApplicationStack() {
|
||||
return (
|
||||
<>
|
||||
<Stack screenOptions={HeaderStyles}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(user)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="admin" options={{ headerShown: false }} />
|
||||
|
||||
@@ -28,8 +28,7 @@ function ApplicationStack() {
|
||||
<Stack.Screen
|
||||
name="(image)/take-picture/[id]/index"
|
||||
options={{
|
||||
title: "Ambil Gambar",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Ambil Gambar" />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -37,8 +36,7 @@ function ApplicationStack() {
|
||||
<Stack.Screen
|
||||
name="(image)/preview-image/[id]/index"
|
||||
options={{
|
||||
title: "Preview Gambar",
|
||||
headerLeft: () => <BackButton />,
|
||||
header: () => <AppHeader title="Preview Gambar" />,
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
|
||||
import NavbarMenu from "@/components/Drawer/NavbarMenu";
|
||||
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
ICON_SIZE_XLARGE,
|
||||
} from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import AdminNotificationBell from "@/screens/Admin/AdminNotificationBell";
|
||||
import {
|
||||
adminListMenu,
|
||||
@@ -34,12 +36,28 @@ import { useState } from "react";
|
||||
export default function AdminLayout() {
|
||||
const [openDrawerNavbar, setOpenDrawerNavbar] = useState(false);
|
||||
const [openDrawerUser, setOpenDrawerUser] = useState(false);
|
||||
// const [user, setUser] = useState(null);
|
||||
|
||||
const { logout, user } = useAuth();
|
||||
|
||||
console.log("[USER LAYOUT]", JSON.stringify(user, null, 2));
|
||||
|
||||
const headerLeft = () => (
|
||||
<Ionicons
|
||||
name="menu"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.white}
|
||||
onPress={() => setOpenDrawerNavbar(true)}
|
||||
/>
|
||||
);
|
||||
|
||||
const headerRight = () => (
|
||||
<FontAwesome6
|
||||
name="circle-user"
|
||||
size={ICON_SIZE_MEDIUM}
|
||||
color={MainColor.white}
|
||||
onPress={() => setOpenDrawerUser(true)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
@@ -51,20 +69,33 @@ export default function AdminLayout() {
|
||||
contentStyle: {
|
||||
borderBottomColor: AccentColor.blue,
|
||||
},
|
||||
headerLeft: () => (
|
||||
<Ionicons
|
||||
name="menu"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.white}
|
||||
onPress={() => setOpenDrawerNavbar(true)}
|
||||
/>
|
||||
),
|
||||
headerRight: () => (
|
||||
<FontAwesome6
|
||||
name="circle-user"
|
||||
size={ICON_SIZE_MEDIUM}
|
||||
color={MainColor.white}
|
||||
onPress={() => setOpenDrawerUser(true)}
|
||||
|
||||
// headerLeft: () => (
|
||||
// <Ionicons
|
||||
// name="menu"
|
||||
// size={ICON_SIZE_XLARGE}
|
||||
// color={MainColor.white}
|
||||
// onPress={() => setOpenDrawerNavbar(true)}
|
||||
// />
|
||||
// ),
|
||||
// headerRight: () => (
|
||||
// <FontAwesome6
|
||||
// name="circle-user"
|
||||
// size={ICON_SIZE_MEDIUM}
|
||||
// color={MainColor.white}
|
||||
// onPress={() => setOpenDrawerUser(true)}
|
||||
// />
|
||||
// ),
|
||||
|
||||
|
||||
|
||||
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="HIPMI DASHBOARD"
|
||||
showBack={false}
|
||||
left={headerLeft()}
|
||||
right={headerRight()}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -1,254 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
AlertDefaultSystem,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconDot, IconList } from "@/components/_Icon/IconComponent";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
|
||||
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
|
||||
import { DEEP_LINK_URL } from "@/service/api-config";
|
||||
import { colorBadgeStatus } from "@/utils/colorBadge";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import React, { useCallback } from "react";
|
||||
import QRCode from "react-native-qrcode-svg";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { Admin_ScreenEventDetail } from "@/screens/Admin/Event/ScreenEventDetail";
|
||||
|
||||
export default function AdminEventDetail() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
const [openDrawer, setOpenDrawer] = React.useState(false);
|
||||
|
||||
const [data, setData] = React.useState<any | null>(null);
|
||||
const [loadData, setLoadData] = React.useState(false);
|
||||
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
|
||||
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
|
||||
|
||||
const isDevLink =
|
||||
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminEventById({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const listData = [
|
||||
{
|
||||
label: "Pembuat Event",
|
||||
value: (data && data?.Author?.username) || "-",
|
||||
},
|
||||
{
|
||||
label: "Judul Event",
|
||||
value: (data && data?.title) || "-",
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
value:
|
||||
(data && (
|
||||
<BadgeCustom color={colorBadgeStatus({ status: status as string })}>
|
||||
{_.startCase(status as string)}
|
||||
</BadgeCustom>
|
||||
)) ||
|
||||
"-",
|
||||
},
|
||||
{
|
||||
label: "Lokasi",
|
||||
value: (data && data?.lokasi) || "-",
|
||||
},
|
||||
{
|
||||
label: "Tipe Acara",
|
||||
value: (data && data?.EventMaster_TipeAcara?.name) || "-",
|
||||
},
|
||||
{
|
||||
label: "Mulai Event",
|
||||
value:
|
||||
(data && data?.tanggal && dateTimeView({ date: data?.tanggal })) || "-",
|
||||
},
|
||||
{
|
||||
label: "Event Berakhir",
|
||||
value:
|
||||
(data &&
|
||||
data?.tanggalSelesai &&
|
||||
dateTimeView({ date: data?.tanggalSelesai })) ||
|
||||
"-",
|
||||
},
|
||||
{
|
||||
label: "Deskripsi",
|
||||
value: (data && data?.deskripsi) || "-",
|
||||
},
|
||||
];
|
||||
|
||||
const rightComponent = (
|
||||
<ActionIcon
|
||||
icon={<IconDot size={ICON_SIZE_BUTTON} />}
|
||||
onPress={() => {
|
||||
setOpenDrawer(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
try {
|
||||
const response = await funUpdateStatusEvent({
|
||||
id: id as string,
|
||||
changeStatus: "publish",
|
||||
data: { catatan: "", senderId: user?.id as string },
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mempublikasikan event",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Event berhasil dipublikasikan",
|
||||
});
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<AdminBackButtonAntTitle
|
||||
title={`Detail Data`}
|
||||
rightComponent={
|
||||
(status === "publish" || status === "history") && rightComponent
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
{listData.map((item, i) => (
|
||||
<GridSpan_4_8
|
||||
key={i}
|
||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
||||
value={<TextCustom>{item.value}</TextCustom>}
|
||||
/>
|
||||
))}
|
||||
</StackCustom>
|
||||
|
||||
<Spacing />
|
||||
</BaseBox>
|
||||
|
||||
{data &&
|
||||
data?.catatan &&
|
||||
(status === "reject" || status === "review") && (
|
||||
<ReportBox text={data?.catatan} />
|
||||
)}
|
||||
|
||||
{(status === "publish" || status === "history") && (
|
||||
<BaseBox>
|
||||
<StackCustom style={{ alignItems: "center" }}>
|
||||
<TextCustom bold>QR Code Event</TextCustom>
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : (
|
||||
<QRCode
|
||||
value={isDevLink}
|
||||
size={200}
|
||||
// logo={require("@/assets/images/logo-hipmi.png")}
|
||||
// logoSize={70}
|
||||
// logoBackgroundColor="transparent"
|
||||
// logoBorderRadius={50}
|
||||
// color="black"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* <TextCustom align="center">{isDevLink}</TextCustom> */}
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
)}
|
||||
|
||||
{status === "review" && (
|
||||
<AdminButtonReview
|
||||
onPublish={() => {
|
||||
AlertDefaultSystem({
|
||||
title: "Publish",
|
||||
message: "Apakah anda yakin ingin mempublikasikan data ini?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => handlerSubmit(),
|
||||
});
|
||||
}}
|
||||
onReject={() => {
|
||||
router.push(`/admin/event/${id}/reject-input?status=${status}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === "reject" && (
|
||||
<AdminButtonReject
|
||||
title="Tambah Catatan"
|
||||
onReject={() => {
|
||||
router.push(`/admin/event/${id}/reject-input?status=${status}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Daftar Peserta",
|
||||
icon: <IconList />,
|
||||
path: `/admin/event/${id}/list-of-participants`,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
setOpenDrawer(false);
|
||||
router.push(item.path as any);
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenEventDetail />;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function AdminForumDetailPosting() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -72,6 +72,10 @@ export default function AdminForumDetailPosting() {
|
||||
label: "Total Report",
|
||||
value: data?.JumlahReportPosting || 0,
|
||||
},
|
||||
{
|
||||
label: "Postingan",
|
||||
value: (data && data?.diskusi) || "-",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -111,13 +115,6 @@ export default function AdminForumDetailPosting() {
|
||||
))}
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<BaseBox>
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>Postingan</TextCustom>
|
||||
<TextCustom>{(data && data?.diskusi) || "-"}</TextCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</ViewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
|
||||
@@ -1,91 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import { IconOpenTo } from "@/components/_Icon/IconOpenTo";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import { apiAdminForumCommentById } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenForumListComment } from "@/screens/Admin/Forum/ScreenForumListComment";
|
||||
|
||||
export default function AdminForumListComment() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [listComment, setListComment] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadComment();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadComment = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
const response = await apiAdminForumCommentById({
|
||||
id: id as string,
|
||||
category: "get-all",
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListComment(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setListComment([]);
|
||||
} finally {
|
||||
setLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={<AdminBackButtonAntTitle title="Daftar Komentar" />}
|
||||
>
|
||||
<StackCustom>
|
||||
<AdminTitleTable title1="Aksi" title2="Report" title3="Komentar" />
|
||||
<Divider />
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listComment) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Tidak ada komentar
|
||||
</TextCustom>
|
||||
) : (
|
||||
listComment?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<IconOpenTo
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item.id}/list-report-comment`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.countReport || 0}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2}>{item?.komentar || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenForumListComment />;
|
||||
}
|
||||
|
||||
@@ -1,262 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
AlertDefaultSystem,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminForumCommentById,
|
||||
apiAdminForumDeactivateComment,
|
||||
apiAdminForumListReportCommentById,
|
||||
} from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { Admin_ScreenForumDetailReportComment } from "@/screens/Admin/Forum/ScreenForumDetailReportComment";
|
||||
|
||||
export default function AdminForumReportComment() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listReport, setListReport] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
||||
const [selectedReport, setSelectedReport] = useState({
|
||||
id: "",
|
||||
username: "",
|
||||
kategori: "",
|
||||
keterangan: "",
|
||||
deskripsi: "",
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
const response = await apiAdminForumCommentById({
|
||||
id: id as string,
|
||||
category: "get-one",
|
||||
});
|
||||
|
||||
const responseReport = await apiAdminForumListReportCommentById({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
if (responseReport.success) {
|
||||
setListReport(responseReport.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setData(null);
|
||||
setListReport([]);
|
||||
} finally {
|
||||
setLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<AdminBackButtonAntTitle
|
||||
title="Report Komentar"
|
||||
rightComponent={
|
||||
<ActionIcon
|
||||
icon={<IconDot size={16} color={MainColor.darkblue} />}
|
||||
onPress={() => setOpenDrawer(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<BaseBox>
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Username</TextCustom>}
|
||||
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Komentar</TextCustom>}
|
||||
text2={<TextCustom>{data?.komentar || "-"}</TextCustom>}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<AdminComp_BoxTitle title="Daftar Report Komentar" />
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
text2={<TextCustom bold>Pelapor</TextCustom>}
|
||||
text3={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
/>
|
||||
<Divider />
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listReport) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Tidak ada report
|
||||
</TextCustom>
|
||||
) : (
|
||||
listReport?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
||||
}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item.id,
|
||||
username: item.User?.username,
|
||||
kategori: item.ForumMaster_KategoriReport?.title,
|
||||
keterangan:
|
||||
item.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
icon: <IconTrash />,
|
||||
label: "Hapus Komentar",
|
||||
value: "delete",
|
||||
path: "",
|
||||
color: MainColor.red,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
AlertDefaultSystem({
|
||||
title: "Hapus Komentar",
|
||||
message: "Apakah Anda yakin ingin menghapus komentar ini?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Hapus",
|
||||
onPressRight: async () => {
|
||||
const deleteComment = await apiAdminForumDeactivateComment({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
// if (!deleteComment.success) {
|
||||
// Toast.show({
|
||||
// type: "error",
|
||||
// text1: "Komentar gagal dihapus",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
setOpenDrawer(false);
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Komentar berhasil dihapus",
|
||||
});
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawerAction}
|
||||
closeDrawer={() => setOpenDrawerAction(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<StackCustom>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Pelapor</TextCustom>}
|
||||
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
|
||||
{selectedReport?.kategori && (
|
||||
<>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Keterangan</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedReport?.deskripsi && (
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Deskripsi</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StackCustom>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenForumDetailReportComment />;
|
||||
}
|
||||
|
||||
@@ -1,283 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
AlertDefaultSystem,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminForumDeactivatePosting,
|
||||
apiAdminForumListReportPostingById,
|
||||
apiAdminForumPostingById,
|
||||
} from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { Admin_ScreenForumDetailReportPosting } from "@/screens/Admin/Forum/ScreenForumDetailReportPosting";
|
||||
|
||||
export default function AdminForumReportPosting() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawerPage, setOpenDrawerPage] = useState(false);
|
||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
||||
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listReport, setListReport] = useState<any[] | null>(null);
|
||||
const [loadListReport, setLoadListReport] = useState(false);
|
||||
const [selectedReport, setSelectedReport] = useState({
|
||||
id: "",
|
||||
username: "",
|
||||
kategori: "",
|
||||
keterangan: "",
|
||||
deskripsi: "",
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadListReport(true);
|
||||
const response = await apiAdminForumPostingById({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
const responseReport = await apiAdminForumListReportPostingById({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
|
||||
if (responseReport.success) {
|
||||
setListReport(responseReport.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadListReport(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<AdminBackButtonAntTitle
|
||||
title="Report Posting"
|
||||
rightComponent={
|
||||
<ActionIcon
|
||||
icon={<IconDot size={16} color={MainColor.darkblue} />}
|
||||
onPress={() => setOpenDrawerPage(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<BaseBox>
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Username</TextCustom>}
|
||||
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Status</TextCustom>}
|
||||
text2={
|
||||
data && data?.ForumMaster_StatusPosting?.status ? (
|
||||
<BadgeCustom
|
||||
color={
|
||||
data?.ForumMaster_StatusPosting?.status === "Open"
|
||||
? MainColor.green
|
||||
: MainColor.red
|
||||
}
|
||||
>
|
||||
{data?.ForumMaster_StatusPosting?.status === "Open"
|
||||
? "Open"
|
||||
: "Close"}
|
||||
</BadgeCustom>
|
||||
) : (
|
||||
<TextCustom>{"-"}</TextCustom>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold>Postingan</TextCustom>}
|
||||
text2={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
<AdminComp_BoxTitle title="Daftar Report Posting" />
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
text2={<TextCustom bold>Pelapor</TextCustom>}
|
||||
text3={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
/>
|
||||
<Divider />
|
||||
{loadListReport ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listReport) ? (
|
||||
<TextCustom align="center" color={"gray"}>
|
||||
Belum ada report
|
||||
</TextCustom>
|
||||
) : (
|
||||
listReport?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
||||
}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item?.id,
|
||||
username: item?.User?.username,
|
||||
kategori: item?.ForumMaster_KategoriReport?.title,
|
||||
keterangan:
|
||||
item?.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item?.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawerPage}
|
||||
closeDrawer={() => setOpenDrawerPage(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
icon: <IconTrash />,
|
||||
label: "Hapus Posting",
|
||||
value: "delete",
|
||||
path: "",
|
||||
color: MainColor.red,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
AlertDefaultSystem({
|
||||
title: "Hapus Posting",
|
||||
message: "Apakah Anda yakin ingin menghapus posting ini?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Hapus",
|
||||
onPressRight: async () => {
|
||||
const response = await apiAdminForumDeactivatePosting({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Posting gagal dihapus",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenDrawerPage(false);
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Posting berhasil dihapus",
|
||||
});
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawerAction}
|
||||
closeDrawer={() => setOpenDrawerAction(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<StackCustom>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Pelapor</TextCustom>}
|
||||
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
|
||||
{selectedReport?.kategori && (
|
||||
<>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Keterangan</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedReport?.deskripsi && (
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Deskripsi</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StackCustom>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
export default function AdminForumDetailReportPosting() {
|
||||
return <Admin_ScreenForumDetailReportPosting />;
|
||||
}
|
||||
|
||||
@@ -1,121 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ClickableCustom,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenForumPosting } from "@/screens/Admin/Forum/ScreenForumPosting";
|
||||
|
||||
export default function AdminForumPosting() {
|
||||
const [list, setList] = useState<any | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
handlerLoadList();
|
||||
}, [search])
|
||||
);
|
||||
|
||||
const handlerLoadList = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
const response = await apiAdminForum({
|
||||
category: "posting",
|
||||
search: search,
|
||||
});
|
||||
|
||||
console.log("DATA", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setList(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari postingan"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
|
||||
<AdminComp_BoxTitle title={"Posting"} rightComponent={rightComponent} />
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold truncate>Username</TextCustom>}
|
||||
text2={<TextCustom bold truncate> Postingan</TextCustom>}
|
||||
text3={<TextCustom bold align="center" truncate> Report Posting</TextCustom>}
|
||||
text4={<TextCustom bold align="center" truncate> Komentar</TextCustom>}
|
||||
/>
|
||||
<Divider />
|
||||
<Spacing />
|
||||
<StackCustom>
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(list) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
list?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(`/admin/forum/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate>
|
||||
{item?.diskusi || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom align="center" truncate={2}>
|
||||
{item?.reportPosting || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text4={
|
||||
<TextCustom align="center" truncate={2}>
|
||||
{item?.komentar || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
</ClickableCustom>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenForumPosting />;
|
||||
}
|
||||
|
||||
@@ -1,137 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconView } from "@/components/_Icon/IconComponent";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenForumReportComment } from "@/screens/Admin/Forum/ScreenForumReportComment";
|
||||
|
||||
export default function AdminForumReportComment() {
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState<boolean>(false);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
|
||||
const response = await apiAdminForum({
|
||||
category: "report_comment",
|
||||
search: search,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari Komentar"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title="Report Komentar"
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold truncate>
|
||||
Pelapor
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom bold truncate>
|
||||
Komentar
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom bold truncate>
|
||||
Jenis Laporan
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<Spacing />
|
||||
<StackCustom gap={"lg"}>
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.Forum_Komentar?.komentar || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.ForumMaster_KategoriReport?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</ClickableCustom>
|
||||
<Spacing />
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenForumReportComment />;
|
||||
}
|
||||
|
||||
@@ -1,124 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
Divider,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconView } from "@/components/_Icon/IconComponent";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Admin_ScreenForumReportPosting } from "@/screens/Admin/Forum/ScreenForumReportPosting";
|
||||
|
||||
export default function AdminForumReportPosting() {
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState<boolean>(false);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
|
||||
const response = await apiAdminForum({
|
||||
category: "report_posting",
|
||||
search: search,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari Postingan"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title="Report Posting"
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold truncate>
|
||||
Pelapor
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom bold truncate>
|
||||
Postingan
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<StackCustom>
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`
|
||||
);
|
||||
}}
|
||||
>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Forum_Posting?.diskusi || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</ClickableCustom>
|
||||
<Divider />
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenForumReportPosting />;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -182,9 +183,9 @@ export default function AdminInvestmentDetail() {
|
||||
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>File Prospektus</TextCustom>}
|
||||
value={
|
||||
<GridTwoView
|
||||
leftItem={<TextCustom bold>File Prospektus</TextCustom>}
|
||||
rightItem={
|
||||
<ButtonCustom
|
||||
iconLeft={
|
||||
<IconProspectus
|
||||
@@ -202,9 +203,10 @@ export default function AdminInvestmentDetail() {
|
||||
</ButtonCustom>
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>File Dokumen</TextCustom>}
|
||||
value={
|
||||
<GridTwoView
|
||||
|
||||
leftItem={<TextCustom bold>File Dokumen</TextCustom>}
|
||||
rightItem={
|
||||
<StackCustom>
|
||||
{_.isEmpty(data?.DokumenInvestasi) ? (
|
||||
<TextCustom align="center">-</TextCustom>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
value: (data && data?.MasterBank?.namaBank) || "-",
|
||||
},
|
||||
{
|
||||
label: "Jumlah Investasi",
|
||||
label: "Nominal",
|
||||
value: (data && `Rp. ${formatCurrencyDisplay(data?.nominal)}`) || "-",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,195 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
BadgeCustom,
|
||||
CenterCustom,
|
||||
LoaderCustom,
|
||||
SelectCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconView } from "@/components/_Icon/IconComponent";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminInvestmentListOfInvestor } from "@/service/api-admin/api-admin-investment";
|
||||
import { apiMasterTransaction } from "@/service/api-client/api-master";
|
||||
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenInvestmentListOfInvestor } from "@/screens/Admin/Investment/ScreenInvestmentListOfInvestor";
|
||||
|
||||
export default function AdminInvestmentListOfInvestor() {
|
||||
const { id } = useLocalSearchParams();
|
||||
console.log("[ID]", id);
|
||||
|
||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = React.useState(false);
|
||||
const [master, setMaster] = React.useState<any[]>([]);
|
||||
|
||||
const [selectValue, setSelectValue] = React.useState<string | null>(null);
|
||||
const [selectedStatus, setSelectedStatus] = React.useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onLoadMaster();
|
||||
}, []);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
try {
|
||||
const response = await apiMasterTransaction();
|
||||
|
||||
if (response.success) {
|
||||
setMaster(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setMaster([]);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id, selectValue])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminInvestmentListOfInvestor({
|
||||
id: id as string,
|
||||
status: selectedStatus as any,
|
||||
});
|
||||
console.log("[LIST OF INVESTOR]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setListData([]);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onLoadMaster();
|
||||
}, []);
|
||||
|
||||
const searchComponent = (
|
||||
<View style={{ flexDirection: "row", gap: 5 }}>
|
||||
<SelectCustom
|
||||
placeholder="Pilih status transaksi"
|
||||
data={
|
||||
_.isEmpty(master)
|
||||
? []
|
||||
: master?.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))
|
||||
}
|
||||
value={selectValue}
|
||||
onChange={(value: any) => {
|
||||
setSelectValue(value);
|
||||
const nameSelected = master.find((item: any) => item.id === value);
|
||||
const statusChooses = _.lowerCase(nameSelected?.name);
|
||||
setSelectedStatus(statusChooses);
|
||||
}}
|
||||
styleContainer={{ width: "100%", marginBottom: 0 }}
|
||||
allowClear
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const headerComponent = (
|
||||
<StackCustom gap={"xs"}>
|
||||
<AdminBackButtonAntTitle title="Daftar Investor" />
|
||||
{searchComponent}
|
||||
</StackCustom>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={headerComponent}>
|
||||
<StackCustom>
|
||||
<GridViewCustomSpan
|
||||
span1={3}
|
||||
span2={5}
|
||||
span3={4}
|
||||
component1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom bold align="center">
|
||||
Investor
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<TextCustom bold align="center">
|
||||
Status
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<StackCustom>
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText />
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<GridViewCustomSpan
|
||||
span1={3}
|
||||
span2={5}
|
||||
span3={4}
|
||||
component1={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/investment/${item?.id}/${_.lowerCase(
|
||||
item?.StatusInvoice?.name
|
||||
)}/transaction-detail`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom bold align="center" truncate>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<BadgeCustom
|
||||
style={{ alignSelf: "center" }}
|
||||
color={colorBadgeTransaction({
|
||||
status: item?.StatusInvoice?.name,
|
||||
})}
|
||||
>
|
||||
{item?.StatusInvoice?.name}
|
||||
</BadgeCustom>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenInvestmentListOfInvestor />;
|
||||
}
|
||||
|
||||
@@ -1,113 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminInvestment } from "@/service/api-admin/api-admin-investment";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import React, { useCallback } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenInvestmentStatus } from "@/screens/Admin/Investment/ScreenInvestmentStatus";
|
||||
|
||||
export default function AdminInvestmentStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
||||
const [loadData, setLoadingData] = React.useState(false);
|
||||
const [search, setSearch] = React.useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [status, search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadingData(true);
|
||||
const response = await apiAdminInvestment({
|
||||
category: status as "publish" | "review" | "reject",
|
||||
search,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setListData([]);
|
||||
} finally {
|
||||
setLoadingData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Investasi" />}>
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminComp_BoxTitle
|
||||
title={`${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Username"
|
||||
title3="Judul Investasi"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText />
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/investment/${item.id}/${status}`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={<TextCustom truncate={1}>{item?.author?.username}</TextCustom>}
|
||||
value3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.title}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenInvestmentStatus />;
|
||||
}
|
||||
|
||||
@@ -9,32 +9,14 @@ import {
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import API_IMAGE from "@/constants/api-storage";
|
||||
import { MapMarker, MapsV2Custom } from "@/components/Map/MapsV2Custom";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { apiMapsGetAll } from "@/service/api-client/api-maps";
|
||||
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
|
||||
import { FontAwesome, Ionicons } from "@expo/vector-icons";
|
||||
import { Image } from "expo-image";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import MapView, { Marker } from "react-native-maps";
|
||||
|
||||
const defaultRegion = {
|
||||
latitude: -8.737109,
|
||||
longitude: 115.1756897,
|
||||
latitudeDelta: 0.1,
|
||||
longitudeDelta: 0.1,
|
||||
height: 300,
|
||||
};
|
||||
|
||||
export interface LocationItem {
|
||||
id: string | number;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
name: string;
|
||||
imageId?: string;
|
||||
}
|
||||
export default function AdminMaps() {
|
||||
const [list, setList] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
@@ -72,74 +54,30 @@ export default function AdminMaps() {
|
||||
}
|
||||
};
|
||||
|
||||
const markers: MapMarker[] = list?.map((item) => ({
|
||||
id: item.id,
|
||||
coordinate: [item.longitude, item.latitude] as [number, number],
|
||||
imageId: item.Portofolio?.logoId,
|
||||
onSelected: () => {
|
||||
setOpenDrawer(true);
|
||||
setSelected({
|
||||
id: item?.id,
|
||||
bidangBisnis: item?.Portofolio?.MasterBidangBisnis?.name,
|
||||
nomorTelepon: item?.Portofolio?.tlpn,
|
||||
alamatBisnis: item?.Portofolio?.alamatKantor,
|
||||
namePin: item?.namePin,
|
||||
imageId: item?.imageId,
|
||||
portofolioId: item?.Portofolio?.id,
|
||||
latitude: item?.latitude,
|
||||
longitude: item?.longitude,
|
||||
});
|
||||
},
|
||||
})) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
|
||||
{/* <MapCustom height={"100%"} /> */}
|
||||
<View style={{ flex: 1 }}>
|
||||
{loadList ? (
|
||||
<MapView
|
||||
initialRegion={defaultRegion}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MapView
|
||||
initialRegion={defaultRegion}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{list?.map((item: any, index: number) => {
|
||||
return (
|
||||
<Marker
|
||||
key={item?.id}
|
||||
coordinate={{
|
||||
latitude: item?.latitude,
|
||||
longitude: item?.longitude,
|
||||
}}
|
||||
title={item?.namePin}
|
||||
onPress={() => {
|
||||
setOpenDrawer(true);
|
||||
setSelected({
|
||||
id: item?.id,
|
||||
bidangBisnis:
|
||||
item?.Portofolio?.MasterBidangBisnis?.name,
|
||||
nomorTelepon: item?.Portofolio?.tlpn,
|
||||
alamatBisnis: item?.Portofolio?.alamatKantor,
|
||||
namePin: item?.namePin,
|
||||
imageId: item?.imageId,
|
||||
portofolioId: item?.Portofolio?.id,
|
||||
latitude: item?.latitude,
|
||||
longitude: item?.longitude,
|
||||
});
|
||||
}}
|
||||
// Gunakan gambar kustom jika tersedia
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
source={{
|
||||
uri: API_IMAGE.GET({
|
||||
fileId: item?.Portofolio?.logoId,
|
||||
}),
|
||||
}}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 100,
|
||||
borderWidth: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
</MapView>
|
||||
)}
|
||||
</View>
|
||||
<MapsV2Custom markers={markers} />
|
||||
</ViewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
@@ -147,7 +85,9 @@ export default function AdminMaps() {
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
||||
{selected.imageId && (
|
||||
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
||||
)}
|
||||
<Spacing />
|
||||
<StackCustom gap={"xs"}>
|
||||
<GridTwoView
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import AppHeader from "@/components/_ShareComponent/AppHeader";
|
||||
import { router, Stack } from "expo-router";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
@@ -15,7 +16,7 @@ export default function NotFoundScreen() {
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
|
||||
options={{ header: () => <AppHeader title="" left={<BackButton onPress={() => handleBack()} />} /> }}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
<StackCustom
|
||||
|
||||
45
bun.lock
45
bun.lock
@@ -5,6 +5,7 @@
|
||||
"name": "hipmi-mobile",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@maplibre/maplibre-react-native": "^10.4.2",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/datetimepicker": "8.4.4",
|
||||
"@react-native-firebase/app": "^23.7.0",
|
||||
@@ -14,7 +15,6 @@
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@react-navigation/native-stack": "^7.3.10",
|
||||
"@rnmapbox/maps": "^10.2.7",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"axios": "^1.11.0",
|
||||
@@ -41,6 +41,7 @@
|
||||
"expo-symbols": "~1.0.7",
|
||||
"expo-system-ui": "~6.0.7",
|
||||
"expo-web-browser": "~15.0.9",
|
||||
"libphonenumber-js": "^1.12.40",
|
||||
"lodash": "^4.17.21",
|
||||
"moti": "^0.30.0",
|
||||
"react": "19.1.0",
|
||||
@@ -580,6 +581,8 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@maplibre/maplibre-react-native": ["@maplibre/maplibre-react-native@10.4.2", "", { "dependencies": { "@turf/distance": "^7.1.0", "@turf/helpers": "^7.1.0", "@turf/length": "^7.1.0", "@turf/nearest-point-on-line": "^7.1.0", "debounce": "^2.2.0" }, "peerDependencies": { "@expo/config-plugins": ">=7", "@types/geojson": "^7946.0.0", "@types/react": ">=16.6.1", "react": ">=16.6.1", "react-native": ">=0.59.9" }, "optionalPeers": ["@expo/config-plugins", "@types/geojson", "@types/react"] }, "sha512-5qAfaEe66eMXyILklm2DMHwyaXwXxsZWVop4BqfU7AyTg13LHAcaMmLJNJ3jPkMtiJvjH2m8ywGnobdIg2I0lg=="],
|
||||
|
||||
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
|
||||
|
||||
"@motionone/dom": ["@motionone/dom@10.12.0", "", { "dependencies": { "@motionone/animation": "^10.12.0", "@motionone/generators": "^10.12.0", "@motionone/types": "^10.12.0", "@motionone/utils": "^10.12.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw=="],
|
||||
@@ -742,8 +745,6 @@
|
||||
|
||||
"@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="],
|
||||
|
||||
"@rnmapbox/maps": ["@rnmapbox/maps@10.2.10", "", { "dependencies": { "@turf/along": "6.5.0", "@turf/distance": "6.5.0", "@turf/helpers": "6.5.0", "@turf/length": "6.5.0", "@turf/nearest-point-on-line": "6.5.0", "@types/geojson": "^7946.0.7", "debounce": "^2.2.0" }, "peerDependencies": { "expo": ">=47.0.0", "mapbox-gl": "^2.9.0", "react": ">=17.0.0", "react-dom": ">= 17.0.0", "react-native": ">=0.69" }, "optionalPeers": ["expo", "mapbox-gl", "react-dom"] }, "sha512-OfjW0rHp5bUWfzBo5fZ7qdKwAzGoocXYTsSssSPVMxZ2Y7axuhcbmsO5bV6gg+BJs5RwEsghzwTIoGydBNUClA=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="],
|
||||
@@ -764,29 +765,17 @@
|
||||
|
||||
"@tsconfig/node18": ["@tsconfig/node18@18.2.6", "", {}, "sha512-eAWQzAjPj18tKnDzmWstz4OyWewLUNBm9tdoN9LayzoboRktYx3Enk1ZXPmThj55L7c4VWYq/Bzq0A51znZfhw=="],
|
||||
|
||||
"@turf/along": ["@turf/along@6.5.0", "", { "dependencies": { "@turf/bearing": "^6.5.0", "@turf/destination": "^6.5.0", "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-LLyWQ0AARqJCmMcIEAXF4GEu8usmd4Kbz3qk1Oy5HoRNpZX47+i5exQtmIWKdqJ1MMhW26fCTXgpsEs5zgJ5gw=="],
|
||||
"@turf/distance": ["@turf/distance@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@turf/invariant": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-9drWgd46uHPPyzgrcRQLgSvdS/SjVlQ6ZIBoRQagS5P2kSjUbcOXHIMeOSPwfxwlKhEtobLyr+IiR2ns1TfF8w=="],
|
||||
|
||||
"@turf/bbox": ["@turf/bbox@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-D5ErVWtfQbEPh11yzI69uxqrcJmbPU/9Y59f1uTapgwAwQHQztDWgsYpnL3ns8r1GmPWLP8sGJLVTIk2TZSiYA=="],
|
||||
"@turf/helpers": ["@turf/helpers@7.3.4", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g=="],
|
||||
|
||||
"@turf/bearing": ["@turf/bearing@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-dxINYhIEMzgDOztyMZc20I7ssYVNEpSv04VbMo5YPQsqa80KO3TFvbuCahMsCAW5z8Tncc8dwBlEFrmRjJG33A=="],
|
||||
"@turf/invariant": ["@turf/invariant@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ=="],
|
||||
|
||||
"@turf/destination": ["@turf/destination@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-4cnWQlNC8d1tItOz9B4pmJdWpXqS0vEvv65bI/Pj/genJnsL7evI0/Xw42RvEGROS481MPiU80xzvwxEvhQiMQ=="],
|
||||
"@turf/length": ["@turf/length@7.3.4", "", { "dependencies": { "@turf/distance": "7.3.4", "@turf/helpers": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-Dg1GnQ/B2go5NIWXt91N4L7XTjIgIWCftBSYIXkrpIM7QGjItzglek0Z5caytvb8ZRWXzZOGs8//+Q5we91WuQ=="],
|
||||
|
||||
"@turf/distance": ["@turf/distance@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0" } }, "sha512-xzykSLfoURec5qvQJcfifw/1mJa+5UwByZZ5TZ8iaqjGYN0vomhV9aiSLeYdUGtYRESZ+DYC/OzY+4RclZYgMg=="],
|
||||
"@turf/meta": ["@turf/meta@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw=="],
|
||||
|
||||
"@turf/helpers": ["@turf/helpers@6.5.0", "", {}, "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw=="],
|
||||
|
||||
"@turf/invariant": ["@turf/invariant@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg=="],
|
||||
|
||||
"@turf/length": ["@turf/length@6.5.0", "", { "dependencies": { "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-5pL5/pnw52fck3oRsHDcSGrj9HibvtlrZ0QNy2OcW8qBFDNgZ4jtl6U7eATVoyWPKBHszW3dWETW+iLV7UARig=="],
|
||||
|
||||
"@turf/line-intersect": ["@turf/line-intersect@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/line-segment": "^6.5.0", "@turf/meta": "^6.5.0", "geojson-rbush": "3.x" } }, "sha512-CS6R1tZvVQD390G9Ea4pmpM6mJGPWoL82jD46y0q1KSor9s6HupMIo1kY4Ny+AEYQl9jd21V3Scz20eldpbTVA=="],
|
||||
|
||||
"@turf/line-segment": ["@turf/line-segment@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-jI625Ho4jSuJESNq66Mmi290ZJ5pPZiQZruPVpmHkUw257Pew0alMmb6YrqYNnLUuiVVONxAAKXUVeeUGtycfw=="],
|
||||
|
||||
"@turf/meta": ["@turf/meta@6.5.0", "", { "dependencies": { "@turf/helpers": "^6.5.0" } }, "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA=="],
|
||||
|
||||
"@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@6.5.0", "", { "dependencies": { "@turf/bearing": "^6.5.0", "@turf/destination": "^6.5.0", "@turf/distance": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/invariant": "^6.5.0", "@turf/line-intersect": "^6.5.0", "@turf/meta": "^6.5.0" } }, "sha512-WthrvddddvmymnC+Vf7BrkHGbDOUu6Z3/6bFYUGv1kxw8tiZ6n83/VG6kHz4poHOfS0RaNflzXSkmCi64fLBlg=="],
|
||||
"@turf/nearest-point-on-line": ["@turf/nearest-point-on-line@7.3.4", "", { "dependencies": { "@turf/distance": "7.3.4", "@turf/helpers": "7.3.4", "@turf/invariant": "7.3.4", "@turf/meta": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-DQrP3lRju83rIXFN68tUEpc7ki/eRwdwBkK2CTT4RAcyCxbcH2NGJPQv8dYiww/Ar77u1WLVn+aINXZH904dWw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
@@ -1482,8 +1471,6 @@
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"geojson-rbush": ["geojson-rbush@3.2.0", "", { "dependencies": { "@turf/bbox": "*", "@turf/helpers": "6.x", "@turf/meta": "6.x", "@types/geojson": "7946.0.8", "rbush": "^3.0.1" } }, "sha512-oVltQTXolxvsz1sZnutlSuLDEcQAKYC/uXt9zDzJJ6bu0W+baTI8LZBaTup5afzibEH4N3jlq2p+a152wlBJ7w=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
@@ -1786,6 +1773,8 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"libphonenumber-js": ["libphonenumber-js@1.12.40", "", {}, "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg=="],
|
||||
|
||||
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
|
||||
@@ -2082,14 +2071,10 @@
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"quickselect": ["quickselect@2.0.0", "", {}, "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="],
|
||||
|
||||
"rbush": ["rbush@3.0.1", "", { "dependencies": { "quickselect": "^2.0.0" } }, "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w=="],
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
@@ -2694,10 +2679,6 @@
|
||||
|
||||
"@testing-library/react-native/pretty-format": ["pretty-format@30.2.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA=="],
|
||||
|
||||
"@turf/bbox/@turf/helpers": ["@turf/helpers@7.3.4", "", { "dependencies": { "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g=="],
|
||||
|
||||
"@turf/bbox/@turf/meta": ["@turf/meta@7.3.4", "", { "dependencies": { "@turf/helpers": "7.3.4", "@types/geojson": "^7946.0.10", "tslib": "^2.8.1" } }, "sha512-tlmw9/Hs1p2n0uoHVm1w3ugw1I6L8jv9YZrcdQa4SH5FX5UY0ATrKeIvfA55FlL//PGuYppJp+eyg/0eb4goqw=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
@@ -2790,8 +2771,6 @@
|
||||
|
||||
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"geojson-rbush/@types/geojson": ["@types/geojson@7946.0.8", "", {}, "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
@@ -12,14 +12,15 @@ export default function BackButtonFromNotification({
|
||||
return (
|
||||
<>
|
||||
<BackButton
|
||||
|
||||
onPress={() => {
|
||||
if (from === "notifications") {
|
||||
router.replace(`/notifications?category=${category}`);
|
||||
router.push(`/notifications?category=${category}`);
|
||||
} else {
|
||||
if (from) {
|
||||
router.replace(`/${from}` as any);
|
||||
router.back();
|
||||
} else {
|
||||
router.navigate("/home");
|
||||
router.back();
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
110
components/Map/MapSelectedPlatform.tsx
Normal file
110
components/Map/MapSelectedPlatform.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Platform } from "react-native";
|
||||
import MapSelected from "./MapSelected";
|
||||
import MapSelectedV2 from "./MapSelectedV2";
|
||||
import { LatLng } from "react-native-maps";
|
||||
|
||||
/**
|
||||
* Props untuk komponen MapSelectedPlatform
|
||||
* Mendukung kedua format koordinat (LatLng untuk iOS, [number, number] untuk Android)
|
||||
*/
|
||||
export interface MapSelectedPlatformProps {
|
||||
/** Region awal kamera */
|
||||
initialRegion?: {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
latitudeDelta?: number;
|
||||
longitudeDelta?: number;
|
||||
};
|
||||
|
||||
/** Lokasi yang dipilih (support kedua format) */
|
||||
selectedLocation: LatLng | [number, number] | null;
|
||||
|
||||
/** Callback ketika lokasi dipilih */
|
||||
onLocationSelect: (location: LatLng | [number, number]) => void;
|
||||
|
||||
/** Tinggi peta dalam pixels (default: 400) */
|
||||
height?: number;
|
||||
|
||||
/** Tampilkan lokasi user (default: true) */
|
||||
showUserLocation?: boolean;
|
||||
|
||||
/** Tampilkan tombol my location (default: true) */
|
||||
showsMyLocationButton?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponen Map yang otomatis memilih implementasi berdasarkan platform
|
||||
*
|
||||
* Platform Strategy:
|
||||
* - **iOS**: Menggunakan react-native-maps (MapSelected)
|
||||
* - **Android**: Menggunakan @maplibre/maplibre-react-native (MapSelectedV2)
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <MapSelectedPlatform
|
||||
* selectedLocation={selectedLocation}
|
||||
* onLocationSelect={setSelectedLocation}
|
||||
* height={300}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function MapSelectedPlatform({
|
||||
initialRegion,
|
||||
selectedLocation,
|
||||
onLocationSelect,
|
||||
height = 400,
|
||||
showUserLocation = true,
|
||||
showsMyLocationButton = true,
|
||||
}: MapSelectedPlatformProps) {
|
||||
// iOS: Gunakan react-native-maps
|
||||
// if (Platform.OS === "ios") {
|
||||
// return (
|
||||
// <MapSelected
|
||||
// initialRegion={initialRegion}
|
||||
// selectedLocation={(selectedLocation as LatLng) || { latitude: 0, longitude: 0 }}
|
||||
// setSelectedLocation={(location: LatLng) => {
|
||||
// onLocationSelect(location);
|
||||
// }}
|
||||
// height={height}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
// Android: Gunakan MapLibre
|
||||
// Konversi dari LatLng ke [longitude, latitude] jika perlu
|
||||
const androidLocation: [number, number] | undefined = selectedLocation
|
||||
? isLatLng(selectedLocation)
|
||||
? [selectedLocation.longitude, selectedLocation.latitude]
|
||||
: selectedLocation
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<MapSelectedV2
|
||||
selectedLocation={androidLocation}
|
||||
onLocationSelect={(location: [number, number]) => {
|
||||
// Konversi dari [longitude, latitude] ke LatLng untuk konsistensi
|
||||
const latLng: LatLng = {
|
||||
latitude: location[1],
|
||||
longitude: location[0],
|
||||
};
|
||||
onLocationSelect(latLng);
|
||||
}}
|
||||
height={height}
|
||||
// showUserLocation={showUserLocation}
|
||||
// showsMyLocationButton={showsMyLocationButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard untuk mengecek apakah object adalah LatLng
|
||||
*/
|
||||
function isLatLng(location: any): location is LatLng {
|
||||
return (
|
||||
location &&
|
||||
typeof location.latitude === "number" &&
|
||||
typeof location.longitude === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export default MapSelectedPlatform;
|
||||
150
components/Map/MapSelectedV2.tsx
Normal file
150
components/Map/MapSelectedV2.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useCallback, useRef, useEffect, useState } from "react";
|
||||
import { StyleSheet, View, ActivityIndicator } from "react-native";
|
||||
import {
|
||||
MapView,
|
||||
Camera,
|
||||
PointAnnotation,
|
||||
} from "@maplibre/maplibre-react-native";
|
||||
import * as Location from "expo-location";
|
||||
|
||||
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||
const DEBOUNCE_MS = 800;
|
||||
|
||||
interface Props {
|
||||
selectedLocation?: [number, number];
|
||||
onLocationSelect?: (location: [number, number]) => void;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function MapSelectedV2({
|
||||
selectedLocation,
|
||||
onLocationSelect,
|
||||
height = 400,
|
||||
}: Props) {
|
||||
const lastTapRef = useRef<number>(0);
|
||||
const cameraRef = useRef<any>(null);
|
||||
|
||||
const [userLocation, setUserLocation] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoadingLocation, setIsLoadingLocation] = useState(true);
|
||||
|
||||
// ✅ Ambil lokasi user saat pertama mount
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== "granted") {
|
||||
console.log("Permission lokasi ditolak");
|
||||
setIsLoadingLocation(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const location = await Location.getCurrentPositionAsync({
|
||||
accuracy: Location.Accuracy.Balanced,
|
||||
});
|
||||
|
||||
const coords: [number, number] = [
|
||||
location.coords.longitude,
|
||||
location.coords.latitude,
|
||||
];
|
||||
|
||||
setUserLocation(coords);
|
||||
|
||||
// ✅ Fly ke posisi user jika belum ada selectedLocation
|
||||
if (!selectedLocation && cameraRef.current) {
|
||||
cameraRef.current.flyTo(coords, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Gagal ambil lokasi:", error);
|
||||
} finally {
|
||||
setIsLoadingLocation(false);
|
||||
}
|
||||
})();
|
||||
}, [isLoadingLocation]);
|
||||
|
||||
const handleMapPress = useCallback(
|
||||
(event: any) => {
|
||||
const now = Date.now();
|
||||
if (now - lastTapRef.current < DEBOUNCE_MS) return;
|
||||
lastTapRef.current = now;
|
||||
|
||||
const coords = event?.geometry?.coordinates;
|
||||
if (!coords || coords.length < 2) return;
|
||||
|
||||
onLocationSelect?.([coords[0], coords[1]]);
|
||||
},
|
||||
[onLocationSelect],
|
||||
);
|
||||
|
||||
// Center awal kamera:
|
||||
// 1. Jika ada selectedLocation → pakai itu
|
||||
// 2. Jika ada userLocation → pakai itu
|
||||
// 3. Fallback → Bali
|
||||
const initialCenter: [number, number] = selectedLocation ??
|
||||
userLocation ?? [115.1756897, -8.737109];
|
||||
|
||||
return (
|
||||
<View style={{ height, width: "100%" }}>
|
||||
{/* Loading indicator saat fetch lokasi */}
|
||||
{isLoadingLocation && (
|
||||
<View style={styles.loadingOverlay}>
|
||||
<ActivityIndicator size="small" color="#0a1f44" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<MapView
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
mapStyle={MAP_STYLE}
|
||||
onPress={handleMapPress}
|
||||
logoEnabled={false}
|
||||
>
|
||||
<Camera
|
||||
ref={cameraRef}
|
||||
defaultSettings={{
|
||||
centerCoordinate: initialCenter,
|
||||
zoomLevel: 14,
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedLocation && (
|
||||
<PointAnnotation
|
||||
id="selected-location"
|
||||
key="selected-location"
|
||||
coordinate={selectedLocation}
|
||||
>
|
||||
<View style={styles.dot} />
|
||||
</PointAnnotation>
|
||||
)}
|
||||
</MapView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dot: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "#0a1f44",
|
||||
borderWidth: 2,
|
||||
borderColor: "#fff",
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
zIndex: 10,
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
elevation: 4,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default MapSelectedV2;
|
||||
542
components/Map/MapsV2Custom.tsx
Normal file
542
components/Map/MapsV2Custom.tsx
Normal file
@@ -0,0 +1,542 @@
|
||||
import { ReactNode, useCallback, useMemo, useState, useEffect } from "react";
|
||||
import {
|
||||
Image,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
StyleProp,
|
||||
Animated,
|
||||
Easing,
|
||||
} from "react-native";
|
||||
|
||||
import API_IMAGE from "@/constants/api-storage";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
Camera,
|
||||
MapView,
|
||||
PointAnnotation,
|
||||
} from "@maplibre/maplibre-react-native";
|
||||
|
||||
// Style peta default
|
||||
const DEFAULT_MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||
|
||||
// Region default (Bali, Indonesia)
|
||||
const DEFAULT_REGION = {
|
||||
latitude: -8.737109,
|
||||
longitude: 115.1756897,
|
||||
latitudeDelta: 0.1,
|
||||
longitudeDelta: 0.1,
|
||||
};
|
||||
|
||||
// Zoom level default
|
||||
const DEFAULT_ZOOM_LEVEL = 12;
|
||||
|
||||
// Ukuran marker default
|
||||
const DEFAULT_MARKER_SIZE = 30;
|
||||
|
||||
/**
|
||||
* Interface data marker untuk MapsV2Custom
|
||||
*/
|
||||
export interface MapMarker {
|
||||
id: string;
|
||||
coordinate: [number, number]; // [longitude, latitude]
|
||||
imageId?: string;
|
||||
imageUrl?: string;
|
||||
onSelected?: () => void;
|
||||
[key: string]: any; // Izinkan properti custom tambahan
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface region untuk positioning kamera
|
||||
*/
|
||||
export interface Region {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
latitudeDelta: number;
|
||||
longitudeDelta: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props untuk komponen MapsV2Custom
|
||||
*/
|
||||
export interface MapsV2CustomProps {
|
||||
/** URL style peta custom (default: liberty style) */
|
||||
mapStyle?: string;
|
||||
|
||||
/** Override style container */
|
||||
style?: StyleProp<ViewStyle>;
|
||||
|
||||
/** Override style MapView */
|
||||
mapViewStyle?: StyleProp<ViewStyle>;
|
||||
|
||||
/** Region awal kamera */
|
||||
initialRegion?: Region;
|
||||
|
||||
/** Zoom level awal (default: 12) */
|
||||
zoomLevel?: number;
|
||||
|
||||
/**
|
||||
* Data marker - mendukung single marker atau array of markers
|
||||
* @example
|
||||
* // Single marker
|
||||
* markers={{ id: "1", coordinate: [115.175, -8.737], imageId: "abc" }}
|
||||
*
|
||||
* @example
|
||||
* // Multiple markers
|
||||
* markers={[
|
||||
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
|
||||
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
|
||||
* ]}
|
||||
*/
|
||||
markers?: MapMarker | MapMarker[];
|
||||
|
||||
/** Custom renderer marker */
|
||||
renderMarker?: (marker: MapMarker) => ReactNode;
|
||||
|
||||
/** Callback ketika marker ditekan */
|
||||
onMarkerPress?: (marker: MapMarker) => void;
|
||||
|
||||
/** Gunakan style marker image default (default: true jika markers disediakan) */
|
||||
showDefaultMarkers?: boolean;
|
||||
|
||||
/** Ukuran untuk marker default (default: 30) */
|
||||
markerSize?: number;
|
||||
|
||||
/** Warna border untuk marker default */
|
||||
markerBorderColor?: string;
|
||||
|
||||
/** Children tambahan untuk MapView (custom overlays, dll.) */
|
||||
children?: ReactNode;
|
||||
|
||||
/** Handler untuk tekan pada peta */
|
||||
onMapPress?: (coordinates: [number, number]) => void;
|
||||
|
||||
/** Test identifier */
|
||||
testID?: string;
|
||||
|
||||
/** Props tambahan untuk Camera */
|
||||
cameraProps?: Partial<Omit<React.ComponentProps<typeof Camera>, "centerCoordinate" | "zoomLevel">>;
|
||||
|
||||
/** Props tambahan untuk MapView */
|
||||
mapViewProps?: Partial<React.ComponentProps<typeof MapView>>;
|
||||
|
||||
/** Props tambahan untuk PointAnnotation */
|
||||
annotationProps?: Partial<{
|
||||
id: string;
|
||||
title?: string;
|
||||
snippet?: string;
|
||||
selected?: boolean;
|
||||
draggable?: boolean;
|
||||
coordinate: number[];
|
||||
anchor?: { x: number; y: number };
|
||||
onSelected?: (payload: any) => void;
|
||||
onDeselected?: (payload: any) => void;
|
||||
onDragStart?: (payload: any) => void;
|
||||
onDragEnd?: (payload: any) => void;
|
||||
onDrag?: (payload: any) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisasi markers ke array - mendukung single marker atau array
|
||||
*/
|
||||
function normalizeMarkers(markers: MapMarker | MapMarker[] | undefined): MapMarker[] {
|
||||
if (!markers) return [];
|
||||
return Array.isArray(markers) ? markers : [markers];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validasi marker memiliki props yang required (hanya development mode)
|
||||
*/
|
||||
function validateMarker(marker: MapMarker, index: number): boolean {
|
||||
if (!marker.id) {
|
||||
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'id' yang required`);
|
||||
return false;
|
||||
}
|
||||
if (!marker.coordinate || !Array.isArray(marker.coordinate) || marker.coordinate.length !== 2) {
|
||||
console.warn(`[MapsV2Custom] Marker pada index ${index} tidak memiliki prop 'coordinate' yang required. Format yang diharapkan: [longitude, latitude]`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponen skeleton untuk loading state dengan shimmer animation
|
||||
*/
|
||||
function SkeletonMarker({
|
||||
size = DEFAULT_MARKER_SIZE,
|
||||
borderColor = MainColor.darkblue,
|
||||
loadingColor = "#C5C5C5",
|
||||
}: {
|
||||
size?: number;
|
||||
borderColor?: string;
|
||||
loadingColor?: string;
|
||||
}) {
|
||||
const shimmerAnim = useMemo(() => new Animated.Value(0), []);
|
||||
|
||||
useEffect(() => {
|
||||
const animation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(shimmerAnim, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shimmerAnim, {
|
||||
toValue: 0,
|
||||
duration: 800,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
animation.start();
|
||||
|
||||
return () => animation.stop();
|
||||
}, [shimmerAnim]);
|
||||
|
||||
const shimmerOpacity = shimmerAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.3, 0.7],
|
||||
});
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.markerContainer,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
borderColor,
|
||||
backgroundColor: loadingColor,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.skeletonShimmer,
|
||||
{
|
||||
opacity: shimmerOpacity,
|
||||
backgroundColor: "#FFFFFF",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponen fallback untuk error state
|
||||
*/
|
||||
function FallbackMarker({
|
||||
size = DEFAULT_MARKER_SIZE,
|
||||
borderColor = MainColor.darkblue,
|
||||
iconColor = MainColor.darkblue,
|
||||
}: {
|
||||
size?: number;
|
||||
borderColor?: string;
|
||||
iconColor?: string;
|
||||
}) {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.markerContainer,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
borderColor,
|
||||
backgroundColor: "#F5F5F5",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={[styles.fallbackIcon, { borderColor: iconColor }]}>
|
||||
<View style={[styles.fallbackIconInner, { backgroundColor: iconColor }]} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Props untuk DefaultMarker component
|
||||
*/
|
||||
export interface DefaultMarkerProps {
|
||||
/** ID file image dari API */
|
||||
imageId?: string;
|
||||
/** URL image langsung */
|
||||
imageUrl?: string;
|
||||
/** Ukuran marker (default: 30) */
|
||||
size?: number;
|
||||
/** Warna border marker (default: darkblue) */
|
||||
borderColor?: string;
|
||||
/** Warna skeleton loading (default: gray) */
|
||||
loadingColor?: string;
|
||||
/** Warna icon fallback (default: darkblue) */
|
||||
fallbackIconColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponen marker default dengan image, border, shadows, loading skeleton, dan error fallback
|
||||
*/
|
||||
export function DefaultMarker({
|
||||
imageId,
|
||||
imageUrl,
|
||||
size = DEFAULT_MARKER_SIZE,
|
||||
borderColor = MainColor.darkblue,
|
||||
loadingColor = MainColor.white_gray,
|
||||
fallbackIconColor = MainColor.darkblue,
|
||||
}: DefaultMarkerProps) {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const uri = imageId ? API_IMAGE.GET({ fileId: imageId }) : imageUrl;
|
||||
|
||||
// Debug log untuk development
|
||||
if (__DEV__ && uri) {
|
||||
console.log("[DefaultMarker] Image URI:", uri);
|
||||
}
|
||||
|
||||
const handleError = useCallback((error: any) => {
|
||||
console.log("[DefaultMarker] Image error:", error?.nativeEvent?.error || error);
|
||||
setHasError(true);
|
||||
}, []);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
console.log("[DefaultMarker] Image loaded successfully");
|
||||
}, []);
|
||||
|
||||
// Jika tidak ada URI atau error, tampilkan fallback
|
||||
if (!uri || hasError) {
|
||||
return (
|
||||
<FallbackMarker
|
||||
size={size}
|
||||
borderColor={borderColor}
|
||||
iconColor={fallbackIconColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Render image dengan placeholder (defaultSource) untuk loading state
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.markerContainer,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
borderColor,
|
||||
backgroundColor: loadingColor, // Background color sebagai placeholder
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={{ uri }}
|
||||
style={[styles.markerImage, { width: size, height: size }]}
|
||||
resizeMode="cover"
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
// Placeholder untuk Android saat loading
|
||||
defaultSource={require("@/assets/images/icon.png")}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponen Map yang reusable dan customizable menggunakan Mapbox/MapLibre
|
||||
*
|
||||
* Mendukung single marker, multiple markers, atau empty state.
|
||||
*
|
||||
* @example
|
||||
* // Single marker
|
||||
* <MapsV2Custom
|
||||
* markers={{
|
||||
* id: "1",
|
||||
* coordinate: [115.1756897, -8.737109],
|
||||
* imageId: "file-123"
|
||||
* }}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Multiple markers
|
||||
* <MapsV2Custom
|
||||
* markers={[
|
||||
* { id: "1", coordinate: [115.175, -8.737], imageId: "abc" },
|
||||
* { id: "2", coordinate: [115.180, -8.740], imageId: "xyz" }
|
||||
* ]}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Peta dengan custom style dan custom markers
|
||||
* <MapsV2Custom
|
||||
* mapStyle="https://your-custom-style.com"
|
||||
* markers={markers}
|
||||
* markerSize={40}
|
||||
* markerBorderColor={MainColor.primary}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Dengan custom marker renderer
|
||||
* <MapsV2Custom
|
||||
* markers={data}
|
||||
* renderMarker={(marker) => <CustomMarker {...marker} />}
|
||||
* />
|
||||
*
|
||||
* @example
|
||||
* // Peta kosong (tanpa markers)
|
||||
* <MapsV2Custom
|
||||
* initialRegion={{ latitude: -6.2, longitude: 106.8, latitudeDelta: 0.1, longitudeDelta: 0.1 }}
|
||||
* />
|
||||
*/
|
||||
export function MapsV2Custom({
|
||||
mapStyle = DEFAULT_MAP_STYLE,
|
||||
style = styles.container,
|
||||
mapViewStyle = styles.map,
|
||||
initialRegion = DEFAULT_REGION,
|
||||
zoomLevel = DEFAULT_ZOOM_LEVEL,
|
||||
markers,
|
||||
renderMarker,
|
||||
onMarkerPress,
|
||||
showDefaultMarkers = true,
|
||||
markerSize = DEFAULT_MARKER_SIZE,
|
||||
markerBorderColor = MainColor.darkblue,
|
||||
children,
|
||||
onMapPress,
|
||||
testID,
|
||||
cameraProps,
|
||||
mapViewProps,
|
||||
annotationProps,
|
||||
}: MapsV2CustomProps) {
|
||||
// Normalisasi markers ke array (mendukung single atau multiple)
|
||||
const normalizedMarkers = useMemo(
|
||||
() => {
|
||||
const arr = normalizeMarkers(markers);
|
||||
// Filter marker yang invalid
|
||||
return arr.filter((marker) => {
|
||||
if (!marker.id) {
|
||||
console.warn("[MapsV2Custom] Marker tanpa id akan diabaikan");
|
||||
return false;
|
||||
}
|
||||
if (!marker.coordinate || marker.coordinate.length !== 2) {
|
||||
console.warn("[MapsV2Custom] Marker tanpa coordinate valid akan diabaikan");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
[markers]
|
||||
);
|
||||
|
||||
// Validasi markers dalam development mode
|
||||
useMemo(() => {
|
||||
if (__DEV__) {
|
||||
normalizedMarkers.forEach((marker, index) => {
|
||||
validateMarker(marker, index);
|
||||
});
|
||||
}
|
||||
}, [normalizedMarkers]);
|
||||
|
||||
const handleMarkerSelected = useCallback(
|
||||
(marker: MapMarker) => {
|
||||
if (marker.onSelected) {
|
||||
marker.onSelected();
|
||||
}
|
||||
if (onMarkerPress) {
|
||||
onMarkerPress(marker);
|
||||
}
|
||||
},
|
||||
[onMarkerPress]
|
||||
);
|
||||
|
||||
const renderMarkerComponent = useCallback(
|
||||
(marker: MapMarker): ReactNode => {
|
||||
if (renderMarker) {
|
||||
return renderMarker(marker);
|
||||
}
|
||||
|
||||
if (showDefaultMarkers) {
|
||||
return (
|
||||
<DefaultMarker
|
||||
imageId={marker.imageId}
|
||||
imageUrl={marker.imageUrl}
|
||||
size={markerSize}
|
||||
borderColor={markerBorderColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[renderMarker, showDefaultMarkers, markerSize, markerBorderColor]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={style} testID={testID}>
|
||||
<MapView style={mapViewStyle} mapStyle={mapStyle} {...mapViewProps}>
|
||||
<Camera
|
||||
zoomLevel={zoomLevel}
|
||||
centerCoordinate={[initialRegion.longitude, initialRegion.latitude]}
|
||||
{...cameraProps}
|
||||
/>
|
||||
|
||||
{normalizedMarkers.map((marker) => (
|
||||
<PointAnnotation
|
||||
key={marker.id}
|
||||
id={marker.id}
|
||||
coordinate={marker.coordinate}
|
||||
onSelected={() => handleMarkerSelected(marker)}
|
||||
{...annotationProps}
|
||||
>
|
||||
{renderMarkerComponent(marker) as any}
|
||||
</PointAnnotation>
|
||||
))}
|
||||
|
||||
{children}
|
||||
</MapView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
map: { flex: 1 },
|
||||
markerContainer: {
|
||||
overflow: "hidden",
|
||||
borderWidth: 1,
|
||||
elevation: 4,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 3,
|
||||
},
|
||||
markerImage: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
skeletonShimmer: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: 999,
|
||||
},
|
||||
fallbackIcon: {
|
||||
width: "60%",
|
||||
height: "60%",
|
||||
borderRadius: 999,
|
||||
borderWidth: 2,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
fallbackIconInner: {
|
||||
width: "40%",
|
||||
height: "40%",
|
||||
borderRadius: 999,
|
||||
},
|
||||
});
|
||||
272
components/Map/SelectLocationMap.tsx
Normal file
272
components/Map/SelectLocationMap.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import React, { useState, useCallback, useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import {
|
||||
MapView,
|
||||
Camera,
|
||||
PointAnnotation,
|
||||
MarkerView,
|
||||
} from "@maplibre/maplibre-react-native";
|
||||
import * as Location from "expo-location";
|
||||
import { useFocusEffect, useRouter } from "expo-router";
|
||||
|
||||
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||
|
||||
type Coordinate = {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
|
||||
export default function SelectLocationMap() {
|
||||
const router = useRouter();
|
||||
const annotationRef = useRef<any>(null);
|
||||
|
||||
const [selectedCoord, setSelectedCoord] = useState<Coordinate | null>(null);
|
||||
const [address, setAddress] = useState<string>("");
|
||||
const [isLoadingAddress, setIsLoadingAddress] = useState(false);
|
||||
const [cameraCenter, setCameraCenter] = useState<[number, number]>([
|
||||
106.8272, -6.1751,
|
||||
]);
|
||||
|
||||
const reverseGeocode = async (coord: Coordinate): Promise<string> => {
|
||||
try {
|
||||
const { status } = await Location.getForegroundPermissionsAsync();
|
||||
if (status !== "granted") {
|
||||
await Location.requestForegroundPermissionsAsync();
|
||||
}
|
||||
|
||||
const results = await Location.reverseGeocodeAsync({
|
||||
latitude: coord.latitude,
|
||||
longitude: coord.longitude,
|
||||
});
|
||||
|
||||
if (!results || results.length === 0) return "Alamat tidak ditemukan";
|
||||
|
||||
const loc = results[0];
|
||||
const parts = [
|
||||
loc.street,
|
||||
loc.district,
|
||||
loc.subregion,
|
||||
loc.city,
|
||||
loc.region,
|
||||
loc.country,
|
||||
].filter(Boolean);
|
||||
|
||||
return parts.length > 0 ? parts.join(", ") : "Alamat tidak ditemukan";
|
||||
} catch (error: any) {
|
||||
console.log("reverseGeocode error:", error?.message || error);
|
||||
return "Gagal mengambil alamat";
|
||||
}
|
||||
};
|
||||
|
||||
const handleMapPress = useCallback(async (event: any) => {
|
||||
try {
|
||||
const coordinates = event?.geometry?.coordinates;
|
||||
if (!coordinates) return;
|
||||
|
||||
const [longitude, latitude] = coordinates;
|
||||
if (!longitude || !latitude) return;
|
||||
|
||||
const coord: Coordinate = { latitude, longitude };
|
||||
|
||||
// ✅ Update state koordinat, BUKAN ganti key
|
||||
setSelectedCoord(coord);
|
||||
setCameraCenter([longitude, latitude]);
|
||||
setAddress("");
|
||||
setIsLoadingAddress(true);
|
||||
|
||||
const resolvedAddress = await reverseGeocode(coord);
|
||||
setAddress(resolvedAddress);
|
||||
setIsLoadingAddress(false);
|
||||
|
||||
// ✅ Refresh annotation tanpa unmount
|
||||
annotationRef.current?.refresh?.();
|
||||
} catch (error: any) {
|
||||
console.log("handleMapPress error:", error?.message || error);
|
||||
setIsLoadingAddress(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!selectedCoord) return;
|
||||
router.navigate({
|
||||
pathname: "/maps/create",
|
||||
params: {
|
||||
latitude: String(selectedCoord.latitude),
|
||||
longitude: String(selectedCoord.longitude),
|
||||
address,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Sembunyikan marker sebelum halaman unmount
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
return () => {
|
||||
// Cleanup saat leave — sembunyikan marker dulu sebelum unmount
|
||||
setSelectedCoord(null);
|
||||
};
|
||||
}, []),
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<MapView style={styles.map} mapStyle={MAP_STYLE} onPress={handleMapPress}>
|
||||
<Camera
|
||||
zoomLevel={14}
|
||||
centerCoordinate={cameraCenter}
|
||||
animationMode="flyTo"
|
||||
animationDuration={300}
|
||||
/>
|
||||
|
||||
{/* ✅ Key statis — tidak pernah berubah, tidak unmount/remount */}
|
||||
{selectedCoord && (
|
||||
<MarkerView
|
||||
id="selected-marker"
|
||||
coordinate={[selectedCoord.longitude, selectedCoord.latitude]}
|
||||
anchor={{ x: 0.5, y: 1 }} // Anchor bawah tengah
|
||||
>
|
||||
<View style={styles.pin}>
|
||||
<View style={styles.pinDot} />
|
||||
</View>
|
||||
</MarkerView>
|
||||
)}
|
||||
</MapView>
|
||||
|
||||
{/* Bottom Sheet */}
|
||||
<View style={styles.bottomSheet}>
|
||||
{!selectedCoord ? (
|
||||
<Text style={styles.hintText}>
|
||||
Tap pada peta untuk memilih lokasi
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.coordRow}>
|
||||
<View style={styles.coordItem}>
|
||||
<Text style={styles.coordLabel}>Latitude</Text>
|
||||
<Text style={styles.coordValue}>
|
||||
{selectedCoord.latitude.toFixed(6)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.dividerVertical} />
|
||||
<View style={styles.coordItem}>
|
||||
<Text style={styles.coordLabel}>Longitude</Text>
|
||||
<Text style={styles.coordValue}>
|
||||
{selectedCoord.longitude.toFixed(6)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.addressContainer}>
|
||||
<Text style={styles.coordLabel}>Alamat</Text>
|
||||
{isLoadingAddress ? (
|
||||
<ActivityIndicator size="small" color="#0a1f44" />
|
||||
) : (
|
||||
<Text style={styles.addressText} numberOfLines={2}>
|
||||
{address || "-"}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.confirmButton,
|
||||
isLoadingAddress && styles.confirmButtonDisabled,
|
||||
]}
|
||||
onPress={handleConfirm}
|
||||
disabled={isLoadingAddress}
|
||||
>
|
||||
<Text style={styles.confirmButtonText}>Konfirmasi Lokasi</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
map: { flex: 1 },
|
||||
pin: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#0a1f44",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 2,
|
||||
borderColor: "#fff",
|
||||
},
|
||||
pinDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 100,
|
||||
backgroundColor: "#fff",
|
||||
},
|
||||
bottomSheet: {
|
||||
backgroundColor: "#fff",
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 32,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
elevation: 10,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: -3 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 6,
|
||||
minHeight: 140,
|
||||
justifyContent: "center",
|
||||
},
|
||||
hintText: {
|
||||
textAlign: "center",
|
||||
color: "#888",
|
||||
fontSize: 14,
|
||||
},
|
||||
coordRow: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 12,
|
||||
},
|
||||
coordItem: { flex: 1 },
|
||||
dividerVertical: {
|
||||
width: 1,
|
||||
backgroundColor: "#e0e0e0",
|
||||
marginHorizontal: 12,
|
||||
},
|
||||
coordLabel: {
|
||||
fontSize: 11,
|
||||
color: "#888",
|
||||
marginBottom: 2,
|
||||
},
|
||||
coordValue: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#0a1f44",
|
||||
},
|
||||
addressContainer: { marginBottom: 16 },
|
||||
addressText: {
|
||||
fontSize: 13,
|
||||
color: "#333",
|
||||
lineHeight: 18,
|
||||
},
|
||||
confirmButton: {
|
||||
backgroundColor: "#0a1f44",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 14,
|
||||
alignItems: "center",
|
||||
},
|
||||
confirmButtonDisabled: {
|
||||
backgroundColor: "#aaa",
|
||||
},
|
||||
confirmButtonText: {
|
||||
color: "#fff",
|
||||
fontWeight: "700",
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
@@ -77,8 +77,12 @@ export default function NotificationInitializer() {
|
||||
});
|
||||
|
||||
console.log("✅ Device token berhasil didaftarkan ke backend");
|
||||
} catch (error) {
|
||||
console.error("❌ Gagal mendaftarkan device token:", error);
|
||||
} catch (error: any) {
|
||||
// Log error detail tapi jangan crash aplikasi
|
||||
console.error("❌ Gagal mendaftarkan device token:", error?.message);
|
||||
console.error("Response status:", error?.response?.status);
|
||||
console.error("Response data:", error?.response?.data);
|
||||
// Skip logout - biarkan user tetap bisa pakai app meski notif gagal
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
256
components/PhoneInput/PhoneInputCustom.tsx
Normal file
256
components/PhoneInput/PhoneInputCustom.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
DEFAULT_COUNTRY,
|
||||
searchCountries,
|
||||
type CountryData,
|
||||
} from "@/constants/countries";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
interface PhoneInputProps {
|
||||
value: string;
|
||||
onChangePhoneNumber: (phone: string) => void;
|
||||
selectedCountry?: CountryData;
|
||||
onChangeCountry: (country: CountryData) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function PhoneInputCustom({
|
||||
value,
|
||||
onChangePhoneNumber,
|
||||
selectedCountry = DEFAULT_COUNTRY,
|
||||
onChangeCountry,
|
||||
placeholder = "Masukkan nomor",
|
||||
disabled = false,
|
||||
}: PhoneInputProps) {
|
||||
const [countryPickerVisible, setCountryPickerVisible] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const filteredCountries = searchCountries(searchQuery);
|
||||
|
||||
const handleSelectCountry = (country: CountryData) => {
|
||||
onChangeCountry(country);
|
||||
setCountryPickerVisible(false);
|
||||
setSearchQuery("");
|
||||
};
|
||||
|
||||
const handlePhoneChange = (text: string) => {
|
||||
// Only allow numbers and spaces
|
||||
const cleaned = text.replace(/[^\d\s]/g, "");
|
||||
onChangePhoneNumber(cleaned);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Phone Input Field */}
|
||||
<View style={styles.container}>
|
||||
<TouchableOpacity
|
||||
style={styles.countryPickerButton}
|
||||
onPress={() => setCountryPickerVisible(true)}
|
||||
disabled={disabled}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.countryCodeText}>+{selectedCountry.callingCode}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.divider} />
|
||||
|
||||
<TextInput
|
||||
style={[styles.phoneInput, disabled && styles.disabledInput]}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={MainColor.placeholder}
|
||||
value={value}
|
||||
onChangeText={handlePhoneChange}
|
||||
keyboardType="phone-pad"
|
||||
autoComplete="tel"
|
||||
importantForAutofill="yes"
|
||||
editable={!disabled}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Country Picker Modal */}
|
||||
<Modal
|
||||
visible={countryPickerVisible}
|
||||
transparent
|
||||
animationType="slide"
|
||||
onRequestClose={() => setCountryPickerVisible(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Pilih Negara</Text>
|
||||
<TouchableOpacity onPress={() => setCountryPickerVisible(false)}>
|
||||
<Text style={styles.modalClose}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Cari negara atau kode..."
|
||||
placeholderTextColor={MainColor.placeholder}
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<ScrollView style={styles.countryList}>
|
||||
{filteredCountries.map((country) => (
|
||||
<TouchableOpacity
|
||||
key={country.code}
|
||||
style={[
|
||||
styles.countryItem,
|
||||
selectedCountry.code === country.code &&
|
||||
styles.countryItemSelected,
|
||||
]}
|
||||
onPress={() => handleSelectCountry(country)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.countryInfo}>
|
||||
<Text style={styles.countryName}>{country.name}</Text>
|
||||
<Text style={styles.countryCode}>+{country.callingCode}</Text>
|
||||
</View>
|
||||
{selectedCountry.code === country.code && (
|
||||
<Text style={styles.checkmark}>✓</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// Container
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
backgroundColor: MainColor.white,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: MainColor.white_gray,
|
||||
marginBottom: 16,
|
||||
overflow: "hidden",
|
||||
},
|
||||
// Country Picker Button
|
||||
countryPickerButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
backgroundColor: MainColor.text_input,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: MainColor.white_gray,
|
||||
},
|
||||
countryCodeText: {
|
||||
fontSize: 16,
|
||||
color: MainColor.black,
|
||||
fontWeight: "600",
|
||||
},
|
||||
dropdownIcon: {
|
||||
fontSize: 18,
|
||||
color: MainColor.placeholder,
|
||||
marginLeft: 4,
|
||||
},
|
||||
// Divider
|
||||
divider: {
|
||||
width: 1,
|
||||
backgroundColor: MainColor.white_gray,
|
||||
},
|
||||
// Phone Input
|
||||
phoneInput: {
|
||||
flex: 1,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: 16,
|
||||
color: MainColor.black,
|
||||
},
|
||||
disabledInput: {
|
||||
backgroundColor: MainColor.text_input,
|
||||
color: MainColor.placeholder,
|
||||
},
|
||||
// Modal
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: MainColor.white,
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
maxHeight: "80%",
|
||||
paddingBottom: 34,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: MainColor.white_gray,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: MainColor.black,
|
||||
},
|
||||
modalClose: {
|
||||
fontSize: 24,
|
||||
color: MainColor.placeholder,
|
||||
padding: 5,
|
||||
},
|
||||
// Search Input
|
||||
searchInput: {
|
||||
backgroundColor: MainColor.text_input,
|
||||
margin: 16,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
fontSize: 16,
|
||||
color: MainColor.black,
|
||||
},
|
||||
// Country List
|
||||
countryList: {
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
countryItem: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: MainColor.white_gray,
|
||||
},
|
||||
countryItemSelected: {
|
||||
backgroundColor: MainColor.soft_darkblue + "15",
|
||||
},
|
||||
countryInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
countryName: {
|
||||
fontSize: 16,
|
||||
color: MainColor.black,
|
||||
fontWeight: "500",
|
||||
},
|
||||
countryCode: {
|
||||
fontSize: 14,
|
||||
color: MainColor.placeholder,
|
||||
marginTop: 2,
|
||||
},
|
||||
checkmark: {
|
||||
fontSize: 20,
|
||||
color: MainColor.green,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
});
|
||||
114
components/_ShareComponent/AppHeader.tsx
Normal file
114
components/_ShareComponent/AppHeader.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { Platform, StyleSheet, Text, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { BackButton } from "..";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
right?: React.ReactNode;
|
||||
showBack?: boolean;
|
||||
onPressLeft?: () => void;
|
||||
left?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function AppHeader({
|
||||
title,
|
||||
right,
|
||||
showBack = true,
|
||||
onPressLeft,
|
||||
left,
|
||||
}: Props) {
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
// iOS 16+ detection (Dynamic Island) - insets.top > 47 indicates Dynamic Island
|
||||
const isIOS26Plus =
|
||||
Platform.OS === "ios" && insets.top > 47;
|
||||
|
||||
// Dynamic padding berdasarkan platform dan iOS version
|
||||
const paddingTop =
|
||||
Platform.OS === "ios"
|
||||
? isIOS26Plus
|
||||
? insets.top - 10
|
||||
: insets.top
|
||||
: 40;
|
||||
|
||||
const paddingBottom = Platform.OS === "ios" ? 8 : 13;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
backgroundColor: MainColor.darkblue,
|
||||
paddingTop,
|
||||
paddingBottom,
|
||||
},
|
||||
]}
|
||||
pointerEvents="box-none"
|
||||
>
|
||||
{/* Header Container dengan absolute positioning untuk title center */}
|
||||
<View style={styles.headerApp} pointerEvents="box-none">
|
||||
{/* Left Section - Absolute Left */}
|
||||
<View style={styles.headerLeft}>
|
||||
{showBack ? (
|
||||
<BackButton onPress={onPressLeft} />
|
||||
) : left ? (
|
||||
left
|
||||
) : (
|
||||
<View style={styles.placeholder} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Title - Absolute Center */}
|
||||
<View style={styles.headerCenter}>
|
||||
<Text
|
||||
style={styles.headerTitle}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{title ? title.charAt(0).toUpperCase() + title.slice(1) : ""}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Right Section - Absolute Right */}
|
||||
<View style={styles.headerRight}>
|
||||
{right}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerApp: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 16,
|
||||
height: 44, // Fixed height untuk consistency
|
||||
},
|
||||
headerLeft: {
|
||||
position: "absolute",
|
||||
left: 16,
|
||||
zIndex: 1,
|
||||
},
|
||||
headerCenter: {
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
headerRight: {
|
||||
position: "absolute",
|
||||
right: 16,
|
||||
zIndex: 1,
|
||||
},
|
||||
placeholder: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
headerTitle: {
|
||||
color: MainColor.yellow,
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
textAlign: "center",
|
||||
},
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
SafeAreaView,
|
||||
} from "react-native-safe-area-context";
|
||||
import type { ScrollViewProps, FlatListProps } from "react-native";
|
||||
import Spacing from "./Spacing";
|
||||
|
||||
// --- ✅ Tambahkan refreshControl ke BaseProps ---
|
||||
interface BaseProps {
|
||||
@@ -83,7 +84,7 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
return <View style={[GStyles.container, style]}>{content}</View>;
|
||||
};
|
||||
|
||||
// 🔹 Mode Dinamis
|
||||
// 🔹 Mode Dinamis (FlatList)
|
||||
if ("listData" in props) {
|
||||
const listProps = props as ListModeProps;
|
||||
|
||||
@@ -95,7 +96,7 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
{headerComponent && (
|
||||
<View style={GStyles.stickyHeader}>{headerComponent}</View>
|
||||
)}
|
||||
<View style={[GStyles.container, style]}>
|
||||
<View style={[GStyles.container, style, { flex: 1 }]}>
|
||||
<FlatList
|
||||
data={listProps.listData}
|
||||
renderItem={listProps.renderItem}
|
||||
@@ -107,30 +108,36 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
return `fallback-${index}-${JSON.stringify(item)}`;
|
||||
}
|
||||
|
||||
// Gabungkan ID dengan indeks untuk mencegah duplikasi
|
||||
return `${String(item.id)}-${index}`;
|
||||
})
|
||||
}
|
||||
|
||||
refreshControl={refreshControl} // ✅ dari BaseProps
|
||||
refreshControl={refreshControl}
|
||||
onEndReached={listProps.onEndReached}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListHeaderComponent={listProps.ListHeaderComponent}
|
||||
ListFooterComponent={listProps.ListFooterComponent}
|
||||
ListEmptyComponent={listProps.ListEmptyComponent}
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{footerComponent ? (
|
||||
<SafeAreaView
|
||||
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
|
||||
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
|
||||
>
|
||||
{footerComponent}
|
||||
</SafeAreaView>
|
||||
) : hideFooter ? null : (
|
||||
{/* Footer dengan position absolute untuk stay di bawah */}
|
||||
{footerComponent && !hideFooter && (
|
||||
<View style={styles.footerContainer}>
|
||||
<SafeAreaView
|
||||
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
|
||||
style={{ backgroundColor: MainColor.darkblue }}
|
||||
>
|
||||
{footerComponent}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!footerComponent && !hideFooter && (
|
||||
<SafeAreaView
|
||||
edges={["bottom"]}
|
||||
style={{ backgroundColor: MainColor.darkblue }}
|
||||
@@ -144,7 +151,7 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
// 🔹 Mode Statis
|
||||
// 🔹 Mode Statis (ScrollView)
|
||||
const staticProps = props as StaticModeProps;
|
||||
|
||||
return (
|
||||
@@ -156,24 +163,34 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
<View style={GStyles.stickyHeader}>{headerComponent}</View>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
refreshControl={refreshControl} // ✅ sekarang valid
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
{renderContainer(staticProps.children)}
|
||||
</TouchableWithoutFeedback>
|
||||
</ScrollView>
|
||||
|
||||
{footerComponent ? (
|
||||
<SafeAreaView
|
||||
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
|
||||
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
|
||||
<View style={{ flex: 0 }} collapsable={false}>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
paddingBottom: footerComponent && !hideFooter ? OS_HEIGHT : 0
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
refreshControl={refreshControl}
|
||||
>
|
||||
{footerComponent}
|
||||
</SafeAreaView>
|
||||
) : hideFooter ? null : (
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
{renderContainer(staticProps.children)}
|
||||
</TouchableWithoutFeedback>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Footer dengan position absolute untuk stay di bawah */}
|
||||
{footerComponent && !hideFooter && (
|
||||
<View style={styles.footerContainer}>
|
||||
<SafeAreaView
|
||||
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
|
||||
style={{ backgroundColor: MainColor.darkblue }}
|
||||
>
|
||||
{footerComponent}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!footerComponent && !hideFooter && (
|
||||
<SafeAreaView
|
||||
edges={["bottom"]}
|
||||
style={{ backgroundColor: MainColor.darkblue }}
|
||||
@@ -187,4 +204,15 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Styles untuk footer dengan position absolute
|
||||
const styles = {
|
||||
footerContainer: {
|
||||
position: "absolute" as const,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: MainColor.darkblue,
|
||||
},
|
||||
};
|
||||
|
||||
export default NewWrapper;
|
||||
|
||||
@@ -49,7 +49,7 @@ const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
|
||||
right: 0,
|
||||
height: 100,
|
||||
backgroundColor: MainColor.soft_darkblue,
|
||||
borderRadius: 4,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -49,6 +49,8 @@ import MapCustom from "./Map/MapCustom";
|
||||
import CenterCustom from "./Center/CenterCustom";
|
||||
// Clickable
|
||||
import ClickableCustom from "./Clickable/ClickableCustom";
|
||||
// PhoneInput
|
||||
import PhoneInputCustom from "./PhoneInput/PhoneInputCustom";
|
||||
// Scroll
|
||||
import ScrollableCustom from "./Scroll/ScrollCustom";
|
||||
// ShareComponent
|
||||
@@ -95,6 +97,8 @@ export {
|
||||
CheckboxGroup,
|
||||
// Clickable
|
||||
ClickableCustom,
|
||||
// PhoneInput
|
||||
PhoneInputCustom,
|
||||
// Container
|
||||
CircleContainer,
|
||||
// Divider
|
||||
|
||||
@@ -23,8 +23,8 @@ export {
|
||||
};
|
||||
|
||||
// OS Height
|
||||
const OS_ANDROID_HEIGHT = 115
|
||||
const OS_IOS_HEIGHT = 90
|
||||
const OS_ANDROID_HEIGHT = 70
|
||||
const OS_IOS_HEIGHT = 80
|
||||
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
|
||||
|
||||
// Text Size
|
||||
|
||||
89
constants/countries.ts
Normal file
89
constants/countries.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { type CountryCode } from "libphonenumber-js";
|
||||
|
||||
/**
|
||||
* Country data for phone number input
|
||||
* Contains only country name and calling code (NO flags for maximum compatibility)
|
||||
*/
|
||||
export interface CountryData {
|
||||
code: CountryCode;
|
||||
name: string;
|
||||
callingCode: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of supported countries for phone number input
|
||||
*
|
||||
* @description
|
||||
* This list includes major countries across different regions.
|
||||
* Countries are ordered by likelihood of use (Indonesia first as default).
|
||||
*
|
||||
* @note
|
||||
* NO emoji flags used - only text-based country name and calling code
|
||||
* This ensures maximum compatibility across all platforms and iOS versions
|
||||
*/
|
||||
export const COUNTRIES: CountryData[] = [
|
||||
// Asia Pacific (Primary markets)
|
||||
{ code: "ID", name: "Indonesia", callingCode: "62" },
|
||||
{ code: "SG", name: "Singapore", callingCode: "65" },
|
||||
{ code: "MY", name: "Malaysia", callingCode: "60" },
|
||||
{ code: "AU", name: "Australia", callingCode: "61" },
|
||||
|
||||
// Asia (Other)
|
||||
{ code: "CN", name: "China", callingCode: "86" },
|
||||
{ code: "JP", name: "Japan", callingCode: "81" },
|
||||
{ code: "KR", name: "South Korea", callingCode: "82" },
|
||||
{ code: "IN", name: "India", callingCode: "91" },
|
||||
|
||||
// Middle East
|
||||
{ code: "AE", name: "United Arab Emirates", callingCode: "971" },
|
||||
{ code: "SA", name: "Saudi Arabia", callingCode: "966" },
|
||||
|
||||
// Europe
|
||||
{ code: "GB", name: "United Kingdom", callingCode: "44" },
|
||||
{ code: "DE", name: "Germany", callingCode: "49" },
|
||||
{ code: "FR", name: "France", callingCode: "33" },
|
||||
{ code: "NL", name: "Netherlands", callingCode: "31" },
|
||||
|
||||
// Americas
|
||||
{ code: "US", name: "United States", callingCode: "1" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Default country for phone number input
|
||||
* Used when no country is selected (Indonesia by default)
|
||||
*/
|
||||
export const DEFAULT_COUNTRY: CountryData = COUNTRIES[0];
|
||||
|
||||
/**
|
||||
* Get country by calling code
|
||||
* @param callingCode - The calling code to search for (e.g., "62", "1")
|
||||
* @returns The matching country data or undefined if not found
|
||||
*/
|
||||
export function getCountryByCallingCode(callingCode: string): CountryData | undefined {
|
||||
return COUNTRIES.find((country) => country.callingCode === callingCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country by country code (ISO 3166-1 alpha-2)
|
||||
* @param code - The country code to search for (e.g., "ID", "US")
|
||||
* @returns The matching country data or undefined if not found
|
||||
*/
|
||||
export function getCountryByCode(code: CountryCode): CountryData | undefined {
|
||||
return COUNTRIES.find((country) => country.code === code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search countries by name or calling code
|
||||
* @param query - The search query (case-insensitive)
|
||||
* @returns Array of matching countries
|
||||
*/
|
||||
export function searchCountries(query: string): CountryData[] {
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
|
||||
return COUNTRIES.filter(
|
||||
(country) =>
|
||||
country.name.toLowerCase().includes(normalizedQuery) ||
|
||||
country.code.toLowerCase().includes(normalizedQuery) ||
|
||||
country.callingCode.includes(normalizedQuery)
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,7 @@ type AuthContextType = {
|
||||
isAdmin: boolean;
|
||||
isUserActive: boolean;
|
||||
loginWithNomor: (nomor: string) => Promise<boolean>;
|
||||
validateOtp: (nomor: string) => Promise<any>;
|
||||
validateOtp: (nomor: string, code: string) => Promise<any>;
|
||||
logout: () => Promise<void>;
|
||||
registerUser: (userData: {
|
||||
username: string;
|
||||
@@ -97,10 +97,10 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
// --- 2. Validasi OTP & cek user ---
|
||||
const validateOtp = async (nomor: string) => {
|
||||
const validateOtp = async (nomor: string, code: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiValidationCode({ nomor: nomor });
|
||||
const response = await apiValidationCode({ nomor: nomor, code: code });
|
||||
const { token } = response;
|
||||
console.log("[RESPONSE VALIDASI OTP]", JSON.stringify(response, null, 2));
|
||||
|
||||
|
||||
101
docs/PODS.back
Normal file
101
docs/PODS.back
Normal file
@@ -0,0 +1,101 @@
|
||||
NOTE:
|
||||
|
||||
Untuk Development Selanjutnya:
|
||||
Sekarang Anda bisa menjalankan:
|
||||
|
||||
1 # Untuk run iOS dev client
|
||||
2 bun run ios
|
||||
3
|
||||
4 # Atau dengan Expo
|
||||
5 bunx expo run:ios
|
||||
|
||||
Jika di masa depan terjadi error serupa, Anda bisa gunakan command ini:
|
||||
|
||||
1 cd ios
|
||||
2 rm -rf Pods Podfile.lock
|
||||
3 pod install
|
||||
|
||||
|
||||
|
||||
use_modular_headers!
|
||||
|
||||
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||
|
||||
require 'json'
|
||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||
|
||||
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||
|
||||
prepare_react_native_project!
|
||||
|
||||
target 'HIPMIBadungConnect' do
|
||||
use_expo_modules!
|
||||
|
||||
if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1'
|
||||
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||
else
|
||||
config_command = [
|
||||
'npx',
|
||||
'expo-modules-autolinking',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
'--platform',
|
||||
'ios'
|
||||
]
|
||||
end
|
||||
|
||||
config = use_native_modules!(config_command)
|
||||
|
||||
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||
# An absolute path to your application root.
|
||||
:app_path => "#{Pod::Config.instance.installation_root}/..",
|
||||
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||
)
|
||||
|
||||
pod 'Firebase'
|
||||
pod 'Firebase/Messaging'
|
||||
|
||||
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
|
||||
post_install do |installer|
|
||||
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
|
||||
$MLRN.post_install(installer)
|
||||
# @generated end @maplibre/maplibre-react-native:post-install
|
||||
|
||||
# Fix all script phases with incorrect paths
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_phases.each do |phase|
|
||||
next unless phase.respond_to?(:shell_script)
|
||||
|
||||
# Fix duplicated path issue
|
||||
if phase.shell_script.include?('with-environment.sh')
|
||||
# Remove any existing path and use proper relative path
|
||||
phase.shell_script = phase.shell_script.gsub(
|
||||
%r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},
|
||||
'${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Standard React Native post install
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
||||
)
|
||||
end
|
||||
# @generated end post_installer
|
||||
|
||||
end
|
||||
253
docs/QR_CODE_TESTING.md
Normal file
253
docs/QR_CODE_TESTING.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# QR Code Testing Guide - HIPMI Mobile
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
Dokumentasi ini menjelaskan cara testing QR Code untuk Universal Links (iOS) dan App Links (Android) pada fitur Event Confirmation.
|
||||
|
||||
## 🔧 Update Terbaru
|
||||
|
||||
File `screens/Admin/Event/EventDetailQRCode.tsx` telah diupdate dengan fitur:
|
||||
- **Toggle Button**: Switch antara HTTPS link dan Custom Scheme link
|
||||
- **HTTPS Link**: Untuk testing Universal Links/App Links dengan domain staging
|
||||
- **Custom Scheme**: Untuk testing langsung tanpa domain verification
|
||||
|
||||
## 🎯 Cara Testing QR Code
|
||||
|
||||
### Opsi 1: HTTPS Link (Recommended untuk Production)
|
||||
|
||||
**Gunakan tombol "HTTPS"** di component QR Code.
|
||||
|
||||
**Link yang di-generate:**
|
||||
```
|
||||
https://cld-dkr-staging-hipmi.wibudev.com/event/{id}/confirmation?userId={userId}
|
||||
```
|
||||
|
||||
**Cara kerja:**
|
||||
1. User scan QR code dengan kamera
|
||||
2. Safari/Chrome terbuka dengan URL HTTPS
|
||||
3. iOS/Android mendeteksi domain terverifikasi
|
||||
4. App terbuka otomatis dan menuju halaman confirmation
|
||||
|
||||
**Prerequisites:**
|
||||
- ✅ File `apple-app-site-association` harus accessible di Next.js server
|
||||
- ✅ File `assetlinks.json` harus accessible di Next.js server
|
||||
- ✅ Domain harus terverifikasi di app.config.js
|
||||
- ✅ App harus di-build ulang setelah perubahan domain
|
||||
|
||||
**Testing Steps:**
|
||||
```bash
|
||||
# 1. Pastikan .well-known files accessible
|
||||
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
|
||||
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json
|
||||
|
||||
# 2. Rebuild app
|
||||
bunx expo prebuild --clean
|
||||
|
||||
# 3. Run di physical device (bukan simulator)
|
||||
bun run android # untuk Android
|
||||
bun run ios # untuk iOS
|
||||
```
|
||||
|
||||
### Opsi 2: Custom Scheme Link (Untuk Development/Testing Cepat)
|
||||
|
||||
**Gunakan tombol "Custom Scheme"** di component QR Code.
|
||||
|
||||
**Link yang di-generate:**
|
||||
```
|
||||
hipmimobile://event/{id}/confirmation?userId={userId}
|
||||
```
|
||||
|
||||
**Cara kerja:**
|
||||
1. User scan QR code dengan kamera
|
||||
2. iOS: Pilih "Open in HIPMI Badung Connect"
|
||||
3. Android: Langsung buka app
|
||||
4. App terbuka dan menuju halaman confirmation
|
||||
|
||||
**Keuntungan:**
|
||||
- ✅ Tidak butuh domain verification
|
||||
- ✅ Bisa testing langsung tanpa rebuild
|
||||
- ✅ Cocok untuk development
|
||||
|
||||
**Kekurangan:**
|
||||
- ❌ Tidak bisa dibuka dari web browser
|
||||
- ❌ Tidak support universal linking dari website lain
|
||||
|
||||
## 📱 Testing Checklist
|
||||
|
||||
### iOS (Universal Links)
|
||||
|
||||
- [ ] File `apple-app-site-association` valid dan accessible
|
||||
- [ ] Domain terdaftar di `app.config.js` → `ios.associatedDomains`
|
||||
- [ ] Bundle ID match dengan konfigurasi
|
||||
- [ ] Team ID benar di apple-app-site-association
|
||||
- [ ] Test dengan **physical device** (simulator tidak support)
|
||||
- [ ] Test dengan **Safari** (bukan Chrome)
|
||||
- [ ] Long press link → ada opsi "Open"
|
||||
|
||||
**Debug iOS:**
|
||||
```bash
|
||||
# Cek apple-app-site-association
|
||||
curl -I https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
|
||||
|
||||
# Harus return:
|
||||
# Content-Type: application/json
|
||||
# HTTP/2 200
|
||||
```
|
||||
|
||||
### Android (App Links)
|
||||
|
||||
- [ ] File `assetlinks.json` valid dan accessible
|
||||
- [ ] SHA256 fingerprint benar
|
||||
- [ ] Package name match
|
||||
- [ ] Intent filters terdaftar di app.config.js
|
||||
- [ ] Test dengan **physical device**
|
||||
- [ ] Test dengan **Chrome**
|
||||
|
||||
**Debug Android:**
|
||||
```bash
|
||||
# Dapatkan SHA256 fingerprint
|
||||
cd android
|
||||
./gradlew signingReport
|
||||
|
||||
# Cek assetlinks.json
|
||||
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/assetlinks.json
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Problem: QR Scan Terbuka di Safari, Tidak Balik ke App
|
||||
|
||||
**Penyebab:**
|
||||
- Domain belum terverifikasi untuk Universal Links/App Links
|
||||
- File `.well-known` tidak accessible atau invalid
|
||||
- App belum di-rebuild setelah perubahan domain
|
||||
|
||||
**Solusi:**
|
||||
1. Pastikan file `.well-known` accessible:
|
||||
```bash
|
||||
curl https://cld-dkr-staging-hipmi.wibudev.com/.well-known/apple-app-site-association
|
||||
```
|
||||
|
||||
2. Rebuild app:
|
||||
```bash
|
||||
bunx expo prebuild --clean
|
||||
bun run android # atau bun run ios
|
||||
```
|
||||
|
||||
3. Gunakan **Custom Scheme** untuk testing cepat
|
||||
|
||||
### Problem: Link Tidak Membuka App Sama Sekali
|
||||
|
||||
**Cek:**
|
||||
1. App sudah terinstall di device
|
||||
2. Link format benar (hipmimobile:// atau https://)
|
||||
3. Route handler sudah ada di app folder
|
||||
|
||||
**Test manual:**
|
||||
```bash
|
||||
# iOS Simulator
|
||||
xcrun simctl openurl booted "hipmimobile://event/123/confirmation?userId=456"
|
||||
|
||||
# Android Emulator
|
||||
adb shell am start -W -a android.intent.action.VIEW \
|
||||
-d "hipmimobile://event/123/confirmation?userId=456" \
|
||||
com.bip.hipmimobileapp
|
||||
```
|
||||
|
||||
### Problem: "Cannot GET /event/..." di Next.js
|
||||
|
||||
**Penyebab:**
|
||||
Route `/event/[id]/confirmation` tidak ada di Next.js server
|
||||
|
||||
**Solusi:**
|
||||
Pastikan Next.js project punya file:
|
||||
```
|
||||
public/.well-known/apple-app-site-association
|
||||
public/.well-known/assetlinks.json
|
||||
```
|
||||
|
||||
Dan API route untuk handle:
|
||||
```
|
||||
pages/api/event/[id]/confirmation.ts
|
||||
```
|
||||
|
||||
## 📄 File Configuration
|
||||
|
||||
### app.config.js - iOS
|
||||
```javascript
|
||||
ios: {
|
||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||
}
|
||||
```
|
||||
|
||||
### app.config.js - Android
|
||||
```javascript
|
||||
android: {
|
||||
intentFilters: [
|
||||
{
|
||||
action: "VIEW",
|
||||
autoVerify: true,
|
||||
data: [
|
||||
{
|
||||
scheme: "https",
|
||||
host: "cld-dkr-staging-hipmi.wibudev.com",
|
||||
pathPrefix: "/",
|
||||
},
|
||||
],
|
||||
category: ["BROWSABLE", "DEFAULT"],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### apple-app-site-association (Next.js)
|
||||
```json
|
||||
{
|
||||
"applinks": {
|
||||
"apps": [],
|
||||
"details": [
|
||||
{
|
||||
"appID": "TEAM_ID.com.anonymous.hipmi-mobile",
|
||||
"paths": ["/event/*/confirmation"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### assetlinks.json (Next.js)
|
||||
```json
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.bip.hipmimobileapp",
|
||||
"sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 🎓 Best Practices
|
||||
|
||||
1. **Development**: Gunakan Custom Scheme untuk testing cepat
|
||||
2. **Staging**: Gunakan HTTPS link dengan domain staging
|
||||
3. **Production**: Gunakan HTTPS link dengan domain production
|
||||
4. **Testing**: Selalu test di physical device, bukan simulator
|
||||
5. **Debugging**: Enable logging di confirmation page untuk track deep link
|
||||
|
||||
## 🔗 Related Files
|
||||
|
||||
- `screens/Admin/Event/EventDetailQRCode.tsx` - QR Code generator
|
||||
- `app/(application)/(user)/event/[id]/confirmation.tsx` - Confirmation page
|
||||
- `app.config.js` - App configuration
|
||||
- `service/api-config.ts` - API configuration (DEEP_LINK_URL)
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Jika masih ada masalah:
|
||||
1. Cek logs di console
|
||||
2. Test manual dengan adb/xcrun
|
||||
3. Verify .well-known files dengan curl
|
||||
4. Pastikan app rebuild setelah perubahan config
|
||||
@@ -55,10 +55,10 @@ Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
|
||||
|
||||
<!-- START Prompt Admin Refactoring -->
|
||||
<!-- Pindah kode ke Screen Component -->
|
||||
File source: app/(application)/admin/donation/[id]/list-disbursement-of-funds.tsx
|
||||
Folder tujuan: screens/Admin/Donation
|
||||
Nama file utama: ScreenDonationListDisbursementOfFunds.tsx
|
||||
Nama function utama: Admin_ScreenDonationListDisbursementOfFunds
|
||||
File source: app/(application)/(user)/portofolio/[id]/create.tsx
|
||||
Folder tujuan: screens/Portofolio
|
||||
Nama file utama: ScreenPortofolioCreate.tsx
|
||||
Nama function utama: Admin_ScreenPortofolioCreate
|
||||
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||
|
||||
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
||||
@@ -66,8 +66,8 @@ Analisa juga file "Nama file utama" , jika belum menggunakan NewWrapper pada fil
|
||||
|
||||
|
||||
<!-- Penerapan Pagination -->
|
||||
Function fecth: apiAdminDonationDisbursementOfFundsListById
|
||||
File function fetch: service/api-admin/api-admin-donation.ts
|
||||
Function fecth: apiAdminForumCommentById
|
||||
File function fetch: service/api-admin/api-admin-forum.ts
|
||||
|
||||
Terapkan pagination pada file "Nama file utama"
|
||||
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||
@@ -80,10 +80,13 @@ Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
|
||||
<!-- END Prompt Admin Refactoring -->
|
||||
|
||||
<!-- Additional -->
|
||||
File refrensi: screens/Admin/Voting/ScreenEventTypeOfEvent.tsx
|
||||
File refrensi: screens/Admin/Forum/ScreenForumDetailReportPosting.tsx
|
||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang hampir sama
|
||||
|
||||
Untuk refrensi tampilan Box bisa anda gunakan dari file: screens/Admin/Donation/BoxDonationCategory.tsx dan buatkan komponen yang mirip untuk list of donatur dengan nama file: BoxDonationListOfDonatur.tsx
|
||||
Untuk refrensi tampilan Box bisa anda gunakan dari file: screens/Admin/Donation/BoxDonationListOfDonatur.tsx dan buatkan komponen yang mirip untuk list of donatur dengan nama file: BoxDonationListOfInvestor.tsx
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Use Prompt Now -->
|
||||
Terapkan NewWrapper pada file: screens/Admin/App-Information/InformationBankSection.tsx
|
||||
@@ -108,10 +111,18 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1" ( stri
|
||||
Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
|
||||
|
||||
<!-- Create Box -->
|
||||
File Utama: screens/Admin/Donation/Admin_ScreenDonationStatus.tsx
|
||||
Folder tujuan: screens/Admin/Donation
|
||||
Buat box component baru pada file "File Utama" di bagian renderItem,
|
||||
File Utama: app/(application)/(user)/maps/[id]/edit.tsx
|
||||
Folder tujuan: screens/Maps
|
||||
Nama file utama: ScreenMapsEdit.tsx
|
||||
Nama function utama: Maps_ScreenMapsEdit
|
||||
|
||||
Buatkan file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
||||
|
||||
<!-- END Create Box -->
|
||||
|
||||
<!-- Random Prompt -->
|
||||
Diskusi pada file screens/Authentication/LoginView.tsx , tentang penggunaan phone number input. Karena tidak berfungsi dengan baik pada versi ios 26 keatas
|
||||
|
||||
<!-- END Random Prompt -->
|
||||
|
||||
<!-- END Use Prompt Now -->
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:cld-dkr-staging-hipmi.wibudev.com</string>
|
||||
<string>applinks:hipmi.muku.id</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1</string>
|
||||
<string>1.0.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -39,7 +39,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>21</string>
|
||||
<string>7</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
46
ios/Podfile
46
ios/Podfile
@@ -1,15 +1,22 @@
|
||||
use_modular_headers!
|
||||
|
||||
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
|
||||
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
|
||||
|
||||
require 'json'
|
||||
podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {}
|
||||
|
||||
def ccache_enabled?(podfile_properties)
|
||||
# Environment variable takes precedence
|
||||
return ENV['USE_CCACHE'] == '1' if ENV['USE_CCACHE']
|
||||
|
||||
# Fall back to Podfile properties
|
||||
podfile_properties['apple.ccacheEnabled'] == 'true'
|
||||
end
|
||||
|
||||
ENV['RCT_NEW_ARCH_ENABLED'] ||= '0' if podfile_properties['newArchEnabled'] == 'false'
|
||||
ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] ||= podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR']
|
||||
ENV['RCT_USE_RN_DEP'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
ENV['RCT_USE_PREBUILT_RNCORE'] ||= '1' if podfile_properties['ios.buildReactNativeFromSource'] != 'true' && podfile_properties['newArchEnabled'] != 'false'
|
||||
use_modular_headers!
|
||||
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'
|
||||
|
||||
prepare_react_native_project!
|
||||
@@ -21,7 +28,10 @@ target 'HIPMIBadungConnect' do
|
||||
config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"];
|
||||
else
|
||||
config_command = [
|
||||
'npx',
|
||||
'node',
|
||||
'--no-warnings',
|
||||
'--eval',
|
||||
'require(\'expo/bin/autolinking\')',
|
||||
'expo-modules-autolinking',
|
||||
'react-native-config',
|
||||
'--json',
|
||||
@@ -35,14 +45,6 @@ target 'HIPMIBadungConnect' do
|
||||
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
|
||||
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
|
||||
|
||||
# @generated begin pre_installer - expo prebuild (DO NOT MODIFY) sync-c8812095000d6054b846ce74840f0ffb540c2757
|
||||
pre_install do |installer|
|
||||
# @generated begin @rnmapbox/maps-pre_installer - expo prebuild (DO NOT MODIFY) sync-ea4905840bf9fcea0acc62e92aa2e784f9d760f8
|
||||
$RNMapboxMaps.pre_install(installer)
|
||||
# @generated end @rnmapbox/maps-pre_installer
|
||||
end
|
||||
# @generated end pre_installer
|
||||
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
|
||||
@@ -51,23 +53,12 @@ target 'HIPMIBadungConnect' do
|
||||
:privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false',
|
||||
)
|
||||
|
||||
pod 'Firebase'
|
||||
pod 'Firebase/Messaging'
|
||||
|
||||
# @generated begin post_installer - expo prebuild (DO NOT MODIFY) sync-4092f82b887b5b9edb84642c2a56984d69b9a403
|
||||
post_install do |installer|
|
||||
# @generated begin @rnmapbox/maps-post_installer - expo prebuild (DO NOT MODIFY) sync-c4e8f90e96f6b6c6ea9241dd7b52ab5f57f7bf36
|
||||
$RNMapboxMaps.post_install(installer)
|
||||
# @generated end @rnmapbox/maps-post_installer
|
||||
|
||||
# Fix all script phases with incorrect paths
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_phases.each do |phase|
|
||||
next unless phase.respond_to?(:shell_script)
|
||||
|
||||
# Fix duplicated path issue
|
||||
if phase.shell_script.include?('with-environment.sh')
|
||||
# Remove any existing path and use proper relative path
|
||||
phase.shell_script = phase.shell_script.gsub(
|
||||
%r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},
|
||||
'${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'
|
||||
@@ -75,15 +66,14 @@ target 'HIPMIBadungConnect' do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Standard React Native post install
|
||||
# @generated begin @maplibre/maplibre-react-native:post-install - expo prebuild (DO NOT MODIFY) sync-6e76c80af0d70c0003d06822dd59b7c729fca472
|
||||
$MLRN.post_install(installer)
|
||||
# @generated end @maplibre/maplibre-react-native:post-install
|
||||
react_native_post_install(
|
||||
installer,
|
||||
config[:reactNativePath],
|
||||
:mac_catalyst_enabled => false,
|
||||
:ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true',
|
||||
:ccache_enabled => ccache_enabled?(podfile_properties),
|
||||
)
|
||||
end
|
||||
# @generated end post_installer
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
990
ios/Podfile.lock
990
ios/Podfile.lock
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@maplibre/maplibre-react-native": "^10.4.2",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/datetimepicker": "8.4.4",
|
||||
"@react-native-firebase/app": "^23.7.0",
|
||||
@@ -21,7 +22,6 @@
|
||||
"@react-navigation/elements": "^2.3.8",
|
||||
"@react-navigation/native": "^7.1.6",
|
||||
"@react-navigation/native-stack": "^7.3.10",
|
||||
"@rnmapbox/maps": "^10.2.7",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"axios": "^1.11.0",
|
||||
@@ -48,6 +48,7 @@
|
||||
"expo-symbols": "~1.0.7",
|
||||
"expo-system-ui": "~6.0.7",
|
||||
"expo-web-browser": "~15.0.9",
|
||||
"libphonenumber-js": "^1.12.40",
|
||||
"lodash": "^4.17.21",
|
||||
"moti": "^0.30.0",
|
||||
"react": "19.1.0",
|
||||
|
||||
287
plugins/withCustomConfig.js
Normal file
287
plugins/withCustomConfig.js
Normal file
@@ -0,0 +1,287 @@
|
||||
const {
|
||||
withAppBuildGradle,
|
||||
withProjectBuildGradle,
|
||||
withInfoPlist,
|
||||
} = require("@expo/config-plugins");
|
||||
|
||||
const { withPodfile } = require("@expo/config-plugins");
|
||||
const { withAndroidManifest } = require("@expo/config-plugins");
|
||||
const { withDangerousMod } = require("@expo/config-plugins");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// 1. PROJECT-LEVEL build.gradle
|
||||
// Tambah: google-services classpath + Mapbox maven
|
||||
// ─────────────────────────────────────────
|
||||
const withCustomProjectBuildGradle = (config) => {
|
||||
return withProjectBuildGradle(config, (config) => {
|
||||
let contents = config.modResults.contents;
|
||||
|
||||
// Tambah google-services classpath jika belum ada
|
||||
if (!contents.includes("com.google.gms:google-services")) {
|
||||
contents = contents.replace(
|
||||
/classpath\('com\.android\.tools\.build:gradle'\)/,
|
||||
`classpath('com.android.tools.build:gradle')
|
||||
classpath 'com.google.gms:google-services:4.4.1'`,
|
||||
);
|
||||
}
|
||||
|
||||
// Tambah Mapbox maven repository jika belum ada
|
||||
if (!contents.includes("api.mapbox.com")) {
|
||||
contents = contents.replace(
|
||||
/allprojects\s*\{[\s\S]*?repositories\s*\{/,
|
||||
`allprojects {
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://api.mapbox.com/downloads/v2/releases/maven'
|
||||
def token = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv('RNMAPBOX_MAPS_DOWNLOAD_TOKEN')
|
||||
if (token) {
|
||||
authentication { basic(BasicAuthentication) }
|
||||
credentials {
|
||||
username = 'mapbox'
|
||||
password = token
|
||||
}
|
||||
}
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
config.modResults.contents = contents;
|
||||
return config;
|
||||
});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// 2. APP-LEVEL build.gradle
|
||||
// Tambah: buildConfigField + google-services plugin
|
||||
// ─────────────────────────────────────────
|
||||
const withCustomAppBuildGradle = (config) => {
|
||||
return withAppBuildGradle(config, (config) => {
|
||||
let contents = config.modResults.contents;
|
||||
|
||||
// Tambah Mapbox packagingOptions
|
||||
if (!contents.includes("rnmapbox/maps-libcpp")) {
|
||||
contents = contents.replace(
|
||||
/android\s*\{/,
|
||||
`android {
|
||||
// @generated begin @rnmapbox/maps-libcpp - expo prebuild (DO NOT MODIFY) sync-e24830a5a3e854b398227dfe9630aabfaa1cadd1
|
||||
packagingOptions {
|
||||
pickFirst 'lib/x86/libc++_shared.so'
|
||||
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
}
|
||||
// @generated end @rnmapbox/maps-libcpp`,
|
||||
);
|
||||
}
|
||||
|
||||
// Tambah buildConfigField REACT_NATIVE_RELEASE_LEVEL
|
||||
if (!contents.includes("REACT_NATIVE_RELEASE_LEVEL")) {
|
||||
contents = contents.replace(
|
||||
/defaultConfig\s*\{/,
|
||||
`defaultConfig {
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\\"${`$`}{findProperty('reactNativeReleaseLevel') ?: 'stable'}\\""`,
|
||||
);
|
||||
}
|
||||
|
||||
// Tambah apply plugin google-services di akhir file
|
||||
if (!contents.includes("com.google.gms.google-services")) {
|
||||
contents += `\napply plugin: 'com.google.gms.google-services'\n`;
|
||||
}
|
||||
|
||||
config.modResults.contents = contents;
|
||||
return config;
|
||||
});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// 3. Info.plist
|
||||
// Tambah: custom URL schemes + deskripsi Bahasa Indonesia
|
||||
// ─────────────────────────────────────────
|
||||
const withCustomInfoPlist = (config) => {
|
||||
return withInfoPlist(config, (config) => {
|
||||
const plist = config.modResults;
|
||||
|
||||
// Custom URL Schemes
|
||||
// Pastikan CFBundleURLTypes sudah ada, lalu tambahkan scheme custom
|
||||
if (!plist.CFBundleURLTypes) {
|
||||
plist.CFBundleURLTypes = [];
|
||||
}
|
||||
|
||||
const hasHipmiScheme = plist.CFBundleURLTypes.some((entry) =>
|
||||
entry.CFBundleURLSchemes?.includes("hipmimobile"),
|
||||
);
|
||||
|
||||
if (!hasHipmiScheme) {
|
||||
plist.CFBundleURLTypes.push({
|
||||
CFBundleURLSchemes: ["hipmimobile", "com.anonymous.hipmi-mobile"],
|
||||
});
|
||||
}
|
||||
|
||||
// NSLocationWhenInUseUsageDescription — Bahasa Indonesia
|
||||
plist.NSLocationWhenInUseUsageDescription =
|
||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.";
|
||||
|
||||
// NSPhotoLibraryUsageDescription — Bahasa Indonesia (panjang)
|
||||
plist.NSPhotoLibraryUsageDescription =
|
||||
"Untuk mengunggah dokumen dan media bisnis seperti foto profil, logo usaha, poster lowongan, atau bukti transaksi di berbagai fitur aplikasi: Profile, Portofolio, Job Vacancy, Investasi, dan Donasi.";
|
||||
|
||||
plist.NSFaceIDUsageDescription =
|
||||
"Allow $(PRODUCT_NAME) to access your Face ID biometric data.";
|
||||
|
||||
return config;
|
||||
});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// 4. Android Manifest
|
||||
// Tambah: backup rules untuk expo-secure-store
|
||||
// ─────────────────────────────────────────
|
||||
const withCustomManifest = (config) => {
|
||||
return withAndroidManifest(config, (config) => {
|
||||
const manifest = config.modResults.manifest;
|
||||
const application = manifest.application[0];
|
||||
|
||||
// Tambah atribut backup untuk expo-secure-store
|
||||
application.$["android:fullBackupContent"] =
|
||||
"@xml/secure_store_backup_rules";
|
||||
application.$["android:dataExtractionRules"] =
|
||||
"@xml/secure_store_data_extraction_rules";
|
||||
|
||||
// Tambah tools:replace pada meta-data notification color
|
||||
const metaDataList = application["meta-data"] || [];
|
||||
const notifColorMeta = metaDataList.find(
|
||||
(m) =>
|
||||
m.$["android:name"] ===
|
||||
"com.google.firebase.messaging.default_notification_color",
|
||||
);
|
||||
if (notifColorMeta) {
|
||||
notifColorMeta.$["tools:replace"] = "android:resource";
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// 5. Podfile
|
||||
// Tambah: use_modular_headers!
|
||||
// ─────────────────────────────────────────
|
||||
|
||||
const withCustomPodfile = (config) => {
|
||||
return withPodfile(config, (config) => {
|
||||
let contents = config.modResults.contents;
|
||||
|
||||
// Tambah use_modular_headers! jika belum ada
|
||||
if (!contents.includes("use_modular_headers!")) {
|
||||
contents = contents.replace(
|
||||
/platform :ios/,
|
||||
`use_modular_headers!\nplatform :ios`,
|
||||
);
|
||||
}
|
||||
|
||||
// Tambah Firebase pods jika belum ada
|
||||
if (!contents.includes("pod 'Firebase/Messaging'")) {
|
||||
contents = contents.replace(
|
||||
/use_react_native_pods\!/,
|
||||
`pod 'Firebase'\n pod 'Firebase/Messaging'\n\n use_react_native_pods!`,
|
||||
);
|
||||
}
|
||||
|
||||
// Tambah fix script with-environment.sh jika belum ada
|
||||
// Tambah fix script with-environment.sh jika belum ada
|
||||
if (!contents.includes("with-environment.sh")) {
|
||||
const fixScript = [
|
||||
"post_install do |installer|",
|
||||
" # Fix all script phases with incorrect paths",
|
||||
" installer.pods_project.targets.each do |target|",
|
||||
" target.build_phases.each do |phase|",
|
||||
" next unless phase.respond_to?(:shell_script)",
|
||||
" if phase.shell_script.include?('with-environment.sh')",
|
||||
" phase.shell_script = phase.shell_script.gsub(",
|
||||
" %r{(/.*?/node_modules/react-native)+/scripts/xcode/with-environment.sh},",
|
||||
" '${PODS_ROOT}/../../node_modules/react-native/scripts/xcode/with-environment.sh'",
|
||||
" )",
|
||||
" end",
|
||||
" end",
|
||||
" end",
|
||||
].join("\n");
|
||||
|
||||
contents = contents.replace(/post_install do \|installer\|/, fixScript);
|
||||
}
|
||||
|
||||
config.modResults.contents = contents;
|
||||
return config;
|
||||
});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// 6. Android XML Files
|
||||
// Tambah: secure_store_backup_rules.xml dan secure_store_data_extraction_rules.xml
|
||||
// ─────────────────────────────────────────
|
||||
|
||||
const withSecureStoreXml = (config) => {
|
||||
return withDangerousMod(config, [
|
||||
"android",
|
||||
(config) => {
|
||||
const xmlDir = path.join(
|
||||
config.modRequest.platformProjectRoot,
|
||||
"app/src/main/res/xml",
|
||||
);
|
||||
|
||||
// Buat folder jika belum ada
|
||||
if (!fs.existsSync(xmlDir)) {
|
||||
fs.mkdirSync(xmlDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Definisikan path variabel di sini ← INI yang kurang sebelumnya
|
||||
const backupRulesPath = path.join(
|
||||
xmlDir,
|
||||
"secure_store_backup_rules.xml",
|
||||
);
|
||||
const dataExtractionPath = path.join(
|
||||
xmlDir,
|
||||
"secure_store_data_extraction_rules.xml",
|
||||
);
|
||||
|
||||
// secure_store_backup_rules.xml
|
||||
fs.writeFileSync(
|
||||
backupRulesPath,
|
||||
`<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="SECURESTORE"/>
|
||||
</full-backup-content>`,
|
||||
);
|
||||
|
||||
// secure_store_data_extraction_rules.xml
|
||||
fs.writeFileSync(
|
||||
dataExtractionPath,
|
||||
`<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="sharedpref" path="SECURESTORE"/>
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="sharedpref" path="SECURESTORE"/>
|
||||
</device-transfer>
|
||||
</data-extraction-rules>`,
|
||||
);
|
||||
|
||||
return config;
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────
|
||||
// EXPORT
|
||||
// ─────────────────────────────────────────
|
||||
module.exports = (config) => {
|
||||
config = withCustomProjectBuildGradle(config);
|
||||
config = withCustomAppBuildGradle(config);
|
||||
config = withCustomManifest(config);
|
||||
config = withSecureStoreXml(config);
|
||||
config = withCustomInfoPlist(config);
|
||||
config = withCustomPodfile(config);
|
||||
return config;
|
||||
};
|
||||
@@ -4,10 +4,16 @@ import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import { useEffect } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
|
||||
export default function AdminNotificationBell() {
|
||||
const { unreadCount } = useNotificationStore();
|
||||
const { unreadCount, syncUnreadCount } = useNotificationStore();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Syncing unread count");
|
||||
syncUnreadCount();
|
||||
}, [syncUnreadCount]);
|
||||
|
||||
return (
|
||||
<View style={{ position: "relative" }}>
|
||||
|
||||
@@ -38,7 +38,7 @@ export default function Admin_BoxDonationStatus({
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom>Target</TextCustom>}
|
||||
label={<TextCustom>Target Dana</TextCustom>}
|
||||
value={
|
||||
<TextCustom>
|
||||
{item?.target
|
||||
|
||||
85
screens/Admin/Event/BoxEventDetail.tsx
Normal file
85
screens/Admin/Event/BoxEventDetail.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { BadgeCustom, BaseBox, Spacing, StackCustom, TextCustom } from "@/components";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { colorBadgeStatus } from "@/utils/colorBadge";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import _ from "lodash";
|
||||
|
||||
interface EventDetailData {
|
||||
Author?: {
|
||||
username?: string;
|
||||
};
|
||||
title?: string;
|
||||
lokasi?: string;
|
||||
EventMaster_TipeAcara?: {
|
||||
name?: string;
|
||||
};
|
||||
tanggal?: string;
|
||||
tanggalSelesai?: string;
|
||||
deskripsi?: string;
|
||||
catatan?: string;
|
||||
}
|
||||
|
||||
interface BoxEventDetailProps {
|
||||
data: EventDetailData | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export function BoxEventDetail({ data, status }: BoxEventDetailProps) {
|
||||
const listData = [
|
||||
{
|
||||
label: "Pembuat Event",
|
||||
value: data?.Author?.username || "-",
|
||||
},
|
||||
{
|
||||
label: "Judul Event",
|
||||
value: data?.title || "-",
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
value: data ? (
|
||||
<BadgeCustom color={colorBadgeStatus({ status })}>
|
||||
{_.startCase(status)}
|
||||
</BadgeCustom>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Lokasi",
|
||||
value: data?.lokasi || "-",
|
||||
},
|
||||
{
|
||||
label: "Tipe Acara",
|
||||
value: data?.EventMaster_TipeAcara?.name || "-",
|
||||
},
|
||||
{
|
||||
label: "Mulai Event",
|
||||
value: data?.tanggal ? dateTimeView({ date: data.tanggal }) : "-",
|
||||
},
|
||||
{
|
||||
label: "Event Berakhir",
|
||||
value: data?.tanggalSelesai
|
||||
? dateTimeView({ date: data.tanggalSelesai })
|
||||
: "-",
|
||||
},
|
||||
{
|
||||
label: "Deskripsi",
|
||||
value: data?.deskripsi || "-",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
{listData.map((item, i) => (
|
||||
<GridSpan_4_8
|
||||
key={i}
|
||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
||||
value={<TextCustom>{item.value}</TextCustom>}
|
||||
/>
|
||||
))}
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</BaseBox>
|
||||
);
|
||||
}
|
||||
37
screens/Admin/Event/EventDetailDrawer.tsx
Normal file
37
screens/Admin/Event/EventDetailDrawer.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { DrawerCustom, MenuDrawerDynamicGrid } from "@/components";
|
||||
import { IconList } from "@/components/_Icon/IconComponent";
|
||||
import { router } from "expo-router";
|
||||
|
||||
interface EventDetailDrawerProps {
|
||||
isVisible: boolean;
|
||||
onClose: () => void;
|
||||
eventId: string;
|
||||
}
|
||||
|
||||
export function EventDetailDrawer({
|
||||
isVisible,
|
||||
onClose,
|
||||
eventId,
|
||||
}: EventDetailDrawerProps) {
|
||||
return (
|
||||
<DrawerCustom
|
||||
isVisible={isVisible}
|
||||
closeDrawer={onClose}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Daftar Peserta",
|
||||
icon: <IconList />,
|
||||
path: `/admin/event/${eventId}/list-of-participants`,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
onClose();
|
||||
router.push(item.path as any);
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
);
|
||||
}
|
||||
96
screens/Admin/Event/EventDetailQRCode.tsx
Normal file
96
screens/Admin/Event/EventDetailQRCode.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import {
|
||||
BaseBox,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { BASE_URL } from "@/service/api-config";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { StyleSheet } from "react-native";
|
||||
import QRCode from "react-native-qrcode-svg";
|
||||
|
||||
interface EventDetailQRCodeProps {
|
||||
userId: string;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function EventDetailQRCode({
|
||||
userId,
|
||||
isLoading,
|
||||
}: EventDetailQRCodeProps) {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [useHttpsLink, setUseHttpsLink] = useState(true);
|
||||
|
||||
// HTTPS link untuk Universal Links (iOS) dan App Links (Android)
|
||||
// Ini akan membuka file .well-known di Next.js server Anda
|
||||
const httpsLink = `https://cld-dkr-hipmi-stg.wibudev.com/event/${id}/confirmation?userId=${userId}`;
|
||||
|
||||
// Custom scheme link untuk fallback atau testing tanpa universal links
|
||||
const deepLinkURL = `${BASE_URL}/event/${id}/confirmation?userId=${userId}`;
|
||||
|
||||
// Toggle antara HTTPS link dan custom scheme
|
||||
// const qrValue = useHttpsLink ? httpsLink : deepLinkURL;
|
||||
const qrValue = deepLinkURL;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<BaseBox>
|
||||
<StackCustom style={{ alignItems: "center" }}>
|
||||
<TextCustom bold>QR Code Event</TextCustom>
|
||||
{isLoading ? <LoaderCustom /> : <QRCode value={qrValue} size={200} />}
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
<TextCustom color="gray" align="center" size="small">
|
||||
{qrValue}
|
||||
</TextCustom>
|
||||
<Spacing />
|
||||
{/* <StackCustom direction="row" gap="sm">
|
||||
<ButtonCustom
|
||||
onPress={() => setUseHttpsLink(true)}
|
||||
backgroundColor={useHttpsLink ? MainColor.yellow : "transparent"}
|
||||
textColor={useHttpsLink ? MainColor.black : MainColor.yellow}
|
||||
style={[
|
||||
stylesButton.smallButton,
|
||||
useHttpsLink && stylesButton.border,
|
||||
]}
|
||||
>
|
||||
HTTPS
|
||||
</ButtonCustom>
|
||||
<ButtonCustom
|
||||
onPress={() => setUseHttpsLink(false)}
|
||||
backgroundColor={!useHttpsLink ? MainColor.yellow : "transparent"}
|
||||
textColor={!useHttpsLink ? MainColor.black : MainColor.yellow}
|
||||
style={[
|
||||
stylesButton.smallButton,
|
||||
!useHttpsLink && stylesButton.border,
|
||||
]}
|
||||
>
|
||||
Custom Scheme
|
||||
</ButtonCustom>
|
||||
</StackCustom> */}
|
||||
{/* <Spacing />
|
||||
<TextCustom color="gray" align="center" size={"small"}>
|
||||
{useHttpsLink
|
||||
? "✅ Testing Universal Links/App Links (butuh .well-known config)"
|
||||
: "🔧 Testing langsung (tanpa domain verification)"}
|
||||
</TextCustom> */}
|
||||
</BaseBox>
|
||||
);
|
||||
}
|
||||
|
||||
const stylesButton = StyleSheet.create({
|
||||
smallButton: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
minWidth: 140,
|
||||
},
|
||||
border: {
|
||||
borderWidth: 1,
|
||||
borderColor: MainColor.yellow,
|
||||
},
|
||||
});
|
||||
166
screens/Admin/Event/ScreenEventDetail.tsx
Normal file
166
screens/Admin/Event/ScreenEventDetail.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ActionIcon, AlertDefaultSystem, Spacing } from "@/components";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
|
||||
import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { BoxEventDetail } from "./BoxEventDetail";
|
||||
import { EventDetailDrawer } from "./EventDetailDrawer";
|
||||
import { EventDetailQRCode } from "./EventDetailQRCode";
|
||||
import { View } from "react-native";
|
||||
|
||||
export function Admin_ScreenEventDetail() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminEventById({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<ActionIcon
|
||||
icon={<IconDot size={ICON_SIZE_BUTTON} />}
|
||||
onPress={() => {
|
||||
setOpenDrawer(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
try {
|
||||
const response = await funUpdateStatusEvent({
|
||||
id: id as string,
|
||||
changeStatus: "publish",
|
||||
data: { catatan: "", senderId: user?.id as string },
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal mempublikasikan event",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Event berhasil dipublikasikan",
|
||||
});
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminBackButtonAntTitle
|
||||
title={`Detail Data`}
|
||||
rightComponent={
|
||||
status === "publish" || status === "history"
|
||||
? rightComponent
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
[status],
|
||||
);
|
||||
|
||||
const footerComponent = useMemo(() => {
|
||||
if (status === "review") {
|
||||
return (
|
||||
<AdminButtonReview
|
||||
onPublish={() => {
|
||||
AlertDefaultSystem({
|
||||
title: "Publish",
|
||||
message: "Apakah anda yakin ingin mempublikasikan data ini?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => handlerSubmit(),
|
||||
});
|
||||
}}
|
||||
onReject={() => {
|
||||
router.push(`/admin/event/${id}/reject-input?status=${status}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "reject") {
|
||||
return (
|
||||
<AdminButtonReject
|
||||
title="Tambah Catatan"
|
||||
onReject={() => {
|
||||
router.push(`/admin/event/${id}/reject-input?status=${status}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Spacing height={100} />;
|
||||
}, [status, id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
headerComponent={headerComponent}
|
||||
// footerComponent={
|
||||
// <View style={{ paddingInline: 8 }}>
|
||||
// {footerComponent}
|
||||
// </View>
|
||||
// }
|
||||
>
|
||||
<BoxEventDetail data={data} status={status as string} />
|
||||
|
||||
{data?.catatan && (status === "reject" || status === "review") && (
|
||||
<ReportBox text={data.catatan} />
|
||||
)}
|
||||
|
||||
{(status === "publish" || status === "history") && (
|
||||
<EventDetailQRCode userId={user?.id || ""} isLoading={loadData} />
|
||||
)}
|
||||
|
||||
{footerComponent}
|
||||
<Spacing />
|
||||
</NewWrapper>
|
||||
|
||||
<EventDetailDrawer
|
||||
isVisible={openDrawer}
|
||||
onClose={() => setOpenDrawer(false)}
|
||||
eventId={id as string}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
292
screens/Admin/Forum/ScreenForumDetailReportComment.tsx
Normal file
292
screens/Admin/Forum/ScreenForumDetailReportComment.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
AlertDefaultSystem,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
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 {
|
||||
apiAdminForumCommentById,
|
||||
apiAdminForumDeactivateComment,
|
||||
apiAdminForumListReportCommentById,
|
||||
} from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export function Admin_ScreenForumDetailReportComment() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawerPage, setOpenDrawerPage] = useState(false);
|
||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [selectedReport, setSelectedReport] = useState({
|
||||
id: "",
|
||||
username: "",
|
||||
kategori: "",
|
||||
keterangan: "",
|
||||
deskripsi: "",
|
||||
});
|
||||
|
||||
// Load data komentar saat screen fokus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadDataKomentar();
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
// Pagination untuk list report comment
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
const response = await apiAdminForumListReportCommentById({
|
||||
id: id as string,
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
}
|
||||
return { data: [] };
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
});
|
||||
|
||||
const onLoadDataKomentar = async () => {
|
||||
try {
|
||||
const response = await apiAdminForumCommentById({
|
||||
id: id as string,
|
||||
category: "get-one",
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Render item untuk daftar report comment
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<AdminBasicBox
|
||||
key={index}
|
||||
style={{ marginHorizontal: 5, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item?.id,
|
||||
username: item?.User?.username,
|
||||
kategori: item?.ForumMaster_KategoriReport?.title,
|
||||
keterangan: item?.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item?.deskripsi,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StackCustom gap={0}>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Pelapor</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Jenis Laporan</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={2}>
|
||||
{item
|
||||
? item?.ForumMaster_KategoriReport?.title
|
||||
? item?.ForumMaster_KategoriReport?.title
|
||||
: "Lainnya"
|
||||
: "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Header component dengan back button dan menu
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminBackButtonAntTitle
|
||||
title="Report Komentar"
|
||||
rightComponent={
|
||||
<ActionIcon
|
||||
icon={<IconDot size={16} color={MainColor.darkblue} />}
|
||||
onPress={() => setOpenDrawerPage(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Detail komentar component
|
||||
const ListHeader = useMemo(
|
||||
() => (
|
||||
<AdminBasicBox>
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Username</TextCustom>}
|
||||
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Komentar</TextCustom>}
|
||||
value={<TextCustom>{data?.komentar || "-"}</TextCustom>}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[data],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada report komentar",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
headerComponent={headerComponent}
|
||||
ListHeaderComponent={ListHeader}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Drawer untuk menu halaman (hapus komentar) */}
|
||||
<DrawerCustom
|
||||
isVisible={openDrawerPage}
|
||||
closeDrawer={() => setOpenDrawerPage(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
icon: <IconTrash />,
|
||||
label: "Hapus Komentar",
|
||||
value: "delete",
|
||||
path: "",
|
||||
color: MainColor.red,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
AlertDefaultSystem({
|
||||
title: "Hapus Komentar",
|
||||
message: "Apakah Anda yakin ingin menghapus komentar ini?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Hapus",
|
||||
onPressRight: async () => {
|
||||
const response = await apiAdminForumDeactivateComment({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Komentar gagal dihapus",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenDrawerPage(false);
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Komentar berhasil dihapus",
|
||||
});
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
|
||||
{/* Drawer untuk detail report comment */}
|
||||
<DrawerCustom
|
||||
isVisible={openDrawerAction}
|
||||
closeDrawer={() => setOpenDrawerAction(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<StackCustom>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Pelapor</TextCustom>}
|
||||
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
|
||||
{selectedReport?.kategori && (
|
||||
<>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Keterangan</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedReport?.deskripsi && (
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Deskripsi</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StackCustom>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
297
screens/Admin/Forum/ScreenForumDetailReportPosting.tsx
Normal file
297
screens/Admin/Forum/ScreenForumDetailReportPosting.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
AlertDefaultSystem,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
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 {
|
||||
apiAdminForumDeactivatePosting,
|
||||
apiAdminForumListReportPostingById,
|
||||
apiAdminForumPostingById,
|
||||
} from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export function Admin_ScreenForumDetailReportPosting() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawerPage, setOpenDrawerPage] = useState(false);
|
||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [selectedReport, setSelectedReport] = useState({
|
||||
id: "",
|
||||
username: "",
|
||||
kategori: "",
|
||||
keterangan: "",
|
||||
deskripsi: "",
|
||||
});
|
||||
|
||||
// Load data postingan saat screen fokus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadDataPosting();
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
// Pagination untuk list report
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
const response = await apiAdminForumListReportPostingById({
|
||||
id: id as string,
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
}
|
||||
return { data: [] };
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
});
|
||||
|
||||
const onLoadDataPosting = async () => {
|
||||
try {
|
||||
const response = await apiAdminForumPostingById({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Render item untuk daftar report
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<AdminBasicBox
|
||||
key={index}
|
||||
style={{ marginHorizontal: 5, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item?.id,
|
||||
username: item?.User?.username,
|
||||
kategori: item?.ForumMaster_KategoriReport?.title,
|
||||
keterangan: item?.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item?.deskripsi,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<StackCustom gap={0}>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Pelapor</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.User?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Jenis Laporan</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={2}>
|
||||
{item
|
||||
? item?.ForumMaster_KategoriReport?.title
|
||||
? item?.ForumMaster_KategoriReport?.title
|
||||
: "Lainnya"
|
||||
: "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Header component dengan detail postingan
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminBackButtonAntTitle
|
||||
title="Detail Report Posting"
|
||||
rightComponent={
|
||||
<ActionIcon
|
||||
icon={<IconDot size={16} color={MainColor.darkblue} />}
|
||||
onPress={() => setOpenDrawerPage(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Detail postingan component
|
||||
const ListHeader = useMemo(
|
||||
() => (
|
||||
<AdminBasicBox>
|
||||
<StackCustom gap={0}>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom bold>Username</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom>{data ? data?.Author?.username : "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom bold>Postingan</TextCustom>}
|
||||
rightItem={<TextCustom>{data ? data?.diskusi : "-"}</TextCustom>}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[data],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada report",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
headerComponent={headerComponent}
|
||||
ListHeaderComponent={ListHeader}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Drawer untuk menu halaman (hapus posting) */}
|
||||
<DrawerCustom
|
||||
isVisible={openDrawerPage}
|
||||
closeDrawer={() => setOpenDrawerPage(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
icon: <IconTrash />,
|
||||
label: "Hapus Posting",
|
||||
value: "delete",
|
||||
path: "",
|
||||
color: MainColor.red,
|
||||
},
|
||||
]}
|
||||
onPressItem={(item) => {
|
||||
AlertDefaultSystem({
|
||||
title: "Hapus Posting",
|
||||
message: "Apakah Anda yakin ingin menghapus posting ini?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Hapus",
|
||||
onPressRight: async () => {
|
||||
const response = await apiAdminForumDeactivatePosting({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Posting gagal dihapus",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setOpenDrawerPage(false);
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Posting berhasil dihapus",
|
||||
});
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawerAction}
|
||||
closeDrawer={() => setOpenDrawerAction(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<StackCustom>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Pelapor</TextCustom>}
|
||||
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
|
||||
/>
|
||||
|
||||
{selectedReport?.kategori && (
|
||||
<>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Keterangan</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedReport?.deskripsi && (
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Deskripsi</TextCustom>}
|
||||
value={
|
||||
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StackCustom>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
105
screens/Admin/Forum/ScreenForumListComment.tsx
Normal file
105
screens/Admin/Forum/ScreenForumListComment.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { StackCustom, TextCustom } from "@/components";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
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 { apiAdminForumCommentById } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
export function Admin_ScreenForumListComment() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
||||
const [selectedComment, setSelectedComment] = useState({
|
||||
id: "",
|
||||
komentar: "",
|
||||
});
|
||||
|
||||
// Pagination untuk list comment
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
const response = await apiAdminForumCommentById({
|
||||
id: id as string,
|
||||
category: "get-all",
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
}
|
||||
return { data: [] };
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
});
|
||||
|
||||
// Render item untuk daftar komentar
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<AdminBasicBox
|
||||
key={index}
|
||||
style={{ marginHorizontal: 5, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
router.push(`/admin/forum/${item.id}/list-report-comment`);
|
||||
}}
|
||||
>
|
||||
<StackCustom gap={"md"}>
|
||||
<TextCustom truncate={1}>
|
||||
Report : {item?.countReport || 0}
|
||||
</TextCustom>
|
||||
<Divider />
|
||||
<TextCustom truncate={2}>{item?.komentar || "-"}</TextCustom>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Header component dengan back button
|
||||
const headerComponent = useMemo(
|
||||
() => <AdminBackButtonAntTitle title="Daftar Komentar" />,
|
||||
[],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada komentar",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
headerComponent={headerComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
133
screens/Admin/Forum/ScreenForumPosting.tsx
Normal file
133
screens/Admin/Forum/ScreenForumPosting.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
SearchInput, StackCustom,
|
||||
TextCustom
|
||||
} from "@/components";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
|
||||
export function Admin_ScreenForumPosting() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Gunakan hook pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiAdminForum({
|
||||
category: "posting",
|
||||
search: searchQuery || "",
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
} else {
|
||||
return { data: [] };
|
||||
}
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
searchQuery: search,
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
// Komponen search input
|
||||
const searchComponent = useMemo(
|
||||
() => (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari postingan"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
),
|
||||
[search],
|
||||
);
|
||||
|
||||
// Header component
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminComp_BoxTitle
|
||||
title={"Forum Posting"}
|
||||
rightComponent={searchComponent}
|
||||
/>
|
||||
),
|
||||
[searchComponent],
|
||||
);
|
||||
|
||||
// Render item untuk daftar posting
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<AdminBasicBox
|
||||
style={{ marginHorizontal: 5, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
router.push(`/admin/forum/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<StackCustom gap={0}>
|
||||
<View style={{ paddingBlock: 8 }}>
|
||||
<TextCustom size={"large"} bold truncate={2}>
|
||||
{item?.diskusi || "-"}
|
||||
</TextCustom>
|
||||
</View>
|
||||
<Divider />
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom>Komentar</TextCustom>}
|
||||
value={
|
||||
<TextCustom truncate={1}>{item?.komentar || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom>Report</TextCustom>}
|
||||
value={
|
||||
<TextCustom truncate={1}>{item?.reportPosting || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Belum ada data posting",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||
headerComponent={headerComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
140
screens/Admin/Forum/ScreenForumReportComment.tsx
Normal file
140
screens/Admin/Forum/ScreenForumReportComment.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { SearchInput, StackCustom, TextCustom } from "@/components";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export function Admin_ScreenForumReportComment() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Gunakan hook pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiAdminForum({
|
||||
category: "report_comment",
|
||||
search: searchQuery || "",
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return { data: response.data };
|
||||
} else {
|
||||
return { data: [] };
|
||||
}
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
searchQuery: search,
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, []),
|
||||
);
|
||||
|
||||
// Komponen search input
|
||||
const searchComponent = useMemo(
|
||||
() => (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari Komentar"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
),
|
||||
[search],
|
||||
);
|
||||
|
||||
// Header component dengan box title
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminComp_BoxTitle
|
||||
title="Report Komentar"
|
||||
rightComponent={searchComponent}
|
||||
/>
|
||||
),
|
||||
[searchComponent],
|
||||
);
|
||||
|
||||
// Render item untuk daftar report comment
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<AdminBasicBox
|
||||
key={index}
|
||||
style={{ marginHorizontal: 5, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<StackCustom gap={0}>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Jumlah Report</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.count || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Komentar</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.Forum_Komentar?.komentar || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Belum ada data report komentar",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||
headerComponent={headerComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor="#E1B525"
|
||||
colors={["#E1B525"]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
141
screens/Admin/Forum/ScreenForumReportPosting.tsx
Normal file
141
screens/Admin/Forum/ScreenForumReportPosting.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { SearchInput, StackCustom, TextCustom } from "@/components";
|
||||
import AdminBasicBox from "@/components/_ShareComponent/Admin/AdminBasicBox";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
export function Admin_ScreenForumReportPosting() {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Gunakan hook pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
const response = await apiAdminForum({
|
||||
category: "report_posting",
|
||||
search: searchQuery || "",
|
||||
page: String(page),
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
|
||||
return { data: response.data };
|
||||
} else {
|
||||
return { data: [] };
|
||||
}
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
searchQuery: search,
|
||||
dependencies: [],
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, []),
|
||||
);
|
||||
|
||||
// Komponen search input
|
||||
const searchComponent = useMemo(
|
||||
() => (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari Postingan"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
),
|
||||
[search],
|
||||
);
|
||||
|
||||
// Box title component
|
||||
const headerComponent = useMemo(
|
||||
() => (
|
||||
<AdminComp_BoxTitle
|
||||
title="Report Posting"
|
||||
rightComponent={searchComponent}
|
||||
/>
|
||||
),
|
||||
[searchComponent],
|
||||
);
|
||||
|
||||
// Render item untuk daftar report posting
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: { item: any; index: number }) => (
|
||||
<AdminBasicBox
|
||||
key={index}
|
||||
style={{ marginHorizontal: 5, marginVertical: 5 }}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<StackCustom gap={0}>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Jumlah Report</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.count|| "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<GridTwoView
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom>Postingan</TextCustom>}
|
||||
rightItem={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.Forum_Posting?.diskusi || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</AdminBasicBox>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
// Buat komponen-komponen pagination
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Belum ada data report posting",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item: any) => item.id?.toString() || `fallback-${item.id}`}
|
||||
headerComponent={headerComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
onEndReached={pagination.loadMore}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user