Compare commits
33 Commits
loaddata/1
...
app-header
| Author | SHA1 | Date | |
|---|---|---|---|
| f68deab8c0 | |||
| 37d2fbe48a | |||
| 57c9215771 | |||
| 4efdbd3c7b | |||
| ad32eb6fe6 | |||
| a5026cc285 | |||
| 836ef709d2 | |||
| 3bbee15c3a | |||
| ad7dbaf162 | |||
| 9c94ec0454 | |||
| 4c63485a5b | |||
| f5d09a2906 | |||
| 67070bb2f1 | |||
| fb19ec60b2 | |||
| e8f5c5b174 | |||
| 74a4d88277 | |||
| 2ad93a26a8 | |||
| 768b0caa9e | |||
| 208b0ce813 | |||
| 66e6aebf41 | |||
| 32a42d1b60 | |||
| 107d4312e1 | |||
| 4862975402 | |||
| f284e2ec02 | |||
| 1d61ad51e5 | |||
| 76845b71b4 | |||
| 97e1f50660 | |||
| 1cbe4ab330 | |||
| 42fa80c228 | |||
| fb697366fe | |||
| 6d71c3a86f | |||
| e030b8f486 | |||
| 5c931b069c |
542
QWEN.md
542
QWEN.md
@@ -2,53 +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
|
||||
├── 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)
|
||||
- **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
|
||||
|
||||
@@ -60,110 +75,441 @@ 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
|
||||
bun run lint
|
||||
```
|
||||
|
||||
### 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
|
||||
### Build Commands
|
||||
|
||||
### EAS Build Configuration
|
||||
The project uses Expo Application Services (EAS) for building and deploying:
|
||||
- Development builds with development client
|
||||
- Preview builds for internal distribution
|
||||
- Production builds for app stores
|
||||
#### EAS Build (Production)
|
||||
```bash
|
||||
# Production build (App Store / Play Store)
|
||||
eas build --profile production
|
||||
|
||||
## Features and Functionality
|
||||
# Preview build (Internal distribution)
|
||||
eas build --profile preview
|
||||
|
||||
The application appears to include several key modules:
|
||||
- **Authentication**: Login, registration, and verification flows
|
||||
- **Admin Panel**: Administrative functions
|
||||
- **Collaboration**: Tools for member collaboration
|
||||
- **Events**: Event management and calendar
|
||||
- **Forum**: Discussion forums
|
||||
- **Maps**: Location-based services with Mapbox integration
|
||||
- **Donations**: Donation functionality
|
||||
- **Job Board**: Employment opportunities
|
||||
- **Investment**: Investment-related features
|
||||
- **Voting**: Voting systems
|
||||
- **Portfolio**: Member portfolio showcase
|
||||
- **Notifications**: Push notifications via Firebase
|
||||
# Development build (Development client)
|
||||
eas build --profile development
|
||||
```
|
||||
|
||||
#### Local Native Builds
|
||||
```bash
|
||||
# Generate native folders (iOS & Android)
|
||||
npx expo prebuild
|
||||
|
||||
# iOS specific
|
||||
bunx expo prebuild --platform ios
|
||||
open ios/HIPMIBadungConnect.xcworkspace
|
||||
|
||||
# Android specific
|
||||
bunx expo prebuild --platform android
|
||||
```
|
||||
|
||||
#### Version Management
|
||||
```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
|
||||
```bash
|
||||
# List connected devices
|
||||
adb devices
|
||||
|
||||
# Install APK to device/emulator
|
||||
adb install android/app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
# Install to specific device
|
||||
adb -s <device_id> install android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env` file in the project root with:
|
||||
|
||||
```env
|
||||
API_BASE_URL=https://your-api-base-url.com
|
||||
BASE_URL=https://your-app-url.com
|
||||
DEEP_LINK_URL=hipmimobile://
|
||||
```
|
||||
|
||||
These are loaded in `app.config.js` and accessible via `Constants.expoConfig.extra`.
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### 1. Separation of Concerns
|
||||
|
||||
**Route Files** (`app/`) should be minimal (max 5 lines):
|
||||
```typescript
|
||||
import { Admin_ScreenXXX } from "@/screens/Admin/XXX/ScreenXXX";
|
||||
|
||||
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
|
||||
- File-based routing with Expo Router
|
||||
- Consistent naming conventions using camelCase for variables and PascalCase for components
|
||||
- **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.)
|
||||
|
||||
### Component Structure
|
||||
```typescript
|
||||
// 1. Imports (grouped)
|
||||
import { ... } from "@/components";
|
||||
import { ... } from "@/hooks";
|
||||
import { ... } from "@/service";
|
||||
|
||||
// 2. Types/Interfaces
|
||||
interface Props { ... }
|
||||
|
||||
// 3. Main Component
|
||||
export function ComponentName() {
|
||||
// State
|
||||
// Hooks
|
||||
// Functions
|
||||
// Render
|
||||
}
|
||||
```
|
||||
|
||||
### Testing
|
||||
- Linting is configured with ESLint
|
||||
- Standard Expo linting configuration is used
|
||||
- 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
|
||||
### 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
|
||||
### 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**: With tablet support and proper permissions
|
||||
- **Android**: With adaptive icons and intent filters for 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
|
||||
```
|
||||
|
||||
### iOS Configuration
|
||||
- Bundle identifier: `com.anonymous.hipmi-mobile`
|
||||
- Supports tablets
|
||||
- Google Services integration
|
||||
- Location permission handling
|
||||
- Associated domains for deep linking
|
||||
### 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;
|
||||
});
|
||||
```
|
||||
|
||||
### Android Configuration
|
||||
- Package name: `com.bip.hipmimobileapp`
|
||||
- Adaptive icons
|
||||
- Edge-to-edge display enabled
|
||||
- Intent filters for HTTPS deep linking
|
||||
- Google Services integration
|
||||
## Platform Configuration
|
||||
|
||||
### Maps Integration
|
||||
The application uses Mapbox for mapping functionality with the `@rnmapbox/maps` plugin.
|
||||
### 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
|
||||
|
||||
### Push Notifications
|
||||
Firebase Cloud Messaging is integrated for push notifications with proper configuration for both iOS and Android platforms.
|
||||
### 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://`
|
||||
- HTTPS: `cld-dkr-hipmi-stg.wibudev.com`
|
||||
- Configured for both platforms
|
||||
|
||||
### Camera
|
||||
- Camera and microphone permissions
|
||||
- QR code generation support
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Admin Screen
|
||||
|
||||
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 ... />;
|
||||
}
|
||||
```
|
||||
|
||||
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 1
|
||||
versionName "1.0.2"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
|
||||
@@ -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="cld-dkr-hipmi-stg.wibudev.com" android:pathPrefix="/"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
|
||||
@@ -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",
|
||||
@@ -20,8 +31,10 @@ export default {
|
||||
NSLocationWhenInUseUsageDescription:
|
||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||
},
|
||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||
buildNumber: "20",
|
||||
associatedDomains: [
|
||||
"applinks:cld-dkr-hipmi-stg.wibudev.com",
|
||||
],
|
||||
buildNumber: "5",
|
||||
},
|
||||
|
||||
android: {
|
||||
@@ -32,7 +45,7 @@ export default {
|
||||
},
|
||||
edgeToEdgeEnabled: true,
|
||||
package: "com.bip.hipmimobileapp",
|
||||
versionCode: 4,
|
||||
versionCode: 1,
|
||||
// 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: "cld-dkr-hipmi-stg.wibudev.com",
|
||||
pathPrefix: "/",
|
||||
},
|
||||
],
|
||||
@@ -77,7 +90,6 @@ export default {
|
||||
},
|
||||
],
|
||||
"expo-font",
|
||||
"@rnmapbox/maps",
|
||||
"@react-native-firebase/app",
|
||||
[
|
||||
"expo-notifications",
|
||||
@@ -87,6 +99,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,92 +1,153 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { StackCustom, ViewWrapper } from "@/components";
|
||||
import { BasicWrapper, 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");
|
||||
return <Redirect href={`/waiting-room`} />;
|
||||
console.warn("User is not active");
|
||||
return (
|
||||
<BasicWrapper>
|
||||
<Redirect href={`/waiting-room`} />
|
||||
</BasicWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (data && data?.Profile === null) {
|
||||
console.log("Profile is null");
|
||||
return <Redirect href={`/profile/create`} />;
|
||||
console.warn("Profile is null");
|
||||
return (
|
||||
<BasicWrapper>
|
||||
<Redirect href={`/profile/create`} />
|
||||
</BasicWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// if (data && data?.masterUserRoleId !== "1") {
|
||||
// console.log("User is not admin");
|
||||
// return (
|
||||
// <BasicWrapper>
|
||||
// <Redirect href={`/admin/dashboard`} />
|
||||
// </BasicWrapper>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
@@ -97,24 +158,51 @@ 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,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<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={200} />
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
|
||||
@@ -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 { Admin_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 <Admin_ScreenPortofolioCreate />;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
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";
|
||||
@@ -49,7 +51,7 @@ export default function Portofolio() {
|
||||
useCallback(() => {
|
||||
onLoadData(id as string);
|
||||
onLoadUserByToken();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
async function onLoadData(id: string) {
|
||||
@@ -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,36 +148,38 @@ 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
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<FontAwesome
|
||||
name="building-o"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
|
||||
rightItem={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
|
||||
/>
|
||||
|
||||
<GridTwoView
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<Ionicons
|
||||
name="list-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={
|
||||
rightItem={
|
||||
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
|
||||
}
|
||||
/>
|
||||
@@ -172,26 +187,26 @@ export default function Portofolio() {
|
||||
<GridTwoView
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<Ionicons
|
||||
name="call-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={<TextCustom>{data?.tlpn}</TextCustom>}
|
||||
rightItem={<TextCustom>{data?.tlpn}</TextCustom>}
|
||||
/>
|
||||
<GridTwoView
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<Ionicons
|
||||
name="location-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={<TextCustom>{data?.alamatKantor}</TextCustom>}
|
||||
rightItem={<TextCustom>{data?.alamatKantor}</TextCustom>}
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
|
||||
@@ -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,8 +6,11 @@ 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";
|
||||
import NavbarMenu_V3 from "@/components/Drawer/NavbarMenu_V3";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
ICON_SIZE_MEDIUM,
|
||||
@@ -15,11 +18,16 @@ 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,
|
||||
superAdminListMenu,
|
||||
} from "@/screens/Admin/listPageAdmin";
|
||||
import {
|
||||
adminListMenu_V2,
|
||||
superAdminListMenu_V2,
|
||||
} from "@/screens/Admin/listPageAdmin_V2";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack } from "expo-router";
|
||||
@@ -28,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
|
||||
@@ -45,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()}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -148,6 +185,24 @@ export default function AdminLayout() {
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/>
|
||||
|
||||
{/* <NavbarMenu_V2
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu_V2
|
||||
: superAdminListMenu_V2
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/> */}
|
||||
|
||||
{/* <NavbarMenu_V3
|
||||
items={
|
||||
user?.masterUserRoleId === "2"
|
||||
? adminListMenu_V2
|
||||
: superAdminListMenu_V2
|
||||
}
|
||||
onClose={() => setOpenDrawerNavbar(false)}
|
||||
/> */}
|
||||
</StackCustom>
|
||||
</DrawerAdmin>
|
||||
|
||||
@@ -198,7 +253,7 @@ export default function AdminLayout() {
|
||||
// size={ICON_SIZE_SMALL}
|
||||
// color={MainColor.white}
|
||||
// />
|
||||
<AdminNotificationBell/>
|
||||
<AdminNotificationBell />
|
||||
),
|
||||
path: "/admin/notification",
|
||||
},
|
||||
|
||||
@@ -1,135 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
BaseBox,
|
||||
CenterCustom,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenBusinessFieldDetail } from "@/screens/Admin/App-Information/ScreenBusinessFieldDetail";
|
||||
|
||||
export default function AdminAppInformation_BusinessFieldDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadDetail();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadDetail = async () => {
|
||||
try {
|
||||
const response = await apiAdminMasterBusinessFieldById({
|
||||
id: id as string,
|
||||
category: "all",
|
||||
});
|
||||
|
||||
console.log("Response >>", JSON.stringify(response, null, 2));
|
||||
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setData(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<StackCustom>
|
||||
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
|
||||
|
||||
{!data ? (
|
||||
<LoaderCustom />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold>Nama Bidang</TextCustom>
|
||||
<Spacing height={5} />
|
||||
<BaseBox>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextCustom bold>
|
||||
Status: {data?.bidang?.active ? "Aktif" : "Tidak Aktif"}
|
||||
</TextCustom>
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<TextCustom bold size={"large"}>
|
||||
{data?.bidang?.name}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={<IconEdit size={16} color={MainColor.black} />}
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${id}/bidang-update`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
{/* <Divider /> */}
|
||||
<Spacing height={5} />
|
||||
|
||||
<TextCustom bold>Sub Bidang Bisnis</TextCustom>
|
||||
<Spacing height={5} />
|
||||
|
||||
{data?.subBidang?.map((item: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
<StackCustom gap={0}>
|
||||
<TextCustom bold>
|
||||
Status: {item?.isActive ? "Aktif" : "Tidak Aktif"}
|
||||
</TextCustom>
|
||||
|
||||
<GridSpan_NewComponent
|
||||
span1={10}
|
||||
span2={2}
|
||||
text1={
|
||||
<TextCustom bold size={"large"}>
|
||||
{item.name}
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconEdit size={16} color={MainColor.black} />
|
||||
}
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
/>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
))}
|
||||
</StackCustom>
|
||||
)}
|
||||
|
||||
{/* <TextCustom>{JSON.stringify(data, null, 2)}</TextCustom> */}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenBusinessFieldDetail />;
|
||||
}
|
||||
|
||||
@@ -1,86 +1,5 @@
|
||||
import { ScrollableCustom, StackCustom, ViewWrapper } from "@/components";
|
||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminAppInformation_BusinessFieldSection from "@/screens/Admin/App-Information/BusinessFieldSection";
|
||||
import AdminAppInformation_Bank from "@/screens/Admin/App-Information/InformationBankSection";
|
||||
import AdminAppInformation_StickerSection from "@/screens/Admin/App-Information/StickerSection";
|
||||
import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import { Admin_ScreenAppInformation } from "@/screens/Admin/App-Information/ScreenAppInformation";
|
||||
|
||||
export default function AdminInformation() {
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("bank");
|
||||
const [activePage, setActivePage] = useState<string>("Informasi Bank");
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
setActivePage(item.label);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
const scrollComponent = (
|
||||
<StackCustom>
|
||||
<ScrollableCustom
|
||||
data={listPage}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as any}
|
||||
/>
|
||||
</StackCustom>
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
switch (activeCategory) {
|
||||
case "bank":
|
||||
return <AdminAppInformation_Bank />;
|
||||
case "business":
|
||||
return <AdminAppInformation_BusinessFieldSection />;
|
||||
case "sticker":
|
||||
return <AdminAppInformation_StickerSection />;
|
||||
default:
|
||||
return <AdminAppInformation_Bank />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={scrollComponent}>
|
||||
<AdminComp_BoxTitle
|
||||
title={activePage}
|
||||
rightComponent={
|
||||
<AdminActionIconPlus
|
||||
onPress={() => {
|
||||
if (activeCategory === "bank") {
|
||||
router.push("/admin/app-information/information-bank/create");
|
||||
} else if (activeCategory === "business") {
|
||||
router.push("/admin/app-information/business-field/create");
|
||||
} else if (activeCategory === "sticker") {
|
||||
Alert.alert("Coming Soon", "Next Update");
|
||||
// router.push("/admin/app-information/sticker/create");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{renderContent()}
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenAppInformation />;
|
||||
}
|
||||
|
||||
const listPage = [
|
||||
{
|
||||
id: "1",
|
||||
label: "Informasi Bank",
|
||||
value: "bank",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "Bidang & Sub Bidang",
|
||||
value: "business",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
label: "Stiker",
|
||||
value: "sticker",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
Grid,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
Grid,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
@@ -22,7 +22,7 @@ export default function AdminCollaborationPublish() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
handlerLoadData();
|
||||
}, [status])
|
||||
}, [status]),
|
||||
);
|
||||
|
||||
const handlerLoadData = async () => {
|
||||
@@ -78,16 +78,16 @@ export default function AdminCollaborationPublish() {
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
{data?.report && (
|
||||
<BaseBox>
|
||||
<GridTwoView
|
||||
spanLeft={4}
|
||||
spanRight={8}
|
||||
leftIcon={<TextCustom bold>Catatan report</TextCustom>}
|
||||
rightIcon={<TextCustom>{data?.report}</TextCustom>}
|
||||
/>
|
||||
</BaseBox>
|
||||
)}
|
||||
{data?.report && (
|
||||
<BaseBox>
|
||||
<GridTwoView
|
||||
spanLeft={4}
|
||||
spanRight={8}
|
||||
leftItem={<TextCustom bold>Catatan report</TextCustom>}
|
||||
rightItem={<TextCustom>{data?.report}</TextCustom>}
|
||||
/>
|
||||
</BaseBox>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -42,19 +42,19 @@ export default function AdminDonationDetailDisbursementOfFunds() {
|
||||
const listData = [
|
||||
{
|
||||
label: "Nominal",
|
||||
value: `Rp ${(data && formatCurrencyDisplay(data?.nominalCair)) || 0}`,
|
||||
value: `Rp ${data ? formatCurrencyDisplay(data?.nominalCair) : 0}`,
|
||||
},
|
||||
{
|
||||
label: "Tanggal",
|
||||
value: dateTimeView({ date: data?.createdAt }),
|
||||
value: data ? dateTimeView({ date: data?.createdAt }) : "-",
|
||||
},
|
||||
{
|
||||
label: "Judul",
|
||||
value: (data && data?.title) || "-",
|
||||
value: data ? data?.title : "-",
|
||||
},
|
||||
{
|
||||
label: "Deskripsi",
|
||||
value: (data && data?.deskripsi) || "-",
|
||||
value: data ? data?.deskripsi : "-",
|
||||
},
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -1,126 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
CenterCustom,
|
||||
Divider,
|
||||
LoaderCustom,
|
||||
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 { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import dayjs from "dayjs";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import React, { useCallback } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Admin_ScreenDonationListDisbursementOfFunds } from "@/screens/Admin/Donation/ScreenDonationListDisbursementOfFunds";
|
||||
|
||||
export default function AdminDonasiListOfDisbursementOfFunds() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = React.useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminDonationDisbursementOfFundsListById({
|
||||
id: id as string,
|
||||
category: "get-all",
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<AdminBackButtonAntTitle title="Daftar Pencairan Dana" />
|
||||
}
|
||||
>
|
||||
<GridViewCustomSpan
|
||||
span1={3}
|
||||
span2={5}
|
||||
span3={4}
|
||||
component1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom bold align="center">
|
||||
Tanggal
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<TextCustom bold align="center">
|
||||
Nominal
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<StackCustom>
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item, index) => (
|
||||
<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/donation/${item?.id}/detail-disbursement-of-funds`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom align="center" truncate>
|
||||
{dayjs(item?.createdAt).format("DD-MM-YYYY")}
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<TextCustom align="center" truncate>
|
||||
Rp. {formatCurrencyDisplay(item?.nominalCair)}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenDonationListDisbursementOfFunds />;
|
||||
}
|
||||
|
||||
@@ -1,186 +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 { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminDonationListOfDonatur } from "@/service/api-admin/api-admin-donation";
|
||||
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_ScreenDonationListOfDonatur } from "@/screens/Admin/Donation/ScreenDonationListOfDonatur";
|
||||
|
||||
export default function AdminDonasiListOfDonatur() {
|
||||
const { id } = useLocalSearchParams();
|
||||
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
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id, selectValue])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminDonationListOfDonatur({
|
||||
id: id as string,
|
||||
status: selectedStatus as any,
|
||||
});
|
||||
// console.log("[LIST OF DONATUR]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setListData([]);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onLoadMaster();
|
||||
}, []);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
try {
|
||||
const response = await apiMasterTransaction();
|
||||
|
||||
if (response.success) {
|
||||
setMaster(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setMaster([]);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<AdminBackButtonAntTitle newComponent={searchComponent} />
|
||||
}
|
||||
>
|
||||
<StackCustom>
|
||||
<GridViewCustomSpan
|
||||
span1={3}
|
||||
span2={5}
|
||||
span3={4}
|
||||
component1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom bold align="center">
|
||||
Donatur
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<TextCustom bold align="center">
|
||||
Status
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
<StackCustom>
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
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/donation/${item?.id}/${_.lowerCase(
|
||||
item?.DonasiMaster_StatusInvoice?.name
|
||||
)}/transaction-detail`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom bold align="center" truncate>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<BadgeCustom
|
||||
style={{ alignSelf: "center" }}
|
||||
color={colorBadgeTransaction({
|
||||
status: item?.DonasiMaster_StatusInvoice?.name,
|
||||
})}
|
||||
>
|
||||
{item?.DonasiMaster_StatusInvoice?.name}
|
||||
</BadgeCustom>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenDonationListOfDonatur />;
|
||||
}
|
||||
|
||||
@@ -1,117 +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 { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminDonation } from "@/service/api-admin/api-admin-donation";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenDonationStatus } from "@/screens/Admin/Donation/ScreenDonationStatus";
|
||||
|
||||
export default function AdminDonationStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
console.log("[STATUS]", status);
|
||||
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const [loadData, setLoadData] = useState<boolean>(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [status, search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminDonation({
|
||||
category: status as "publish" | "review" | "reject",
|
||||
search,
|
||||
});
|
||||
|
||||
console.log("[RES]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
setData([]);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari"
|
||||
value={search}
|
||||
onChangeText={(value) => setSearch(value)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Donasi" />}>
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminComp_BoxTitle
|
||||
title={`${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Username"
|
||||
title3="Judul Donasi"
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(data) ? (
|
||||
<TextCustom align="center" size="small" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
data?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/donation/${item.id}/${status}`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
|
||||
value3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenDonationStatus />;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ export default function AdminDonationCategoryUpdate() {
|
||||
const response = await apiAdminMasterDonationCategoryById({
|
||||
id: id as any,
|
||||
});
|
||||
console.log(JSON.stringify(response.data, null, 2));
|
||||
|
||||
setData(response.data);
|
||||
};
|
||||
@@ -44,10 +43,9 @@ export default function AdminDonationCategoryUpdate() {
|
||||
id: id as any,
|
||||
data: data,
|
||||
});
|
||||
console.log(JSON.stringify(response.data, null, 2));
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log("Error update category:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -1,135 +1,5 @@
|
||||
import {
|
||||
BadgeCustom,
|
||||
CenterCustom,
|
||||
ClickableCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||
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 { apiAdminMasterDonationCategory } from "@/service/api-admin/api-master-admin";
|
||||
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenDonationCategory } from "@/screens/Admin/Donation/ScreenDonationCategory";
|
||||
|
||||
export default function AdminDonationCategory() {
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchMaster();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const fetchMaster = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiAdminMasterDonationCategory();
|
||||
if (response.success) {
|
||||
console.log(JSON.stringify(response.data, null, 2));
|
||||
setListData(response.data);
|
||||
} else {
|
||||
setListData([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[Error]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
await fetchMaster();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
headerComponent={<AdminTitlePage title="Donasi" />}
|
||||
>
|
||||
<AdminComp_BoxTitle
|
||||
title="Kategori"
|
||||
rightComponent={
|
||||
<AdminActionIconPlus
|
||||
onPress={() => {
|
||||
router.push(`/admin/donation/category-create`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<View>
|
||||
<GridSpan_4_8
|
||||
label={<TextCustom bold>Status</TextCustom>}
|
||||
value={<TextCustom bold>Kategori</TextCustom>}
|
||||
/>
|
||||
{/* <Grid>
|
||||
<Grid.Col style={{ paddingLeft: 10 }} span={4}>
|
||||
<TextCustom bold>Status</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom bold>Kategori</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid> */}
|
||||
|
||||
<Divider />
|
||||
<Spacing />
|
||||
|
||||
<StackCustom>
|
||||
{listData.map((item, index) => (
|
||||
<ClickableCustom
|
||||
onPress={() => {
|
||||
router.push(`/admin/donation/category-update?id=${item.id}`);
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
<GridSpan_4_8
|
||||
label={
|
||||
<CenterCustom>
|
||||
<BadgeCustom
|
||||
color={colorActivationForBadge({
|
||||
status: item.active,
|
||||
})}
|
||||
>
|
||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
||||
</BadgeCustom>
|
||||
</CenterCustom>
|
||||
}
|
||||
value={<TextCustom>{item.name}</TextCustom>}
|
||||
/>
|
||||
{/* <Grid containerStyle={{ paddingBottom: 10 }}>
|
||||
<Grid.Col span={4} style={{ paddingLeft: 10 }}>
|
||||
<CenterCustom>
|
||||
<BadgeCustom
|
||||
color={item.active ? MainColor.green : MainColor.red}
|
||||
>
|
||||
{item.active ? "Aktif" : "Tidak Aktif"}
|
||||
</BadgeCustom>
|
||||
</CenterCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom bold>{item.name}</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid> */}
|
||||
<Divider />
|
||||
</ClickableCustom>
|
||||
))}
|
||||
</StackCustom>
|
||||
</View>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenDonationCategory />;
|
||||
}
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -1,105 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
Grid,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { View } from "moti";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Admin_ScreenEventListOfParticipants } from "@/screens/Admin/Event/ScreenEventListOfParticipants";
|
||||
|
||||
export default function AdminEventListOfParticipants() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminEventListOfParticipants({
|
||||
id: id as string,
|
||||
});
|
||||
|
||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
setStartDate(dayjs(response.data.Event.tanggal));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={<AdminBackButtonAntTitle title="Daftar Peserta" />}
|
||||
>
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada peserta
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold truncate>
|
||||
{item?.User?.username}
|
||||
</TextCustom>
|
||||
<TextCustom>+{item?.User?.nomor}</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6} style={{ justifyContent: "center" }}>
|
||||
{startDate &&
|
||||
startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
||||
<BadgeCustom
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
color={item?.isPresent ? "green" : "red"}
|
||||
>
|
||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
||||
</BadgeCustom>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<BadgeCustom
|
||||
style={{ alignSelf: "flex-end" }}
|
||||
color="gray"
|
||||
>
|
||||
-
|
||||
</BadgeCustom>
|
||||
</View>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenEventListOfParticipants />;
|
||||
}
|
||||
|
||||
@@ -1,135 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
ActionIcon,
|
||||
ClickableCustom,
|
||||
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 { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminEvent } from "@/service/api-admin/api-admin-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenEventStatus } from "@/screens/Admin/Event/ScreenEventStatus";
|
||||
|
||||
export default function AdminEventStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
console.log("[STATUS EVENT]", status);
|
||||
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = useState(false);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [status, search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminEvent({
|
||||
category: status as "publish" | "review" | "reject" | "history" as any,
|
||||
search,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[RES LIST BY STATUS: ${status}]`,
|
||||
JSON.stringify(response, null, 2)
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari"
|
||||
value={search}
|
||||
onChangeText={(value) => setSearch(value)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title={`${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminTitleTable
|
||||
title1="Username"
|
||||
title2="Tanggal"
|
||||
title3="Judul Event"
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" size="small" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item, index) => (
|
||||
<ClickableCustom
|
||||
key={index}
|
||||
onPress={() => {
|
||||
router.push(`/admin/event/${item.id}/${status}`);
|
||||
}}
|
||||
>
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
// <ActionIcon
|
||||
// icon={
|
||||
// <Octicons
|
||||
// name="eye"
|
||||
// size={ICON_SIZE_BUTTON}
|
||||
// color="black"
|
||||
// />
|
||||
// }
|
||||
// onPress={() => {
|
||||
// router.push(`/admin/event/${item.id}/${status}`);
|
||||
// }}
|
||||
// />
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{dateTimeView({ date: item?.tanggal })}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2}>{item?.title || "-"}</TextCustom>
|
||||
}
|
||||
/>
|
||||
<Divider/>
|
||||
</ClickableCustom>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenEventStatus />;
|
||||
}
|
||||
|
||||
@@ -1,129 +1,5 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
BadgeCustom,
|
||||
CenterCustom,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import { IconEdit } from "@/components/_Icon";
|
||||
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
|
||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminMasterTypeOfEvent } from "@/service/api-admin/api-master-admin";
|
||||
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
|
||||
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_ScreenEventTypeOfEvent } from "@/screens/Admin/Voting/ScreenEventTypeOfEvent";
|
||||
|
||||
export default function AdminEventTypeOfEvent() {
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadData, setLoadData] = useState<boolean>(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiAdminMasterTypeOfEvent();
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]",error);
|
||||
setListData([]);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Event" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title="Tipe Acara"
|
||||
rightComponent={
|
||||
<AdminActionIconPlus
|
||||
onPress={() => {
|
||||
router.push(`/admin/event/type-create`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<>
|
||||
<GridViewCustomSpan
|
||||
span1={2}
|
||||
span2={5}
|
||||
span3={5}
|
||||
component1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
component2={<TextCustom bold align="center">Status</TextCustom>}
|
||||
component3={<TextCustom bold>Tipe Acara</TextCustom>}
|
||||
/>
|
||||
<Divider />
|
||||
<Spacing />
|
||||
|
||||
<StackCustom>
|
||||
{loadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item, index) => (
|
||||
<View key={index}>
|
||||
<GridViewCustomSpan
|
||||
span1={2}
|
||||
span2={5}
|
||||
span3={5}
|
||||
component1={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconEdit size={ICON_SIZE_BUTTON} color="black" />
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/event/type-update?id=${item.id}`);
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
style2={{ alignItems: "center" }}
|
||||
component2={
|
||||
<CenterCustom>
|
||||
<BadgeCustom
|
||||
color={colorActivationForBadge({
|
||||
status: item?.active,
|
||||
})}
|
||||
>
|
||||
{item?.active ? "Aktif" : "Tidak Aktif"}
|
||||
</BadgeCustom>
|
||||
</CenterCustom>
|
||||
}
|
||||
component3={<TextCustom >{item.name}</TextCustom>}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenEventTypeOfEvent />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
ButtonCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
AlertDefaultSystem,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
ButtonCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
@@ -15,8 +15,8 @@ import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminInvestmentGetOneInvoiceById,
|
||||
apiAdminInvestmentUpdateInvoice,
|
||||
apiAdminInvestmentGetOneInvoiceById,
|
||||
apiAdminInvestmentUpdateInvoice,
|
||||
} from "@/service/api-admin/api-admin-investment";
|
||||
import { colorBadgeTransaction } from "@/utils/colorBadge";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
@@ -60,7 +60,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
value: (data && data?.MasterBank?.namaBank) || "-",
|
||||
},
|
||||
{
|
||||
label: "Jumlah Investasi",
|
||||
label: "Nominal",
|
||||
value: (data && `Rp. ${formatCurrencyDisplay(data?.nominal)}`) || "-",
|
||||
},
|
||||
{
|
||||
@@ -158,7 +158,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
spanRight={6}
|
||||
styleLeft={{ paddingRight: 10 }}
|
||||
styleRight={{ paddingLeft: 10 }}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
@@ -181,7 +181,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
Tolak
|
||||
</ButtonCustom>
|
||||
}
|
||||
rightIcon={
|
||||
rightItem={
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BaseBox,
|
||||
DummyLandscapeImage,
|
||||
Grid,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
@@ -120,7 +121,7 @@ export default function AdminJobDetailStatus() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
||||
>
|
||||
<BaseBox>
|
||||
@@ -184,7 +185,7 @@ export default function AdminJobDetailStatus() {
|
||||
/>
|
||||
)}
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BoxButtonOnFooter,
|
||||
NewWrapper,
|
||||
TextAreaCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
@@ -100,7 +101,7 @@ export default function AdminJobRejectInput() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
footerComponent={buttonSubmit}
|
||||
headerComponent={<AdminBackButtonAntTitle title="Penolakan Job" />}
|
||||
>
|
||||
@@ -112,7 +113,7 @@ export default function AdminJobRejectInput() {
|
||||
showCount
|
||||
maxLength={1000}
|
||||
/>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,117 +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 { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminJob } from "@/service/api-admin/api-admin-job";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenJobStatus } from "@/screens/Admin/Job/ScreenJobStatus";
|
||||
|
||||
export default function AdminJobStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
const [list, setList] = useState<any | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
handlerLoadList();
|
||||
}, [status, search])
|
||||
);
|
||||
|
||||
const handlerLoadList = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
const response = await apiAdminJob({
|
||||
category: status as "publish" | "review" | "reject",
|
||||
search,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setList(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = (
|
||||
<SearchInput
|
||||
placeholder="Cari"
|
||||
onChangeText={setSearch}
|
||||
value={search}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Job Vacancy" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title={`${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Username"
|
||||
title3="Judul Pekerjaan"
|
||||
/>
|
||||
{/* <Spacing /> */}
|
||||
<Divider />
|
||||
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(list) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Tidak ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
list?.map((item: any, index: number) => (
|
||||
<AdminTableValue
|
||||
key={index}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/job/${item.id}/${status}`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom align="center" truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2} align="center">
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenJobStatus />;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
import { ButtonCustom, DrawerCustom, DummyLandscapeImage, Grid, Spacing, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import {
|
||||
ButtonCustom,
|
||||
DrawerCustom,
|
||||
DummyLandscapeImage,
|
||||
Grid,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
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);
|
||||
@@ -45,7 +36,7 @@ export default function AdminMaps() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
handlerLoadList();
|
||||
}, [])
|
||||
}, []),
|
||||
);
|
||||
|
||||
const handlerLoadList = async () => {
|
||||
@@ -63,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
|
||||
@@ -138,58 +85,60 @@ 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
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<FontAwesome
|
||||
name="building-o"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
|
||||
rightItem={<TextCustom>{selected.namePin}</TextCustom>}
|
||||
/>
|
||||
|
||||
<GridTwoView
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<Ionicons
|
||||
name="list-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
||||
rightItem={<TextCustom>{selected.bidangBisnis}</TextCustom>}
|
||||
/>
|
||||
|
||||
<GridTwoView
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<Ionicons
|
||||
name="call-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
|
||||
rightItem={<TextCustom>+{selected.nomorTelepon}</TextCustom>}
|
||||
/>
|
||||
<GridTwoView
|
||||
spanLeft={2}
|
||||
spanRight={10}
|
||||
leftIcon={
|
||||
leftItem={
|
||||
<Ionicons
|
||||
name="location-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color="white"
|
||||
/>
|
||||
}
|
||||
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
||||
rightItem={<TextCustom>{selected.alamatBisnis}</TextCustom>}
|
||||
/>
|
||||
|
||||
<Grid>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import {
|
||||
apiAdminUserAccessGetById,
|
||||
apiAdminUserAccessUpdateStatus,
|
||||
apiAdminUserAccessGetById,
|
||||
apiAdminUserAccessUpdateStatus,
|
||||
} from "@/service/api-admin/api-admin-user-access";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
@@ -26,7 +26,7 @@ export default function SuperAdminDetail() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -48,7 +48,7 @@ export default function SuperAdminDetail() {
|
||||
const response = await apiAdminUserAccessUpdateStatus({
|
||||
id: id as string,
|
||||
role: data?.masterUserRoleId === "2" ? "user" : "admin",
|
||||
category: "role"
|
||||
category: "role",
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
@@ -102,8 +102,8 @@ export default function SuperAdminDetail() {
|
||||
key={index}
|
||||
spanLeft={4}
|
||||
spanRight={8}
|
||||
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
|
||||
rightIcon={<TextCustom>{item?.value}</TextCustom>}
|
||||
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
|
||||
rightItem={<TextCustom>{item?.value}</TextCustom>}
|
||||
/>
|
||||
))}
|
||||
</StackCustom>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminUserAccessGetById,
|
||||
apiAdminUserAccessUpdateStatus,
|
||||
apiAdminUserAccessGetById,
|
||||
apiAdminUserAccessUpdateStatus,
|
||||
} from "@/service/api-admin/api-admin-user-access";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
@@ -28,7 +28,7 @@ export default function AdminUserAccessDetail() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -102,8 +102,8 @@ export default function AdminUserAccessDetail() {
|
||||
key={index}
|
||||
spanLeft={4}
|
||||
spanRight={8}
|
||||
leftIcon={<TextCustom bold>{item?.label}</TextCustom>}
|
||||
rightIcon={<TextCustom>{item?.value}</TextCustom>}
|
||||
leftItem={<TextCustom bold>{item?.label}</TextCustom>}
|
||||
rightItem={<TextCustom>{item?.value}</TextCustom>}
|
||||
/>
|
||||
))}
|
||||
</StackCustom>
|
||||
|
||||
@@ -1,139 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BadgeCustom,
|
||||
CenterCustom,
|
||||
Divider,
|
||||
SearchInput,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import { GridViewCustomSpan } from "@/components/_ShareComponent/GridViewCustomSpan";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
import { apiAdminUserAccessGetAll } from "@/service/api-admin/api-admin-user-access";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Admin_ScreenUserAccess } from "@/screens/Admin/User-Access/ScreenUserAccess";
|
||||
|
||||
export default function AdminUserAccess() {
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiAdminUserAccessGetAll({
|
||||
search: search,
|
||||
category: "only-user",
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR LOAD DATA]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const rightComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<SearchInput
|
||||
containerStyle={{ width: "100%", marginBottom: 0 }}
|
||||
placeholder="Cari User"
|
||||
onChangeText={(text) => setSearch(text)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<AdminComp_BoxTitle
|
||||
title="User Access"
|
||||
rightComponent={rightComponent()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GridViewCustomSpan
|
||||
span1={2}
|
||||
span2={5}
|
||||
span3={5}
|
||||
component1={
|
||||
<TextCustom align="center" bold>
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
component2={<TextCustom bold>Username</TextCustom>}
|
||||
component3={
|
||||
<TextCustom align="center" bold>
|
||||
Status Akses
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<StackCustom>
|
||||
{_.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray" size={"small"}>
|
||||
Tidak ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<GridViewCustomSpan
|
||||
key={index}
|
||||
span1={2}
|
||||
span2={5}
|
||||
span3={5}
|
||||
component1={
|
||||
<CenterCustom>
|
||||
<Ionicons
|
||||
onPress={() =>
|
||||
router.push(`/admin/user-access/${item?.id}`)
|
||||
}
|
||||
name="open"
|
||||
size={ICON_SIZE_XLARGE}
|
||||
color={MainColor.yellow}
|
||||
/>
|
||||
</CenterCustom>
|
||||
// <ButtonCustom
|
||||
// onPress={() =>
|
||||
// router.push(`/admin/user-access/${item?.id}`)
|
||||
// }
|
||||
// >
|
||||
// Detail
|
||||
// </ButtonCustom>
|
||||
}
|
||||
component2={
|
||||
<TextCustom bold truncate>
|
||||
{item?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
component3={
|
||||
<CenterCustom>
|
||||
{item?.active ? (
|
||||
<BadgeCustom color="green">Aktif</BadgeCustom>
|
||||
) : (
|
||||
<BadgeCustom color="red">Tidak Aktif</BadgeCustom>
|
||||
)}
|
||||
</CenterCustom>
|
||||
}
|
||||
style3={{ alignItems: "center", justifyContent: "center" }}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenUserAccess />;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BaseBox,
|
||||
CircleContainer,
|
||||
Grid,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
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 GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -40,7 +41,7 @@ export default function AdminVotingDetail() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -169,26 +170,28 @@ export default function AdminVotingDetail() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
headerComponent={<AdminBackButtonAntTitle title={`Detail Data`} />}
|
||||
>
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
{listData.map((item, i) => (
|
||||
<GridSpan_4_8
|
||||
<GridTwoView
|
||||
key={i}
|
||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
||||
value={<TextCustom>{item.value}</TextCustom>}
|
||||
spanLeft={5}
|
||||
spanRight={7}
|
||||
leftItem={<TextCustom bold>{item.label}</TextCustom>}
|
||||
rightItem={<TextCustom>{item.value}</TextCustom>}
|
||||
/>
|
||||
))}
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
{status === "publish" ||
|
||||
(status === "history" && (
|
||||
<BaseBox>
|
||||
<TextCustom bold align="center">
|
||||
Hasil Voting
|
||||
{(status === "publish" || status === "history") && (
|
||||
<BaseBox>
|
||||
<TextCustom bold align="center">
|
||||
Hasil Voting
|
||||
</TextCustom>
|
||||
<Spacing />
|
||||
<Grid>
|
||||
@@ -209,11 +212,11 @@ export default function AdminVotingDetail() {
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
))}
|
||||
)}
|
||||
|
||||
{data &&
|
||||
data?.catatan &&
|
||||
@@ -250,7 +253,7 @@ export default function AdminVotingDetail() {
|
||||
/>
|
||||
)}
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,117 +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 { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenVotingStatus } from "@/screens/Admin/Voting/ScreenVotingStatus";
|
||||
|
||||
export default function AdminVotingStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
const [list, setList] = useState<any | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [status, search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
const response = await apiAdminVoting({
|
||||
category: status as "publish" | "review" | "reject" as any,
|
||||
search,
|
||||
});
|
||||
|
||||
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"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title={`${_.startCase(status as string)}`}
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Username"
|
||||
title3="Judul Voting"
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{loadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(list) ? (
|
||||
<TextCustom align="center" bold color="gray">
|
||||
Belum ada data
|
||||
</TextCustom>
|
||||
) : (
|
||||
list.map((item: any, i: number) => (
|
||||
<AdminTableValue
|
||||
key={i}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/voting/${item.id}/${status}`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={
|
||||
<TextCustom truncate={1}>
|
||||
{item?.Author?.username || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
value3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenVotingStatus />;
|
||||
}
|
||||
|
||||
@@ -1,104 +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 { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { apiAdminVoting } from "@/service/api-admin/api-admin-voting";
|
||||
import { Octicons } from "@expo/vector-icons";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Divider } from "react-native-paper";
|
||||
import { Admin_ScreenVotingHistory } from "@/screens/Admin/Voting/ScreenVotingHistory";
|
||||
|
||||
export default function AdminVotingHistory() {
|
||||
const [list, setList] = useState<any | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [ search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadList(true);
|
||||
const response = await apiAdminVoting({
|
||||
category: "history",
|
||||
search,
|
||||
});
|
||||
|
||||
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"
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper headerComponent={<AdminTitlePage title="Voting" />}>
|
||||
<AdminComp_BoxTitle
|
||||
title="Riwayat"
|
||||
rightComponent={rightComponent}
|
||||
/>
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<AdminTitleTable
|
||||
title1="Aksi"
|
||||
title2="Username"
|
||||
title3="Judul Voting"
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
{loadList ? <LoaderCustom/> : _.isEmpty(list) ? <TextCustom align="center" bold color="gray">Belum ada data</TextCustom> : list.map((item: any, i: number) => (
|
||||
<AdminTableValue
|
||||
key={i}
|
||||
value1={
|
||||
<ActionIcon
|
||||
icon={
|
||||
<Octicons
|
||||
name="eye"
|
||||
size={ICON_SIZE_BUTTON}
|
||||
color="black"
|
||||
/>
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(`/admin/voting/${item.id}/history`);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
|
||||
value3={
|
||||
<TextCustom truncate={2}>
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
return <Admin_ScreenVotingHistory />;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
276
components/Drawer/NavbarMenu.back.tsx
Normal file
276
components/Drawer/NavbarMenu.back.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
export interface NavbarItem {
|
||||
label: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
color?: string;
|
||||
link?: string;
|
||||
links?: {
|
||||
label: string;
|
||||
link: string;
|
||||
}[];
|
||||
initiallyOpened?: boolean;
|
||||
}
|
||||
|
||||
interface NavbarMenuProps {
|
||||
items: NavbarItem[];
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||
const pathname = usePathname();
|
||||
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||
|
||||
// Normalisasi path: hapus trailing slash
|
||||
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||
|
||||
// Set activeLink saat pathname berubah
|
||||
useEffect(() => {
|
||||
if (normalizedPathname) {
|
||||
setActiveLink(normalizedPathname);
|
||||
}
|
||||
}, [normalizedPathname]);
|
||||
|
||||
// Toggle dropdown
|
||||
const toggleOpen = (label: string) => {
|
||||
setOpenKeys((prev) =>
|
||||
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
// flex: 1,
|
||||
// backgroundColor: MainColor.black,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingVertical: 10, // Opsional: tambahkan padding
|
||||
}}
|
||||
// showsVerticalScrollIndicator={false} // Opsional: sembunyikan indikator scroll
|
||||
>
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
onClose={onClose}
|
||||
activeLink={activeLink}
|
||||
setActiveLink={setActiveLink}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
toggleOpen={() => toggleOpen(item.label)}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Komponen Item Menu
|
||||
function MenuItem({
|
||||
item,
|
||||
onClose,
|
||||
activeLink,
|
||||
setActiveLink,
|
||||
isOpen,
|
||||
toggleOpen,
|
||||
}: {
|
||||
item: NavbarItem;
|
||||
onClose?: () => void;
|
||||
activeLink: string | null;
|
||||
setActiveLink: (link: string | null) => void;
|
||||
isOpen: boolean;
|
||||
toggleOpen: () => void;
|
||||
}) {
|
||||
const isActive = activeLink === item.link;
|
||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Animasi saat isOpen berubah
|
||||
React.useEffect(() => {
|
||||
Animated.timing(animatedHeight, {
|
||||
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [isOpen, item.links, animatedHeight]);
|
||||
|
||||
// Jika ada submenu
|
||||
if (item.links && item.links.length > 0) {
|
||||
return (
|
||||
<View>
|
||||
{/* Parent Item */}
|
||||
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.parentText}>{item.label}</Text>
|
||||
<Ionicons
|
||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Submenu (Animated) */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.submenu,
|
||||
// {
|
||||
// backgroundColor: "red",
|
||||
// },
|
||||
{
|
||||
height: animatedHeight,
|
||||
opacity: animatedHeight.interpolate({
|
||||
inputRange: [0, item.links.length * 40],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: "clamp",
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{item.links.map((subItem, index) => {
|
||||
const isSubActive = activeLink === subItem.link;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.subItem, isSubActive && styles.subItemActive]}
|
||||
onPress={() => {
|
||||
setActiveLink(subItem.link);
|
||||
onClose?.();
|
||||
router.push(subItem.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name="radio-button-on-outline"
|
||||
size={16}
|
||||
color={isSubActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.subText,
|
||||
isSubActive && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{subItem.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Menu tanpa submenu
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||
onPress={() => {
|
||||
setActiveLink(item.link || null);
|
||||
onClose?.();
|
||||
router.push(item.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.singleText,
|
||||
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
// Styles
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
parentItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 10,
|
||||
// backgroundColor: AccentColor.darkblue,
|
||||
borderRadius: 8,
|
||||
marginBottom: 5,
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
parentText: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
marginLeft: 10,
|
||||
color: MainColor.white,
|
||||
},
|
||||
singleItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 10,
|
||||
// backgroundColor: AccentColor.darkblue,
|
||||
borderRadius: 8,
|
||||
marginBottom: 5,
|
||||
},
|
||||
singleItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
singleText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
marginLeft: 10,
|
||||
color: MainColor.white,
|
||||
},
|
||||
icon: {
|
||||
width: 24,
|
||||
textAlign: "center",
|
||||
paddingRight: 10,
|
||||
},
|
||||
submenu: {
|
||||
overflow: "hidden",
|
||||
marginLeft: 30,
|
||||
marginTop: 5,
|
||||
},
|
||||
subItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 6,
|
||||
marginBottom: 4,
|
||||
},
|
||||
subItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
subText: {
|
||||
color: MainColor.white,
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
@@ -28,7 +28,7 @@ interface NavbarMenuProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
||||
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
|
||||
const pathname = usePathname();
|
||||
const [activeLink, setActiveLink] = useState<string | null>(null);
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
|
||||
@@ -41,13 +41,41 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
|
||||
useEffect(() => {
|
||||
if (normalizedPathname) {
|
||||
setActiveLink(normalizedPathname);
|
||||
|
||||
// Temukan menu induk yang sesuai dengan path saat ini dan buka dropdown-nya
|
||||
for (const item of items) {
|
||||
// Cocokkan dengan link langsung
|
||||
if (item.link && normalizedPathname.startsWith(item.link)) {
|
||||
setOpenKeys(prev => {
|
||||
if (!prev.includes(item.label)) {
|
||||
return [...prev, item.label];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||
}
|
||||
|
||||
// Cocokkan dengan submenu
|
||||
if (item.links && item.links.length > 0) {
|
||||
const matchingSubItem = item.links.find(link => normalizedPathname.startsWith(link.link));
|
||||
if (matchingSubItem) {
|
||||
setOpenKeys(prev => {
|
||||
if (!prev.includes(item.label)) {
|
||||
return [...prev, item.label];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
break; // Hentikan loop setelah menemukan kecocokan pertama
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [normalizedPathname]);
|
||||
}, [normalizedPathname, items]);
|
||||
|
||||
// Toggle dropdown
|
||||
const toggleOpen = (label: string) => {
|
||||
setOpenKeys((prev) =>
|
||||
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
|
||||
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -97,35 +125,71 @@ function MenuItem({
|
||||
isOpen: boolean;
|
||||
toggleOpen: () => void;
|
||||
}) {
|
||||
// Cek apakah menu ini atau submenu-nya yang aktif
|
||||
const isActive = activeLink === item.link;
|
||||
|
||||
// Cek apakah path saat ini cocok dengan salah satu submenu
|
||||
const isSubmenuActive = item.links && item.links.some(subItem => activeLink === subItem.link);
|
||||
|
||||
// Cek apakah path saat ini adalah detail dari submenu ini (misalnya /admin/event/123/detail)
|
||||
const isDetailPageOfThisMenu = item.links && item.links.length > 0 && activeLink &&
|
||||
item.links.some(link => {
|
||||
const linkPath = link.link.replace(/\/+$/, "");
|
||||
return activeLink.startsWith(linkPath + "/");
|
||||
});
|
||||
|
||||
// Gabungkan status aktif untuk menentukan apakah menu ini harus aktif
|
||||
const isMenuActive = isActive || isSubmenuActive || isDetailPageOfThisMenu;
|
||||
|
||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Animasi saat isOpen berubah
|
||||
React.useEffect(() => {
|
||||
// Jika ini adalah halaman detail dari menu ini, buka dropdown secara otomatis
|
||||
const shouldAutoOpen = isDetailPageOfThisMenu && !isOpen;
|
||||
|
||||
Animated.timing(animatedHeight, {
|
||||
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 40 : 0) : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [isOpen, item.links, animatedHeight]);
|
||||
|
||||
// Jika perlu membuka dropdown otomatis, panggil toggleOpen
|
||||
if (shouldAutoOpen) {
|
||||
toggleOpen();
|
||||
}
|
||||
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu, toggleOpen]);
|
||||
|
||||
// Jika ada submenu
|
||||
if (item.links && item.links.length > 0) {
|
||||
return (
|
||||
<View>
|
||||
{/* Parent Item */}
|
||||
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.parentItem,
|
||||
isMenuActive && styles.parentItemActive,
|
||||
]}
|
||||
onPress={toggleOpen}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={MainColor.white}
|
||||
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.parentText}>{item.label}</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.parentText,
|
||||
isMenuActive && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={MainColor.white}
|
||||
color={isMenuActive ? MainColor.yellow : MainColor.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -222,6 +286,9 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 5,
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
parentItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
parentText: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
|
||||
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
550
components/Drawer/NavbarMenu_V2.tsx
Normal file
@@ -0,0 +1,550 @@
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
export interface NavbarItem_V2 {
|
||||
label: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
color?: string;
|
||||
link?: string;
|
||||
links?: {
|
||||
label: string;
|
||||
link: string;
|
||||
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||
}[];
|
||||
initiallyOpened?: boolean;
|
||||
}
|
||||
|
||||
interface NavbarMenuProps {
|
||||
items: NavbarItem_V2[];
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) {
|
||||
const pathname = usePathname();
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
|
||||
// Normalisasi path: hapus trailing slash
|
||||
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||
|
||||
// Auto-open parent menu jika submenu aktif
|
||||
useEffect(() => {
|
||||
if (!normalizedPathname || !items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newOpenKeys: string[] = [];
|
||||
|
||||
// Helper function yang sama dengan di MenuItem
|
||||
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||
|
||||
// Exact match
|
||||
if (normalizedPathname === normalizedLink) return true;
|
||||
|
||||
// Detail pattern match
|
||||
if (detailPattern) {
|
||||
const patternRegex = new RegExp(
|
||||
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||
);
|
||||
if (patternRegex.test(normalizedPathname)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Detail page match (fallback)
|
||||
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
const commonWords = [
|
||||
// Actions
|
||||
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||
|
||||
// Status types
|
||||
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||
|
||||
// General pages
|
||||
'category', 'history', 'dashboard', 'index',
|
||||
|
||||
// Event specific
|
||||
'type-of-event', 'type-create', 'type-update',
|
||||
|
||||
// Forum specific
|
||||
'posting', 'report-posting', 'report-comment',
|
||||
|
||||
// Collaboration
|
||||
'group',
|
||||
|
||||
// App Information
|
||||
'business-field', 'information-bank', 'sticker',
|
||||
'bidang-update', 'sub-bidang-update',
|
||||
|
||||
// Transaction/Finance related
|
||||
'transaction-detail', 'transaction', 'payment',
|
||||
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||
'list-disbursement-of-funds',
|
||||
|
||||
// List pages (CRITICAL!)
|
||||
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||
|
||||
// Input/Form pages
|
||||
'reject-input',
|
||||
|
||||
// Category pages
|
||||
'category-create', 'category-update'
|
||||
];
|
||||
|
||||
const hasIdSegment = segments.some(segment => {
|
||||
if (commonWords.includes(segment.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPureNumber = /^\d+$/.test(segment);
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||
const hasNumber = /\d/.test(segment);
|
||||
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||
|
||||
return isPureNumber || isUUID || isAlphanumericId;
|
||||
});
|
||||
|
||||
return hasIdSegment;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.links && item.links.length > 0) {
|
||||
// Check jika ada submenu yang match dengan current path
|
||||
const hasActiveSubmenu = item.links.some((subItem) => {
|
||||
return checkPathMatch(subItem.link, subItem.detailPattern);
|
||||
});
|
||||
|
||||
if (hasActiveSubmenu) {
|
||||
newOpenKeys.push(item.label);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setOpenKeys(newOpenKeys);
|
||||
} catch (error) {
|
||||
console.error("Error in NavbarMenu useEffect:", error);
|
||||
}
|
||||
}, [normalizedPathname, items]);
|
||||
|
||||
// Toggle dropdown
|
||||
const toggleOpen = (label: string) => {
|
||||
setOpenKeys((prev) =>
|
||||
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
{items && items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
onClose={onClose}
|
||||
currentPath={normalizedPathname}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
toggleOpen={() => toggleOpen(item.label)}
|
||||
/>
|
||||
))
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Komponen Item Menu
|
||||
function MenuItem({
|
||||
item,
|
||||
onClose,
|
||||
currentPath,
|
||||
isOpen,
|
||||
toggleOpen,
|
||||
}: {
|
||||
item: NavbarItem_V2;
|
||||
onClose?: () => void;
|
||||
currentPath: string;
|
||||
isOpen: boolean;
|
||||
toggleOpen: () => void;
|
||||
}) {
|
||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Helper function untuk check apakah path aktif
|
||||
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||
if (!linkPath) return false;
|
||||
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||
|
||||
// 1. Match exact - prioritas tertinggi
|
||||
if (currentPath === normalizedLink) return true;
|
||||
|
||||
// 2. Jika ada detailPattern, cek pattern dulu
|
||||
if (detailPattern) {
|
||||
// detailPattern contoh: "/admin/job/*/review"
|
||||
// akan match dengan:
|
||||
// - /admin/job/123/review ✅
|
||||
// - /admin/job/123/review/transaction-detail ✅
|
||||
// - /admin/job/123/review/anything/nested ✅
|
||||
const patternRegex = new RegExp(
|
||||
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||
);
|
||||
const isMatch = patternRegex.test(currentPath);
|
||||
|
||||
// Debug log untuk pattern matching
|
||||
if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||
console.log('🔍 Pattern Match Check:', {
|
||||
currentPath,
|
||||
detailPattern,
|
||||
regex: patternRegex.toString(),
|
||||
isMatch
|
||||
});
|
||||
}
|
||||
|
||||
if (isMatch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Match untuk detail pages (fallback)
|
||||
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
const commonWords = [
|
||||
// Actions
|
||||
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||
|
||||
// Status types
|
||||
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||
|
||||
// General pages
|
||||
'category', 'history', 'dashboard', 'index',
|
||||
|
||||
// Event specific
|
||||
'type-of-event', 'type-create', 'type-update',
|
||||
|
||||
// Forum specific
|
||||
'posting', 'report-posting', 'report-comment',
|
||||
|
||||
// Collaboration
|
||||
'group',
|
||||
|
||||
// App Information
|
||||
'business-field', 'information-bank', 'sticker',
|
||||
'bidang-update', 'sub-bidang-update',
|
||||
|
||||
// Transaction/Finance related
|
||||
'transaction-detail', 'transaction', 'payment',
|
||||
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||
'list-disbursement-of-funds',
|
||||
|
||||
// List pages (CRITICAL!)
|
||||
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||
|
||||
// Input/Form pages
|
||||
'reject-input',
|
||||
|
||||
// Category pages
|
||||
'category-create', 'category-update'
|
||||
];
|
||||
|
||||
const hasIdSegment = segments.some(segment => {
|
||||
if (commonWords.includes(segment.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPureNumber = /^\d+$/.test(segment);
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||
const hasNumber = /\d/.test(segment);
|
||||
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||
|
||||
return isPureNumber || isUUID || isAlphanumericId;
|
||||
});
|
||||
|
||||
return hasIdSegment;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||
const isActive = isPathActive(item.link);
|
||||
const hasActiveSubmenu =
|
||||
item.links?.some((subItem) => isPathActive(subItem.link, subItem.detailPattern)) || false;
|
||||
|
||||
// Animasi saat isOpen berubah
|
||||
useEffect(() => {
|
||||
Animated.timing(animatedHeight, {
|
||||
toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [isOpen, item.links, animatedHeight]);
|
||||
|
||||
// Jika ada submenu
|
||||
if (item.links && item.links.length > 0) {
|
||||
// PRE-CALCULATE semua active states untuk submenu
|
||||
const submenuActiveStates = item.links.map(subItem => ({
|
||||
subItem,
|
||||
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||
pathLength: subItem.link.length
|
||||
}));
|
||||
|
||||
return (
|
||||
<View>
|
||||
{/* Parent Item */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.parentItem,
|
||||
hasActiveSubmenu && styles.parentItemActive,
|
||||
]}
|
||||
onPress={toggleOpen}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.parentText,
|
||||
hasActiveSubmenu && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Submenu (Animated) */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.submenu,
|
||||
{
|
||||
height: animatedHeight,
|
||||
opacity: animatedHeight.interpolate({
|
||||
inputRange: [0, item.links.length * 44],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: "clamp",
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||
|
||||
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||
|
||||
const isOtherLonger = other.pathLength > pathLength;
|
||||
|
||||
// Debug log untuk Dashboard
|
||||
if (subItem.label === "Dashboard" && isSubActive) {
|
||||
console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||
dashboardLink: subItem.link,
|
||||
dashboardLength: pathLength,
|
||||
otherLabel: other.subItem.label,
|
||||
otherLink: other.subItem.link,
|
||||
otherPattern: other.subItem.detailPattern,
|
||||
otherLength: other.pathLength,
|
||||
otherIsActive: other.isActive,
|
||||
isOtherLonger,
|
||||
willDisableDashboard: other.isActive && isOtherLonger,
|
||||
currentURL: currentPath
|
||||
});
|
||||
}
|
||||
|
||||
// Conflict log
|
||||
if (isSubActive && other.isActive) {
|
||||
console.log('🔍 CONFLICT DETECTED:', {
|
||||
current: subItem.label,
|
||||
currentPath: subItem.link,
|
||||
currentLength: pathLength,
|
||||
other: other.subItem.label,
|
||||
otherPath: other.subItem.link,
|
||||
otherLength: other.pathLength,
|
||||
isOtherLonger,
|
||||
shouldDisableCurrent: isOtherLonger,
|
||||
currentURL: currentPath
|
||||
});
|
||||
}
|
||||
|
||||
return other.isActive && isOtherLonger;
|
||||
});
|
||||
|
||||
// Final decision
|
||||
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||
|
||||
// Debug final
|
||||
if (isSubActive) {
|
||||
console.log('✅ Active check:', {
|
||||
label: subItem.label,
|
||||
link: subItem.link,
|
||||
isSubActive,
|
||||
hasMoreSpecificMatch,
|
||||
finalIsActive
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.subItem, finalIsActive && styles.subItemActive]}
|
||||
onPress={() => {
|
||||
onClose?.();
|
||||
router.push(subItem.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name="radio-button-on-outline"
|
||||
size={16}
|
||||
color={finalIsActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.subText,
|
||||
finalIsActive && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{subItem.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Menu tanpa submenu
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||
onPress={() => {
|
||||
onClose?.();
|
||||
router.push(item.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.singleText,
|
||||
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// Styles
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
parentItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 8,
|
||||
marginBottom: 5,
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
parentItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
parentText: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
marginLeft: 10,
|
||||
color: MainColor.white,
|
||||
},
|
||||
singleItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 8,
|
||||
marginBottom: 5,
|
||||
},
|
||||
singleItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
singleText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
marginLeft: 10,
|
||||
color: MainColor.white,
|
||||
},
|
||||
icon: {
|
||||
width: 24,
|
||||
textAlign: "center",
|
||||
paddingRight: 10,
|
||||
},
|
||||
submenu: {
|
||||
overflow: "hidden",
|
||||
marginLeft: 30,
|
||||
marginTop: 5,
|
||||
},
|
||||
subItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 6,
|
||||
marginBottom: 4,
|
||||
},
|
||||
subItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
subText: {
|
||||
color: MainColor.white,
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
871
components/Drawer/NavbarMenu_V3.tsx
Normal file
@@ -0,0 +1,871 @@
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Animated,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
export interface NavbarItem_V3 {
|
||||
label: string;
|
||||
icon?: keyof typeof Ionicons.glyphMap;
|
||||
color?: string;
|
||||
link?: string;
|
||||
links?: {
|
||||
label: string;
|
||||
link: string;
|
||||
detailPattern?: string; // NEW: Pattern untuk match detail pages
|
||||
}[];
|
||||
initiallyOpened?: boolean;
|
||||
}
|
||||
|
||||
interface NavbarMenuProps {
|
||||
items: NavbarItem_V3[];
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export default function NavbarMenu_V3({ items, onClose }: NavbarMenuProps) {
|
||||
const pathname = usePathname();
|
||||
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||
|
||||
// Normalisasi path: hapus trailing slash
|
||||
const normalizePath = (path: string) => path.replace(/\/+$/, "");
|
||||
const normalizedPathname = pathname ? normalizePath(pathname) : "";
|
||||
|
||||
// Auto-open parent menu jika submenu aktif
|
||||
useEffect(() => {
|
||||
if (!normalizedPathname || !items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newOpenKeys: string[] = [];
|
||||
|
||||
// Helper function yang sama dengan di MenuItem
|
||||
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
|
||||
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||
|
||||
// Exact match
|
||||
if (normalizedPathname === normalizedLink) return true;
|
||||
|
||||
// Detail pattern match
|
||||
if (detailPattern) {
|
||||
const patternRegex = new RegExp(
|
||||
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||
);
|
||||
if (patternRegex.test(normalizedPathname)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Detail page match (fallback)
|
||||
if (normalizedPathname.startsWith(normalizedLink + "/")) {
|
||||
const remainder = normalizedPathname.substring(normalizedLink.length + 1);
|
||||
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
const commonWords = [
|
||||
// Actions
|
||||
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||
|
||||
// Status types
|
||||
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||
|
||||
// General pages
|
||||
'category', 'history', 'dashboard', 'index',
|
||||
|
||||
// Event specific
|
||||
'type-of-event', 'type-create', 'type-update',
|
||||
|
||||
// Forum specific
|
||||
'posting', 'report-posting', 'report-comment',
|
||||
|
||||
// Collaboration
|
||||
'group',
|
||||
|
||||
// App Information
|
||||
'business-field', 'information-bank', 'sticker',
|
||||
'bidang-update', 'sub-bidang-update',
|
||||
|
||||
// Transaction/Finance related
|
||||
'transaction-detail', 'transaction', 'payment',
|
||||
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||
'list-disbursement-of-funds',
|
||||
|
||||
// List pages (CRITICAL!)
|
||||
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||
|
||||
// Input/Form pages
|
||||
'reject-input',
|
||||
|
||||
// Category pages
|
||||
'category-create', 'category-update'
|
||||
];
|
||||
|
||||
const hasIdSegment = segments.some(segment => {
|
||||
if (commonWords.includes(segment.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPureNumber = /^\d+$/.test(segment);
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||
const hasNumber = /\d/.test(segment);
|
||||
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||
|
||||
return isPureNumber || isUUID || isAlphanumericId;
|
||||
});
|
||||
|
||||
return hasIdSegment;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Calculate all potential matches for conflict resolution
|
||||
const allMatches = items.flatMap(item => {
|
||||
if (!item.links || item.links.length === 0) return [];
|
||||
|
||||
return item.links
|
||||
.filter(subItem => checkPathMatch(subItem.link, subItem.detailPattern))
|
||||
.map(subItem => ({
|
||||
parentLabel: item.label,
|
||||
subItem,
|
||||
pathLength: subItem.link.length
|
||||
}));
|
||||
});
|
||||
|
||||
// Find the most specific match for each parent
|
||||
const uniqueParents = new Map<string, { parentLabel: string, longestPathLength: number }>();
|
||||
|
||||
allMatches.forEach(match => {
|
||||
const existing = uniqueParents.get(match.parentLabel);
|
||||
if (!existing || match.pathLength > existing.longestPathLength) {
|
||||
uniqueParents.set(match.parentLabel, {
|
||||
parentLabel: match.parentLabel,
|
||||
longestPathLength: match.pathLength
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add only the parents with the most specific matches
|
||||
newOpenKeys.push(...Array.from(uniqueParents.values()).map(item => item.parentLabel));
|
||||
|
||||
// Additionally, if no specific submenu match was found but the current path
|
||||
// starts with one of the parent menu links, add that parent
|
||||
if (newOpenKeys.length === 0) {
|
||||
// Find the parent whose link is the longest prefix of the current path
|
||||
let longestMatchParent = null;
|
||||
let longestMatchLength = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.links && item.links.length > 0) {
|
||||
item.links.forEach(link => {
|
||||
const linkPath = link.link.replace(/\/+$/, "");
|
||||
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||
longestMatchLength = linkPath.length;
|
||||
longestMatchParent = item.label;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (longestMatchParent) {
|
||||
newOpenKeys.push(longestMatchParent);
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||
const isOnDetailPage = (() => {
|
||||
// Check if current path has ID-like segments or detail keywords
|
||||
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
const commonWords = [
|
||||
// Actions
|
||||
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||
|
||||
// Status types
|
||||
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||
|
||||
// General pages
|
||||
'category', 'history', 'dashboard', 'index',
|
||||
|
||||
// Event specific
|
||||
'type-of-event', 'type-create', 'type-update',
|
||||
|
||||
// Forum specific
|
||||
'posting', 'report-posting', 'report-comment',
|
||||
|
||||
// Collaboration
|
||||
'group',
|
||||
|
||||
// App Information
|
||||
'business-field', 'information-bank', 'sticker',
|
||||
'bidang-update', 'sub-bidang-update',
|
||||
|
||||
// Transaction/Finance related
|
||||
'transaction-detail', 'transaction', 'payment',
|
||||
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||
'list-disbursement-of-funds',
|
||||
|
||||
// List pages (CRITICAL!)
|
||||
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||
|
||||
// Input/Form pages
|
||||
'reject-input',
|
||||
|
||||
// Category pages
|
||||
'category-create', 'category-update'
|
||||
];
|
||||
|
||||
const hasIdSegment = segments.some(segment => {
|
||||
if (commonWords.includes(segment.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isPureNumber = /^\d+$/.test(segment);
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||
const hasNumber = /\d/.test(segment);
|
||||
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||
|
||||
return isPureNumber || isUUID || isAlphanumericId;
|
||||
});
|
||||
|
||||
return hasIdSegment;
|
||||
})();
|
||||
|
||||
// NEW: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||
const isOnDetailPageGlobal = (() => {
|
||||
// Check if current path has ID-like segments or detail keywords
|
||||
const segments = normalizedPathname.split('/').filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
const commonWords = [
|
||||
// Actions
|
||||
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||
|
||||
// Status types
|
||||
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||
|
||||
// General pages
|
||||
'category', 'history', 'dashboard', 'index',
|
||||
|
||||
// Event specific
|
||||
'type-of-event', 'type-create', 'type-update',
|
||||
|
||||
// Forum specific
|
||||
'posting', 'report-posting', 'report-comment',
|
||||
|
||||
// Collaboration
|
||||
'group',
|
||||
|
||||
// App Information
|
||||
'business-field', 'information-bank', 'sticker',
|
||||
'bidang-update', 'sub-bidang-update',
|
||||
|
||||
// Transaction/Finance related
|
||||
'transaction-detail', 'transaction', 'payment',
|
||||
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||
'list-disbursement-of-funds',
|
||||
|
||||
// List pages (CRITICAL!)
|
||||
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||
|
||||
// Input/Form pages
|
||||
'reject-input',
|
||||
|
||||
// Category pages
|
||||
'category-create', 'category-update'
|
||||
];
|
||||
|
||||
// Check if any segment is a common word
|
||||
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||
|
||||
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||
const hasIdSegment = segments.some(segment => {
|
||||
const isPureNumber = /^\d+$/.test(segment);
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||
const hasNumber = /\d/.test(segment);
|
||||
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||
|
||||
return isPureNumber || isUUID || isAlphanumericId;
|
||||
});
|
||||
|
||||
// A detail page is one that has either common words or ID segments
|
||||
return hasCommonWord || hasIdSegment;
|
||||
})();
|
||||
|
||||
// NEW: Only open parent menu if the current path is a detail page of the most relevant parent
|
||||
if (isOnDetailPageGlobal && newOpenKeys.length === 0) {
|
||||
// Find the parent whose link is the longest prefix of the current path
|
||||
let longestMatchParent = null;
|
||||
let longestMatchLength = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.links && item.links.length > 0) {
|
||||
item.links.forEach(link => {
|
||||
const linkPath = link.link.replace(/\/+$/, "");
|
||||
if (normalizedPathname.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||
longestMatchLength = linkPath.length;
|
||||
longestMatchParent = item.label;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (longestMatchParent) {
|
||||
newOpenKeys.push(longestMatchParent);
|
||||
}
|
||||
}
|
||||
|
||||
setOpenKeys(newOpenKeys);
|
||||
} catch (error) {
|
||||
console.error("Error in NavbarMenu useEffect:", error);
|
||||
}
|
||||
}, [normalizedPathname, items]);
|
||||
|
||||
// Toggle dropdown
|
||||
const toggleOpen = (label: string) => {
|
||||
setOpenKeys((prev) =>
|
||||
prev.includes(label) ? prev.filter((key) => key !== label) : [...prev, label]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
{items && items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
<MenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
items={items}
|
||||
onClose={onClose}
|
||||
currentPath={normalizedPathname}
|
||||
isOpen={openKeys.includes(item.label)}
|
||||
toggleOpen={() => toggleOpen(item.label)}
|
||||
/>
|
||||
))
|
||||
) : null}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Komponen Item Menu
|
||||
function MenuItem({
|
||||
item,
|
||||
items,
|
||||
onClose,
|
||||
currentPath,
|
||||
isOpen,
|
||||
toggleOpen,
|
||||
}: {
|
||||
item: NavbarItem_V3;
|
||||
items: NavbarItem_V3[];
|
||||
onClose?: () => void;
|
||||
currentPath: string;
|
||||
isOpen: boolean;
|
||||
toggleOpen: () => void;
|
||||
}) {
|
||||
const animatedHeight = useRef(new Animated.Value(0)).current;
|
||||
|
||||
// Helper function untuk check apakah path aktif
|
||||
const isPathActive = (linkPath: string | undefined, detailPattern?: string) => {
|
||||
if (!linkPath) return false;
|
||||
const normalizedLink = linkPath.replace(/\/+$/, "");
|
||||
|
||||
// 1. Match exact - prioritas tertinggi
|
||||
if (currentPath === normalizedLink) return true;
|
||||
|
||||
// 2. Jika ada detailPattern, cek pattern dulu
|
||||
if (detailPattern) {
|
||||
// detailPattern contoh: "/admin/job/*/review"
|
||||
// akan match dengan:
|
||||
// - /admin/job/123/review ✅
|
||||
// - /admin/job/123/review/transaction-detail ✅
|
||||
// - /admin/job/123/review/anything/nested ✅
|
||||
const patternRegex = new RegExp(
|
||||
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$"
|
||||
);
|
||||
const isMatch = patternRegex.test(currentPath);
|
||||
|
||||
// Debug log untuk pattern matching
|
||||
// if (currentPath.includes('transaction-detail') || currentPath.includes('disbursement')) {
|
||||
// console.log('🔍 Pattern Match Check:', {
|
||||
// currentPath,
|
||||
// detailPattern,
|
||||
// regex: patternRegex.toString(),
|
||||
// isMatch
|
||||
// });
|
||||
// }
|
||||
|
||||
if (isMatch) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Match untuk detail pages (fallback)
|
||||
if (currentPath.startsWith(normalizedLink + "/")) {
|
||||
const remainder = currentPath.substring(normalizedLink.length + 1);
|
||||
const segments = remainder.split("/").filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
const commonWords = [
|
||||
// Actions
|
||||
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||
|
||||
// Status types
|
||||
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||
|
||||
// General pages
|
||||
'category', 'history', 'index',
|
||||
|
||||
// Event specific
|
||||
'type-of-event', 'type-create', 'type-update',
|
||||
|
||||
// Forum specific
|
||||
'posting', 'report-posting', 'report-comment',
|
||||
|
||||
// Collaboration
|
||||
'group',
|
||||
|
||||
// App Information
|
||||
'business-field', 'information-bank', 'sticker',
|
||||
'bidang-update', 'sub-bidang-update',
|
||||
|
||||
// Transaction/Finance related
|
||||
'transaction-detail', 'transaction', 'payment',
|
||||
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||
'list-disbursement-of-funds',
|
||||
|
||||
// List pages (CRITICAL!)
|
||||
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||
|
||||
// Input/Form pages
|
||||
'reject-input',
|
||||
|
||||
// Category pages
|
||||
'category-create', 'category-update'
|
||||
];
|
||||
|
||||
const hasCommonWord = segments.some(segment =>
|
||||
commonWords.includes(segment.toLowerCase())
|
||||
);
|
||||
|
||||
// Hanya anggap sebagai detail page jika mengandung commonWords
|
||||
return hasCommonWord;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Check apakah menu item ini atau submenu-nya yang aktif
|
||||
const isActive = isPathActive(item.link);
|
||||
|
||||
// NEW LOGIC: Check if user is on a detail page (contains ID segments or specific keywords)
|
||||
const isOnDetailPage = (() => {
|
||||
// Check if current path has ID-like segments or detail keywords
|
||||
const segments = currentPath.split('/').filter(s => s.length > 0);
|
||||
|
||||
if (segments.length === 0) return false;
|
||||
|
||||
const commonWords = [
|
||||
// Actions
|
||||
'detail', 'edit', 'create', 'new', 'add', 'delete', 'view',
|
||||
|
||||
// Status types
|
||||
'publish', 'review', 'reject', 'status', 'active', 'inactive', 'pending',
|
||||
|
||||
// General pages
|
||||
'category', 'history', 'dashboard', 'index',
|
||||
|
||||
// Event specific
|
||||
'type-of-event', 'type-create', 'type-update',
|
||||
|
||||
// Forum specific
|
||||
'posting', 'report-posting', 'report-comment',
|
||||
|
||||
// Collaboration
|
||||
'group',
|
||||
|
||||
// App Information
|
||||
'business-field', 'information-bank', 'sticker',
|
||||
'bidang-update', 'sub-bidang-update',
|
||||
|
||||
// Transaction/Finance related
|
||||
'transaction-detail', 'transaction', 'payment',
|
||||
'disbursement-of-funds', 'detail-disbursement-of-funds',
|
||||
'list-disbursement-of-funds',
|
||||
|
||||
// List pages (CRITICAL!)
|
||||
'list-of-investor', 'list-of-donatur', 'list-of-participants',
|
||||
'list-comment', 'list-report-comment', 'list-report-posting',
|
||||
|
||||
// Input/Form pages
|
||||
'reject-input',
|
||||
|
||||
// Category pages
|
||||
'category-create', 'category-update'
|
||||
];
|
||||
|
||||
// Check if any segment is a common word
|
||||
const hasCommonWord = segments.some(segment => commonWords.includes(segment.toLowerCase()));
|
||||
|
||||
// Check if any segment looks like an ID (number, UUID, alphanumeric with numbers)
|
||||
const hasIdSegment = segments.some(segment => {
|
||||
const isPureNumber = /^\d+$/.test(segment);
|
||||
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(segment);
|
||||
const hasNumber = /\d/.test(segment);
|
||||
const isAlphanumericId = /^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
|
||||
|
||||
return isPureNumber || isUUID || isAlphanumericId;
|
||||
});
|
||||
|
||||
// A detail page is one that has either common words or ID segments
|
||||
return hasCommonWord || hasIdSegment;
|
||||
})();
|
||||
|
||||
// Calculate all submenu active states for conflict resolution
|
||||
const submenuActiveStates = item.links?.map(subItem => ({
|
||||
subItem,
|
||||
isActive: isPathActive(subItem.link, subItem.detailPattern),
|
||||
pathLength: subItem.link.length
|
||||
})) || [];
|
||||
|
||||
// Determine if any submenu is active considering conflicts
|
||||
const hasActiveSubmenu = submenuActiveStates.some(({ isActive: isSubActive, pathLength, subItem }) => {
|
||||
if (!isSubActive) return false;
|
||||
|
||||
// Check if there's a more specific match elsewhere
|
||||
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||
return other.isActive && other.pathLength > pathLength;
|
||||
});
|
||||
|
||||
return isSubActive && !hasMoreSpecificMatch;
|
||||
}) || false;
|
||||
|
||||
// For parent menu detection, if current path contains common words,
|
||||
// check if this parent menu's link is a prefix of the current path
|
||||
const isParentOfDetailPage = !isActive && !hasActiveSubmenu && item.links && item.links.length > 0 &&
|
||||
item.links.some(link => currentPath.startsWith(link.link.replace(/\/+$/, "") + "/"));
|
||||
|
||||
// Determine if this is the most relevant parent menu for the current path
|
||||
const isMostRelevantParent = isParentOfDetailPage && (() => {
|
||||
let longestMatchLength = 0;
|
||||
let mostRelevantParent = null;
|
||||
|
||||
// Find the parent with the longest matching prefix
|
||||
items.forEach(parentItem => {
|
||||
if (parentItem.links && parentItem.links.length > 0) {
|
||||
parentItem.links.forEach(link => {
|
||||
const linkPath = link.link.replace(/\/+$/, "");
|
||||
if (currentPath.startsWith(linkPath + "/") && linkPath.length > longestMatchLength) {
|
||||
longestMatchLength = linkPath.length;
|
||||
mostRelevantParent = parentItem.label;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return mostRelevantParent === item.label;
|
||||
})();
|
||||
|
||||
// NEW LOGIC: If we're on a detail page, NO submenu should be active regardless of pattern matching
|
||||
const hasActiveSubmenuOnDetailPage = isOnDetailPage ? false : hasActiveSubmenu;
|
||||
|
||||
// NEW LOGIC: If user is on a detail page that belongs to this parent menu,
|
||||
// activate only the parent menu (open dropdown) without activating any submenu
|
||||
const isDetailPageOfThisMenu = !isActive && !hasActiveSubmenuOnDetailPage &&
|
||||
item.links && item.links.length > 0 &&
|
||||
item.links.some(link => {
|
||||
const linkPath = link.link.replace(/\/+$/, "");
|
||||
return currentPath.startsWith(linkPath + "/");
|
||||
}) &&
|
||||
!isMostRelevantParent; // Only apply this logic if this isn't the most relevant parent
|
||||
|
||||
// NEW LOGIC: Check if this is a page that doesn't belong to any specific menu in the navbar
|
||||
const isUnlistedPage = !isActive && !hasActiveSubmenu && !isMostRelevantParent && !isDetailPageOfThisMenu && isOnDetailPage;
|
||||
|
||||
// NEW LOGIC: If we're on a detail page and this menu is not the relevant parent or detail page owner,
|
||||
// then it should not be highlighted even if it would normally be the most relevant
|
||||
const isOnDetailPageAndNotRelevant = isOnDetailPage && !isMostRelevantParent && !isDetailPageOfThisMenu && !isActive;
|
||||
|
||||
// NEW LOGIC: If this is an unlisted page, no menu should be highlighted
|
||||
const isUnlistedPageAndNotRelevant = isUnlistedPage;
|
||||
|
||||
// FINAL LOGIC: Only activate this menu if:
|
||||
// 1. It's the exact match for current path, OR
|
||||
// 2. It's the most relevant parent, OR
|
||||
// 3. It's a detail page of this menu
|
||||
// But NOT if we're on a detail page and this isn't the relevant parent
|
||||
// And NOT if this is an unlisted page
|
||||
const isActuallyRelevant = (isActive || isMostRelevantParent || isDetailPageOfThisMenu) && !isOnDetailPageAndNotRelevant && !isUnlistedPageAndNotRelevant;
|
||||
|
||||
// Animasi saat isOpen berubah
|
||||
useEffect(() => {
|
||||
Animated.timing(animatedHeight, {
|
||||
toValue: (isOpen || isDetailPageOfThisMenu) ? (item.links ? item.links.length * 44 : 0) : 0,
|
||||
duration: 200,
|
||||
useNativeDriver: false,
|
||||
}).start();
|
||||
}, [isOpen, item.links, animatedHeight, isDetailPageOfThisMenu]);
|
||||
|
||||
// Jika ada submenu
|
||||
if (item.links && item.links.length > 0) {
|
||||
return (
|
||||
<View>
|
||||
{/* Parent Item */}
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.parentItem,
|
||||
isActuallyRelevant && styles.parentItemActive,
|
||||
]}
|
||||
onPress={toggleOpen}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.parentText,
|
||||
isActuallyRelevant && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={isOpen ? "chevron-up" : "chevron-down"}
|
||||
size={16}
|
||||
color={isActuallyRelevant ? MainColor.yellow : MainColor.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Submenu (Animated) */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.submenu,
|
||||
{
|
||||
height: animatedHeight,
|
||||
opacity: animatedHeight.interpolate({
|
||||
inputRange: [0, item.links.length * 44],
|
||||
outputRange: [0, 1],
|
||||
extrapolate: "clamp",
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{submenuActiveStates.map(({ subItem, isActive: isSubActive, pathLength }, index) => {
|
||||
|
||||
// CRITICAL FIX: Cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
|
||||
const hasMoreSpecificMatch = submenuActiveStates.some(other => {
|
||||
if (other.subItem.link === subItem.link) return false; // Skip self
|
||||
|
||||
const isOtherLonger = other.pathLength > pathLength;
|
||||
|
||||
// Debug log untuk Dashboard
|
||||
// if (subItem.label === "Dashboard" && isSubActive) {
|
||||
// console.log(`🔎 Dashboard checking against ${other.subItem.label}:`, {
|
||||
// dashboardLink: subItem.link,
|
||||
// dashboardLength: pathLength,
|
||||
// otherLabel: other.subItem.label,
|
||||
// otherLink: other.subItem.link,
|
||||
// otherPattern: other.subItem.detailPattern,
|
||||
// otherLength: other.pathLength,
|
||||
// otherIsActive: other.isActive,
|
||||
// isOtherLonger,
|
||||
// willDisableDashboard: other.isActive && isOtherLonger,
|
||||
// currentURL: currentPath
|
||||
// });
|
||||
// }
|
||||
|
||||
// Conflict log
|
||||
// if (isSubActive && other.isActive) {
|
||||
// console.log('🔍 CONFLICT DETECTED:', {
|
||||
// current: subItem.label,
|
||||
// currentPath: subItem.link,
|
||||
// currentLength: pathLength,
|
||||
// other: other.subItem.label,
|
||||
// otherPath: other.subItem.link,
|
||||
// otherLength: other.pathLength,
|
||||
// isOtherLonger,
|
||||
// shouldDisableCurrent: isOtherLonger,
|
||||
// currentURL: currentPath
|
||||
// });
|
||||
// }
|
||||
|
||||
return other.isActive && isOtherLonger;
|
||||
});
|
||||
|
||||
// Final decision
|
||||
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
|
||||
|
||||
// NEW: If this is a detail page (regardless of which menu), don't highlight any submenu items
|
||||
// Also don't highlight if this is an unlisted page
|
||||
const shouldHighlight = (isOnDetailPage || isUnlistedPage) ? false : finalIsActive;
|
||||
|
||||
// Debug final
|
||||
// if (isSubActive) {
|
||||
// console.log('✅ Active check:', {
|
||||
// label: subItem.label,
|
||||
// link: subItem.link,
|
||||
// isSubActive,
|
||||
// hasMoreSpecificMatch,
|
||||
// finalIsActive,
|
||||
// shouldHighlight,
|
||||
// isOnDetailPage,
|
||||
// isUnlistedPage
|
||||
// });
|
||||
// }
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.subItem, shouldHighlight && styles.subItemActive]}
|
||||
onPress={() => {
|
||||
onClose?.();
|
||||
router.push(subItem.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name="radio-button-on-outline"
|
||||
size={16}
|
||||
color={shouldHighlight ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.subText,
|
||||
shouldHighlight && { color: MainColor.yellow },
|
||||
]}
|
||||
>
|
||||
{subItem.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Menu tanpa submenu
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.singleItem, isActive && styles.singleItemActive]}
|
||||
onPress={() => {
|
||||
onClose?.();
|
||||
router.push(item.link as any);
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.icon}
|
||||
size={16}
|
||||
color={isActive ? MainColor.yellow : MainColor.white}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.singleText,
|
||||
{ color: isActive ? MainColor.yellow : MainColor.white },
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// Styles
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 5,
|
||||
},
|
||||
parentItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 8,
|
||||
marginBottom: 5,
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
parentItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
parentText: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
marginLeft: 10,
|
||||
color: MainColor.white,
|
||||
},
|
||||
singleItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 8,
|
||||
marginBottom: 5,
|
||||
},
|
||||
singleItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
singleText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
marginLeft: 10,
|
||||
color: MainColor.white,
|
||||
},
|
||||
icon: {
|
||||
width: 24,
|
||||
textAlign: "center",
|
||||
paddingRight: 10,
|
||||
},
|
||||
submenu: {
|
||||
overflow: "hidden",
|
||||
marginLeft: 30,
|
||||
marginTop: 5,
|
||||
},
|
||||
subItem: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 6,
|
||||
marginBottom: 4,
|
||||
},
|
||||
subItemActive: {
|
||||
backgroundColor: AccentColor.blue,
|
||||
},
|
||||
subText: {
|
||||
color: MainColor.white,
|
||||
fontSize: 16,
|
||||
fontWeight: "500",
|
||||
},
|
||||
});
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
20
components/_ShareComponent/Admin/AdminBasicBox.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import BaseBox from "@/components/Box/BaseBox";
|
||||
import TextCustom from "@/components/Text/TextCustom";
|
||||
import { AccentColor } from "@/constants/color-palet";
|
||||
import { StyleProp, ViewStyle } from "react-native";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export default function AdminBasicBox({ children, onPress, style }: Props) {
|
||||
return (
|
||||
<>
|
||||
<BaseBox onPress={onPress} style={style}>
|
||||
{children}
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import BaseBox from "@/components/Box/BaseBox";
|
||||
import Grid from "@/components/Grid/GridCustom";
|
||||
import TextCustom from "@/components/Text/TextCustom";
|
||||
import { AccentColor } from "@/constants/color-palet";
|
||||
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
|
||||
import { View } from "react-native";
|
||||
|
||||
export default function AdminComp_BoxTitle({
|
||||
title,
|
||||
@@ -12,13 +14,33 @@ export default function AdminComp_BoxTitle({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
{/* <BaseBox
|
||||
style={{ flexDirection: "row", justifyContent: "space-between" }}
|
||||
paddingTop={5}
|
||||
paddingBottom={5}
|
||||
backgroundColor={AccentColor.blue}
|
||||
> */}
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: AccentColor.darkblue,
|
||||
borderColor: AccentColor.blue,
|
||||
paddingBlock: 5,
|
||||
paddingInline: 10,
|
||||
borderWidth: 1,
|
||||
borderRadius: 10,
|
||||
}}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={rightComponent ? 6 : 12} style={{ justifyContent: "center" }}>
|
||||
<Grid
|
||||
// containerStyle={{
|
||||
// bottom: 0,
|
||||
// left: 0,
|
||||
// right: 0,
|
||||
// }}
|
||||
>
|
||||
<Grid.Col
|
||||
span={rightComponent ? 6 : 12}
|
||||
style={{ justifyContent: "center" }}
|
||||
>
|
||||
<TextCustom
|
||||
// style={{ alignSelf: "center" }}
|
||||
bold
|
||||
@@ -39,7 +61,8 @@ export default function AdminComp_BoxTitle({
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
</BaseBox>
|
||||
</View>
|
||||
{/* </BaseBox> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
: 10;
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
16
components/_ShareComponent/BasicWrapper.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { View } from "react-native";
|
||||
|
||||
export default function BasicWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
|
||||
{children}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import Grid from "../Grid/GridCustom";
|
||||
export default function GridTwoView({
|
||||
spanLeft = 6,
|
||||
spanRight = 6,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
leftItem,
|
||||
rightItem,
|
||||
styleLeft,
|
||||
styleRight,
|
||||
}: {
|
||||
spanLeft?: number;
|
||||
spanRight?: number;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftItem?: React.ReactNode;
|
||||
rightItem?: React.ReactNode;
|
||||
styleLeft?: ViewStyle;
|
||||
styleRight?: ViewStyle;
|
||||
}) {
|
||||
@@ -24,13 +24,13 @@ export default function GridTwoView({
|
||||
span={spanLeft}
|
||||
style={styleLeft ? { ...baseStyle, ...styleLeft } : baseStyle}
|
||||
>
|
||||
{leftIcon}
|
||||
{leftItem}
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={spanRight}
|
||||
style={styleRight ? { ...baseStyle, ...styleRight } : baseStyle}
|
||||
>
|
||||
{rightIcon}
|
||||
{rightItem}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
|
||||
right: 0,
|
||||
height: 100,
|
||||
backgroundColor: MainColor.soft_darkblue,
|
||||
borderRadius: 4,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput";
|
||||
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
|
||||
import GridComponentView from "./_ShareComponent/GridSectionView";
|
||||
import NewWrapper from "./_ShareComponent/NewWrapper";
|
||||
import BasicWrapper from "./_ShareComponent/BasicWrapper";
|
||||
// Progress
|
||||
import ProgressCustom from "./Progress/ProgressCustom";
|
||||
// Loader
|
||||
@@ -121,6 +122,7 @@ export {
|
||||
GridComponentView,
|
||||
Spacing,
|
||||
NewWrapper,
|
||||
BasicWrapper,
|
||||
// Stack
|
||||
StackCustom,
|
||||
TabBarBackground,
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
28
docs/CHANGE_LOG.md
Normal file
28
docs/CHANGE_LOG.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# CHANGE LOG - fixed-admin/18-feb-26
|
||||
|
||||
## Perubahan Tampilan Admin
|
||||
|
||||
### File Baru (4)
|
||||
- `screens/Admin/Voting/ScreenVotingStatus.tsx`
|
||||
- `screens/Admin/Voting/ScreenVotingHistory.tsx`
|
||||
- `screens/Admin/Voting/ScreenEventTypeOfEvent.tsx`
|
||||
- `screens/Admin/Voting/BoxVotingStatus.tsx`
|
||||
|
||||
### File Diubah (3)
|
||||
- `app/(application)/admin/voting/[status]/status.tsx` → 5 baris
|
||||
- `app/(application)/admin/voting/history.tsx` → 5 baris
|
||||
- `app/(application)/admin/event/type-of-event.tsx` → 5 baris
|
||||
|
||||
### API Updates (2)
|
||||
- `service/api-admin/api-admin-voting.ts` → tambah param `page`
|
||||
- `service/api-admin/api-master-admin.ts` → tambah param `page`
|
||||
|
||||
## Fitur Baru
|
||||
- Pagination (infinite scroll)
|
||||
- Pull-to-Refresh
|
||||
- Skeleton Loading
|
||||
- Empty State
|
||||
- Search Functionality
|
||||
|
||||
## Stats
|
||||
+305 baris, -531 baris (net: -226)
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user